forked from erp-dev/erp
fix: added merchant_id to printing_order and plate_order
This commit is contained in:
@@ -25,6 +25,7 @@ from .views.products import ProductQuickViewSet
|
||||
from .views.parameters import StateParameterViewSet
|
||||
from .views.users import CreateUserWithProfileView
|
||||
from .views.mingdaoyun import MDYPlateOrderStagingViewSet
|
||||
from .views.shipment import SalesItemByPrintingOrderView
|
||||
|
||||
# 创建 DRF Router for Stateflow
|
||||
stateflow_router = DefaultRouter()
|
||||
@@ -98,6 +99,13 @@ urlpatterns = [
|
||||
# Stateflow API (使用 Router)
|
||||
path('stateflow/', include(stateflow_router.urls)),
|
||||
|
||||
# Shipment API
|
||||
path(
|
||||
'shipment/sales-items/by-printing-order/<int:printing_order_id>/',
|
||||
SalesItemByPrintingOrderView.as_view(),
|
||||
name='sales_items_by_printing_order'
|
||||
),
|
||||
|
||||
# 主 Router (printing-orders 等)
|
||||
path('', include(main_router.urls)),
|
||||
]
|
||||
|
||||
@@ -141,11 +141,12 @@ class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||
jobs_last_status_summary = serializers.SerializerMethodField()
|
||||
created_by = serializers.IntegerField(source='created_by_id', read_only=True)
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PrintingOrder
|
||||
fields = [
|
||||
'id', 'human_id', 'customer', 'customer_name', 'customer_phone',
|
||||
'id', 'human_id', 'merchant_id', 'customer', 'customer_name', 'customer_phone',
|
||||
'fabric', 'width', 'is_urgent', 'area', 'address', 'curve',
|
||||
'is_fabric_received', 'outgoing_date', 'is_invalid', 'new_curve',
|
||||
'process', 'process_name', 'progress', 'position', 'print_count',
|
||||
@@ -153,7 +154,7 @@ class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||
'created_by', 'created_by_name',
|
||||
'created_at', 'updated_at',
|
||||
]
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id']
|
||||
|
||||
def get_jobs_status_summary(self, obj):
|
||||
"""
|
||||
@@ -228,11 +229,12 @@ class PrintingOrderDetailSerializer(serializers.ModelSerializer):
|
||||
process_name = serializers.CharField(source='process.name', read_only=True)
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
progress = serializers.IntegerField(read_only=True)
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PrintingOrder
|
||||
fields = [
|
||||
'id', 'human_id', 'customer', 'customer_name', 'customer_phone', 'customer_area',
|
||||
'id', 'human_id', 'merchant_id', 'customer', 'customer_name', 'customer_phone', 'customer_area',
|
||||
'fabric', 'width', 'is_urgent', 'area', 'address', 'fabric_source',
|
||||
'is_fabric_received', 'craft', 'description', 'outgoing_date',
|
||||
'curve', 'new_curve', 'position', 'created_by_name',
|
||||
@@ -240,7 +242,7 @@ class PrintingOrderDetailSerializer(serializers.ModelSerializer):
|
||||
'is_invalid', 'process', 'process_name', 'progress', 'print_count',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count']
|
||||
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at', 'progress', 'print_count', 'merchant_id']
|
||||
|
||||
def get_created_by_name(self, obj):
|
||||
"""获取创建人名称(员工姓名)"""
|
||||
@@ -316,11 +318,12 @@ class PrintingJobListSerializer(serializers.ModelSerializer):
|
||||
last_completed_state = serializers.CharField(read_only=True)
|
||||
business_object_id = serializers.SerializerMethodField()
|
||||
batch_advance_records = serializers.SerializerMethodField()
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PrintingJob
|
||||
fields = [
|
||||
'id', 'original_id', 'printing_order', 'printing_order_id', 'product', 'product_name',
|
||||
'id', 'original_id', 'merchant_id', 'printing_order', 'printing_order_id', 'product', 'product_name',
|
||||
'product_image_url', 'has_started',
|
||||
'quantity', 'unit', 'size', 'pieces', 'description',
|
||||
'work_state', 'work_state_display',
|
||||
@@ -332,7 +335,7 @@ class PrintingJobListSerializer(serializers.ModelSerializer):
|
||||
read_only_fields = [
|
||||
'id', 'created_at', 'updated_at',
|
||||
'status', 'is_completed', 'progress_percentage', 'last_completed_state',
|
||||
'business_object_id'
|
||||
'business_object_id', 'merchant_id'
|
||||
]
|
||||
|
||||
def get_business_object_id(self, obj):
|
||||
@@ -394,11 +397,12 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer):
|
||||
last_completed_state = serializers.CharField(read_only=True)
|
||||
business_object_id = serializers.SerializerMethodField()
|
||||
batch_advance_records = serializers.SerializerMethodField()
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PrintingJob
|
||||
fields = [
|
||||
'id', 'original_id', 'printing_order', 'printing_order_id', 'product', 'product_name', 'product_code',
|
||||
'id', 'original_id', 'merchant_id', 'printing_order', 'printing_order_id', 'product', 'product_name', 'product_code',
|
||||
'quantity', 'unit', 'size', 'pieces', 'description',
|
||||
'work_state', 'work_state_display',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
@@ -411,7 +415,7 @@ class PrintingJobDetailSerializer(serializers.ModelSerializer):
|
||||
'id', 'created_at', 'updated_at',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'last_completed_state',
|
||||
'business_object_id'
|
||||
'business_object_id', 'merchant_id'
|
||||
]
|
||||
|
||||
def get_business_object_id(self, obj):
|
||||
@@ -550,12 +554,13 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
last_completed_state = serializers.CharField(read_only=True)
|
||||
content_type_id = serializers.SerializerMethodField()
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
|
||||
fields = [
|
||||
'id', 'original_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
|
||||
'id', 'original_id', 'merchant_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
|
||||
'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason',
|
||||
'urgency_level', 'is_invalid',
|
||||
'customer', 'customer_name', 'area', 'default_address',
|
||||
@@ -576,7 +581,7 @@ class PlateOrderListSerializer(PlateOrderDesignCodeMixin, serializers.ModelSeria
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'status', 'progress_percentage', 'last_completed_state',
|
||||
'created_at', 'updated_at', 'print_count'
|
||||
'created_at', 'updated_at', 'print_count', 'merchant_id'
|
||||
]
|
||||
|
||||
def get_plate_image_url(self, obj):
|
||||
@@ -630,11 +635,12 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
plate_image_url = serializers.SerializerMethodField()
|
||||
process_name = serializers.SerializerMethodField()
|
||||
last_completed_state = serializers.CharField(read_only=True)
|
||||
merchant_id = serializers.IntegerField(source='merchant.id', read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
fields = [
|
||||
'id', 'original_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
|
||||
'id', 'original_id', 'merchant_id', 'design_code', 'plate_type', 'plate_date', 'plate_method',
|
||||
'plate_image', 'plate_image_url', 'image_name', 'plate_notes', 'reprint_reason',
|
||||
'urgency_level', 'is_invalid',
|
||||
'customer', 'customer_name', 'customer_phone', 'area', 'default_address',
|
||||
@@ -656,7 +662,7 @@ class PlateOrderDetailSerializer(PlateOrderDesignCodeMixin, serializers.ModelSer
|
||||
read_only_fields = [
|
||||
'id', 'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id', 'last_completed_state',
|
||||
'created_at', 'updated_at', 'print_count'
|
||||
'created_at', 'updated_at', 'print_count', 'merchant_id'
|
||||
]
|
||||
|
||||
def get_business_object_id(self, obj):
|
||||
|
||||
@@ -46,6 +46,10 @@ class PrintingOrderService:
|
||||
# 绑定创建人
|
||||
data['created_by'] = user
|
||||
|
||||
# 绑定商户(从当前用户的 employee 获取)
|
||||
if hasattr(user, 'employee') and user.employee and user.employee.merchant:
|
||||
data['merchant'] = user.employee.merchant
|
||||
|
||||
order = printing_models.PrintingOrder.objects.create(**data)
|
||||
return order
|
||||
|
||||
@@ -128,6 +132,10 @@ class PrintingJobService:
|
||||
# 绑定创建人
|
||||
data['created_by'] = user
|
||||
|
||||
# 绑定商户(从当前用户的 employee 获取)
|
||||
if hasattr(user, 'employee') and user.employee and user.employee.merchant:
|
||||
data['merchant'] = user.employee.merchant
|
||||
|
||||
# 创建 PrintingJob
|
||||
job = printing_models.PrintingJob.objects.create(**data)
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
self.assertIsNotNone(order)
|
||||
self.assertEqual(order.customer.id, self.customer.id)
|
||||
self.assertEqual(order.fabric, '纯棉布料')
|
||||
# 验证 merchant 自动绑定
|
||||
self.assertEqual(order.merchant.id, self.merchant.id)
|
||||
|
||||
def test_list_printing_orders(self):
|
||||
"""测试获取订单列表"""
|
||||
|
||||
@@ -740,9 +740,13 @@ class PlateOrderViewSet(LimitedModelViewSet):
|
||||
|
||||
def perform_create(self, serializer):
|
||||
"""
|
||||
创建时自动绑定创建人(created_by),不允许前端传参控制。
|
||||
创建时自动绑定创建人(created_by)和商户(merchant),不允许前端传参控制。
|
||||
"""
|
||||
serializer.save(created_by=self.request.user)
|
||||
user = self.request.user
|
||||
merchant = None
|
||||
if hasattr(user, 'employee') and user.employee and user.employee.merchant:
|
||||
merchant = user.employee.merchant
|
||||
serializer.save(created_by=user, merchant=merchant)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def invalidate(self, request, pk=None):
|
||||
|
||||
8
api_v1/views/shipment/__init__.py
Normal file
8
api_v1/views/shipment/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Shipment API 模块
|
||||
|
||||
提供出货单和销售品相关的 API 接口
|
||||
"""
|
||||
from .views import SalesItemByPrintingOrderView
|
||||
|
||||
__all__ = ['SalesItemByPrintingOrderView']
|
||||
37
api_v1/views/shipment/serializers.py
Normal file
37
api_v1/views/shipment/serializers.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Shipment API 序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class SalesItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
销售品序列化器(只读)
|
||||
|
||||
用于返回销售品数据
|
||||
"""
|
||||
id = serializers.IntegerField(read_only=True)
|
||||
name = serializers.CharField(read_only=True)
|
||||
quantity = serializers.DecimalField(max_digits=12, decimal_places=2, read_only=True)
|
||||
unit = serializers.IntegerField(read_only=True)
|
||||
unit_display = serializers.SerializerMethodField()
|
||||
position = serializers.CharField(read_only=True)
|
||||
remark = serializers.CharField(read_only=True)
|
||||
printing_job_id = serializers.IntegerField(read_only=True)
|
||||
customer_id = serializers.IntegerField(read_only=True)
|
||||
shipment_id = serializers.IntegerField(source='shipment.id', read_only=True, allow_null=True)
|
||||
shipment_date = serializers.DateField(source='shipment.shipment_date', read_only=True, allow_null=True)
|
||||
created_at = serializers.DateTimeField(read_only=True)
|
||||
created_by_id = serializers.IntegerField(source='created_by.id', read_only=True, allow_null=True)
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
|
||||
def get_unit_display(self, obj):
|
||||
return obj.get_unit_display()
|
||||
|
||||
def get_created_by_name(self, obj):
|
||||
if obj.created_by:
|
||||
employee = getattr(obj.created_by, 'employee', None)
|
||||
if employee:
|
||||
return employee.name
|
||||
return obj.created_by.username
|
||||
return None
|
||||
266
api_v1/views/shipment/test_api.py
Normal file
266
api_v1/views/shipment/test_api.py
Normal file
@@ -0,0 +1,266 @@
|
||||
"""
|
||||
Shipment API 测试
|
||||
"""
|
||||
from decimal import Decimal
|
||||
|
||||
from django.test import TestCase
|
||||
from django.conf import settings
|
||||
from rest_framework.test import APIClient
|
||||
from rest_framework import status
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from basic_info import models as basic_models
|
||||
from printing import models as printing_models
|
||||
from shipment import models as shipment_models
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
"""测试通过生产订单查询销售品 API"""
|
||||
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
|
||||
# 创建商户
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试印花厂',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
|
||||
# 创建用户
|
||||
self.user = User.objects.create_user(
|
||||
username='testuser',
|
||||
password='testpass123',
|
||||
email='test@example.com'
|
||||
)
|
||||
|
||||
# 创建员工并关联商户
|
||||
self.employee = basic_models.Employee.objects.create(
|
||||
sys_user=self.user,
|
||||
merchant=self.merchant,
|
||||
name='测试员工',
|
||||
mobile='13800138000',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
# 创建客户
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
mobile='13900139000',
|
||||
area='测试地区'
|
||||
)
|
||||
|
||||
# 创建流程
|
||||
self.state1 = stateflow_models.State.objects.create(name='待印染')
|
||||
self.state2 = stateflow_models.State.objects.create(name='印染中')
|
||||
self.state3 = stateflow_models.State.objects.create(name='已完成')
|
||||
|
||||
self.process = stateflow_models.Process.objects.create(name='印染流程')
|
||||
self.process.replace_nodes([self.state1, self.state2, self.state3])
|
||||
|
||||
# 创建产品分类
|
||||
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='测试产品',
|
||||
human_id='TEST001',
|
||||
)
|
||||
|
||||
# 创建印染订单
|
||||
self.printing_order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试面料',
|
||||
width='150cm',
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建印染任务
|
||||
self.printing_job1 = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
printing_order=self.printing_order,
|
||||
product=self.product,
|
||||
quantity=100,
|
||||
unit='米',
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.printing_job2 = printing_models.PrintingJob.objects.create(
|
||||
merchant=self.merchant,
|
||||
printing_order=self.printing_order,
|
||||
product=self.product,
|
||||
quantity=200,
|
||||
unit='米',
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建出货单
|
||||
self.shipment = shipment_models.Shipment.objects.create(
|
||||
customer=self.customer,
|
||||
shipment_date='2026-01-14',
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建销售品 - 未关联出货单
|
||||
self.sales_item1 = shipment_models.SalesItem.objects.create(
|
||||
name='销售品1',
|
||||
quantity=Decimal('50.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=self.printing_job1.id,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.sales_item2 = shipment_models.SalesItem.objects.create(
|
||||
name='销售品2',
|
||||
quantity=Decimal('30.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=self.printing_job1.id,
|
||||
position='A1-01',
|
||||
remark='备注信息',
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建销售品 - 已关联出货单
|
||||
self.sales_item3 = shipment_models.SalesItem.objects.create(
|
||||
name='销售品3(已出货)',
|
||||
quantity=Decimal('100.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
printing_job_id=self.printing_job2.id,
|
||||
shipment=self.shipment,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建与该订单无关的销售品
|
||||
self.sales_item_other = shipment_models.SalesItem.objects.create(
|
||||
name='其它销售品',
|
||||
quantity=Decimal('999.00'),
|
||||
unit=shipment_models.UnitChoices.PIECE,
|
||||
printing_job_id=99999, # 不存在的 job
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 认证用户
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_get_sales_items_by_printing_order_exclude_shipped(self):
|
||||
"""测试查询销售品 - 默认不包含已出货的"""
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{self.printing_order.id}/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
# 应该只返回2个未出货的销售品
|
||||
self.assertEqual(data['count'], 2)
|
||||
|
||||
# 检查返回的销售品
|
||||
item_ids = [item['id'] for item in data['results']]
|
||||
self.assertIn(self.sales_item1.id, item_ids)
|
||||
self.assertIn(self.sales_item2.id, item_ids)
|
||||
self.assertNotIn(self.sales_item3.id, item_ids) # 已出货的不应该在列表中
|
||||
self.assertNotIn(self.sales_item_other.id, item_ids) # 其它订单的也不在
|
||||
|
||||
def test_get_sales_items_by_printing_order_include_shipped(self):
|
||||
"""测试查询销售品 - 包含已出货的"""
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{self.printing_order.id}/?include_already_has_shipment=true'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
# 应该返回3个销售品(包含已出货的)
|
||||
self.assertEqual(data['count'], 3)
|
||||
|
||||
# 检查返回的销售品
|
||||
item_ids = [item['id'] for item in data['results']]
|
||||
self.assertIn(self.sales_item1.id, item_ids)
|
||||
self.assertIn(self.sales_item2.id, item_ids)
|
||||
self.assertIn(self.sales_item3.id, item_ids) # 已出货的也应该在列表中
|
||||
self.assertNotIn(self.sales_item_other.id, item_ids) # 其它订单的依然不在
|
||||
|
||||
def test_get_sales_items_response_format(self):
|
||||
"""测试返回数据格式"""
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{self.printing_order.id}/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
# 找到 sales_item2(包含 position 和 remark)
|
||||
item = next(item for item in data['results'] if item['id'] == self.sales_item2.id)
|
||||
|
||||
# 检查所有字段
|
||||
self.assertEqual(item['name'], '销售品2')
|
||||
self.assertEqual(Decimal(item['quantity']), Decimal('30.00'))
|
||||
self.assertEqual(item['unit'], shipment_models.UnitChoices.METER)
|
||||
self.assertEqual(item['unit_display'], '米')
|
||||
self.assertEqual(item['position'], 'A1-01')
|
||||
self.assertEqual(item['remark'], '备注信息')
|
||||
self.assertEqual(item['printing_job_id'], self.printing_job1.id)
|
||||
self.assertIsNone(item['shipment_id'])
|
||||
self.assertIsNone(item['shipment_date'])
|
||||
self.assertIsNotNone(item['created_at'])
|
||||
self.assertEqual(item['created_by_id'], self.user.id)
|
||||
|
||||
def test_get_sales_items_shipped_item_format(self):
|
||||
"""测试已出货的销售品返回格式"""
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{self.printing_order.id}/?include_already_has_shipment=true'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
# 找到已出货的销售品
|
||||
item = next(item for item in data['results'] if item['id'] == self.sales_item3.id)
|
||||
|
||||
# 检查出货单信息
|
||||
self.assertEqual(item['shipment_id'], self.shipment.id)
|
||||
self.assertEqual(item['shipment_date'], '2026-01-14')
|
||||
|
||||
def test_get_sales_items_printing_order_not_found(self):
|
||||
"""测试生产订单不存在"""
|
||||
url = '/api/v1/shipment/sales-items/by-printing-order/99999/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertIn('不存在', response.json()['detail'])
|
||||
|
||||
def test_get_sales_items_empty_result(self):
|
||||
"""测试生产订单没有关联销售品"""
|
||||
# 创建一个没有销售品的订单
|
||||
empty_order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试面料2',
|
||||
width='150cm',
|
||||
process=self.process,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{empty_order.id}/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
data = response.json()
|
||||
|
||||
self.assertEqual(data['count'], 0)
|
||||
self.assertEqual(data['results'], [])
|
||||
|
||||
def test_get_sales_items_unauthenticated(self):
|
||||
"""测试未认证用户"""
|
||||
self.client.logout()
|
||||
|
||||
url = f'/api/v1/shipment/sales-items/by-printing-order/{self.printing_order.id}/'
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
78
api_v1/views/shipment/views.py
Normal file
78
api_v1/views/shipment/views.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
Shipment API ViewSet
|
||||
"""
|
||||
from rest_framework import status
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .serializers import SalesItemSerializer
|
||||
|
||||
|
||||
class SalesItemByPrintingOrderView(APIView):
|
||||
"""
|
||||
通过生产订单查询销售品
|
||||
|
||||
GET /api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/
|
||||
|
||||
返回与该 PrintingOrder 下所有 PrintingJob 关联的 SalesItem 列表。
|
||||
|
||||
查询参数:
|
||||
- include_already_has_shipment: 是否包含已关联出货单的销售品(true/false),默认 false
|
||||
|
||||
返回:
|
||||
{
|
||||
"count": 5,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "产品A",
|
||||
"quantity": "100.00",
|
||||
"unit": 1,
|
||||
"unit_display": "米",
|
||||
"position": "A1-01",
|
||||
"remark": "",
|
||||
"printing_job_id": 123,
|
||||
"customer_id": null,
|
||||
"shipment_id": null,
|
||||
"shipment_date": null,
|
||||
"created_at": "2026-01-14T10:00:00Z",
|
||||
"created_by_id": 1,
|
||||
"created_by_name": "张三"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, printing_order_id):
|
||||
# 验证生产订单是否存在
|
||||
from printing.models import PrintingOrder
|
||||
try:
|
||||
printing_order = PrintingOrder.objects.get(id=printing_order_id)
|
||||
except PrintingOrder.DoesNotExist:
|
||||
return Response(
|
||||
{'detail': f'生产订单 {printing_order_id} 不存在'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# 获取查询参数
|
||||
include_already_has_shipment = request.query_params.get(
|
||||
'include_already_has_shipment', 'false'
|
||||
).lower() == 'true'
|
||||
|
||||
# 调用 shipment 业务逻辑
|
||||
from shipment.services import get_sales_items_by_printing_order
|
||||
sales_items = get_sales_items_by_printing_order(
|
||||
printing_order_id=printing_order.id,
|
||||
include_already_has_shipment=include_already_has_shipment,
|
||||
)
|
||||
|
||||
# 序列化返回
|
||||
serializer = SalesItemSerializer(sales_items, many=True)
|
||||
|
||||
return Response({
|
||||
'count': len(serializer.data),
|
||||
'results': serializer.data
|
||||
})
|
||||
191
docs/2026-01-13_summary.md
Normal file
191
docs/2026-01-13_summary.md
Normal file
@@ -0,0 +1,191 @@
|
||||
# 2026-01-13 工作日志
|
||||
|
||||
## 今日目标
|
||||
|
||||
1. SSE 模块重构 → Notifications 通知系统(独立分支)
|
||||
2. Stateflow 信号机制 + Shipment 模块开发(主分支)
|
||||
|
||||
---
|
||||
|
||||
## 已完成工作(SSE 重构分支)
|
||||
|
||||
### A1. SSE 问题修复
|
||||
|
||||
- **根因**:SSE 长连接占用数据库连接不释放,导致连接池耗尽
|
||||
- **修复**:在 `sse/views.py` 中认证后立即 `connection.close()`
|
||||
- **新增**:连接超时(30分钟)、心跳超时(2分钟)、连接数上限(每商户30个)
|
||||
|
||||
### A2. Notifications 通知系统(新模块)
|
||||
|
||||
- **目的**:将事件发布与 SSE 渠道解耦,支持未来扩展(企业微信等)
|
||||
- **文件结构**:
|
||||
```
|
||||
notifications/
|
||||
├── __init__.py
|
||||
├── apps.py # 渠道注册
|
||||
├── base.py # NotificationPayload + NotificationChannel 抽象基类
|
||||
├── dispatcher.py # NotificationDispatcher 分发器
|
||||
├── events.py # resource_changed(), object_event()
|
||||
└── channels/
|
||||
└── sse.py # SSE 渠道实现
|
||||
```
|
||||
|
||||
- **核心设计**:
|
||||
- 业务代码调用 `notifications.events`,不关心具体渠道
|
||||
- 分发器同步调用各渠道的 `send()`
|
||||
- 各渠道自行决定同步/异步(SSE 同步放入队列,企业微信应内部 Celery)
|
||||
- 新增渠道只需实现 `NotificationChannel` 并注册
|
||||
|
||||
- **配置**:
|
||||
- `SSE_ENABLED` 同时控制 SSE 端点和通知渠道
|
||||
|
||||
### A3. 文档整理
|
||||
|
||||
- **创建**:
|
||||
- `docs/notifications.md` — 后端通知接口文档
|
||||
- `docs/notifications_frontend.md` — 前端 SSE 接入文档
|
||||
- `docs/sse_refactor_execute_django_ver.md` — 技术方案文档
|
||||
|
||||
- **删除**(减少心智成本):
|
||||
- `docs/sse.md`
|
||||
- `docs/sse_event_interface.md`
|
||||
- `docs/sse_refactor.md`
|
||||
|
||||
### A4. 兼容处理
|
||||
|
||||
- `sse/events.py` 改为兼容层,调用会触发 `DeprecationWarning`
|
||||
- 现有业务代码无需立即修改
|
||||
|
||||
---
|
||||
|
||||
## 已完成工作(主分支)
|
||||
|
||||
### 1. Stateflow 信号机制
|
||||
|
||||
- **文件**:`stateflow/signals.py`(新增)
|
||||
- **功能**:
|
||||
- 定义 `process_completed` 信号 — 流程全部完成时触发
|
||||
- 定义 `state_advanced` 信号 — 每次状态推进时触发
|
||||
- `sender` 使用 `content_object.__class__`,支持按类型过滤
|
||||
- 参数包含:`process_id`、`business_object`、`content_object`、`last_completed_state_id`、`last_completed_by` 等
|
||||
|
||||
- **文件**:`stateflow/services.py`(修改)
|
||||
- 在 `advance_to_next_state` 中发送信号
|
||||
- 添加日志记录信号发送过程
|
||||
|
||||
---
|
||||
|
||||
### 2. Shipment 模块(新建)
|
||||
|
||||
- **文件结构**:
|
||||
```
|
||||
shipment/
|
||||
├── __init__.py
|
||||
├── apps.py
|
||||
├── models.py
|
||||
├── admin.py
|
||||
└── migrations/
|
||||
├── 0001_initial.py
|
||||
├── 0002_salesitem_shipment_nullable.py
|
||||
└── 0003_salesitem_position_remark.py
|
||||
```
|
||||
|
||||
- **模型**:
|
||||
- `Shipment`(出货单)— 关联客户,包含多个销售品
|
||||
- `SalesItem`(销售品)— 名称、数量、单位(米/件/码/个)、货位、备注、关联生产任务ID
|
||||
|
||||
- **设计特点**:
|
||||
- `SalesItem.shipment` 可空,支持"待分配"状态
|
||||
- `printing_job_id` 使用整数而非外键,避免模块间强依赖
|
||||
- 提供 `get_printing_job()` 方法获取关联对象,带完整 type hint
|
||||
|
||||
---
|
||||
|
||||
### 3. PrintingJob 流程完成自动创建销售品
|
||||
|
||||
- **文件**:`printing/handlers.py`(新增)
|
||||
- 监听 `process_completed` 信号(sender=PrintingJob)
|
||||
- 从指定流程节点获取"米数"参数
|
||||
- 自动创建 `SalesItem`,关联 `printing_job_id`
|
||||
|
||||
- **文件**:`printing/apps.py`(修改)
|
||||
- 在 `ready()` 中注册信号处理器
|
||||
- 添加启动日志确认注册成功
|
||||
|
||||
- **配置项**(settings.py + .env):
|
||||
```python
|
||||
PRINTING_SALES_ITEM_SOURCE_STATE_ID = env.int('PRINTING_SALES_ITEM_SOURCE_STATE_ID') # 必须配置
|
||||
PRINTING_SALES_ITEM_QUANTITY_KEY = '米数'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 其他修改
|
||||
|
||||
- **stateflow/services.py**:
|
||||
- 添加 `logging` 模块导入
|
||||
- `clone_business_object` 函数暂停使用(2026-01-13),抛出 `NotImplementedError`
|
||||
|
||||
- **flower/settings.py**:
|
||||
- 添加 `'shipment'` 到 `INSTALLED_APPS`
|
||||
- 添加销售品自动创建配置项
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
### 信号流程
|
||||
|
||||
```
|
||||
PrintingJob 流程推进完成
|
||||
↓
|
||||
stateflow.services.advance_to_next_state()
|
||||
↓
|
||||
发送 process_completed 信号 (sender=PrintingJob)
|
||||
↓
|
||||
printing.handlers.on_printing_job_process_completed() 接收
|
||||
↓
|
||||
从指定节点获取"米数"参数
|
||||
↓
|
||||
创建 SalesItem(待分配出货单)
|
||||
```
|
||||
|
||||
### 解耦设计
|
||||
|
||||
- **stateflow** 不依赖任何业务模块,只发送信号
|
||||
- **printing** 监听信号并处理自己的业务逻辑
|
||||
- **shipment** 被 printing 调用,但不知道调用者是谁
|
||||
|
||||
---
|
||||
|
||||
## 待办事项
|
||||
|
||||
### SSE 重构分支
|
||||
|
||||
- [ ] 合并到主分支后,在测试环境启用 SSE(`SSE_ENABLED=True`)
|
||||
- [ ] 前端实现重连逻辑
|
||||
- [ ] 监控数据库连接数,确认不再泄漏
|
||||
- [ ] 逐步迁移业务代码到 `notifications.events`
|
||||
|
||||
### 主分支
|
||||
|
||||
- [ ] 在 `.env` 中配置 `PRINTING_SALES_ITEM_SOURCE_STATE_ID`
|
||||
- [ ] 应用数据库迁移:`uv run python manage.py migrate shipment`
|
||||
- [ ] 测试完整流程:推进 PrintingJob 直到完成,验证 SalesItem 创建
|
||||
- [ ] 移除调试日志(生产环境前)
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
### SSE 重构分支
|
||||
|
||||
- SSE 模块此前因数据库连接泄漏导致线上灾难,已被禁用
|
||||
- Notifications 模块设计考虑了未来 Golang 迁移和企业微信扩展
|
||||
- 企业微信渠道仅有示例代码,尚未实现
|
||||
|
||||
### 主分支
|
||||
|
||||
- Shipment 模块是全新创建的,需要执行迁移
|
||||
- 信号处理器只在流程**全部完成**时触发,不是每次推进
|
||||
- 如果指定节点的"米数"参数不存在,会跳过创建并记录警告日志
|
||||
78
docs/2026-01-14_summary.md
Normal file
78
docs/2026-01-14_summary.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 2026-01-14 工作日志
|
||||
|
||||
## 已完成
|
||||
|
||||
### 1. 通过生产订单查询销售品 API
|
||||
|
||||
创建了独立的 Shipment API 模块,提供出货管理相关接口。
|
||||
|
||||
- **接口路径**: `GET /api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/`
|
||||
- **查询参数**: `include_already_has_shipment`(默认 false,不包含已出货的销售品)
|
||||
|
||||
#### 新建文件
|
||||
- `api_v1/views/shipment/__init__.py`: 模块入口
|
||||
- `api_v1/views/shipment/views.py`: API 视图 `SalesItemByPrintingOrderView`
|
||||
- `api_v1/views/shipment/serializers.py`: 序列化器 `SalesItemSerializer`
|
||||
- `api_v1/views/shipment/test_api.py`: API 测试用例(8 个测试场景)
|
||||
- `shipment/services.py`: 业务逻辑层 `get_sales_items_by_printing_order()`
|
||||
- `docs/shipment_api.md`: API 文档
|
||||
|
||||
#### 修改文件
|
||||
- `api_v1/urls.py`: 注册新路由
|
||||
|
||||
#### 业务逻辑
|
||||
1. 根据 PrintingOrder ID 获取所有 PrintingJob 的 ID
|
||||
2. 查询 SalesItem,过滤 printing_job_id 在这些 job_ids 中
|
||||
3. 根据参数决定是否过滤已关联出货单的销售品
|
||||
|
||||
---
|
||||
|
||||
### 2. Printing 模块添加 merchant 字段
|
||||
|
||||
为多租户支持,为 printing 模块的核心模型添加 `merchant` 外键字段:
|
||||
|
||||
- `PlateOrder.merchant` - 开版订单所属商户
|
||||
- `PrintingOrder.merchant` - 印染订单所属商户
|
||||
- `PrintingJob.merchant` - 印染任务所属商户
|
||||
|
||||
所有字段设置为可空(`null=True, blank=True`),以兼容现有数据。
|
||||
|
||||
**迁移文件**: `printing/migrations/0029_add_merchant_to_models.py`
|
||||
|
||||
---
|
||||
|
||||
### 3. 确保 API 创建时自动绑定 merchant
|
||||
|
||||
修复了创建 API,确保新建记录时自动从当前用户获取 merchant:
|
||||
|
||||
- `PrintingOrderService.create_printing_order()` - 自动绑定 merchant
|
||||
- `PrintingJobService.create_printing_job()` - 自动绑定 merchant
|
||||
- `PlateOrderViewSet.perform_create()` - 自动绑定 merchant
|
||||
|
||||
---
|
||||
|
||||
### 4. 数据补录命令
|
||||
|
||||
创建了 management command 用于补录历史数据的 merchant_id:
|
||||
|
||||
```bash
|
||||
# 预览(不执行)
|
||||
python manage.py backfill_merchant <merchant_id> --dry-run
|
||||
|
||||
# 执行补录
|
||||
python manage.py backfill_merchant <merchant_id>
|
||||
```
|
||||
|
||||
**文件**: `printing/management/commands/backfill_merchant.py`
|
||||
|
||||
---
|
||||
|
||||
## 待办
|
||||
|
||||
---
|
||||
|
||||
## 备注
|
||||
|
||||
- API 独立于 printing 模块,避免影响现有功能
|
||||
- 完整的测试覆盖:正常查询、包含已出货、数据格式、404 错误、空结果、未认证
|
||||
- printing 和 shipment 模块全部测试通过(31个测试用例)
|
||||
157
docs/shipment_api.md
Normal file
157
docs/shipment_api.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Shipment API 文档
|
||||
|
||||
出货管理模块 API 文档,包含出货单和销售品相关接口。
|
||||
|
||||
## 目录
|
||||
|
||||
- [通过生产订单查询销售品](#通过生产订单查询销售品)
|
||||
|
||||
---
|
||||
|
||||
## 通过生产订单查询销售品
|
||||
|
||||
查询与指定生产订单(PrintingOrder)关联的所有销售品(SalesItem)。
|
||||
|
||||
### 接口信息
|
||||
|
||||
- **URL**: `/api/v1/shipment/sales-items/by-printing-order/<printing_order_id>/`
|
||||
- **Method**: `GET`
|
||||
- **认证**: 需要登录(JWT Token)
|
||||
|
||||
### 路径参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| printing_order_id | int | 是 | 生产订单ID |
|
||||
|
||||
### 查询参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|------|------|------|--------|------|
|
||||
| include_already_has_shipment | bool | 否 | false | 是否包含已关联出货单的销售品 |
|
||||
|
||||
### 业务逻辑
|
||||
|
||||
1. 根据 `printing_order_id` 获取该生产订单下所有 `PrintingJob` 的 ID
|
||||
2. 查询 `SalesItem`,过滤 `printing_job_id` 在这些 job ID 中的记录
|
||||
3. 根据 `include_already_has_shipment` 参数决定是否过滤已关联出货单的销售品:
|
||||
- `false`(默认):只返回 `shipment` 为空的销售品(待出货)
|
||||
- `true`:返回所有销售品(包含已出货的)
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 2,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "产品A - 红色",
|
||||
"quantity": "100.00",
|
||||
"unit": 1,
|
||||
"unit_display": "米",
|
||||
"position": "A1-01",
|
||||
"remark": "加急处理",
|
||||
"printing_job_id": 123,
|
||||
"customer_id": null,
|
||||
"shipment_id": null,
|
||||
"shipment_date": null,
|
||||
"created_at": "2026-01-14T10:00:00Z",
|
||||
"created_by_id": 1,
|
||||
"created_by_name": "张三"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "产品B - 蓝色",
|
||||
"quantity": "50.50",
|
||||
"unit": 1,
|
||||
"unit_display": "米",
|
||||
"position": "",
|
||||
"remark": "",
|
||||
"printing_job_id": 124,
|
||||
"customer_id": 10,
|
||||
"shipment_id": 5,
|
||||
"shipment_date": "2026-01-13",
|
||||
"created_at": "2026-01-13T15:30:00Z",
|
||||
"created_by_id": 2,
|
||||
"created_by_name": "李四"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| count | int | 结果总数 |
|
||||
| results | array | 销售品列表 |
|
||||
| results[].id | int | 销售品ID |
|
||||
| results[].name | string | 销售品名称 |
|
||||
| results[].quantity | string | 数量(Decimal,保留2位小数) |
|
||||
| results[].unit | int | 单位编码(1=米, 2=件, 3=码, 4=个) |
|
||||
| results[].unit_display | string | 单位显示名称 |
|
||||
| results[].position | string | 货位(可能为空) |
|
||||
| results[].remark | string | 备注(可能为空) |
|
||||
| results[].printing_job_id | int/null | 关联的生产任务ID |
|
||||
| results[].customer_id | int/null | 销售品级别的客户ID |
|
||||
| results[].shipment_id | int/null | 关联的出货单ID,null 表示未出货 |
|
||||
| results[].shipment_date | string/null | 出货日期(YYYY-MM-DD),null 表示未出货 |
|
||||
| results[].created_at | string | 创建时间(ISO 8601) |
|
||||
| results[].created_by_id | int/null | 创建人ID |
|
||||
| results[].created_by_name | string/null | 创建人名称 |
|
||||
|
||||
### 错误响应
|
||||
|
||||
#### 404 Not Found - 生产订单不存在
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "生产订单 999 不存在"
|
||||
}
|
||||
```
|
||||
|
||||
#### 401 Unauthorized - 未登录
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "Authentication credentials were not provided."
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### 查询待出货的销售品(默认)
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/123/' \
|
||||
-H 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
#### 查询所有销售品(包含已出货)
|
||||
|
||||
```bash
|
||||
curl -X GET \
|
||||
'https://api.example.com/api/v1/shipment/sales-items/by-printing-order/123/?include_already_has_shipment=true' \
|
||||
-H 'Authorization: Bearer <token>'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 单位编码对照表
|
||||
|
||||
| 编码 | 名称 |
|
||||
|------|------|
|
||||
| 1 | 米 |
|
||||
| 2 | 件 |
|
||||
| 3 | 码 |
|
||||
| 4 | 个 |
|
||||
|
||||
---
|
||||
|
||||
## 相关模块
|
||||
|
||||
- `shipment/services.py`: 业务逻辑层
|
||||
- `api_v1/views/shipment/`: API 视图层
|
||||
- `shipment/models.py`: 数据模型(SalesItem, Shipment)
|
||||
0
printing/management/__init__.py
Normal file
0
printing/management/__init__.py
Normal file
0
printing/management/commands/__init__.py
Normal file
0
printing/management/commands/__init__.py
Normal file
79
printing/management/commands/backfill_merchant.py
Normal file
79
printing/management/commands/backfill_merchant.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
补录 merchant_id 字段的 management command
|
||||
|
||||
用于为历史数据补充 merchant_id 字段值。
|
||||
"""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
|
||||
from basic_info.models import Merchant
|
||||
from printing.models import PlateOrder, PrintingOrder, PrintingJob
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = '为 PlateOrder、PrintingOrder、PrintingJob 补录 merchant_id 字段'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'merchant_id',
|
||||
type=int,
|
||||
help='要设置的商户ID'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='仅显示将要更新的记录数,不实际执行'
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
merchant_id = options['merchant_id']
|
||||
dry_run = options['dry_run']
|
||||
|
||||
# 验证商户是否存在
|
||||
try:
|
||||
merchant = Merchant.objects.get(id=merchant_id)
|
||||
except Merchant.DoesNotExist:
|
||||
raise CommandError(f'商户 ID {merchant_id} 不存在')
|
||||
|
||||
self.stdout.write(f'目标商户: {merchant.name} (ID: {merchant.id})')
|
||||
self.stdout.write('')
|
||||
|
||||
# 统计需要更新的记录
|
||||
plate_orders_count = PlateOrder.objects.filter(merchant__isnull=True).count()
|
||||
printing_orders_count = PrintingOrder.objects.filter(merchant__isnull=True).count()
|
||||
printing_jobs_count = PrintingJob.objects.filter(merchant__isnull=True).count()
|
||||
|
||||
self.stdout.write(f'待更新记录:')
|
||||
self.stdout.write(f' - PlateOrder: {plate_orders_count} 条')
|
||||
self.stdout.write(f' - PrintingOrder: {printing_orders_count} 条')
|
||||
self.stdout.write(f' - PrintingJob: {printing_jobs_count} 条')
|
||||
self.stdout.write('')
|
||||
|
||||
total = plate_orders_count + printing_orders_count + printing_jobs_count
|
||||
if total == 0:
|
||||
self.stdout.write(self.style.SUCCESS('没有需要更新的记录'))
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('--dry-run 模式,未执行实际更新'))
|
||||
return
|
||||
|
||||
# 执行更新
|
||||
with transaction.atomic():
|
||||
updated_plate_orders = PlateOrder.objects.filter(
|
||||
merchant__isnull=True
|
||||
).update(merchant=merchant)
|
||||
|
||||
updated_printing_orders = PrintingOrder.objects.filter(
|
||||
merchant__isnull=True
|
||||
).update(merchant=merchant)
|
||||
|
||||
updated_printing_jobs = PrintingJob.objects.filter(
|
||||
merchant__isnull=True
|
||||
).update(merchant=merchant)
|
||||
|
||||
self.stdout.write('')
|
||||
self.stdout.write(self.style.SUCCESS(f'更新完成:'))
|
||||
self.stdout.write(self.style.SUCCESS(f' - PlateOrder: {updated_plate_orders} 条'))
|
||||
self.stdout.write(self.style.SUCCESS(f' - PrintingOrder: {updated_printing_orders} 条'))
|
||||
self.stdout.write(self.style.SUCCESS(f' - PrintingJob: {updated_printing_jobs} 条'))
|
||||
30
printing/migrations/0029_add_merchant_to_models.py
Normal file
30
printing/migrations/0029_add_merchant_to_models.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Generated by Django 5.2.8 on 2026-01-14 05:41
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('basic_info', '0024_frontend_page_and_visible_pages'),
|
||||
('printing', '0028_batch_advance_record_only_parameters'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='plateorder',
|
||||
name='merchant',
|
||||
field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='plate_orders', to='basic_info.merchant', verbose_name='商户'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='printingjob',
|
||||
name='merchant',
|
||||
field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='printing_jobs', to='basic_info.merchant', verbose_name='商户'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='printingorder',
|
||||
name='merchant',
|
||||
field=models.ForeignKey(blank=True, help_text='所属商户,可空以兼容历史数据', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='printing_orders', to='basic_info.merchant', verbose_name='商户'),
|
||||
),
|
||||
]
|
||||
@@ -11,6 +11,17 @@ from stateflow import models as stateflow_models
|
||||
class PlateOrder(ModelBase):
|
||||
"""开版管理订单"""
|
||||
|
||||
# 商户关联(多租户)
|
||||
merchant = models.ForeignKey(
|
||||
basic_models.Merchant,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='plate_orders',
|
||||
verbose_name='商户',
|
||||
help_text='所属商户,可空以兼容历史数据'
|
||||
)
|
||||
|
||||
# 自动编号相关
|
||||
design_code = models.CharField(max_length=50, blank=True, null=True, verbose_name='设计编号')
|
||||
original_id = models.PositiveBigIntegerField(
|
||||
@@ -245,6 +256,18 @@ class PlateOrder(ModelBase):
|
||||
|
||||
class PrintingOrder(ModelBase):
|
||||
"""PrintingOrder model representing a printing order."""
|
||||
|
||||
# 商户关联(多租户)
|
||||
merchant = models.ForeignKey(
|
||||
basic_models.Merchant,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='printing_orders',
|
||||
verbose_name='商户',
|
||||
help_text='所属商户,可空以兼容历史数据'
|
||||
)
|
||||
|
||||
customer = models.ForeignKey(
|
||||
basic_models.Customer,
|
||||
on_delete=models.PROTECT,
|
||||
@@ -327,6 +350,18 @@ class PrintingJobWorkStateEnum(models.IntegerChoices):
|
||||
|
||||
class PrintingJob(ModelBase):
|
||||
"""PrintingJob model representing a printing job associated with an order."""
|
||||
|
||||
# 商户关联(多租户)
|
||||
merchant = models.ForeignKey(
|
||||
basic_models.Merchant,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='printing_jobs',
|
||||
verbose_name='商户',
|
||||
help_text='所属商户,可空以兼容历史数据'
|
||||
)
|
||||
|
||||
printing_order = models.ForeignKey(
|
||||
PrintingOrder,
|
||||
on_delete=models.CASCADE,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
from flower.common import ModelBase
|
||||
from basic_info import models as basic_models
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from printing.models import PrintingJob
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -138,7 +144,7 @@ class SalesItem(ModelBase):
|
||||
def __str__(self):
|
||||
return f'{self.name} x {self.quantity} {self.get_unit_display()}'
|
||||
|
||||
def get_printing_job(self):
|
||||
def get_printing_job(self) -> PrintingJob | None:
|
||||
"""
|
||||
获取关联的 PrintingJob 实例
|
||||
|
||||
@@ -150,10 +156,10 @@ class SalesItem(ModelBase):
|
||||
try:
|
||||
from printing.models import PrintingJob
|
||||
return PrintingJob.objects.get(id=self.printing_job_id)
|
||||
except Exception:
|
||||
except PrintingJob.DoesNotExist:
|
||||
return None
|
||||
|
||||
def get_customer(self):
|
||||
def get_customer(self) -> basic_models.Customer | None:
|
||||
"""
|
||||
获取关联的 Customer 实例
|
||||
|
||||
|
||||
39
shipment/services.py
Normal file
39
shipment/services.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Shipment 模块业务逻辑层
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from shipment.models import SalesItem
|
||||
|
||||
|
||||
def get_sales_items_by_printing_order(
|
||||
printing_order_id: int,
|
||||
include_already_has_shipment: bool = False,
|
||||
) -> QuerySet[SalesItem]:
|
||||
"""
|
||||
通过生产订单ID查询对应的销售品
|
||||
|
||||
Args:
|
||||
printing_order_id: 生产订单ID
|
||||
include_already_has_shipment: 是否包含已关联出货单的销售品,默认为 False
|
||||
|
||||
Returns:
|
||||
SalesItem 查询集
|
||||
"""
|
||||
from printing.models import PrintingJob
|
||||
|
||||
# 1. 获取该生产订单下所有 PrintingJob 的 ID
|
||||
job_ids = PrintingJob.objects.filter(
|
||||
printing_order_id=printing_order_id
|
||||
).values_list('id', flat=True)
|
||||
|
||||
# 2. 查询 SalesItem,过滤 printing_job_id 在这些 job_ids 中
|
||||
queryset = SalesItem.objects.filter(printing_job_id__in=list(job_ids))
|
||||
|
||||
# 3. 根据参数决定是否过滤已出货的销售品
|
||||
if not include_already_has_shipment:
|
||||
queryset = queryset.filter(shipment__isnull=True)
|
||||
|
||||
return queryset.select_related('shipment').order_by('id')
|
||||
Reference in New Issue
Block a user