mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 21:25: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:
		| @@ -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 | ||||
|   | ||||
| @@ -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", | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user