1
0
forked from erp-dev/erp
Files
erpnew/printing/print_docx_services.py

367 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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