""" PrintingOrder -> docx template context mapping. This is intentionally separated from `printing/services.py` (WeCom logic) to keep concerns clear. """ from __future__ import annotations import re from collections.abc import Mapping from datetime import datetime from decimal import Decimal from typing import Any from django.utils import timezone from printing.models import PrintingOrder def _safe_str(v: Any) -> str: if v is None: return "" return str(v) def _fmt_date(dt: datetime | None) -> str: if not dt: return "" try: return timezone.localtime(dt).strftime("%Y-%m-%d") except Exception: return str(dt) def _pick_warehouse_name(order: PrintingOrder) -> str: """ PrintingOrder 模型本身没有 warehouse 字段,这里做一个“尽量饱满”的推断: - 优先取 order.merchant.warehouses.first().name - 否则空字符串 """ m = getattr(order, "merchant", None) if not m: return "" try: wh = m.warehouses.order_by("id").first() return _safe_str(getattr(wh, "name", "")).strip() except Exception: return "" def _detect_max_line_index(placeholders: set[str]) -> int: """ Detect max line index from placeholders like: 打印-销售单(8*9).产品名称17 打印-销售单(8*9).销售数量32 """ max_n = 0 for k in placeholders or set(): m = re.search(r"(产品名称|幅宽|规格|销售数量|单位)(\d+)$", k) if not m: continue try: n = int(m.group(2)) except Exception: continue max_n = max(max_n, n) return max_n def build_printing_order_docx_context( *, order: PrintingOrder, placeholders: set[str] | None = None, printer_label: str = "", ) -> dict[str, str]: """ Build context mapping for this repo's sales/printing docx template placeholders. This function tries to be "data rich" for testing: - Fill header fields: 客户名称/仓库名称/日期/页码 - Fill line items from `PrintingOrder.printing_jobs` - Fill per-8-line subtotal: 数量和8/16/24/... - Fill brace placeholders: 备注/打印人 """ prefix = "打印-销售单(8*9)." ph = placeholders or set() max_line = _detect_max_line_index(ph) or 32 customer_name = _safe_str(getattr(getattr(order, "customer", None), "name", "")) warehouse_name = _pick_warehouse_name(order) order_date = _fmt_date(getattr(order, "created_at", None)) human_id = _safe_str(getattr(order, "human_id", "")) or _safe_str(getattr(order, "id", "")) # remarks: try best-effort remark_parts = [] for attr in ("description", "printing_warn", "production_warn", "rolling_warn"): v = _safe_str(getattr(order, attr, "")).strip() if v: remark_parts.append(v) remark = "\n".join(remark_parts[:3]) # avoid overlong ctx: dict[str, str] = { # main title f"{prefix}印花销售单": f"印花销售单 {human_id}".strip(), f"{prefix}客户名称": customer_name, f"{prefix}仓库名称": warehouse_name, f"{prefix}日期(订单日期)": order_date, f"{prefix}当前页码": "1", f"{prefix}总页数": "1", # brace placeholders (non-prefixed) "备注": remark, "打印人": (printer_label or "").strip(), } # Fill line items from jobs jobs = list( order.printing_jobs.select_related("product").order_by("id") if getattr(order, "id", None) else [] ) # For "尽可能饱满" testing: if few jobs, still fill what we can; unresolved will be cleared by renderer. def _job_product_name(job) -> str: p = getattr(job, "product", None) name = _safe_str(getattr(p, "name", "")).strip() if name: return name return _safe_str(getattr(job, "id", "")).strip() def _job_spec(job) -> str: parts = [] size = _safe_str(getattr(job, "size", "")).strip() if size: parts.append(size) pieces = getattr(job, "pieces", None) if pieces not in (None, ""): parts.append(f"{pieces}件") return " / ".join(parts) def _job_width(job) -> str: # prefer order.width for consistent printing layout w = _safe_str(getattr(order, "width", "")).strip() if w: return w # fallback to product width_size if exists p = getattr(job, "product", None) w2 = getattr(p, "width_size", None) return _safe_str(w2) qty_by_index: dict[int, Decimal] = {} for i in range(1, max_line + 1): job = jobs[i - 1] if i - 1 < len(jobs) else None if job is None: continue qty = getattr(job, "quantity", None) try: qty_dec = Decimal(str(qty)) if qty is not None else Decimal("0") except Exception: qty_dec = Decimal("0") qty_by_index[i] = qty_dec ctx[f"{prefix}产品名称{i}"] = _job_product_name(job) ctx[f"{prefix}幅宽{i}"] = _job_width(job) ctx[f"{prefix}规格{i}"] = _job_spec(job) ctx[f"{prefix}销售数量{i}"] = _safe_str(qty) ctx[f"{prefix}单位{i}"] = _safe_str(getattr(job, "unit", "")).strip() # Subtotals: 数量和8/16/24/... if template has them (or just fill anyway) for end in range(8, max_line + 1, 8): s = sum((qty_by_index.get(i, Decimal("0")) for i in range(end - 7, end + 1)), Decimal("0")) ctx[f"{prefix}数量和{end}"] = _safe_str(s) # If template uses SalesOrder naming but we feed PrintingOrder, at least fill known placeholders. # Any placeholder not covered here will be cleared by renderer (when clear_unresolved=True). return ctx def build_debug_context_summary(context: Mapping[str, str]) -> dict[str, int]: """ Lightweight stats for logging/command output. """ filled = sum(1 for _k, v in (context or {}).items() if str(v or "").strip()) total = len(context or {}) return {"context_total_keys": int(total), "context_filled_keys": int(filled)} def build_mock_sales_docx_context( *, placeholders: set[str] | None = None, printer_label: str = "系统自动生成", seed: int = 20260126, ) -> dict[str, str]: """ Build a "realistic enough" mock context for quickly testing docx template rendering, without requiring any DB data. Notes: - Fills as many indexed line placeholders as detected (default 32). - Also fills per-8-line subtotal placeholders. - Any missing placeholders will be cleared by renderer when clear_unresolved=True. """ import random r = random.Random(int(seed)) prefix = "打印-销售单(8*9)." ph = placeholders or set() max_line = _detect_max_line_index(ph) or 32 customer_names = ["宇文服饰", "星河面料", "青山印染", "江南布业", "云图设计"] warehouse_names = ["主仓库", "广州仓", "苏州仓", "上海仓", "临时仓"] crafts = ["活性印花", "分散印花", "数码直喷", "拔染", "涂料印花"] widths = ["145cm", "150cm", "160cm", "180cm"] specs = ["40S*40S", "32S", "21S", "涤棉65/35", "全棉"] units = ["米", "码"] now = timezone.localtime(timezone.now()) order_date = now.strftime("%Y-%m-%d") human_id = f"{now.strftime('%Y%m%d')}{r.randint(100000, 999999)}" ctx: dict[str, str] = { f"{prefix}印花销售单": f"印花销售单 {human_id}", f"{prefix}当前页码": "1", f"{prefix}总页数": "1", f"{prefix}客户名称": r.choice(customer_names), f"{prefix}仓库名称": r.choice(warehouse_names), f"{prefix}日期(订单日期)": order_date, "备注": ( f"工艺:{r.choice(crafts)}\n" f"交期:{(now + timezone.timedelta(days=r.randint(3, 10))).strftime('%Y-%m-%d')}\n" f"注意:色差控制、走货前复核数量" ), "打印人": (printer_label or "").strip(), } # Fill line items 1..N from decimal import Decimal qty_by_index: dict[int, Decimal] = {} for i in range(1, max_line + 1): color = r.choice(["蓝", "红", "黑", "绿", "紫", "灰"]) craft = r.choice(crafts) name = f"花型{i:02d}-{craft}-{color}" width = r.choice(widths) spec = f"{r.choice(specs)} / {r.randint(45, 120)}gsm" qty = Decimal(str(r.randint(8, 120))) + Decimal(str(r.choice([0, 0, 0, 0.5]))) unit = r.choice(units) ctx[f"{prefix}产品名称{i}"] = name ctx[f"{prefix}幅宽{i}"] = width ctx[f"{prefix}规格{i}"] = spec ctx[f"{prefix}销售数量{i}"] = _safe_str(qty) ctx[f"{prefix}单位{i}"] = unit qty_by_index[i] = qty # Subtotals: 数量和8/16/... for end in range(8, max_line + 1, 8): s = sum((qty_by_index.get(i, Decimal("0")) for i in range(end - 7, end + 1)), Decimal("0")) ctx[f"{prefix}数量和{end}"] = _safe_str(s) return ctx def build_mock_sales_docx_pages( *, placeholders: set[str] | None = None, pages: int = 1, printer_label: str = "系统自动生成", seed: int = 20260126, ) -> list[dict[str, str]]: """ Build per-page contexts for the fixed-size template: - 9 rows of items per page - each row has 8 quantity cells => 72 quantity cells per page Page-level summary fields (must be per-page): - `匹数`: count of non-empty quantity cells - `总数量`: sum of values in non-empty quantity cells """ import random if pages <= 0: return [] r = random.Random(int(seed)) prefix = "打印-销售单(8*9)." customer_names = ["宇文服饰", "星河面料", "青山印染", "江南布业", "云图设计"] warehouse_names = ["主仓库", "广州仓", "苏州仓", "上海仓", "临时仓"] crafts = ["活性印花", "分散印花", "数码直喷", "拔染", "涂料印花"] widths = ["145cm", "150cm", "160cm", "180cm"] specs = ["40S*40S", "32S", "21S", "涤棉65/35", "全棉"] units = ["米", "码"] now = timezone.localtime(timezone.now()) order_date = now.strftime("%Y-%m-%d") human_id = f"{now.strftime('%Y%m%d')}{r.randint(100000, 999999)}" # Template rows indices for product fields: 1,9,17,...,65 (9 rows) row_start_indices = [1 + 8 * i for i in range(9)] from decimal import Decimal result: list[dict[str, str]] = [] for page_no in range(1, int(pages) + 1): ctx: dict[str, str] = { f"{prefix}印花销售单": f"印花销售单 {human_id}", f"{prefix}当前页码": str(page_no), f"{prefix}总页数": str(pages), f"{prefix}客户名称": r.choice(customer_names), f"{prefix}仓库名称": r.choice(warehouse_names), f"{prefix}日期(订单日期)": order_date, "备注": ( f"工艺:{r.choice(crafts)}\n" f"交期:{(now + timezone.timedelta(days=r.randint(3, 10))).strftime('%Y-%m-%d')}\n" f"注意:色差控制、走货前复核数量" ), "打印人": (printer_label or "").strip(), } bolts = 0 total_qty = Decimal("0") for row_i, start_idx in enumerate(row_start_indices, start=1): color = r.choice(["蓝", "红", "黑", "绿", "紫", "灰"]) craft = r.choice(crafts) name = f"花型{page_no:02d}-{row_i:02d}-{craft}-{color}" width = r.choice(widths) spec = f"{r.choice(specs)} / {r.randint(45, 120)}gsm" unit = r.choice(units) ctx[f"{prefix}产品名称{start_idx}"] = name ctx[f"{prefix}幅宽{start_idx}"] = width ctx[f"{prefix}规格{start_idx}"] = spec ctx[f"{prefix}单位{start_idx}"] = unit row_sum = Decimal("0") # 8 quantity cells for j in range(8): idx = start_idx + j # make some cells blank to simulate real data if r.random() < 0.18: v = "" else: v = Decimal(str(r.randint(8, 120))) + Decimal(str(r.choice([0, 0, 0, 0.5]))) ctx[f"{prefix}销售数量{idx}"] = _safe_str(v) if str(v).strip() != "": bolts += 1 try: vv = Decimal(str(v)) except Exception: vv = Decimal("0") total_qty += vv row_sum += vv # row subtotal placeholder: 数量和8/16/.../72 (end index) row_end = start_idx + 7 ctx[f"{prefix}数量和{row_end}"] = _safe_str(row_sum) ctx[f"{prefix}匹数"] = _safe_str(bolts) ctx[f"{prefix}总数量"] = _safe_str(total_qty) result.append(ctx) return result