forked from erp-dev/erp
fix: added permission for plate_order and printing_order api
This commit is contained in:
@@ -209,7 +209,7 @@ def _upsert_product(product_data, merchant, category):
|
||||
merchant=merchant,
|
||||
human_id=product_data.uid,
|
||||
**defaults,
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
if not product_obj.from_mdy:
|
||||
|
||||
@@ -25,7 +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
|
||||
from .views.shipment import SalesItemByPrintingOrderView, ShipmentCreateView
|
||||
|
||||
# 创建 DRF Router for Stateflow
|
||||
stateflow_router = DefaultRouter()
|
||||
@@ -100,6 +100,11 @@ urlpatterns = [
|
||||
path('stateflow/', include(stateflow_router.urls)),
|
||||
|
||||
# Shipment API
|
||||
path(
|
||||
'shipment/shipments/',
|
||||
ShipmentCreateView.as_view(),
|
||||
name='shipment_create'
|
||||
),
|
||||
path(
|
||||
'shipment/sales-items/by-printing-order/<int:printing_order_id>/',
|
||||
SalesItemByPrintingOrderView.as_view(),
|
||||
|
||||
106
api_v1/views/printing/mixins.py
Normal file
106
api_v1/views/printing/mixins.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
Printing API Mixins
|
||||
|
||||
提供客户可见性过滤等通用功能
|
||||
"""
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class CustomerVisibilityFilterMixin:
|
||||
"""
|
||||
基于客户可见性的查询过滤 Mixin
|
||||
|
||||
适用于关联了 Customer 的模型(PlateOrder, PrintingOrder, PrintingJob)
|
||||
|
||||
使用方式:
|
||||
1. 在 ViewSet 中继承此 Mixin(放在 LimitedModelViewSet 之前)
|
||||
2. 设置 customer_field 指定关联 customer 的字段路径
|
||||
3. 设置 view_all_permission 指定突破限制的权限名
|
||||
|
||||
可见性规则:
|
||||
- superuser: 无限制
|
||||
- 有 view_all_permission 权限: 无限制
|
||||
- 订单.customer 为空: 全员可见(同 merchant)
|
||||
- 订单.customer 不为空: 按客户绑定关系判断
|
||||
- customer.created_by == 当前员工 → 可见
|
||||
- 当前员工 in customer.visible_employees → 可见
|
||||
"""
|
||||
# 子类需要指定:通过哪个字段关联到 customer
|
||||
customer_field = 'customer' # PlateOrder/PrintingOrder 直接关联
|
||||
# customer_field = 'printing_order__customer' # PrintingJob 间接关联
|
||||
|
||||
# 子类需要指定:突破限制的权限名(完整格式如 'printing.view_all_plateorders')
|
||||
view_all_permission = None
|
||||
|
||||
def can_bypass_customer_visibility(self) -> bool:
|
||||
"""
|
||||
是否可以突破客户可见性限制
|
||||
|
||||
注意:view_all_customers 权限不影响此判断
|
||||
"""
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
return True
|
||||
# 仅检查订单级别的专属权限
|
||||
if self.view_all_permission and user.has_perm(self.view_all_permission):
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter_by_customer_visibility(self, queryset):
|
||||
"""
|
||||
应用客户可见性过滤
|
||||
|
||||
Returns:
|
||||
过滤后的 queryset
|
||||
"""
|
||||
if self.can_bypass_customer_visibility():
|
||||
return queryset
|
||||
|
||||
user = self.request.user
|
||||
emp = getattr(user, 'employee', None)
|
||||
if emp is None:
|
||||
return queryset.none()
|
||||
|
||||
# 可见的客户条件:
|
||||
# 1. customer.created_by == 当前员工
|
||||
# 2. 当前员工 in customer.visible_employees
|
||||
visible_customer_filter = (
|
||||
Q(**{f'{self.customer_field}__created_by': emp}) |
|
||||
Q(**{f'{self.customer_field}__visible_employees': emp})
|
||||
)
|
||||
|
||||
# customer 为空时全员可见(同 merchant 下)
|
||||
no_customer_filter = Q(**{f'{self.customer_field}__isnull': True})
|
||||
|
||||
return queryset.filter(visible_customer_filter | no_customer_filter).distinct()
|
||||
|
||||
def filter_by_merchant(self, queryset):
|
||||
"""
|
||||
应用 merchant 隔离过滤
|
||||
|
||||
Returns:
|
||||
过滤后的 queryset,如果用户没有关联 merchant 则返回空
|
||||
"""
|
||||
user = self.request.user
|
||||
if user.is_superuser:
|
||||
return queryset
|
||||
|
||||
emp = getattr(user, 'employee', None)
|
||||
if emp is None or emp.merchant is None:
|
||||
return queryset.none()
|
||||
|
||||
return queryset.filter(merchant=emp.merchant)
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
重写 get_queryset,应用 merchant 隔离和客户可见性过滤
|
||||
"""
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# 1. merchant 隔离(必须)
|
||||
queryset = self.filter_by_merchant(queryset)
|
||||
|
||||
# 2. 客户可见性过滤
|
||||
queryset = self.filter_by_customer_visibility(queryset)
|
||||
|
||||
return queryset
|
||||
@@ -46,12 +46,13 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
# 创建客户
|
||||
# 创建客户(设置 created_by 以便客户可见性过滤)
|
||||
self.customer = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='测试客户',
|
||||
mobile='13900139000',
|
||||
area='测试地区'
|
||||
area='测试地区',
|
||||
created_by=self.employee
|
||||
)
|
||||
|
||||
# 创建流程
|
||||
@@ -72,7 +73,9 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
view_perm = Permission.objects.get(codename='view_printingorder')
|
||||
add_perm = Permission.objects.get(codename='add_printingorder')
|
||||
change_perm = Permission.objects.get(codename='change_printingorder')
|
||||
self.user.user_permissions.add(view_perm, add_perm, change_perm)
|
||||
# 添加 view_all 权限以便测试 API 功能(权限过滤逻辑在专门的测试类中验证)
|
||||
view_all_perm = Permission.objects.get(codename='view_all_printingorders')
|
||||
self.user.user_permissions.add(view_perm, add_perm, change_perm, view_all_perm)
|
||||
|
||||
def test_create_printing_order(self):
|
||||
"""测试创建印染订单"""
|
||||
@@ -113,11 +116,13 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
"""测试获取订单列表"""
|
||||
# 创建测试订单
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料1',
|
||||
width='150cm'
|
||||
)
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料2',
|
||||
width='160cm'
|
||||
@@ -131,6 +136,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_retrieve_printing_order(self):
|
||||
"""测试获取订单详情"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -148,6 +154,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_update_printing_order(self):
|
||||
"""测试更新订单"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='旧布料',
|
||||
width='150cm'
|
||||
@@ -176,6 +183,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_partial_update_printing_order(self):
|
||||
"""测试部分更新订单"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='原布料',
|
||||
width='150cm',
|
||||
@@ -206,6 +214,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
self.user.user_permissions.add(delete_perm)
|
||||
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm'
|
||||
@@ -227,6 +236,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
self.user.user_permissions.add(invalidate_perm)
|
||||
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -243,6 +253,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_invalidate_without_permission(self):
|
||||
"""测试没有权限时作废订单失败"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm'
|
||||
@@ -259,6 +270,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
self.user.user_permissions.add(activate_perm)
|
||||
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -275,6 +287,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_activate_without_permission(self):
|
||||
"""测试没有权限时恢复订单失败"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -288,6 +301,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_mark_fabric_received(self):
|
||||
"""测试标记布料已收"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -304,6 +318,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_mark_fabric_received_already_received(self):
|
||||
"""测试重复标记布料已收"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -319,15 +334,18 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
customer2 = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='客户2',
|
||||
mobile='13900139001'
|
||||
mobile='13900139001',
|
||||
created_by=self.employee
|
||||
)
|
||||
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料1',
|
||||
width='150cm'
|
||||
)
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=customer2,
|
||||
fabric='布料2',
|
||||
width='160cm'
|
||||
@@ -340,12 +358,14 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_filter_by_urgent(self):
|
||||
"""测试按紧急状态过滤"""
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料1',
|
||||
width='150cm',
|
||||
is_urgent=True
|
||||
)
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料2',
|
||||
width='160cm',
|
||||
@@ -360,11 +380,13 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_search_by_fabric(self):
|
||||
"""测试按布料搜索"""
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='纯棉布料',
|
||||
width='150cm'
|
||||
)
|
||||
printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='涤纶布料',
|
||||
width='160cm'
|
||||
@@ -378,11 +400,13 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_ordering(self):
|
||||
"""测试排序"""
|
||||
order1 = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料1',
|
||||
width='150cm'
|
||||
)
|
||||
order2 = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='布料2',
|
||||
width='160cm'
|
||||
@@ -406,6 +430,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
from datetime import date
|
||||
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm'
|
||||
@@ -452,6 +477,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_order_progress_in_list(self):
|
||||
"""测试订单列表包含进度字段"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -466,6 +492,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_update_process_when_no_jobs(self):
|
||||
"""测试没有任务时可以修改流程"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -491,6 +518,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_cannot_update_process_when_job_started(self):
|
||||
"""测试有已开始的任务时不能修改流程"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -549,6 +577,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
"""测试订单列表包含 jobs_status_summary 字段(PrintingJob 状态汇总)"""
|
||||
# 创建订单
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='测试布料',
|
||||
width='150cm',
|
||||
@@ -622,6 +651,7 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
def test_jobs_status_summary_empty_when_no_jobs(self):
|
||||
"""测试没有任务时 jobs_status_summary 为空列表"""
|
||||
order = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer,
|
||||
fabric='无任务订单',
|
||||
width='150cm',
|
||||
@@ -640,3 +670,174 @@ class PrintingOrderAPITestCase(TestCase):
|
||||
|
||||
summary = response.data['results'][0]['jobs_status_summary']
|
||||
self.assertEqual(summary, [])
|
||||
|
||||
|
||||
class CustomerVisibilityFilterTestCase(TestCase):
|
||||
"""测试客户可见性过滤"""
|
||||
|
||||
def setUp(self):
|
||||
from django.core.cache import cache
|
||||
cache.clear()
|
||||
|
||||
self.client = APIClient()
|
||||
|
||||
# 创建商户
|
||||
self.merchant = basic_models.Merchant.objects.create(
|
||||
name='测试印花厂',
|
||||
type=basic_models.MerchantTypeEnum.FACTORY
|
||||
)
|
||||
|
||||
# 创建第二个商户(用于测试 merchant 隔离)
|
||||
self.merchant2 = 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.user2 = User.objects.create_user(
|
||||
username='testuser2',
|
||||
password='testpass123',
|
||||
email='test2@example.com'
|
||||
)
|
||||
self.employee2 = basic_models.Employee.objects.create(
|
||||
sys_user=self.user2,
|
||||
merchant=self.merchant,
|
||||
name='测试员工2',
|
||||
mobile='13800138001',
|
||||
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||
)
|
||||
|
||||
# 创建客户(employee 创建的,employee 可见)
|
||||
self.customer_by_emp1 = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='员工1的客户',
|
||||
mobile='13900139000',
|
||||
created_by=self.employee
|
||||
)
|
||||
|
||||
# 创建客户(employee2 创建的,默认 employee 不可见)
|
||||
self.customer_by_emp2 = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant,
|
||||
name='员工2的客户',
|
||||
mobile='13900139001',
|
||||
created_by=self.employee2
|
||||
)
|
||||
|
||||
# 创建 employee 可见客户的订单
|
||||
self.order_visible = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer_by_emp1,
|
||||
fabric='可见订单',
|
||||
width='150cm'
|
||||
)
|
||||
|
||||
# 创建 employee 不可见客户的订单
|
||||
self.order_invisible = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant,
|
||||
customer=self.customer_by_emp2,
|
||||
fabric='不可见订单',
|
||||
width='150cm'
|
||||
)
|
||||
|
||||
# 创建其他商户的订单(merchant 隔离测试)
|
||||
self.customer_other_merchant = basic_models.Customer.objects.create(
|
||||
merchant=self.merchant2,
|
||||
name='其他商户客户',
|
||||
mobile='13900139002'
|
||||
)
|
||||
self.order_other_merchant = printing_models.PrintingOrder.objects.create(
|
||||
merchant=self.merchant2,
|
||||
customer=self.customer_other_merchant,
|
||||
fabric='其他商户订单',
|
||||
width='150cm'
|
||||
)
|
||||
|
||||
# 给用户添加基础权限(不含 view_all)
|
||||
view_perm = Permission.objects.get(codename='view_printingorder')
|
||||
self.user.user_permissions.add(view_perm)
|
||||
self.user2.user_permissions.add(view_perm)
|
||||
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_list_only_visible_orders(self):
|
||||
"""测试普通员工只能看到可见客户的订单"""
|
||||
response = self.client.get('/api/v1/printing-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 应该只能看到自己创建的客户的订单 = 1
|
||||
self.assertEqual(response.data['count'], 1)
|
||||
|
||||
fabrics = [r['fabric'] for r in response.data['results']]
|
||||
self.assertIn('可见订单', fabrics)
|
||||
self.assertNotIn('不可见订单', fabrics)
|
||||
self.assertNotIn('其他商户订单', fabrics)
|
||||
|
||||
def test_retrieve_visible_order(self):
|
||||
"""测试可以获取可见订单详情"""
|
||||
response = self.client.get(f'/api/v1/printing-orders/{self.order_visible.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
def test_retrieve_invisible_order_404(self):
|
||||
"""测试无法获取不可见订单详情"""
|
||||
response = self.client.get(f'/api/v1/printing-orders/{self.order_invisible.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_retrieve_other_merchant_order_404(self):
|
||||
"""测试无法获取其他商户的订单"""
|
||||
response = self.client.get(f'/api/v1/printing-orders/{self.order_other_merchant.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
def test_customer_in_visible_employees_can_see(self):
|
||||
"""测试被加入 visible_employees 后可以看到订单"""
|
||||
# 将 employee 加入 customer_by_emp2 的可见员工列表
|
||||
self.customer_by_emp2.visible_employees.add(self.employee)
|
||||
|
||||
response = self.client.get('/api/v1/printing-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 现在应该能看到 2 个订单(自己创建的 + 被加入可见列表的)
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
|
||||
fabrics = [r['fabric'] for r in response.data['results']]
|
||||
self.assertIn('不可见订单', fabrics)
|
||||
|
||||
def test_view_all_permission_bypasses_filter(self):
|
||||
"""测试 view_all 权限可以突破客户可见性限制"""
|
||||
# 添加 view_all 权限
|
||||
view_all_perm = Permission.objects.get(codename='view_all_printingorders')
|
||||
self.user.user_permissions.add(view_all_perm)
|
||||
|
||||
response = self.client.get('/api/v1/printing-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 应该能看到本商户的所有订单(2个),但不能看到其他商户的
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
|
||||
fabrics = [r['fabric'] for r in response.data['results']]
|
||||
self.assertIn('不可见订单', fabrics)
|
||||
self.assertNotIn('其他商户订单', fabrics)
|
||||
|
||||
def test_superuser_sees_all(self):
|
||||
"""测试超级用户可以看到所有订单"""
|
||||
self.user.is_superuser = True
|
||||
self.user.save()
|
||||
|
||||
response = self.client.get('/api/v1/printing-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
# 超级用户可以看到所有订单(包括其他商户)
|
||||
self.assertEqual(response.data['count'], 3)
|
||||
|
||||
@@ -28,6 +28,7 @@ from .serializers import (
|
||||
PrintingJobDetailSerializer,
|
||||
PrintingJobCreateUpdateSerializer,
|
||||
)
|
||||
from .mixins import CustomerVisibilityFilterMixin
|
||||
|
||||
|
||||
class IsPrintingFactory(BasePermission):
|
||||
@@ -96,7 +97,7 @@ class PrintingOrderFilterSet(django_filters.FilterSet):
|
||||
|
||||
|
||||
@method_decorator(cache_page(20), name='list')
|
||||
class PrintingOrderViewSet(LimitedModelViewSet):
|
||||
class PrintingOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
||||
"""
|
||||
印染订单 ViewSet
|
||||
|
||||
@@ -129,8 +130,14 @@ class PrintingOrderViewSet(LimitedModelViewSet):
|
||||
格式: [{"state_name": "进度一", "state_id": 1, "count": 3}, ...]
|
||||
- jobs_last_status_summary: 订单下所有印染任务按最后完成状态分组的数量汇总
|
||||
格式: [{"state_name": "进度一", "count": 3}, ...]
|
||||
|
||||
权限控制:
|
||||
- 默认按客户可见性过滤(员工只能看到自己负责的客户的订单)
|
||||
- 拥有 printing.view_all_printingorders 权限可突破此限制
|
||||
"""
|
||||
queryset = models.PrintingOrder.objects.all()
|
||||
customer_field = 'customer'
|
||||
view_all_permission = 'printing.view_all_printingorders'
|
||||
permission_classes = [DjangoModelPermissions]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = PrintingOrderFilterSet
|
||||
@@ -286,7 +293,7 @@ class PrintingJobFilterSet(django_filters.FilterSet):
|
||||
fields = ['printing_order', 'product']
|
||||
|
||||
|
||||
class PrintingJobViewSet(LimitedModelViewSet):
|
||||
class PrintingJobViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
||||
"""
|
||||
印染款式明细 ViewSet
|
||||
|
||||
@@ -318,8 +325,14 @@ class PrintingJobViewSet(LimitedModelViewSet):
|
||||
- pieces_max: 最大件数
|
||||
- search: 全文搜索(产品名称、单位、尺寸、备注)
|
||||
- ordering: 排序字段
|
||||
|
||||
权限控制:
|
||||
- 默认按客户可见性过滤(通过 printing_order.customer)
|
||||
- 拥有 printing.view_all_printingorders 权限可突破此限制
|
||||
"""
|
||||
queryset = models.PrintingJob.objects.all()
|
||||
customer_field = 'printing_order__customer'
|
||||
view_all_permission = 'printing.view_all_printingorders'
|
||||
permission_classes = [DjangoModelPermissions, IsPrintingFactory]
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
filterset_class = PrintingJobFilterSet
|
||||
@@ -630,7 +643,7 @@ def _plate_order_last_modified(request, *args, **kwargs):
|
||||
|
||||
|
||||
# @method_decorator(condition(last_modified_func=_plate_order_last_modified), name='list')
|
||||
class PlateOrderViewSet(LimitedModelViewSet):
|
||||
class PlateOrderViewSet(CustomerVisibilityFilterMixin, LimitedModelViewSet):
|
||||
"""
|
||||
开版订单 ViewSet
|
||||
|
||||
@@ -677,8 +690,14 @@ class PlateOrderViewSet(LimitedModelViewSet):
|
||||
- created_date_to: 创建日期结束
|
||||
- search: 全文搜索(设计编号、款式名称、客户名称、面料)
|
||||
- ordering: 排序字段
|
||||
|
||||
权限控制:
|
||||
- 默认按客户可见性过滤(员工只能看到自己负责的客户的订单)
|
||||
- 拥有 printing.view_all_plateorders 权限可突破此限制
|
||||
"""
|
||||
queryset = models.PlateOrder.objects.all()
|
||||
customer_field = 'customer'
|
||||
view_all_permission = 'printing.view_all_plateorders'
|
||||
permission_classes = [DjangoModelPermissions]
|
||||
parser_classes = [MultiPartParser, FormParser, JSONParser] # 支持文件上传
|
||||
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
|
||||
@@ -3,6 +3,6 @@ Shipment API 模块
|
||||
|
||||
提供出货单和销售品相关的 API 接口
|
||||
"""
|
||||
from .views import SalesItemByPrintingOrderView
|
||||
from .views import SalesItemByPrintingOrderView, ShipmentCreateView
|
||||
|
||||
__all__ = ['SalesItemByPrintingOrderView']
|
||||
__all__ = ['SalesItemByPrintingOrderView', 'ShipmentCreateView']
|
||||
|
||||
@@ -3,6 +3,57 @@ Shipment API 序列化器
|
||||
"""
|
||||
from rest_framework import serializers
|
||||
|
||||
from shipment.models import Shipment
|
||||
|
||||
|
||||
class ShipmentSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
出货单序列化器(只读,用于返回数据)
|
||||
"""
|
||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||
created_by_id = serializers.IntegerField(source='created_by.id', read_only=True, allow_null=True)
|
||||
created_by_name = serializers.SerializerMethodField()
|
||||
items_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Shipment
|
||||
fields = [
|
||||
'id', 'customer', 'customer_name', 'shipment_date', 'remark',
|
||||
'items_count', 'created_by_id', 'created_by_name',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||
|
||||
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
|
||||
|
||||
def get_items_count(self, obj):
|
||||
return obj.items.count()
|
||||
|
||||
|
||||
class ShipmentCreateSerializer(serializers.Serializer):
|
||||
"""
|
||||
出货单创建序列化器
|
||||
"""
|
||||
customer = serializers.IntegerField(help_text='客户ID')
|
||||
shipment_date = serializers.DateField(help_text='出货日期')
|
||||
remark = serializers.CharField(required=False, default='', allow_blank=True, help_text='备注')
|
||||
sales_items = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
required=False,
|
||||
default=list,
|
||||
help_text='要关联的销售品ID列表'
|
||||
)
|
||||
|
||||
def validate_sales_items(self, value):
|
||||
# 去重
|
||||
return list(set(value)) if value else []
|
||||
|
||||
|
||||
class SalesItemSerializer(serializers.Serializer):
|
||||
"""
|
||||
|
||||
@@ -264,3 +264,165 @@ class SalesItemByPrintingOrderAPITestCase(TestCase):
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
class ShipmentCreateAPITestCase(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.sales_item1 = shipment_models.SalesItem.objects.create(
|
||||
name='销售品1',
|
||||
quantity=Decimal('50.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
self.sales_item2 = shipment_models.SalesItem.objects.create(
|
||||
name='销售品2',
|
||||
quantity=Decimal('30.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 创建已关联出货单的销售品
|
||||
self.existing_shipment = shipment_models.Shipment.objects.create(
|
||||
customer=self.customer,
|
||||
shipment_date='2026-01-13',
|
||||
created_by=self.user,
|
||||
)
|
||||
self.sales_item_shipped = shipment_models.SalesItem.objects.create(
|
||||
name='销售品3(已出货)',
|
||||
quantity=Decimal('100.00'),
|
||||
unit=shipment_models.UnitChoices.METER,
|
||||
shipment=self.existing_shipment,
|
||||
created_by=self.user,
|
||||
)
|
||||
|
||||
# 认证用户
|
||||
self.client.force_authenticate(user=self.user)
|
||||
|
||||
def test_create_shipment_success(self):
|
||||
"""测试成功创建出货单"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'remark': '测试备注',
|
||||
'sales_items': [self.sales_item1.id, self.sales_item2.id]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
result = response.json()
|
||||
|
||||
# 验证返回数据
|
||||
self.assertIn('id', result)
|
||||
self.assertEqual(result['customer'], self.customer.id)
|
||||
self.assertEqual(result['customer_name'], self.customer.name)
|
||||
self.assertEqual(result['shipment_date'], '2026-01-14')
|
||||
self.assertEqual(result['remark'], '测试备注')
|
||||
self.assertEqual(result['items_count'], 2)
|
||||
self.assertEqual(result['created_by_id'], self.user.id)
|
||||
|
||||
# 验证销售品已关联到出货单
|
||||
self.sales_item1.refresh_from_db()
|
||||
self.sales_item2.refresh_from_db()
|
||||
self.assertEqual(self.sales_item1.shipment_id, result['id'])
|
||||
self.assertEqual(self.sales_item2.shipment_id, result['id'])
|
||||
|
||||
def test_create_shipment_without_sales_items(self):
|
||||
"""测试创建出货单但不关联销售品"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
result = response.json()
|
||||
self.assertEqual(result['items_count'], 0)
|
||||
|
||||
def test_create_shipment_customer_not_found(self):
|
||||
"""测试客户不存在"""
|
||||
data = {
|
||||
'customer': 99999,
|
||||
'shipment_date': '2026-01-14',
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('不存在', response.json()['detail'])
|
||||
|
||||
def test_create_shipment_sales_item_not_found(self):
|
||||
"""测试销售品不存在"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'sales_items': [99999]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('不存在', response.json()['detail'])
|
||||
|
||||
def test_create_shipment_sales_item_already_shipped(self):
|
||||
"""测试销售品已关联到其他出货单"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
'sales_items': [self.sales_item_shipped.id]
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('已关联', response.json()['detail'])
|
||||
|
||||
def test_create_shipment_unauthenticated(self):
|
||||
"""测试未认证用户"""
|
||||
self.client.logout()
|
||||
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'shipment_date': '2026-01-14',
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/shipment/shipments/', data, format='json')
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
@@ -6,7 +6,63 @@ from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .serializers import SalesItemSerializer
|
||||
from .serializers import SalesItemSerializer, ShipmentSerializer, ShipmentCreateSerializer
|
||||
|
||||
|
||||
class ShipmentCreateView(APIView):
|
||||
"""
|
||||
创建出货单
|
||||
|
||||
POST /api/v1/shipment/shipments/
|
||||
|
||||
请求体:
|
||||
{
|
||||
"customer": 1,
|
||||
"shipment_date": "2026-01-14",
|
||||
"remark": "备注信息(可选)",
|
||||
"sales_items": [1, 2, 3]
|
||||
}
|
||||
|
||||
返回:
|
||||
{
|
||||
"id": 1,
|
||||
"customer": 1,
|
||||
"customer_name": "客户A",
|
||||
"shipment_date": "2026-01-14",
|
||||
"remark": "备注信息",
|
||||
"items_count": 3,
|
||||
"created_by_id": 1,
|
||||
"created_by_name": "张三",
|
||||
"created_at": "2026-01-14T10:00:00Z",
|
||||
"updated_at": "2026-01-14T10:00:00Z"
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
# 验证请求数据
|
||||
serializer = ShipmentCreateSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
data = serializer.validated_data
|
||||
|
||||
# 调用业务逻辑
|
||||
from shipment.services import create_shipment
|
||||
try:
|
||||
shipment = create_shipment(
|
||||
customer_id=data['customer'],
|
||||
shipment_date=data['shipment_date'],
|
||||
sales_item_ids=data['sales_items'],
|
||||
created_by=request.user,
|
||||
remark=data.get('remark', ''),
|
||||
)
|
||||
except ValueError as e:
|
||||
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 返回创建的出货单
|
||||
response_serializer = ShipmentSerializer(shipment)
|
||||
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class SalesItemByPrintingOrderView(APIView):
|
||||
|
||||
@@ -67,6 +67,71 @@ python manage.py backfill_merchant <merchant_id>
|
||||
|
||||
---
|
||||
|
||||
### 5. 创建出货单 API
|
||||
|
||||
实现了创建出货单的 API,支持同时关联多个销售品。
|
||||
|
||||
- **接口路径**: `POST /api/v1/shipment/shipments/`
|
||||
- **请求参数**: `customer`(客户ID)、`shipment_date`(出货日期)、`remark`(备注)、`sales_items`(销售品ID列表)
|
||||
|
||||
#### 业务逻辑
|
||||
1. 验证客户存在
|
||||
2. 验证销售品存在且未被关联到其他出货单
|
||||
3. 创建出货单并将销售品关联到该出货单(事务保护)
|
||||
|
||||
#### 修改文件
|
||||
- `shipment/services.py`: 添加 `create_shipment()` 函数
|
||||
- `api_v1/views/shipment/views.py`: 添加 `ShipmentCreateView`
|
||||
- `api_v1/views/shipment/serializers.py`: 添加 `ShipmentSerializer`、`ShipmentCreateSerializer`
|
||||
- `api_v1/views/shipment/test_api.py`: 添加 6 个测试用例
|
||||
- `api_v1/urls.py`: 注册新路由
|
||||
- `docs/shipment_api.md`: 更新 API 文档
|
||||
|
||||
---
|
||||
|
||||
### 6. 添加销售品自动创建开关
|
||||
|
||||
在 `settings.py` 中添加 `AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER` 配置项:
|
||||
|
||||
- **默认值**: `True`(保持现有行为)
|
||||
- **环境变量**: `AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER`
|
||||
- **作用**: 当设为 `False` 时,`printing/handlers.py` 中的流程完成信号处理器将不再自动创建销售品
|
||||
|
||||
#### 修改文件
|
||||
- `flower/settings.py`: 添加配置项
|
||||
- `printing/handlers.py`: 检查开关状态,为 False 时提前返回
|
||||
|
||||
---
|
||||
|
||||
### 7. Printing 模块客户可见性过滤
|
||||
|
||||
实现了基于客户可见性的订单查询过滤功能,确保员工只能看到自己负责的客户的订单。
|
||||
|
||||
#### 新增权限
|
||||
- `printing.view_all_plateorders`: 查看所有开版订单(突破客户可见性限制)
|
||||
- `printing.view_all_printingorders`: 查看所有印染订单(突破客户可见性限制)
|
||||
|
||||
#### 可见性规则
|
||||
- 超级用户:无限制
|
||||
- 有 `view_all_xxx` 权限:可见本商户所有订单
|
||||
- 普通员工:
|
||||
- 可见自己创建的客户的订单
|
||||
- 可见被加入 `visible_employees` 的客户的订单
|
||||
- 所有用户:受 merchant 隔离限制
|
||||
|
||||
#### 新建文件
|
||||
- `api_v1/views/printing/mixins.py`: `CustomerVisibilityFilterMixin`
|
||||
|
||||
#### 修改文件
|
||||
- `printing/models.py`: 添加新权限定义
|
||||
- `api_v1/views/printing/views.py`: 三个 ViewSet 继承 Mixin
|
||||
- `api_v1/views/printing/test_api.py`: 修复测试数据 + 新增 7 个权限过滤测试
|
||||
|
||||
#### Migration
|
||||
- `printing/migrations/0030_add_view_all_permissions.py`
|
||||
|
||||
---
|
||||
|
||||
## 待办
|
||||
|
||||
---
|
||||
@@ -75,4 +140,4 @@ python manage.py backfill_merchant <merchant_id>
|
||||
|
||||
- API 独立于 printing 模块,避免影响现有功能
|
||||
- 完整的测试覆盖:正常查询、包含已出货、数据格式、404 错误、空结果、未认证
|
||||
- printing 和 shipment 模块全部测试通过(31个测试用例)
|
||||
- printing 和 shipment 模块全部测试通过(共 13 个测试用例)
|
||||
@@ -4,10 +4,101 @@
|
||||
|
||||
## 目录
|
||||
|
||||
- [创建出货单](#创建出货单)
|
||||
- [通过生产订单查询销售品](#通过生产订单查询销售品)
|
||||
|
||||
---
|
||||
|
||||
## 创建出货单
|
||||
|
||||
创建出货单并关联销售品。
|
||||
|
||||
### 接口信息
|
||||
|
||||
- **URL**: `/api/v1/shipment/shipments/`
|
||||
- **Method**: `POST`
|
||||
- **认证**: 需要登录(JWT Token)
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| customer | int | 是 | 客户ID |
|
||||
| shipment_date | string | 是 | 出货日期(YYYY-MM-DD) |
|
||||
| remark | string | 否 | 备注 |
|
||||
| sales_items | array[int] | 否 | 要关联的销售品ID列表 |
|
||||
|
||||
### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"customer": 1,
|
||||
"shipment_date": "2026-01-14",
|
||||
"remark": "备注信息",
|
||||
"sales_items": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"customer": 1,
|
||||
"customer_name": "客户A",
|
||||
"shipment_date": "2026-01-14",
|
||||
"remark": "备注信息",
|
||||
"items_count": 3,
|
||||
"created_by_id": 1,
|
||||
"created_by_name": "张三",
|
||||
"created_at": "2026-01-14T10:00:00Z",
|
||||
"updated_at": "2026-01-14T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | int | 出货单ID |
|
||||
| customer | int | 客户ID |
|
||||
| customer_name | string | 客户名称 |
|
||||
| shipment_date | string | 出货日期 |
|
||||
| remark | string | 备注 |
|
||||
| items_count | int | 关联的销售品数量 |
|
||||
| created_by_id | int | 创建人ID |
|
||||
| created_by_name | string | 创建人名称 |
|
||||
| created_at | string | 创建时间 |
|
||||
| updated_at | string | 更新时间 |
|
||||
|
||||
### 错误响应
|
||||
|
||||
#### 400 Bad Request - 客户不存在
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "客户 999 不存在"
|
||||
}
|
||||
```
|
||||
|
||||
#### 400 Bad Request - 销售品不存在
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "以下销售品不存在: [999]"
|
||||
}
|
||||
```
|
||||
|
||||
#### 400 Bad Request - 销售品已关联其他出货单
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "以下销售品已关联到其他出货单: [1, 2]"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 通过生产订单查询销售品
|
||||
|
||||
查询与指定生产订单(PrintingOrder)关联的所有销售品(SalesItem)。
|
||||
|
||||
@@ -408,6 +408,7 @@ PLATE_ORDER_DEFAULT_PROCESS_ID = 2 # 默认开版流程ID
|
||||
# 指定从哪个流程节点获取数量参数,单位固定为"米"
|
||||
# 如果该节点未完成或参数不存在,则不创建销售品
|
||||
# ------------------------------------------------------------------------------
|
||||
AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER = env.bool('AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER', default=False)
|
||||
PRINTING_SALES_ITEM_SOURCE_STATE_ID = env.int('PRINTING_SALES_ITEM_SOURCE_STATE_ID') # 必须在 .env 中配置
|
||||
PRINTING_SALES_ITEM_QUANTITY_KEY = '米数' # 数量参数的 key
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ class PrintingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'printing'
|
||||
verbose_name = '印染管理'
|
||||
|
||||
|
||||
def ready(self):
|
||||
"""注册信号处理函数"""
|
||||
from stateflow.signals import process_completed
|
||||
|
||||
@@ -20,6 +20,7 @@ def on_printing_job_process_completed(sender, **kwargs):
|
||||
数量和单位从指定的流程节点参数中获取(通过 settings 配置)。
|
||||
|
||||
配置项:
|
||||
AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER: 是否启用自动创建销售品(默认 True)
|
||||
PRINTING_SALES_ITEM_SOURCE_STATE_ID: 指定从哪个 State 获取参数
|
||||
PRINTING_SALES_ITEM_QUANTITY_KEY: 数量参数的 key
|
||||
PRINTING_SALES_ITEM_UNIT_KEY: 单位参数的 key
|
||||
@@ -31,6 +32,11 @@ def on_printing_job_process_completed(sender, **kwargs):
|
||||
last_completed_by: 最后步骤操作人
|
||||
其他参数见 stateflow.signals.process_completed
|
||||
"""
|
||||
# 检查是否启用自动创建销售品
|
||||
if not getattr(settings, 'AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER', True):
|
||||
logger.debug('[printing.handlers] AUTO_CREATE_SALESITEM_FROM_PRINT_ORDER=False,跳过销售品创建')
|
||||
return
|
||||
|
||||
from shipment.models import SalesItem, UnitChoices
|
||||
|
||||
logger.info(
|
||||
|
||||
21
printing/migrations/0030_add_view_all_permissions.py
Normal file
21
printing/migrations/0030_add_view_all_permissions.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.2.8 on 2026-01-14 08:46
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('printing', '0029_add_merchant_to_models'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='plateorder',
|
||||
options={'ordering': ['-created_at'], 'permissions': [('can_invalidate_plateorder', '可以作废开版订单'), ('can_activate_plateorder', '可以恢复开版订单'), ('view_all_plateorders', '查看所有开版订单(突破客户可见性限制)')], 'verbose_name': '开版订单', 'verbose_name_plural': '开版订单'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='printingorder',
|
||||
options={'permissions': [('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单'), ('view_all_printingorders', '查看所有印染订单(突破客户可见性限制)')], 'verbose_name': '印染订单', 'verbose_name_plural': '印染订单'},
|
||||
),
|
||||
]
|
||||
@@ -185,6 +185,7 @@ class PlateOrder(ModelBase):
|
||||
permissions = [
|
||||
('can_invalidate_plateorder', '可以作废开版订单'),
|
||||
('can_activate_plateorder', '可以恢复开版订单'),
|
||||
('view_all_plateorders', '查看所有开版订单(突破客户可见性限制)'),
|
||||
]
|
||||
|
||||
@property
|
||||
@@ -318,6 +319,7 @@ class PrintingOrder(ModelBase):
|
||||
permissions = [
|
||||
('can_invalidate_printingorder', '可以作废印染订单'),
|
||||
('can_activate_printingorder', '可以恢复印染订单'),
|
||||
('view_all_printingorders', '查看所有印染订单(突破客户可见性限制)'),
|
||||
]
|
||||
|
||||
@property
|
||||
|
||||
@@ -3,9 +3,12 @@ Shipment 模块业务逻辑层
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from shipment.models import SalesItem
|
||||
from shipment.models import SalesItem, Shipment
|
||||
|
||||
|
||||
def get_sales_items_by_printing_order(
|
||||
@@ -37,3 +40,66 @@ def get_sales_items_by_printing_order(
|
||||
queryset = queryset.filter(shipment__isnull=True)
|
||||
|
||||
return queryset.select_related('shipment').order_by('id')
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_shipment(
|
||||
customer_id: int,
|
||||
shipment_date,
|
||||
sales_item_ids: List[int],
|
||||
created_by,
|
||||
remark: str = '',
|
||||
) -> Shipment:
|
||||
"""
|
||||
创建出货单并关联销售品
|
||||
|
||||
Args:
|
||||
customer_id: 客户ID
|
||||
shipment_date: 出货日期
|
||||
sales_item_ids: 要关联的销售品ID列表
|
||||
created_by: 创建人
|
||||
remark: 备注
|
||||
|
||||
Returns:
|
||||
创建的 Shipment 实例
|
||||
|
||||
Raises:
|
||||
ValueError: 如果销售品不存在或已被关联到其他出货单
|
||||
"""
|
||||
from basic_info.models import Customer
|
||||
|
||||
# 验证客户存在
|
||||
try:
|
||||
customer = Customer.objects.get(id=customer_id)
|
||||
except Customer.DoesNotExist:
|
||||
raise ValueError(f'客户 {customer_id} 不存在')
|
||||
|
||||
# 验证销售品
|
||||
if sales_item_ids:
|
||||
# 查询销售品
|
||||
sales_items = SalesItem.objects.filter(id__in=sales_item_ids)
|
||||
found_ids = set(sales_items.values_list('id', flat=True))
|
||||
missing_ids = set(sales_item_ids) - found_ids
|
||||
|
||||
if missing_ids:
|
||||
raise ValueError(f'以下销售品不存在: {list(missing_ids)}')
|
||||
|
||||
# 检查是否有已关联出货单的销售品
|
||||
already_shipped = sales_items.filter(shipment__isnull=False)
|
||||
if already_shipped.exists():
|
||||
shipped_ids = list(already_shipped.values_list('id', flat=True))
|
||||
raise ValueError(f'以下销售品已关联到其他出货单: {shipped_ids}')
|
||||
|
||||
# 创建出货单
|
||||
shipment = Shipment.objects.create(
|
||||
customer=customer,
|
||||
shipment_date=shipment_date,
|
||||
remark=remark,
|
||||
created_by=created_by,
|
||||
)
|
||||
|
||||
# 关联销售品
|
||||
if sales_item_ids:
|
||||
SalesItem.objects.filter(id__in=sales_item_ids).update(shipment=shipment)
|
||||
|
||||
return shipment
|
||||
|
||||
Reference in New Issue
Block a user