mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
[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:
parent
453254c278
commit
eacd28bf19
@ -1,12 +1,15 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v224 - 2024-07-14 : https://github.com/inventree/InvenTree/pull/7667
|
||||||
- Add notes field to ManufacturerPart and SupplierPart API endpoints
|
- Add notes field to ManufacturerPart and SupplierPart API endpoints
|
||||||
|
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
@ -32,8 +32,9 @@ class DataImportSession(models.Model):
|
|||||||
data_file: FileField for the data file to import
|
data_file: FileField for the data file to import
|
||||||
status: IntegerField for the status of the import session
|
status: IntegerField for the status of the import session
|
||||||
user: ForeignKey to the User who initiated the import
|
user: ForeignKey to the User who initiated the import
|
||||||
field_defaults: JSONField for field default values
|
field_defaults: JSONField for field default values - provides a backup value for a field
|
||||||
field_overrides: JSONField for field override values
|
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
|
@staticmethod
|
||||||
@ -101,6 +102,13 @@ class DataImportSession(models.Model):
|
|||||||
validators=[importer.validators.validate_field_defaults],
|
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
|
@property
|
||||||
def field_mapping(self):
|
def field_mapping(self):
|
||||||
"""Construct a dict of field mappings for this import session.
|
"""Construct a dict of field mappings for this import session.
|
||||||
|
@ -50,6 +50,7 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
|
|||||||
'column_mappings',
|
'column_mappings',
|
||||||
'field_defaults',
|
'field_defaults',
|
||||||
'field_overrides',
|
'field_overrides',
|
||||||
|
'field_filters',
|
||||||
'row_count',
|
'row_count',
|
||||||
'completed_row_count',
|
'completed_row_count',
|
||||||
]
|
]
|
||||||
@ -104,6 +105,19 @@ class DataImportSessionSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
return overrides
|
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):
|
def create(self, validated_data):
|
||||||
"""Override create method for this serializer.
|
"""Override create method for this serializer.
|
||||||
|
|
||||||
|
@ -373,13 +373,13 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
'pk',
|
'pk',
|
||||||
|
'part',
|
||||||
'quantity',
|
'quantity',
|
||||||
'reference',
|
'reference',
|
||||||
'notes',
|
'notes',
|
||||||
'order',
|
'order',
|
||||||
'order_detail',
|
'order_detail',
|
||||||
'overdue',
|
'overdue',
|
||||||
'part',
|
|
||||||
'part_detail',
|
'part_detail',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
'received',
|
'received',
|
||||||
@ -454,6 +454,14 @@ class PurchaseOrderLineItemSerializer(
|
|||||||
|
|
||||||
return queryset
|
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)
|
quantity = serializers.FloatField(min_value=0, required=True)
|
||||||
|
|
||||||
def validate_quantity(self, quantity):
|
def validate_quantity(self, quantity):
|
||||||
|
@ -138,16 +138,27 @@ export default function ImporterDataSelector({
|
|||||||
// Find the field definition in session.availableFields
|
// Find the field definition in session.availableFields
|
||||||
let fieldDef = session.availableFields[field];
|
let fieldDef = session.availableFields[field];
|
||||||
if (fieldDef) {
|
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] = {
|
fields[field] = {
|
||||||
...fieldDef,
|
...fieldDef,
|
||||||
field_type: fieldDef.type,
|
field_type: fieldDef.type,
|
||||||
description: fieldDef.help_text
|
description: fieldDef.help_text,
|
||||||
|
filters: filters
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fields;
|
return fields;
|
||||||
}, [selectedFieldNames, session.availableFields]);
|
}, [selectedFieldNames, session.availableFields, session.fieldFilters]);
|
||||||
|
|
||||||
const importData = useCallback(
|
const importData = useCallback(
|
||||||
(rows: number[]) => {
|
(rows: number[]) => {
|
||||||
|
@ -71,7 +71,7 @@ const RendererLookup: EnumDictionary<
|
|||||||
[ModelType.parttesttemplate]: RenderPartTestTemplate,
|
[ModelType.parttesttemplate]: RenderPartTestTemplate,
|
||||||
[ModelType.projectcode]: RenderProjectCode,
|
[ModelType.projectcode]: RenderProjectCode,
|
||||||
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
[ModelType.purchaseorder]: RenderPurchaseOrder,
|
||||||
[ModelType.purchaseorderline]: RenderPurchaseOrder,
|
[ModelType.purchaseorderlineitem]: RenderPurchaseOrder,
|
||||||
[ModelType.returnorder]: RenderReturnOrder,
|
[ModelType.returnorder]: RenderReturnOrder,
|
||||||
[ModelType.salesorder]: RenderSalesOrder,
|
[ModelType.salesorder]: RenderSalesOrder,
|
||||||
[ModelType.salesordershipment]: RenderSalesOrderShipment,
|
[ModelType.salesordershipment]: RenderSalesOrderShipment,
|
||||||
|
@ -143,7 +143,7 @@ export const ModelInformationDict: ModelDict = {
|
|||||||
api_endpoint: ApiEndpoints.purchase_order_list,
|
api_endpoint: ApiEndpoints.purchase_order_list,
|
||||||
admin_url: '/order/purchaseorder/'
|
admin_url: '/order/purchaseorder/'
|
||||||
},
|
},
|
||||||
purchaseorderline: {
|
purchaseorderlineitem: {
|
||||||
label: t`Purchase Order Line`,
|
label: t`Purchase Order Line`,
|
||||||
label_multiple: t`Purchase Order Lines`,
|
label_multiple: t`Purchase Order Lines`,
|
||||||
api_endpoint: ApiEndpoints.purchase_order_line_list
|
api_endpoint: ApiEndpoints.purchase_order_line_list
|
||||||
|
@ -9,7 +9,7 @@ import { ModelType } from '../enums/ModelType';
|
|||||||
export const statusCodeList: Record<string, ModelType> = {
|
export const statusCodeList: Record<string, ModelType> = {
|
||||||
BuildStatus: ModelType.build,
|
BuildStatus: ModelType.build,
|
||||||
PurchaseOrderStatus: ModelType.purchaseorder,
|
PurchaseOrderStatus: ModelType.purchaseorder,
|
||||||
ReturnOrderLineStatus: ModelType.purchaseorderline,
|
ReturnOrderLineStatus: ModelType.purchaseorderlineitem,
|
||||||
ReturnOrderStatus: ModelType.returnorder,
|
ReturnOrderStatus: ModelType.returnorder,
|
||||||
SalesOrderStatus: ModelType.salesorder,
|
SalesOrderStatus: ModelType.salesorder,
|
||||||
StockHistoryCode: ModelType.stockhistory,
|
StockHistoryCode: ModelType.stockhistory,
|
||||||
|
@ -18,7 +18,7 @@ export enum ModelType {
|
|||||||
builditem = 'builditem',
|
builditem = 'builditem',
|
||||||
company = 'company',
|
company = 'company',
|
||||||
purchaseorder = 'purchaseorder',
|
purchaseorder = 'purchaseorder',
|
||||||
purchaseorderline = 'purchaseorderline',
|
purchaseorderlineitem = 'purchaseorderlineitem',
|
||||||
salesorder = 'salesorder',
|
salesorder = 'salesorder',
|
||||||
salesordershipment = 'salesordershipment',
|
salesordershipment = 'salesordershipment',
|
||||||
returnorder = 'returnorder',
|
returnorder = 'returnorder',
|
||||||
|
@ -11,6 +11,10 @@ export function dataImporterSessionFields(): ApiFormFieldSet {
|
|||||||
field_overrides: {
|
field_overrides: {
|
||||||
hidden: true,
|
hidden: true,
|
||||||
value: {}
|
value: {}
|
||||||
|
},
|
||||||
|
field_filters: {
|
||||||
|
hidden: true,
|
||||||
|
value: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ export type ImportSessionState = {
|
|||||||
columnMappings: any[];
|
columnMappings: any[];
|
||||||
fieldDefaults: any;
|
fieldDefaults: any;
|
||||||
fieldOverrides: any;
|
fieldOverrides: any;
|
||||||
|
fieldFilters: any;
|
||||||
rowCount: number;
|
rowCount: number;
|
||||||
completedRowCount: number;
|
completedRowCount: number;
|
||||||
};
|
};
|
||||||
@ -113,6 +114,10 @@ export function useImportSession({
|
|||||||
return sessionData?.field_overrides ?? {};
|
return sessionData?.field_overrides ?? {};
|
||||||
}, [sessionData]);
|
}, [sessionData]);
|
||||||
|
|
||||||
|
const fieldFilters: any = useMemo(() => {
|
||||||
|
return sessionData?.field_filters ?? {};
|
||||||
|
}, [sessionData]);
|
||||||
|
|
||||||
const rowCount: number = useMemo(() => {
|
const rowCount: number = useMemo(() => {
|
||||||
return sessionData?.row_count ?? 0;
|
return sessionData?.row_count ?? 0;
|
||||||
}, [sessionData]);
|
}, [sessionData]);
|
||||||
@ -134,6 +139,7 @@ export function useImportSession({
|
|||||||
mappedFields,
|
mappedFields,
|
||||||
fieldDefaults,
|
fieldDefaults,
|
||||||
fieldOverrides,
|
fieldOverrides,
|
||||||
|
fieldFilters,
|
||||||
rowCount,
|
rowCount,
|
||||||
completedRowCount
|
completedRowCount
|
||||||
};
|
};
|
||||||
|
@ -242,6 +242,7 @@ export default function PurchaseOrderDetail() {
|
|||||||
icon: <IconList />,
|
icon: <IconList />,
|
||||||
content: (
|
content: (
|
||||||
<PurchaseOrderLineItemTable
|
<PurchaseOrderLineItemTable
|
||||||
|
order={order}
|
||||||
orderId={Number(id)}
|
orderId={Number(id)}
|
||||||
supplierId={Number(order.supplier)}
|
supplierId={Number(order.supplier)}
|
||||||
/>
|
/>
|
||||||
|
@ -561,7 +561,7 @@ export function BomTable({
|
|||||||
</Stack>
|
</Stack>
|
||||||
<ImporterDrawer
|
<ImporterDrawer
|
||||||
sessionId={selectedSession ?? -1}
|
sessionId={selectedSession ?? -1}
|
||||||
opened={selectedSession !== undefined && importOpened}
|
opened={selectedSession != undefined && importOpened}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setSelectedSession(undefined);
|
setSelectedSession(undefined);
|
||||||
setImportOpened(false);
|
setImportOpened(false);
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Text } from '@mantine/core';
|
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 { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { ActionButton } from '../../components/buttons/ActionButton';
|
import { ActionButton } from '../../components/buttons/ActionButton';
|
||||||
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
import { AddItemButton } from '../../components/buttons/AddItemButton';
|
||||||
import { Thumbnail } from '../../components/images/Thumbnail';
|
import { Thumbnail } from '../../components/images/Thumbnail';
|
||||||
|
import ImporterDrawer from '../../components/importer/ImporterDrawer';
|
||||||
import { ProgressBar } from '../../components/items/ProgressBar';
|
import { ProgressBar } from '../../components/items/ProgressBar';
|
||||||
import { RenderStockLocation } from '../../components/render/Stock';
|
import { RenderStockLocation } from '../../components/render/Stock';
|
||||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||||
import { ModelType } from '../../enums/ModelType';
|
import { ModelType } from '../../enums/ModelType';
|
||||||
import { UserRoles } from '../../enums/Roles';
|
import { UserRoles } from '../../enums/Roles';
|
||||||
|
import { dataImporterSessionFields } from '../../forms/ImporterForms';
|
||||||
import {
|
import {
|
||||||
usePurchaseOrderLineItemFields,
|
usePurchaseOrderLineItemFields,
|
||||||
useReceiveLineItems
|
useReceiveLineItems
|
||||||
@ -44,10 +47,12 @@ import { TableHoverCard } from '../TableHoverCard';
|
|||||||
* Display a table of purchase order line items, for a specific order
|
* Display a table of purchase order line items, for a specific order
|
||||||
*/
|
*/
|
||||||
export function PurchaseOrderLineItemTable({
|
export function PurchaseOrderLineItemTable({
|
||||||
|
order,
|
||||||
orderId,
|
orderId,
|
||||||
supplierId,
|
supplierId,
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
|
order: any;
|
||||||
orderId: number;
|
orderId: number;
|
||||||
supplierId?: number;
|
supplierId?: number;
|
||||||
params?: any;
|
params?: any;
|
||||||
@ -56,6 +61,49 @@ export function PurchaseOrderLineItemTable({
|
|||||||
|
|
||||||
const user = useUserState();
|
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 [singleRecord, setSingleRecord] = useState(null);
|
||||||
|
|
||||||
const receiveLineItems = useReceiveLineItems({
|
const receiveLineItems = useReceiveLineItems({
|
||||||
@ -277,6 +325,12 @@ export function PurchaseOrderLineItemTable({
|
|||||||
// Custom table actions
|
// Custom table actions
|
||||||
const tableActions = useMemo(() => {
|
const tableActions = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
|
<ActionButton
|
||||||
|
hidden={!user.hasAddRole(UserRoles.purchase_order)}
|
||||||
|
tooltip={t`Import Line Items`}
|
||||||
|
icon={<IconFileArrowLeft />}
|
||||||
|
onClick={() => importLineItems.open()}
|
||||||
|
/>,
|
||||||
<AddItemButton
|
<AddItemButton
|
||||||
tooltip={t`Add line item`}
|
tooltip={t`Add line item`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -298,6 +352,7 @@ export function PurchaseOrderLineItemTable({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{importLineItems.modal}
|
||||||
{receiveLineItems.modal}
|
{receiveLineItems.modal}
|
||||||
{newLine.modal}
|
{newLine.modal}
|
||||||
{editLine.modal}
|
{editLine.modal}
|
||||||
@ -320,6 +375,15 @@ export function PurchaseOrderLineItemTable({
|
|||||||
modelField: 'part'
|
modelField: 'part'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ImporterDrawer
|
||||||
|
sessionId={selectedSession ?? -1}
|
||||||
|
opened={selectedSession != undefined && importOpened}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedSession(undefined);
|
||||||
|
setImportOpened(false);
|
||||||
|
table.refreshTable();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user