Add configurable default for PO line item merging (#12472)

* Add configurable default for PO line item merging

Introduce PURCHASEORDER_MERGE_LINE_ITEMS global setting to control the
default state of the merge_items option when creating purchase order lines.

Fixes #10947

* Apply ruff and biome formatting for CI
This commit is contained in:
Senior Data Engineer
2026-07-27 11:10:11 +10:00
committed by GitHub
parent b5abf47115
commit e43390badb
8 changed files with 71 additions and 4 deletions
+1
View File
@@ -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.
+1
View File
@@ -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") }}
@@ -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'),
+8 -1
View File
@@ -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)
@@ -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')
)
+36
View File
@@ -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(
@@ -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<PartOrderRecord | null>(
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),
@@ -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;