forked from erp-dev/erp
feat: added print-template(test) and sync_mdy_plate_order beat task setting
This commit is contained in:
366
printing/print_docx_services.py
Normal file
366
printing/print_docx_services.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user