mirror of
https://github.com/inventree/InvenTree.git
synced 2026-07-21 22:23:03 +00:00
Add "discount" support to order line items
This commit is contained in:
@@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user