from decimal import Decimal from rest_framework import serializers from rest_framework.exceptions import ParseError from apps.utils.serializers import CustomModelSerializer from apps.utils.constants import EXCLUDE_FIELDS from apps.mpr.models import ( PurchaseRequisition, PurchaseRequisitionItem, WarehouseEntry, WarehouseEntryItem, WarehouseStock, MaterialRequisition, MaterialRequisitionItem, ) from apps.wf.serializers import TicketSimpleSerializer # ========== 物资申购单 ========== class PurchaseRequisitionItemSerializer(CustomModelSerializer): class Meta: model = PurchaseRequisitionItem fields = '__all__' read_only_fields = ['create_time', 'update_time', 'is_deleted'] class PurchaseRequisitionListSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) class Meta: model = PurchaseRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS class PurchaseRequisitionDetailSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) items_ = PurchaseRequisitionItemSerializer(source='items', many=True, read_only=True) class Meta: model = PurchaseRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS class PurchaseRequisitionCreateSerializer(CustomModelSerializer): items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[]) class Meta: model = PurchaseRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number', 'total_amount'] def create(self, validated_data): items_data = validated_data.pop('items', []) validated_data['number'] = PurchaseRequisition.get_a_number() instance = super().create(validated_data) self._save_items(instance, items_data) return instance def update(self, instance, validated_data): items_data = validated_data.pop('items', None) instance = super().update(instance, validated_data) if items_data is not None: instance.items.all().delete() self._save_items(instance, items_data) return instance def _save_items(self, instance, items_data): total = 0 for item in items_data: item.pop('id', None) unit_price = float(item.get('unit_price', 0) or 0) purchase_quantity = float(item.get('purchase_quantity', 0) or 0) item['total_price'] = round(unit_price * purchase_quantity, 2) total += item['total_price'] PurchaseRequisitionItem.objects.create(requisition=instance, **item) instance.total_amount = total instance.save(update_fields=['total_amount']) # ========== 仓库入库单 ========== class WarehouseEntryItemSerializer(CustomModelSerializer): class Meta: model = WarehouseEntryItem fields = '__all__' read_only_fields = ['create_time', 'update_time', 'is_deleted'] class WarehouseEntryListSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True) entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True) class Meta: model = WarehouseEntry fields = '__all__' read_only_fields = EXCLUDE_FIELDS class WarehouseEntryDetailSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) items_ = WarehouseEntryItemSerializer(source='items', many=True, read_only=True) entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True) entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True) class Meta: model = WarehouseEntry fields = '__all__' read_only_fields = EXCLUDE_FIELDS class WarehouseEntryCreateSerializer(CustomModelSerializer): items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[]) class Meta: model = WarehouseEntry fields = '__all__' read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number', 'total_amount'] def create(self, validated_data): items_data = validated_data.pop('items', []) validated_data['number'] = WarehouseEntry.get_a_number() instance = super().create(validated_data) self._save_items(instance, items_data) return instance def update(self, instance, validated_data): items_data = validated_data.pop('items', None) instance = super().update(instance, validated_data) if items_data is not None: instance.items.all().delete() self._save_items(instance, items_data) return instance def _save_items(self, instance, items_data): total = 0 for item in items_data: item.pop('id', None) unit_price = float(item.get('unit_price', 0) or 0) quantity = float(item.get('quantity', 0) or 0) item['amount'] = round(unit_price * quantity, 2) total += item['amount'] WarehouseEntryItem.objects.create(entry=instance, **item) instance.total_amount = total instance.save(update_fields=['total_amount']) # ========== 物料库存 ========== class WarehouseStockSerializer(CustomModelSerializer): warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) entry_type_display = serializers.CharField(source='get_entry_type_display', read_only=True) entry_method_display = serializers.CharField(source='get_entry_method_display', read_only=True) status_display = serializers.SerializerMethodField() class Meta: model = WarehouseStock fields = '__all__' read_only_fields = ['create_time', 'update_time', 'is_deleted'] def get_status_display(self, obj): if obj.quantity <= 0: return '已领完' return obj.get_status_display() # ========== 物资领用单 ========== class MaterialRequisitionItemSerializer(CustomModelSerializer): stock_name = serializers.CharField(source='stock.name', read_only=True, default='') class Meta: model = MaterialRequisitionItem fields = '__all__' read_only_fields = ['create_time', 'update_time', 'is_deleted'] class MaterialRequisitionListSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) class Meta: model = MaterialRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS class MaterialRequisitionDetailSerializer(CustomModelSerializer): create_by_name = serializers.CharField(source='create_by.name', read_only=True) belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) items_ = MaterialRequisitionItemSerializer(source='items', many=True, read_only=True) class Meta: model = MaterialRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS class MaterialRequisitionCreateSerializer(CustomModelSerializer): items = serializers.ListField(child=serializers.DictField(), write_only=True, required=False, default=[]) class Meta: model = MaterialRequisition fields = '__all__' read_only_fields = EXCLUDE_FIELDS + ['belong_dept', 'number'] def create(self, validated_data): items_data = validated_data.pop('items', []) validated_data['number'] = MaterialRequisition.get_a_number() instance = super().create(validated_data) self._save_items(instance, items_data) return instance def update(self, instance, validated_data): items_data = validated_data.pop('items', None) instance = super().update(instance, validated_data) if items_data is not None: self._restore_stock(instance) instance.items.all().delete() self._save_items(instance, items_data) return instance def _save_items(self, instance, items_data): for item in items_data: item.pop('id', None) is_stock_item = item.get('is_stock_item', True) stock_id = item.pop('stock', None) or item.pop('stock_id', None) quantity = Decimal(str(item.get('quantity', 0) or 0)) stock_obj = None if is_stock_item and stock_id: try: stock_obj = WarehouseStock.objects.select_for_update().get(id=stock_id) except WarehouseStock.DoesNotExist: raise ParseError(f"库存记录不存在: {stock_id}") if stock_obj.quantity < quantity: raise ParseError(f"库存不足: {stock_obj.name} 库存{stock_obj.quantity} < 领用{quantity}") stock_obj.quantity -= quantity stock_obj.status = 'in_requisition' stock_obj.save(update_fields=['quantity', 'status']) MaterialRequisitionItem.objects.create( requisition=instance, is_stock_item=is_stock_item, stock=stock_obj, req_type=item.get('req_type', ''), name=item.get('name', ''), spec=item.get('spec', ''), unit=item.get('unit', ''), quantity=quantity, note=item.get('note', ''), ) @staticmethod def _restore_stock(instance): """恢复库存(用于编辑或拒绝时)""" for item in instance.items.filter(is_stock_item=True, stock__isnull=False): stock = WarehouseStock.objects.select_for_update().get(id=item.stock_id) stock.quantity += item.quantity stock.status = 'idle' stock.save(update_fields=['quantity', 'status'])