From 646dbe7f184113099cf18d1fe91537055d32bd63 Mon Sep 17 00:00:00 2001 From: colaftc Date: Thu, 9 Apr 2026 17:35:14 +0800 Subject: [PATCH] feat: added wecom notify when printing_job production was completed(new status) and shipment was created --- .env | 2 +- api_v1/views/printing/API.md | 20 +++ api_v1/views/printing/serializers.py | 10 +- api_v1/views/printing/services.py | 33 +++- .../views/printing/test_printing_job_api.py | 149 ++++++++++++++++++ api_v1/views/printing/test_services.py | 69 +++++++- api_v1/views/printing/views.py | 58 ++++++- api_v1/views/shipment/test_api.py | 30 +++- docs/plant/printing-job-completion-flow.png | Bin 0 -> 79126 bytes docs/plant/printing-job-completion-flow.puml | 61 +++++++ docs/shipment_api.md | 1 + flower/settings.py | 9 ++ printing/apps.py | 13 +- printing/handlers.py | 45 +++++- ...037_printingjob_is_production_completed.py | 20 +++ printing/models.py | 5 + printing/services.py | 115 ++++++++++++++ printing/signals.py | 6 + printing/tasks.py | 16 ++ .../test_production_completed_notification.py | 136 ++++++++++++++++ printing/test_wecom_markdown.py | 19 +++ shipment/apps.py | 11 ++ shipment/handlers.py | 46 ++++++ shipment/services.py | 128 +++++++++++++++ shipment/signals.py | 11 ++ shipment/tasks.py | 24 +++ shipment/test_notification.py | 98 ++++++++++++ 27 files changed, 1122 insertions(+), 13 deletions(-) create mode 100644 docs/plant/printing-job-completion-flow.png create mode 100644 docs/plant/printing-job-completion-flow.puml create mode 100644 printing/migrations/0037_printingjob_is_production_completed.py create mode 100644 printing/test_production_completed_notification.py create mode 100644 shipment/handlers.py create mode 100644 shipment/signals.py create mode 100644 shipment/tasks.py create mode 100644 shipment/test_notification.py diff --git a/.env b/.env index bd0a9c3..ade33ed 100644 --- a/.env +++ b/.env @@ -7,5 +7,5 @@ DB_NAME=flower DB_USER=flower DB_PASSWORD=flower_pg_zuowei1216 PRINTING_SALES_ITEM_SOURCE_STATE_ID=2 -WECOM_WEBHOOK_KEY= +WECOM_WEBHOOK_KEY=cc88bdef-a13f-4d7e-bdb6-ee51b68b8205 PRINTING_JOB_STATE_ADVANCED_FOLLOWUP_URL_TEMPLATE=https://app.yuwen.cloud/workstation/production/batch-advance?orderId={order_id} diff --git a/api_v1/views/printing/API.md b/api_v1/views/printing/API.md index 3192046..f429355 100644 --- a/api_v1/views/printing/API.md +++ b/api_v1/views/printing/API.md @@ -170,6 +170,26 @@ - **描述**: 将任务回退到上一个流程状态。 - **响应**: 成功时返回 `200 OK`,包含 `detail` 和 `data`。失败时返回 `400 Bad Request`。 +- **POST** `/api/v1/printing-jobs/mark-production-completed/` + - **描述**: 显式将指定的 `PrintingJob` 标记为“已完成生产”。 + - **后续动作**: 成功后会触发 `printing_job_production_completed` 领域信号,并由异步 Celery task 发送企业微信通知。 + - **请求体**: + ```json + { + "printing_job_id": 123 + } + ``` + - **成功响应**: `200 OK` + ```json + { + "message": "OK" + } + ``` + - **失败响应**: + - `400 Bad Request` - 缺少 `printing_job_id` + - `403 Forbidden` - 当前用户未关联商户,或试图操作其他商户的 `PrintingJob` + - `404 Not Found` - `PrintingJob` 不存在 + ### 2.3. 状态查询操作 - **GET** `/api/v1/printing-jobs/{id}/completed-states/` diff --git a/api_v1/views/printing/serializers.py b/api_v1/views/printing/serializers.py index caacd38..cdaa9ac 100644 --- a/api_v1/views/printing/serializers.py +++ b/api_v1/views/printing/serializers.py @@ -340,6 +340,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer): work_state_display = serializers.SerializerMethodField() has_started = serializers.BooleanField(read_only=True) is_completed = serializers.BooleanField(read_only=True) + is_production_completed = serializers.BooleanField(read_only=True) progress_percentage = serializers.FloatField(read_only=True) last_completed_state = serializers.CharField(read_only=True) business_object_id = serializers.SerializerMethodField() @@ -354,7 +355,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer): 'product_image_url', 'has_started', 'quantity', 'unit', 'size', 'pieces', 'description', 'work_state', 'work_state_display', - 'status', 'is_completed', 'progress_percentage', 'last_completed_state', + 'status', 'is_completed', 'is_production_completed', 'progress_percentage', 'last_completed_state', 'business_object_id', 'batch_advance_records', 'saleitems', @@ -362,7 +363,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer): ] read_only_fields = [ 'id', 'created_at', 'updated_at', - 'status', 'is_completed', 'progress_percentage', 'last_completed_state', + 'status', 'is_completed', 'is_production_completed', 'progress_percentage', 'last_completed_state', 'business_object_id', 'merchant_id' ] @@ -432,6 +433,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer): status_id = serializers.IntegerField(read_only=True) work_state_display = serializers.SerializerMethodField() is_completed = serializers.BooleanField(read_only=True) + is_production_completed = serializers.BooleanField(read_only=True) has_started = serializers.BooleanField(read_only=True) progress_percentage = serializers.FloatField(read_only=True) last_completed_state = serializers.CharField(read_only=True) @@ -446,7 +448,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer): 'id', 'original_id', 'merchant_id', 'printing_order', 'printing_order_id', 'external_order_id', 'product', 'product_name', 'product_code', 'quantity', 'unit', 'size', 'pieces', 'description', 'work_state', 'work_state_display', - 'status', 'status_id', 'is_completed', 'has_started', + 'status', 'status_id', 'is_completed', 'is_production_completed', 'has_started', 'progress_percentage', 'last_completed_state', 'business_object_id', 'batch_advance_records', @@ -455,7 +457,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer): ] read_only_fields = [ 'id', 'created_at', 'updated_at', - 'status', 'status_id', 'is_completed', 'has_started', + 'status', 'status_id', 'is_completed', 'is_production_completed', 'has_started', 'progress_percentage', 'last_completed_state', 'business_object_id', 'merchant_id' ] diff --git a/api_v1/views/printing/services.py b/api_v1/views/printing/services.py index 161c093..5fe708a 100644 --- a/api_v1/views/printing/services.py +++ b/api_v1/views/printing/services.py @@ -6,6 +6,7 @@ from django.conf import settings from django.db import transaction from django.contrib.contenttypes.models import ContentType from printing import models as printing_models +from printing.signals import printing_job_production_completed, printing_order_created from stateflow import models as stateflow_models from stateflow import services as stateflow_services @@ -55,7 +56,6 @@ class PrintingOrderService: # domain event: printing order created # handler will ensure transaction.on_commit for external side effects (e.g. WeCom notify) try: - from printing.signals import printing_order_created printing_order_created.send( sender=printing_models.PrintingOrder, instance=order, @@ -173,6 +173,8 @@ class PrintingJobService: Returns: 创建的任务实例 """ + data = dict(data) + data.pop('is_production_completed', None) printing_order = data.get('printing_order') # 绑定创建人 @@ -218,6 +220,9 @@ class PrintingJobService: Returns: (success, message, updated_job) """ + data = dict(data) + data.pop('is_production_completed', None) + # 更新字段 for field, value in data.items(): # 不允许直接修改 business_object @@ -227,6 +232,31 @@ class PrintingJobService: printing_job.save() return True, '更新成功', printing_job + + @staticmethod + @transaction.atomic + def make_printing_job_production_completed( + printing_job: printing_models.PrintingJob, + triggered_by=None, + ) -> printing_models.PrintingJob: + """显式标记 PrintingJob 已完成生产,并触发领域信号。""" + if not printing_job.is_production_completed: + printing_job.is_production_completed = True + printing_job.save(update_fields=['is_production_completed', 'updated_at']) + + try: + printing_job_production_completed.send( + sender=printing_models.PrintingJob, + instance=printing_job, + triggered_by=triggered_by, + ) + except Exception: + import logging + logging.getLogger(__name__).exception( + "[api_v1.views.printing.services] 触发 printing_job_production_completed signal 失败(已忽略)" + ) + + return printing_job @staticmethod def get_job_status(printing_job: printing_models.PrintingJob) -> Dict[str, Any]: @@ -243,5 +273,6 @@ class PrintingJobService: 'status': printing_job.status, 'status_id': printing_job.status_id, 'is_completed': printing_job.is_completed, + 'is_production_completed': printing_job.is_production_completed, 'has_started': printing_job.has_started, } diff --git a/api_v1/views/printing/test_printing_job_api.py b/api_v1/views/printing/test_printing_job_api.py index 36f2407..6e5e8dd 100644 --- a/api_v1/views/printing/test_printing_job_api.py +++ b/api_v1/views/printing/test_printing_job_api.py @@ -128,6 +128,23 @@ class PrintingJobAPITestCase(TestCase): self.assertEqual(job.quantity, 100) self.assertEqual(job.unit, '米') self.assertEqual(job.pieces, 10) + self.assertFalse(job.is_production_completed) + + def test_create_printing_job_ignores_is_production_completed(self): + """测试创建接口忽略 is_production_completed""" + data = { + 'printing_order': self.printing_order.id, + 'product': self.product.id, + 'quantity': 100, + 'unit': '米', + 'is_production_completed': True, + } + + response = self.client.post('/api/v1/printing-jobs/', data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + job = printing_models.PrintingJob.objects.get(id=response.data['id']) + self.assertFalse(job.is_production_completed) def test_list_printing_jobs(self): """测试获取款式明细列表""" @@ -158,6 +175,89 @@ class PrintingJobAPITestCase(TestCase): self.assertIn('batch_advance_records', item) self.assertIsInstance(item['batch_advance_records'], list) self.assertEqual(len(item['batch_advance_records']), 0) + + def test_mark_production_completed(self): + """测试显式标记完成生产接口""" + job = printing_models.PrintingJob.objects.create( + merchant=self.merchant, + printing_order=self.printing_order, + product=self.product, + quantity=100, + unit='米', + is_production_completed=False, + ) + + response = self.client.post( + '/api/v1/printing-jobs/mark-production-completed/', + {'printing_job_id': job.id}, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, {'message': 'OK'}) + + job.refresh_from_db() + self.assertTrue(job.is_production_completed) + + def test_mark_production_completed_requires_printing_job_id(self): + """测试显式标记完成生产接口缺少 printing_job_id""" + response = self.client.post( + '/api/v1/printing-jobs/mark-production-completed/', + {}, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn('printing_job_id', response.data['detail']) + + def test_mark_production_completed_forbidden_for_other_merchant(self): + """测试不能操作其他商户的 PrintingJob""" + other_merchant = basic_models.Merchant.objects.create( + name='其他印花厂', + type=basic_models.MerchantTypeEnum.FACTORY + ) + job = printing_models.PrintingJob.objects.create( + merchant=other_merchant, + printing_order=self.printing_order, + product=self.product, + quantity=100, + unit='米', + is_production_completed=False, + ) + + response = self.client.post( + '/api/v1/printing-jobs/mark-production-completed/', + {'printing_job_id': job.id}, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertIn('其他商户', response.data['detail']) + + job.refresh_from_db() + self.assertFalse(job.is_production_completed) + + def test_mark_production_completed_requires_employee(self): + """测试显式标记完成生产接口需要 employee""" + no_employee_user = User.objects.create_user( + username='noemployee', + password='testpass123', + email='noemployee@example.com' + ) + no_employee_user.user_permissions.add( + Permission.objects.get(codename='view_printingjob'), + Permission.objects.get(codename='add_printingjob'), + Permission.objects.get(codename='change_printingjob'), + ) + + self.client.force_authenticate(user=no_employee_user) + try: + response = self.client.post( + '/api/v1/printing-jobs/mark-production-completed/', + {'printing_job_id': 1}, + format='json' + ) + finally: + self.client.force_authenticate(user=self.user) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) def test_retrieve_printing_job(self): """测试获取款式明细详情""" @@ -347,6 +447,28 @@ class PrintingJobAPITestCase(TestCase): self.assertEqual(job.quantity, 200) self.assertEqual(job.unit, '码') self.assertEqual(job.pieces, 20) + self.assertFalse(job.is_production_completed) + + def test_update_printing_job_ignores_is_production_completed(self): + """测试更新接口忽略 is_production_completed""" + job = printing_models.PrintingJob.objects.create( + printing_order=self.printing_order, + product=self.product, + quantity=100, + unit='米', + is_production_completed=False, + ) + + response = self.client.patch( + f'/api/v1/printing-jobs/{job.id}/', + {'is_production_completed': True, 'quantity': 120}, + format='json' + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + job.refresh_from_db() + self.assertEqual(job.quantity, 120) + self.assertFalse(job.is_production_completed) def test_cannot_change_printing_order_on_update(self): """测试:不允许通过更新接口修改 PrintingJob.printing_order""" @@ -706,12 +828,14 @@ class PrintingJobAPITestCase(TestCase): self.assertIn('status', response.data) self.assertIn('status_id', response.data) self.assertIn('is_completed', response.data) + self.assertIn('is_production_completed', response.data) self.assertIn('has_started', response.data) self.assertIn('business_object_id', response.data) # 未推进状态,但有 BusinessObject,当前是初始状态 self.assertIn(response.data['status'], ['待印染', '未开始']) # 初始状态或未开始 self.assertFalse(response.data['is_completed']) + self.assertFalse(response.data['is_production_completed']) self.assertFalse(response.data['has_started']) def test_job_status_in_list(self): @@ -731,6 +855,7 @@ class PrintingJobAPITestCase(TestCase): # 验证列表中的状态字段 self.assertIn('status', response.data['results'][0]) self.assertIn('is_completed', response.data['results'][0]) + self.assertIn('is_production_completed', response.data['results'][0]) def test_job_completion_status(self): """测试任务完成状态判断""" @@ -763,7 +888,31 @@ class PrintingJobAPITestCase(TestCase): # 验证完成状态 self.assertEqual(response.data['status'], '已完成') self.assertTrue(response.data['is_completed']) + self.assertFalse(response.data['is_production_completed']) self.assertTrue(response.data['has_started']) + + def test_list_filter_by_is_production_completed(self): + """测试按 is_production_completed 过滤 PrintingJob 列表""" + completed_job = printing_models.PrintingJob.objects.create( + printing_order=self.printing_order, + product=self.product, + quantity=100, + unit='米', + is_production_completed=True, + ) + printing_models.PrintingJob.objects.create( + printing_order=self.printing_order, + product=self.product, + quantity=80, + unit='米', + is_production_completed=False, + ) + + response = self.client.get('/api/v1/printing-jobs/?is_production_completed=true') + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data['results']), 1) + self.assertEqual(response.data['results'][0]['id'], completed_job.id) + self.assertTrue(response.data['results'][0]['is_production_completed']) def test_advance_to_next_state(self): """测试推进到下一个状态 API""" diff --git a/api_v1/views/printing/test_services.py b/api_v1/views/printing/test_services.py index 53ddd23..b30bbaa 100644 --- a/api_v1/views/printing/test_services.py +++ b/api_v1/views/printing/test_services.py @@ -7,6 +7,7 @@ from django.conf import settings from basic_info.models import Merchant, MerchantTypeEnum, Employee, Customer, Product from stateflow.models import State, Process from printing.models import PrintingOrder, PrintingJob +from printing.signals import printing_job_production_completed from api_v1.views.printing.services import PrintingOrderService, PrintingJobService @@ -380,7 +381,71 @@ class PrintingJobServiceTest(TestCase): self.assertTrue(success) self.assertEqual(updated_job.quantity, 20) self.assertEqual(updated_job.pieces, 10) - + + def test_create_printing_job_ignores_is_production_completed(self): + """测试创建任务时忽略 is_production_completed 输入""" + data = { + 'printing_order': self.order, + 'product': self.product, + 'quantity': 10, + 'unit': '件', + 'is_production_completed': True, + } + + job = PrintingJobService.create_printing_job(data, self.user) + + self.assertFalse(job.is_production_completed) + + def test_update_printing_job_ignores_is_production_completed(self): + """测试更新任务时忽略 is_production_completed 输入""" + job = PrintingJob.objects.create( + printing_order=self.order, + product=self.product, + quantity=10, + unit='件', + is_production_completed=False, + ) + + success, _, updated_job = PrintingJobService.update_printing_job( + job, + {'is_production_completed': True, 'quantity': 20}, + self.user, + ) + + updated_job.refresh_from_db() + self.assertTrue(success) + self.assertEqual(updated_job.quantity, 20) + self.assertFalse(updated_job.is_production_completed) + + def test_make_printing_job_production_completed_sets_flag_and_sends_signal(self): + """测试显式完成生产会更新字段并触发 signal""" + job = PrintingJob.objects.create( + printing_order=self.order, + product=self.product, + quantity=10, + unit='件', + ) + + received = {} + uid = 'test.printing_job_production_completed' + + def _receiver(sender, **kwargs): + received['sender'] = sender + received['instance_id'] = kwargs['instance'].id + received['triggered_by'] = kwargs.get('triggered_by') + + printing_job_production_completed.connect(_receiver, dispatch_uid=uid, weak=False) + try: + updated_job = PrintingJobService.make_printing_job_production_completed(job, self.user) + finally: + printing_job_production_completed.disconnect(dispatch_uid=uid) + + updated_job.refresh_from_db() + self.assertTrue(updated_job.is_production_completed) + self.assertEqual(received['sender'], PrintingJob) + self.assertEqual(received['instance_id'], job.id) + self.assertEqual(received['triggered_by'], self.user) + def test_get_job_status(self): """测试获取任务状态""" job = PrintingJob.objects.create( @@ -397,9 +462,11 @@ class PrintingJobServiceTest(TestCase): self.assertIn('status', status_info) self.assertIn('status_id', status_info) self.assertIn('is_completed', status_info) + self.assertIn('is_production_completed', status_info) self.assertIn('has_started', status_info) # 未开始状态(现在返回空字符串或第一个状态名称) self.assertIn(status_info['status'], ['', self.state1.name]) self.assertFalse(status_info['is_completed']) + self.assertFalse(status_info['is_production_completed']) self.assertFalse(status_info['has_started']) diff --git a/api_v1/views/printing/views.py b/api_v1/views/printing/views.py index 3ff0f44..9af7ec5 100644 --- a/api_v1/views/printing/views.py +++ b/api_v1/views/printing/views.py @@ -412,10 +412,11 @@ class PrintingJobFilterSet(django_filters.FilterSet): quantity_max = django_filters.NumberFilter(field_name="quantity", lookup_expr="lte") pieces_min = django_filters.NumberFilter(field_name="pieces", lookup_expr="gte") pieces_max = django_filters.NumberFilter(field_name="pieces", lookup_expr="lte") + is_production_completed = django_filters.BooleanFilter() class Meta: model = models.PrintingJob - fields = ["printing_order", "product", "external_order_id"] + fields = ["printing_order", "product", "external_order_id", "is_production_completed"] class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): @@ -449,6 +450,7 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): - quantity_max: 最大数量 - pieces_min: 最小件数 - pieces_max: 最大件数 + - is_production_completed: 是否已标记完成生产(true/false) - search: 全文搜索(产品名称、单位、尺寸、备注) - ordering: 排序字段 @@ -537,6 +539,60 @@ class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet): serializer = self.get_serializer(instance) return Response(serializer.data) + @action(detail=False, methods=["post"], url_path="mark-production-completed") + def mark_production_completed(self, request): + """ + 显式标记印染任务已完成生产。 + + 请求体: + { + "printing_job_id": 123 + } + """ + printing_job_id = request.data.get("printing_job_id") + if not printing_job_id: + return Response( + {"detail": "printing_job_id 不能为空"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + user = request.user + merchant = None + if hasattr(user, "employee") and user.employee and user.employee.merchant: + merchant = user.employee.merchant + + if merchant is None: + return Response( + {"detail": "当前用户未关联商户"}, + status=status.HTTP_403_FORBIDDEN, + ) + + try: + job = models.PrintingJob.objects.get(pk=printing_job_id) + except models.PrintingJob.DoesNotExist: + return Response( + {"detail": "PrintingJob 不存在"}, + status=status.HTTP_404_NOT_FOUND, + ) + + job_merchant = getattr(job, "merchant", None) + if job_merchant is None and getattr(job, "printing_order", None): + job_merchant = getattr(job.printing_order, "merchant", None) + + if job_merchant and job_merchant.id != merchant.id: + return Response( + {"detail": "您不能操作其他商户的 PrintingJob"}, + status=status.HTTP_403_FORBIDDEN, + ) + + from .services import PrintingJobService + + PrintingJobService.make_printing_job_production_completed( + job, + triggered_by=user, + ) + return Response({"message": "OK"}, status=status.HTTP_200_OK) + def destroy(self, request, *args, **kwargs): """禁用删除操作""" return Response( diff --git a/api_v1/views/shipment/test_api.py b/api_v1/views/shipment/test_api.py index fff7202..e44e9cc 100644 --- a/api_v1/views/shipment/test_api.py +++ b/api_v1/views/shipment/test_api.py @@ -3,8 +3,10 @@ Shipment API 测试 """ from decimal import Decimal +from unittest.mock import patch from django.test import TestCase +from django.test import override_settings from django.conf import settings from django.contrib.auth.models import Permission from django.utils import timezone @@ -1294,6 +1296,26 @@ class ShipmentCreateAPITestCase(TestCase): self.assertEqual(self.sales_item1.shipment_id, result["id"]) self.assertEqual(self.sales_item2.shipment_id, result["id"]) + @override_settings(TESTING=False, SHIPMENT_CREATED_WECOM_NOTIFY_ENABLED=True) + def test_create_shipment_enqueues_wecom_notification_task(self): + """测试创建出货单后投递企业微信通知任务""" + data = { + "customer": self.customer.id, + "shipment_date": "2026-01-14", + "sales_items": [self.sales_item1.id, self.sales_item2.id], + } + + with patch("shipment.tasks.notify_shipment_created_wecom.delay") as mock_delay: + with self.captureOnCommitCallbacks(execute=True): + response = self.client.post("/api/v1/shipment/shipments/", data, format="json") + + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + shipment_id = response.json()["id"] + mock_delay.assert_called_once_with( + shipment_id=shipment_id, + created_by_id=self.user.id, + ) + def test_create_shipment_without_sales_items(self): """测试创建出货单但不关联销售品""" data = { @@ -2208,11 +2230,17 @@ class SalesItemCreateAPITestCase(APITestCase): area="测试地区Sales", ) + self.category = basic_models.ProductCategory.objects.create( + merchant=self.merchant, + name="测试分类Sales", + ) + # 创建产品 self.product = basic_models.Product.objects.create( merchant=self.merchant, + category=self.category, name="测试产品", - code="TEST001", + human_id="TEST001", unit=basic_models.ProductUnitEnum.METER, ) diff --git a/docs/plant/printing-job-completion-flow.png b/docs/plant/printing-job-completion-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..451ac77aeefb2bbfafce00703970aadddfe64970 GIT binary patch literal 79126 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 90% ?) then (是) + :发送“生产子单(printing_job)已完成”信号; + note right + 完成判定规则: + 关联销售品数量之和 + 大于或约等于 job.quantity 的 90% + end note + else (否) + :不发送完成信号\n等待后续销售品继续创建; + endif +else (否) + :保持“滚筒进行中”; +endif + +stop +@enduml diff --git a/docs/shipment_api.md b/docs/shipment_api.md index 0e4a044..4fddf44 100644 --- a/docs/shipment_api.md +++ b/docs/shipment_api.md @@ -141,6 +141,7 @@ - 并且这些销售品必须全部来自同一个 `printing_order` - 如果存在缺少生产任务、生产任务不存在、或跨生产订单混装,接口会拒绝创建 - 新创建的出货单默认状态为 `草稿(未发布)` +- 创建成功后会触发 `shipment_created` 领域信号,并通过异步 Celery task 发送企业微信通知 ### 接口信息 diff --git a/flower/settings.py b/flower/settings.py index 6efeb24..d93f958 100644 --- a/flower/settings.py +++ b/flower/settings.py @@ -301,6 +301,10 @@ PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED = env.bool( "PRINTING_ORDER_CREATED_WECOM_NOTIFY_ENABLED", default=(not TESTING), ) +PRINTING_JOB_PRODUCTION_COMPLETED_WECOM_NOTIFY_ENABLED = env.bool( + "PRINTING_JOB_PRODUCTION_COMPLETED_WECOM_NOTIFY_ENABLED", + default=True, +) PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE = ( "### 印染订单创建\n" "\n" @@ -313,6 +317,11 @@ PRINTING_ORDER_CREATED_WECOM_MARKDOWN_TEMPLATE = ( "- **出货日期**:`{outgoing_date}`\n" ) +SHIPMENT_CREATED_WECOM_NOTIFY_ENABLED = env.bool( + "SHIPMENT_CREATED_WECOM_NOTIFY_ENABLED", + default=True, +) + # PreSalesOrder:创建时的企业微信通知(markdown) # - 默认:测试环境关闭(避免单测出网/刷屏),非测试环境开启 PRE_SALES_ORDER_CREATED_WECOM_NOTIFY_ENABLED = env.bool( diff --git a/printing/apps.py b/printing/apps.py index 1a5ef6f..0d05927 100644 --- a/printing/apps.py +++ b/printing/apps.py @@ -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}' - ) \ No newline at end of file + ) + logger.info( + f'[printing.apps] 已注册 printing_job_production_completed 信号处理器, ' + f'sender={PrintingJob}, handler={handlers.on_printing_job_production_completed}' + ) diff --git a/printing/handlers.py b/printing/handlers.py index 726f4b1..6122dd9 100644 --- a/printing/handlers.py +++ b/printing/handlers.py @@ -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) diff --git a/printing/migrations/0037_printingjob_is_production_completed.py b/printing/migrations/0037_printingjob_is_production_completed.py new file mode 100644 index 0000000..25b44bc --- /dev/null +++ b/printing/migrations/0037_printingjob_is_production_completed.py @@ -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="是否完成生产", + ), + ), + ] diff --git a/printing/models.py b/printing/models.py index d9d4f15..2b7b734 100644 --- a/printing/models.py +++ b/printing/models.py @@ -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='一段尺寸') diff --git a/printing/services.py b/printing/services.py index 28cfb34..0395e90 100644 --- a/printing/services.py +++ b/printing/services.py @@ -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, + } diff --git a/printing/signals.py b/printing/signals.py index 731da0f..1a58898 100644 --- a/printing/signals.py +++ b/printing/signals.py @@ -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() diff --git a/printing/tasks.py b/printing/tasks.py index 9ccda43..e29e40f 100644 --- a/printing/tasks.py +++ b/printing/tasks.py @@ -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 diff --git a/printing/test_production_completed_notification.py b/printing/test_production_completed_notification.py new file mode 100644 index 0000000..7b0bdfc --- /dev/null +++ b/printing/test_production_completed_notification.py @@ -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, + ) diff --git a/printing/test_wecom_markdown.py b/printing/test_wecom_markdown.py index 8311a2d..60d4681 100644 --- a/printing/test_wecom_markdown.py +++ b/printing/test_wecom_markdown.py @@ -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) diff --git a/shipment/apps.py b/shipment/apps.py index defbb6e..f9a94ec 100644 --- a/shipment/apps.py +++ b/shipment/apps.py @@ -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", + ) diff --git a/shipment/handlers.py b/shipment/handlers.py new file mode 100644 index 0000000..d518e82 --- /dev/null +++ b/shipment/handlers.py @@ -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) diff --git a/shipment/services.py b/shipment/services.py index a5fceef..f245748 100644 --- a/shipment/services.py +++ b/shipment/services.py @@ -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, diff --git a/shipment/signals.py b/shipment/signals.py new file mode 100644 index 0000000..eb937e0 --- /dev/null +++ b/shipment/signals.py @@ -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() diff --git a/shipment/tasks.py b/shipment/tasks.py new file mode 100644 index 0000000..f1d6447 --- /dev/null +++ b/shipment/tasks.py @@ -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 diff --git a/shipment/test_notification.py b/shipment/test_notification.py new file mode 100644 index 0000000..dc85ee4 --- /dev/null +++ b/shipment/test_notification.py @@ -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, + )