1
0
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:
2026-04-09 17:35:14 +08:00
parent 76ea445880
commit 646dbe7f18
27 changed files with 1122 additions and 13 deletions

View File

@@ -5,3 +5,14 @@ class ShipmentConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'shipment'
verbose_name = '出货管理'
def ready(self):
from . import handlers
from .models import Shipment
from .signals import shipment_created
shipment_created.connect(
handlers.on_shipment_created,
sender=Shipment,
dispatch_uid="shipment.on_shipment_created",
)

46
shipment/handlers.py Normal file
View File

@@ -0,0 +1,46 @@
import logging
from django.conf import settings
from django.db import transaction
logger = logging.getLogger(__name__)
def on_shipment_created(sender, **kwargs):
"""
Shipment 创建后的处理。
当前通过 Celery task 异步发送企业微信通知,避免阻塞主流程。
"""
if not getattr(settings, "SHIPMENT_CREATED_WECOM_NOTIFY_ENABLED", True):
return
if getattr(settings, "TESTING", False):
return
shipment = kwargs.get("instance")
created_by = kwargs.get("created_by")
if shipment is None:
logger.warning("[shipment.handlers] shipment_created 信号缺少 instance跳过通知")
return
logger.info(
"[shipment.handlers] 收到 shipment_created 信号: sender=%s, shipment_id=%s",
sender,
getattr(shipment, "id", None),
)
def _enqueue_task():
try:
from shipment.tasks import notify_shipment_created_wecom
notify_shipment_created_wecom.delay(
shipment_id=shipment.id,
created_by_id=getattr(created_by, "id", None),
)
except Exception:
logger.exception(
"[shipment.handlers] 投递 notify_shipment_created_wecom task 失败(已忽略,不影响主流程)"
)
transaction.on_commit(_enqueue_task)

View File

