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.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,
+13 -1
View File
@@ -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',
@@ -340,6 +343,8 @@ class AbstractExtraLineSerializer(
quantity = serializers.FloatField()
discount = InvenTreeDecimalField(required=False)
price = InvenTreeMoneySerializer(allow_null=True)
price_currency = InvenTreeCurrencySerializer()
@@ -355,6 +360,7 @@ class AbstractExtraLineMeta:
fields = [
'pk',
'description',
'discount',
'quantity',
'reference',
'notes',
@@ -558,6 +564,7 @@ class PurchaseOrderLineItemSerializer(
fields = AbstractLineItemSerializer.line_fields([
'part',
'build_order',
'discount',
'overdue',
'received',
'purchase_price',
@@ -604,7 +611,10 @@ class PurchaseOrderLineItemSerializer(
queryset = queryset.annotate(
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([
'allocated',
'customer_detail',
'discount',
'overdue',
'part',
'part_detail',
@@ -2305,6 +2316,7 @@ class ReturnOrderLineItemSerializer(
'item',
'received_date',
'outcome',
'discount',
'price',
'price_currency',
# Filterable detail fields