mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-28 01:17:21 +00:00
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:
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### 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.
|
- [#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
|
- [#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.
|
- [#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.
|
||||||
|
|||||||
@@ -225,3 +225,4 @@ The following [global settings](../settings/global.md) are available for purchas
|
|||||||
{{ globalsetting("PURCHASEORDER_CONVERT_CURRENCY") }}
|
{{ globalsetting("PURCHASEORDER_CONVERT_CURRENCY") }}
|
||||||
{{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }}
|
{{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }}
|
||||||
{{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }}
|
{{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }}
|
||||||
|
{{ globalsetting("PURCHASEORDER_MERGE_LINE_ITEMS") }}
|
||||||
|
|||||||
@@ -1003,6 +1003,14 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
|||||||
'default': True,
|
'default': True,
|
||||||
'validator': bool,
|
'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 / SSO
|
||||||
'LOGIN_ENABLE_PWD_FORGOT': {
|
'LOGIN_ENABLE_PWD_FORGOT': {
|
||||||
'name': _('Enable password forgot'),
|
'name': _('Enable password forgot'),
|
||||||
|
|||||||
@@ -684,7 +684,14 @@ class PurchaseOrderLineItemList(
|
|||||||
|
|
||||||
# possibly merge duplicate items
|
# possibly merge duplicate items
|
||||||
line_item = None
|
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():
|
with transaction.atomic():
|
||||||
# Lock the matching row, so concurrent line creations cannot
|
# Lock the matching row, so concurrent line creations cannot
|
||||||
# both read the same starting quantity (lost update)
|
# both read the same starting quantity (lost update)
|
||||||
|
|||||||
@@ -745,6 +745,14 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
write_only=True,
|
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(
|
sku = serializers.CharField(
|
||||||
source='part.SKU', read_only=True, allow_null=True, label=_('SKU')
|
source='part.SKU', read_only=True, allow_null=True, label=_('SKU')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1060,6 +1060,42 @@ class PurchaseOrderLineItemTest(OrderTest):
|
|||||||
).json()
|
).json()
|
||||||
self.assertEqual(float(li5['purchase_price']), 1)
|
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):
|
def test_output_options(self):
|
||||||
"""Test PurchaseOrderLineItem output option endpoint."""
|
"""Test PurchaseOrderLineItem output option endpoint."""
|
||||||
self.run_output_test(
|
self.run_output_test(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { usePurchaseOrderFields } from '../../forms/PurchaseOrderForms';
|
|||||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||||
import { useInstance } from '../../hooks/UseInstance';
|
import { useInstance } from '../../hooks/UseInstance';
|
||||||
import useWizard from '../../hooks/UseWizard';
|
import useWizard from '../../hooks/UseWizard';
|
||||||
|
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||||
import RemoveRowButton from '../buttons/RemoveRowButton';
|
import RemoveRowButton from '../buttons/RemoveRowButton';
|
||||||
import { StandaloneField } from '../forms/StandaloneField';
|
import { StandaloneField } from '../forms/StandaloneField';
|
||||||
import Expand from '../items/Expand';
|
import Expand from '../items/Expand';
|
||||||
@@ -195,6 +196,7 @@ function SelectPartsStep({
|
|||||||
const [selectedRecord, setSelectedRecord] = useState<PartOrderRecord | null>(
|
const [selectedRecord, setSelectedRecord] = useState<PartOrderRecord | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const globalSettings = useGlobalSettingsState();
|
||||||
|
|
||||||
const purchaseOrderFields = usePurchaseOrderFields({
|
const purchaseOrderFields = usePurchaseOrderFields({
|
||||||
supplierId: selectedRecord?.supplier_part?.supplier
|
supplierId: selectedRecord?.supplier_part?.supplier
|
||||||
@@ -240,9 +242,11 @@ function SelectPartsStep({
|
|||||||
},
|
},
|
||||||
purchase_price: {},
|
purchase_price: {},
|
||||||
purchase_price_currency: {},
|
purchase_price_currency: {},
|
||||||
merge_items: {}
|
merge_items: {
|
||||||
|
default: globalSettings.isSet('PURCHASEORDER_MERGE_LINE_ITEMS', true)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [selectedRecord]);
|
}, [selectedRecord, globalSettings]);
|
||||||
|
|
||||||
const addToOrder = useCreateApiFormModal({
|
const addToOrder = useCreateApiFormModal({
|
||||||
url: apiUrl(ApiEndpoints.purchase_order_line_list),
|
url: apiUrl(ApiEndpoints.purchase_order_line_list),
|
||||||
|
|||||||
@@ -220,7 +220,9 @@ export function usePurchaseOrderLineItemFields({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (create) {
|
if (create) {
|
||||||
fields['merge_items'] = {};
|
fields['merge_items'] = {
|
||||||
|
default: globalSettings.isSet('PURCHASEORDER_MERGE_LINE_ITEMS', true)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
|
|||||||
Reference in New Issue
Block a user