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

@@ -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/`

View File

@@ -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'
]

View File

@@ -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,
}

View File

@@ -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"""

View File

@@ -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'])

View File

@@ -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(