From 73484192a59edd7e82b78e6c23bdc418d4e2a258 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 28 Feb 2022 22:47:41 +1100 Subject: [PATCH] Add "batch code" and "serial numbers" serializer fields when receiving stock items against a purchase order --- InvenTree/order/models.py | 75 ++++++++++++++++++++++------------ InvenTree/order/serializers.py | 67 ++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 35 deletions(-) diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 7f6192dfcd..3b1cad2ff5 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -398,12 +398,22 @@ class PurchaseOrder(Order): return self.lines.count() > 0 and self.pending_line_items().count() == 0 @transaction.atomic - def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, purchase_price=None, **kwargs): - """ Receive a line item (or partial line item) against this PO + def receive_line_item(self, line, location, quantity, user, status=StockStatus.OK, **kwargs): + """ + Receive a line item (or partial line item) against this PO """ + # Extract optional batch code for the new stock item + batch_code = kwargs.get('batch_code', '') + + # Extract optional list of serial numbers + serials = kwargs.get('serials', None) + + # Extract optional notes field notes = kwargs.get('notes', '') - barcode = kwargs.get('barcode', '') + + # Extract optional barcode field + barcode = kwargs.get('barcode', None) # Prevent null values for barcode if barcode is None: @@ -427,33 +437,44 @@ class PurchaseOrder(Order): # Create a new stock item if line.part and quantity > 0: - stock = stock_models.StockItem( - part=line.part.part, - supplier_part=line.part, - location=location, - quantity=quantity, - purchase_order=self, - status=status, - purchase_price=line.purchase_price, - uid=barcode - ) - stock.save(add_note=False) + # Determine if we should individually serialize the items, or not + if type(serials) is list and len(serials) > 0: + quantity = 1 + else: + serials = [None] - tracking_info = { - 'status': status, - 'purchaseorder': self.pk, - } + for sn in serials: - stock.add_tracking_entry( - StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, - user, - notes=notes, - deltas=tracking_info, - location=location, - purchaseorder=self, - quantity=quantity - ) + stock = stock_models.StockItem( + part=line.part.part, + supplier_part=line.part, + location=location, + quantity=quantity, + purchase_order=self, + status=status, + batch=batch_code, + serial=sn, + purchase_price=line.purchase_price, + uid=barcode + ) + + stock.save(add_note=False) + + tracking_info = { + 'status': status, + 'purchaseorder': self.pk, + } + + stock.add_tracking_entry( + StockHistoryCode.RECEIVED_AGAINST_PURCHASE_ORDER, + user, + notes=notes, + deltas=tracking_info, + location=location, + purchaseorder=self, + quantity=quantity + ) # Update the number of parts received against the particular line item line.received += quantity diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index 5e78b3e3a3..216bc63b11 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -5,6 +5,8 @@ JSON serializers for the Order API # -*- coding: utf-8 -*- from __future__ import unicode_literals +from decimal import Decimal + from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ValidationError as DjangoValidationError @@ -203,6 +205,17 @@ class POLineItemReceiveSerializer(serializers.Serializer): A serializer for receiving a single purchase order line item against a purchase order """ + class Meta: + fields = [ + 'barcode', + 'line_item', + 'location', + 'quantity', + 'status', + 'batch_code' + 'serial_numbers', + ] + line_item = serializers.PrimaryKeyRelatedField( queryset=order.models.PurchaseOrderLineItem.objects.all(), many=False, @@ -241,6 +254,22 @@ class POLineItemReceiveSerializer(serializers.Serializer): return quantity + batch_code = serializers.CharField( + label=_('Batch Code'), + help_text=_('Enter batch code for incoming stock items'), + required=False, + default='', + allow_blank=True, + ) + + serial_numbers = serializers.CharField( + label=_('Serial Numbers'), + help_text=_('Enter serial numbers for incoming stock items'), + required=False, + default='', + allow_blank=True, + ) + status = serializers.ChoiceField( choices=list(StockStatus.items()), default=StockStatus.OK, @@ -270,15 +299,35 @@ class POLineItemReceiveSerializer(serializers.Serializer): return barcode - class Meta: - fields = [ - 'barcode', - 'line_item', - 'location', - 'quantity', - 'status', - ] + def validate(self, data): + data = super().validate(data) + + line_item = data['line_item'] + quantity = data['quantity'] + serial_numbers = data.get('serial_numbers', '').strip() + + base_part = line_item.part.part + + # Does the quantity need to be "integer" (for trackable parts?) + if base_part.trackable: + + if Decimal(quantity) != int(quantity): + raise ValidationError({ + 'quantity': _('An integer quantity must be provided for trackable parts'), + }) + + # If serial numbers are provided + if serial_numbers: + try: + # Pass the serial numbers through to the parent serializer once validated + data['serials'] = extract_serial_numbers(serial_numbers, quantity, base_part.getLatestSerialNumberInt()) + except DjangoValidationError as e: + raise ValidationError({ + 'serial_numbers': e.messages, + }) + + return data class POReceiveSerializer(serializers.Serializer): """ @@ -366,6 +415,8 @@ class POReceiveSerializer(serializers.Serializer): request.user, status=item['status'], barcode=item.get('barcode', ''), + batch_code=item.get('batch_code', ''), + serials=item.get('serials', None), ) except (ValidationError, DjangoValidationError) as exc: # Catch model errors and re-throw as DRF errors