@@ -7,11 +7,14 @@ from __future__ import annotations
from decimal import Decimal, InvalidOperation
from typing import List
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import transaction
from django.db.models import Count, Exists, IntegerField, OuterRef, QuerySet, Subquery
from django.db.models.functions import Coalesce
from django.utils import timezone
from api_v1.utils.wecom_webhook import send_wecom_webhook_message
from shipment.models import (
ExternalFinishedProduct,
SalesItem,
@@ -22,6 +25,7 @@ from shipment.models import (
ShipmentDeliveryStatus,
ShipmentStatus,
)
from shipment.signals import shipment_created
def _resolve_user_merchant(user):
@@ -33,6 +37,19 @@ def _resolve_user_employee(user):
return getattr(user, "employee", None)
def _user_label(user) -> str:
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_active_sales_items_queryset() -> QuerySet[SalesItem]:
"""
返回未软删除的销售品查询集。
@@ -716,6 +733,7 @@ def update_shipment(
return shipment
@transaction.atomic
@transaction.atomic
def create_shipment(
customer_id: int,
@@ -834,9 +852,119 @@ def create_shipment(
if updated != len(sales_item_ids):
raise ValueError("存在不属于当前商户的销售品,无法关联到出货单")
try:
shipment_created.send(
sender=Shipment,
instance=shipment,
created_by=created_by,
)
except Exception:
import logging
logging.getLogger(__name__).exception(
"[shipment.services] 触发 shipment_created signal 失败(已忽略)"
)
return shipment
def render_shipment_created_markdown(
*,
shipment_id: str,
customer_name: str,
shipment_date: str,
items_count: str,
sender_label: str,
) -> str:
template = getattr(
settings,
"SHIPMENT_CREATED_WECOM_MARKDOWN_TEMPLATE",
(
"### 出货单创建\n"
"\n"
"- **出货单ID**`{shipment_id}`\n"
"- **客户**{customer_name}\n"
"- **出货日期**`{shipment_date}`\n"
"- **销售品数量**`{items_count}`\n"
"- **发送者**{sender}\n"
),
)
return template.format(
shipment_id=str(shipment_id or "-"),
customer_name=str(customer_name or "-"),
shipment_date=str(shipment_date or "-"),
items_count=str(items_count or "0"),
sender=str(sender_label or "系统自动发送"),
)
def send_shipment_created_wecom(
*,
shipment_id: int,
created_by_id: int | None = None,
key: str | None = None,
timeout_seconds: float = 10.0,
dry_run: bool = False,
) -> dict:
shipment = (
Shipment.objects.select_related("customer")
.prefetch_related("items")
.filter(id=int(shipment_id))
.first()
)
if not shipment:
raise ValueError(f"Shipment 不存在id={shipment_id}")
created_by = None
if created_by_id:
created_by = get_user_model().objects.filter(id=int(created_by_id)).first()
customer_name = getattr(getattr(shipment, "customer", None), "name", None) or "-"
shipment_date_text = (
shipment.shipment_date.isoformat() if getattr(shipment, "shipment_date", None) else "-"
)
items_count = shipment.items.filter(delete_at__isnull=True).count()
sender_label = _user_label(created_by)
msg = render_shipment_created_markdown(
shipment_id=str(shipment.id),
customer_name=str(customer_name),
shipment_date=str(shipment_date_text),
items_count=str(items_count),
sender_label=str(sender_label),
)
if dry_run:
return {
"dry_run": True,
"message": msg,
"shipment_id": shipment.id,
"customer_name": str(customer_name),
"shipment_date": str(shipment_date_text),
"items_count": items_count,
"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,
"shipment_id": shipment.id,
"customer_name": str(customer_name),
"shipment_date": str(shipment_date_text),
"items_count": items_count,
"sender_label": str(sender_label),
"wecom": resp.raw,
"ok": resp.ok,
"errcode": resp.errcode,
"errmsg": resp.errmsg,
}
@transaction.atomic
def create_external_shipment(
customer_id: int,

11
shipment/signals.py Normal file
View File

@@ -0,0 +1,11 @@
"""
Shipment domain signals.
"""
from django.dispatch import Signal
# Fired when a Shipment is created via business service.
# Payload:
# - instance: Shipment
# - created_by: Django User (may be None)
shipment_created = Signal()

24
shipment/tasks.py Normal file
View File

@@ -0,0 +1,24 @@
import logging
from celery import shared_task
from shipment.services import send_shipment_created_wecom
logger = logging.getLogger(__name__)
@shared_task(bind=True)
def notify_shipment_created_wecom(
self,
*,
shipment_id: int,
created_by_id: int | None = None,
) -> dict:
payload = send_shipment_created_wecom(
shipment_id=shipment_id,
created_by_id=created_by_id,
)
payload["task_id"] = self.request.id
logger.info("[shipment.tasks] 出货单创建企业微信通知发送完成: %s", payload)
return payload

View File

@@ -0,0 +1,98 @@
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 shipment import handlers
from shipment import models as shipment_models
from shipment import tasks as shipment_tasks
class ShipmentCreatedWeComServiceTestCase(TestCase):
def setUp(self):
self.user = get_user_model().objects.create_user(username="shipment-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="测试客户",
)
def test_send_shipment_created_wecom_dry_run(self):
shipment = shipment_models.Shipment.objects.create(
merchant=self.merchant,
customer=self.customer,
shipment_date="2026-01-14",
created_by=self.user,
)
from shipment.services import send_shipment_created_wecom
payload = send_shipment_created_wecom(
shipment_id=shipment.id,
created_by_id=self.user.id,
dry_run=True,
)
self.assertEqual(payload["shipment_id"], shipment.id)
self.assertEqual(payload["customer_name"], self.customer.name)
self.assertEqual(payload["shipment_date"], "2026-01-14")
self.assertEqual(payload["items_count"], 0)
self.assertEqual(payload["sender_label"], "出货员工")
@override_settings(
CELERY_TASK_ALWAYS_EAGER=True,
CELERY_TASK_EAGER_PROPAGATES=True,
)
class ShipmentCreatedWeComTaskTestCase(TestCase):
def test_task_delegates_to_service(self):
with patch("shipment.tasks.send_shipment_created_wecom") as mock_sync:
mock_sync.return_value = {"shipment_id": 123, "ok": True}
async_result = shipment_tasks.notify_shipment_created_wecom.delay(
shipment_id=123,
created_by_id=456,
)
payload = async_result.get(timeout=5)
mock_sync.assert_called_once_with(
shipment_id=123,
created_by_id=456,
)
self.assertEqual(payload["shipment_id"], 123)
self.assertIn("task_id", payload)
@override_settings(
TESTING=False,
SHIPMENT_CREATED_WECOM_NOTIFY_ENABLED=True,
)
class ShipmentCreatedHandlerTestCase(TransactionTestCase):
def test_handler_enqueues_task_on_commit(self):
shipment = type("Shipment", (), {"id": 321})()
user = type("User", (), {"id": 654})()
with patch("shipment.tasks.notify_shipment_created_wecom.delay") as mock_delay:
with transaction.atomic():
handlers.on_shipment_created(
sender=shipment_models.Shipment,
instance=shipment,
created_by=user,
)
self.assertFalse(mock_delay.called)
mock_delay.assert_called_once_with(
shipment_id=321,
created_by_id=654,
)