Add "discount" support to order line items

This commit is contained in:
Oliver Walters
2026-07-14 02:32:41 +00:00
parent 2b25a46eaa
commit e5e5d0712c
3 changed files with 154 additions and 6 deletions
@@ -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",
),
),
]
+22 -5
View File
@@ -5,7 +5,7 @@ from typing import Any, Optional, TypedDict
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ValidationError 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 import models, transaction
from django.db.models import F, Q, QuerySet, Sum from django.db.models import F, Q, QuerySet, Sum
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
@@ -157,7 +157,11 @@ class TotalPriceMixin(models.Model):
continue continue
try: 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: except MissingRate:
log_error('order.calculate_total_price') log_error('order.calculate_total_price')
logger.exception("Missing exchange rate for '%s'", target_currency) logger.exception("Missing exchange rate for '%s'", target_currency)
@@ -171,7 +175,11 @@ class TotalPriceMixin(models.Model):
continue continue
try: 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: except MissingRate:
# Record the error, try to press on # Record the error, try to press on
@@ -2109,11 +2117,20 @@ class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
validators=[MinValueValidator(0)], 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 @property
def total_line_price(self): 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: if self.price:
return self.quantity * self.price return self.quantity * self.price * (1 - self.discount / 100)
line = models.CharField( line = models.CharField(
max_length=20, max_length=20,
+13 -1
View File
@@ -305,6 +305,8 @@ class AbstractLineItemSerializer(FilterableSerializerMixin, serializers.Serializ
required=False, allow_null=True, label=_('Target Date') required=False, allow_null=True, label=_('Target Date')
) )
discount = InvenTreeDecimalField(required=False)
project_code_label = common.filters.enable_project_label_filter() project_code_label = common.filters.enable_project_label_filter()
project_code_detail = common.filters.enable_project_code_filter() project_code_detail = common.filters.enable_project_code_filter()
@@ -322,6 +324,7 @@ class AbstractExtraLineSerializer(
'pk', 'pk',
'line', 'line',
'description', 'description',
'discount',
'link', 'link',
'notes', 'notes',
'order', 'order',
@@ -340,6 +343,8 @@ class AbstractExtraLineSerializer(
quantity = serializers.FloatField() quantity = serializers.FloatField()
discount = InvenTreeDecimalField(required=False)
price = InvenTreeMoneySerializer(allow_null=True) price = InvenTreeMoneySerializer(allow_null=True)
price_currency = InvenTreeCurrencySerializer() price_currency = InvenTreeCurrencySerializer()
@@ -355,6 +360,7 @@ class AbstractExtraLineMeta:
fields = [ fields = [
'pk', 'pk',
'description', 'description',
'discount',
'quantity', 'quantity',
'reference', 'reference',
'notes', 'notes',
@@ -558,6 +564,7 @@ class PurchaseOrderLineItemSerializer(
fields = AbstractLineItemSerializer.line_fields([ fields = AbstractLineItemSerializer.line_fields([
'part', 'part',
'build_order', 'build_order',
'discount',
'overdue', 'overdue',
'received', 'received',
'purchase_price', 'purchase_price',
@@ -604,7 +611,10 @@ class PurchaseOrderLineItemSerializer(
queryset = queryset.annotate( queryset = queryset.annotate(
total_price=ExpressionWrapper( total_price=ExpressionWrapper(
F('purchase_price') * F('quantity'), output_field=models.DecimalField() F('purchase_price')
* F('quantity')
* (1 - F('discount') / Decimal(100)),
output_field=models.DecimalField(),
) )
) )
@@ -1208,6 +1218,7 @@ class SalesOrderLineItemSerializer(
fields = AbstractLineItemSerializer.line_fields([ fields = AbstractLineItemSerializer.line_fields([
'allocated', 'allocated',
'customer_detail', 'customer_detail',
'discount',
'overdue', 'overdue',
'part', 'part',
'part_detail', 'part_detail',
@@ -2305,6 +2316,7 @@ class ReturnOrderLineItemSerializer(
'item', 'item',
'received_date', 'received_date',
'outcome', 'outcome',
'discount',
'price', 'price',
'price_currency', 'price_currency',
# Filterable detail fields # Filterable detail fields