diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 40e3951dbcf5..946c2c7c8e0c 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 269 +INVENTREE_API_VERSION = 270 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v270 - 2024-10-19 : https://github.com/inventree/InvenTree/pull/8307 + - Adds missing date fields from order API endpoint(s) + v269 - 2024-10-16 : https://github.com/inventree/InvenTree/pull/8295 - Adds "include_variants" filter to the BuildOrder API endpoint - Adds "include_variants" filter to the SalesOrder API endpoint diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 1fb5c9f3de85..abeb1229c6ed 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -6,9 +6,8 @@ from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User -from rest_framework.exceptions import ValidationError - from django_filters import rest_framework as rest_filters +from rest_framework.exceptions import ValidationError from importer.mixins import DataExportViewMixin @@ -149,7 +148,7 @@ def filter_issued_by(self, queryset, name, owner): def filter_responsible(self, queryset, name, owner): """Filter by orders which are assigned to the specified owner.""" - owners = list(Owner.objects.filter(pk=value)) + owners = list(Owner.objects.filter(pk=owner)) # if we query by a user, also find all ownerships through group memberships if len(owners) > 0 and owners[0].label() == 'user': diff --git a/src/backend/InvenTree/build/status_codes.py b/src/backend/InvenTree/build/status_codes.py index bc7fc47ddc9b..1c8e66de3004 100644 --- a/src/backend/InvenTree/build/status_codes.py +++ b/src/backend/InvenTree/build/status_codes.py @@ -23,3 +23,7 @@ class BuildStatusGroups: BuildStatus.ON_HOLD.value, BuildStatus.PRODUCTION.value, ] + + COMPLETE = [ + BuildStatus.COMPLETE.value, + ] diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 2e9b0ae30b48..ac36216abade 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -1773,6 +1773,8 @@ class Meta: model = order.models.ReturnOrder fields = AbstractOrderSerializer.order_fields([ + 'issue_date', + 'complete_date', 'customer', 'customer_detail', 'customer_reference', diff --git a/src/backend/InvenTree/order/status_codes.py b/src/backend/InvenTree/order/status_codes.py index 3dcbee01f518..fa0d4594b7a2 100644 --- a/src/backend/InvenTree/order/status_codes.py +++ b/src/backend/InvenTree/order/status_codes.py @@ -35,6 +35,8 @@ class PurchaseOrderStatusGroups: PurchaseOrderStatus.RETURNED.value, ] + COMPLETE = [PurchaseOrderStatus.COMPLETE.value] + class SalesOrderStatus(StatusCode): """Defines a set of status codes for a SalesOrder.""" @@ -91,6 +93,8 @@ class ReturnOrderStatusGroups: ReturnOrderStatus.IN_PROGRESS.value, ] + COMPLETE = [ReturnOrderStatus.COMPLETE.value] + class ReturnOrderLineStatus(StatusCode): """Defines a set of status codes for a ReturnOrderLineItem.""" diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index 9ee10250f64b..edd7f5a33030 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -81,6 +81,7 @@ export enum ApiEndpoints { build_order_auto_allocate = 'build/:id/auto-allocate/', build_order_allocate = 'build/:id/allocate/', build_order_deallocate = 'build/:id/unallocate/', + build_line_list = 'build/line/', build_item_list = 'build/item/', @@ -160,11 +161,12 @@ export enum ApiEndpoints { sales_order_cancel = 'order/so/:id/cancel/', sales_order_ship = 'order/so/:id/ship/', sales_order_complete = 'order/so/:id/complete/', + sales_order_allocate = 'order/so/:id/allocate/', + sales_order_allocate_serials = 'order/so/:id/allocate-serials/', + sales_order_line_list = 'order/so-line/', sales_order_extra_line_list = 'order/so-extra-line/', sales_order_allocation_list = 'order/so-allocation/', - sales_order_allocate = 'order/so/:id/allocate/', - sales_order_allocate_serials = 'order/so/:id/allocate-serials/', sales_order_shipment_list = 'order/so/shipment/', sales_order_shipment_complete = 'order/so/shipment/:id/ship/', diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 38f64de3be6e..c5f1adc03343 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -13,8 +13,11 @@ import { IconBuildingStore, IconBusinessplan, IconCalendar, + IconCalendarCheck, + IconCalendarDot, IconCalendarStats, IconCalendarTime, + IconCalendarX, IconCheck, IconCircleCheck, IconCircleMinus, @@ -179,8 +182,15 @@ const icons = { locked: IconLock, calendar: IconCalendar, + calendar_target: IconCalendarDot, + calendar_cross: IconCalendarX, + calendar_check: IconCalendarCheck, external: IconExternalLink, creation_date: IconCalendarTime, + target_date: IconCalendarDot, + date: IconCalendar, + shipment_date: IconCalendarCheck, + complete_date: IconCalendarCheck, location: IconMapPin, default_location: IconMapPinHeart, category_default_location: IconMapPinHeart, diff --git a/src/frontend/src/pages/part/PartAllocationPanel.tsx b/src/frontend/src/pages/part/PartAllocationPanel.tsx new file mode 100644 index 000000000000..e64a8a999ab0 --- /dev/null +++ b/src/frontend/src/pages/part/PartAllocationPanel.tsx @@ -0,0 +1,55 @@ +import { t } from '@lingui/macro'; +import { Accordion } from '@mantine/core'; + +import { StylishText } from '../../components/items/StylishText'; +import { ModelType } from '../../enums/ModelType'; +import { UserRoles } from '../../enums/Roles'; +import { useUserState } from '../../states/UserState'; +import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; +import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; + +export default function PartAllocationPanel({ part }: { part: any }) { + const user = useUserState(); + + return ( + <> + + {part.component && user.hasViewRole(UserRoles.build) && ( + + + {t`Build Order Allocations`} + + + + + + )} + {part.salable && user.hasViewRole(UserRoles.sales_order) && ( + + + {t`Sales Order Allocations`} + + + + + + )} + + + ); +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index cd6a1b30da24..2d6d4ecef47f 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,6 +1,5 @@ import { t } from '@lingui/macro'; import { - Accordion, Alert, Center, Grid, @@ -54,7 +53,6 @@ import { EditItemAction, OptionsActionDropdown } from '../../components/items/ActionDropdown'; -import { StylishText } from '../../components/items/StylishText'; import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; @@ -89,7 +87,6 @@ import { import { useUserState } from '../../states/UserState'; import { BomTable } from '../../tables/bom/BomTable'; import { UsedInTable } from '../../tables/bom/UsedInTable'; -import BuildAllocatedStockTable from '../../tables/build/BuildAllocatedStockTable'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { PartParameterTable } from '../../tables/part/PartParameterTable'; import PartPurchaseOrdersTable from '../../tables/part/PartPurchaseOrdersTable'; @@ -99,10 +96,10 @@ import { RelatedPartTable } from '../../tables/part/RelatedPartTable'; import { ManufacturerPartTable } from '../../tables/purchasing/ManufacturerPartTable'; import { SupplierPartTable } from '../../tables/purchasing/SupplierPartTable'; import { ReturnOrderTable } from '../../tables/sales/ReturnOrderTable'; -import SalesOrderAllocationTable from '../../tables/sales/SalesOrderAllocationTable'; import { SalesOrderTable } from '../../tables/sales/SalesOrderTable'; import { StockItemTable } from '../../tables/stock/StockItemTable'; import { TestStatisticsTable } from '../../tables/stock/TestStatisticsTable'; +import PartAllocationPanel from './PartAllocationPanel'; import PartPricingPanel from './PartPricingPanel'; import PartSchedulingDetail from './PartSchedulingDetail'; import PartStocktakeDetail from './PartStocktakeDetail'; @@ -351,7 +348,7 @@ export default function PartDetail() { }, { type: 'boolean', - name: 'saleable', + name: 'salable', label: t`Saleable Part` }, { @@ -591,45 +588,7 @@ export default function PartDetail() { label: t`Allocations`, icon: , hidden: !part.component && !part.salable, - content: ( - - {part.component && ( - - - {t`Build Order Allocations`} - - - - - - )} - {part.salable && ( - - - {t`Sales Order Allocations`} - - - - - - )} - - ) + content: part.pk ? : }, { name: 'bom', @@ -644,8 +603,8 @@ export default function PartDetail() { name: 'builds', label: t`Build Orders`, icon: , - hidden: !part.assembly || !part.active, - content: part?.pk ? : + hidden: !part.assembly || !user.hasViewRole(UserRoles.build), + content: part.pk ? : }, { name: 'used_in', @@ -677,7 +636,8 @@ export default function PartDetail() { name: 'suppliers', label: t`Suppliers`, icon: , - hidden: !part.purchaseable, + hidden: + !part.purchaseable || !user.hasViewRole(UserRoles.purchase_order), content: part.pk && ( , - hidden: !part.purchaseable, - content: + hidden: + !part.purchaseable || !user.hasViewRole(UserRoles.purchase_order), + content: part.pk ? ( + + ) : ( + + ) }, { name: 'sales_orders', label: t`Sales Orders`, icon: , - hidden: !part.salable, + hidden: !part.salable || !user.hasViewRole(UserRoles.sales_order), content: part.pk ? : }, { name: 'return_orders', label: t`Return Orders`, icon: , - hidden: !part.salable || !globalSettings.isSet('RETURNORDER_ENABLED'), + hidden: + !part.salable || + !user.hasViewRole(UserRoles.return_order) || + !globalSettings.isSet('RETURNORDER_ENABLED'), content: part.pk ? : }, { diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index 60d5540585e6..b7631b984662 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -196,18 +196,34 @@ export default function PurchaseOrderDetail() { let br: DetailsField[] = [ { - type: 'text', + type: 'date', name: 'creation_date', - label: t`Created On`, + label: t`Creation Date`, icon: 'calendar' }, { - type: 'text', + type: 'date', + name: 'issue_date', + label: t`Issue Date`, + icon: 'calendar', + copy: true, + hidden: !order.issue_date + }, + { + type: 'date', name: 'target_date', label: t`Target Date`, icon: 'calendar', hidden: !order.target_date }, + { + type: 'date', + name: 'complete_date', + icon: 'calendar_check', + label: t`Completion Date`, + copy: true, + hidden: !order.complete_date + }, { type: 'text', name: 'responsible', diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 0f0ed61daffc..3fd7bf5765bb 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -173,18 +173,36 @@ export default function ReturnOrderDetail() { let br: DetailsField[] = [ { - type: 'text', + type: 'date', name: 'creation_date', - label: t`Created On`, - icon: 'calendar' + label: t`Creation Date`, + icon: 'calendar', + copy: true, + hidden: !order.creation_date }, { - type: 'text', + type: 'date', + name: 'issue_date', + label: t`Issue Date`, + icon: 'calendar', + copy: true, + hidden: !order.issue_date + }, + { + type: 'date', name: 'target_date', label: t`Target Date`, - icon: 'calendar', + copy: true, hidden: !order.target_date }, + { + type: 'date', + name: 'complete_date', + icon: 'calendar_check', + label: t`Completion Date`, + copy: true, + hidden: !order.complete_date + }, { type: 'text', name: 'responsible', diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 6a49f2838038..7e5f94dc0fae 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -100,6 +100,7 @@ export default function SalesOrderDetail() { name: 'customer_reference', label: t`Customer Reference`, copy: true, + icon: 'reference', hidden: !order.customer_reference }, { @@ -184,17 +185,33 @@ export default function SalesOrderDetail() { let br: DetailsField[] = [ { - type: 'text', + type: 'date', name: 'creation_date', - label: t`Created On`, - icon: 'calendar' + label: t`Creation Date`, + copy: true, + hidden: !order.creation_date }, { - type: 'text', + type: 'date', + name: 'issue_date', + label: t`Issue Date`, + icon: 'calendar', + copy: true, + hidden: !order.issue_date + }, + { + type: 'date', name: 'target_date', label: t`Target Date`, - icon: 'calendar', - hidden: !order.target_date + hidden: !order.target_date, + copy: true + }, + { + type: 'date', + name: 'shipment_date', + label: t`Completion Date`, + hidden: !order.shipment_date, + copy: true }, { type: 'text', diff --git a/src/frontend/src/states/UserState.tsx b/src/frontend/src/states/UserState.tsx index 6678e9dba0ad..c2b34b84f274 100644 --- a/src/frontend/src/states/UserState.tsx +++ b/src/frontend/src/states/UserState.tsx @@ -67,6 +67,15 @@ export const useUserState = create((set, get) => ({ setApiDefaults(); }, fetchUserToken: async () => { + // If neither the csrf or session cookies are available, we cannot fetch a token + if ( + !document.cookie.includes('csrftoken') && + !document.cookie.includes('sessionid') + ) { + get().clearToken(); + return; + } + await api .get(apiUrl(ApiEndpoints.user_token)) .then((response) => { @@ -85,6 +94,12 @@ export const useUserState = create((set, get) => ({ await get().fetchUserToken(); } + // If we still don't have a token, clear the user state and return + if (!get().token) { + get().clearUserState(); + return; + } + // Fetch user data await api .get(apiUrl(ApiEndpoints.user_me), { diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 03004d5428e5..c293247ab748 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -95,6 +95,7 @@ export function BuildOrderTable({ TargetDateColumn({}), DateColumn({ accessor: 'completion_date', + title: t`Completion Date`, sortable: true }), { diff --git a/src/frontend/src/tables/company/CompanyTable.tsx b/src/frontend/src/tables/company/CompanyTable.tsx index eabdf1a18eb4..0e14bbf6f914 100644 --- a/src/frontend/src/tables/company/CompanyTable.tsx +++ b/src/frontend/src/tables/company/CompanyTable.tsx @@ -9,6 +9,7 @@ import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; import { companyFields } from '../../forms/CompanyForms'; +import { navigateToLink } from '../../functions/navigation'; import { useCreateApiFormModal, useEditApiFormModal @@ -157,16 +158,17 @@ export function CompanyTable({ params: { ...params }, + onRowClick: (record: any, index: number, event: any) => { + if (record.pk) { + let base = path ?? 'company'; + navigateToLink(`/${base}/${record.pk}`, navigate, event); + } + }, + modelType: ModelType.company, tableFilters: tableFilters, tableActions: tableActions, enableDownload: true, - rowActions: rowActions, - onRowClick: (row: any) => { - if (row.pk) { - let base = path ?? 'company'; - navigate(`/${base}/${row.pk}`); - } - } + rowActions: rowActions }} /> diff --git a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx index ed50428c5277..d9878bbdeb5a 100644 --- a/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx +++ b/src/frontend/src/tables/part/PartPurchaseOrdersTable.tsx @@ -92,6 +92,10 @@ export default function PartPurchaseOrdersTable({ ); } }, + DateColumn({ + accessor: 'order_detail.complete_date', + title: t`Order Completed Date` + }), DateColumn({ accessor: 'target_date', title: t`Target Date` diff --git a/src/frontend/src/tables/sales/ReturnOrderTable.tsx b/src/frontend/src/tables/sales/ReturnOrderTable.tsx index 906f2569b729..f399c5b26298 100644 --- a/src/frontend/src/tables/sales/ReturnOrderTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderTable.tsx @@ -15,6 +15,7 @@ import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { CreationDateColumn, + DateColumn, DescriptionColumn, LineItemsProgressColumn, ProjectCodeColumn, @@ -116,6 +117,10 @@ export function ReturnOrderTable({ ProjectCodeColumn({}), CreationDateColumn({}), TargetDateColumn({}), + DateColumn({ + accessor: 'complete_date', + title: t`Completion Date` + }), ResponsibleColumn({}), { accessor: 'total_price', diff --git a/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx index a2d38b8b7fba..14a4465708d3 100644 --- a/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderAllocationTable.tsx @@ -16,6 +16,7 @@ import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; import { + DateColumn, LocationColumn, PartColumn, ReferenceColumn, @@ -136,6 +137,12 @@ export default function SalesOrderAllocationTable({ switchable: true, sortable: false }, + DateColumn({ + accessor: 'shipment_detail.shipment_date', + title: t`Shipment Date`, + switchable: true, + sortable: false + }), { accessor: 'shipment_date', title: t`Shipped`, diff --git a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx index 72db46588fd9..b2c09f7c42af 100644 --- a/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderShipmentTable.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { AddItemButton } from '../../components/buttons/AddItemButton'; +import { YesNoButton } from '../../components/buttons/YesNoButton'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; import { UserRoles } from '../../enums/Roles'; @@ -23,7 +24,12 @@ import { useTable } from '../../hooks/UseTable'; import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { TableColumn } from '../Column'; -import { DateColumn, LinkColumn, NoteColumn } from '../ColumnRenderers'; +import { + BooleanColumn, + DateColumn, + LinkColumn, + NoteColumn +} from '../ColumnRenderers'; import { TableFilter } from '../Filter'; import { InvenTreeTable } from '../InvenTreeTable'; import { RowAction, RowCancelAction, RowEditAction } from '../RowActions'; @@ -97,6 +103,13 @@ export default function SalesOrderShipmentTable({ switchable: false, title: t`Items` }, + { + accessor: 'shipped', + title: t`Shipped`, + switchable: true, + sortable: false, + render: (record: any) => + }, DateColumn({ accessor: 'shipment_date', title: t`Shipment Date`