from __future__ import annotations from decimal import Decimal, InvalidOperation from typing import Any, Dict, List, Optional from django.db import transaction from django.core.exceptions import ValidationError from basic_info import models as basic_info_models from . import models def _to_decimal(value: Any, *, field_name: str) -> Decimal: if value is None or value == '': raise ValueError(f'{field_name} 不能为空') try: return Decimal(str(value)) except (InvalidOperation, ValueError) as exc: raise ValueError(f'{field_name} 格式不正确') from exc def _to_optional_int(value: Any) -> Optional[int]: if value is None or value == '': return None try: parsed = int(value) except (TypeError, ValueError) as exc: raise ValueError('order_quantity 必须为整数') from exc if parsed < 0: raise ValueError('order_quantity 不能为负数') return parsed def _to_optional_positive_int(value: Any, *, field_name: str) -> Optional[int]: if value is None or value == '': return None try: parsed = int(value) except (TypeError, ValueError) as exc: raise ValueError(f'{field_name} 必须为整数') from exc if parsed <= 0: raise ValueError(f'{field_name} 必须大于 0') return parsed def _normalize_pre_sales_items( *, merchant: basic_info_models.Merchant, items: List[Dict[str, Any]], ) -> List[Dict[str, Any]]: if not isinstance(items, list) or not items: raise ValueError('items 需要为非空数组') product_ids: List[int] = [] for item in items: if not isinstance(item, dict): raise ValueError('items 每一项需要为对象') product_id = item.get('product_id') or item.get('product') if not product_id: raise ValueError('明细缺少 product_id') try: product_ids.append(int(product_id)) except (TypeError, ValueError) as exc: raise ValueError('product_id 必须为整数') from exc products_by_id = basic_info_models.Product.objects.filter( merchant=merchant, id__in=product_ids, ).in_bulk(field_name='id') normalized: List[Dict[str, Any]] = [] for item in items: product_id = int(item.get('product_id') or item.get('product')) product = products_by_id.get(product_id) if not product: raise ValueError(f'产品 {product_id} 不存在') quantity = _to_decimal(item.get('quantity'), field_name='quantity') unit = (item.get('unit') or '').strip() if not unit: unit = str(product.unit) if not unit: raise ValueError('unit 不能为空') normalized.append( { 'product_id': product_id, 'product_name': (item.get('product_name') or product.name or '').strip(), 'color': item.get('color'), 'quantity': quantity, 'unit': unit, 'spec': item.get('spec'), 'quantity_of_rolls': item.get('quantity_of_rolls'), 'num_of_rolls': _to_optional_positive_int(item.get('num_of_rolls'), field_name='num_of_rolls'), 'order_quantity': _to_optional_int(item.get('order_quantity')), 'remarks': item.get('remarks'), } ) return normalized def _normalize_pre_purchase_items( *, merchant: basic_info_models.Merchant, items: List[Dict[str, Any]], ) -> List[Dict[str, Any]]: # 与预销售单明细字段保持一致的弱关联产品输入结构 return _normalize_pre_sales_items(merchant=merchant, items=items) def list_pre_sales_orders(*, merchant: basic_info_models.Merchant): return models.PreSalesOrder.objects.filter(merchant=merchant).select_related( 'merchant', 'customer', 'warehouse', 'created_by' ).prefetch_related('items') def get_pre_sales_order(*, merchant: basic_info_models.Merchant, pre_sales_order_id: int) -> models.PreSalesOrder: try: return models.PreSalesOrder.objects.select_related( 'merchant', 'customer', 'warehouse', 'created_by' ).prefetch_related('items').get(id=pre_sales_order_id, merchant=merchant) except models.PreSalesOrder.DoesNotExist as exc: raise ValueError('预销售单不存在') from exc def create_pre_sales_order( *, merchant: basic_info_models.Merchant, customer_id: int | str | None, warehouse_id: int | str | None, operator: basic_info_models.Employee, created_by=None, kind: int | str | None = None, items: List[Dict[str, Any]], remarks: str | None = '', ) -> models.PreSalesOrder: if not customer_id: raise ValueError('缺少客户 ID') if not warehouse_id: raise ValueError('缺少仓库 ID') try: customer_id_int = int(customer_id) except (TypeError, ValueError) as exc: raise ValueError('customer 必须为整数') from exc try: warehouse_id_int = int(warehouse_id) except (TypeError, ValueError) as exc: raise ValueError('warehouse 必须为整数') from exc kind_int: int | None if kind is None or kind == '': kind_int = None else: try: kind_int = int(kind) except (TypeError, ValueError) as exc: raise ValueError('kind 必须为整数') from exc try: customer = basic_info_models.Customer.objects.get(id=customer_id_int, merchant=merchant) except basic_info_models.Customer.DoesNotExist as exc: raise ValueError(f'客户 {customer_id_int} 不存在') from exc try: warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant) except basic_info_models.WareHouse.DoesNotExist as exc: raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc if operator.merchant_id != merchant.id: raise ValueError('经办人所属商户与预销售单所属商户不一致') normalized_items = _normalize_pre_sales_items(merchant=merchant, items=items) with transaction.atomic(): pre_sales_order = models.PreSalesOrder.objects.create( merchant=merchant, customer=customer, warehouse=warehouse, created_by=created_by, operator=operator, kind=kind_int or models.SalesOrderKindEnum.WHOLESALE, remarks=remarks, ) # enforce cross-merchant consistency try: pre_sales_order.full_clean() except ValidationError as exc: raise ValueError(str(exc)) from exc models.PreSalesOrderItem.objects.bulk_create( [ models.PreSalesOrderItem( pre_sales_order=pre_sales_order, product_id=item['product_id'], product_name=item.get('product_name') or '', color=item.get('color'), quantity=item.get('quantity'), unit=item.get('unit') or '', spec=item.get('spec'), quantity_of_rolls=item.get('quantity_of_rolls'), num_of_rolls=item.get('num_of_rolls') or 1, order_quantity=item.get('order_quantity'), remarks=item.get('remarks'), ) for item in normalized_items ] ) # domain event: pre sales order created # must be fired only after commit to avoid triggering on rollbacks def _send_created_signal(): try: from .signals import pre_sales_order_created pre_sales_order_created.send( sender=models.PreSalesOrder, instance=pre_sales_order, created_by=created_by, operator=operator, items_count=len(normalized_items), ) except Exception: import logging logging.getLogger(__name__).exception( '[business.pre_order_services] 触发 pre_sales_order_created signal 失败(已忽略)' ) transaction.on_commit(_send_created_signal) pre_sales_order.refresh_from_db() return pre_sales_order def update_pre_sales_order( *, pre_sales_order: models.PreSalesOrder, customer_id: int | str | None = None, warehouse_id: int | str | None = None, kind: int | str | None = None, items: List[Dict[str, Any]] | None = None, remarks: str | None = None, ) -> models.PreSalesOrder: merchant = pre_sales_order.merchant new_customer = pre_sales_order.customer if customer_id is not None: if customer_id == '': raise ValueError('customer 必须为整数') try: customer_id_int = int(customer_id) except (TypeError, ValueError) as exc: raise ValueError('customer 必须为整数') from exc try: new_customer = basic_info_models.Customer.objects.get(id=customer_id_int, merchant=merchant) except basic_info_models.Customer.DoesNotExist as exc: raise ValueError(f'客户 {customer_id_int} 不存在') from exc new_warehouse = pre_sales_order.warehouse if warehouse_id is not None: if warehouse_id == '': raise ValueError('warehouse 必须为整数') try: warehouse_id_int = int(warehouse_id) except (TypeError, ValueError) as exc: raise ValueError('warehouse 必须为整数') from exc try: new_warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant) except basic_info_models.WareHouse.DoesNotExist as exc: raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc if kind is None or kind == '': new_kind = pre_sales_order.kind else: try: new_kind = int(kind) except (TypeError, ValueError) as exc: raise ValueError('kind 必须为整数') from exc new_remarks = remarks if remarks is not None else pre_sales_order.remarks if items is None: raise ValueError('items 需要为非空数组') normalized_items = _normalize_pre_sales_items(merchant=merchant, items=items) with transaction.atomic(): pre_sales_order.customer = new_customer pre_sales_order.warehouse = new_warehouse pre_sales_order.kind = new_kind pre_sales_order.remarks = new_remarks try: pre_sales_order.full_clean() except ValidationError as exc: raise ValueError(str(exc)) from exc pre_sales_order.save(update_fields=['customer', 'warehouse', 'kind', 'remarks', 'updated_at']) pre_sales_order.items.all().delete() models.PreSalesOrderItem.objects.bulk_create( [ models.PreSalesOrderItem( pre_sales_order=pre_sales_order, product_id=item['product_id'], product_name=item.get('product_name') or '', color=item.get('color'), quantity=item.get('quantity'), unit=item.get('unit') or '', spec=item.get('spec'), quantity_of_rolls=item.get('quantity_of_rolls'), num_of_rolls=item.get('num_of_rolls') or 1, order_quantity=item.get('order_quantity'), remarks=item.get('remarks'), ) for item in normalized_items ] ) pre_sales_order.refresh_from_db() return pre_sales_order def delete_pre_sales_order(*, pre_sales_order: models.PreSalesOrder) -> None: with transaction.atomic(): pre_sales_order.delete() def list_pre_purchase_orders(*, merchant: basic_info_models.Merchant): return models.PrePurchaseOrder.objects.filter(merchant=merchant).select_related( 'merchant', 'supplier', 'warehouse', 'created_by' ).prefetch_related('items') def get_pre_purchase_order(*, merchant: basic_info_models.Merchant, pre_purchase_order_id: int) -> models.PrePurchaseOrder: try: return models.PrePurchaseOrder.objects.select_related( 'merchant', 'supplier', 'warehouse', 'created_by' ).prefetch_related('items').get(id=pre_purchase_order_id, merchant=merchant) except models.PrePurchaseOrder.DoesNotExist as exc: raise ValueError('预采购单不存在') from exc def create_pre_purchase_order( *, merchant: basic_info_models.Merchant, supplier_id: int | str | None, warehouse_id: int | str | None, operator: basic_info_models.Employee, created_by=None, kind: int | str | None = None, items: List[Dict[str, Any]], remarks: str | None = '', ) -> models.PrePurchaseOrder: if not supplier_id: raise ValueError('缺少供应商 ID') if not warehouse_id: raise ValueError('缺少仓库 ID') try: supplier_id_int = int(supplier_id) except (TypeError, ValueError) as exc: raise ValueError('supplier 必须为整数') from exc try: warehouse_id_int = int(warehouse_id) except (TypeError, ValueError) as exc: raise ValueError('warehouse 必须为整数') from exc kind_int: int | None if kind is None or kind == '': kind_int = None else: try: kind_int = int(kind) except (TypeError, ValueError) as exc: raise ValueError('kind 必须为整数') from exc try: supplier = basic_info_models.Supplier.objects.get(id=supplier_id_int, merchant=merchant) except basic_info_models.Supplier.DoesNotExist as exc: raise ValueError(f'供应商 {supplier_id_int} 不存在') from exc try: warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant) except basic_info_models.WareHouse.DoesNotExist as exc: raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc if operator.merchant_id != merchant.id: raise ValueError('经办人所属商户与预采购单所属商户不一致') normalized_items = _normalize_pre_purchase_items(merchant=merchant, items=items) with transaction.atomic(): pre_purchase_order = models.PrePurchaseOrder.objects.create( merchant=merchant, supplier=supplier, warehouse=warehouse, created_by=created_by, operator=operator, kind=kind_int or models.PurchaseOrderKindEnum.WHOLESALE, remarks=remarks, ) try: pre_purchase_order.full_clean() except ValidationError as exc: raise ValueError(str(exc)) from exc models.PrePurchaseOrderItem.objects.bulk_create( [ models.PrePurchaseOrderItem( pre_purchase_order=pre_purchase_order, product_id=item['product_id'], product_name=item.get('product_name') or '', color=item.get('color'), quantity=item.get('quantity'), unit=item.get('unit') or '', spec=item.get('spec'), quantity_of_rolls=item.get('quantity_of_rolls'), num_of_rolls=item.get('num_of_rolls') or 1, order_quantity=item.get('order_quantity'), remarks=item.get('remarks'), ) for item in normalized_items ] ) pre_purchase_order.refresh_from_db() return pre_purchase_order def update_pre_purchase_order( *, pre_purchase_order: models.PrePurchaseOrder, supplier_id: int | str | None = None, warehouse_id: int | str | None = None, kind: int | str | None = None, items: List[Dict[str, Any]] | None = None, remarks: str | None = None, ) -> models.PrePurchaseOrder: merchant = pre_purchase_order.merchant new_supplier = pre_purchase_order.supplier if supplier_id is not None: if supplier_id == '': raise ValueError('supplier 必须为整数') try: supplier_id_int = int(supplier_id) except (TypeError, ValueError) as exc: raise ValueError('supplier 必须为整数') from exc try: new_supplier = basic_info_models.Supplier.objects.get(id=supplier_id_int, merchant=merchant) except basic_info_models.Supplier.DoesNotExist as exc: raise ValueError(f'供应商 {supplier_id_int} 不存在') from exc new_warehouse = pre_purchase_order.warehouse if warehouse_id is not None: if warehouse_id == '': raise ValueError('warehouse 必须为整数') try: warehouse_id_int = int(warehouse_id) except (TypeError, ValueError) as exc: raise ValueError('warehouse 必须为整数') from exc try: new_warehouse = basic_info_models.WareHouse.objects.get(id=warehouse_id_int, merchant=merchant) except basic_info_models.WareHouse.DoesNotExist as exc: raise ValueError(f'仓库 {warehouse_id_int} 不存在') from exc if kind is None or kind == '': new_kind = pre_purchase_order.kind else: try: new_kind = int(kind) except (TypeError, ValueError) as exc: raise ValueError('kind 必须为整数') from exc new_remarks = remarks if remarks is not None else pre_purchase_order.remarks if items is None: raise ValueError('items 需要为非空数组') normalized_items = _normalize_pre_purchase_items(merchant=merchant, items=items) with transaction.atomic(): pre_purchase_order.supplier = new_supplier pre_purchase_order.warehouse = new_warehouse pre_purchase_order.kind = new_kind pre_purchase_order.remarks = new_remarks try: pre_purchase_order.full_clean() except ValidationError as exc: raise ValueError(str(exc)) from exc pre_purchase_order.save(update_fields=['supplier', 'warehouse', 'kind', 'remarks', 'updated_at']) pre_purchase_order.items.all().delete() models.PrePurchaseOrderItem.objects.bulk_create( [ models.PrePurchaseOrderItem( pre_purchase_order=pre_purchase_order, product_id=item['product_id'], product_name=item.get('product_name') or '', color=item.get('color'), quantity=item.get('quantity'), unit=item.get('unit') or '', spec=item.get('spec'), quantity_of_rolls=item.get('quantity_of_rolls'), num_of_rolls=item.get('num_of_rolls') or 1, order_quantity=item.get('order_quantity'), remarks=item.get('remarks'), ) for item in normalized_items ] ) pre_purchase_order.refresh_from_db() return pre_purchase_order def delete_pre_purchase_order(*, pre_purchase_order: models.PrePurchaseOrder) -> None: with transaction.atomic(): pre_purchase_order.delete()