From 71373e3c19b5045dc369b98e3e9b3be9854c415c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 8 Apr 2026 15:36:08 +1000 Subject: [PATCH] Order line number (#11692) * Add "line number" field for external orders * Updated serializers * Add columns to UI tables * Update form fields * Adds API ordering * Bump API version * Update CHANGELOG.md --- CHANGELOG.md | 1 + .../InvenTree/InvenTree/api_version.py | 7 +- src/backend/InvenTree/order/api.py | 8 +- ...16_purchaseorderextraline_line_and_more.py | 79 +++++++++++++++++++ src/backend/InvenTree/order/models.py | 9 +++ src/backend/InvenTree/order/serializers.py | 2 + src/frontend/src/forms/CommonForms.tsx | 1 + src/frontend/src/forms/PurchaseOrderForms.tsx | 1 + src/frontend/src/forms/ReturnOrderForms.tsx | 3 +- src/frontend/src/forms/SalesOrderForms.tsx | 1 + src/frontend/src/tables/ColumnRenderers.tsx | 10 +++ .../src/tables/general/ExtraLineItemTable.tsx | 3 + .../purchasing/PurchaseOrderLineItemTable.tsx | 5 +- .../tables/sales/ReturnOrderLineItemTable.tsx | 3 + .../tables/sales/SalesOrderLineItemTable.tsx | 3 + 15 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/backend/InvenTree/order/migrations/0116_purchaseorderextraline_line_and_more.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b1d2d483f..2dcdf7ce75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#11692](https://github.com/inventree/InvenTree/pull/11692) adds line item numbering for external orders (purchase, sales and return orders). This allows users to specify a line number for each line item on the order, which can be used for reference purposes. The line number is optional, and can be left blank if not required. The line number is stored as a string, to allow for more flexible formatting (e.g. "1", "1.1", "A", etc). - [#11641](https://github.com/inventree/InvenTree/pull/11641) adds support for custom parameters against the SalesOrderShipment model. - [#11527](https://github.com/inventree/InvenTree/pull/11527) adds a new API endpoint for monitoring the status of a particular background task. This endpoint allows clients to check the status of a background task and receive updates when the task is complete. This is useful for long-running tasks that may take some time to complete, allowing clients to provide feedback to users about the progress of the task. - [#11405](https://github.com/inventree/InvenTree/pull/11405) adds default table filters, which hide inactive items by default. The default table filters are overridden by user filter selection, and only apply to the table view initially presented to the user. This means that users can still view inactive items if they choose to, but they will not be shown by default. diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 71b6477333..bc7f19bf6a 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 472 +INVENTREE_API_VERSION = 473 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v473 -> 2026-04-08 : https://github.com/inventree/InvenTree/pull/11692 + - Adds "line" field to PurchaseOrderLineItem and PurchaseOrderExtraLineItem API endpoints + - Adds "line" field to SalesOrderLineItem and SalesOrderExtraLineItem API endpoints + - Adds "line" field to ReturnOrderLineItem and ReturnOrderExtraLineItem API endpoints + v472 -> 2026-04-01 : https://github.com/inventree/InvenTree/pull/xxxx - Fixes writable fields on the user detail endpoint diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 7cb92230c2..7ad506bc9e 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -82,7 +82,7 @@ class GeneralExtraLineList(SerializerContextMixin, DataExportViewMixin): filter_backends = SEARCH_ORDER_FILTER - ordering_fields = ['quantity', 'notes', 'reference'] + ordering_fields = ['quantity', 'notes', 'reference', 'line'] search_fields = ['quantity', 'notes', 'reference', 'description'] @@ -717,6 +717,7 @@ class PurchaseOrderLineItemList( 'order': 'order__reference', 'status': 'order__status', 'complete_date': 'order__complete_date', + 'line': ['line', 'part__SKU'], } ordering_fields = [ @@ -733,6 +734,7 @@ class PurchaseOrderLineItemList( 'order', 'status', 'complete_date', + 'line', ] search_fields = [ @@ -1067,6 +1069,7 @@ class SalesOrderLineItemList( 'reference', 'sale_price', 'target_date', + 'line', ] ordering_field_aliases = { @@ -1074,6 +1077,7 @@ class SalesOrderLineItemList( 'part': 'part__name', 'IPN': 'part__IPN', 'order': 'order__reference', + 'line': ['line', 'part__name'], } search_fields = ['part__name', 'quantity', 'reference'] @@ -1720,9 +1724,11 @@ class ReturnOrderLineItemList( 'reference', 'target_date', 'received_date', + 'line', ] ordering_field_aliases = { + 'line': ['line', 'item__part__name'], 'part': 'item__part__name', 'IPN': 'item__part__IPN', 'stock': ['item__quantity', 'item__serial_int', 'item__serial'], diff --git a/src/backend/InvenTree/order/migrations/0116_purchaseorderextraline_line_and_more.py b/src/backend/InvenTree/order/migrations/0116_purchaseorderextraline_line_and_more.py new file mode 100644 index 0000000000..740c9382e2 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0116_purchaseorderextraline_line_and_more.py @@ -0,0 +1,79 @@ +# Generated by Django 5.2.12 on 2026-04-08 03:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("order", "0115_purchaseorder_updated_at_returnorder_updated_at_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="purchaseorderextraline", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + migrations.AddField( + model_name="purchaseorderlineitem", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + migrations.AddField( + model_name="returnorderextraline", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + migrations.AddField( + model_name="returnorderlineitem", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + migrations.AddField( + model_name="salesorderextraline", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + migrations.AddField( + model_name="salesorderlineitem", + name="line", + field=models.CharField( + blank=True, + default="", + help_text="Line number for this item (optional)", + max_length=20, + verbose_name="Line Number", + ), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index 2d7a550aea..fc63b6cbb0 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -1793,6 +1793,15 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel): if self.price: return self.quantity * self.price + line = models.CharField( + max_length=20, + blank=True, + default='', + null=False, + verbose_name=_('Line Number'), + help_text=_('Line number for this item (optional)'), + ) + reference = models.CharField( max_length=100, blank=True, diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index e761fdba8c..f79e6d076b 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -276,6 +276,7 @@ class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializ """Construct a set of fields for this serializer.""" return [ 'pk', + 'line', 'link', 'notes', 'order', @@ -309,6 +310,7 @@ class AbstractExtraLineSerializer( """Construct a set of fields for this serializer.""" return [ 'pk', + 'line', 'description', 'link', 'notes', diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index 04d1da4ed9..530703959b 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -83,6 +83,7 @@ export function extraLineItemFields(): ApiFormFieldSet { order: { hidden: true }, + line: {}, reference: {}, description: {}, quantity: {}, diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 65e2ebc34e..0f87fd5673 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -144,6 +144,7 @@ export function usePurchaseOrderLineItemFields({ }; } }, + line: {}, reference: {}, quantity: { onValueChange: (value) => { diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index ca4e5a51f6..2f5be88aa8 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -128,8 +128,9 @@ export function useReturnOrderLineItemFields({ part_detail: true } }, - quantity: {}, + line: {}, reference: {}, + quantity: {}, outcome: { hidden: create == true }, diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index c4a7f2401b..5239a6101e 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -169,6 +169,7 @@ export function useSalesOrderLineItemFields({ }, onValueChange: (_: any, record?: any) => setPart(record) }, + line: {}, reference: {}, quantity: { onValueChange: (value) => { diff --git a/src/frontend/src/tables/ColumnRenderers.tsx b/src/frontend/src/tables/ColumnRenderers.tsx index c31b1684ab..21c3a1caa0 100644 --- a/src/frontend/src/tables/ColumnRenderers.tsx +++ b/src/frontend/src/tables/ColumnRenderers.tsx @@ -767,3 +767,13 @@ export function TotalPriceColumn(): TableColumn { title: t`Total Price` }); } + +export function LineItemColumn(props: TableColumnProps): TableColumn { + return { + accessor: 'line', + title: t`Line Item`, + sortable: true, + switchable: true, + ...props + }; +} diff --git a/src/frontend/src/tables/general/ExtraLineItemTable.tsx b/src/frontend/src/tables/general/ExtraLineItemTable.tsx index 909cd4a76b..299a7b07ef 100644 --- a/src/frontend/src/tables/general/ExtraLineItemTable.tsx +++ b/src/frontend/src/tables/general/ExtraLineItemTable.tsx @@ -24,6 +24,7 @@ import { useUserState } from '../../states/UserState'; import { DecimalColumn, DescriptionColumn, + LineItemColumn, LinkColumn, NoteColumn, ProjectCodeColumn @@ -50,6 +51,7 @@ export default function ExtraLineItemTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + LineItemColumn({}), { accessor: 'reference', switchable: false @@ -177,6 +179,7 @@ export default function ExtraLineItemTable({ params: { order: orderId }, + defaultSortColumn: 'line', rowActions: rowActions, tableActions: tableActions }} diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index e529f93c34..b3dc66dd34 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -36,11 +36,11 @@ import { import useStatusCodes from '../../hooks/UseStatusCodes'; import { useTable } from '../../hooks/UseTable'; import { useImporterState } from '../../states/ImporterState'; -import { useGlobalSettingsState } from '../../states/SettingsStates'; import { useUserState } from '../../states/UserState'; import { CurrencyColumn, DescriptionColumn, + LineItemColumn, LinkColumn, LocationColumn, NoteColumn, @@ -74,7 +74,6 @@ export function PurchaseOrderLineItemTable({ }>) { const table = useTable('purchase-order-line-item'); - const globalSettings = useGlobalSettingsState(); const navigate = useNavigate(); const user = useUserState(); const openImporter = useImporterState((state) => state.openImporter); @@ -134,6 +133,7 @@ export function PurchaseOrderLineItemTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + LineItemColumn({}), PartColumn({ part: 'part_detail', ordering: 'part_name' @@ -435,6 +435,7 @@ export function PurchaseOrderLineItemTable({ props={{ enableSelection: true, enableDownload: true, + defaultSortColumn: 'line', params: { ...params, order: orderId, diff --git a/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx index 456afb5afc..6189338798 100644 --- a/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx @@ -31,6 +31,7 @@ import { useUserState } from '../../states/UserState'; import { DateColumn, DescriptionColumn, + LineItemColumn, LinkColumn, NoteColumn, PartColumn, @@ -110,6 +111,7 @@ export default function ReturnOrderLineItemTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + LineItemColumn({}), PartColumn({ part: 'part_detail', ordering: 'part' @@ -267,6 +269,7 @@ export default function ReturnOrderLineItemTable({ item_detail: true, order_detail: true }, + defaultSortColumn: 'line', enableSelection: inProgress && user.hasChangeRole(UserRoles.return_order), tableActions: tableActions, diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx index 40f1956477..115f7f59b9 100644 --- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx @@ -48,6 +48,7 @@ import { DecimalColumn, DescriptionColumn, IPNColumn, + LineItemColumn, LinkColumn, ProjectCodeColumn, ReferenceColumn, @@ -77,6 +78,7 @@ export default function SalesOrderLineItemTable({ const tableColumns: TableColumn[] = useMemo(() => { return [ + LineItemColumn({}), { accessor: 'part', sortable: true, @@ -539,6 +541,7 @@ export default function SalesOrderLineItemTable({ order: orderId, part_detail: true }, + defaultSortColumn: 'line', rowActions: rowActions, tableActions: tableActions, tableFilters: tableFilters,