""" Printing 模块信号处理函数 监听 stateflow 信号,在流程完成时执行业务逻辑。 """ import logging from decimal import Decimal, InvalidOperation from django.conf import settings from django.db import transaction from django.utils import timezone logger = logging.getLogger(__name__) def on_printing_job_process_completed(sender, **kwargs): """ PrintingJob 流程完成时的处理 当 PrintingJob 的工艺流程全部完成时,自动创建 SalesItem(销售品)。 数量和单位从指定的流程节点参数中获取(通过 settings 配置)。 配置项: AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER: 是否启用自动创建销售品(默认 True) PRINTING_SALES_ITEM_SOURCE_STATE_ID: 指定从哪个 State 获取参数 PRINTING_SALES_ITEM_QUANTITY_KEY: 数量参数的 key PRINTING_SALES_ITEM_UNIT_KEY: 单位参数的 key Args: sender: PrintingJob 类 content_object: PrintingJob 实例 business_object: BusinessObject 实例 last_completed_by: 最后步骤操作人 其他参数见 stateflow.signals.process_completed """ # 检查是否启用自动创建销售品 if not getattr(settings, 'AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER', True): logger.debug('[printing.handlers] AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER=False,跳过销售品创建') return from shipment.models import SalesItem, UnitChoices logger.info( f'[printing.handlers] 收到 process_completed 信号: ' f'sender={sender}, kwargs keys={list(kwargs.keys())}' ) content_object = kwargs.get('content_object') business_object = kwargs.get('business_object') last_completed_by = kwargs.get('last_completed_by') if content_object is None or business_object is None: logger.warning('process_completed 信号缺少 content_object 或 business_object') return printing_job = content_object logger.info(f'[printing.handlers] 处理 PrintingJob #{printing_job.id}') # 从 settings 获取配置 source_state_id = getattr(settings, 'PRINTING_SALES_ITEM_SOURCE_STATE_ID', None) quantity_key = getattr(settings, 'PRINTING_SALES_ITEM_QUANTITY_KEY', '米数') if source_state_id is None: logger.debug( f'PrintingJob #{printing_job.id} 流程完成,' f'但 PRINTING_SALES_ITEM_SOURCE_STATE_ID 未配置,跳过销售品创建' ) return # 从 business_object 的状态流转记录中找到指定 state_id 的记录 state_log = business_object.state_logs.filter( state_id=source_state_id, is_cancelled=False ).first() if state_log is None: logger.warning( f'PrintingJob #{printing_job.id} 流程完成,' f'但未找到 state_id={source_state_id} 的已完成记录,跳过销售品创建' ) return # 获取参数汇总 params = state_log.get_all_parameters_summary() # 获取数量 quantity_raw = params.get(quantity_key) if quantity_raw is None: logger.warning( f'PrintingJob #{printing_job.id} 流程完成,' f'但 state_id={source_state_id} 的参数中缺少 {quantity_key},跳过销售品创建' ) return # 转换数量为 Decimal try: quantity = Decimal(str(quantity_raw)) except (InvalidOperation, ValueError, TypeError) as e: logger.warning( f'PrintingJob #{printing_job.id} 流程完成,' f'但 {quantity_key}={quantity_raw} 无法转换为数字: {e},跳过销售品创建' ) return # 单位固定为"米" sales_unit = UnitChoices.METER # 获取产品名称 product_name = printing_job.product.name if printing_job.product else '未知产品' # 获取客户ID(从主订单获取) customer_id = None if printing_job.printing_order and printing_job.printing_order.customer: customer_id = printing_job.printing_order.customer_id try: with transaction.atomic(): # SalesItem.merchant 不允许为空,优先取 printing_job.merchant sales_merchant = getattr(printing_job, 'merchant', None) if not sales_merchant and getattr(printing_job, 'printing_order', None): sales_merchant = getattr(printing_job.printing_order, 'merchant', None) if not sales_merchant and last_completed_by and hasattr(last_completed_by, 'employee'): sales_merchant = getattr(last_completed_by.employee, 'merchant', None) if not sales_merchant: logger.warning( f'PrintingJob #{printing_job.id} 流程完成,' f'但无法确定 merchant,跳过销售品创建' ) return sales_item = SalesItem.objects.create( shipment=None, # 暂不关联出货单,待后续分配 merchant=sales_merchant, name=product_name, quantity=quantity, unit=sales_unit, created_by=last_completed_by, printing_job_id=printing_job.id, customer_id=customer_id, ) logger.info( f'PrintingJob #{printing_job.id} 流程完成,' f'已创建销售品 SalesItem #{sales_item.id}: ' f'{product_name} x {quantity} {sales_item.get_unit_display()}' ) except Exception as e: logger.error( f'PrintingJob #{printing_job.id} 流程完成后创建销售品失败: {e}', exc_info=True ) def on_printing_job_state_advanced(sender, **kwargs): """ PrintingJob 每次状态推进时的处理(监听 stateflow.signals.state_advanced)。 需求: - 仅监听 PrintingJob - 在事务提交后发送企业微信机器人通知(markdown) - 消息包含: - PrintingOrder.id - PrintingJob.id - 被推进状态名 state.name - 推进时间(state_log.completed_at) """ # 测试环境默认关闭,避免单测出网/刷屏 if not getattr(settings, 'PRINTING_JOB_STATE_ADVANCED_WECOM_NOTIFY_ENABLED', True): return if getattr(settings, 'TESTING', False): return content_object = kwargs.get('content_object') state = kwargs.get('state') state_log = kwargs.get('state_log') if content_object is None or state is None or state_log is None: logger.warning('[printing.handlers] state_advanced 信号缺少关键参数,跳过通知') return printing_job = content_object printing_job_id = getattr(printing_job, 'id', None) printing_order = getattr(printing_job, 'printing_order', None) printing_order_id = getattr(printing_order, 'id', None) state_name = getattr(state, 'name', None) or str(state) advanced_at_dt = getattr(state_log, 'completed_at', None) or getattr(state_log, 'created_at', None) advanced_at = ( timezone.localtime(advanced_at_dt).strftime('%Y-%m-%d %H:%M:%S') if advanced_at_dt is not None else timezone.localtime(timezone.now()).strftime('%Y-%m-%d %H:%M:%S') ) sender_label = None completed_by = kwargs.get('completed_by') if completed_by: emp = getattr(completed_by, 'employee', None) sender_label = getattr(emp, 'name', None) if emp is not None else None sender_label = sender_label or getattr(completed_by, 'username', None) or '系统自动发送' else: sender_label = '系统自动发送' # 工序参数:取该 state_log 的最新汇总 try: params = state_log.get_all_parameters_summary(include_cancelled=False) if state_log else {} except Exception: logger.exception('[printing.handlers] 读取工序参数失败,降级为空') params = {} try: from printing.services import ( build_printing_job_followup_url, render_printing_job_state_markdown, render_process_params_markdown, ) followup_url = build_printing_job_followup_url(printing_order_id=printing_order_id) process_params_markdown = render_process_params_markdown(params=params) message = render_printing_job_state_markdown( printing_order_id=str(printing_order_id) if printing_order_id is not None else '-', printing_job_id=str(printing_job_id) if printing_job_id is not None else '-', state_name=str(state_name), advanced_at=str(advanced_at), sender_label=str(sender_label), followup_url=str(followup_url), process_params_markdown=str(process_params_markdown), ) except Exception: logger.exception('[printing.handlers] 渲染企业微信消息失败,跳过通知') return def _send_wecom(): try: from api_v1.utils.wecom_webhook import send_wecom_webhook_message resp = send_wecom_webhook_message(content=message, msgtype='markdown') if not resp.ok: logger.warning( '[printing.handlers] WeCom webhook 返回失败:errcode=%s, errmsg=%s, raw=%s', resp.errcode, resp.errmsg, resp.raw ) except Exception: logger.exception('[printing.handlers] 发送 WeCom webhook 失败(已忽略,不影响主流程)') # 在事务提交后再发送,避免事务回滚但通知已发出 transaction.on_commit(_send_wecom) def on_printing_order_created(sender, **kwargs): """ PrintingOrder 创建时的处理(领域 signal:printing.signals.printing_order_created)。 需求: - 在事务提交后发送企业微信机器人通知(markdown) - 消息字段(中文): - 印染订单ID / human_id - 创建时间 - 发送者(优先 created_by.employee.name / created_by.username) - 客户、面料、出货日期(可选字段,缺省展示为 '-') """ # 测试环境默认关闭,避免单测出网/刷屏 if not getattr(settings, "PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED", True): return if getattr(settings, "TESTING", False): return order = kwargs.get("instance") created_by = kwargs.get("created_by") if order is None: logger.warning("[printing.handlers] printing_order_created 信号缺少 instance,跳过通知") return try: from printing.services import render_printing_order_created_markdown created_at_dt = getattr(order, "created_at", None) created_at = ( timezone.localtime(created_at_dt).strftime("%Y-%m-%d %H:%M:%S") if created_at_dt is not None else timezone.localtime(timezone.now()).strftime("%Y-%m-%d %H:%M:%S") ) emp = getattr(created_by, "employee", None) if created_by else None sender_label = getattr(emp, "name", None) if emp is not None else None sender_label = sender_label or getattr(created_by, "username", None) if created_by else None sender_label = sender_label or "系统自动发送" customer_name = None try: customer = getattr(order, "customer", None) customer_name = getattr(customer, "name", None) if customer else None except Exception: customer_name = None outgoing_dt = getattr(order, "outgoing_date", None) outgoing_date = ( timezone.localtime(outgoing_dt).strftime("%Y-%m-%d %H:%M:%S") if outgoing_dt is not None else "-" ) message = render_printing_order_created_markdown( printing_order_id=str(getattr(order, "id", "-") or "-"), printing_order_human_id=str(getattr(order, "human_id", "-") or "-"), created_at=str(created_at), sender_label=str(sender_label), customer_name=str(customer_name or "-"), fabric=str(getattr(order, "fabric", "-") or "-"), outgoing_date=str(outgoing_date), ) except Exception: logger.exception("[printing.handlers] 渲染企业微信消息失败,跳过通知") return def _send_wecom(): try: from api_v1.utils.wecom_webhook import send_wecom_webhook_message resp = send_wecom_webhook_message(content=message, msgtype="markdown") if not resp.ok: logger.warning( "[printing.handlers] WeCom webhook 返回失败:errcode=%s, errmsg=%s, raw=%s", resp.errcode, resp.errmsg, resp.raw, ) except Exception: logger.exception("[printing.handlers] 发送 WeCom webhook 失败(已忽略,不影响主流程)") # 在事务提交后再发送,避免事务回滚但通知已发出 transaction.on_commit(_send_wecom)