[PUI] PO line item import (#7671)

* Fix typo

* Adds new field to DataImportSession model

- field_filters
- Allows custom API field filters to be specified

* Update serializer

* Add button to import purchase order line items

* Fix instance renderer

* Make use of "filters" attribute

* Specify default currency for import

* Update serializer

* Bump API version

* Rename purchaseorderline -> purchaseorderlineitem
This commit is contained in:
Oliver 2024-07-17 17:44:42 +10:00 committed by GitHub
parent 453254c278
commit eacd28bf19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 150 additions and 12 deletions

View File

@ -1,12 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 224
INVENTREE_API_VERSION = 225
"""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-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

View File

@ -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'),
),
]

View File

@ -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.

View File

@ -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.

View File

@ -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):

View File

@ -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[]) => {

View File

@ -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,

View File

@ -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

View File

@ -9,7 +9,7 @@ import { ModelType } from '../enums/ModelType';
export const statusCodeList: Record<string, ModelType> = {
BuildStatus: ModelType.build,
PurchaseOrderStatus: ModelType.purchaseorder,
ReturnOrderLineStatus: ModelType.purchaseorderline,
ReturnOrderLineStatus: ModelType.purchaseorderlineitem,
ReturnOrderStatus: ModelType.returnorder,
SalesOrderStatus: ModelType.salesorder,
StockHistoryCode: ModelType.stockhistory,

View File

@ -18,7 +18,7 @@ export enum ModelType {
builditem = 'builditem',
company = 'company',
purchaseorder = 'purchaseorder',
purchaseorderline = 'purchaseorderline',
purchaseorderlineitem = 'purchaseorderlineitem',
salesorder = 'salesorder',
salesordershipment = 'salesordershipment',
returnorder = 'returnorder',

View File

@ -11,6 +11,10 @@ export function dataImporterSessionFields(): ApiFormFieldSet {
field_overrides: {
hidden: true,
value: {}
},
field_filters: {
hidden: true,
value: {}
}
};
}

View File

@ -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
};

View File

@ -242,6 +242,7 @@ export default function PurchaseOrderDetail() {
icon: <IconList />,
content: (
<PurchaseOrderLineItemTable
order={order}
orderId={Number(id)}
supplierId={Number(order.supplier)}
/>

View File

@ -561,7 +561,7 @@ export function BomTable({
</Stack>
<ImporterDrawer
sessionId={selectedSession ?? -1}
opened={selectedSession !== undefined && importOpened}
opened={selectedSession != undefined && importOpened}
onClose={() => {
setSelectedSession(undefined);
setImportOpened(false);

View File

@ -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<boolean>(false);
const [selectedSession, setSelectedSession] = useState<number | undefined>(
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 [
<ActionButton
hidden={!user.hasAddRole(UserRoles.purchase_order)}
tooltip={t`Import Line Items`}
icon={<IconFileArrowLeft />}
onClick={() => importLineItems.open()}
/>,
<AddItemButton
tooltip={t`Add line item`}
onClick={() => {
@ -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'
}}
/>
<ImporterDrawer
sessionId={selectedSession ?? -1}
opened={selectedSession != undefined && importOpened}
onClose={() => {
setSelectedSession(undefined);
setImportOpened(false);
table.refreshTable();
}}
/>
</>
);
}