""" Shipment 模块业务逻辑层 """ from __future__ import annotations from typing import List from django.db import transaction from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Subquery from django.db.models.functions import Coalesce from shipment.models import ExternalFinishedProduct, SalesItem, Shipment def get_customers_with_unshipped_sales_items(*, merchant) -> QuerySet: """ 查询当前商户下“存在未出货销售品”的客户列表。 返回 Customer 查询集,并附带: - unshipped_sales_items_count: 该客户未出货销售品数量 """ from basic_info.models import Customer base_sales_items = SalesItem.objects.filter( merchant=merchant, shipment__isnull=True, customer_id=OuterRef("pk"), ) count_subquery = ( base_sales_items.values("customer_id") .annotate(total=Count("id")) .values("total")[:1] ) return ( Customer.objects.filter(merchant=merchant) .annotate(has_unshipped_sales_items=Exists(base_sales_items)) .filter(has_unshipped_sales_items=True) .annotate( unshipped_sales_items_count=Coalesce( Subquery(count_subquery, output_field=IntegerField()), 0, ) ) .order_by("id") ) def get_sales_items_by_printing_order( printing_order_id: int, include_already_has_shipment: bool = False, ) -> QuerySet[SalesItem]: """ 通过生产订单ID查询对应的销售品 Args: printing_order_id: 生产订单ID include_already_has_shipment: 是否包含已关联出货单的销售品,默认为 False Returns: SalesItem 查询集 """ from printing.models import PrintingJob # 1. 获取该生产订单下所有 PrintingJob 的 ID job_ids = PrintingJob.objects.filter( printing_order_id=printing_order_id ).values_list("id", flat=True) # 2. 查询 SalesItem,过滤 printing_job_id 在这些 job_ids 中 queryset = SalesItem.objects.filter(printing_job_id__in=list(job_ids)) # 3. 根据参数决定是否过滤已出货的销售品 if not include_already_has_shipment: queryset = queryset.filter(shipment__isnull=True) return queryset.select_related("shipment").order_by("id") def get_sales_items_by_customer( *, merchant, customer_id: int, include_already_has_shipment: bool = False, external_order_id: str | None = None, ) -> QuerySet[SalesItem]: """ 通过客户ID查询对应销售品。 仅返回当前商户下、customer_id 匹配的销售品。 """ queryset = SalesItem.objects.filter( merchant=merchant, customer_id=customer_id, ) if external_order_id: from printing.models import PrintingJob printing_job_ids = PrintingJob.objects.filter( merchant=merchant, printing_order__external_order_id=external_order_id, ).values_list("id", flat=True) queryset = queryset.filter(printing_job_id__in=list(printing_job_ids)) if not include_already_has_shipment: queryset = queryset.filter(shipment__isnull=True) return queryset.select_related("shipment").order_by("id") @transaction.atomic def create_shipment( customer_id: int, shipment_date, sales_item_ids: List[int], created_by, remark: str = "", area: str = "", ) -> Shipment: """ 创建出货单并关联销售品 Args: customer_id: 客户ID shipment_date: 出货日期 sales_item_ids: 要关联的销售品ID列表 created_by: 创建人 remark: 备注 Returns: 创建的 Shipment 实例 Raises: ValueError: 如果销售品不存在或已被关联到其他出货单 """ from basic_info.models import Customer # 验证客户存在 try: customer = Customer.objects.get(id=customer_id) except Customer.DoesNotExist: raise ValueError(f"客户 {customer_id} 不存在") # merchant 隔离:必须能解析出当前用户 merchant emp = getattr(created_by, "employee", None) merchant = getattr(emp, "merchant", None) if emp else None if not merchant: # superuser 也必须绑定 merchant(避免产生无法隔离的数据) raise ValueError("用户未关联商户,无法创建出货单") if customer.merchant_id != merchant.id: raise ValueError("无权限为该客户创建出货单") # 验证销售品 if sales_item_ids: # 查询销售品 sales_items = SalesItem.objects.filter(id__in=sales_item_ids) found_ids = set(sales_items.values_list("id", flat=True)) missing_ids = set(sales_item_ids) - found_ids if missing_ids: raise ValueError(f"以下销售品不存在: {list(missing_ids)}") # 检查是否有已关联出货单的销售品 already_shipped = sales_items.filter(shipment__isnull=False) if already_shipped.exists(): shipped_ids = list(already_shipped.values_list("id", flat=True)) raise ValueError(f"以下销售品已关联到其他出货单: {shipped_ids}") # 创建出货单 shipment = Shipment.objects.create( merchant=merchant, customer=customer, shipment_date=shipment_date, area=(area or "").strip(), remark=remark, created_by=created_by, ) # 关联销售品 if sales_item_ids: updated = SalesItem.objects.filter( id__in=sales_item_ids, merchant=merchant ).update(shipment=shipment) if updated != len(sales_item_ids): raise ValueError("存在不属于当前商户的销售品,无法关联到出货单") return shipment @transaction.atomic def create_external_shipment( customer_id: int, shipment_date, external_id: str, external_finished_products: List[dict], created_by, remark: str = "", area: str = "", ) -> Shipment: """ 创建出货单(external 版),并批量写入外部成品表并关联到出货单。 特点: - 不绑定任何 SalesItem - external_id 必填 - external_finished_products 必填(至少 1 条) """ from basic_info.models import Customer # 验证客户存在 try: customer = Customer.objects.get(id=customer_id) except Customer.DoesNotExist: raise ValueError(f"客户 {customer_id} 不存在") # merchant 隔离 emp = getattr(created_by, "employee", None) merchant = getattr(emp, "merchant", None) if emp else None if not merchant: raise ValueError("用户未关联商户,无法创建出货单") if customer.merchant_id != merchant.id: raise ValueError("无权限为该客户创建出货单") external_id = (external_id or "").strip() if not external_id: raise ValueError("external_id 不能为空") if not external_finished_products: raise ValueError("external_finished_products 不能为空") shipment = Shipment.objects.create( merchant=merchant, customer=customer, shipment_date=shipment_date, area=(area or "").strip(), remark=remark, created_by=created_by, external_id=external_id, ) objs = [] for item in external_finished_products: objs.append( ExternalFinishedProduct( shipment=shipment, style_name=item.get("style_name", ""), num_of_rolls=item.get("num_of_rolls", 0), remark=item.get("remark") or "", created_by=created_by, ) ) ExternalFinishedProduct.objects.bulk_create(objs) return shipment @transaction.atomic def create_sales_item( printing_job_id: int, name: str, quantity: str, unit: int, created_by, customer_id: int | None = None, remark: str = "", position: str = "", ) -> SalesItem: """ 手动创建销售品 Args: printing_job_id: 生产任务ID(必填) name: 销售品名称 quantity: 数量(字符串,会被转换为Decimal) unit: 单位(1=米, 2=件, 3=码, 4=个) created_by: 创建人 customer_id: 客户ID(可选) remark: 备注(可选) position: 货位(可选) Returns: 创建的 SalesItem 实例 Raises: ValueError: 如果生产任务不存在或不属于当前商户 """ from decimal import Decimal, InvalidOperation from printing.models import PrintingJob # 获取当前用户的商户 emp = getattr(created_by, "employee", None) merchant = getattr(emp, "merchant", None) if emp else None if not merchant: raise ValueError("用户未关联商户,无法创建销售品") # 验证生产任务存在且属于当前商户 try: printing_job = PrintingJob.objects.select_related("printing_order").get( id=printing_job_id, merchant=merchant, ) except PrintingJob.DoesNotExist: raise ValueError(f"生产任务 {printing_job_id} 不存在或不属于当前商户") # 转换数量为Decimal try: quantity_decimal = Decimal(str(quantity)) except (InvalidOperation, ValueError, TypeError) as e: raise ValueError(f"数量 {quantity} 格式无效: {e}") # 如果未提供客户ID,尝试从主订单获取 if ( customer_id is None and printing_job.printing_order and printing_job.printing_order.customer ): customer_id = printing_job.printing_order.customer_id # 创建销售品 sales_item = SalesItem.objects.create( shipment=None, # 初始状态:待分配 merchant=merchant, name=name, quantity=quantity_decimal, unit=unit, created_by=created_by, printing_job_id=printing_job.id, customer_id=customer_id, remark=remark, position=position, ) return sales_item