certflow.controllers.certificate_controller 源代码

"""合格证控制器模块

处理合格证的生成、打印、刻印标记及统计查询等业务逻辑.
支持批量操作和按产品型号分组打印.
"""

from __future__ import annotations

from datetime import datetime
from typing import Any

from loguru import logger
from sqlalchemy.orm import Session

from certflow.config.settings import CERTIFICATE_PREFIX
from certflow.controllers.base_controller import BaseController
from certflow.services.cert_numbering_policy import get_number_model
from certflow.services.certificate_print_service import CertificatePrintService
from certflow.services.certificate_service import Certificate, CertificateService, SalePlan


[文档] class CertificateController(BaseController): """合格证控制器 - 处理合格证打印、刻印等业务 提供合格证的生成、批量打印、刻印标记、状态查询及统计等功能. 支持按产品型号分组打印,自动处理合格证编号生成和打印日志记录. Attributes: certificate_cache: 合格证编号缓存字典,用于避免重复生成编号 """ def __init__(self, db_session: Session | None = None, db_manager: Any = None) -> None: """初始化合格证控制器 Args: db_session: 数据库会话对象,如果为None则自动创建或由 db_manager 派生 db_manager: 数据库管理器实例;优先以它派生会话,统一会话生命周期 """ super().__init__(db_session, db_manager=db_manager) self.certificate_cache: dict[str, str] = {} # 缓存合格证编号 self._cert_service = self.get_service(CertificateService)
[文档] def generate_certificates_for_sale_plans( self, sale_plan_ids: list[int], force_regenerate: bool = False ) -> dict[str, object]: """为销售计划批量生成合格证 遍历指定的销售计划ID列表,为每条销售计划生成对应的合格证记录. 当force_regenerate为True时,即使合格证已存在也会重新生成. Args: sale_plan_ids: 销售计划ID列表 force_regenerate: 是否强制重新生成已存在的合格证,默认为False Returns: Dict[str, object]: 生成结果字典,包含以下键: - total: 处理总数 - generated: 成功生成数 - failed: 失败数 - certificates: 生成的合格证对象列表 Examples: >>> controller = CertificateController() >>> result = controller.generate_certificates_for_sale_plans([1, 2, 3]) >>> print(f"生成成功: {result['generated']}/{result['total']}") """ result = {"total": len(sale_plan_ids), "generated": 0, "failed": 0, "certificates": []} for sale_plan_id in sale_plan_ids: try: certificate = self._generate_single_certificate(sale_plan_id, force_regenerate) if certificate: result["generated"] += 1 result["certificates"].append(certificate) else: result["failed"] += 1 except Exception as e: logger.error(f"生成合格证失败 (sale_plan_id={sale_plan_id}): {e}") result["failed"] += 1 self.commit() logger.info(f"合格证生成完成: {result['generated']}/{result['total']}") return result
[文档] def get_history_last_values(self, model: str | None) -> dict[str, str] | None: """取同型号上次实际打印值(供统一护栏 ④ diff 闸)。 收口 print_view 原先直连 ``HistoryBackfillService(self.session)`` 的写法, 视图层不再直接持有 Service,统一经 Controller 访问。 Args: model: 产品型号字符串 Returns: dict[str, str] | None: 历史候选值;无历史或异常时返回 None """ try: from certflow.services.print_history_service import HistoryBackfillService return HistoryBackfillService(self.session).candidates(model) except Exception as exc: # noqa: BLE001 logger.debug(f"[CertificateController] 历史候选获取失败({model}): {exc}") return None
def _generate_single_certificate( self, sale_plan_id: int, force_regenerate: bool = False ) -> Certificate | None: """生成单个合格证 根据销售计划ID查找对应数据,生成合格证编号并创建合格证记录. 如果合格证已存在且不强制重新生成,则直接返回已有记录. Args: sale_plan_id: 销售计划ID force_regenerate: 是否强制重新生成,默认为False Returns: Optional[Certificate]: 生成的合格证对象,失败时返回None """ cert_svc = self._cert_service # 检查是否已存在 existing = cert_svc.get_certificate_by_sale_plan_id(sale_plan_id) if existing and not force_regenerate: logger.debug(f"合格证已存在: {existing.certificate_no}") return existing # 获取销售计划 sale_plan = cert_svc.get_sale_plan_by_id(sale_plan_id) if not sale_plan: logger.error(f"销售计划不存在: id={sale_plan_id}") return None # 生成新的合格证编号 certificate_no = self._generate_certificate_number(sale_plan) # 创建合格证记录 certificate = Certificate( certificate_no=certificate_no, sale_plan_id=sale_plan.id, unique_key=sale_plan.unique_key, product_model=sale_plan.product_model, sales_order_no=sale_plan.sales_order_no, production_order_no=sale_plan.production_order_no, customer=sale_plan.customer, print_status="待打印", engrave_status="未刻印", ) # 用型号压力对照字典补全技术参数(标准号/公称压力/温度/介质) self._fill_cert_from_dict(certificate, sale_plan) if existing and force_regenerate: # 更新已存在的记录 existing.certificate_no = certificate_no existing.product_model = sale_plan.product_model existing.sales_order_no = sale_plan.sales_order_no existing.production_order_no = sale_plan.production_order_no existing.customer = sale_plan.customer existing.print_status = "待打印" existing.engrave_status = "未刻印" self._fill_cert_from_dict(existing, sale_plan) cert_svc.merge_certificate(existing) certificate = existing else: cert_svc.add(certificate) cert_svc.flush() logger.info(f"生成合格证: {certificate_no} for {sale_plan.product_model}") return certificate def _fill_cert_from_dict(self, cert: Certificate, sale_plan: SalePlan) -> None: """用型号压力对照字典(model_param_mappings)补全合格证技术参数。 仅填充字典可确定的字段:公称压力(pn_value/pn_display/pn_unit)、 试压标准、工作温度、工作介质。不覆盖合格证已手工维护的值, 也不伪造字典中没有的信息(如产品名称/材质牌号)。 """ model = (sale_plan.product_model or "").strip() if not model: return try: from certflow.services.pn_service import PNService pn = PNService(self.session).resolve_pn(model) db = PNService(self.session).lookup_pn_from_db(model) except Exception as e: logger.debug(f"字典补全合格证技术参数失败({model}): {e}") return if pn.get("pn_value"): cert.pn_value = pn["pn_value"] if pn.get("pn_display"): cert.pn_display = pn["pn_display"] if pn.get("pn_unit"): cert.pn_unit = pn["pn_unit"] if db: if db.get("test_standard"): cert.test_standard = db["test_standard"] if db.get("working_temp"): cert.working_temp = db["working_temp"] if db.get("working_medium"): cert.working_medium = db["working_medium"] def _generate_certificate_number(self, sale_plan: SalePlan) -> str: """生成合格证编号 基于当天已生成的合格证数量生成唯一编号,格式为 CERT-YYYYMMDD-NNNN. Args: sale_plan: 销售计划对象,用于获取产品信息 Returns: str: 生成的合格证编号字符串,格式如"CERT-20231201-0001" """ count_today = self._cert_service.count_certificates_created_today() # 生成编号: CERT-20231201-0001 return CertificateService.generate_certificate_no( prefix=CERTIFICATE_PREFIX, sequence=count_today + 1 ) # ============================================================ # 编号续号(打印视图「目标年月 / 起始流水手填覆盖」后台支撑) # ============================================================
[文档] def get_next_serial(self, prefix: str) -> int: """只读预瞄某前缀下一个可用流水号(不消耗计数器,供 UI 自动续号/预览)。 Args: prefix: 编号前缀(如 ``V2604``) Returns: int: 下一可用流水号(跨 SalePlan + Certificate 取该月最大 + 1) """ from certflow.services.certificate_number_service import CertificateNumberService return CertificateNumberService().peek_next_serial(self.session, prefix)
[文档] def sync_serial_counter(self, prefix: str, end_serial: int) -> None: """手动编号写库后,将计数器推进到 ``end_serial``,保证后续自动续号不回退。 Args: prefix: 编号前缀(如 ``V2604``) end_serial: 本次手动编号覆盖到的最大流水号 """ from certflow.services.certificate_number_service import CertificateNumberService CertificateNumberService()._bump_serial(self.session, prefix, end_serial) self.session.flush()
[文档] def batch_print_certificates( self, certificate_ids: list[int], printer_name: str = "Default Printer", copies: int = 1 ) -> dict[str, object]: """批量打印合格证(相同产品会分组打印) 按产品型号对合格证进行分组,逐组执行打印操作, 并为每张合格证记录打印日志. Args: certificate_ids: 合格证ID列表 printer_name: 打印机名称,默认为"Default Printer" copies: 每张合格证的打印份数,默认为1 Returns: Dict[str, object]: 打印结果字典,包含以下键: - total: 总处理数 - success: 成功数 - failed: 失败数 - groups: 各产品组的打印详情列表 - logs: 打印日志列表 Examples: >>> controller = CertificateController() >>> result = controller.batch_print_certificates([1, 2, 3], "HP LaserJet", 2) >>> print(f"打印成功: {result['success']}/{result['total']}") """ result = { "total": len(certificate_ids), "success": 0, "failed": 0, "groups": [], "logs": [], } # 获取合格证并按产品分组 certificates = self._cert_service.get_certificates_by_ids(certificate_ids) # 按产品型号分组 groups = {} for cert in certificates: product = cert.product_model if product not in groups: groups[product] = [] groups[product].append(cert) logger.info(f"打印分组: {len(groups)} 个产品组") # 逐组打印 for product_model, certs in groups.items(): group_result = { "product_model": product_model, "count": len(certs), "certificate_nos": [c.certificate_no for c in certs], "status": "success", } try: # 使用 CertificatePrintService 的 record_print_log 记录日志 print_service = CertificatePrintService(self.session) for cert in certs: cert.print_time = datetime.now() cert.printer_name = printer_name cert.print_status = "已打印" # 构建打印数据字典(字段对齐 batch_print 中的 cert_data) cert_data = { # Br(1) ~ Br(9): 基础信息 "plan_date": cert.plan_date or "", "customer": cert.customer or "", "project_name": cert.project_name or "", "product_name": cert.product_name or "", "product_model": cert.product_model or "", "dn": cert.product_spec or "", "pn": cert.pn_display or cert.pn_value or "", "medium": cert.working_medium or "", "temperature": cert.working_temp or "", # Br(10) ~ Br(11): 出厂日期年月(从 product_code_ym 提取) "manufacture_year": int(cert.product_code_ym[:4]) if cert.product_code_ym and len(cert.product_code_ym) >= 4 else 0, "manufacture_month": int(cert.product_code_ym[4:6]) if cert.product_code_ym and len(cert.product_code_ym) >= 6 else 0, # Br(25) ~ Br(27): 合格证版型/图片/编码模式 "template_style": 1, "pic_style": 0, # 步骤 6.2:编码模式由 certificate.numbering.modes 配置判定(默认 0) "number_model": get_number_model(cert), # Br(28): 合格证模板名称 "template_name": cert.product_name or "", # Br(35) ~ Br(45): 材质/技术要求/销售信息 "body_material": "", "stem_material": "", "disc_material": "", "tech_requirements": cert.tech_requirements, "equipment_no": "", "sub_project": "", "sales_sn": "", "category": "", "weight": "", "seq_no": "", "requisition_no": "", # Br(49): 打印模式 "print_mode": "1", # Br(50) ~ Br(51): 水头 "max_inlet_head": "", "max_boost_head": "", # hgz_ShuLiang "copies": copies, } log = print_service.record_print_log( certificate_id=cert.id, certificate_no=cert.certificate_no or "", cert_data=cert_data, printer_name=printer_name, status="success", content_summary=f"产品: {cert.product_model}", ) if log: self._cert_service.add(log) result["success"] += 1 self.commit() logger.info(f"打印成功: {product_model} - {len(certs)} 张") except Exception as e: group_result["status"] = "failed" group_result["error"] = str(e) result["failed"] += len(certs) logger.error(f"打印失败: {product_model} - {e}") result["groups"].append(group_result) return result
[文档] def get_certificates_by_status(self, print_status: str = "待打印") -> list[dict[str, str]]: """按打印状态获取合格证列表 查询指定打印状态的所有合格证,按创建时间升序排序. Args: print_status: 合格证打印状态,可选值: "待打印", "已打印", "打印失败"等,默认为"待打印" Returns: List[Dict[str, str]]: 合格证信息字典列表,每个字典包含id、编号、产品型号等字段 """ certificates = self._cert_service.get_certificates_by_status(print_status) return [ { "id": c.id, "certificate_no": c.certificate_no, "product_model": c.product_model, "order_no": c.order_no, "customer": c.customer, "print_status": c.print_status, "engrave_status": c.engrave_status, "created_at": c.created_at.strftime("%Y-%m-%d %H:%M:%S") if c.created_at else "", } for c in certificates ]
[文档] def get_certificates_by_product(self, product_model: str) -> list[dict[str, str]]: """按产品型号获取合格证列表 查询指定产品型号的所有合格证. Args: product_model: 产品型号,支持完全匹配 Returns: List[Dict[str, str]]: 合格证信息字典列表,每个字典包含id、编号、订单号和状态 """ certificates = self._cert_service.get_certificates_by_product(product_model) return [ { "id": c.id, "certificate_no": c.certificate_no, "order_no": c.order_no, "print_status": c.print_status, "engrave_status": c.engrave_status, } for c in certificates ]
[文档] def mark_as_engraved(self, certificate_ids: list[int]) -> dict[str, int]: """标记合格证为已刻印(铭牌刻印完成后调用) 将指定合格证的状态更新为"已刻印",并记录刻印时间. Args: certificate_ids: 合格证ID列表 Returns: Dict[str, int]: 更新结果字典,包含以下键: - total: 总处理数 - success: 成功数 - failed: 失败数 """ result = self._cert_service.mark_as_engraved(certificate_ids) self._cert_service.commit() return result
[文档] def get_print_statistics(self, days: int = 7) -> dict[str, object]: """获取打印统计信息 统计指定天数内的打印数量、按类型统计及合格证状态分布. Args: days: 统计天数,默认为7天 Returns: Dict[str, object]: 统计结果字典 """ return self._cert_service.get_print_statistics(days)
# ============================================================ # 视图层收口(阶段 C 补丁 58):取代 print_view 直连 Service # ============================================================
[文档] def get_sale_plan_by_id(self, sale_plan_id: int) -> Any: """按 ID 取销售计划(收口 print_view 原直连 CertificateService)。""" return self._cert_service.get_sale_plan_by_id(sale_plan_id)
[文档] def get_certificate_by_unique_key(self, unique_key: str) -> Any: """按 unique_key 取最新合格证(收口 print_view 原直连 CertificateService)。""" return self._cert_service.get_certificate_by_unique_key(unique_key)
[文档] def render_certificate_bytes( self, config: Any, data: dict[str, Any], copies: int = 1, x_offsets: dict[str, float] | None = None, printer_name: str | None = None, ) -> bytes | None: """渲染合格证 ESC/P-K 原始字节(仅预览/落盘,不真正出纸)。 收口 print_view 原直连 ``PrinterService.render_certificate_bytes`` 的写法, 视图层不再直接持有 Service。 """ from certflow.services.printer.printer_service import PrinterService try: svc = PrinterService(printer_name) return svc.render_certificate_bytes(config, data, copies=copies, x_offsets=x_offsets) except Exception as exc: # noqa: BLE001 logger.debug(f"[CertificateController] 原始字节渲染失败: {exc}") return None
[文档] def auto_save_log( self, serials: list[str], data: dict[str, Any], printer_name: str, certificate_id: int | None = None, ) -> None: """自动保存打印记录(委托 CertLogService,收口 print_view 原直连)。""" from certflow.services.cert_log_service import CertLogService try: log_service = CertLogService(self.session, certificate_id) log_service.auto_save(serials, data, printer_name) except Exception as exc: # noqa: BLE001 logger.error(f"[CertificateController] 打印记录自动保存失败: {exc}")
[文档] def get_certificate_by_id(self, cert_id: int) -> Any: """按 ID 取合格证(收口 print_view 原直连 session.get(Certificate, ...))。""" from certflow.models.certificate import Certificate return self.session.get(Certificate, cert_id)
[文档] def get_job_certificate_id(self, job_id: int) -> int | None: """按打印任务 ID 取关联合格证 ID(H2 事件驱动重载用)。 视图层不直连 models,经本门面查 ``print_jobs.certificate_id``。 任务不存在或 certificate_id 为空时返回 None。 """ from certflow.models.print_job import PrintJob job = self.session.query(PrintJob).filter(PrintJob.id == job_id).first() if job is None: return None return job.certificate_id
[文档] def save_correction(self, cert_id: int, data: dict[str, Any], template_key: str) -> None: """保存订正写回数据库(收口 print_view 原直连 ORM + commit)。 视图层只负责采集表单数据 / 触发护栏,不再直接持有 session 或提交事务。 """ from certflow.models.certificate import Certificate from certflow.services.pn_service import PNService from certflow.services.printer.template_field_formatter import family_of from certflow.utils.database import DatabaseManager cert = self.session.get(Certificate, cert_id) if cert is None: raise RuntimeError("Certificate 不存在,无法保存订正") cert.product_name = data["product_name"] cert.product_model = data["product_model"] cert.product_spec = data["dn"] family = family_of(template_key) pn_raw = (data.get("pn") or "").strip() cert.pn_display, cert.pn_value, cert.pn_unit = PNService.derive_pn_fields(pn_raw, family) cert.working_temp = data["temperature"] cert.working_medium = data["medium"] cert.test_standard = data["check_standard"] cert.inspector_id = data["inspector_id"] cert.issue_date = self._format_issue_ym(data["manufacture_date"]) cert.template_type = data["template_type"] prefix = data["prefix"] or "V" ym = data["year_month"] start = data["start_number"] quantity = data["quantity"] or 1 suffix = data["suffix"] start_str = f"{start:03d}" if start < 1000 else str(start) end = start + quantity - 1 end_str = f"{end:03d}" if end < 1000 else str(end) cert.product_code_prefix = prefix cert.product_code_ym = ym cert.product_code_seq_start = start cert.product_code_seq_end = end cert.quantity = quantity cert.product_code_range = ( f"{prefix}{ym}{start_str}{suffix}" if quantity == 1 else f"{prefix}{ym}{start_str}{suffix}---{end_str}{suffix}" ) # 续号保序:手动编号写库后推进计数器(异常不影响本次写库) try: self.sync_serial_counter(prefix, end) except Exception as exc: # noqa: BLE001 logger.warning(f"[CertificateController] 续号计数器同步失败: {exc}") DatabaseManager.commit_with_retry(self.session) logger.info( f"[CertificateController] 订正已写库 cert_id={cert.id} | " f"{cert.product_name} {cert.product_model}" )
@staticmethod def _format_issue_ym(issue_date: str | None) -> str: """出厂日期格式化为 YYYY.MM(与 BUG-006 J 展示口径一致)。""" if not issue_date: return datetime.now().strftime("%Y.%m") s = str(issue_date).replace("/", "-") parts = s.split("-") if len(parts) >= 2: return f"{parts[0]}.{parts[1]}" return str(issue_date)
[文档] def get_ungenerated_sale_plans(self) -> list[SalePlan]: """获取未生成合格证的销售计划 查询所有尚未关联合格证记录的销售计划. Returns: List[SalePlan]: 未生成合格证的销售计划对象列表 """ return self._cert_service.get_ungenerated_sale_plans()
[文档] def get_all_certificates(self, limit: int = 100, offset: int = 0) -> list[Certificate]: """获取所有合格证(分页) 按创建时间倒序分页查询合格证列表. Args: limit: 每页数量,默认为100 offset: 偏移量,默认为0 Returns: List[Certificate]: 合格证对象列表 """ return self._cert_service.get_all_certificates(limit, offset)