from __future__ import annotations from decimal import Decimal, InvalidOperation from typing import Any, Dict, List, Optional from django.conf import settings from django.db import transaction from django.core.exceptions import ValidationError from django.utils import timezone from basic_info import models as basic_info_models from . import models from . import services as business_services def render_pre_sales_order_created_markdown( *, pre_sales_order_id: str, human_id: str, created_at: str, sender_label: str, customer_name: str, warehouse_name: str, kind: str, items_count: int, ) -> str: followup_url_template = getattr( settings, "PRE_SALES_ORDER_CREATED_FOLLOWUP_URL_TEMPLATE", "", ) followup_url = "" if followup_url_template: try: followup_url = str(followup_url_template).format( pre_sales_order_id=str(pre_sales_order_id or "") ) except Exception: followup_url = "" followup_line = ( f"- **跟进**:[点击跟进]({followup_url})\n" f" {followup_url}\n" if followup_url else "" ) template = getattr( settings, "PRE_SALES_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE", ( "### 预销售单创建\n" "\n" "- **预销售单ID**:`{pre_sales_order_id}`\n" "- **订单编号**:`{human_id}`\n" "- **创建时间**:`{created_at}`\n" "- **发送者**:{sender_label}\n" "- **客户**:{customer_name}\n" "- **仓库**:{warehouse_name}\n" "- **类型**:{kind}\n" "- **明细条数**:`{items_count}`\n" "{followup_line}" ), ) created_at_str = (created_at or "").strip() if not created_at_str: created_at_str = timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S") return template.format( pre_sales_order_id=str(pre_sales_order_id or "-") or "-", human_id=str(human_id or "-") or "-", created_at=str(created_at_str), sender_label=str(sender_label or "系统自动发送"), customer_name=str(customer_name or "-"), warehouse_name=str(warehouse_name or "-"), kind=str(kind or "-"), items_count=int(items_count) if items_count is not None else 0, followup_line=str(followup_line), ) 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 convert_pre_sales_order_to_sales_order( *, merchant: basic_info_models.Merchant, pre_sales_order_id: int, operator: basic_info_models.Employee, created_by=None, order_date=None, ) -> models.SalesOrder: order = models.PreSalesOrder.objects.select_related( 'customer', 'warehouse', 'operator', 'created_by', ).prefetch_related('items', 'items__allocation_records').get( id=pre_sales_order_id, merchant=merchant, ) warehouse = order.warehouse if warehouse.mode != basic_info_models.WareHouseModeEnum.RESTRICT_IN_OUT: raise PermissionError('仅支持严进严出仓库模式') items_payload: List[Dict[str, Any]] = [] for item in order.items.all(): if not item.product_id: raise ValueError(f'明细 {item.id} 缺少 product_id') if item.quantity is None or str(item.quantity) == '': raise ValueError(f'明细 {item.id} 缺少 quantity') consume_ids: List[int] = [] for record in item.allocation_records.all(): if record.status != models.AllocationRecordStatusEnum.ACTIVE: continue consume_ids.extend(record.stock_id_list) if not consume_ids: raise ValueError(f'明细 {item.id} 缺少配货库存明细') items_payload.append( { 'product_id': item.product_id, 'price': '0', 'quantity': str(item.quantity), 'unit': item.unit or None, 'color': item.color, 'spec': item.spec, 'remarks': item.remarks, 'order_quantity': item.order_quantity, 'consume_detail_ids': consume_ids, } ) resolved_order_date = order_date or (order.created_at.date() if order.created_at else timezone.localdate()) return business_services.create_sales_order( merchant=merchant, customer=order.customer, order_date=resolved_order_date, warehouse=warehouse, operator=operator, items=items_payload, remarks=order.remarks or '', created_by=created_by, from_pre_sales_order_id=order.id, ) 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()