mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
0716238f3b
* Implement structural stock locations * Bumped API version
1187 lines
32 KiB
Python
1187 lines
32 KiB
Python
"""JSON serializers for Stock app."""
|
|
|
|
from datetime import datetime, timedelta
|
|
from decimal import Decimal
|
|
|
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
|
from django.db import transaction
|
|
from django.db.models import BooleanField, Case, Q, Value, When
|
|
from django.db.models.functions import Coalesce
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
from rest_framework import serializers
|
|
from rest_framework.serializers import ValidationError
|
|
from sql_util.utils import SubqueryCount, SubquerySum
|
|
|
|
import common.models
|
|
import company.models
|
|
import InvenTree.helpers
|
|
import InvenTree.serializers
|
|
import part.models as part_models
|
|
import stock.filters
|
|
from common.settings import currency_code_default, currency_code_mappings
|
|
from company.serializers import SupplierPartSerializer
|
|
from InvenTree.models import extract_int
|
|
from InvenTree.serializers import InvenTreeDecimalField
|
|
from part.serializers import PartBriefSerializer
|
|
|
|
from .models import (StockItem, StockItemAttachment, StockItemTestResult,
|
|
StockItemTracking, StockLocation)
|
|
|
|
|
|
class LocationBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Provides a brief serializer for a StockLocation object."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockLocation
|
|
fields = [
|
|
'pk',
|
|
'name',
|
|
'pathstring',
|
|
]
|
|
|
|
|
|
class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Brief serializers for a StockItem."""
|
|
|
|
part_name = serializers.CharField(source='part.full_name', read_only=True)
|
|
|
|
quantity = InvenTreeDecimalField()
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockItem
|
|
fields = [
|
|
'part',
|
|
'part_name',
|
|
'pk',
|
|
'location',
|
|
'quantity',
|
|
'serial',
|
|
'supplier_part',
|
|
'barcode_hash',
|
|
]
|
|
|
|
read_only_fields = [
|
|
'barcode_hash',
|
|
]
|
|
|
|
def validate_serial(self, value):
|
|
"""Make sure serial is not to big."""
|
|
if abs(extract_int(value)) > 0x7fffffff:
|
|
raise serializers.ValidationError(_("Serial number is too large"))
|
|
return value
|
|
|
|
|
|
class StockItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Serializer for a StockItem.
|
|
|
|
- Includes serialization for the linked part
|
|
- Includes serialization for the item location
|
|
"""
|
|
|
|
part = serializers.PrimaryKeyRelatedField(
|
|
queryset=part_models.Part.objects.all(),
|
|
many=False, allow_null=False,
|
|
help_text=_("Base Part"),
|
|
label=_("Part"),
|
|
)
|
|
|
|
def validate_part(self, part):
|
|
"""Ensure the provided Part instance is valid"""
|
|
|
|
if part.virtual:
|
|
raise ValidationError(_("Stock item cannot be created for virtual parts"))
|
|
|
|
return part
|
|
|
|
def update(self, instance, validated_data):
|
|
"""Custom update method to pass the user information through to the instance."""
|
|
instance._user = self.context['user']
|
|
|
|
return super().update(instance, validated_data)
|
|
|
|
@staticmethod
|
|
def annotate_queryset(queryset):
|
|
"""Add some extra annotations to the queryset, performing database queries as efficiently as possible."""
|
|
|
|
queryset = queryset.prefetch_related(
|
|
'sales_order',
|
|
'purchase_order',
|
|
)
|
|
|
|
# Annotate the queryset with the total allocated to sales orders
|
|
queryset = queryset.annotate(
|
|
allocated=Coalesce(
|
|
SubquerySum('sales_order_allocations__quantity'), Decimal(0)
|
|
) + Coalesce(
|
|
SubquerySum('allocations__quantity'), Decimal(0)
|
|
)
|
|
)
|
|
|
|
# Annotate the queryset with the number of tracking items
|
|
queryset = queryset.annotate(
|
|
tracking_items=SubqueryCount('tracking_info')
|
|
)
|
|
|
|
# Add flag to indicate if the StockItem has expired
|
|
queryset = queryset.annotate(
|
|
expired=Case(
|
|
When(
|
|
StockItem.EXPIRED_FILTER, then=Value(True, output_field=BooleanField()),
|
|
),
|
|
default=Value(False, output_field=BooleanField())
|
|
)
|
|
)
|
|
|
|
# Add flag to indicate if the StockItem is stale
|
|
stale_days = common.models.InvenTreeSetting.get_setting('STOCK_STALE_DAYS')
|
|
stale_date = datetime.now().date() + timedelta(days=stale_days)
|
|
stale_filter = StockItem.IN_STOCK_FILTER & ~Q(expiry_date=None) & Q(expiry_date__lt=stale_date)
|
|
|
|
queryset = queryset.annotate(
|
|
stale=Case(
|
|
When(
|
|
stale_filter, then=Value(True, output_field=BooleanField()),
|
|
),
|
|
default=Value(False, output_field=BooleanField()),
|
|
)
|
|
)
|
|
|
|
return queryset
|
|
|
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
|
|
|
supplier_part_detail = SupplierPartSerializer(source='supplier_part', many=False, read_only=True)
|
|
|
|
part_detail = PartBriefSerializer(source='part', many=False, read_only=True)
|
|
|
|
location_detail = LocationBriefSerializer(source='location', many=False, read_only=True)
|
|
|
|
quantity = InvenTreeDecimalField()
|
|
|
|
# Annotated fields
|
|
tracking_items = serializers.IntegerField(read_only=True, required=False)
|
|
allocated = serializers.FloatField(required=False)
|
|
expired = serializers.BooleanField(required=False, read_only=True)
|
|
stale = serializers.BooleanField(required=False, read_only=True)
|
|
|
|
purchase_price = InvenTree.serializers.InvenTreeMoneySerializer(
|
|
label=_('Purchase Price'),
|
|
max_digits=19, decimal_places=6,
|
|
allow_null=True,
|
|
help_text=_('Purchase price of this stock item'),
|
|
)
|
|
|
|
purchase_price_currency = serializers.ChoiceField(
|
|
choices=currency_code_mappings(),
|
|
default=currency_code_default,
|
|
label=_('Currency'),
|
|
help_text=_('Purchase currency of this stock item'),
|
|
)
|
|
|
|
purchase_order_reference = serializers.CharField(source='purchase_order.reference', read_only=True)
|
|
sales_order_reference = serializers.CharField(source='sales_order.reference', read_only=True)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Add detail fields."""
|
|
part_detail = kwargs.pop('part_detail', False)
|
|
location_detail = kwargs.pop('location_detail', False)
|
|
supplier_part_detail = kwargs.pop('supplier_part_detail', False)
|
|
|
|
super(StockItemSerializer, self).__init__(*args, **kwargs)
|
|
|
|
if part_detail is not True:
|
|
self.fields.pop('part_detail')
|
|
|
|
if location_detail is not True:
|
|
self.fields.pop('location_detail')
|
|
|
|
if supplier_part_detail is not True:
|
|
self.fields.pop('supplier_part_detail')
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockItem
|
|
fields = [
|
|
'allocated',
|
|
'batch',
|
|
'belongs_to',
|
|
'build',
|
|
'customer',
|
|
'delete_on_deplete',
|
|
'expired',
|
|
'expiry_date',
|
|
'is_building',
|
|
'link',
|
|
'location',
|
|
'location_detail',
|
|
'notes',
|
|
'owner',
|
|
'packaging',
|
|
'part',
|
|
'part_detail',
|
|
'purchase_order',
|
|
'purchase_order_reference',
|
|
'pk',
|
|
'quantity',
|
|
'sales_order',
|
|
'sales_order_reference',
|
|
'serial',
|
|
'stale',
|
|
'status',
|
|
'status_text',
|
|
'stocktake_date',
|
|
'supplier_part',
|
|
'supplier_part_detail',
|
|
'tracking_items',
|
|
'barcode_hash',
|
|
'updated',
|
|
'purchase_price',
|
|
'purchase_price_currency',
|
|
]
|
|
|
|
"""
|
|
These fields are read-only in this context.
|
|
They can be updated by accessing the appropriate API endpoints
|
|
"""
|
|
read_only_fields = [
|
|
'allocated',
|
|
'barcode_hash',
|
|
'stocktake_date',
|
|
'stocktake_user',
|
|
'updated',
|
|
]
|
|
|
|
|
|
class SerializeStockItemSerializer(serializers.Serializer):
|
|
"""A DRF serializer for "serializing" a StockItem.
|
|
|
|
(Sorry for the confusing naming...)
|
|
|
|
Here, "serializing" means splitting out a single StockItem,
|
|
into multiple single-quantity items with an assigned serial number
|
|
|
|
Note: The base StockItem object is provided to the serializer context
|
|
"""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'quantity',
|
|
'serial_numbers',
|
|
'destination',
|
|
'notes',
|
|
]
|
|
|
|
quantity = serializers.IntegerField(
|
|
min_value=0,
|
|
required=True,
|
|
label=_('Quantity'),
|
|
help_text=_('Enter number of stock items to serialize'),
|
|
)
|
|
|
|
def validate_quantity(self, quantity):
|
|
"""Validate that the quantity value is correct."""
|
|
item = self.context['item']
|
|
|
|
if quantity < 0:
|
|
raise ValidationError(_("Quantity must be greater than zero"))
|
|
|
|
if quantity > item.quantity:
|
|
q = item.quantity
|
|
raise ValidationError(_(f"Quantity must not exceed available stock quantity ({q})"))
|
|
|
|
return quantity
|
|
|
|
serial_numbers = serializers.CharField(
|
|
label=_('Serial Numbers'),
|
|
help_text=_('Enter serial numbers for new items'),
|
|
allow_blank=False,
|
|
required=True,
|
|
)
|
|
|
|
destination = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockLocation.objects.all(),
|
|
many=False,
|
|
required=True,
|
|
allow_null=False,
|
|
label=_('Location'),
|
|
help_text=_('Destination stock location'),
|
|
)
|
|
|
|
notes = serializers.CharField(
|
|
required=False,
|
|
allow_blank=True,
|
|
label=_("Notes"),
|
|
help_text=_("Optional note field")
|
|
)
|
|
|
|
def validate(self, data):
|
|
"""Check that the supplied serial numbers are valid."""
|
|
data = super().validate(data)
|
|
|
|
item = self.context['item']
|
|
|
|
if not item.part.trackable:
|
|
raise ValidationError(_("Serial numbers cannot be assigned to this part"))
|
|
|
|
# Ensure the serial numbers are valid!
|
|
quantity = data['quantity']
|
|
serial_numbers = data['serial_numbers']
|
|
|
|
try:
|
|
serials = InvenTree.helpers.extract_serial_numbers(
|
|
serial_numbers,
|
|
quantity,
|
|
item.part.get_latest_serial_number()
|
|
)
|
|
except DjangoValidationError as e:
|
|
raise ValidationError({
|
|
'serial_numbers': e.messages,
|
|
})
|
|
|
|
existing = item.part.find_conflicting_serial_numbers(serials)
|
|
|
|
if len(existing) > 0:
|
|
exists = ','.join([str(x) for x in existing])
|
|
error = _('Serial numbers already exist') + ": " + exists
|
|
|
|
raise ValidationError({
|
|
'serial_numbers': error,
|
|
})
|
|
|
|
return data
|
|
|
|
def save(self):
|
|
"""Serialize stock item."""
|
|
item = self.context['item']
|
|
request = self.context['request']
|
|
user = request.user
|
|
|
|
data = self.validated_data
|
|
|
|
serials = InvenTree.helpers.extract_serial_numbers(
|
|
data['serial_numbers'],
|
|
data['quantity'],
|
|
item.part.get_latest_serial_number()
|
|
)
|
|
|
|
item.serializeStock(
|
|
data['quantity'],
|
|
serials,
|
|
user,
|
|
notes=data.get('notes', ''),
|
|
location=data['destination'],
|
|
)
|
|
|
|
|
|
class InstallStockItemSerializer(serializers.Serializer):
|
|
"""Serializer for installing a stock item into a given part."""
|
|
|
|
stock_item = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockItem.objects.all(),
|
|
many=False,
|
|
required=True,
|
|
allow_null=False,
|
|
label=_('Stock Item'),
|
|
help_text=_('Select stock item to install'),
|
|
)
|
|
|
|
note = serializers.CharField(
|
|
label=_('Note'),
|
|
required=False,
|
|
allow_blank=True,
|
|
)
|
|
|
|
def validate_stock_item(self, stock_item):
|
|
"""Validate the selected stock item."""
|
|
if not stock_item.in_stock:
|
|
# StockItem must be in stock to be "installed"
|
|
raise ValidationError(_("Stock item is unavailable"))
|
|
|
|
# Extract the "parent" item - the item into which the stock item will be installed
|
|
parent_item = self.context['item']
|
|
parent_part = parent_item.part
|
|
|
|
if not parent_part.check_if_part_in_bom(stock_item.part):
|
|
raise ValidationError(_("Selected part is not in the Bill of Materials"))
|
|
|
|
return stock_item
|
|
|
|
def save(self):
|
|
"""Install the selected stock item into this one."""
|
|
data = self.validated_data
|
|
|
|
stock_item = data['stock_item']
|
|
note = data.get('note', '')
|
|
|
|
parent_item = self.context['item']
|
|
request = self.context['request']
|
|
|
|
parent_item.installStockItem(
|
|
stock_item,
|
|
stock_item.quantity,
|
|
request.user,
|
|
note,
|
|
)
|
|
|
|
|
|
class UninstallStockItemSerializer(serializers.Serializer):
|
|
"""API serializers for uninstalling an installed item from a stock item."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'location',
|
|
'note',
|
|
]
|
|
|
|
location = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockLocation.objects.all(),
|
|
many=False, required=True, allow_null=False,
|
|
label=_('Location'),
|
|
help_text=_('Destination location for uninstalled item')
|
|
)
|
|
|
|
note = serializers.CharField(
|
|
label=_('Notes'),
|
|
help_text=_('Add transaction note (optional)'),
|
|
required=False, allow_blank=True,
|
|
)
|
|
|
|
def save(self):
|
|
"""Uninstall stock item."""
|
|
item = self.context['item']
|
|
|
|
data = self.validated_data
|
|
request = self.context['request']
|
|
|
|
location = data['location']
|
|
|
|
note = data.get('note', '')
|
|
|
|
item.uninstall_into_location(
|
|
location,
|
|
request.user,
|
|
note
|
|
)
|
|
|
|
|
|
class ConvertStockItemSerializer(serializers.Serializer):
|
|
"""DRF serializer class for converting a StockItem to a valid variant part"""
|
|
|
|
class Meta:
|
|
"""Metaclass options"""
|
|
fields = [
|
|
'part',
|
|
]
|
|
|
|
part = serializers.PrimaryKeyRelatedField(
|
|
queryset=part_models.Part.objects.all(),
|
|
label=_('Part'),
|
|
help_text=_('Select part to convert stock item into'),
|
|
many=False, required=True, allow_null=False
|
|
)
|
|
|
|
def validate_part(self, part):
|
|
"""Ensure that the provided part is a valid option for the stock item"""
|
|
|
|
stock_item = self.context['item']
|
|
valid_options = stock_item.part.get_conversion_options()
|
|
|
|
if part not in valid_options:
|
|
raise ValidationError(_("Selected part is not a valid option for conversion"))
|
|
|
|
return part
|
|
|
|
def save(self):
|
|
"""Save the serializer to convert the StockItem to the selected Part"""
|
|
data = self.validated_data
|
|
|
|
part = data['part']
|
|
|
|
stock_item = self.context['item']
|
|
request = self.context['request']
|
|
|
|
stock_item.convert_to_variant(part, request.user)
|
|
|
|
|
|
class ReturnStockItemSerializer(serializers.Serializer):
|
|
"""DRF serializer for returning a stock item from a customer"""
|
|
|
|
class Meta:
|
|
"""Metaclass options"""
|
|
|
|
fields = [
|
|
'location',
|
|
'note',
|
|
]
|
|
|
|
location = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockLocation.objects.all(),
|
|
many=False, required=True, allow_null=False,
|
|
label=_('Location'),
|
|
help_text=_('Destination location for returned item'),
|
|
)
|
|
|
|
notes = serializers.CharField(
|
|
label=_('Notes'),
|
|
help_text=_('Add transaction note (optional)'),
|
|
required=False, allow_blank=True,
|
|
)
|
|
|
|
def save(self):
|
|
"""Save the serialzier to return the item into stock"""
|
|
|
|
item = self.context['item']
|
|
request = self.context['request']
|
|
|
|
data = self.validated_data
|
|
|
|
location = data['location']
|
|
notes = data.get('notes', '')
|
|
|
|
item.return_from_customer(
|
|
location,
|
|
user=request.user,
|
|
notes=notes
|
|
)
|
|
|
|
|
|
class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Serializer for a simple tree view."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockLocation
|
|
fields = [
|
|
'pk',
|
|
'name',
|
|
'parent',
|
|
'icon',
|
|
]
|
|
|
|
|
|
class LocationSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Detailed information about a stock location."""
|
|
|
|
@staticmethod
|
|
def annotate_queryset(queryset):
|
|
"""Annotate extra information to the queryset"""
|
|
|
|
# Annotate the number of stock items which exist in this category (including subcategories)
|
|
queryset = queryset.annotate(
|
|
items=stock.filters.annotate_location_items()
|
|
)
|
|
|
|
return queryset
|
|
|
|
url = serializers.CharField(source='get_absolute_url', read_only=True)
|
|
|
|
items = serializers.IntegerField(read_only=True)
|
|
|
|
level = serializers.IntegerField(read_only=True)
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockLocation
|
|
fields = [
|
|
'pk',
|
|
'barcode_hash',
|
|
'url',
|
|
'name',
|
|
'level',
|
|
'description',
|
|
'parent',
|
|
'pathstring',
|
|
'items',
|
|
'owner',
|
|
'icon',
|
|
'structural',
|
|
]
|
|
|
|
read_only_fields = [
|
|
'barcode_hash',
|
|
]
|
|
|
|
|
|
class StockItemAttachmentSerializer(InvenTree.serializers.InvenTreeAttachmentSerializer):
|
|
"""Serializer for StockItemAttachment model."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockItemAttachment
|
|
|
|
fields = [
|
|
'pk',
|
|
'stock_item',
|
|
'attachment',
|
|
'filename',
|
|
'link',
|
|
'comment',
|
|
'upload_date',
|
|
'user',
|
|
'user_detail',
|
|
]
|
|
|
|
read_only_fields = [
|
|
'upload_date',
|
|
'user',
|
|
'user_detail'
|
|
]
|
|
|
|
|
|
class StockItemTestResultSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Serializer for the StockItemTestResult model."""
|
|
|
|
user_detail = InvenTree.serializers.UserSerializer(source='user', read_only=True)
|
|
|
|
key = serializers.CharField(read_only=True)
|
|
|
|
attachment = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Add detail fields."""
|
|
user_detail = kwargs.pop('user_detail', False)
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if user_detail is not True:
|
|
self.fields.pop('user_detail')
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockItemTestResult
|
|
|
|
fields = [
|
|
'pk',
|
|
'stock_item',
|
|
'key',
|
|
'test',
|
|
'result',
|
|
'value',
|
|
'attachment',
|
|
'notes',
|
|
'user',
|
|
'user_detail',
|
|
'date'
|
|
]
|
|
|
|
read_only_fields = [
|
|
'pk',
|
|
'user',
|
|
'date',
|
|
]
|
|
|
|
|
|
class StockTrackingSerializer(InvenTree.serializers.InvenTreeModelSerializer):
|
|
"""Serializer for StockItemTracking model."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
"""Add detail fields."""
|
|
item_detail = kwargs.pop('item_detail', False)
|
|
user_detail = kwargs.pop('user_detail', False)
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if item_detail is not True:
|
|
self.fields.pop('item_detail')
|
|
|
|
if user_detail is not True:
|
|
self.fields.pop('user_detail')
|
|
|
|
label = serializers.CharField(read_only=True)
|
|
|
|
item_detail = StockItemSerializerBrief(source='item', many=False, read_only=True)
|
|
|
|
user_detail = InvenTree.serializers.UserSerializer(source='user', many=False, read_only=True)
|
|
|
|
deltas = serializers.JSONField(read_only=True)
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
model = StockItemTracking
|
|
fields = [
|
|
'pk',
|
|
'item',
|
|
'item_detail',
|
|
'date',
|
|
'deltas',
|
|
'label',
|
|
'notes',
|
|
'tracking_type',
|
|
'user',
|
|
'user_detail',
|
|
]
|
|
|
|
read_only_fields = [
|
|
'date',
|
|
'user',
|
|
'label',
|
|
'tracking_type',
|
|
]
|
|
|
|
|
|
class StockAssignmentItemSerializer(serializers.Serializer):
|
|
"""Serializer for a single StockItem with in StockAssignment request.
|
|
|
|
Here, the particular StockItem is being assigned (manually) to a customer
|
|
|
|
Fields:
|
|
- item: StockItem object
|
|
"""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'item',
|
|
]
|
|
|
|
item = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockItem.objects.all(),
|
|
many=False,
|
|
allow_null=False,
|
|
required=True,
|
|
label=_('Stock Item'),
|
|
)
|
|
|
|
def validate_item(self, item):
|
|
"""Validate item.
|
|
|
|
Ensures:
|
|
- is in stock
|
|
- Is salable
|
|
- Is not allocated
|
|
"""
|
|
# The item must currently be "in stock"
|
|
if not item.in_stock:
|
|
raise ValidationError(_("Item must be in stock"))
|
|
|
|
# The base part must be "salable"
|
|
if not item.part.salable:
|
|
raise ValidationError(_("Part must be salable"))
|
|
|
|
# The item must not be allocated to a sales order
|
|
if item.sales_order_allocations.count() > 0:
|
|
raise ValidationError(_("Item is allocated to a sales order"))
|
|
|
|
# The item must not be allocated to a build order
|
|
if item.allocations.count() > 0:
|
|
raise ValidationError(_("Item is allocated to a build order"))
|
|
|
|
return item
|
|
|
|
|
|
class StockAssignmentSerializer(serializers.Serializer):
|
|
"""Serializer for assigning one (or more) stock items to a customer.
|
|
|
|
This is a manual assignment process, separate for (for example) a Sales Order
|
|
"""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'items',
|
|
'customer',
|
|
'notes',
|
|
]
|
|
|
|
items = StockAssignmentItemSerializer(
|
|
many=True,
|
|
required=True,
|
|
)
|
|
|
|
customer = serializers.PrimaryKeyRelatedField(
|
|
queryset=company.models.Company.objects.all(),
|
|
many=False,
|
|
allow_null=False,
|
|
required=True,
|
|
label=_('Customer'),
|
|
help_text=_('Customer to assign stock items'),
|
|
)
|
|
|
|
def validate_customer(self, customer):
|
|
"""Make sure provided company is customer."""
|
|
if customer and not customer.is_customer:
|
|
raise ValidationError(_('Selected company is not a customer'))
|
|
|
|
return customer
|
|
|
|
notes = serializers.CharField(
|
|
required=False,
|
|
allow_blank=True,
|
|
label=_('Notes'),
|
|
help_text=_('Stock assignment notes'),
|
|
)
|
|
|
|
def validate(self, data):
|
|
"""Make sure items were provided."""
|
|
data = super().validate(data)
|
|
|
|
items = data.get('items', [])
|
|
|
|
if len(items) == 0:
|
|
raise ValidationError(_("A list of stock items must be provided"))
|
|
|
|
return data
|
|
|
|
def save(self):
|
|
"""Assign stock."""
|
|
request = self.context['request']
|
|
|
|
user = getattr(request, 'user', None)
|
|
|
|
data = self.validated_data
|
|
|
|
items = data['items']
|
|
customer = data['customer']
|
|
notes = data.get('notes', '')
|
|
|
|
with transaction.atomic():
|
|
for item in items:
|
|
|
|
stock_item = item['item']
|
|
|
|
stock_item.allocateToCustomer(
|
|
customer,
|
|
user=user,
|
|
notes=notes,
|
|
)
|
|
|
|
|
|
class StockMergeItemSerializer(serializers.Serializer):
|
|
"""Serializer for a single StockItem within the StockMergeSerializer class.
|
|
|
|
Here, the individual StockItem is being checked for merge compatibility.
|
|
"""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'item',
|
|
]
|
|
|
|
item = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockItem.objects.all(),
|
|
many=False,
|
|
allow_null=False,
|
|
required=True,
|
|
label=_('Stock Item'),
|
|
)
|
|
|
|
def validate_item(self, item):
|
|
"""Make sure item can be merged."""
|
|
# Check that the stock item is able to be merged
|
|
item.can_merge(raise_error=True)
|
|
|
|
return item
|
|
|
|
|
|
class StockMergeSerializer(serializers.Serializer):
|
|
"""Serializer for merging two (or more) stock items together."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'items',
|
|
'location',
|
|
'notes',
|
|
'allow_mismatched_suppliers',
|
|
'allow_mismatched_status',
|
|
]
|
|
|
|
items = StockMergeItemSerializer(
|
|
many=True,
|
|
required=True,
|
|
)
|
|
|
|
location = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockLocation.objects.all(),
|
|
many=False,
|
|
required=True,
|
|
allow_null=False,
|
|
label=_('Location'),
|
|
help_text=_('Destination stock location'),
|
|
)
|
|
|
|
notes = serializers.CharField(
|
|
required=False,
|
|
allow_blank=True,
|
|
label=_('Notes'),
|
|
help_text=_('Stock merging notes'),
|
|
)
|
|
|
|
allow_mismatched_suppliers = serializers.BooleanField(
|
|
required=False,
|
|
label=_('Allow mismatched suppliers'),
|
|
help_text=_('Allow stock items with different supplier parts to be merged'),
|
|
)
|
|
|
|
allow_mismatched_status = serializers.BooleanField(
|
|
required=False,
|
|
label=_('Allow mismatched status'),
|
|
help_text=_('Allow stock items with different status codes to be merged'),
|
|
)
|
|
|
|
def validate(self, data):
|
|
"""Make sure all needed values are provided and that the items can be merged."""
|
|
data = super().validate(data)
|
|
|
|
items = data['items']
|
|
|
|
if len(items) < 2:
|
|
raise ValidationError(_('At least two stock items must be provided'))
|
|
|
|
unique_items = set()
|
|
|
|
# The "base item" is the first item
|
|
base_item = items[0]['item']
|
|
|
|
data['base_item'] = base_item
|
|
|
|
# Ensure stock items are unique!
|
|
for element in items:
|
|
item = element['item']
|
|
|
|
if item.pk in unique_items:
|
|
raise ValidationError(_('Duplicate stock items'))
|
|
|
|
unique_items.add(item.pk)
|
|
|
|
# Checks from here refer to the "base_item"
|
|
if item == base_item:
|
|
continue
|
|
|
|
# Check that this item can be merged with the base_item
|
|
item.can_merge(
|
|
raise_error=True,
|
|
other=base_item,
|
|
allow_mismatched_suppliers=data.get('allow_mismatched_suppliers', False),
|
|
allow_mismatched_status=data.get('allow_mismatched_status', False),
|
|
)
|
|
|
|
return data
|
|
|
|
def save(self):
|
|
"""Actually perform the stock merging action.
|
|
|
|
At this point we are confident that the merge can take place
|
|
"""
|
|
data = self.validated_data
|
|
|
|
base_item = data['base_item']
|
|
items = data['items'][1:]
|
|
|
|
request = self.context['request']
|
|
user = getattr(request, 'user', None)
|
|
|
|
items = []
|
|
|
|
for item in data['items'][1:]:
|
|
items.append(item['item'])
|
|
|
|
base_item.merge_stock_items(
|
|
items,
|
|
allow_mismatched_suppliers=data.get('allow_mismatched_suppliers', False),
|
|
allow_mismatched_status=data.get('allow_mismatched_status', False),
|
|
user=user,
|
|
location=data['location'],
|
|
notes=data.get('notes', None)
|
|
)
|
|
|
|
|
|
class StockAdjustmentItemSerializer(serializers.Serializer):
|
|
"""Serializer for a single StockItem within a stock adjument request.
|
|
|
|
Fields:
|
|
- item: StockItem object
|
|
- quantity: Numerical quantity
|
|
"""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'item',
|
|
'quantity'
|
|
]
|
|
|
|
pk = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockItem.objects.all(),
|
|
many=False,
|
|
allow_null=False,
|
|
required=True,
|
|
label='stock_item',
|
|
help_text=_('StockItem primary key value')
|
|
)
|
|
|
|
quantity = serializers.DecimalField(
|
|
max_digits=15,
|
|
decimal_places=5,
|
|
min_value=0,
|
|
required=True
|
|
)
|
|
|
|
|
|
class StockAdjustmentSerializer(serializers.Serializer):
|
|
"""Base class for managing stock adjustment actions via the API."""
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'items',
|
|
'notes',
|
|
]
|
|
|
|
items = StockAdjustmentItemSerializer(many=True)
|
|
|
|
notes = serializers.CharField(
|
|
required=False,
|
|
allow_blank=True,
|
|
label=_("Notes"),
|
|
help_text=_("Stock transaction notes"),
|
|
)
|
|
|
|
def validate(self, data):
|
|
"""Make sure items are provided."""
|
|
super().validate(data)
|
|
|
|
items = data.get('items', [])
|
|
|
|
if len(items) == 0:
|
|
raise ValidationError(_("A list of stock items must be provided"))
|
|
|
|
return data
|
|
|
|
|
|
class StockCountSerializer(StockAdjustmentSerializer):
|
|
"""Serializer for counting stock items."""
|
|
|
|
def save(self):
|
|
"""Count stock."""
|
|
request = self.context['request']
|
|
|
|
data = self.validated_data
|
|
items = data['items']
|
|
notes = data.get('notes', '')
|
|
|
|
with transaction.atomic():
|
|
for item in items:
|
|
|
|
stock_item = item['pk']
|
|
quantity = item['quantity']
|
|
|
|
stock_item.stocktake(
|
|
quantity,
|
|
request.user,
|
|
notes=notes
|
|
)
|
|
|
|
|
|
class StockAddSerializer(StockAdjustmentSerializer):
|
|
"""Serializer for adding stock to stock item(s)."""
|
|
|
|
def save(self):
|
|
"""Add stock."""
|
|
request = self.context['request']
|
|
|
|
data = self.validated_data
|
|
notes = data.get('notes', '')
|
|
|
|
with transaction.atomic():
|
|
for item in data['items']:
|
|
|
|
stock_item = item['pk']
|
|
quantity = item['quantity']
|
|
|
|
stock_item.add_stock(
|
|
quantity,
|
|
request.user,
|
|
notes=notes
|
|
)
|
|
|
|
|
|
class StockRemoveSerializer(StockAdjustmentSerializer):
|
|
"""Serializer for removing stock from stock item(s)."""
|
|
|
|
def save(self):
|
|
"""Remove stock."""
|
|
request = self.context['request']
|
|
|
|
data = self.validated_data
|
|
notes = data.get('notes', '')
|
|
|
|
with transaction.atomic():
|
|
for item in data['items']:
|
|
|
|
stock_item = item['pk']
|
|
quantity = item['quantity']
|
|
|
|
stock_item.take_stock(
|
|
quantity,
|
|
request.user,
|
|
notes=notes
|
|
)
|
|
|
|
|
|
class StockTransferSerializer(StockAdjustmentSerializer):
|
|
"""Serializer for transferring (moving) stock item(s)."""
|
|
|
|
location = serializers.PrimaryKeyRelatedField(
|
|
queryset=StockLocation.objects.all(),
|
|
many=False,
|
|
required=True,
|
|
allow_null=False,
|
|
label=_('Location'),
|
|
help_text=_('Destination stock location'),
|
|
)
|
|
|
|
class Meta:
|
|
"""Metaclass options."""
|
|
|
|
fields = [
|
|
'items',
|
|
'notes',
|
|
'location',
|
|
]
|
|
|
|
def save(self):
|
|
"""Transfer stock."""
|
|
request = self.context['request']
|
|
|
|
data = self.validated_data
|
|
|
|
items = data['items']
|
|
notes = data.get('notes', '')
|
|
location = data['location']
|
|
|
|
with transaction.atomic():
|
|
for item in items:
|
|
|
|
stock_item = item['pk']
|
|
quantity = item['quantity']
|
|
|
|
stock_item.move(
|
|
location,
|
|
notes,
|
|
request.user,
|
|
quantity=quantity
|
|
)
|