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

293 lines
8.8 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.
"""
Printing domain services.
This module is intended for reusable business logic that may be called by:
- management commands
- API views
- signal handlers
"""
from __future__ import annotations
from dataclasses import dataclass
from urllib.parse import urlparse
from django.conf import settings
from django.utils import timezone
from api_v1.utils.wecom_webhook import send_wecom_webhook_message
from printing.models import PrintingJob
@dataclass(frozen=True)
class PrintingJobLatestState:
state_name: str
advanced_at: str
def build_printing_job_followup_url(*, printing_order_id: int | None) -> str:
tmpl = getattr(
settings,
"PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE",
"",
)
tmpl = (str(tmpl or "")).strip()
if not tmpl:
return ""
oid = printing_order_id if printing_order_id is not None else ""
try:
return str(tmpl).format(order_id=str(oid))
except Exception:
# template mis-config should not crash critical flows
return f"https://app.yuwen.cloud/workstation/production/batch-advance?orderId={oid}"
def _truncate_text(s: str, max_len: int) -> str:
s = s or ""
if len(s) <= max_len:
return s
return s[: max(0, int(max_len) - 6)] + "...(截断)"
def _is_http_url(s: str) -> bool:
"""
Minimal URL check for markdown linkify.
Only treat http/https absolute URLs as linkable.
"""
s = (s or "").strip()
if not s:
return False
try:
p = urlparse(s)
except Exception:
return False
return p.scheme in ("http", "https") and bool(p.netloc)
def _escape_markdown_link_text(s: str) -> str:
# avoid breaking markdown link text
return (s or "").replace("[", r"\[").replace("]", r"\]")
def render_process_params_markdown(*, params: dict, max_items: int = 60, max_value_len: int = 200) -> str:
"""
Render process params as markdown lines.
Example:
- **温度**25
"""
if not params:
return "- (无)"
items = []
for k in sorted(params.keys(), key=lambda x: str(x)):
v = params.get(k)
kk = _truncate_text(str(k), 80)
v_str = "" if v is None else str(v)
v_href = v_str.strip()
if _is_http_url(v_href):
v_disp = _escape_markdown_link_text(_truncate_text(v_href, int(max_value_len)))
vv = f"[{v_disp}]({v_href})"
else:
vv = _truncate_text(v_str, int(max_value_len))
items.append(f"- **{kk}**{vv}")
if len(items) > int(max_items):
remain = len(items) - int(max_items)
items = items[: int(max_items)] + [f"- ……(其余 {remain} 项已省略)"]
return "\n".join(items)
def _user_label(user) -> str:
"""
Format a human-readable sender label from Django User.
Prefer employee.name if available.
"""
if not user:
return "系统自动发送"
emp = getattr(user, "employee", None)
name = getattr(emp, "name", None) if emp is not None else None
if name:
return str(name)
username = getattr(user, "username", None)
if username:
return str(username)
return "系统自动发送"
def get_printing_job_latest_state(*, job: PrintingJob) -> PrintingJobLatestState:
"""
Latest state is defined as the latest non-cancelled StateFlowRecord
on job.business_object.
"""
if not job.business_object:
raise ValueError(f"PrintingJob 未绑定 business_object无法获取状态id={job.id}")
state_log = (
job.business_object.state_logs.filter(is_cancelled=False)
.select_related("state")
.order_by("-completed_at", "-id")
.first()
)
if state_log is None:
return PrintingJobLatestState(state_name="未开始", advanced_at="-")
state_name = getattr(state_log.state, "name", None) or str(state_log.state)
advanced_at_dt = state_log.completed_at
advanced_at = timezone.localtime(advanced_at_dt).strftime("%Y-%m-%d %H:%M:%S")
return PrintingJobLatestState(state_name=str(state_name), advanced_at=str(advanced_at))
def render_printing_job_state_markdown(
*,
printing_order_id: str,
printing_job_id: str,
state_name: str,
advanced_at: str,
sender_label: str,
followup_url: str,
process_params_markdown: str,
) -> str:
template = getattr(
settings,
"PRINTING_JOB_STATE_ADVANCED_WECOM_MARKDOWN_TEMPLATE",
(
"### 印染任务状态推进\n"
"\n"
"- **印染订单ID**`{printing_order_id}`\n"
"- **印染任务ID**`{printing_job_id}`\n"
"- **推进状态****{state_name}**\n"
"- **推进时间**`{advanced_at}`\n"
"- **发送者**{sender}\n"
"{followup_line}"
"\n"
"**工序参数**\n"
"{process_params_markdown}\n"
),
)
followup_url = (followup_url or "").strip()
followup_line = f"- **跟进地址**[{followup_url}]({followup_url})\n" if followup_url else ""
return template.format(
printing_order_id=str(printing_order_id),
printing_job_id=str(printing_job_id),
state_name=str(state_name),
advanced_at=str(advanced_at),
sender=str(sender_label or "系统自动发送"),
followup_line=str(followup_line),
process_params_markdown=str(process_params_markdown),
)
def send_printing_job_latest_state_wecom(
*,
printing_job_id: int,
sender_label: str,
key: str | None = None,
timeout_seconds: float = 10.0,
dry_run: bool = False,
) -> dict:
"""
Reusable entrypoint for command / future API.
"""
job = (
PrintingJob.objects.select_related("printing_order", "business_object")
.filter(id=int(printing_job_id))
.first()
)
if not job:
raise ValueError(f"PrintingJob 不存在id={printing_job_id}")
# latest state log for parameters
state_log = (
job.business_object.state_logs.filter(is_cancelled=False)
.select_related("state")
.order_by("-completed_at", "-id")
.first()
if job.business_object
else None
)
if state_log is None:
state_name = "未开始"
advanced_at = "-"
params = {}
else:
state_name = getattr(state_log.state, "name", None) or str(state_log.state)
advanced_at = timezone.localtime(state_log.completed_at).strftime("%Y-%m-%d %H:%M:%S")
params = state_log.get_all_parameters_summary(include_cancelled=False)
followup_url = build_printing_job_followup_url(printing_order_id=job.printing_order_id)
process_params_markdown = render_process_params_markdown(params=params)
msg = render_printing_job_state_markdown(
printing_order_id=str(job.printing_order_id),
printing_job_id=str(job.id),
state_name=str(state_name),
advanced_at=str(advanced_at),
sender_label=sender_label,
followup_url=followup_url,
process_params_markdown=process_params_markdown,
)
if dry_run:
return {
"dry_run": True,
"message": msg,
"state_name": str(state_name),
"advanced_at": str(advanced_at),
"followup_url": str(followup_url),
"process_params": params,
}
resp = send_wecom_webhook_message(
content=msg,
msgtype="markdown",
key=key,
timeout_seconds=timeout_seconds,
)
return {
"dry_run": False,
"message": msg,
"state_name": str(state_name),
"advanced_at": str(advanced_at),
"followup_url": str(followup_url),
"process_params": params,
"wecom": resp.raw,
"ok": resp.ok,
"errcode": resp.errcode,
"errmsg": resp.errmsg,
}
def render_printing_order_created_markdown(
*,
printing_order_id: str,
printing_order_human_id: str,
created_at: str,
sender_label: str,
customer_name: str,
fabric: str,
outgoing_date: str,
) -> str:
template = getattr(
settings,
"PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE",
(
"### 印染订单创建\n"
"\n"
"- **印染订单ID**`{printing_order_id}`\n"
"- **订单编号**`{printing_order_human_id}`\n"
"- **创建时间**`{created_at}`\n"
"- **发送者**{sender}\n"
"- **客户**{customer_name}\n"
"- **面料**{fabric}\n"
"- **出货日期**`{outgoing_date}`\n"
),
)
return template.format(
printing_order_id=str(printing_order_id),
printing_order_human_id=str(printing_order_human_id),
created_at=str(created_at),
sender=str(sender_label or "系统自动发送"),
customer_name=str(customer_name or "-"),
fabric=str(fabric or "-"),
outgoing_date=str(outgoing_date or "-"),
)