mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-27 14:16:02 +00:00
Default receive location (#12911)
* Add global setting * docs * Dynamically set API field default * UI elements * regression testing * Add CHANGELOG entry * Fix unit test
This commit is contained in:
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- [#12713](https://github.com/inventree/InvenTree/pull/12713) adds SCIM 2 provisioning support, allowing InvenTree to be integrated with external identity providers for user management.
|
||||
- [#12731](https://github.com/inventree/InvenTree/pull/12731) adds OIDC provider settings to the Admin Center - making all Identity Federation settings now available in one place without the need to use the database admin interface.
|
||||
- [#12837](https://github.com/inventree/InvenTree/pull/12837) adds a user setting `ROTATE_TABLE_HEADERS` which rotates table headers by 90 degrees, improving readability for tables with long column titles.
|
||||
- [#12911](https://github.com/inventree/InvenTree/pull/12911) adds global setting for default receive location against purchase orders
|
||||
- [#12914](https://github.com/inventree/InvenTree/pull/12914) adds the AllocateMixin, allowing plugins to customize automatic stock allocation for build orders and sales orders.
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -109,7 +109,7 @@ There are two options to mark items as "received":
|
||||
|
||||
When receiving items from a purchase order, the location of the items must be specified. There are multiple ways to specify the location:
|
||||
|
||||
* **Order Destination**: The *destination* field of the purchase order can be set to a specific location. When receiving items, the location will default to the destination location.
|
||||
* **Order Destination**: The *destination* field of the purchase order can be set to a specific location. When receiving items, the location will default to the destination location. If the [Default Receive Location](#purchase-order-settings) setting is configured, the *destination* field will be pre-filled with this location when creating a new purchase order.
|
||||
|
||||
* **Line Item Location**: Each line item can have a specific location set. When receiving items, the location will default to the line item location. *Note: A destination specified at the line item level will override the destination specified at the order level.*
|
||||
|
||||
@@ -213,3 +213,4 @@ The following [global settings](../settings/global.md) are available for purchas
|
||||
{{ globalsetting("PURCHASEORDER_EDIT_COMPLETED_ORDERS") }}
|
||||
{{ globalsetting("PURCHASEORDER_AUTO_COMPLETE") }}
|
||||
{{ globalsetting("PURCHASEORDER_MERGE_LINE_ITEMS") }}
|
||||
{{ globalsetting("PURCHASEORDER_DEFAULT_RECEIVE_LOCATION") }}
|
||||
|
||||
@@ -422,7 +422,14 @@ class InvenTreeMetadata(SimpleMetadata):
|
||||
|
||||
# If a default value is specified for the serializer field, add it!
|
||||
if 'default' not in field_info and field.default != empty:
|
||||
field_info['default'] = field.get_default()
|
||||
default = field.get_default()
|
||||
|
||||
# Related fields resolve their default to a model instance,
|
||||
# which is not JSON serializable - reduce it to its primitive representation
|
||||
if isinstance(field, serializers.RelatedField) and default is not None:
|
||||
default = field.to_representation(default)
|
||||
|
||||
field_info['default'] = default
|
||||
|
||||
# Force non-nullable fields to read as "required"
|
||||
# (even if there is a default value!)
|
||||
|
||||
@@ -1031,6 +1031,15 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
|
||||
'default': True,
|
||||
'validator': bool,
|
||||
},
|
||||
'PURCHASEORDER_DEFAULT_RECEIVE_LOCATION': {
|
||||
'name': _('Default Receive Location'),
|
||||
'description': _(
|
||||
'Default destination location for received goods on new purchase orders'
|
||||
),
|
||||
'default': '',
|
||||
'model': 'stock.stocklocation',
|
||||
'model_filters': {'structural': False},
|
||||
},
|
||||
# login / SSO
|
||||
'LOGIN_ENABLE_PWD_FORGOT': {
|
||||
'name': _('Enable password forgot'),
|
||||
|
||||
@@ -503,6 +503,8 @@ class SettingsTest(InvenTreeTestCase):
|
||||
'hidden',
|
||||
'choices',
|
||||
'units',
|
||||
'model',
|
||||
'model_filters',
|
||||
'requires_restart',
|
||||
'after_save',
|
||||
'before_save',
|
||||
|
||||
@@ -420,6 +420,22 @@ class PurchaseOrderSerializer(
|
||||
|
||||
return [*fields, 'duplicate']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Set a dynamic default for the 'destination' field, on creation only."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.instance is None:
|
||||
location_pk = get_global_setting(
|
||||
'PURCHASEORDER_DEFAULT_RECEIVE_LOCATION', backup_value=None
|
||||
)
|
||||
|
||||
if location_pk:
|
||||
self.fields[
|
||||
'destination'
|
||||
].default = stock.models.StockLocation.objects.filter(
|
||||
pk=location_pk
|
||||
).first()
|
||||
|
||||
duplicate = DuplicateOptionsSerializer(
|
||||
order.models.PurchaseOrder.objects.all(),
|
||||
copy_lines=True,
|
||||
|
||||
@@ -527,6 +527,53 @@ class PurchaseOrderTest(OrderTest):
|
||||
# Revert the setting to previous value
|
||||
InvenTreeSetting.set_setting(setting, False)
|
||||
|
||||
def test_po_create_default_destination(self):
|
||||
"""Test that the PURCHASEORDER_DEFAULT_RECEIVE_LOCATION setting is applied on creation."""
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
url = reverse('api-po-list')
|
||||
location = StockLocation.objects.first()
|
||||
assert location
|
||||
|
||||
# By default, no destination is set on the setting - so the field is left blank
|
||||
set_global_setting('PURCHASEORDER_DEFAULT_RECEIVE_LOCATION', '')
|
||||
|
||||
data = {
|
||||
'reference': 'PO-99990001',
|
||||
'supplier': 1,
|
||||
'description': 'A test purchase order',
|
||||
}
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
self.assertIsNone(response.data['destination'])
|
||||
|
||||
# Now, set the global default - newly created orders should inherit it
|
||||
set_global_setting('PURCHASEORDER_DEFAULT_RECEIVE_LOCATION', location.pk)
|
||||
|
||||
# The OPTIONS metadata for the 'destination' field should reflect the default location
|
||||
response = self.options(url, expected_code=200)
|
||||
self.assertEqual(
|
||||
response.data['actions']['POST']['destination']['default'], location.pk
|
||||
)
|
||||
|
||||
data['reference'] = 'PO-99990002'
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
self.assertEqual(response.data['destination'], location.pk)
|
||||
|
||||
# An explicitly provided destination should always take priority
|
||||
other_location = StockLocation.objects.exclude(pk=location.pk).first()
|
||||
assert other_location
|
||||
|
||||
data['reference'] = 'PO-99990003'
|
||||
data['destination'] = other_location.pk
|
||||
|
||||
response = self.post(url, data, expected_code=201)
|
||||
self.assertEqual(response.data['destination'], other_location.pk)
|
||||
|
||||
# Revert the setting to its previous value
|
||||
set_global_setting('PURCHASEORDER_DEFAULT_RECEIVE_LOCATION', '')
|
||||
|
||||
def test_po_creation_date(self):
|
||||
"""Test that we can create set the creation_date field of PurchaseOrder via the API."""
|
||||
self.assignRole('purchase_order.add')
|
||||
|
||||
@@ -199,7 +199,8 @@ function SelectPartsStep({
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const purchaseOrderFields = usePurchaseOrderFields({
|
||||
supplierId: selectedRecord?.supplier_part?.supplier
|
||||
supplierId: selectedRecord?.supplier_part?.supplier,
|
||||
create: true
|
||||
});
|
||||
|
||||
const newPurchaseOrder = useCreateApiFormModal({
|
||||
|
||||
@@ -245,10 +245,12 @@ export function usePurchaseOrderLineItemFields({
|
||||
*/
|
||||
export function usePurchaseOrderFields({
|
||||
supplierId,
|
||||
duplicateOrderId
|
||||
duplicateOrderId,
|
||||
create
|
||||
}: {
|
||||
supplierId?: number;
|
||||
duplicateOrderId?: number;
|
||||
create?: boolean;
|
||||
}): ApiFormFieldSet {
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
@@ -285,7 +287,15 @@ export function usePurchaseOrderFields({
|
||||
destination: {
|
||||
filters: {
|
||||
structural: false
|
||||
}
|
||||
},
|
||||
default: create
|
||||
? toNumber(
|
||||
globalSettings.getSetting(
|
||||
'PURCHASEORDER_DEFAULT_RECEIVE_LOCATION'
|
||||
),
|
||||
null
|
||||
)
|
||||
: undefined
|
||||
},
|
||||
tags: TagsField({}),
|
||||
link: {},
|
||||
@@ -336,7 +346,7 @@ export function usePurchaseOrderFields({
|
||||
}
|
||||
|
||||
return fields;
|
||||
}, [duplicateOrderId, supplierId, globalSettings]);
|
||||
}, [duplicateOrderId, supplierId, create, globalSettings]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -356,7 +356,8 @@ export default function SystemSettings() {
|
||||
'PURCHASEORDER_REQUIRE_RESPONSIBLE',
|
||||
'PURCHASEORDER_CONVERT_CURRENCY',
|
||||
'PURCHASEORDER_EDIT_COMPLETED_ORDERS',
|
||||
'PURCHASEORDER_AUTO_COMPLETE'
|
||||
'PURCHASEORDER_AUTO_COMPLETE',
|
||||
'PURCHASEORDER_DEFAULT_RECEIVE_LOCATION'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -82,7 +82,8 @@ export default function PurchaseOrderDetail() {
|
||||
const purchaseOrderFields = usePurchaseOrderFields({});
|
||||
|
||||
const duplicatePurchaseOrderFields = usePurchaseOrderFields({
|
||||
duplicateOrderId: order.pk
|
||||
duplicateOrderId: order.pk,
|
||||
create: true
|
||||
});
|
||||
|
||||
const editPurchaseOrder = useEditApiFormModal({
|
||||
|
||||
@@ -111,7 +111,7 @@ export function PurchaseOrderTable({
|
||||
];
|
||||
}, []);
|
||||
|
||||
const purchaseOrderFields = usePurchaseOrderFields({});
|
||||
const purchaseOrderFields = usePurchaseOrderFields({ create: true });
|
||||
|
||||
const newPurchaseOrder = useCreateApiFormModal({
|
||||
url: ApiEndpoints.purchase_order_list,
|
||||
|
||||
Reference in New Issue
Block a user