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
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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[]) => {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -18,7 +18,7 @@ export enum ModelType {
|
||||
builditem = 'builditem',
|
||||
company = 'company',
|
||||
purchaseorder = 'purchaseorder',
|
||||
purchaseorderline = 'purchaseorderline',
|
||||
purchaseorderlineitem = 'purchaseorderlineitem',
|
||||
salesorder = 'salesorder',
|
||||
salesordershipment = 'salesordershipment',
|
||||
returnorder = 'returnorder',
|
||||
|
@ -11,6 +11,10 @@ export function dataImporterSessionFields(): ApiFormFieldSet {
|
||||
field_overrides: {
|
||||
hidden: true,
|
||||
value: {}
|
||||
},
|
||||
field_filters: {
|
||||
hidden: true,
|
||||
value: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -242,6 +242,7 @@ export default function PurchaseOrderDetail() {
|
||||
icon: <IconList />,
|
||||
content: (
|
||||
<PurchaseOrderLineItemTable
|
||||
order={order}
|
||||
orderId={Number(id)}
|
||||
supplierId={Number(order.supplier)}
|
||||
/>
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user