diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ae546cd20..4e93954c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [#12393](https://github.com/inventree/InvenTree/pull/12393) adds "discount" attribute to order line items, allowing users to specify a discount for each line item on an order. The discount can be specified as either a percentage or a fixed amount, and is applied to the line item total when calculating the order total. - [#12391](https://github.com/inventree/InvenTree/pull/12391) adds facility for bulk deleting line items against orders - [#12388](https://github.com/inventree/InvenTree/pull/12388) adds uniqueness requirements options for the Parameter and ParameterTemplate models. This allows users to specify whether a parameter value should be unique for a given model type, or globally unique across all models. - [#12310](https://github.com/inventree/InvenTree/pull/12310) adds the ability to disassemble (or break apart) assembled stock items into their component parts, based on the Bill of Materials (BOM) associated with the stock item. This allows users to easily break down assembled items into their constituent parts, which can be useful for inventory management and tracking purposes. diff --git a/docs/docs/concepts/pricing.md b/docs/docs/concepts/pricing.md index 8e1e5a5345..8d08c2ece5 100644 --- a/docs/docs/concepts/pricing.md +++ b/docs/docs/concepts/pricing.md @@ -23,6 +23,36 @@ Throughout this documentation (and within InvenTree) the concepts of *cost* and | Price | The theoretical amount of money required to pay for something. | | Cost | The actual amount of money paid. | +## Line Items + +Orders (Purchase Orders, Sales Orders, and Return Orders) are made up of *line items*, each linking a *Quantity* to a *Unit Price*. A line item's *Line Total* is calculated as follows: + +``` +Line Total = Quantity * Unit Price +``` + +An order's overall *Total Price* is calculated by summing the *Line Total* of every line item and [extra line item](#extra-line-items) associated with the order. + +### Extra Line Items + +*Extra Line Items* provide a way to add itemized costs to an order which are not tied to a specific part or stock item - for example freight charges, service fees, or other miscellaneous costs. Extra line items support the same *Quantity* and *Unit Price* fields as regular line items, and are included in the order's *Total Price* calculation. + +### Line Item Discount + +Line items - and their associated [extra line items](#extra-line-items) - support an optional *Discount* field, expressed as a percentage between 0% and 100%. This is available on: + +- [Purchase Order](../purchasing/purchase_order.md#add-line-items) line items and [extra line items](../purchasing/purchase_order.md#extra-line-items) +- [Sales Order](../sales/sales_order.md#add-line-items) line items and [extra line items](../sales/sales_order.md#extra-line-items) +- [Return Order](../sales/return_order.md#line-items) line items and [extra line items](../sales/return_order.md#extra-line-items) + +If specified, the discount is applied to the *Line Total*, using the following formula: + +``` +Line Total = Quantity * Unit Price * (1 - Discount / 100) +``` + +!!! info "Optional" + The discount percentage is optional, and defaults to 0% (no discount) if not specified. ## Currency Support diff --git a/docs/docs/purchasing/purchase_order.md b/docs/docs/purchasing/purchase_order.md index fd2cfacb77..368377e3b9 100644 --- a/docs/docs/purchasing/purchase_order.md +++ b/docs/docs/purchasing/purchase_order.md @@ -92,6 +92,12 @@ Once the "Add Line Item" form opens, select a supplier part in the list. Fill out the rest of the form then click on Submit +!!! info "Auto Pricing" + Enable the *Auto Pricing* option to automatically calculate the line item's *Unit Price* from the [supplier part pricing data](../part/pricing.md#supplier-pricing), based on the line item quantity. While enabled, the *Unit Price* field is calculated automatically and cannot be edited manually. This option is disabled by default. + +!!! info "Discount" + An optional [discount](../concepts/pricing.md#line-item-discount) percentage can be applied to each line item. + #### Upload File It is possible to upload an exported purchase order from the supplier instead of manually entering each line item. To start the process, click on {{ icon("upload") }} Upload File button next to the {{ icon("plus-circle") }} Add Line Item button and follow the steps. @@ -99,6 +105,10 @@ It is possible to upload an exported purchase order from the supplier instead of !!! info "Supported Formats" This process only supports tabular data and the following formats are supported: CSV, TSV, XLS, XLSX, JSON and YAML +### Extra Line Items + +While [line items](#add-line-items) must reference a particular supplier part, extra line items are available for any other itemized information that needs to be conveyed with the order - for example freight charges or service fees. Extra line items support an optional [discount](../concepts/pricing.md#line-item-discount) percentage, the same as regular line items. + ## Issue Order Once all the line items were added, click on the {{ icon("send", title="Issue") }} button on the main purchase order detail panel and confirm the order has been submitted. diff --git a/docs/docs/sales/return_order.md b/docs/docs/sales/return_order.md index d57dcc562a..673f2c9d96 100644 --- a/docs/docs/sales/return_order.md +++ b/docs/docs/sales/return_order.md @@ -117,9 +117,12 @@ Return Order line items can be added while the [status](#return-order-status-cod !!! info "Serialized Stock Only" Only stock items which are serialized can be selected for return from the customer +!!! info "Discount" + An optional [discount](../concepts/pricing.md#line-item-discount) percentage can be applied to each line item. + ### Extra Line Items -While [line items](#line-items) must reference a particular stock item, extra line items are available for any other itemized information that needs to be conveyed with the order. +While [line items](#line-items) must reference a particular stock item, extra line items are available for any other itemized information that needs to be conveyed with the order - for example freight charges or service fees. Extra line items support an optional [discount](../concepts/pricing.md#line-item-discount) percentage, the same as regular line items. ## Return Order Reports diff --git a/docs/docs/sales/sales_order.md b/docs/docs/sales/sales_order.md index 239651b753..bfb3a7b9db 100644 --- a/docs/docs/sales/sales_order.md +++ b/docs/docs/sales/sales_order.md @@ -90,6 +90,13 @@ Once the "Add Line Item" form opens, select a part in the list. Fill out the rest of the form then click on Submit +!!! info "Discount" + An optional [discount](../concepts/pricing.md#line-item-discount) percentage can be applied to each line item. + +### Extra Line Items + +While [line items](#add-line-items) must reference a particular part, extra line items are available for any other itemized information that needs to be conveyed with the order - for example freight charges or service fees. Extra line items support an optional [discount](../concepts/pricing.md#line-item-discount) percentage, the same as regular line items. + ## Shipments After all line items were added to the sales order, user needs to create one or more [shipments](#sales-order-shipments) in order to allocate stock for those parts. diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index f4766c4d9c..4d19917bb2 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 523 +INVENTREE_API_VERSION = 524 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v524 -> 2026-07-20 : https://github.com/inventree/InvenTree/pull/12393 + - Adds "discount" field to order line items (and extra line items) + v523 -> 2026-07-14 : https://github.com/inventree/InvenTree/pull/12391 - Adds "bulk delete" support for order line item API endpoints (PurchaseOrder / SalesOrder / ReturnOrder / TransferOrder) - Adds "bulk delete" support for order extra line item API endpoints diff --git a/src/backend/InvenTree/generic/states/fields.py b/src/backend/InvenTree/generic/states/fields.py index 0ca677b4ab..9ce4f24069 100644 --- a/src/backend/InvenTree/generic/states/fields.py +++ b/src/backend/InvenTree/generic/states/fields.py @@ -3,6 +3,7 @@ from collections.abc import Iterable from typing import Any, Optional +from django.apps import apps as global_apps from django.core.exceptions import ObjectDoesNotExist from django.db import models from django.utils.encoding import force_str @@ -117,11 +118,23 @@ class InvenTreeCustomStatusModelField(models.PositiveIntegerField): return name, path, args, kwargs def contribute_to_class(self, cls, name): - """Add the _custom_key field to the model.""" + """Add the _custom_key field to the model. + + Only auto-add the companion field for the 'live' app registry. Any other + registry (migration state, or the throwaway model registries that some + backends - e.g. SQLite's table-rebuild routine - construct internally) + either already has the field explicitly declared, or will get it via a + separate migration operation. Auto-adding it there too would create a + duplicate field on the model. + """ cls._meta.supports_custom_status = True - if not hasattr(self, '_custom_key_field') and not hasattr( - cls, f'{name}_custom_key' + is_live_model = cls._meta.apps is global_apps + + if ( + is_live_model + and not hasattr(self, '_custom_key_field') + and not hasattr(cls, f'{name}_custom_key') ): self.add_field(cls, name) diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index a5e66434cd..634a31198a 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -744,7 +744,6 @@ class PurchaseOrderLineItemList( 'reference', 'SKU', 'IPN', - 'total_price', 'target_date', 'order', 'status', diff --git a/src/backend/InvenTree/order/migrations/0121_add_line_item_discount.py b/src/backend/InvenTree/order/migrations/0121_add_line_item_discount.py new file mode 100644 index 0000000000..f5f9371e75 --- /dev/null +++ b/src/backend/InvenTree/order/migrations/0121_add_line_item_discount.py @@ -0,0 +1,119 @@ +# Generated by Django 5.2.15 on 2026-07-14 02:18 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("order", "0120_purchaseorder_tags_returnorder_tags_salesorder_tags_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="purchaseorderextraline", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="purchaseorderlineitem", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="returnorderextraline", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="returnorderlineitem", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="salesorderextraline", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="salesorderlineitem", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + migrations.AddField( + model_name="transferorderlineitem", + name="discount", + field=models.DecimalField( + decimal_places=2, + default=0, + help_text="Discount percentage applied to this line item (0-100)", + max_digits=5, + validators=[ + django.core.validators.MinValueValidator(0), + django.core.validators.MaxValueValidator(100), + ], + verbose_name="Discount", + ), + ), + ] diff --git a/src/backend/InvenTree/order/models.py b/src/backend/InvenTree/order/models.py index f1501e0a73..eeae120d81 100644 --- a/src/backend/InvenTree/order/models.py +++ b/src/backend/InvenTree/order/models.py @@ -5,7 +5,7 @@ from typing import Any, Optional, TypedDict from django.contrib.auth.models import User from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.db.models import F, Q, QuerySet, Sum from django.db.models.functions import Coalesce @@ -157,7 +157,11 @@ class TotalPriceMixin(models.Model): continue try: - total += line.quantity * convert_money(line.price, target_currency) + total += ( + line.quantity + * convert_money(line.price, target_currency) + * (1 - line.discount / 100) + ) except MissingRate: log_error('order.calculate_total_price') logger.exception("Missing exchange rate for '%s'", target_currency) @@ -171,7 +175,11 @@ class TotalPriceMixin(models.Model): continue try: - total += line.quantity * convert_money(line.price, target_currency) + total += ( + line.quantity + * convert_money(line.price, target_currency) + * (1 - line.discount / 100) + ) except MissingRate: # Record the error, try to press on @@ -2109,11 +2117,20 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel): validators=[MinValueValidator(0)], ) + discount = models.DecimalField( + verbose_name=_('Discount'), + help_text=_('Discount percentage applied to this line item (0-100)'), + default=0, + max_digits=5, + decimal_places=2, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + @property def total_line_price(self): - """Return the total price for this line item.""" + """Return the total price for this line item, after any discount is applied.""" if self.price: - return self.quantity * self.price + return self.quantity * self.price * (1 - self.discount / 100) line = models.CharField( max_length=20, diff --git a/src/backend/InvenTree/order/serializers.py b/src/backend/InvenTree/order/serializers.py index 9fcabc1cff..e5d7f0ac0a 100644 --- a/src/backend/InvenTree/order/serializers.py +++ b/src/backend/InvenTree/order/serializers.py @@ -305,6 +305,8 @@ class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializ required=False, allow_null=True, label=_('Target Date') ) + discount = InvenTreeDecimalField(required=False) + project_code_label = common.filters.enable_project_label_filter() project_code_detail = common.filters.enable_project_code_filter() @@ -322,6 +324,7 @@ class AbstractExtraLineSerializer( 'pk', 'line', 'description', + 'discount', 'link', 'notes', 'order', @@ -331,6 +334,7 @@ class AbstractExtraLineSerializer( 'quantity', 'reference', 'target_date', + 'total_price', # Filterable detail fields 'order_detail', 'project_code_label', @@ -340,10 +344,16 @@ class AbstractExtraLineSerializer( quantity = serializers.FloatField() + discount = InvenTreeDecimalField(required=False) + price = InvenTreeMoneySerializer(allow_null=True) price_currency = InvenTreeCurrencySerializer() + total_price = InvenTreeMoneySerializer( + source='total_line_price', allow_null=True, read_only=True + ) + project_code_label = common.filters.enable_project_label_filter() project_code_detail = common.filters.enable_project_code_filter() @@ -355,6 +365,7 @@ class AbstractExtraLineMeta: fields = [ 'pk', 'description', + 'discount', 'quantity', 'reference', 'notes', @@ -363,6 +374,7 @@ class AbstractExtraLineMeta: 'order_detail', 'price', 'price_currency', + 'total_price', 'link', ] @@ -558,6 +570,7 @@ class PurchaseOrderLineItemSerializer( fields = AbstractLineItemSerializer.line_fields([ 'part', 'build_order', + 'discount', 'overdue', 'received', 'purchase_price', @@ -586,7 +599,6 @@ class PurchaseOrderLineItemSerializer( def annotate_queryset(queryset): """Add some extra annotations to this queryset. - - "total_price" = purchase_price * quantity - "overdue" status (boolean field) """ queryset = queryset.prefetch_related( @@ -602,12 +614,6 @@ class PurchaseOrderLineItemSerializer( 'part__manufacturer_part__manufacturer', ) - queryset = queryset.annotate( - total_price=ExpressionWrapper( - F('purchase_price') * F('quantity'), output_field=models.DecimalField() - ) - ) - queryset = queryset.annotate( overdue=Case( When( @@ -648,7 +654,9 @@ class PurchaseOrderLineItemSerializer( overdue = serializers.BooleanField(read_only=True, allow_null=True) - total_price = serializers.FloatField(read_only=True) + total_price = InvenTreeMoneySerializer( + source='total_line_price', allow_null=True, read_only=True + ) part_detail = OptionalField( serializer_class=PartBriefSerializer, @@ -1234,6 +1242,7 @@ class SalesOrderLineItemSerializer( fields = AbstractLineItemSerializer.line_fields([ 'allocated', 'customer_detail', + 'discount', 'overdue', 'part', 'part_detail', @@ -2331,6 +2340,7 @@ class ReturnOrderLineItemSerializer( 'item', 'received_date', 'outcome', + 'discount', 'price', 'price_currency', # Filterable detail fields diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index 7e39a92939..60803b182e 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -3579,6 +3579,79 @@ class ReturnOrderLineItemTests(InvenTreeAPITestCase): self.assertEqual(models.ReturnOrderExtraLine.objects.count(), n - 2) +class ExtraLineTotalPriceTest(InvenTreeAPITestCase): + """Unit tests for the 'total_price' field on ExtraLine API endpoints. + + Covers PurchaseOrderExtraLine, SalesOrderExtraLine and ReturnOrderExtraLine, + which all share the same 'total_price' field via AbstractExtraLineSerializer. + """ + + fixtures = [ + 'category', + 'part', + 'company', + 'location', + 'supplier_part', + 'stock', + 'order', + 'sales_order', + 'return_order', + ] + + roles = ['purchase_order.change', 'sales_order.change', 'return_order.change'] + + def check_total_price( + self, order, extra_line_model, list_url_name, detail_url_name + ): + """Create an ExtraLine with a known price/quantity/discount, and check total_price.""" + line = extra_line_model.objects.create( + order=order, quantity=5, price=Money(10, 'USD'), discount=20 + ) + + # 5 * 10 = 50, less 20% discount = 40 + expected = 40 + + # List endpoint + response = self.get( + reverse(list_url_name), {'order': order.pk}, expected_code=200 + ) + result = next(r for r in response.data if r['pk'] == line.pk) + self.assertEqual(float(result['total_price']), expected) + + # Detail endpoint + response = self.get( + reverse(detail_url_name, kwargs={'pk': line.pk}), expected_code=200 + ) + self.assertEqual(float(response.data['total_price']), expected) + + def test_po_extra_line_total_price(self): + """Check 'total_price' for a PurchaseOrderExtraLine.""" + self.check_total_price( + models.PurchaseOrder.objects.get(pk=1), + models.PurchaseOrderExtraLine, + 'api-po-extra-line-list', + 'api-po-extra-line-detail', + ) + + def test_so_extra_line_total_price(self): + """Check 'total_price' for a SalesOrderExtraLine.""" + self.check_total_price( + models.SalesOrder.objects.get(pk=1), + models.SalesOrderExtraLine, + 'api-so-extra-line-list', + 'api-so-extra-line-detail', + ) + + def test_return_order_extra_line_total_price(self): + """Check 'total_price' for a ReturnOrderExtraLine.""" + self.check_total_price( + models.ReturnOrder.objects.get(pk=1), + models.ReturnOrderExtraLine, + 'api-return-order-extra-line-list', + 'api-return-order-extra-line-detail', + ) + + class TransferOrderTest(OrderTest): """Tests for the TransferOrder API.""" diff --git a/src/frontend/src/components/tables/ColumnRenderers.tsx b/src/frontend/src/components/tables/ColumnRenderers.tsx index fb32383a68..c57d061e45 100644 --- a/src/frontend/src/components/tables/ColumnRenderers.tsx +++ b/src/frontend/src/components/tables/ColumnRenderers.tsx @@ -473,6 +473,22 @@ export function DecimalColumn(props: TableColumn): TableColumn { }; } +export function PercentageColumn(props: TableColumn): TableColumn { + return { + sortable: true, + render: (record: any) => { + const value = resolveItem(record, props.accessor ?? ''); + + if (value == null || value === 0) { + return '-'; + } + + return `${formatDecimal(value)}%`; + }, + ...props + }; +} + export function DescriptionColumn(props: TableColumnProps): TableColumn { return { accessor: 'description', diff --git a/src/frontend/src/forms/CommonForms.tsx b/src/frontend/src/forms/CommonForms.tsx index b062b2a7ba..1a646cf051 100644 --- a/src/frontend/src/forms/CommonForms.tsx +++ b/src/frontend/src/forms/CommonForms.tsx @@ -91,6 +91,7 @@ export function extraLineItemFields(): ApiFormFieldSet { quantity: {}, price: {}, price_currency: {}, + discount: {}, project_code: ProjectCodeField(), notes: {}, link: {} diff --git a/src/frontend/src/forms/PurchaseOrderForms.tsx b/src/frontend/src/forms/PurchaseOrderForms.tsx index 4776e668eb..cdf37160df 100644 --- a/src/frontend/src/forms/PurchaseOrderForms.tsx +++ b/src/frontend/src/forms/PurchaseOrderForms.tsx @@ -186,6 +186,7 @@ export function usePurchaseOrderLineItemFields({ value: purchasePriceCurrency, onValueChange: setPurchasePriceCurrency }, + discount: {}, auto_pricing: { default: create !== false, value: autoPricing, diff --git a/src/frontend/src/forms/ReturnOrderForms.tsx b/src/frontend/src/forms/ReturnOrderForms.tsx index 17bbaac6fd..f268b28d7a 100644 --- a/src/frontend/src/forms/ReturnOrderForms.tsx +++ b/src/frontend/src/forms/ReturnOrderForms.tsx @@ -133,6 +133,7 @@ export function useReturnOrderLineItemFields({ }, price: {}, price_currency: {}, + discount: {}, project_code: ProjectCodeField(), target_date: {}, notes: {}, diff --git a/src/frontend/src/forms/SalesOrderForms.tsx b/src/frontend/src/forms/SalesOrderForms.tsx index 40bb783133..6e9c955c56 100644 --- a/src/frontend/src/forms/SalesOrderForms.tsx +++ b/src/frontend/src/forms/SalesOrderForms.tsx @@ -197,6 +197,7 @@ export function useSalesOrderLineItemFields({ value: partCurrency, onValueChange: setPartCurrency }, + discount: {}, project_code: ProjectCodeField(), target_date: {}, notes: {}, diff --git a/src/frontend/src/tables/general/ExtraLineItemTable.tsx b/src/frontend/src/tables/general/ExtraLineItemTable.tsx index 43b5b25d72..be109d07d8 100644 --- a/src/frontend/src/tables/general/ExtraLineItemTable.tsx +++ b/src/frontend/src/tables/general/ExtraLineItemTable.tsx @@ -19,6 +19,7 @@ import { LineItemColumn, LinkColumn, NoteColumn, + PercentageColumn, ProjectCodeColumn } from '../../components/tables/ColumnRenderers'; import { InvenTreeTable } from '../../components/tables/InvenTreeTable'; @@ -69,13 +70,17 @@ export default function ExtraLineItemTable({ currency: record.price_currency }) }, + PercentageColumn({ + accessor: 'discount', + title: t`Discount`, + defaultVisible: false + }), { accessor: 'total_price', title: t`Total Price`, render: (record: any) => - formatCurrency(record.price, { - currency: record.price_currency, - multiplier: record.quantity + formatCurrency(record.total_price, { + currency: record.price_currency }) }, ProjectCodeColumn({}), diff --git a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx index 335605c473..fa50766fa9 100644 --- a/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx +++ b/src/frontend/src/tables/purchasing/PurchaseOrderLineItemTable.tsx @@ -30,6 +30,7 @@ import { LocationColumn, NoteColumn, PartColumn, + PercentageColumn, ProjectCodeColumn, ReferenceColumn, TargetDateColumn @@ -254,13 +255,17 @@ export function PurchaseOrderLineItemTable({ accessor: 'purchase_price', title: t`Unit Price` }), + PercentageColumn({ + accessor: 'discount', + title: t`Discount`, + defaultVisible: false + }), { accessor: 'total_price', title: t`Total Price`, render: (record: any) => - formatCurrency(record.purchase_price, { - currency: record.purchase_price_currency, - multiplier: record.quantity + formatCurrency(record.total_price, { + currency: record.purchase_price_currency }) }, TargetDateColumn({}), diff --git a/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx index fc847ae775..2f533067f3 100644 --- a/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/ReturnOrderLineItemTable.tsx @@ -23,6 +23,7 @@ import { LinkColumn, NoteColumn, PartColumn, + PercentageColumn, ProjectCodeColumn, ReferenceColumn, StatusColumn, @@ -148,6 +149,11 @@ export default function ReturnOrderLineItemTable({ render: (record: any) => formatCurrency(record.price, { currency: record.price_currency }) }, + PercentageColumn({ + accessor: 'discount', + title: t`Discount`, + defaultVisible: false + }), DateColumn({ accessor: 'target_date', title: t`Target Date` diff --git a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx index 6ebf3e89b5..1f226cc73e 100644 --- a/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx +++ b/src/frontend/src/tables/sales/SalesOrderLineItemTable.tsx @@ -36,6 +36,7 @@ import { IPNColumn, LineItemColumn, LinkColumn, + PercentageColumn, ProjectCodeColumn, ReferenceColumn, RenderPartColumn, @@ -121,13 +122,18 @@ export default function SalesOrderLineItemTable({ currency: record.sale_price_currency }) }, + PercentageColumn({ + accessor: 'discount', + title: t`Discount`, + defaultVisible: false + }), { accessor: 'total_price', title: t`Total Price`, render: (record: any) => formatCurrency(record.sale_price, { currency: record.sale_price_currency, - multiplier: record.quantity + multiplier: record.quantity * (1 - (record.discount ?? 0) / 100) }) }, DateColumn({ diff --git a/src/frontend/tests/pui_login.spec.ts b/src/frontend/tests/pui_login.spec.ts index 66598a94c5..aacd04b9be 100644 --- a/src/frontend/tests/pui_login.spec.ts +++ b/src/frontend/tests/pui_login.spec.ts @@ -157,7 +157,7 @@ test('Login - Cold vs Warm vs Hot Load', async ({ page }) => { // Note: Vite server in dev mode is significantly slower than production build const COLD_MS_THRESHOLD: number = 5000; const WARM_MS_THRESHOLD: number = 4000; - const HOT_MS_THRESHOLD: number = 3000; + const HOT_MS_THRESHOLD: number = 3500; const coldMs = await loginAndMeasure(page); diff --git a/tasks.py b/tasks.py index 6e086e2c30..1e99fdbbb5 100644 --- a/tasks.py +++ b/tasks.py @@ -1896,6 +1896,7 @@ def setup_test( c, filename=template_dir.joinpath('inventree_data.json'), clear=True, + ignore_nonexistent=True, verbose=verbose, )