""" Shipment API 序列化器 """ from rest_framework import serializers from shipment.models import ( ExternalFinishedProduct, SalesItem, Shipment, ShipmentStatus, ShipmentDelivery, ShipmentDeliveryStatus, ) def _build_nested_sales_item_context(items): customer_ids = {item.customer_id for item in items if item.customer_id} printing_job_ids = {item.printing_job_id for item in items if item.printing_job_id} customer_name_map = {} if customer_ids: from basic_info.models import Customer customer_name_map = dict( Customer.objects.filter(id__in=customer_ids).values_list("id", "name") ) printing_order_map = {} external_order_id_map = {} product_image_map = {} if printing_job_ids: from printing.models import PrintingJob printing_jobs = list( PrintingJob.objects.filter(id__in=printing_job_ids).select_related( "printing_order", "product" ) ) printing_order_map = {job.id: job.printing_order_id for job in printing_jobs} external_order_id_map = { job.id: getattr(job.printing_order, "external_order_id", None) for job in printing_jobs } for job in printing_jobs: if not getattr(job, "product", None): product_image_map[job.id] = None continue primary_url = job.product.get_primary_image_url() if primary_url: product_image_map[job.id] = primary_url elif job.product.image: product_image_map[job.id] = job.product.image.url else: product_image_map[job.id] = None return { "customer_name_map": customer_name_map, "printing_order_map": printing_order_map, "external_order_id_map": external_order_id_map, "product_image_map": product_image_map, } class ShipmentSerializer(serializers.ModelSerializer): """ 出货单序列化器(只读,用于返回数据) """ customer_name = serializers.CharField(source="customer.name", read_only=True) created_by_id = serializers.IntegerField( source="created_by.id", read_only=True, allow_null=True ) created_by_name = serializers.SerializerMethodField() items_count = serializers.SerializerMethodField() cancelled_by_id = serializers.IntegerField( source="cancelled_by.id", read_only=True, allow_null=True ) cancelled_by_name = serializers.SerializerMethodField() approved_by_id = serializers.IntegerField( source="approved_by.id", read_only=True, allow_null=True ) approved_by_name = serializers.SerializerMethodField() status_display = serializers.CharField(source="get_status_display", read_only=True) external_finished_products_count = serializers.SerializerMethodField() merchant_id = serializers.IntegerField(source="merchant.id", read_only=True) merchant_name = serializers.CharField(source="merchant.name", read_only=True) sales_items = serializers.SerializerMethodField() external_finished_products = serializers.SerializerMethodField() class Meta: model = Shipment fields = [ "id", "merchant_id", "merchant_name", "customer", "customer_name", "shipment_date", "address", "contact_name", "contact_phone", "area", "remark", "status", "status_display", "external_id", "status_modified_at", "cancelled_by_id", "cancelled_by_name", "approved_by_id", "approved_by_name", "items_count", "created_by_id", "created_by_name", "external_finished_products_count", "sales_items", "external_finished_products", "created_at", "updated_at", ] read_only_fields = ["id", "created_at", "updated_at"] def get_created_by_name(self, obj): if obj.created_by: employee = getattr(obj.created_by, "employee", None) if employee: return employee.name return obj.created_by.username return None def get_items_count(self, obj): return obj.items.filter(delete_at__isnull=True).count() def get_cancelled_by_name(self, obj): if obj.cancelled_by: employee = getattr(obj.cancelled_by, "employee", None) if employee: return employee.name return obj.cancelled_by.username return None def get_external_finished_products_count(self, obj): return obj.external_finished_products.count() def get_approved_by_name(self, obj): if obj.approved_by: employee = getattr(obj.approved_by, "employee", None) if employee: return employee.name return obj.approved_by.username return None def get_sales_items(self, obj): """ 出货单关联的销售品明细(无则返回空数组)。 """ # 优先使用 prefetch 的 related manager;兜底为 none() rel = getattr(obj, "items", None) items = list(rel.filter(delete_at__isnull=True)) if rel is not None else [] serializer_context = dict(self.context) serializer_context.update(_build_nested_sales_item_context(items)) return SalesItemDetailSerializer( items, many=True, context=serializer_context, ).data def get_external_finished_products(self, obj): """ 出货单关联的外部成品表明细(无则返回空数组)。 """ rel = getattr(obj, "external_finished_products", None) products = list(rel.all()) if rel is not None else [] return ExternalFinishedProductSerializer(products, many=True).data class ExternalFinishedProductSerializer(serializers.ModelSerializer): """ 外部成品表序列化器(只读) """ created_by_id = serializers.IntegerField( source="created_by.id", read_only=True, allow_null=True ) created_by_name = serializers.SerializerMethodField() class Meta: model = ExternalFinishedProduct fields = [ "id", "style_name", "num_of_rolls", "remark", "created_at", "created_by_id", "created_by_name", ] read_only_fields = fields def get_created_by_name(self, obj): if obj.created_by: employee = getattr(obj.created_by, "employee", None) if employee: return employee.name return obj.created_by.username return None class ShipmentCreateNormalSerializer(serializers.Serializer): """ 出货单创建序列化器(普通版) """ customer = serializers.IntegerField(help_text="客户ID") shipment_date = serializers.DateField(help_text="出货日期") address = serializers.CharField( max_length=255, required=False, default="", allow_blank=True, help_text="地址(可选)", ) contact_name = serializers.CharField( max_length=100, required=False, default="", allow_blank=True, help_text="联系人(可选)", ) contact_phone = serializers.CharField( max_length=50, required=False, default="", allow_blank=True, help_text="联系电话(可选)", ) area = serializers.CharField( max_length=30, required=False, default="", allow_blank=True, help_text="出货地区(可选)", ) remark = serializers.CharField( required=False, default="", allow_blank=True, help_text="备注" ) sales_items = serializers.ListField( child=serializers.IntegerField(), required=False, default=list, help_text="要关联的销售品ID列表", ) def validate_sales_items(self, value): # 去重 return list(set(value)) if value else [] class ExternalFinishedProductInputSerializer(serializers.Serializer): """外部成品表写入结构(external create 专用)""" style_name = serializers.CharField(max_length=200) num_of_rolls = serializers.IntegerField(min_value=0) remark = serializers.CharField( max_length=200, required=False, allow_blank=True, allow_null=True, default="" ) class ShipmentCreateExternalSerializer(serializers.Serializer): """ 出货单创建序列化器(external 版) 特点: - 不绑定任何 SalesItem - 必须提供 external_id - 同时写入 ExternalFinishedProduct 列表并关联到 Shipment """ customer = serializers.IntegerField(help_text="客户ID") shipment_date = serializers.DateField(help_text="出货日期") address = serializers.CharField( max_length=255, required=False, default="", allow_blank=True, help_text="地址(可选)", ) contact_name = serializers.CharField( max_length=100, required=False, default="", allow_blank=True, help_text="联系人(可选)", ) contact_phone = serializers.CharField( max_length=50, required=False, default="", allow_blank=True, help_text="联系电话(可选)", ) area = serializers.CharField( max_length=30, required=False, default="", allow_blank=True, help_text="出货地区(可选)", ) remark = serializers.CharField( required=False, default="", allow_blank=True, help_text="备注" ) external_id = serializers.CharField(max_length=120, help_text="外部订单号(必填)") external_finished_products = serializers.ListField( child=ExternalFinishedProductInputSerializer(), required=True, help_text="外部成品表结构数组(必填)", ) def validate_external_id(self, value): value = (value or "").strip() if not value: raise serializers.ValidationError("external_id 不能为空") return value def validate_external_finished_products(self, value): if not value: raise serializers.ValidationError("external_finished_products 不能为空") return value class ShipmentUpdateSerializer(serializers.Serializer): """ 出货单更新序列化器(PATCH/PUT) 说明:本次新增字段 area,需要保证更新入口可写入并回显。 """ customer = serializers.IntegerField(required=False, help_text="客户ID(可选)") shipment_date = serializers.DateField(required=False, help_text="出货日期(可选)") address = serializers.CharField( max_length=255, required=False, allow_blank=True, help_text="地址(可选)" ) contact_name = serializers.CharField( max_length=100, required=False, allow_blank=True, help_text="联系人(可选)" ) contact_phone = serializers.CharField( max_length=50, required=False, allow_blank=True, help_text="联系电话(可选)" ) area = serializers.CharField( max_length=30, required=False, allow_blank=True, help_text="出货地区(可选)" ) remark = serializers.CharField( required=False, allow_blank=True, help_text="备注(可选)" ) external_id = serializers.CharField( max_length=120, required=False, allow_blank=True, allow_null=True, help_text="外部订单号(可选)", ) class ShipmentStatusUpdateSerializer(serializers.Serializer): status = serializers.ChoiceField( choices=ShipmentStatus.choices, help_text="出货单状态(1=草稿(未发布), 2=已发布, 3=已取消, 4=已驳回, 5=已审核)", ) class SalesItemSerializer(serializers.Serializer): """ 销售品序列化器(只读) 用于返回销售品数据 """ id = serializers.IntegerField(read_only=True) name = serializers.CharField(read_only=True) quantity = serializers.DecimalField(max_digits=12, decimal_places=2, read_only=True) unit = serializers.IntegerField(read_only=True) unit_display = serializers.SerializerMethodField() position = serializers.CharField(read_only=True) remark = serializers.CharField(read_only=True) printing_job_id = serializers.IntegerField(read_only=True) printing_order_id = serializers.SerializerMethodField() external_order_id = serializers.SerializerMethodField() customer_id = serializers.IntegerField(read_only=True) customer_name = serializers.SerializerMethodField() shipment_id = serializers.IntegerField( source="shipment.id", read_only=True, allow_null=True ) shipment_date = serializers.DateField( source="shipment.shipment_date", read_only=True, allow_null=True ) merge_remark = serializers.JSONField(read_only=True, allow_null=True) created_at = serializers.DateTimeField(read_only=True) created_by_id = serializers.IntegerField( source="created_by.id", read_only=True, allow_null=True ) created_by_name = serializers.SerializerMethodField() def get_unit_display(self, obj): return obj.get_unit_display() def get_printing_order_id(self, obj): printing_order_map = self.context.get("printing_order_map", {}) if obj.printing_job_id in printing_order_map: return printing_order_map[obj.printing_job_id] printing_job = obj.get_printing_job() if printing_job and getattr(printing_job, "printing_order_id", None): return printing_job.printing_order_id return None def get_external_order_id(self, obj): external_order_id_map = self.context.get("external_order_id_map", {}) if obj.printing_job_id in external_order_id_map: return external_order_id_map[obj.printing_job_id] printing_job = obj.get_printing_job() if printing_job and getattr(printing_job.printing_order, "external_order_id", None): return printing_job.printing_order.external_order_id return None def get_customer_name(self, obj): customer_name_map = self.context.get("customer_name_map", {}) if obj.customer_id in customer_name_map: return customer_name_map[obj.customer_id] customer = obj.get_customer() return customer.name if customer else None def get_created_by_name(self, obj): if obj.created_by: employee = getattr(obj.created_by, "employee", None) if employee: return employee.name return obj.created_by.username return None class SalesItemDetailSerializer(SalesItemSerializer): """ 销售品详情序列化器。 在列表字段基础上补充关联印染任务的产品图片。 """ product_image_url = serializers.SerializerMethodField() def get_product_image_url(self, obj): product_image_map = self.context.get("product_image_map", {}) if obj.printing_job_id in product_image_map: return product_image_map[obj.printing_job_id] printing_job = obj.get_printing_job() if not printing_job or not getattr(printing_job, "product", None): return None primary_url = printing_job.product.get_primary_image_url() if primary_url: return primary_url if printing_job.product.image: request = self.context.get("request") if request: return request.build_absolute_uri(printing_job.product.image.url) return printing_job.product.image.url return None class ShipmentSalesItemCustomerSerializer(serializers.Serializer): """ 由未出货销售品反推的客户摘要序列化器。 """ customer_id = serializers.IntegerField(source="id", read_only=True) customer_name = serializers.CharField(source="name", read_only=True) mobile = serializers.CharField(read_only=True, allow_null=True) area = serializers.CharField(read_only=True, allow_null=True) unshipped_sales_items_count = serializers.IntegerField(read_only=True) class ShipmentDeliveryShipmentSummarySerializer(serializers.ModelSerializer): customer_name = serializers.CharField(source="customer.name", read_only=True) status_display = serializers.CharField(source="get_status_display", read_only=True) class Meta: model = Shipment fields = [ "id", "customer", "customer_name", "shipment_date", "status", "status_display", "external_id", ] read_only_fields = fields class ShipmentDeliverySerializer(serializers.ModelSerializer): status_display = serializers.CharField(source="get_status_display", read_only=True) merchant_id = serializers.IntegerField(source="merchant.id", read_only=True) merchant_name = serializers.CharField(source="merchant.name", read_only=True) created_by_id = serializers.IntegerField( source="created_by.id", read_only=True, allow_null=True ) created_by_name = serializers.SerializerMethodField() operator_id = serializers.IntegerField( source="operator.id", read_only=True, allow_null=True ) operator_name = serializers.CharField(source="operator.name", read_only=True, allow_null=True) cancelled_by_id = serializers.IntegerField( source="cancelled_by.id", read_only=True, allow_null=True ) cancelled_by_name = serializers.SerializerMethodField() shipments_count = serializers.SerializerMethodField() shipments = serializers.SerializerMethodField() class Meta: model = ShipmentDelivery fields = [ "id", "merchant_id", "merchant_name", "driver_name", "vehicle_trip", "contact_phone", "vehicle_capacity", "remark", "internal_remark", "status", "status_display", "started_at", "delivered_at", "cancelled_at", "shipments_count", "shipments", "created_by_id", "created_by_name", "operator_id", "operator_name", "cancelled_by_id", "cancelled_by_name", "created_at", "updated_at", ] read_only_fields = fields def get_created_by_name(self, obj): if obj.created_by: employee = getattr(obj.created_by, "employee", None) if employee: return employee.name return obj.created_by.username return None def get_cancelled_by_name(self, obj): if obj.cancelled_by: employee = getattr(obj.cancelled_by, "employee", None) if employee: return employee.name return obj.cancelled_by.username return None def get_shipments_count(self, obj): return obj.shipments.count() def get_shipments(self, obj): return ShipmentDeliveryShipmentSummarySerializer( obj.shipments.order_by("id"), many=True, ).data class ShipmentDeliveryCreateSerializer(serializers.Serializer): driver_name = serializers.CharField(max_length=100, help_text="司机名") vehicle_trip = serializers.CharField(max_length=100, help_text="车次") contact_phone = serializers.CharField( max_length=50, required=False, default="", allow_blank=True, help_text="联系电话(可选)", ) vehicle_capacity = serializers.CharField( max_length=100, required=False, default="", allow_blank=True, help_text="车辆容量(可选)", ) remark = serializers.CharField( max_length=200, required=False, default="", allow_blank=True, help_text="备注(可选)", ) internal_remark = serializers.CharField( max_length=200, required=False, default="", allow_blank=True, help_text="内部备注(可选)", ) shipments = serializers.ListField( child=serializers.IntegerField(), required=False, default=list, help_text="要关联的出货单ID列表", ) def validate_shipments(self, value): return list(dict.fromkeys(value)) if value else [] class ShipmentDeliveryUpdateSerializer(serializers.Serializer): driver_name = serializers.CharField( max_length=100, required=False, allow_blank=False, help_text="司机名(可选)" ) vehicle_trip = serializers.CharField( max_length=100, required=False, allow_blank=False, help_text="车次(可选)" ) contact_phone = serializers.CharField( max_length=50, required=False, allow_blank=True, help_text="联系电话(可选)" ) vehicle_capacity = serializers.CharField( max_length=100, required=False, allow_blank=True, help_text="车辆容量(可选)" ) remark = serializers.CharField( max_length=200, required=False, allow_blank=True, help_text="备注(可选)" ) internal_remark = serializers.CharField( max_length=200, required=False, allow_blank=True, help_text="内部备注(可选)" ) shipments = serializers.ListField( child=serializers.IntegerField(), required=False, help_text="要绑定的出货单ID列表(可选,传入即视为替换)", ) def validate_shipments(self, value): return list(dict.fromkeys(value)) if value else [] class ShipmentDeliveryStatusUpdateSerializer(serializers.Serializer): status = serializers.ChoiceField( choices=[ (ShipmentDeliveryStatus.PENDING, ShipmentDeliveryStatus.PENDING.label), (ShipmentDeliveryStatus.IN_TRANSIT, ShipmentDeliveryStatus.IN_TRANSIT.label), (ShipmentDeliveryStatus.DELIVERED, ShipmentDeliveryStatus.DELIVERED.label), ], help_text="送货单状态(1=待送货, 2=送货中, 3=已送达)", ) class ShipmentDeliveryBindShipmentsSerializer(serializers.Serializer): shipments = serializers.ListField( child=serializers.IntegerField(), required=True, help_text="要追加绑定到当前送货单的出货单ID列表", ) def validate_shipments(self, value): if not value: raise serializers.ValidationError("shipments 不能为空") return list(dict.fromkeys(value)) class SalesItemCreateSerializer(serializers.Serializer): """ 销售品创建序列化器 用于手动创建销售品(当自动转化开关关闭时使用) """ printing_job_id = serializers.IntegerField( required=True, min_value=1, help_text="生产任务ID(必填)" ) name = serializers.CharField( max_length=200, required=True, help_text="销售品名称(必填)" ) quantity = serializers.CharField( max_length=20, required=True, help_text="数量(必填,支持小数,如:100.50)" ) unit = serializers.IntegerField( required=True, help_text="单位(必填):1=米, 2=件, 3=码, 4=个" ) customer_id = serializers.IntegerField( required=False, allow_null=True, help_text="客户ID(可选,默认从生产订单获取)" ) remark = serializers.CharField( max_length=200, required=False, allow_blank=True, default="", help_text="备注(可选)", ) position = serializers.CharField( max_length=200, required=False, allow_blank=True, default="", help_text="货位(可选)", ) merge_remark = serializers.JSONField( required=False, allow_null=True, default=None, help_text="合卷备注JSON(可选)", ) _MERGE_REMARK_REQUIRED_KEYS = { "merge_type": str, "jobs": list, "main_job": int, "quantity": str, "unit": str, "job_count": int, } def validate_merge_remark(self, value): """严格校验 merge_remark 的结构""" if value is None: return value if not isinstance(value, dict): raise serializers.ValidationError("merge_remark 必须是一个 JSON 对象") missing = set(self._MERGE_REMARK_REQUIRED_KEYS) - set(value.keys()) if missing: raise serializers.ValidationError(f"缺少必填字段: {sorted(missing)}") extra = set(value.keys()) - set(self._MERGE_REMARK_REQUIRED_KEYS) if extra: raise serializers.ValidationError(f"包含未知字段: {sorted(extra)}") for key, expected_type in self._MERGE_REMARK_REQUIRED_KEYS.items(): if not isinstance(value[key], expected_type): raise serializers.ValidationError( f"字段 '{key}' 类型错误,期望 {expected_type.__name__}," f"实际 {type(value[key]).__name__}" ) # jobs 列表内的元素必须都是整数 if not all(isinstance(j, int) for j in value["jobs"]): raise serializers.ValidationError("jobs 列表中的元素必须为整数") if len(value["jobs"]) < 2: raise serializers.ValidationError("jobs 列表至少需要包含 2 个子单ID") if value["main_job"] not in value["jobs"]: raise serializers.ValidationError("main_job 必须是 jobs 列表中的一个成员") if value["job_count"] != len(value["jobs"]): raise serializers.ValidationError( f"job_count ({value['job_count']}) 与 jobs 长度 ({len(value['jobs'])}) 不一致" ) return value def validate_unit(self, value): """验证单位值是否在允许范围内""" from shipment.models import UnitChoices valid_units = [choice[0] for choice in UnitChoices.choices] if value not in valid_units: raise serializers.ValidationError( f"单位值无效。可选值:{dict(UnitChoices.choices)}" ) return value def create(self, validated_data): """创建销售品""" from shipment.services import create_sales_item # 获取当前用户(从context传入) created_by = self.context["request"].user return create_sales_item( printing_job_id=validated_data["printing_job_id"], name=validated_data["name"], quantity=validated_data["quantity"], unit=validated_data["unit"], created_by=created_by, customer_id=validated_data.get("customer_id"), remark=validated_data.get("remark", ""), position=validated_data.get("position", ""), merge_remark=validated_data.get("merge_remark"), ) class SalesItemUpdateSerializer(serializers.Serializer): """ 销售品更新序列化器。 当前仅开放数量、备注、货位的修改。 """ quantity = serializers.CharField( max_length=20, required=False, help_text="数量(可选,支持小数)", ) remark = serializers.CharField( max_length=200, required=False, allow_blank=True, help_text="备注(可选)", ) position = serializers.CharField( max_length=200, required=False, allow_blank=True, help_text="货位(可选)", ) def validate(self, attrs): if not attrs: raise serializers.ValidationError("至少提供一个可修改字段") return attrs class SalesItemRebuildSerializer(serializers.Serializer): """ 销售品重建序列化器。 """ new_printing_job_id = serializers.IntegerField( required=True, min_value=1, help_text="新的生产任务ID(必填)", ) quantity = serializers.CharField( max_length=20, required=False, allow_blank=False, help_text="新的数量(可选,未传则沿用原销售品数量)", )