2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-30 12:45:42 +00:00

[feature] Project code per line (#10657)

* Add project code to line items

* Refactor AbstractOrderSerialiazer

* Refactor AbstractOrderLineItem serializer

* Refactoring for AbstractExtraLineSerializer

* UI elements for extra line item project code

* UI for ReturnOrderLineItems

* UI elements for SalesOrderLineItem

* UI elements for PurchaseOrderLineItem

* Docs updates

* Update API version and CHANGELOG
This commit is contained in:
Oliver
2025-10-24 15:10:58 +11:00
committed by GitHub
parent c3d788eeeb
commit 96dfee4018
14 changed files with 282 additions and 79 deletions

View File

@@ -1,11 +1,15 @@
"""InvenTree API version information."""
# InvenTree API version
INVENTREE_API_VERSION = 417
INVENTREE_API_VERSION = 418
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
v418 -> 2025-10-24 : https://github.com/inventree/InvenTree/pull/10657
- Add "project_code" field(s) to OrderLineItem API endpoint(s)
- Add "project_code" field(s) to ExtraOrderLineItem API endpoint(s)
v417 -> 2025-10-22 : https://github.com/inventree/InvenTree/pull/10654
- Adds "checked" filter to SalesOrderShipment API endpoint
- Adds "order_status" filter to SalesOrdereShipment API endpoint

View File

@@ -0,0 +1,87 @@
# Generated by Django 4.2.25 on 2025-10-24 01:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("common", "0039_emailthread_emailmessage"),
("order", "0113_salesordershipment_shipment_address"),
]
operations = [
migrations.AddField(
model_name="purchaseorderextraline",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
migrations.AddField(
model_name="purchaseorderlineitem",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
migrations.AddField(
model_name="returnorderextraline",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
migrations.AddField(
model_name="returnorderlineitem",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
migrations.AddField(
model_name="salesorderextraline",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
migrations.AddField(
model_name="salesorderlineitem",
name="project_code",
field=models.ForeignKey(
blank=True,
help_text="Select project code for this order",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="common.projectcode",
verbose_name="Project Code",
),
),
]

View File

@@ -1685,6 +1685,7 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
Attributes:
quantity: Number of items
reference: Reference text (e.g. customer reference) for this line item
project_code: Project code associated with this line item (optional)
note: Annotation for the item
target_date: An (optional) date for expected shipment of this line item.
"""
@@ -1768,6 +1769,15 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
),
)
project_code = models.ForeignKey(
common_models.ProjectCode,
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_('Project Code'),
help_text=_('Select project code for this order'),
)
class OrderExtraLine(OrderLineItem):
"""Abstract Model for a single ExtraLine in a Order.
@@ -1992,8 +2002,6 @@ class PurchaseOrderExtraLine(OrderExtraLine):
Attributes:
order: Link to the PurchaseOrder that this line belongs to
title: title of line
price: The unit price for this OrderLine
"""
class Meta:
@@ -2422,8 +2430,6 @@ class SalesOrderExtraLine(OrderExtraLine):
Attributes:
order: Link to the SalesOrder that this line belongs to
title: title of line
price: The unit price for this OrderLine
"""
class Meta:

View File

@@ -45,6 +45,7 @@ from InvenTree.helpers import (
)
from InvenTree.mixins import DataImportExportSerializerMixin
from InvenTree.serializers import (
FilterableCharField,
FilterableSerializerMixin,
InvenTreeCurrencySerializer,
InvenTreeDecimalField,
@@ -105,7 +106,9 @@ class DuplicateOrderSerializer(serializers.Serializer):
)
class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Serializer):
class AbstractOrderSerializer(
DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer
):
"""Abstract serializer class which provides fields common to all order types."""
export_exclude_fields = ['notes', 'duplicate']
@@ -132,30 +135,46 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
reference = serializers.CharField(required=True)
# Detail for point-of-contact field
contact_detail = ContactSerializer(
source='contact', many=False, read_only=True, allow_null=True
contact_detail = enable_filter(
ContactSerializer(
source='contact', many=False, read_only=True, allow_null=True
),
True,
)
# Detail for responsible field
responsible_detail = OwnerSerializer(
source='responsible', read_only=True, allow_null=True, many=False
responsible_detail = enable_filter(
OwnerSerializer(
source='responsible', read_only=True, allow_null=True, many=False
),
True,
)
project_code_label = serializers.CharField(
source='project_code.code',
read_only=True,
label='Project Code Label',
allow_null=True,
project_code_label = enable_filter(
FilterableCharField(
source='project_code.code',
read_only=True,
label='Project Code Label',
allow_null=True,
),
True,
filter_name='project_code_detail',
)
# Detail for project code field
project_code_detail = ProjectCodeSerializer(
source='project_code', read_only=True, many=False, allow_null=True
project_code_detail = enable_filter(
ProjectCodeSerializer(
source='project_code', read_only=True, many=False, allow_null=True
),
True,
)
# Detail for address field
address_detail = AddressBriefSerializer(
source='address', many=False, read_only=True, allow_null=True
address_detail = enable_filter(
AddressBriefSerializer(
source='address', many=False, read_only=True, allow_null=True
),
True,
)
# Boolean field indicating if this order is overdue (Note: must be annotated)
@@ -204,15 +223,10 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
'completed_lines',
'link',
'project_code',
'project_code_label',
'project_code_detail',
'reference',
'responsible',
'responsible_detail',
'contact',
'contact_detail',
'address',
'address_detail',
'status',
'status_text',
'status_custom_key',
@@ -220,6 +234,12 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
'barcode_hash',
'overdue',
'duplicate',
# Extra detail fields
'address_detail',
'contact_detail',
'project_code_detail',
'project_code_label',
'responsible_detail',
*extra_fields,
]
@@ -263,25 +283,103 @@ class AbstractOrderSerializer(DataImportExportSerializerMixin, serializers.Seria
return instance
class AbstractLineItemSerializer:
class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializer):
"""Abstract serializer for LineItem object."""
@staticmethod
def line_fields(extra_fields):
"""Construct a set of fields for this serializer."""
return [
'pk',
'link',
'notes',
'order',
'project_code',
'quantity',
'reference',
'target_date',
# Filterable detail fields
'order_detail',
'project_code_label',
'project_code_detail',
*extra_fields,
]
target_date = serializers.DateField(
required=False, allow_null=True, label=_('Target Date')
)
project_code_label = enable_filter(
FilterableCharField(
source='project_code.code',
read_only=True,
label='Project Code Label',
allow_null=True,
),
True,
filter_name='project_code_detail',
)
project_code_detail = enable_filter(
ProjectCodeSerializer(
source='project_code', read_only=True, many=False, allow_null=True
),
True,
)
class AbstractExtraLineSerializer(
DataImportExportSerializerMixin, serializers.Serializer
DataImportExportSerializerMixin, FilterableSerializerMixin, serializers.Serializer
):
"""Abstract Serializer for a ExtraLine object."""
@staticmethod
def extra_line_fields(extra_fields):
"""Construct a set of fields for this serializer."""
return [
'pk',
'description',
'link',
'notes',
'order',
'price',
'price_currency',
'project_code',
'quantity',
'reference',
'target_date',
# Filterable detail fields
'order_detail',
'project_code_label',
'project_code_detail',
*extra_fields,
]
quantity = serializers.FloatField()
price = InvenTreeMoneySerializer(allow_null=True)
price_currency = InvenTreeCurrencySerializer()
project_code_label = enable_filter(
FilterableCharField(
source='project_code.code',
read_only=True,
label='Project Code Label',
allow_null=True,
),
True,
filter_name='project_code_detail',
)
# Detail for project code field
project_code_detail = enable_filter(
ProjectCodeSerializer(
source='project_code', read_only=True, many=False, allow_null=True
),
True,
)
class AbstractExtraLineMeta:
"""Abstract Meta for ExtraLine."""
@@ -303,7 +401,6 @@ class AbstractExtraLineMeta:
@register_importer()
class PurchaseOrderSerializer(
FilterableSerializerMixin,
NotesFieldMixin,
TotalPriceMixin,
InvenTreeCustomStatusSerializerMixin,
@@ -460,7 +557,6 @@ class PurchaseOrderIssueSerializer(OrderAdjustSerializer):
@register_importer()
class PurchaseOrderLineItemSerializer(
FilterableSerializerMixin,
DataImportExportSerializerMixin,
AbstractLineItemSerializer,
InvenTreeModelSerializer,
@@ -471,35 +567,28 @@ class PurchaseOrderLineItemSerializer(
"""Metaclass options."""
model = order.models.PurchaseOrderLineItem
fields = [
'pk',
fields = AbstractLineItemSerializer.line_fields([
'part',
'quantity',
'reference',
'notes',
'order',
'order_detail',
'build_order',
'build_order_detail',
'overdue',
'part_detail',
'supplier_part_detail',
'received',
'purchase_price',
'purchase_price_currency',
'auto_pricing',
'destination',
'destination_detail',
'target_date',
'total_price',
'link',
'merge_items',
'sku',
'mpn',
'ipn',
'internal_part',
'internal_part_name',
]
# Filterable detail fields
'build_order_detail',
'destination_detail',
'part_detail',
'supplier_part_detail',
])
def skip_create_fields(self):
"""Return a list of fields to skip when creating a new object."""
@@ -693,21 +782,22 @@ class PurchaseOrderLineItemSerializer(
@register_importer()
class PurchaseOrderExtraLineSerializer(
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
AbstractExtraLineSerializer, InvenTreeModelSerializer
):
"""Serializer for a PurchaseOrderExtraLine object."""
class Meta(AbstractExtraLineMeta):
"""Metaclass options."""
model = order.models.PurchaseOrderExtraLine
fields = AbstractExtraLineSerializer.extra_line_fields([])
order_detail = enable_filter(
PurchaseOrderSerializer(
source='order', many=False, read_only=True, allow_null=True
)
)
class Meta(AbstractExtraLineMeta):
"""Metaclass options."""
model = order.models.PurchaseOrderExtraLine
class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
"""A serializer for receiving a single purchase order line item against a purchase order."""
@@ -956,7 +1046,6 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
@register_importer()
class SalesOrderSerializer(
FilterableSerializerMixin,
NotesFieldMixin,
TotalPriceMixin,
InvenTreeCustomStatusSerializerMixin,
@@ -1047,7 +1136,6 @@ class SalesOrderIssueSerializer(OrderAdjustSerializer):
@register_importer()
class SalesOrderLineItemSerializer(
FilterableSerializerMixin,
DataImportExportSerializerMixin,
AbstractLineItemSerializer,
InvenTreeModelSerializer,
@@ -1058,29 +1146,22 @@ class SalesOrderLineItemSerializer(
"""Metaclass options."""
model = order.models.SalesOrderLineItem
fields = [
'pk',
fields = AbstractLineItemSerializer.line_fields([
'allocated',
'customer_detail',
'quantity',
'reference',
'notes',
'order',
'order_detail',
'overdue',
'part',
'part_detail',
'sale_price',
'sale_price_currency',
'shipped',
'target_date',
'link',
# Annotated fields for part stocking information
'available_stock',
'available_variant_stock',
'building',
'on_order',
]
# Filterable detail fields
])
@staticmethod
def annotate_queryset(queryset):
@@ -1795,7 +1876,7 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
@register_importer()
class SalesOrderExtraLineSerializer(
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
AbstractExtraLineSerializer, InvenTreeModelSerializer
):
"""Serializer for a SalesOrderExtraLine object."""
@@ -1803,6 +1884,7 @@ class SalesOrderExtraLineSerializer(
"""Metaclass options."""
model = order.models.SalesOrderExtraLine
fields = AbstractExtraLineSerializer.extra_line_fields([])
order_detail = enable_filter(
SalesOrderSerializer(
@@ -1813,7 +1895,6 @@ class SalesOrderExtraLineSerializer(
@register_importer()
class ReturnOrderSerializer(
FilterableSerializerMixin,
NotesFieldMixin,
InvenTreeCustomStatusSerializerMixin,
AbstractOrderSerializer,
@@ -2004,7 +2085,6 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
@register_importer()
class ReturnOrderLineItemSerializer(
FilterableSerializerMixin,
DataImportExportSerializerMixin,
AbstractLineItemSerializer,
InvenTreeModelSerializer,
@@ -2015,24 +2095,16 @@ class ReturnOrderLineItemSerializer(
"""Metaclass options."""
model = order.models.ReturnOrderLineItem
fields = [
'pk',
'order',
'order_detail',
fields = AbstractLineItemSerializer.line_fields([
'item',
'item_detail',
'quantity',
'received_date',
'outcome',
'part_detail',
'price',
'price_currency',
'link',
'reference',
'notes',
'target_date',
'link',
]
# Filterable detail fields
'item_detail',
'part_detail',
])
order_detail = enable_filter(
ReturnOrderSerializer(
@@ -2062,7 +2134,7 @@ class ReturnOrderLineItemSerializer(
@register_importer()
class ReturnOrderExtraLineSerializer(
FilterableSerializerMixin, AbstractExtraLineSerializer, InvenTreeModelSerializer
AbstractExtraLineSerializer, InvenTreeModelSerializer
):
"""Serializer for a ReturnOrderExtraLine object."""
@@ -2070,6 +2142,7 @@ class ReturnOrderExtraLineSerializer(
"""Metaclass options."""
model = order.models.ReturnOrderExtraLine
fields = AbstractExtraLineSerializer.extra_line_fields([])
order_detail = enable_filter(
ReturnOrderSerializer(

View File

@@ -2,6 +2,7 @@ import { IconUsers } from '@tabler/icons-react';
import { useMemo, useState } from 'react';
import type { ApiFormFieldSet } from '@lib/types/Forms';
import { t } from '@lingui/core/macro';
import type {
StatusCodeInterface,
StatusCodeListInterface
@@ -83,6 +84,9 @@ export function extraLineItemFields(): ApiFormFieldSet {
quantity: {},
price: {},
price_currency: {},
project_code: {
description: t`Select project code for this line item`
},
notes: {},
link: {}
};

View File

@@ -121,6 +121,9 @@ export function usePurchaseOrderLineItemFields({
value: autoPricing,
onValueChange: setAutoPricing
},
project_code: {
description: t`Select project code for this line item`
},
target_date: {
icon: <IconCalendar />
},

View File

@@ -134,6 +134,9 @@ export function useReturnOrderLineItemFields({
},
price: {},
price_currency: {},
project_code: {
description: t`Select project code for this line item`
},
target_date: {},
notes: {},
link: {}

View File

@@ -157,6 +157,9 @@ export function useSalesOrderLineItemFields({
value: partCurrency,
onValueChange: setPartCurrency
},
project_code: {
description: t`Select project code for this line item`
},
target_date: {},
notes: {},
link: {}

View File

@@ -25,7 +25,8 @@ import {
DecimalColumn,
DescriptionColumn,
LinkColumn,
NoteColumn
NoteColumn,
ProjectCodeColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -75,6 +76,7 @@ export default function ExtraLineItemTable({
multiplier: record.quantity
})
},
ProjectCodeColumn({}),
NoteColumn({
accessor: 'notes'
}),

View File

@@ -45,6 +45,7 @@ import {
LocationColumn,
NoteColumn,
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
TargetDateColumn
} from '../ColumnRenderers';
@@ -150,6 +151,7 @@ export function PurchaseOrderLineItemTable({
accessor: 'part_detail.description'
}),
ReferenceColumn({}),
ProjectCodeColumn({}),
{
accessor: 'build_order',
title: t`Build Order`,

View File

@@ -34,6 +34,7 @@ import {
LinkColumn,
NoteColumn,
PartColumn,
ProjectCodeColumn,
ReferenceColumn,
StatusColumn
} from '../ColumnRenderers';
@@ -137,6 +138,7 @@ export default function ReturnOrderLineItemTable({
title: t`Status`
}),
ReferenceColumn({}),
ProjectCodeColumn({}),
StatusColumn({
model: ModelType.returnorderlineitem,
sortable: true,

View File

@@ -48,6 +48,7 @@ import {
DecimalColumn,
DescriptionColumn,
LinkColumn,
ProjectCodeColumn,
RenderPartColumn
} from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
@@ -106,6 +107,7 @@ export default function SalesOrderLineItemTable({
sortable: false,
switchable: true
},
ProjectCodeColumn({}),
DecimalColumn({
accessor: 'quantity',
sortable: true