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

412 lines
13 KiB
Python
Raw Permalink 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 typing import TYPE_CHECKING
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
if TYPE_CHECKING:
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.
"""
from printing.models import PrintingJob
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 "-"),
)
def render_printing_job_production_completed_markdown(
*,
printing_order_identifier: str,
printing_job_id: str,
product_name: str,
unshipped_sales_items_count: str,
unshipped_sales_items_quantity: str,
sender_label: str,
) -> str:
template = getattr(
settings,
"PRINTING_JOB_PRODUCTION_COMPLETED_WECOM_MARKDOWN_TEMPLATE",
(
"### 印染任务已完成生产\n"
"\n"
"- **订单号**`{printing_order_identifier}`\n"
"- **印染任务ID**`{printing_job_id}`\n"
"- **产品名称**{product_name}\n"
"- **未出货销售品条数**`{unshipped_sales_items_count}`\n"
"- **未出货销售品数量**`{unshipped_sales_items_quantity}`\n"
"- **发送者**{sender}\n"
),
)
return template.format(
printing_order_identifier=str(printing_order_identifier or "-"),
printing_job_id=str(printing_job_id or "-"),
product_name=str(product_name or "-"),
unshipped_sales_items_count=str(unshipped_sales_items_count or "0"),
unshipped_sales_items_quantity=str(unshipped_sales_items_quantity or "0"),
sender=str(sender_label or "系统自动发送"),
)
def send_printing_job_production_completed_wecom(
*,
printing_job_id: int,
triggered_by_id: int | None = None,
key: str | None = None,
timeout_seconds: float = 10.0,
dry_run: bool = False,
) -> dict:
from django.contrib.auth import get_user_model
from django.db.models import Sum
from printing.models import PrintingJob
from shipment.services import get_active_sales_items_queryset
job = (
PrintingJob.objects.select_related("product", "printing_order")
.filter(id=int(printing_job_id))
.first()
)
if not job:
raise ValueError(f"PrintingJob 不存在id={printing_job_id}")
triggered_by = None
if triggered_by_id:
triggered_by = get_user_model().objects.filter(id=int(triggered_by_id)).first()
order = getattr(job, "printing_order", None)
external_order_id = str(getattr(order, "external_order_id", "") or "").strip()
printing_order_identifier = external_order_id or str(getattr(order, "id", "-") or "-")
product_name = getattr(getattr(job, "product", None), "name", None) or "-"
unshipped_sales_items_qs = get_active_sales_items_queryset().filter(
printing_job_id=job.id,
shipment__isnull=True,
)
unshipped_sales_items_count = unshipped_sales_items_qs.count()
unshipped_sales_items_quantity = (
unshipped_sales_items_qs.aggregate(total=Sum("quantity")).get("total") or 0
)
sender_label = _user_label(triggered_by)
msg = render_printing_job_production_completed_markdown(
printing_order_identifier=str(printing_order_identifier),
printing_job_id=str(job.id),
product_name=str(product_name),
unshipped_sales_items_count=str(unshipped_sales_items_count),
unshipped_sales_items_quantity=str(unshipped_sales_items_quantity),
sender_label=str(sender_label),
)
if dry_run:
return {
"dry_run": True,
"message": msg,
"printing_job_id": job.id,
"printing_order_identifier": str(printing_order_identifier),
"product_name": str(product_name),
"unshipped_sales_items_count": unshipped_sales_items_count,
"unshipped_sales_items_quantity": str(unshipped_sales_items_quantity),
"sender_label": str(sender_label),
}
resp = send_wecom_webhook_message(
content=msg,
msgtype="markdown",
key=key,
timeout_seconds=timeout_seconds,
)
return {
"dry_run": False,
"message": msg,
"printing_job_id": job.id,
"printing_order_identifier": str(printing_order_identifier),
"product_name": str(product_name),
"unshipped_sales_items_count": unshipped_sales_items_count,
"unshipped_sales_items_quantity": str(unshipped_sales_items_quantity),
"sender_label": str(sender_label),
"wecom": resp.raw,
"ok": resp.ok,
"errcode": resp.errcode,
"errmsg": resp.errmsg,
}