InvenTree/InvenTree/order/serializers.py

855 lines
23 KiB
Python
Raw Normal View History

"""
JSON serializers for the Order API
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
2020-04-19 23:41:21 +00:00
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db import models, transaction
from django.db.models import Case, When, Value
2021-08-25 22:48:19 +00:00
from django.db.models import BooleanField, ExpressionWrapper, F
from rest_framework import serializers
from rest_framework.serializers import ValidationError
from sql_util.utils import SubqueryCount
from common.settings import currency_code_mappings
from company.serializers import CompanyBriefSerializer, SupplierPartSerializer
from InvenTree.serializers import InvenTreeAttachmentSerializer
from InvenTree.helpers import normalize
from InvenTree.serializers import InvenTreeModelSerializer
2021-11-18 12:01:19 +00:00
from InvenTree.serializers import InvenTreeDecimalField
from InvenTree.serializers import InvenTreeMoneySerializer
from InvenTree.serializers import ReferenceIndexingSerializerMixin
2021-12-02 13:08:05 +00:00
from InvenTree.status_codes import StockStatus, SalesOrderStatus
2021-08-23 22:49:23 +00:00
import order.models
from part.serializers import PartBriefSerializer
import stock.models
import stock.serializers
2021-07-03 02:45:59 +00:00
from users.serializers import OwnerSerializer
2021-07-03 02:45:59 +00:00
class POSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
""" Serializer for a PurchaseOrder object """
2020-04-19 23:41:21 +00:00
def __init__(self, *args, **kwargs):
supplier_detail = kwargs.pop('supplier_detail', False)
super().__init__(*args, **kwargs)
if supplier_detail is not True:
self.fields.pop('supplier_detail')
@staticmethod
def annotate_queryset(queryset):
"""
Add extra information to the queryset
- Number of lines in the PurchaseOrder
- Overdue status of the PurchaseOrder
2020-04-19 23:41:21 +00:00
"""
2020-09-05 13:08:59 +00:00
queryset = queryset.annotate(
line_items=SubqueryCount('lines')
2020-04-19 23:41:21 +00:00
)
queryset = queryset.annotate(
overdue=Case(
When(
order.models.PurchaseOrder.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField())
)
)
2020-09-05 13:08:59 +00:00
return queryset
2020-04-19 23:41:21 +00:00
supplier_detail = CompanyBriefSerializer(source='supplier', many=False, read_only=True)
2020-04-19 23:41:21 +00:00
line_items = serializers.IntegerField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
overdue = serializers.BooleanField(required=False, read_only=True)
reference = serializers.CharField(required=True)
responsible_detail = OwnerSerializer(source='responsible', read_only=True, many=False)
class Meta:
model = order.models.PurchaseOrder
fields = [
'pk',
2020-04-19 23:48:33 +00:00
'issue_date',
'complete_date',
'creation_date',
'description',
2020-04-19 23:41:21 +00:00
'line_items',
2020-04-06 01:56:52 +00:00
'link',
'overdue',
2020-04-19 23:41:21 +00:00
'reference',
'responsible',
'responsible_detail',
2020-04-19 23:41:21 +00:00
'supplier',
'supplier_detail',
'supplier_reference',
'status',
2020-04-19 23:41:21 +00:00
'status_text',
'target_date',
'notes',
]
read_only_fields = [
'status'
'issue_date',
'complete_date',
'creation_date',
]
class POLineItemSerializer(InvenTreeModelSerializer):
2021-08-25 22:48:19 +00:00
@staticmethod
def annotate_queryset(queryset):
"""
Add some extra annotations to this queryset:
- Total price = purchase_price * quantity
"""
queryset = queryset.annotate(
total_price=ExpressionWrapper(
F('purchase_price') * F('quantity'),
output_field=models.DecimalField()
)
)
return queryset
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
super().__init__(*args, **kwargs)
if part_detail is not True:
self.fields.pop('part_detail')
self.fields.pop('supplier_part_detail')
quantity = serializers.FloatField(default=1)
received = serializers.FloatField(default=0)
2021-08-25 22:48:19 +00:00
total_price = serializers.FloatField(read_only=True)
part_detail = PartBriefSerializer(source='get_base_part', many=False, read_only=True)
supplier_part_detail = SupplierPartSerializer(source='part', many=False, read_only=True)
purchase_price = InvenTreeMoneySerializer(
allow_null=True
)
purchase_price_string = serializers.CharField(source='purchase_price', read_only=True)
destination_detail = stock.serializers.LocationBriefSerializer(source='get_destination', read_only=True)
purchase_price_currency = serializers.ChoiceField(
2021-07-03 02:45:59 +00:00
choices=currency_code_mappings(),
help_text=_('Purchase price currency'),
)
class Meta:
model = order.models.PurchaseOrderLineItem
fields = [
'pk',
'quantity',
'reference',
'notes',
'order',
'part',
'part_detail',
'supplier_part_detail',
'received',
'purchase_price',
'purchase_price_currency',
'purchase_price_string',
'destination',
'destination_detail',
2021-08-25 22:48:19 +00:00
'total_price',
]
class POLineItemReceiveSerializer(serializers.Serializer):
"""
A serializer for receiving a single purchase order line item against a purchase order
"""
line_item = serializers.PrimaryKeyRelatedField(
queryset=order.models.PurchaseOrderLineItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Line Item'),
)
def validate_line_item(self, item):
if item.order != self.context['order']:
raise ValidationError(_('Line item does not match purchase order'))
return item
location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(),
many=False,
allow_null=True,
required=False,
label=_('Location'),
help_text=_('Select destination location for received items'),
)
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True,
)
def validate_quantity(self, quantity):
if quantity <= 0:
raise ValidationError(_("Quantity must be greater than zero"))
return quantity
2021-08-23 22:49:23 +00:00
status = serializers.ChoiceField(
2021-08-24 12:03:52 +00:00
choices=list(StockStatus.items()),
2021-08-23 22:49:23 +00:00
default=StockStatus.OK,
label=_('Status'),
)
barcode = serializers.CharField(
label=_('Barcode Hash'),
help_text=_('Unique identifier field'),
2021-09-07 13:34:14 +00:00
default='',
required=False,
allow_null=True,
allow_blank=True,
)
def validate_barcode(self, barcode):
"""
Cannot check in a LineItem with a barcode that is already assigned
"""
# Ignore empty barcode values
if not barcode or barcode.strip() == '':
return None
if stock.models.StockItem.objects.filter(uid=barcode).exists():
raise ValidationError(_('Barcode is already in use'))
2021-09-07 13:34:14 +00:00
return barcode
class Meta:
fields = [
'barcode',
'line_item',
'location',
'quantity',
2021-08-23 22:49:23 +00:00
'status',
]
class POReceiveSerializer(serializers.Serializer):
"""
Serializer for receiving items against a purchase order
"""
items = POLineItemReceiveSerializer(many=True)
location = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockLocation.objects.all(),
many=False,
allow_null=True,
label=_('Location'),
help_text=_('Select destination location for received items'),
)
2021-10-04 13:45:49 +00:00
def validate(self, data):
super().validate(data)
items = data.get('items', [])
location = data.get('location', None)
if len(items) == 0:
raise ValidationError(_('Line items must be provided'))
# Check if the location is not specified for any particular item
for item in items:
line = item['line_item']
if not item.get('location', None):
# If a global location is specified, use that
item['location'] = location
if not item['location']:
# The line item specifies a location?
item['location'] = line.get_destination()
if not item['location']:
raise ValidationError({
'location': _("Destination location must be specified"),
})
# Ensure barcodes are unique
unique_barcodes = set()
for item in items:
barcode = item.get('barcode', '')
if barcode:
if barcode in unique_barcodes:
raise ValidationError(_('Supplied barcode values must be unique'))
else:
unique_barcodes.add(barcode)
return data
def save(self):
2021-10-05 05:10:00 +00:00
"""
Perform the actual database transaction to receive purchase order items
"""
data = self.validated_data
request = self.context['request']
order = self.context['order']
items = data['items']
location = data.get('location', None)
# Now we can actually receive the items into stock
with transaction.atomic():
for item in items:
2021-10-05 05:10:00 +00:00
# Select location
loc = item.get('location', None) or item['line_item'].get_destination() or location
try:
order.receive_line_item(
item['line_item'],
2021-10-05 05:10:00 +00:00
loc,
item['quantity'],
request.user,
status=item['status'],
barcode=item.get('barcode', ''),
)
except (ValidationError, DjangoValidationError) as exc:
# Catch model errors and re-throw as DRF errors
raise ValidationError(detail=serializers.as_serializer_error(exc))
class Meta:
fields = [
'items',
'location',
]
class POAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializers for the PurchaseOrderAttachment model
"""
class Meta:
model = order.models.PurchaseOrderAttachment
fields = [
'pk',
'order',
'attachment',
'link',
'filename',
'comment',
2021-06-30 07:44:23 +00:00
'upload_date',
]
read_only_fields = [
'upload_date',
]
class SalesOrderSerializer(ReferenceIndexingSerializerMixin, InvenTreeModelSerializer):
"""
Serializers for the SalesOrder object
"""
def __init__(self, *args, **kwargs):
customer_detail = kwargs.pop('customer_detail', False)
super().__init__(*args, **kwargs)
if customer_detail is not True:
self.fields.pop('customer_detail')
@staticmethod
def annotate_queryset(queryset):
"""
Add extra information to the queryset
- Number of line items in the SalesOrder
- Overdue status of the SalesOrder
"""
2020-09-05 13:08:59 +00:00
queryset = queryset.annotate(
line_items=SubqueryCount('lines')
)
queryset = queryset.annotate(
overdue=Case(
When(
order.models.SalesOrder.OVERDUE_FILTER, then=Value(True, output_field=BooleanField()),
),
default=Value(False, output_field=BooleanField())
)
)
2020-09-05 13:08:59 +00:00
return queryset
customer_detail = CompanyBriefSerializer(source='customer', many=False, read_only=True)
line_items = serializers.IntegerField(read_only=True)
status_text = serializers.CharField(source='get_status_display', read_only=True)
2021-01-03 14:17:05 +00:00
overdue = serializers.BooleanField(required=False, read_only=True)
reference = serializers.CharField(required=True)
class Meta:
model = order.models.SalesOrder
fields = [
'pk',
'creation_date',
'customer',
'customer_detail',
'customer_reference',
2020-04-20 10:27:52 +00:00
'description',
'line_items',
'link',
'notes',
'overdue',
'reference',
2021-07-02 14:14:36 +00:00
'responsible',
'status',
'status_text',
2020-04-26 05:29:21 +00:00
'shipment_date',
2020-12-18 01:40:47 +00:00
'target_date',
]
read_only_fields = [
2021-06-22 06:43:38 +00:00
'status',
'creation_date',
'shipment_date',
]
class SalesOrderAllocationSerializer(InvenTreeModelSerializer):
"""
Serializer for the SalesOrderAllocation model.
This includes some fields from the related model objects.
"""
part = serializers.PrimaryKeyRelatedField(source='item.part', read_only=True)
order = serializers.PrimaryKeyRelatedField(source='line.order', many=False, read_only=True)
serial = serializers.CharField(source='get_serial', read_only=True)
quantity = serializers.FloatField(read_only=False)
location = serializers.PrimaryKeyRelatedField(source='item.location', many=False, read_only=True)
# Extra detail fields
order_detail = SalesOrderSerializer(source='line.order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='item.part', many=False, read_only=True)
item_detail = stock.serializers.StockItemSerializer(source='item', many=False, read_only=True)
location_detail = stock.serializers.LocationSerializer(source='item.location', many=False, read_only=True)
shipment_date = serializers.DateField(source='shipment.shipment_date', read_only=True)
def __init__(self, *args, **kwargs):
order_detail = kwargs.pop('order_detail', False)
2021-11-26 12:02:29 +00:00
part_detail = kwargs.pop('part_detail', True)
item_detail = kwargs.pop('item_detail', False)
location_detail = kwargs.pop('location_detail', False)
super().__init__(*args, **kwargs)
if not order_detail:
self.fields.pop('order_detail')
if not part_detail:
self.fields.pop('part_detail')
if not item_detail:
self.fields.pop('item_detail')
if not location_detail:
self.fields.pop('location_detail')
class Meta:
model = order.models.SalesOrderAllocation
fields = [
'pk',
'line',
'serial',
'quantity',
'location',
'location_detail',
'item',
'item_detail',
'order',
'order_detail',
'part',
'part_detail',
'shipment',
'shipment_date',
]
class SOLineItemSerializer(InvenTreeModelSerializer):
""" Serializer for a SalesOrderLineItem object """
def __init__(self, *args, **kwargs):
part_detail = kwargs.pop('part_detail', False)
order_detail = kwargs.pop('order_detail', False)
allocations = kwargs.pop('allocations', False)
super().__init__(*args, **kwargs)
if part_detail is not True:
self.fields.pop('part_detail')
if order_detail is not True:
self.fields.pop('order_detail')
if allocations is not True:
self.fields.pop('allocations')
order_detail = SalesOrderSerializer(source='order', many=False, read_only=True)
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
2021-10-06 05:38:13 +00:00
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
2021-11-18 12:01:19 +00:00
quantity = InvenTreeDecimalField()
2021-11-22 23:28:23 +00:00
allocated = serializers.FloatField(source='allocated_quantity', read_only=True)
2021-11-22 23:28:23 +00:00
shipped = InvenTreeDecimalField(read_only=True)
sale_price = InvenTreeMoneySerializer(
allow_null=True
2021-07-03 11:43:22 +00:00
)
2021-05-04 19:56:25 +00:00
sale_price_string = serializers.CharField(source='sale_price', read_only=True)
sale_price_currency = serializers.ChoiceField(
2021-07-03 02:45:59 +00:00
choices=currency_code_mappings(),
help_text=_('Sale price currency'),
)
class Meta:
model = order.models.SalesOrderLineItem
fields = [
'pk',
'allocated',
'allocations',
'quantity',
'reference',
'notes',
'order',
'order_detail',
'part',
'part_detail',
2021-05-04 19:56:25 +00:00
'sale_price',
'sale_price_currency',
'sale_price_string',
'shipped',
]
class SalesOrderShipmentSerializer(InvenTreeModelSerializer):
"""
Serializer for the SalesOrderShipment class
"""
allocations = SalesOrderAllocationSerializer(many=True, read_only=True, location_detail=True)
order_detail = SalesOrderSerializer(source='order', read_only=True, many=False)
class Meta:
model = order.models.SalesOrderShipment
fields = [
'pk',
'order',
'order_detail',
'allocations',
'shipment_date',
'checked_by',
'reference',
'tracking_number',
'notes',
]
class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
"""
Serializer for completing (shipping) a SalesOrderShipment
"""
class Meta:
model = order.models.SalesOrderShipment
fields = [
'tracking_number',
]
def validate(self, data):
data = super().validate(data)
shipment = self.context.get('shipment', None)
if not shipment:
raise ValidationError(_("No shipment details provided"))
shipment.check_can_complete()
return data
def save(self):
shipment = self.context.get('shipment', None)
if not shipment:
return
data = self.validated_data
2021-12-02 12:58:02 +00:00
request = self.context['request']
user = request.user
# Extract provided tracking number (optional)
tracking_number = data.get('tracking_number', None)
shipment.complete_shipment(user, tracking_number=tracking_number)
class SOShipmentAllocationItemSerializer(serializers.Serializer):
"""
A serializer for allocating a single stock-item against a SalesOrder shipment
"""
class Meta:
fields = [
'line_item',
'stock_item',
'quantity',
]
line_item = serializers.PrimaryKeyRelatedField(
queryset=order.models.SalesOrderLineItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Stock Item'),
)
def validate_line_item(self, line_item):
order = self.context['order']
# Ensure that the line item points to the correct order
if line_item.order != order:
raise ValidationError(_("Line item is not associated with this order"))
return line_item
stock_item = serializers.PrimaryKeyRelatedField(
queryset=stock.models.StockItem.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Stock Item'),
)
quantity = serializers.DecimalField(
max_digits=15,
decimal_places=5,
min_value=0,
required=True
)
def validate_quantity(self, quantity):
if quantity <= 0:
raise ValidationError(_("Quantity must be positive"))
return quantity
def validate(self, data):
super().validate(data)
stock_item = data['stock_item']
quantity = data['quantity']
if stock_item.serialized and quantity != 1:
raise ValidationError({
'quantity': _("Quantity must be 1 for serialized stock item"),
})
q = normalize(stock_item.unallocated_quantity())
if quantity > q:
raise ValidationError({
'quantity': _(f"Available quantity ({q}) exceeded")
})
return data
class SalesOrderCompleteSerializer(serializers.Serializer):
"""
DRF serializer for manually marking a sales order as complete
"""
def save(self):
request = self.context['request']
order = self.context['order']
data = self.validated_data
2021-12-02 12:58:02 +00:00
# Mark this order as complete!
order.status = SalesOrderStatus.SHIPPED
order.save()
class SOShipmentAllocationSerializer(serializers.Serializer):
"""
DRF serializer for allocation of stock items against a sales order / shipment
"""
class Meta:
fields = [
'items',
'shipment',
]
items = SOShipmentAllocationItemSerializer(many=True)
shipment = serializers.PrimaryKeyRelatedField(
queryset=order.models.SalesOrderShipment.objects.all(),
many=False,
allow_null=False,
required=True,
label=_('Shipment'),
)
def validate_shipment(self, shipment):
"""
Run validation against the provided shipment instance
"""
order = self.context['order']
if shipment.shipment_date is not None:
raise ValidationError(_("Shipment has already been shipped"))
if shipment.order != order:
raise ValidationError(_("Shipment is not associated with this order"))
return shipment
def validate(self, data):
"""
Serializer validation
"""
data = super().validate(data)
# Extract SalesOrder from serializer context
# order = self.context['order']
items = data.get('items', [])
if len(items) == 0:
raise ValidationError(_('Allocation items must be provided'))
return data
def save(self):
"""
Perform the allocation of items against this order
"""
data = self.validated_data
items = data['items']
shipment = data['shipment']
with transaction.atomic():
for entry in items:
# Create a new SalesOrderAllocation
order.models.SalesOrderAllocation.objects.create(
line=entry.get('line_item'),
item=entry.get('stock_item'),
quantity=entry.get('quantity'),
shipment=shipment,
)
try:
pass
except (ValidationError, DjangoValidationError) as exc:
raise ValidationError(detail=serializers.as_serializer_error(exc))
class SOAttachmentSerializer(InvenTreeAttachmentSerializer):
"""
Serializers for the SalesOrderAttachment model
"""
class Meta:
model = order.models.SalesOrderAttachment
fields = [
'pk',
'order',
'attachment',
'filename',
'link',
'comment',
2021-06-30 07:44:23 +00:00
'upload_date',
]
read_only_fields = [
'upload_date',
]