forked from erp-dev/erp
feat: added printing module
This commit is contained in:
@@ -40,7 +40,7 @@ class BaseSerializer(serializers.ModelSerializer):
|
|||||||
class QuickInputSerializer(serializers.ModelSerializer):
|
class QuickInputSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = basic_models.QuickInput
|
model = basic_models.QuickInput
|
||||||
fields = ['name', 'value', 'group']
|
fields = ['id', 'name', 'value', 'group']
|
||||||
|
|
||||||
|
|
||||||
class ProductCategorySerializer(BaseSerializer):
|
class ProductCategorySerializer(BaseSerializer):
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import stock_change_views, user_info, inventory, product_image, stateflow
|
from .views import stock_change_views, user_info, inventory, product_image, stateflow
|
||||||
|
from .views.stock_change_views.snapshot import StockSnapshotListView
|
||||||
|
from printing.views import PrintingOrderViewSet
|
||||||
|
|
||||||
# 创建 DRF Router
|
# 创建 DRF Router for Stateflow
|
||||||
router = DefaultRouter()
|
stateflow_router = DefaultRouter()
|
||||||
router.register(r'states', stateflow.StateViewSet, basename='state')
|
stateflow_router.register(r'states', stateflow.StateViewSet, basename='state')
|
||||||
router.register(r'processes', stateflow.ProcessViewSet, basename='process')
|
stateflow_router.register(r'processes', stateflow.ProcessViewSet, basename='process')
|
||||||
router.register(r'business-objects', stateflow.BusinessObjectViewSet, basename='business-object')
|
|
||||||
|
# 创建主 Router
|
||||||
|
main_router = DefaultRouter()
|
||||||
|
main_router.register(r'printing-orders', PrintingOrderViewSet, basename='printing-order')
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# 库存变动相关API
|
# 库存变动相关API
|
||||||
|
path('stock-snapshots/', StockSnapshotListView.as_view(), name='list_stock_snapshots'),
|
||||||
path('stock-changes/', stock_change_views.list_stock_changes, name='list_stock_changes'),
|
path('stock-changes/', stock_change_views.list_stock_changes, name='list_stock_changes'),
|
||||||
path('stock-change/', stock_change_views.create_full_stock_change, name='create_full_stock_change'),
|
path('stock-change/', stock_change_views.create_full_stock_change, name='create_full_stock_change'),
|
||||||
path('stock-change/<int:record_id>/', stock_change_views.get_stock_change, name='get_stock_change'),
|
path('stock-change/<int:record_id>/', stock_change_views.get_stock_change, name='get_stock_change'),
|
||||||
@@ -29,5 +35,8 @@ urlpatterns = [
|
|||||||
path('products/<int:product_id>/image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'),
|
path('products/<int:product_id>/image/', product_image.ProductImageUploadView.as_view(), name='product_image_upload'),
|
||||||
|
|
||||||
# Stateflow API (使用 Router)
|
# Stateflow API (使用 Router)
|
||||||
path('stateflow/', include(router.urls)),
|
path('stateflow/', include(stateflow_router.urls)),
|
||||||
|
|
||||||
|
# 主 Router (printing-orders 等)
|
||||||
|
path('', include(main_router.urls)),
|
||||||
]
|
]
|
||||||
|
|||||||
0
api_v1/views/printing/serializers.py
Normal file
0
api_v1/views/printing/serializers.py
Normal file
400
api_v1/views/printing/test_api.py
Normal file
400
api_v1/views/printing/test_api.py
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
"""
|
||||||
|
PrintingOrder API 测试
|
||||||
|
"""
|
||||||
|
from django.test import TestCase
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
from rest_framework import status
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from basic_info import models as basic_models
|
||||||
|
from printing import models as printing_models
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderAPITestCase(TestCase):
|
||||||
|
"""测试 PrintingOrder 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',
|
||||||
|
job_type=basic_models.EmployeeTypeEnum.PRINTER,
|
||||||
|
status=basic_models.EmployeeStatusEnum.ACTIVE
|
||||||
|
)
|
||||||
|
|
||||||
|
# 创建客户
|
||||||
|
self.customer = basic_models.Customer.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
name='测试客户',
|
||||||
|
mobile='13900139000',
|
||||||
|
area='测试地区'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 认证用户
|
||||||
|
self.client.force_authenticate(user=self.user)
|
||||||
|
|
||||||
|
# 给用户添加基础权限
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_create_printing_order(self):
|
||||||
|
"""测试创建印染订单"""
|
||||||
|
data = {
|
||||||
|
'customer': self.customer.id,
|
||||||
|
'fabric': '纯棉布料',
|
||||||
|
'width': '150cm',
|
||||||
|
'is_urgent': True,
|
||||||
|
'area': '广州',
|
||||||
|
'address': '白云区xxx',
|
||||||
|
'fabric_source': '客户提供',
|
||||||
|
'is_fabric_received': False,
|
||||||
|
'craft': '活性印花',
|
||||||
|
'description': '测试订单描述',
|
||||||
|
'outgoing_date': '2025-11-20',
|
||||||
|
'printing_warn': '注意颜色',
|
||||||
|
'rolling_warn': '注意温度',
|
||||||
|
'production_warn': '质量检查'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post('/api/v1/printing-orders/', data, format='json')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# 检查响应数据
|
||||||
|
self.assertIn('fabric', response.data)
|
||||||
|
self.assertEqual(response.data['fabric'], '纯棉布料')
|
||||||
|
self.assertEqual(response.data['is_urgent'], True)
|
||||||
|
|
||||||
|
# 验证订单已创建 - 通过fabric查找,因为响应可能不包含id
|
||||||
|
order = printing_models.PrintingOrder.objects.filter(fabric='纯棉布料').first()
|
||||||
|
self.assertIsNotNone(order)
|
||||||
|
self.assertEqual(order.customer.id, self.customer.id)
|
||||||
|
self.assertEqual(order.fabric, '纯棉布料')
|
||||||
|
|
||||||
|
def test_list_printing_orders(self):
|
||||||
|
"""测试获取订单列表"""
|
||||||
|
# 创建测试订单
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料1',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料2',
|
||||||
|
width='160cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/v1/printing-orders/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
# 不使用分页参数时,返回列表
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
def test_retrieve_printing_order(self):
|
||||||
|
"""测试获取订单详情"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
craft='活性印花',
|
||||||
|
description='详情测试'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/v1/printing-orders/{order.id}/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data['fabric'], '测试布料')
|
||||||
|
self.assertEqual(response.data['craft'], '活性印花')
|
||||||
|
self.assertIn('customer_name', response.data)
|
||||||
|
self.assertEqual(response.data['customer_name'], self.customer.name)
|
||||||
|
|
||||||
|
def test_update_printing_order(self):
|
||||||
|
"""测试更新订单"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='旧布料',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
update_data = {
|
||||||
|
'customer': self.customer.id,
|
||||||
|
'fabric': '新布料',
|
||||||
|
'width': '160cm',
|
||||||
|
'is_urgent': True,
|
||||||
|
'craft': '更新的工艺'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.put(
|
||||||
|
f'/api/v1/printing-orders/{order.id}/',
|
||||||
|
update_data,
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
self.assertEqual(order.fabric, '新布料')
|
||||||
|
self.assertEqual(order.width, '160cm')
|
||||||
|
self.assertEqual(order.is_urgent, True)
|
||||||
|
|
||||||
|
def test_partial_update_printing_order(self):
|
||||||
|
"""测试部分更新订单"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='原布料',
|
||||||
|
width='150cm',
|
||||||
|
is_urgent=False
|
||||||
|
)
|
||||||
|
|
||||||
|
patch_data = {
|
||||||
|
'is_urgent': True,
|
||||||
|
'craft': '新工艺'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.patch(
|
||||||
|
f'/api/v1/printing-orders/{order.id}/',
|
||||||
|
patch_data,
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
self.assertEqual(order.is_urgent, True)
|
||||||
|
self.assertEqual(order.craft, '新工艺')
|
||||||
|
self.assertEqual(order.fabric, '原布料') # 未修改字段保持不变
|
||||||
|
|
||||||
|
def test_delete_printing_order_forbidden(self):
|
||||||
|
"""测试删除订单被禁用"""
|
||||||
|
# 添加删除权限以便测试destroy方法的自定义逻辑
|
||||||
|
delete_perm = Permission.objects.get(codename='delete_printingorder')
|
||||||
|
self.user.user_permissions.add(delete_perm)
|
||||||
|
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.delete(f'/api/v1/printing-orders/{order.id}/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
||||||
|
self.assertIn('不支持删除', response.data['detail'])
|
||||||
|
|
||||||
|
# 验证订单仍然存在
|
||||||
|
self.assertTrue(
|
||||||
|
printing_models.PrintingOrder.objects.filter(id=order.id).exists()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_invalidate_printing_order(self):
|
||||||
|
"""测试作废订单"""
|
||||||
|
# 添加作废权限
|
||||||
|
invalidate_perm = Permission.objects.get(codename='can_invalidate_printingorder')
|
||||||
|
self.user.user_permissions.add(invalidate_perm)
|
||||||
|
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
is_invalid=False
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/invalidate/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('已作废', response.data['detail'])
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
self.assertTrue(order.is_invalid)
|
||||||
|
|
||||||
|
def test_invalidate_without_permission(self):
|
||||||
|
"""测试没有权限时作废订单失败"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/invalidate/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertIn('没有权限', response.data['detail'])
|
||||||
|
|
||||||
|
def test_activate_printing_order(self):
|
||||||
|
"""测试恢复订单"""
|
||||||
|
# 添加恢复权限
|
||||||
|
activate_perm = Permission.objects.get(codename='can_activate_printingorder')
|
||||||
|
self.user.user_permissions.add(activate_perm)
|
||||||
|
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
is_invalid=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/activate/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('已恢复', response.data['detail'])
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
self.assertFalse(order.is_invalid)
|
||||||
|
|
||||||
|
def test_activate_without_permission(self):
|
||||||
|
"""测试没有权限时恢复订单失败"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
is_invalid=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/activate/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
self.assertIn('没有权限', response.data['detail'])
|
||||||
|
|
||||||
|
def test_mark_fabric_received(self):
|
||||||
|
"""测试标记布料已收"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
is_fabric_received=False
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/mark_fabric_received/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertIn('已标记布料已收', response.data['detail'])
|
||||||
|
|
||||||
|
order.refresh_from_db()
|
||||||
|
self.assertTrue(order.is_fabric_received)
|
||||||
|
|
||||||
|
def test_mark_fabric_received_already_received(self):
|
||||||
|
"""测试重复标记布料已收"""
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm',
|
||||||
|
is_fabric_received=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.post(f'/api/v1/printing-orders/{order.id}/mark_fabric_received/')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn('已经标记', response.data['detail'])
|
||||||
|
|
||||||
|
def test_filter_by_customer(self):
|
||||||
|
"""测试按客户过滤"""
|
||||||
|
customer2 = basic_models.Customer.objects.create(
|
||||||
|
merchant=self.merchant,
|
||||||
|
name='客户2',
|
||||||
|
mobile='13900139001'
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料1',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=customer2,
|
||||||
|
fabric='布料2',
|
||||||
|
width='160cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(f'/api/v1/printing-orders/?customer={self.customer.id}')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
|
||||||
|
def test_filter_by_urgent(self):
|
||||||
|
"""测试按紧急状态过滤"""
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料1',
|
||||||
|
width='150cm',
|
||||||
|
is_urgent=True
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料2',
|
||||||
|
width='160cm',
|
||||||
|
is_urgent=False
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/v1/printing-orders/?is_urgent=true')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertTrue(response.data[0]['is_urgent'])
|
||||||
|
|
||||||
|
def test_search_by_fabric(self):
|
||||||
|
"""测试按布料搜索"""
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='纯棉布料',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='涤纶布料',
|
||||||
|
width='160cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get('/api/v1/printing-orders/?search=纯棉')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(len(response.data), 1)
|
||||||
|
self.assertIn('纯棉', response.data[0]['fabric'])
|
||||||
|
|
||||||
|
def test_ordering(self):
|
||||||
|
"""测试排序"""
|
||||||
|
order1 = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料1',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
order2 = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='布料2',
|
||||||
|
width='160cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 按 ID 升序
|
||||||
|
response = self.client.get('/api/v1/printing-orders/?ordering=id')
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
results = response.data
|
||||||
|
self.assertEqual(results[0]['id'], order1.id)
|
||||||
|
self.assertEqual(results[1]['id'], order2.id)
|
||||||
|
|
||||||
|
# 按 ID 降序
|
||||||
|
response = self.client.get('/api/v1/printing-orders/?ordering=-id')
|
||||||
|
results = response.data
|
||||||
|
self.assertEqual(results[0]['id'], order2.id)
|
||||||
|
self.assertEqual(results[1]['id'], order1.id)
|
||||||
|
|
||||||
|
def test_human_id_generation(self):
|
||||||
|
"""测试 human_id 自动生成"""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
order = printing_models.PrintingOrder.objects.create(
|
||||||
|
customer=self.customer,
|
||||||
|
fabric='测试布料',
|
||||||
|
width='150cm'
|
||||||
|
)
|
||||||
|
|
||||||
|
# human_id 格式: YYYYMMDD000001
|
||||||
|
self.assertIsNotNone(order.human_id)
|
||||||
|
self.assertTrue(len(order.human_id) >= 14)
|
||||||
|
today = date.today().strftime('%Y%m%d')
|
||||||
|
self.assertTrue(order.human_id.startswith(today))
|
||||||
@@ -139,7 +139,7 @@ class BusinessObjectViewSet(viewsets.ModelViewSet):
|
|||||||
def reset(self, request, pk=None):
|
def reset(self, request, pk=None):
|
||||||
"""重置进度"""
|
"""重置进度"""
|
||||||
business_object = self.get_object()
|
business_object = self.get_object()
|
||||||
services.reset_order_progress(business_object)
|
services.reset_business_object_progress(business_object)
|
||||||
|
|
||||||
return Response({
|
return Response({
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
98
api_v1/views/stock_change_views/snapshot.py
Normal file
98
api_v1/views/stock_change_views/snapshot.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
from rest_framework.generics import ListAPIView
|
||||||
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
from rest_framework import filters, serializers
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django_filters import rest_framework as django_filters
|
||||||
|
from stock import models
|
||||||
|
|
||||||
|
|
||||||
|
class StockSnapshotSerializer(serializers.ModelSerializer):
|
||||||
|
"""库存快照序列化器"""
|
||||||
|
product_name = serializers.CharField(source='product.name', read_only=True)
|
||||||
|
product_code = serializers.CharField(source='product.code', read_only=True)
|
||||||
|
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
|
||||||
|
unit_display = serializers.CharField(source='get_unit_display', read_only=True)
|
||||||
|
stock_change_record_type = serializers.CharField(
|
||||||
|
source='stock_change_record.get_type_display',
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
stock_change_record_source = serializers.CharField(
|
||||||
|
source='stock_change_record.get_source_type_display',
|
||||||
|
read_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.StockSnapshot
|
||||||
|
fields = [
|
||||||
|
'id', 'product', 'product_name', 'product_code',
|
||||||
|
'warehouse', 'warehouse_name',
|
||||||
|
'delta', 'quantity_before', 'quantity_after',
|
||||||
|
'stock_change_record', 'stock_change_record_type', 'stock_change_record_source',
|
||||||
|
'unit', 'unit_display', 'num_of_rolls',
|
||||||
|
'offset_to', 'offset_at', 'cancelled', 'cancelled_at', 'offset_id',
|
||||||
|
'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StockSnapshotFilterSet(django_filters.FilterSet):
|
||||||
|
"""库存快照过滤器"""
|
||||||
|
product_name = django_filters.CharFilter(field_name='product__name', lookup_expr='icontains')
|
||||||
|
product_code = django_filters.CharFilter(field_name='product__code', lookup_expr='icontains')
|
||||||
|
warehouse_name = django_filters.CharFilter(field_name='warehouse__name', lookup_expr='icontains')
|
||||||
|
cancelled = django_filters.BooleanFilter()
|
||||||
|
has_offset = django_filters.BooleanFilter(method='filter_has_offset')
|
||||||
|
date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
|
||||||
|
date_to = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
|
||||||
|
|
||||||
|
def filter_has_offset(self, queryset, name, value):
|
||||||
|
"""过滤是否有冲抵记录"""
|
||||||
|
if value:
|
||||||
|
return queryset.exclude(offset_to__isnull=True)
|
||||||
|
else:
|
||||||
|
return queryset.filter(offset_to__isnull=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.StockSnapshot
|
||||||
|
fields = [
|
||||||
|
'product', 'warehouse', 'stock_change_record',
|
||||||
|
'unit', 'cancelled'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StockSnapshotListView(ListAPIView):
|
||||||
|
"""
|
||||||
|
库存快照列表视图(只读)
|
||||||
|
|
||||||
|
提供库存变动快照的查询功能
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
- product: 产品ID
|
||||||
|
- product_name: 产品名称(模糊查询)
|
||||||
|
- product_code: 产品编码(模糊查询)
|
||||||
|
- warehouse: 仓库ID
|
||||||
|
- warehouse_name: 仓库名称(模糊查询)
|
||||||
|
- stock_change_record: 关联的库存变动记录ID
|
||||||
|
- unit: 单位
|
||||||
|
- cancelled: 是否已取消(true/false)
|
||||||
|
- has_offset: 是否有冲抵记录(true/false)
|
||||||
|
- date_from: 开始日期(格式:YYYY-MM-DD)
|
||||||
|
- date_to: 结束日期(格式:YYYY-MM-DD)
|
||||||
|
- search: 全文搜索(产品名称、产品编码、仓库名称)
|
||||||
|
- ordering: 排序字段
|
||||||
|
"""
|
||||||
|
serializer_class = StockSnapshotSerializer
|
||||||
|
pagination_class = LimitOffsetPagination
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
|
filterset_class = StockSnapshotFilterSet
|
||||||
|
search_fields = ['product__name', 'product__code', 'warehouse__name']
|
||||||
|
ordering_fields = [
|
||||||
|
'id', 'delta', 'quantity_before', 'quantity_after',
|
||||||
|
'created_at', 'updated_at', 'offset_at', 'cancelled_at'
|
||||||
|
]
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""优化查询,预加载关联数据"""
|
||||||
|
return models.StockSnapshot.objects.select_related(
|
||||||
|
'product', 'warehouse', 'stock_change_record'
|
||||||
|
).all()
|
||||||
@@ -8,7 +8,7 @@ class AdminBase(admin.ModelAdmin):
|
|||||||
|
|
||||||
def get_readonly_fields(self, request, obj = ...):
|
def get_readonly_fields(self, request, obj = ...):
|
||||||
readonly_fields = list(super().get_readonly_fields(request, obj))
|
readonly_fields = list(super().get_readonly_fields(request, obj))
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser and isinstance(obj, models.Employee) is False:
|
||||||
readonly_fields.append('merchant')
|
readonly_fields.append('merchant')
|
||||||
return readonly_fields
|
return readonly_fields
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ INSTALLED_APPS = [
|
|||||||
'stock',
|
'stock',
|
||||||
'api_v1',
|
'api_v1',
|
||||||
'api_man',
|
'api_man',
|
||||||
|
'printing',
|
||||||
'stateflow',
|
'stateflow',
|
||||||
'sse',
|
'sse',
|
||||||
]
|
]
|
||||||
|
|||||||
0
printing/__init__.py
Normal file
0
printing/__init__.py
Normal file
26
printing/admin.py
Normal file
26
printing/admin.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingJobInline(admin.StackedInline):
|
||||||
|
model = models.PrintingJob
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.PrintingOrder)
|
||||||
|
class PrintingOrderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'oid',
|
||||||
|
'customer_name',
|
||||||
|
'created_at',
|
||||||
|
'outgoing_date',
|
||||||
|
)
|
||||||
|
inlines = [PrintingJobInline]
|
||||||
|
|
||||||
|
@admin.display(description='客户名称')
|
||||||
|
def customer_name(self, obj: models.PrintingOrder):
|
||||||
|
return obj.customer.name
|
||||||
|
|
||||||
|
@admin.display(description='订单编号(虚拟)')
|
||||||
|
def oid(self, obj: models.PrintingOrder):
|
||||||
|
return obj.human_id
|
||||||
7
printing/apps.py
Normal file
7
printing/apps.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'printing'
|
||||||
|
verbose_name = '印染管理'
|
||||||
65
printing/migrations/0001_initial.py
Normal file
65
printing/migrations/0001_initial.py
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 07:03
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('basic_info', '0009_product_minimum_quantity'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PrintingOrder',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('fabric', models.CharField(max_length=100, verbose_name='面料')),
|
||||||
|
('width', models.CharField(max_length=100, verbose_name='幅宽')),
|
||||||
|
('is_urgent', models.BooleanField(default=False, verbose_name='紧急')),
|
||||||
|
('area', models.CharField(blank=True, max_length=100, null=True, verbose_name='地区')),
|
||||||
|
('address', models.CharField(blank=True, max_length=200, null=True, verbose_name='地址')),
|
||||||
|
('fabric_source', models.CharField(max_length=100, null=True, verbose_name='布料来源')),
|
||||||
|
('is_fabric_received', models.BooleanField(default=False, verbose_name='布料是否已收')),
|
||||||
|
('craft', models.CharField(max_length=100, null=True, verbose_name='工艺')),
|
||||||
|
('description', models.TextField(blank=True, null=True, verbose_name='订单描述')),
|
||||||
|
('outgoing_date', models.DateField(blank=True, null=True, verbose_name='出货日期')),
|
||||||
|
('curve', models.CharField(blank=True, max_length=100, null=True, verbose_name='曲线')),
|
||||||
|
('new_curve', models.CharField(blank=True, max_length=100, null=True, verbose_name='新加曲线')),
|
||||||
|
('position', models.TextField(blank=True, null=True, verbose_name='位置')),
|
||||||
|
('printing_warn', models.TextField(blank=True, null=True, verbose_name='打印注意事项')),
|
||||||
|
('rolling_warn', models.TextField(blank=True, null=True, verbose_name='滚筒注意事项')),
|
||||||
|
('production_warn', models.TextField(blank=True, null=True, verbose_name='生产注意事项')),
|
||||||
|
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='printing_orders', to='basic_info.customer', verbose_name='客户')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '印染订单',
|
||||||
|
'verbose_name_plural': '印染订单',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PrintingJob',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('quantity', models.PositiveIntegerField(verbose_name='数量')),
|
||||||
|
('unit', models.CharField(max_length=50, verbose_name='单位')),
|
||||||
|
('size', models.CharField(max_length=100, verbose_name='一段尺寸')),
|
||||||
|
('pieces', models.PositiveIntegerField(verbose_name='件数')),
|
||||||
|
('description', models.TextField(blank=True, verbose_name='备注')),
|
||||||
|
('status', models.CharField(max_length=50, verbose_name='状态')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='printing_jobs', to='basic_info.product', verbose_name='产品')),
|
||||||
|
('printing_order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='printing_jobs', to='printing.printingorder', verbose_name='主订单')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '印染款式明细',
|
||||||
|
'verbose_name_plural': '印染款式明细',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 07:06
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('printing', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='printingorder',
|
||||||
|
name='craft',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='工艺'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='printingorder',
|
||||||
|
name='fabric_source',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='布料来源'),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
printing/migrations/0003_remove_printingjob_status.py
Normal file
17
printing/migrations/0003_remove_printingjob_status.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 08:00
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('printing', '0002_alter_printingorder_craft_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='printingjob',
|
||||||
|
name='status',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
printing/migrations/0004_printingorder_is_invalid.py
Normal file
18
printing/migrations/0004_printingorder_is_invalid.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 08:04
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('printing', '0003_remove_printingjob_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='printingorder',
|
||||||
|
name='is_invalid',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='是否作废'),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
printing/migrations/0005_alter_printingorder_options.py
Normal file
17
printing/migrations/0005_alter_printingorder_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 08:14
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('printing', '0004_printingorder_is_invalid'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='printingorder',
|
||||||
|
options={'permissions': [('can_invalidate_printingorder', '可以作废印染订单'), ('can_activate_printingorder', '可以恢复印染订单')], 'verbose_name': '印染订单', 'verbose_name_plural': '印染订单'},
|
||||||
|
),
|
||||||
|
]
|
||||||
0
printing/migrations/__init__.py
Normal file
0
printing/migrations/__init__.py
Normal file
73
printing/models.py
Normal file
73
printing/models.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from django.db import models
|
||||||
|
from flower.common import ModelBase
|
||||||
|
from basic_info import models as basic_models
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrder(ModelBase):
|
||||||
|
"""PrintingOrder model representing a printing order."""
|
||||||
|
customer = models.ForeignKey(
|
||||||
|
basic_models.Customer,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='printing_orders',
|
||||||
|
verbose_name='客户',
|
||||||
|
)
|
||||||
|
fabric = models.CharField(max_length=100, verbose_name='面料')
|
||||||
|
width = models.CharField(max_length=100, verbose_name='幅宽')
|
||||||
|
is_urgent = models.BooleanField(default=False, verbose_name='紧急')
|
||||||
|
area = models.CharField(max_length=100, null=True, blank=True, verbose_name='地区')
|
||||||
|
address = models.CharField(max_length=200, null=True, blank=True, verbose_name='地址')
|
||||||
|
fabric_source = models.CharField(max_length=100, null=True, blank=True, verbose_name='布料来源')
|
||||||
|
is_fabric_received = models.BooleanField(default=False, verbose_name='布料是否已收')
|
||||||
|
craft = models.CharField(max_length=100, null=True, blank=True, verbose_name='工艺')
|
||||||
|
description = models.TextField(blank=True, null=True, verbose_name='订单描述')
|
||||||
|
outgoing_date = models.DateField(null=True, blank=True, verbose_name='出货日期')
|
||||||
|
curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='曲线')
|
||||||
|
new_curve = models.CharField(max_length=100, blank=True, null=True, verbose_name='新加曲线')
|
||||||
|
position = models.TextField(blank=True, null=True, verbose_name='位置')
|
||||||
|
printing_warn = models.TextField(blank=True, null=True, verbose_name='打印注意事项')
|
||||||
|
rolling_warn = models.TextField(blank=True, null=True, verbose_name='滚筒注意事项')
|
||||||
|
production_warn = models.TextField(blank=True, null=True, verbose_name='生产注意事项')
|
||||||
|
is_invalid = models.BooleanField(default=False, verbose_name='是否作废')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.human_id
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '印染订单'
|
||||||
|
verbose_name_plural = '印染订单'
|
||||||
|
permissions = [
|
||||||
|
('can_invalidate_printingorder', '可以作废印染订单'),
|
||||||
|
('can_activate_printingorder', '可以恢复印染订单'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def human_id(self) -> str:
|
||||||
|
return f"{self.created_at.year}{self.created_at.month:02d}{self.created_at.day:02d}{self.id:06d}"
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingJob(ModelBase):
|
||||||
|
"""PrintingJob model representing a printing job associated with an order."""
|
||||||
|
printing_order = models.ForeignKey(
|
||||||
|
PrintingOrder,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='printing_jobs',
|
||||||
|
verbose_name='主订单',
|
||||||
|
)
|
||||||
|
product = models.ForeignKey(
|
||||||
|
basic_models.Product,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='printing_jobs',
|
||||||
|
verbose_name='产品',
|
||||||
|
)
|
||||||
|
quantity = models.PositiveIntegerField(verbose_name='数量')
|
||||||
|
unit = models.CharField(max_length=50, verbose_name='单位')
|
||||||
|
size = models.CharField(max_length=100, verbose_name='一段尺寸')
|
||||||
|
pieces = models.PositiveIntegerField(verbose_name='件数')
|
||||||
|
description = models.TextField(blank=True, verbose_name='备注')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.printing_order.human_id
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = '印染款式明细'
|
||||||
|
verbose_name_plural = '印染款式明细'
|
||||||
59
printing/serializers.py
Normal file
59
printing/serializers.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Printing 序列化器
|
||||||
|
"""
|
||||||
|
from rest_framework import serializers
|
||||||
|
from printing import models
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderListSerializer(serializers.ModelSerializer):
|
||||||
|
"""印染订单列表序列化器"""
|
||||||
|
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||||
|
customer_phone = serializers.CharField(source='customer.phone', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PrintingOrder
|
||||||
|
fields = [
|
||||||
|
'id', 'human_id', 'customer', 'customer_name', 'customer_phone',
|
||||||
|
'fabric', 'width', 'is_urgent', 'area', 'address',
|
||||||
|
'is_fabric_received', 'outgoing_date', 'is_invalid',
|
||||||
|
'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderDetailSerializer(serializers.ModelSerializer):
|
||||||
|
"""印染订单详情序列化器"""
|
||||||
|
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||||
|
customer_phone = serializers.CharField(source='customer.phone', read_only=True)
|
||||||
|
customer_address = serializers.CharField(source='customer.address', read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PrintingOrder
|
||||||
|
fields = [
|
||||||
|
'id', 'human_id', 'customer', 'customer_name', 'customer_phone', 'customer_address',
|
||||||
|
'fabric', 'width', 'is_urgent', 'area', 'address', 'fabric_source',
|
||||||
|
'is_fabric_received', 'craft', 'description', 'outgoing_date',
|
||||||
|
'curve', 'new_curve', 'position',
|
||||||
|
'printing_warn', 'rolling_warn', 'production_warn',
|
||||||
|
'is_invalid', 'created_at', 'updated_at'
|
||||||
|
]
|
||||||
|
read_only_fields = ['id', 'human_id', 'created_at', 'updated_at']
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||||
|
"""印染订单创建/更新序列化器"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PrintingOrder
|
||||||
|
fields = [
|
||||||
|
'customer', 'fabric', 'width', 'is_urgent', 'area', 'address',
|
||||||
|
'fabric_source', 'is_fabric_received', 'craft', 'description',
|
||||||
|
'outgoing_date', 'curve', 'new_curve', 'position',
|
||||||
|
'printing_warn', 'rolling_warn', 'production_warn', 'is_invalid'
|
||||||
|
]
|
||||||
|
|
||||||
|
def validate_customer(self, value):
|
||||||
|
"""验证客户是否存在"""
|
||||||
|
if not value:
|
||||||
|
raise serializers.ValidationError("客户不能为空")
|
||||||
|
return value
|
||||||
0
printing/services.py
Normal file
0
printing/services.py
Normal file
3
printing/tests.py
Normal file
3
printing/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
227
printing/views.py
Normal file
227
printing/views.py
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Printing API ViewSet
|
||||||
|
"""
|
||||||
|
from rest_framework import viewsets, filters, status
|
||||||
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import BasePermission
|
||||||
|
from rest_framework.pagination import LimitOffsetPagination
|
||||||
|
from rest_framework.permissions import DjangoModelPermissions
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from django_filters import rest_framework as django_filters
|
||||||
|
from printing import models
|
||||||
|
from basic_info.models import MerchantTypeEnum
|
||||||
|
|
||||||
|
from printing.serializers import (
|
||||||
|
PrintingOrderListSerializer,
|
||||||
|
PrintingOrderDetailSerializer,
|
||||||
|
PrintingOrderCreateUpdateSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IsPrintingFactory(BasePermission):
|
||||||
|
"""自定义权限类,允许印染工厂用户访问"""
|
||||||
|
|
||||||
|
message = '您没有访问印染订单的权限'
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
if hasattr(request.user, 'employee'):
|
||||||
|
return request.user.employee.merchant.type == MerchantTypeEnum.FACTORY
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class HasInvalidatePrintingOrderPermission(BasePermission):
|
||||||
|
"""自定义权限类,检查用户是否有作废印染订单的权限"""
|
||||||
|
|
||||||
|
message = '您没有权限作废订单'
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
return request.user.has_perm('printing.can_invalidate_printingorder')
|
||||||
|
|
||||||
|
|
||||||
|
class HasActivatePrintingOrderPermission(BasePermission):
|
||||||
|
"""自定义权限类,检查用户是否有恢复印染订单的权限"""
|
||||||
|
|
||||||
|
message = '您没有权限恢复订单'
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return False
|
||||||
|
return request.user.has_perm('printing.can_activate_printingorder')
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderFilterSet(django_filters.FilterSet):
|
||||||
|
"""印染订单过滤器"""
|
||||||
|
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
||||||
|
customer_phone = django_filters.CharFilter(field_name='customer__phone', lookup_expr='icontains')
|
||||||
|
fabric = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
is_urgent = django_filters.BooleanFilter()
|
||||||
|
is_fabric_received = django_filters.BooleanFilter()
|
||||||
|
is_invalid = django_filters.BooleanFilter()
|
||||||
|
area = django_filters.CharFilter(lookup_expr='icontains')
|
||||||
|
outgoing_date_from = django_filters.DateFilter(field_name='outgoing_date', lookup_expr='gte')
|
||||||
|
outgoing_date_to = django_filters.DateFilter(field_name='outgoing_date', lookup_expr='lte')
|
||||||
|
created_date_from = django_filters.DateFilter(field_name='created_at', lookup_expr='gte')
|
||||||
|
created_date_to = django_filters.DateFilter(field_name='created_at', lookup_expr='lte')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.PrintingOrder
|
||||||
|
fields = ['customer', 'is_urgent', 'is_fabric_received', 'is_invalid']
|
||||||
|
|
||||||
|
|
||||||
|
class PrintingOrderViewSet(viewsets.ModelViewSet):
|
||||||
|
"""
|
||||||
|
印染订单 ViewSet
|
||||||
|
|
||||||
|
提供印染订单的增改查功能(不支持删除)
|
||||||
|
|
||||||
|
list: 获取印染订单列表
|
||||||
|
retrieve: 获取印染订单详情
|
||||||
|
create: 创建印染订单
|
||||||
|
update: 更新印染订单
|
||||||
|
partial_update: 部分更新印染订单
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
- customer: 客户ID
|
||||||
|
- customer_name: 客户名称(模糊查询)
|
||||||
|
- customer_phone: 客户电话(模糊查询)
|
||||||
|
- fabric: 面料(模糊查询)
|
||||||
|
- is_urgent: 是否紧急(true/false)
|
||||||
|
- is_fabric_received: 布料是否已收(true/false)
|
||||||
|
- is_invalid: 是否作废(true/false)
|
||||||
|
- area: 地区(模糊查询)
|
||||||
|
- outgoing_date_from: 出货日期起始
|
||||||
|
- outgoing_date_to: 出货日期结束
|
||||||
|
- created_date_from: 创建日期起始
|
||||||
|
- created_date_to: 创建日期结束
|
||||||
|
- search: 全文搜索(客户名称、面料、地区、工艺)
|
||||||
|
- ordering: 排序字段
|
||||||
|
"""
|
||||||
|
queryset = models.PrintingOrder.objects.all()
|
||||||
|
permission_classes = [DjangoModelPermissions]
|
||||||
|
pagination_class = LimitOffsetPagination
|
||||||
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||||
|
filterset_class = PrintingOrderFilterSet
|
||||||
|
search_fields = ['customer__name', 'fabric', 'area', 'craft', 'description']
|
||||||
|
ordering_fields = [
|
||||||
|
'id', 'created_at', 'updated_at', 'outgoing_date',
|
||||||
|
'is_urgent', 'is_fabric_received', 'is_invalid'
|
||||||
|
]
|
||||||
|
ordering = ['-created_at']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
permissions = super().get_permissions() + [IsPrintingFactory()]
|
||||||
|
if self.action == 'invalidate':
|
||||||
|
permissions.append(HasInvalidatePrintingOrderPermission())
|
||||||
|
elif self.action == 'activate':
|
||||||
|
permissions.append(HasActivatePrintingOrderPermission())
|
||||||
|
return permissions
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
"""根据动作选择序列化器"""
|
||||||
|
if self.action == 'list':
|
||||||
|
return PrintingOrderListSerializer
|
||||||
|
elif self.action in ['create', 'update', 'partial_update']:
|
||||||
|
return PrintingOrderCreateUpdateSerializer
|
||||||
|
else: # retrieve
|
||||||
|
return PrintingOrderDetailSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""优化查询"""
|
||||||
|
queryset = super().get_queryset()
|
||||||
|
if self.action in ['list', 'retrieve']:
|
||||||
|
queryset = queryset.select_related('customer')
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
"""禁用删除操作"""
|
||||||
|
return Response(
|
||||||
|
{'detail': '印染订单不支持删除操作,请使用作废功能'},
|
||||||
|
status=status.HTTP_405_METHOD_NOT_ALLOWED
|
||||||
|
)
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def invalidate(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
作废订单
|
||||||
|
|
||||||
|
需要权限: printing.can_invalidate_printingorder
|
||||||
|
"""
|
||||||
|
# 检查权限
|
||||||
|
if not request.user.has_perm('printing.can_invalidate_printingorder'):
|
||||||
|
return Response(
|
||||||
|
{'detail': '您没有权限作废订单'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_order = self.get_object()
|
||||||
|
|
||||||
|
if printing_order.is_invalid:
|
||||||
|
return Response(
|
||||||
|
{'detail': '该订单已经作废'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_order.is_invalid = True
|
||||||
|
printing_order.save()
|
||||||
|
|
||||||
|
serializer = PrintingOrderDetailSerializer(printing_order)
|
||||||
|
return Response({
|
||||||
|
'detail': '订单已作废',
|
||||||
|
'data': serializer.data
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def activate(self, request, pk=None):
|
||||||
|
"""
|
||||||
|
恢复订单
|
||||||
|
|
||||||
|
需要权限: printing.can_activate_printingorder
|
||||||
|
"""
|
||||||
|
# 检查权限
|
||||||
|
if not request.user.has_perm('printing.can_activate_printingorder'):
|
||||||
|
return Response(
|
||||||
|
{'detail': '您没有权限恢复订单'},
|
||||||
|
status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_order = self.get_object()
|
||||||
|
|
||||||
|
if not printing_order.is_invalid:
|
||||||
|
return Response(
|
||||||
|
{'detail': '该订单未作废,无需恢复'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_order.is_invalid = False
|
||||||
|
printing_order.save()
|
||||||
|
|
||||||
|
serializer = PrintingOrderDetailSerializer(printing_order)
|
||||||
|
return Response({
|
||||||
|
'detail': '订单已恢复',
|
||||||
|
'data': serializer.data
|
||||||
|
})
|
||||||
|
|
||||||
|
@action(detail=True, methods=['post'])
|
||||||
|
def mark_fabric_received(self, request, pk=None):
|
||||||
|
"""标记布料已收"""
|
||||||
|
printing_order = self.get_object()
|
||||||
|
|
||||||
|
if printing_order.is_fabric_received:
|
||||||
|
return Response(
|
||||||
|
{'detail': '布料已经标记为已收'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
printing_order.is_fabric_received = True
|
||||||
|
printing_order.save()
|
||||||
|
|
||||||
|
serializer = PrintingOrderDetailSerializer(printing_order)
|
||||||
|
return Response({
|
||||||
|
'detail': '已标记布料已收',
|
||||||
|
'data': serializer.data
|
||||||
|
})
|
||||||
@@ -4,4 +4,4 @@ from django.apps import AppConfig
|
|||||||
class StateflowConfig(AppConfig):
|
class StateflowConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'stateflow'
|
name = 'stateflow'
|
||||||
verbose_name = '印染流程'
|
verbose_name = '任务流程'
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 04:08
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stateflow', '0012_alter_businessobject_content_type_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='stateflowrecord',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
]
|
||||||
17
stateflow/migrations/0014_alter_process_options.py
Normal file
17
stateflow/migrations/0014_alter_process_options.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-11-12 06:19
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('stateflow', '0013_alter_stateflowrecord_unique_together'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='process',
|
||||||
|
options={'verbose_name': '流程编排', 'verbose_name_plural': '流程编排'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -72,8 +72,8 @@ class Process(ModelBase):
|
|||||||
return [item.state for item in process_nodes]
|
return [item.state for item in process_nodes]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '印染流程'
|
verbose_name = '流程编排'
|
||||||
verbose_name_plural = '印染流程'
|
verbose_name_plural = '流程编排'
|
||||||
|
|
||||||
|
|
||||||
class ProcessNode(ModelBase):
|
class ProcessNode(ModelBase):
|
||||||
@@ -155,5 +155,5 @@ class StateFlowRecord(ModelBase):
|
|||||||
db_table = 'state_flow_record'
|
db_table = 'state_flow_record'
|
||||||
verbose_name = '状态流转记录'
|
verbose_name = '状态流转记录'
|
||||||
verbose_name_plural = '状态流转记录'
|
verbose_name_plural = '状态流转记录'
|
||||||
unique_together = [['business_object', 'state']]
|
# 移除 unique_together 约束,允许同一状态多次记录(支持撤销后重新执行)
|
||||||
ordering = ['completed_at']
|
ordering = ['completed_at']
|
||||||
|
|||||||
@@ -83,14 +83,18 @@ def get_progress_percentage(business_object: 'models.BusinessObject') -> float:
|
|||||||
return (completed_count / total_nodes) * 100
|
return (completed_count / total_nodes) * 100
|
||||||
|
|
||||||
|
|
||||||
def advance_to_next_state(business_object: 'models.BusinessObject', user: 'User') -> Tuple[bool, str]:
|
def advance_to_next_state(business_object: 'models.BusinessObject', user) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
将订单推进到下一个状态
|
将订单推进到下一个状态
|
||||||
|
|
||||||
返回: (是否成功, 消息)
|
返回: (是否成功, 消息)
|
||||||
"""
|
"""
|
||||||
|
can_advance, reason = can_advance_to_next_state(business_object)
|
||||||
|
if not can_advance:
|
||||||
|
return False, reason
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# 获取当前状态
|
# 获取当前状态(返回第一个未完成的节点)
|
||||||
current_state = get_business_object_current_state(business_object)
|
current_state = get_business_object_current_state(business_object)
|
||||||
|
|
||||||
# 如果当前状态为 None,说明是初始状态(未开始)
|
# 如果当前状态为 None,说明是初始状态(未开始)
|
||||||
@@ -108,10 +112,7 @@ def advance_to_next_state(business_object: 'models.BusinessObject', user: 'User'
|
|||||||
)
|
)
|
||||||
return True, f"已完成状态: {first_node.state.name}"
|
return True, f"已完成状态: {first_node.state.name}"
|
||||||
|
|
||||||
# 检查当前状态是否已完成(且未撤销)
|
# current_state 不为 None 时,它一定是未完成的节点
|
||||||
if business_object.state_logs.filter(state=current_state, is_cancelled=False).exists():
|
|
||||||
return False, f"当前状态 '{current_state.name}' 已完成"
|
|
||||||
|
|
||||||
# 标记当前状态为已完成
|
# 标记当前状态为已完成
|
||||||
models.StateFlowRecord.objects.create(
|
models.StateFlowRecord.objects.create(
|
||||||
business_object=business_object,
|
business_object=business_object,
|
||||||
@@ -128,12 +129,12 @@ def advance_to_next_state(business_object: 'models.BusinessObject', user: 'User'
|
|||||||
return True, f"已完成状态: {current_state.name}"
|
return True, f"已完成状态: {current_state.name}"
|
||||||
|
|
||||||
|
|
||||||
def reset_order_progress(business_object: 'models.BusinessObject') -> None:
|
def reset_business_object_progress(business_object: 'models.BusinessObject') -> None:
|
||||||
"""
|
"""
|
||||||
重置订单进度
|
重置业务对象进度
|
||||||
|
|
||||||
注意:不删除历史记录,而是标记所有记录为已撤销,并记录撤销时间
|
注意:不删除历史记录,而是标记所有记录为已撤销,并记录撤销时间
|
||||||
这样可以保留完整的操作历史
|
这样可以保留完整的操作历史,支持审计需求
|
||||||
"""
|
"""
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -143,6 +144,10 @@ def reset_order_progress(business_object: 'models.BusinessObject') -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# 向后兼容的别名
|
||||||
|
reset_order_progress = reset_business_object_progress
|
||||||
|
|
||||||
|
|
||||||
def get_current_state_parameters(business_object: 'models.BusinessObject') -> List['models.StateParameter']:
|
def get_current_state_parameters(business_object: 'models.BusinessObject') -> List['models.StateParameter']:
|
||||||
"""获取订单当前状态的参数列表"""
|
"""获取订单当前状态的参数列表"""
|
||||||
current_state = get_business_object_current_state(business_object)
|
current_state = get_business_object_current_state(business_object)
|
||||||
@@ -188,10 +193,10 @@ def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple
|
|||||||
|
|
||||||
current_state = get_business_object_current_state(business_object)
|
current_state = get_business_object_current_state(business_object)
|
||||||
|
|
||||||
# 如果当前状态为 None
|
# 如果当前状态为 None,有两种情况:
|
||||||
|
# 1. 没有完成记录 - 未开始
|
||||||
|
# 2. 所有节点已完成 - 已完成
|
||||||
if current_state is None:
|
if current_state is None:
|
||||||
# 检查是否所有状态都已完成
|
|
||||||
total_nodes = business_object.process.process_nodes.count()
|
|
||||||
completed_count = business_object.state_logs.filter(is_cancelled=False).count()
|
completed_count = business_object.state_logs.filter(is_cancelled=False).count()
|
||||||
|
|
||||||
if completed_count == 0:
|
if completed_count == 0:
|
||||||
@@ -201,10 +206,7 @@ def can_advance_to_next_state(business_object: 'models.BusinessObject') -> Tuple
|
|||||||
# 所有状态都已完成
|
# 所有状态都已完成
|
||||||
return False, "流程已完成,无法继续推进"
|
return False, "流程已完成,无法继续推进"
|
||||||
|
|
||||||
# 检查当前状态是否已完成
|
# current_state 不为 None 时,它一定是第一个未完成的节点,可以推进
|
||||||
if business_object.state_logs.filter(state=current_state, is_cancelled=False).exists():
|
|
||||||
return False, f"当前状态 '{current_state.name}' 已完成,无法重复完成"
|
|
||||||
|
|
||||||
return True, f"可以推进到: {current_state.name}"
|
return True, f"可以推进到: {current_state.name}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ class OrderStateFlowTestCase(TestCase):
|
|||||||
self.assertEqual(self.business_object.state_logs.count(), 2)
|
self.assertEqual(self.business_object.state_logs.count(), 2)
|
||||||
|
|
||||||
# 重置进度
|
# 重置进度
|
||||||
services.reset_order_progress(self.business_object)
|
services.reset_business_object_progress(self.business_object)
|
||||||
|
|
||||||
# 日志记录应该还在(不删除)
|
# 日志记录应该还在(不删除)
|
||||||
self.assertEqual(self.business_object.state_logs.count(), 2)
|
self.assertEqual(self.business_object.state_logs.count(), 2)
|
||||||
@@ -147,7 +147,7 @@ class OrderStateFlowTestCase(TestCase):
|
|||||||
services.advance_to_next_state(self.business_object, self.user)
|
services.advance_to_next_state(self.business_object, self.user)
|
||||||
|
|
||||||
# 重置进度
|
# 重置进度
|
||||||
services.reset_order_progress(self.business_object)
|
services.reset_business_object_progress(self.business_object)
|
||||||
|
|
||||||
timeline = self.business_object.get_timeline()
|
timeline = self.business_object.get_timeline()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user