forked from erp-dev/erp
feat: added wecom notify when printing_job production was completed(new status) and shipment was created
This commit is contained in:
@@ -14,7 +14,7 @@ class PrintingConfig(AppConfig):
|
||||
from stateflow.signals import process_completed, state_advanced
|
||||
from .models import PrintingJob, PrintingOrder
|
||||
from . import handlers
|
||||
from .signals import printing_order_created
|
||||
from .signals import printing_job_production_completed, printing_order_created
|
||||
|
||||
# 监听 PrintingJob 流程完成信号
|
||||
process_completed.connect(
|
||||
@@ -34,6 +34,11 @@ class PrintingConfig(AppConfig):
|
||||
sender=PrintingOrder,
|
||||
dispatch_uid="printing.on_printing_order_created",
|
||||
)
|
||||
printing_job_production_completed.connect(
|
||||
handlers.on_printing_job_production_completed,
|
||||
sender=PrintingJob,
|
||||
dispatch_uid="printing.on_printing_job_production_completed",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f'[printing.apps] 已注册 process_completed 信号处理器, '
|
||||
@@ -46,4 +51,8 @@ class PrintingConfig(AppConfig):
|
||||
logger.info(
|
||||
f'[printing.apps] 已注册 printing_order_created 信号处理器, '
|
||||
f'sender={PrintingOrder}, handler={handlers.on_printing_order_created}'
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
f'[printing.apps] 已注册 printing_job_production_completed 信号处理器, '
|
||||
f'sender={PrintingJob}, handler={handlers.on_printing_job_production_completed}'
|
||||
)
|
||||
|
||||
@@ -253,8 +253,8 @@ def on_printing_order_created(sender, **kwargs):
|
||||
- 消息字段(中文):
|
||||
- 印染订单ID / human_id
|
||||
- 创建时间
|
||||
- 发送者(优先 created_by.employee.name / created_by.username)
|
||||
- 客户、面料、出货日期(可选字段,缺省展示为 '-')
|
||||
- 发送者(优先 created_by.employee.name / created_by.username)
|
||||
- 客户、面料、出货日期(可选字段,缺省展示为 '-')
|
||||
"""
|
||||
# 测试环境默认关闭,避免单测出网/刷屏
|
||||
if not getattr(settings, "PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED", True):
|
||||
@@ -336,3 +336,44 @@ def on_printing_order_created(sender, **kwargs):
|
||||
|
||||
# 在事务提交后再发送,避免事务回滚但通知已发出
|
||||
transaction.on_commit(_send_wecom)
|
||||
|
||||
|
||||
def on_printing_job_production_completed(sender, **kwargs):
|
||||
"""
|
||||
PrintingJob 被显式标记为完成生产后的处理。
|
||||
|
||||
当前通过 Celery task 异步发送企业微信通知,避免阻塞主流程。
|
||||
"""
|
||||
if not getattr(settings, "PRINTING_JOB_PRODUCTION_COMPLETED_WECOM_NOTIFY_ENABLED", True):
|
||||
return
|
||||
if getattr(settings, "TESTING", False):
|
||||
return
|
||||
|
||||
job = kwargs.get("instance")
|
||||
triggered_by = kwargs.get("triggered_by")
|
||||
if job is None:
|
||||
logger.warning(
|
||||
"[printing.handlers] printing_job_production_completed 信号缺少 instance,跳过通知"
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"[printing.handlers] 收到 printing_job_production_completed 信号: sender=%s, job_id=%s",
|
||||
sender,
|
||||
getattr(job, "id", None),
|
||||
)
|
||||
|
||||
def _enqueue_task():
|
||||
try:
|
||||
from printing.tasks import notify_printing_job_production_completed_wecom
|
||||
|
||||
notify_printing_job_production_completed_wecom.delay(
|
||||
printing_job_id=job.id,
|
||||
triggered_by_id=getattr(triggered_by, "id", None),
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"[printing.handlers] 投递 notify_printing_job_production_completed_wecom task 失败(已忽略,不影响主流程)"
|
||||
)
|
||||
|
||||
transaction.on_commit(_enqueue_task)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("printing", "0036_printing_external_fields"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="printingjob",
|
||||
name="is_production_completed",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="独立于流程计算状态的生产完成标记",
|
||||
verbose_name="是否完成生产",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -466,6 +466,11 @@ class PrintingJob(ModelBase):
|
||||
default=PrintingJobWorkStateEnum.PRODUCING,
|
||||
verbose_name='业务进展',
|
||||
)
|
||||
is_production_completed = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name='是否完成生产',
|
||||
help_text='独立于流程计算状态的生产完成标记',
|
||||
)
|
||||
quantity = models.PositiveIntegerField(verbose_name='数量')
|
||||
unit = models.CharField(max_length=50, verbose_name='单位')
|
||||
size = models.CharField(max_length=100, null=True, blank=True, verbose_name='一段尺寸')
|
||||
|
||||
@@ -294,3 +294,118 @@ def render_printing_order_created_markdown(
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -15,3 +15,9 @@ from django.dispatch import Signal
|
||||
# - created_by: Django User (may be None)
|
||||
printing_order_created = Signal()
|
||||
|
||||
# Fired when a PrintingJob is explicitly marked as production completed
|
||||
# via business service.
|
||||
# Payload:
|
||||
# - instance: PrintingJob
|
||||
# - triggered_by: Django User (may be None)
|
||||
printing_job_production_completed = Signal()
|
||||
|
||||
@@ -7,6 +7,7 @@ from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from printing.models import PlateOrder, PlateOrderTiiaUploadFailure
|
||||
from printing.services import send_printing_job_production_completed_wecom
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -116,3 +117,18 @@ def upload_yesterday_plate_order_images_to_tencent_tiia(self):
|
||||
logger.info('[TIIA] 昨日 PlateOrder 图片上传任务完成: %s', payload)
|
||||
return payload
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
def notify_printing_job_production_completed_wecom(
|
||||
self,
|
||||
*,
|
||||
printing_job_id: int,
|
||||
triggered_by_id: int | None = None,
|
||||
) -> dict:
|
||||
payload = send_printing_job_production_completed_wecom(
|
||||
printing_job_id=printing_job_id,
|
||||
triggered_by_id=triggered_by_id,
|
||||
)
|
||||
payload["task_id"] = self.request.id
|
||||
logger.info("[printing.tasks] 印染任务完成生产企业微信通知发送完成: %s", payload)
|
||||
return payload
|
||||
|
||||
136
printing/test_production_completed_notification.py
Normal file
136
printing/test_production_completed_notification.py
Normal file
@@ -0,0 +1,136 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from django.test import TestCase, TransactionTestCase, override_settings
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import handlers
|
||||
from printing import models as printing_models
|
||||
from printing import tasks as printing_tasks
|
||||
from shipment import models as shipment_models
|
||||
|
||||
|
||||
class PrintingJobProductionCompletedWeComServiceTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.user = get_user_model().objects.create_user(username="notify-user", password="pass")
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name="测试印花厂",
|
||||
type=basic_models.MerchantTypeEnum.FACTORY,
|
||||
)
|
||||
basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name="测试员工",
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE,
|
||||
)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="测试客户",
|
||||
)
|
||||
self.category = basic_models.ProductCategory.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="测试分类",
|
||||
)
|
||||
self.product = basic_models.Product.objects.create(
|
||||
merchant=self.merchant,
|
||||
category=self.category,
|
||||
name="测试产品",
|
||||
)
|
||||
|
||||
def test_send_printing_job_production_completed_wecom_fallbacks_to_order_id(self):
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric="棉布",
|
||||
width="150cm",
|
||||
external_order_id="",
|
||||
)
|
||||
job = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
printing_order=order,
|
||||
product=self.product,
|
||||
quantity=10,
|
||||
unit="米",
|
||||
)
|
||||
shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="未出货销售品1",
|
||||
quantity="50.50",
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=job.id,
|
||||
created_by=self.user,
|
||||
)
|
||||
shipment_models.SalesItem.objects.create(
|
||||
merchant=self.merchant,
|
||||
name="未出货销售品2",
|
||||
quantity="38.00",
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=job.id,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
from printing.services import send_printing_job_production_completed_wecom
|
||||
|
||||
payload = send_printing_job_production_completed_wecom(
|
||||
printing_job_id=job.id,
|
||||
triggered_by_id=self.user.id,
|
||||
dry_run=True,
|
||||
)
|
||||
|
||||
self.assertEqual(payload["printing_order_identifier"], str(order.id))
|
||||
self.assertEqual(payload["product_name"], self.product.name)
|
||||
self.assertEqual(payload["unshipped_sales_items_count"], 2)
|
||||
self.assertEqual(payload["unshipped_sales_items_quantity"], "88.50")
|
||||
self.assertEqual(payload["sender_label"], "测试员工")
|
||||
|
||||
|
||||
@override_settings(
|
||||
CELERY_TASK_ALWAYS_EAGER=True,
|
||||
CELERY_TASK_EAGER_PROPAGATES=True,
|
||||
)
|
||||
class PrintingJobProductionCompletedWeComTaskTestCase(TestCase):
|
||||
def test_task_delegates_to_service(self):
|
||||
with patch(
|
||||
"printing.tasks.send_printing_job_production_completed_wecom"
|
||||
) as mock_sync:
|
||||
mock_sync.return_value = {"printing_job_id": 123, "ok": True}
|
||||
|
||||
async_result = printing_tasks.notify_printing_job_production_completed_wecom.delay(
|
||||
printing_job_id=123,
|
||||
triggered_by_id=456,
|
||||
)
|
||||
payload = async_result.get(timeout=5)
|
||||
|
||||
mock_sync.assert_called_once_with(
|
||||
printing_job_id=123,
|
||||
triggered_by_id=456,
|
||||
)
|
||||
self.assertEqual(payload["printing_job_id"], 123)
|
||||
self.assertIn("task_id", payload)
|
||||
|
||||
|
||||
@override_settings(
|
||||
TESTING=False,
|
||||
PRINTING_JOB_PRODUCTION_COMPLETED_WECOM_NOTIFY_ENABLED=True,
|
||||
)
|
||||
class PrintingJobProductionCompletedHandlerTestCase(TransactionTestCase):
|
||||
def test_handler_enqueues_task_on_commit(self):
|
||||
job = type("Job", (), {"id": 321})()
|
||||
user = type("User", (), {"id": 654})()
|
||||
|
||||
with patch(
|
||||
"printing.tasks.notify_printing_job_production_completed_wecom.delay"
|
||||
) as mock_delay:
|
||||
with transaction.atomic():
|
||||
handlers.on_printing_job_production_completed(
|
||||
sender=printing_models.PrintingJob,
|
||||
instance=job,
|
||||
triggered_by=user,
|
||||
)
|
||||
self.assertFalse(mock_delay.called)
|
||||
|
||||
mock_delay.assert_called_once_with(
|
||||
printing_job_id=321,
|
||||
triggered_by_id=654,
|
||||
)
|
||||
@@ -21,3 +21,22 @@ class WeComMarkdownRenderTest(SimpleTestCase):
|
||||
md = render_process_params_markdown(params={"图片": " https://example.com/a.png "})
|
||||
self.assertIn("[https://example.com/a.png](https://example.com/a.png)", md)
|
||||
|
||||
def test_render_printing_job_production_completed_markdown(self):
|
||||
from printing.services import render_printing_job_production_completed_markdown
|
||||
|
||||
md = render_printing_job_production_completed_markdown(
|
||||
printing_order_identifier="EXT-001",
|
||||
printing_job_id="123",
|
||||
product_name="测试产品",
|
||||
unshipped_sales_items_count="2",
|
||||
unshipped_sales_items_quantity="88.50",
|
||||
sender_label="张三",
|
||||
)
|
||||
|
||||
self.assertIn("印染任务已完成生产", md)
|
||||
self.assertIn("`EXT-001`", md)
|
||||
self.assertIn("`123`", md)
|
||||
self.assertIn("测试产品", md)
|
||||
self.assertIn("`2`", md)
|
||||
self.assertIn("`88.50`", md)
|
||||
self.assertIn("张三", md)
|
||||
|
||||
Reference in New Issue
Block a user