2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-10-29 12:27:41 +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

@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow stock adjustments for "in production" items in [#10600](https://github.com/inventree/InvenTree/pull/10600)
- Adds optional shipping address against individual sales order shipments in [#10650](https://github.com/inventree/InvenTree/pull/10650)
- Adds UI elements to "check" and "uncheck" sales order shipments in [#10654](https://github.com/inventree/InvenTree/pull/10654)
- Allow assigning project codes to order line items in [#10657](https://github.com/inventree/InvenTree/pull/10657)
### Changed

View File

@@ -6,7 +6,18 @@ title: Project Codes
A project code is a unique identifier assigned to a specific project, which helps in tracking and organizing project-related activities and resources. It enables easy retrieval of project-related data and facilitates project management and reporting.
Individual orders (such as [Purchase Orders](../purchasing/purchase_order.md) or [Sales Orders](../sales/sales_order.md)) can be assigned a *Project Code* to allocate the order against a particular internal project.
### Assigning to Orders
Project codes can be assigned to various orders within the system:
- [Build Orders](../manufacturing/build.md)
- [Purchase Orders](../purchasing/purchase_order.md)
- [Sales Orders](../sales/sales_order.md)
- [Return Orders](../sales/return_order.md)
By assigning a project code to an order, users can easily track and manage orders associated with specific projects, enhancing project oversight and resource allocation.
For orders with external companies, which support individual line items, project codes can be assigned at the line item level, allowing for granular tracking of project-related activities. In such cases, the project code assigned to the order itself serves as a default for all line items, unless explicitly overridden at the line item level.
### Managing Project Codes

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