diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e93954c8c..2cb9feb2e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Adds configurable default for merging purchase order line items via the `PURCHASEORDER_MERGE_LINE_ITEMS` global setting - [#12393](https://github.com/inventree/InvenTree/pull/12393) adds "discount" attribute to order line items, allowing users to specify a discount for each line item on an order. The discount can be specified as either a percentage or a fixed amount, and is applied to the line item total when calculating the order total. - [#12391](https://github.com/inventree/InvenTree/pull/12391) adds facility for bulk deleting line items against orders - [#12388](https://github.com/inventree/InvenTree/pull/12388) adds uniqueness requirements options for the Parameter and ParameterTemplate models. This allows users to specify whether a parameter value should be unique for a given model type, or globally unique across all models. diff --git a/docs/docs/purchasing/purchase_order.md b/docs/docs/purchasing/purchase_order.md index 368377e3b9..e4d6783ed8 100644 --- a/docs/docs/purchasing/purchase_order.md +++ b/docs/docs/purchasing/purchase_order.md @@ -225,3 +225,4 @@ The following [global settings](../settings/global.md) are available for purchas {{ globalsetting("PURCHASEORDER_CONVERT_CURRENCY") }} {{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }} {{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }} +{{ globalsetting("PURCHASEORDER_MERGE_LINE_ITEMS") }} diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index 8b28f93a5a..abdfd55094 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -1003,6 +1003,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = { 'default': True, 'validator': bool, }, + 'PURCHASEORDER_MERGE_LINE_ITEMS': { + 'name': _('Merge Purchase Order Line Items'), + 'description': _( + 'Merge new purchase order line items with existing lines that share the same part, destination, and target date' + ), + 'default': True, + 'validator': bool, + }, # login / SSO 'LOGIN_ENABLE_PWD_FORGOT': { 'name': _('Enable password forgot'), diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index 057838c8ed..d6b66ae4c4 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -684,7 +684,14 @@ class PurchaseOrderLineItemList( # possibly merge duplicate items line_item = None - if data.get('merge_items', True): + merge_items = data.get( + 'merge_items', + common.settings.get_global_setting( + 'PURCHASEORDER_MERGE_LINE_ITEMS', backup_value=True + ), + ) + + if merge_items: with transaction.atomic(): # Lock the matching row, so concurrent line creations cannot # both read the same starting quantity (lost update) diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index f434cf87c0..548323dff5 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -745,6 +745,14 @@ class PurchaseOrderLineItemSerializer( write_only=True, ) + def __init__(self, *args, **kwargs): + """Set dynamic defaults for create-only fields.""" + super().__init__(*args, **kwargs) + + self.fields['merge_items'].default = get_global_setting( + 'PURCHASEORDER_MERGE_LINE_ITEMS', backup_value=True + ) + sku = serializers.CharField( source='part.SKU', read_only=True, allow_null=True, label=_('SKU') ) diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 28d42ea984..7153c5ad77 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -1060,6 +1060,42 @@ class PurchaseOrderLineItemTest(OrderTest): ).json() self.assertEqual(float(li5['purchase_price']), 1) + def test_po_line_merge_default_setting(self): + """Test that merge_items defaults to the global setting value.""" + self.assignRole('purchase_order.add') + + su = Company.objects.get(pk=1) + sp = SupplierPart.objects.get(pk=1) + po = models.PurchaseOrder.objects.create( + supplier=su, reference='PO-MERGE-DEFAULT' + ) + + set_global_setting('PURCHASEORDER_MERGE_LINE_ITEMS', False) + + li1 = self.post( + reverse('api-po-line-list'), + {'order': po.pk, 'part': sp.pk, 'quantity': 1}, + expected_code=201, + ).json() + + li2 = self.post( + reverse('api-po-line-list'), + {'order': po.pk, 'part': sp.pk, 'quantity': 2}, + expected_code=201, + ).json() + + self.assertNotEqual(li1['pk'], li2['pk']) + + set_global_setting('PURCHASEORDER_MERGE_LINE_ITEMS', True) + + li3 = self.post( + reverse('api-po-line-list'), + {'order': po.pk, 'part': sp.pk, 'quantity': 3}, + expected_code=201, + ).json() + + self.assertEqual(li1['pk'], li3['pk']) + def test_output_options(self): """Test PurchaseOrderLineItem output option endpoint.""" self.run_output_test( diff --git a/src/frontend/src/components/wizards/OrderPartsWizard.tsx b/src/frontend/src/components/wizards/OrderPartsWizard.tsx index 0b2dd365b9..990d2cef19 100644 --- a/src/frontend/src/components/wizards/OrderPartsWizard.tsx +++ b/src/frontend/src/components/wizards/OrderPartsWizard.tsx @@ -32,6 +32,7 @@ import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms'; import { useCreateApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; import useWizard from '../../hooks/UseWizard'; +import { useGlobalSettingsState } from '../../states/SettingsStates'; import RemoveRowButton from '../buttons/RemoveRowButton'; import { StandaloneField } from '../forms/StandaloneField'; import Expand from '../items/Expand'; @@ -195,6 +196,7 @@ function SelectPartsStep({ const [selectedRecord, setSelectedRecord] = useState( null ); + const globalSettings = useGlobalSettingsState(); const purchaseOrderFields = usePurchaseOrderFields({ supplierId: selectedRecord?.supplier_part?.supplier @@ -240,9 +242,11 @@ function SelectPartsStep({ }, purchase_price: {}, purchase_price_currency: {}, - merge_items: {} + merge_items: { + default: globalSettings.isSet('PURCHASEORDER_MERGE_LINE_ITEMS', true) + } }; - }, [selectedRecord]); + }, [selectedRecord, globalSettings]); const addToOrder = useCreateApiFormModal({ url: apiUrl(ApiEndpoints.purchase_order_line_list), diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index cdf37160df..8a71696186 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -220,7 +220,9 @@ export function usePurchaseOrderLineItemFields({ } if (create) { - fields['merge_items'] = {}; + fields['merge_items'] = { + default: globalSettings.isSet('PURCHASEORDER_MERGE_LINE_ITEMS', true) + }; } return fields;