diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index 1a70c7ca2d..95b0caa66a 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,15 +1,18 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 225
+INVENTREE_API_VERSION = 226
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
-v225 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
+v226 - 2024-07-15 : https://github.com/inventree/InvenTree/pull/7648
- Adds barcode generation API endpoint
+v225 - 2024-07-17 : https://github.com/inventree/InvenTree/pull/7671
+ - Adds "filters" field to DataImportSession API
+
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
- Add notes field to ManufacturerPart and SupplierPart API endpoints
diff --git a/src/backend/InvenTree/build/models.py b/src/backend/InvenTree/build/models.py
index bcf62710b9..85a97fb625 100644
--- a/src/backend/InvenTree/build/models.py
+++ b/src/backend/InvenTree/build/models.py
@@ -125,26 +125,28 @@ class Build(
self.validate_reference_field(self.reference)
self.reference_int = self.rebuild_reference_field(self.reference)
- if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'):
- # Check that the BOM is valid
- if not self.part.is_bom_valid():
- raise ValidationError({
- 'part': _('Assembly BOM has not been validated')
- })
+ # Check part when initially creating the build order
+ if not self.pk or self.has_field_changed('part'):
+ if get_global_setting('BUILDORDER_REQUIRE_VALID_BOM'):
+ # Check that the BOM is valid
+ if not self.part.is_bom_valid():
+ raise ValidationError({
+ 'part': _('Assembly BOM has not been validated')
+ })
- if get_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART'):
- # Check that the part is active
- if not self.part.active:
- raise ValidationError({
- 'part': _('Build order cannot be created for an inactive part')
- })
+ if get_global_setting('BUILDORDER_REQUIRE_ACTIVE_PART'):
+ # Check that the part is active
+ if not self.part.active:
+ raise ValidationError({
+ 'part': _('Build order cannot be created for an inactive part')
+ })
- if get_global_setting('BUILDORDER_REQUIRE_LOCKED_PART'):
- # Check that the part is locked
- if not self.part.locked:
- raise ValidationError({
- 'part': _('Build order cannot be created for an unlocked part')
- })
+ if get_global_setting('BUILDORDER_REQUIRE_LOCKED_PART'):
+ # Check that the part is locked
+ if not self.part.locked:
+ raise ValidationError({
+ 'part': _('Build order cannot be created for an unlocked part')
+ })
# On first save (i.e. creation), run some extra checks
if self.pk is None:
diff --git a/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py b/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py
new file mode 100644
index 0000000000..b5663a5e31
--- /dev/null
+++ b/src/backend/InvenTree/importer/migrations/0003_dataimportsession_field_filters.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.14 on 2024-07-16 03:04
+
+from django.db import migrations, models
+import importer.validators
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('importer', '0002_dataimportsession_field_overrides'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='dataimportsession',
+ name='field_filters',
+ field=models.JSONField(blank=True, null=True, validators=[importer.validators.validate_field_defaults], verbose_name='Field Filters'),
+ ),
+ ]
diff --git a/src/backend/InvenTree/importer/models.py b/src/backend/InvenTree/importer/models.py
index 83c417f782..219e1ac600 100644
--- a/src/backend/InvenTree/importer/models.py
+++ b/src/backend/InvenTree/importer/models.py
@@ -32,8 +32,9 @@ class DataImportSession(models.Model):
data_file: FileField for the data file to import
status: IntegerField for the status of the import session
user: ForeignKey to the User who initiated the import
- field_defaults: JSONField for field default values
- field_overrides: JSONField for field override values
+ field_defaults: JSONField for field default values - provides a backup value for a field
+ field_overrides: JSONField for field override values - used to force a value for a field
+ field_filters: JSONField for field filter values - optional field API filters
"""
@staticmethod
@@ -101,6 +102,13 @@ class DataImportSession(models.Model):
validators=[importer.validators.validate_field_defaults],
)
+ field_filters = models.JSONField(
+ blank=True,
+ null=True,
+ verbose_name=_('Field Filters'),
+ validators=[importer.validators.validate_field_defaults],
+ )
+
@property
def field_mapping(self):
"""Construct a dict of field mappings for this import session.
diff --git a/src/backend/InvenTree/importer/serializers.py b/src/backend/InvenTree/importer/serializers.py
index 2400dc179d..ac68056f55 100644
--- a/src/backend/InvenTree/importer/serializers.py
+++ b/src/backend/InvenTree/importer/serializers.py
@@ -50,6 +50,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
'column_mappings',
'field_defaults',
'field_overrides',
+ 'field_filters',
'row_count',
'completed_row_count',
]
@@ -104,6 +105,19 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
return overrides
+ def validate_field_filters(self, filters):
+ """De-stringify the field filters."""
+ if filters is None:
+ return None
+
+ if type(filters) is not dict:
+ try:
+ filters = json.loads(str(filters))
+ except:
+ raise ValidationError(_('Invalid field filters'))
+
+ return filters
+
def create(self, validated_data):
"""Override create method for this serializer.
diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py
index dd6129bec8..f8ed5c6f39 100644
--- a/src/backend/InvenTree/order/serializers.py
+++ b/src/backend/InvenTree/order/serializers.py
@@ -373,13 +373,13 @@ class PurchaseOrderLineItemSerializer(
fields = [
'pk',
+ 'part',
'quantity',
'reference',
'notes',
'order',
'order_detail',
'overdue',
- 'part',
'part_detail',
'supplier_part_detail',
'received',
@@ -454,6 +454,14 @@ class PurchaseOrderLineItemSerializer(
return queryset
+ part = serializers.PrimaryKeyRelatedField(
+ queryset=part_models.SupplierPart.objects.all(),
+ many=False,
+ required=True,
+ allow_null=True,
+ label=_('Supplier Part'),
+ )
+
quantity = serializers.FloatField(min_value=0, required=True)
def validate_quantity(self, quantity):
diff --git a/src/backend/InvenTree/templates/js/translated/forms.js b/src/backend/InvenTree/templates/js/translated/forms.js
index 7d273dd9ec..3d888072eb 100644
--- a/src/backend/InvenTree/templates/js/translated/forms.js
+++ b/src/backend/InvenTree/templates/js/translated/forms.js
@@ -1502,8 +1502,23 @@ function handleFormErrors(errors, fields={}, options={}) {
for (var field_name in errors) {
- var field = fields[field_name] || {};
- var field_errors = errors[field_name];
+ let field = fields[field_name] || null;
+ let field_errors = errors[field_name];
+
+ // No matching field - append to non_field_errors
+ if (!field || field.hidden) {
+
+ if (Array.isArray(field_errors)) {
+ field_errors.forEach((err) => {
+ non_field_errors.append(`
${err}
`);
+ });
+ } else {
+ non_field_errors.append(`${field_errors.toString()}
`);
+ }
+
+ continue;
+ }
+
// for nested objects with children and dependent fields with a child defined, extract nested errors
if (((field.type == 'nested object') && ('children' in field)) || ((field.type == 'dependent field') && ('child' in field))) {
diff --git a/src/frontend/src/components/importer/ImportDataSelector.tsx b/src/frontend/src/components/importer/ImportDataSelector.tsx
index 095a42bd59..9c6d7c531f 100644
--- a/src/frontend/src/components/importer/ImportDataSelector.tsx
+++ b/src/frontend/src/components/importer/ImportDataSelector.tsx
@@ -138,16 +138,27 @@ export default function ImporterDataSelector({
// Find the field definition in session.availableFields
let fieldDef = session.availableFields[field];
if (fieldDef) {
+ // Construct field filters based on session field filters
+ let filters = fieldDef.filters ?? {};
+
+ if (session.fieldFilters[field]) {
+ filters = {
+ ...filters,
+ ...session.fieldFilters[field]
+ };
+ }
+
fields[field] = {
...fieldDef,
field_type: fieldDef.type,
- description: fieldDef.help_text
+ description: fieldDef.help_text,
+ filters: filters
};
}
}
return fields;
- }, [selectedFieldNames, session.availableFields]);
+ }, [selectedFieldNames, session.availableFields, session.fieldFilters]);
const importData = useCallback(
(rows: number[]) => {
diff --git a/src/frontend/src/components/render/Instance.tsx b/src/frontend/src/components/render/Instance.tsx
index 85c50ced5a..aa14af36d2 100644
--- a/src/frontend/src/components/render/Instance.tsx
+++ b/src/frontend/src/components/render/Instance.tsx
@@ -71,7 +71,7 @@ const RendererLookup: EnumDictionary<
[ModelType.parttesttemplate]: RenderPartTestTemplate,
[ModelType.projectcode]: RenderProjectCode,
[ModelType.purchaseorder]: RenderPurchaseOrder,
- [ModelType.purchaseorderline]: RenderPurchaseOrder,
+ [ModelType.purchaseorderlineitem]: RenderPurchaseOrder,
[ModelType.returnorder]: RenderReturnOrder,
[ModelType.salesorder]: RenderSalesOrder,
[ModelType.salesordershipment]: RenderSalesOrderShipment,
diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx
index e8b12da9ca..50a22c82ba 100644
--- a/src/frontend/src/components/render/ModelType.tsx
+++ b/src/frontend/src/components/render/ModelType.tsx
@@ -143,7 +143,7 @@ export const ModelInformationDict: ModelDict = {
api_endpoint: ApiEndpoints.purchase_order_list,
admin_url: '/order/purchaseorder/'
},
- purchaseorderline: {
+ purchaseorderlineitem: {
label: t`Purchase Order Line`,
label_multiple: t`Purchase Order Lines`,
api_endpoint: ApiEndpoints.purchase_order_line_list
diff --git a/src/frontend/src/defaults/backendMappings.tsx b/src/frontend/src/defaults/backendMappings.tsx
index 25dd8965e0..3339b27a8e 100644
--- a/src/frontend/src/defaults/backendMappings.tsx
+++ b/src/frontend/src/defaults/backendMappings.tsx
@@ -9,7 +9,7 @@ import { ModelType } from '../enums/ModelType';
export const statusCodeList: Record = {
BuildStatus: ModelType.build,
PurchaseOrderStatus: ModelType.purchaseorder,
- ReturnOrderLineStatus: ModelType.purchaseorderline,
+ ReturnOrderLineStatus: ModelType.purchaseorderlineitem,
ReturnOrderStatus: ModelType.returnorder,
SalesOrderStatus: ModelType.salesorder,
StockHistoryCode: ModelType.stockhistory,
diff --git a/src/frontend/src/enums/ModelType.tsx b/src/frontend/src/enums/ModelType.tsx
index e71944f954..570c382c25 100644
--- a/src/frontend/src/enums/ModelType.tsx
+++ b/src/frontend/src/enums/ModelType.tsx
@@ -18,7 +18,7 @@ export enum ModelType {
builditem = 'builditem',
company = 'company',
purchaseorder = 'purchaseorder',
- purchaseorderline = 'purchaseorderline',
+ purchaseorderlineitem = 'purchaseorderlineitem',
salesorder = 'salesorder',
salesordershipment = 'salesordershipment',
returnorder = 'returnorder',
diff --git a/src/frontend/src/forms/ImporterForms.tsx b/src/frontend/src/forms/ImporterForms.tsx
index 9fac28cbf5..9ce00d30b6 100644
--- a/src/frontend/src/forms/ImporterForms.tsx
+++ b/src/frontend/src/forms/ImporterForms.tsx
@@ -11,6 +11,10 @@ export function dataImporterSessionFields(): ApiFormFieldSet {
field_overrides: {
hidden: true,
value: {}
+ },
+ field_filters: {
+ hidden: true,
+ value: {}
}
};
}
diff --git a/src/frontend/src/hooks/UseImportSession.tsx b/src/frontend/src/hooks/UseImportSession.tsx
index 34d52dff20..361206f5a7 100644
--- a/src/frontend/src/hooks/UseImportSession.tsx
+++ b/src/frontend/src/hooks/UseImportSession.tsx
@@ -31,6 +31,7 @@ export type ImportSessionState = {
columnMappings: any[];
fieldDefaults: any;
fieldOverrides: any;
+ fieldFilters: any;
rowCount: number;
completedRowCount: number;
};
@@ -113,6 +114,10 @@ export function useImportSession({
return sessionData?.field_overrides ?? {};
}, [sessionData]);
+ const fieldFilters: any = useMemo(() => {
+ return sessionData?.field_filters ?? {};
+ }, [sessionData]);
+
const rowCount: number = useMemo(() => {
return sessionData?.row_count ?? 0;
}, [sessionData]);
@@ -134,6 +139,7 @@ export function useImportSession({
mappedFields,
fieldDefaults,
fieldOverrides,
+ fieldFilters,
rowCount,
completedRowCount
};
diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
index 83e49a6951..34b2e23c4e 100644
--- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
+++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx
@@ -242,6 +242,7 @@ export default function PurchaseOrderDetail() {
icon: ,
content: (
diff --git a/src/frontend/src/tables/bom/BomTable.tsx b/src/frontend/src/tables/bom/BomTable.tsx
index f1bac0c23d..d583cff857 100644
--- a/src/frontend/src/tables/bom/BomTable.tsx
+++ b/src/frontend/src/tables/bom/BomTable.tsx
@@ -561,7 +561,7 @@ export function BomTable({
{
setSelectedSession(undefined);
setImportOpened(false);
diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
index 3a4dba67da..8083aafc90 100644
--- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
+++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx
@@ -1,16 +1,19 @@
import { t } from '@lingui/macro';
import { Text } from '@mantine/core';
-import { IconSquareArrowRight } from '@tabler/icons-react';
+import { Action } from '@mdxeditor/editor';
+import { IconFileArrowLeft, IconSquareArrowRight } from '@tabler/icons-react';
import { useCallback, useMemo, useState } from 'react';
import { ActionButton } from '../../components/buttons/ActionButton';
import { AddItemButton } from '../../components/buttons/AddItemButton';
import { Thumbnail } from '../../components/images/Thumbnail';
+import ImporterDrawer from '../../components/importer/ImporterDrawer';
import { ProgressBar } from '../../components/items/ProgressBar';
import { RenderStockLocation } from '../../components/render/Stock';
import { ApiEndpoints } from '../../enums/ApiEndpoints';
import { ModelType } from '../../enums/ModelType';
import { UserRoles } from '../../enums/Roles';
+import { dataImporterSessionFields } from '../../forms/ImporterForms';
import {
usePurchaseOrderLineItemFields,
useReceiveLineItems
@@ -44,10 +47,12 @@ import { TableHoverCard } from '../TableHoverCard';
* Display a table of purchase order line items, for a specific order
*/
export function PurchaseOrderLineItemTable({
+ order,
orderId,
supplierId,
params
}: {
+ order: any;
orderId: number;
supplierId?: number;
params?: any;
@@ -56,6 +61,49 @@ export function PurchaseOrderLineItemTable({
const user = useUserState();
+ // Data import
+ const [importOpened, setImportOpened] = useState(false);
+ const [selectedSession, setSelectedSession] = useState(
+ undefined
+ );
+
+ const importSessionFields = useMemo(() => {
+ let fields = dataImporterSessionFields();
+
+ fields.model_type.hidden = true;
+ fields.model_type.value = ModelType.purchaseorderlineitem;
+
+ // Specify override values for import
+ fields.field_overrides.value = {
+ order: orderId
+ };
+
+ // Specify default values based on the order data
+ fields.field_defaults.value = {
+ purchase_price_currency:
+ order?.order_currency || order?.supplier_detail?.currency || undefined
+ };
+
+ fields.field_filters.value = {
+ part: {
+ supplier: supplierId,
+ active: true
+ }
+ };
+
+ return fields;
+ }, [order, orderId, supplierId]);
+
+ const importLineItems = useCreateApiFormModal({
+ url: ApiEndpoints.import_session_list,
+ title: t`Import Line Items`,
+ fields: importSessionFields,
+ onFormSuccess: (response: any) => {
+ setSelectedSession(response.pk);
+ setImportOpened(true);
+ }
+ });
+
const [singleRecord, setSingleRecord] = useState(null);
const receiveLineItems = useReceiveLineItems({
@@ -277,6 +325,12 @@ export function PurchaseOrderLineItemTable({
// Custom table actions
const tableActions = useMemo(() => {
return [
+ }
+ onClick={() => importLineItems.open()}
+ />,
{
@@ -298,6 +352,7 @@ export function PurchaseOrderLineItemTable({
return (
<>
+ {importLineItems.modal}
{receiveLineItems.modal}
{newLine.modal}
{editLine.modal}
@@ -320,6 +375,15 @@ export function PurchaseOrderLineItemTable({
modelField: 'part'
}}
/>
+ {
+ setSelectedSession(undefined);
+ setImportOpened(false);
+ table.refreshTable();
+ }}
+ />
>
);
}