forked from erp-dev/erp
fix: little bugs, feat: woodpecker settings
This commit is contained in:
45
.woodpecker.yml
Normal file
45
.woodpecker.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
# 使用 screen 部署 Django 项目
|
||||
|
||||
when:
|
||||
branch: main
|
||||
event: push
|
||||
|
||||
steps:
|
||||
- name: test
|
||||
image: ghcr.io/astral-sh/uv:python3.14-bookworm
|
||||
commands:
|
||||
# 安装依赖
|
||||
- uv sync
|
||||
|
||||
# 数据库迁移
|
||||
- uv run python manage.py migrate
|
||||
|
||||
# 运行测试
|
||||
- uv run python manage.py test
|
||||
|
||||
- name: deploy-with-screen
|
||||
image: appleboy/drone-ssh
|
||||
settings:
|
||||
host:
|
||||
from_secret: deploy_host
|
||||
username:
|
||||
from_secret: deploy_user
|
||||
password:
|
||||
from_secret: deploy_password
|
||||
port: 22
|
||||
script:
|
||||
- cd /path/to/your/project
|
||||
- git pull origin main
|
||||
- uv sync
|
||||
- uv run python manage.py migrate
|
||||
- |
|
||||
# 停止旧的 screen 会话
|
||||
screen -S erp -X quit || true
|
||||
# 等待进程完全退出
|
||||
sleep 2
|
||||
# 在新的 screen 会话中启动服务
|
||||
screen -dmS erp uv run python manage.py runserver 0.0.0.0:8000
|
||||
# 验证 screen 会话是否创建成功
|
||||
screen -ls | grep erp && echo "ERP server started in screen session"
|
||||
when:
|
||||
- evaluate: 'CI_PIPELINE_STATUS == "success"'
|
||||
@@ -205,6 +205,7 @@ class PlateOrderListSerializer(serializers.ModelSerializer):
|
||||
merchandiser_name = serializers.CharField(source="merchandiser.name", read_only=True)
|
||||
status = serializers.CharField(read_only=True)
|
||||
progress_percentage = serializers.IntegerField(read_only=True)
|
||||
process_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
@@ -216,13 +217,25 @@ class PlateOrderListSerializer(serializers.ModelSerializer):
|
||||
'merchandiser', 'merchandiser_name',
|
||||
'style_name', 'fabric', 'width',
|
||||
'required_completion_date', 'completion_date',
|
||||
'is_ordered', 'status', 'progress_percentage',
|
||||
'is_ordered', 'process', 'process_name',
|
||||
'status', 'progress_percentage',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'status', 'progress_percentage',
|
||||
'created_at', 'updated_at'
|
||||
]
|
||||
|
||||
def get_process_name(self, obj):
|
||||
"""获取流程名称"""
|
||||
if obj.process:
|
||||
try:
|
||||
from stateflow.models import Process
|
||||
process = Process.objects.get(id=obj.process)
|
||||
return process.name
|
||||
except Process.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class PlateOrderDetailSerializer(serializers.ModelSerializer):
|
||||
@@ -238,6 +251,7 @@ class PlateOrderDetailSerializer(serializers.ModelSerializer):
|
||||
progress_percentage = serializers.IntegerField(read_only=True)
|
||||
business_object_id = serializers.IntegerField(source="business_object.id", read_only=True)
|
||||
plate_image_url = serializers.SerializerMethodField()
|
||||
process_name = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.PlateOrder
|
||||
@@ -254,6 +268,7 @@ class PlateOrderDetailSerializer(serializers.ModelSerializer):
|
||||
'sample_meter', 'required_sample_meters',
|
||||
'required_completion_date', 'completion_date',
|
||||
'approval_result', 'is_ordered', 'customer_feedback',
|
||||
'process', 'process_name',
|
||||
'status', 'status_id', 'is_completed', 'has_started',
|
||||
'progress_percentage', 'business_object_id',
|
||||
'created_at', 'updated_at'
|
||||
@@ -272,6 +287,17 @@ class PlateOrderDetailSerializer(serializers.ModelSerializer):
|
||||
return request.build_absolute_uri(obj.plate_image.url)
|
||||
return obj.plate_image.url
|
||||
return None
|
||||
|
||||
def get_process_name(self, obj):
|
||||
"""获取流程名称"""
|
||||
if obj.process:
|
||||
try:
|
||||
from stateflow.models import Process
|
||||
process = Process.objects.get(id=obj.process)
|
||||
return process.name
|
||||
except Process.DoesNotExist:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
@@ -290,7 +316,8 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"sample_rating", "difficulty_rating",
|
||||
"sample_meter", "required_sample_meters",
|
||||
"required_completion_date", "completion_date",
|
||||
"approval_result", "is_ordered", "customer_feedback"
|
||||
"approval_result", "is_ordered", "customer_feedback",
|
||||
"process"
|
||||
]
|
||||
read_only_fields = ["id"]
|
||||
|
||||
@@ -324,6 +351,14 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
raise serializers.ValidationError("所需样品米数不能为负数")
|
||||
return value
|
||||
|
||||
def validate_process(self, value):
|
||||
"""验证流程是否存在"""
|
||||
if value:
|
||||
from stateflow.models import Process
|
||||
if not Process.objects.filter(id=value).exists():
|
||||
raise serializers.ValidationError("流程不存在")
|
||||
return value
|
||||
|
||||
def validate(self, attrs):
|
||||
"""交叉验证"""
|
||||
return attrs
|
||||
@@ -332,16 +367,14 @@ class PlateOrderCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
"""创建开版订单"""
|
||||
user = self.context["request"].user
|
||||
|
||||
# 创建 PlateOrder
|
||||
# 创建 PlateOrder,save 方法会自动创建 BusinessObject
|
||||
plate_order = models.PlateOrder.objects.create(**validated_data)
|
||||
|
||||
# 暂时不自动创建 BusinessObject,由 admin 或其他地方创建
|
||||
|
||||
return plate_order
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""更新开版订单"""
|
||||
# 直接更新字段
|
||||
# 直接更新字段,save 方法会自动处理 BusinessObject 的创建
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
from stateflow import models as stateflow_models
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
@@ -76,6 +77,19 @@ class PlateOrderAPITestCase(TestCase):
|
||||
change_perm = Permission.objects.get(codename='change_plateorder')
|
||||
delete_perm = Permission.objects.get(codename='delete_plateorder')
|
||||
self.user.user_permissions.add(view_perm, add_perm, change_perm, delete_perm)
|
||||
|
||||
# 创建测试流程
|
||||
self.state1 = stateflow_models.State.objects.create(name='画图', description='画图阶段')
|
||||
self.state2 = stateflow_models.State.objects.create(name='调色', description='调色阶段')
|
||||
self.state3 = stateflow_models.State.objects.create(name='套样', description='套样阶段')
|
||||
|
||||
self.process = stateflow_models.Process.objects.create(
|
||||
name='开版流程',
|
||||
description='标准开版流程'
|
||||
)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
|
||||
|
||||
def test_create_plate_order(self):
|
||||
"""测试创建开版订单"""
|
||||
@@ -401,6 +415,298 @@ class PlateOrderAPITestCase(TestCase):
|
||||
else:
|
||||
self.assertEqual(response.data[0]['id'], order1.id)
|
||||
self.assertEqual(response.data[1]['id'], order2.id)
|
||||
|
||||
def test_create_plate_order_with_process(self):
|
||||
"""测试创建开版订单时指定流程"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN100',
|
||||
'plate_type': '圆网',
|
||||
'style_name': '测试款式',
|
||||
'fabric': '棉布',
|
||||
'process': self.process.id,
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(response.data['process'], self.process.id)
|
||||
|
||||
# 验证数据库中创建了 BusinessObject
|
||||
plate_order = printing_models.PlateOrder.objects.get(id=response.data['id'])
|
||||
self.assertIsNotNone(plate_order.business_object)
|
||||
self.assertEqual(plate_order.business_object.process.id, self.process.id)
|
||||
|
||||
def test_create_plate_order_without_process(self):
|
||||
"""测试创建开版订单时不指定流程(使用默认流程)"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN101',
|
||||
'plate_type': '圆网',
|
||||
'style_name': '测试款式',
|
||||
'fabric': '棉布',
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# 应该使用默认流程
|
||||
self.assertIsNotNone(response.data['process'])
|
||||
|
||||
# 验证使用了默认流程
|
||||
plate_order = printing_models.PlateOrder.objects.get(id=response.data['id'])
|
||||
from django.conf import settings
|
||||
self.assertEqual(plate_order.process, settings.PLATE_ORDER_DEFAULT_PROCESS_ID)
|
||||
|
||||
def test_create_plate_order_with_invalid_process(self):
|
||||
"""测试创建开版订单时指定无效的流程ID"""
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN102',
|
||||
'plate_type': '圆网',
|
||||
'style_name': '测试款式',
|
||||
'fabric': '棉布',
|
||||
'process': 99999, # 不存在的流程ID
|
||||
}
|
||||
|
||||
response = self.client.post('/api/v1/plate-orders/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('process', response.data)
|
||||
|
||||
def test_retrieve_plate_order_with_process_name(self):
|
||||
"""测试获取开版订单详情时包含流程名称"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN103',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
fabric='棉布',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['process'], self.process.id)
|
||||
self.assertEqual(response.data['process_name'], self.process.name)
|
||||
# 验证自动创建了 business_object
|
||||
self.assertIsNotNone(response.data['business_object_id'])
|
||||
|
||||
def test_list_plate_orders_includes_process_name(self):
|
||||
"""测试列表接口包含流程名称"""
|
||||
printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN104',
|
||||
plate_type='圆网',
|
||||
style_name='款式1',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
response = self.client.get('/api/v1/plate-orders/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
if isinstance(response.data, dict):
|
||||
result = response.data['results'][0]
|
||||
else:
|
||||
result = response.data[0]
|
||||
|
||||
self.assertEqual(result['process'], self.process.id)
|
||||
self.assertEqual(result['process_name'], self.process.name)
|
||||
|
||||
def test_update_plate_order_process(self):
|
||||
"""测试更新开版订单的流程"""
|
||||
# 创建另一个流程
|
||||
process2 = stateflow_models.Process.objects.create(
|
||||
name='快速开版流程',
|
||||
description='简化的开版流程'
|
||||
)
|
||||
stateflow_models.ProcessNode.objects.create(process=process2, state=self.state1, order=0)
|
||||
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN105',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 获取原来的 business_object
|
||||
old_business_object_id = plate_order.business_object.id if plate_order.business_object else None
|
||||
|
||||
# 更新流程(注意:这只会更新 process 字段,不会重新创建 business_object)
|
||||
data = {
|
||||
'customer': self.customer.id,
|
||||
'design_code': 'DESIGN105',
|
||||
'plate_type': '圆网',
|
||||
'style_name': '测试款式',
|
||||
'process': process2.id,
|
||||
}
|
||||
|
||||
response = self.client.put(f'/api/v1/plate-orders/{plate_order.id}/', data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['process'], process2.id)
|
||||
|
||||
# 验证 business_object 没有改变(因为已经存在)
|
||||
plate_order.refresh_from_db()
|
||||
if old_business_object_id:
|
||||
self.assertEqual(plate_order.business_object.id, old_business_object_id)
|
||||
|
||||
def test_business_object_auto_creation(self):
|
||||
"""测试 BusinessObject 自动创建逻辑"""
|
||||
# 直接创建 PlateOrder(不通过 API)
|
||||
plate_order = printing_models.PlateOrder(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN106',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 保存前没有 business_object
|
||||
self.assertIsNone(plate_order.business_object)
|
||||
|
||||
# 保存后自动创建
|
||||
plate_order.save()
|
||||
plate_order.refresh_from_db()
|
||||
|
||||
self.assertIsNotNone(plate_order.business_object)
|
||||
self.assertEqual(plate_order.business_object.process.id, self.process.id)
|
||||
|
||||
def test_advance_to_next_state(self):
|
||||
"""测试推进到下一个状态"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN107',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 确保有 business_object
|
||||
self.assertIsNotNone(plate_order.business_object)
|
||||
|
||||
response = self.client.post(f'/api/v1/plate-orders/{plate_order.id}/advance-to-next-state/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('detail', response.data)
|
||||
self.assertIn('data', response.data)
|
||||
|
||||
# 验证状态改变
|
||||
plate_order.refresh_from_db()
|
||||
self.assertIsNotNone(plate_order.status)
|
||||
|
||||
def test_advance_to_next_state_without_business_object(self):
|
||||
"""测试没有 business_object 时推进状态"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN108',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
)
|
||||
|
||||
# 手动清除 business_object
|
||||
plate_order.business_object = None
|
||||
plate_order.save(update_fields=['business_object'])
|
||||
|
||||
response = self.client.post(f'/api/v1/plate-orders/{plate_order.id}/advance-to-next-state/')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertIn('没有关联的流程实例', response.data['detail'])
|
||||
|
||||
def test_step_back_one_state(self):
|
||||
"""测试回退一步"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN109',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 先推进一步
|
||||
from stateflow import services as stateflow_services
|
||||
stateflow_services.advance_to_next_state(plate_order.business_object, self.user)
|
||||
|
||||
# 再回退
|
||||
response = self.client.post(f'/api/v1/plate-orders/{plate_order.id}/step-back-one-state/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('detail', response.data)
|
||||
self.assertIn('data', response.data)
|
||||
|
||||
def test_completed_states(self):
|
||||
"""测试查询已完成的流程列表"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN110',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 推进几步
|
||||
from stateflow import services as stateflow_services
|
||||
stateflow_services.advance_to_next_state(plate_order.business_object, self.user)
|
||||
stateflow_services.advance_to_next_state(plate_order.business_object, self.user)
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/completed-states/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('count', response.data)
|
||||
self.assertIn('results', response.data)
|
||||
self.assertEqual(response.data['count'], 2)
|
||||
|
||||
def test_completed_states_include_cancelled(self):
|
||||
"""测试查询包含已撤销的流程"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN111',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/completed-states/?include_cancelled=true')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('count', response.data)
|
||||
self.assertIn('results', response.data)
|
||||
|
||||
def test_timeline(self):
|
||||
"""测试获取流程时间线"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN112',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
process=self.process.id,
|
||||
)
|
||||
|
||||
# 推进一步
|
||||
from stateflow import services as stateflow_services
|
||||
stateflow_services.advance_to_next_state(plate_order.business_object, self.user)
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/timeline/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('count', response.data)
|
||||
self.assertIn('results', response.data)
|
||||
|
||||
# 验证返回的数据结构
|
||||
if response.data['count'] > 0:
|
||||
first_item = response.data['results'][0]
|
||||
self.assertIn('state_id', first_item)
|
||||
self.assertIn('state_name', first_item)
|
||||
self.assertIn('state_description', first_item)
|
||||
self.assertIn('order', first_item)
|
||||
self.assertIn('status', first_item)
|
||||
|
||||
def test_timeline_without_business_object(self):
|
||||
"""测试没有 business_object 时获取时间线"""
|
||||
plate_order = printing_models.PlateOrder.objects.create(
|
||||
customer=self.customer,
|
||||
design_code='DESIGN113',
|
||||
plate_type='圆网',
|
||||
style_name='测试款式',
|
||||
)
|
||||
|
||||
# 手动清除 business_object
|
||||
plate_order.business_object = None
|
||||
plate_order.save(update_fields=['business_object'])
|
||||
|
||||
response = self.client.get(f'/api/v1/plate-orders/{plate_order.id}/timeline/')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['count'], 0)
|
||||
|
||||
|
||||
class PlateOrderInvalidateAPITestCase(TestCase):
|
||||
@@ -448,6 +754,14 @@ class PlateOrderInvalidateAPITestCase(TestCase):
|
||||
add_perm = Permission.objects.get(codename='add_plateorder')
|
||||
change_perm = Permission.objects.get(codename='change_plateorder')
|
||||
self.user.user_permissions.add(view_perm, add_perm, change_perm)
|
||||
|
||||
# 创建测试流程(用于测试)
|
||||
self.state1 = stateflow_models.State.objects.create(name='画图', description='画图阶段')
|
||||
self.process = stateflow_models.Process.objects.create(
|
||||
name='开版流程',
|
||||
description='标准开版流程'
|
||||
)
|
||||
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
|
||||
|
||||
def test_invalidate_plate_order_without_permission(self):
|
||||
"""测试无权限作废开版订单"""
|
||||
|
||||
@@ -482,9 +482,6 @@ class HasActivatePlateOrderPermission(BasePermission):
|
||||
return request.user.has_perm('printing.can_activate_plateorder')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class PlateOrderFilterSet(django_filters.FilterSet):
|
||||
"""开版订单过滤器"""
|
||||
customer_name = django_filters.CharFilter(field_name='customer__name', lookup_expr='icontains')
|
||||
@@ -523,6 +520,10 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
partial_update: 部分更新开版订单
|
||||
invalidate: 作废开版订单
|
||||
activate: 恢复开版订单
|
||||
advance_to_next_state: 推进到下一个状态
|
||||
step_back_one_state: 回退一步
|
||||
completed_states: 查询已完成的流程列表
|
||||
timeline: 获取流程时间线
|
||||
|
||||
查询参数:
|
||||
- customer: 客户ID
|
||||
@@ -651,3 +652,169 @@ class PlateOrderViewSet(viewsets.ModelViewSet):
|
||||
'detail': '开版订单已恢复',
|
||||
'data': serializer.data
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='advance-to-next-state')
|
||||
def advance_to_next_state(self, request, pk=None):
|
||||
"""
|
||||
推进到下一个状态
|
||||
|
||||
将开版订单推进到下一个流程状态
|
||||
"""
|
||||
plate_order = self.get_object()
|
||||
|
||||
if not plate_order.business_object:
|
||||
return Response(
|
||||
{'detail': '该开版订单没有关联的流程实例'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
from stateflow import services as stateflow_services
|
||||
success, message = stateflow_services.advance_to_next_state(
|
||||
plate_order.business_object, request.user
|
||||
)
|
||||
|
||||
if not success:
|
||||
return Response(
|
||||
{'detail': message},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 重新获取 plate_order 以刷新状态
|
||||
plate_order.refresh_from_db()
|
||||
from .serializers import PlateOrderDetailSerializer
|
||||
serializer = PlateOrderDetailSerializer(plate_order, context={'request': request})
|
||||
|
||||
return Response({
|
||||
'detail': message,
|
||||
'data': serializer.data
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['post'], url_path='step-back-one-state')
|
||||
def step_back_one_state(self, request, pk=None):
|
||||
"""
|
||||
回退一步
|
||||
|
||||
将开版订单回退到上一个流程状态
|
||||
"""
|
||||
plate_order = self.get_object()
|
||||
|
||||
if not plate_order.business_object:
|
||||
return Response(
|
||||
{'detail': '该开版订单没有关联的流程实例'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
from stateflow import services as stateflow_services
|
||||
success, message = stateflow_services.step_back_one_state(
|
||||
plate_order.business_object, request.user
|
||||
)
|
||||
|
||||
if not success:
|
||||
return Response(
|
||||
{'detail': message},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 重新获取 plate_order 以刷新状态
|
||||
plate_order.refresh_from_db()
|
||||
from .serializers import PlateOrderDetailSerializer
|
||||
serializer = PlateOrderDetailSerializer(plate_order, context={'request': request})
|
||||
|
||||
return Response({
|
||||
'detail': message,
|
||||
'data': serializer.data
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='completed-states')
|
||||
def completed_states(self, request, pk=None):
|
||||
"""
|
||||
查询已完成的流程列表
|
||||
|
||||
查询参数:
|
||||
- include_cancelled: 是否包含已撤销的流程(true/false),默认 false
|
||||
"""
|
||||
plate_order = self.get_object()
|
||||
|
||||
if not plate_order.business_object:
|
||||
return Response({
|
||||
'count': 0,
|
||||
'results': []
|
||||
})
|
||||
|
||||
# 获取查询参数
|
||||
include_cancelled = request.query_params.get('include_cancelled', 'false').lower() == 'true'
|
||||
|
||||
# 查询已完成的状态
|
||||
state_logs = plate_order.business_object.state_logs.select_related('state', 'completed_by').order_by('completed_at')
|
||||
|
||||
if not include_cancelled:
|
||||
state_logs = state_logs.filter(is_cancelled=False)
|
||||
|
||||
results = []
|
||||
for log in state_logs:
|
||||
results.append({
|
||||
'id': log.id,
|
||||
'state_id': log.state.id,
|
||||
'state_name': log.state.name,
|
||||
'completed_at': log.completed_at,
|
||||
'completed_by': log.completed_by.username if log.completed_by else None,
|
||||
'completed_by_name': getattr(log.completed_by, 'employee', None) and log.completed_by.employee.name or None,
|
||||
'is_cancelled': log.is_cancelled,
|
||||
'cancelled_at': log.cancelled_at,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'count': len(results),
|
||||
'results': results
|
||||
})
|
||||
|
||||
@action(detail=True, methods=['get'], url_path='timeline')
|
||||
def timeline(self, request, pk=None):
|
||||
"""
|
||||
获取流程时间线
|
||||
|
||||
返回该开版订单的完整流程时间线,包括未开始、进行中和已完成的状态
|
||||
不包含已撤销的流程记录
|
||||
"""
|
||||
plate_order = self.get_object()
|
||||
|
||||
if not plate_order.business_object:
|
||||
return Response({
|
||||
'count': 0,
|
||||
'results': []
|
||||
})
|
||||
|
||||
from stateflow import services as stateflow_services
|
||||
|
||||
# 获取流程
|
||||
from stateflow.models import Process
|
||||
try:
|
||||
process = Process.objects.get(id=plate_order.process)
|
||||
except Process.DoesNotExist:
|
||||
return Response({
|
||||
'count': 0,
|
||||
'results': []
|
||||
})
|
||||
|
||||
timeline_data = stateflow_services.get_business_object_state_timeline(plate_order.business_object)
|
||||
|
||||
# 过滤掉已撤销的记录
|
||||
timeline_data = [item for item in timeline_data if not item.get('is_cancelled')]
|
||||
|
||||
results = []
|
||||
for item in timeline_data:
|
||||
results.append({
|
||||
'state_id': item['state'].id,
|
||||
'state_name': item['state'].name,
|
||||
'state_description': item['state'].description,
|
||||
'order': item['order'],
|
||||
'status': item['status'], # not_started, in_progress, completed
|
||||
'completed_at': item['completed_at'],
|
||||
'completed_by': item['completed_by'].username if item['completed_by'] else None,
|
||||
'completed_by_name': getattr(item['completed_by'], 'employee', None) and item['completed_by'].employee.name or None if item['completed_by'] else None,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'count': len(results),
|
||||
'results': results
|
||||
})
|
||||
|
||||
@@ -75,6 +75,8 @@ GET /api/v1/plate-orders/
|
||||
"required_completion_date": "2025-11-20",
|
||||
"completion_date": null,
|
||||
"is_ordered": false,
|
||||
"process": 1,
|
||||
"process_name": "开版流程",
|
||||
"status": "进行中",
|
||||
"progress_percentage": 50,
|
||||
"created_at": "2025-11-10T09:00:00Z",
|
||||
@@ -138,6 +140,8 @@ GET /api/v1/plate-orders/{id}/
|
||||
"approval_result": "通过",
|
||||
"is_ordered": false,
|
||||
"customer_feedback": "满意",
|
||||
"process": 1,
|
||||
"process_name": "开版流程",
|
||||
"status": "进行中",
|
||||
"status_id": 2,
|
||||
"is_completed": false,
|
||||
@@ -174,7 +178,8 @@ Content-Type: application/json
|
||||
"required_completion_date": "2025-11-25",
|
||||
"is_mark_frame": false,
|
||||
"plate_method": "机器",
|
||||
"production_method": "小批量"
|
||||
"production_method": "小批量",
|
||||
"process": 1
|
||||
}
|
||||
```
|
||||
|
||||
@@ -210,6 +215,7 @@ Content-Type: application/json
|
||||
- `approval_result`: 审批结果
|
||||
- `is_ordered`: 是否已下单
|
||||
- `customer_feedback`: 客户反馈
|
||||
- `process`: 流程ID(如果不传递则使用默认流程)
|
||||
|
||||
**响应**
|
||||
- 状态码: `201 Created`
|
||||
@@ -366,6 +372,191 @@ POST /api/v1/plate-orders/{id}/activate/
|
||||
|
||||
---
|
||||
|
||||
### 9. 推进到下一个状态
|
||||
|
||||
**请求**
|
||||
```
|
||||
POST /api/v1/plate-orders/{id}/advance-to-next-state/
|
||||
```
|
||||
|
||||
**权限要求**: 需要登录和相应权限
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"detail": "已推进到下一个状态",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"status": "调色",
|
||||
"status_id": 2,
|
||||
"progress_percentage": 33,
|
||||
// ... 其他字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
1. 没有流程实例
|
||||
```json
|
||||
{
|
||||
"detail": "该开版订单没有关联的流程实例"
|
||||
}
|
||||
```
|
||||
- 状态码: `400 Bad Request`
|
||||
|
||||
2. 流程已完成或其他错误
|
||||
```json
|
||||
{
|
||||
"detail": "流程已完成,无法继续推进"
|
||||
}
|
||||
```
|
||||
- 状态码: `400 Bad Request`
|
||||
|
||||
---
|
||||
|
||||
### 10. 回退一步
|
||||
|
||||
**请求**
|
||||
```
|
||||
POST /api/v1/plate-orders/{id}/step-back-one-state/
|
||||
```
|
||||
|
||||
**权限要求**: 需要登录和相应权限
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"detail": "已回退到上一个状态",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"status": "画图",
|
||||
"status_id": 1,
|
||||
"progress_percentage": 0,
|
||||
// ... 其他字段
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应**
|
||||
|
||||
1. 没有流程实例
|
||||
```json
|
||||
{
|
||||
"detail": "该开版订单没有关联的流程实例"
|
||||
}
|
||||
```
|
||||
- 状态码: `400 Bad Request`
|
||||
|
||||
2. 没有可回退的状态
|
||||
```json
|
||||
{
|
||||
"detail": "没有可回退的状态"
|
||||
}
|
||||
```
|
||||
- 状态码: `400 Bad Request`
|
||||
|
||||
---
|
||||
|
||||
### 11. 查询已完成的流程列表
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/v1/plate-orders/{id}/completed-states/
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| include_cancelled | boolean | 否 | 是否包含已撤销的流程(true/false),默认 false |
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"count": 2,
|
||||
"results": [
|
||||
{
|
||||
"id": 1,
|
||||
"state_id": 1,
|
||||
"state_name": "画图",
|
||||
"completed_at": "2025-11-11T10:00:00Z",
|
||||
"completed_by": "user123",
|
||||
"completed_by_name": "张三",
|
||||
"is_cancelled": false,
|
||||
"cancelled_at": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"state_id": 2,
|
||||
"state_name": "调色",
|
||||
"completed_at": "2025-11-12T15:30:00Z",
|
||||
"completed_by": "user456",
|
||||
"completed_by_name": "李四",
|
||||
"is_cancelled": false,
|
||||
"cancelled_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 12. 获取流程时间线
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/v1/plate-orders/{id}/timeline/
|
||||
```
|
||||
|
||||
**说明**: 返回完整的流程时间线,包括未开始、进行中和已完成的状态,不包含已撤销的记录
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"state_id": 1,
|
||||
"state_name": "画图",
|
||||
"state_description": "设计图纸绘制",
|
||||
"order": 0,
|
||||
"status": "completed",
|
||||
"completed_at": "2025-11-11T10:00:00Z",
|
||||
"completed_by": "user123",
|
||||
"completed_by_name": "张三"
|
||||
},
|
||||
{
|
||||
"state_id": 2,
|
||||
"state_name": "调色",
|
||||
"state_description": "颜色调配",
|
||||
"order": 1,
|
||||
"status": "in_progress",
|
||||
"completed_at": null,
|
||||
"completed_by": null,
|
||||
"completed_by_name": null
|
||||
},
|
||||
{
|
||||
"state_id": 3,
|
||||
"state_name": "套样",
|
||||
"state_description": "样品套制",
|
||||
"order": 2,
|
||||
"status": "not_started",
|
||||
"completed_at": null,
|
||||
"completed_by": null,
|
||||
"completed_by_name": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**状态说明**
|
||||
- `not_started`: 未开始
|
||||
- `in_progress`: 进行中
|
||||
- `completed`: 已完成
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### PlateOrder(开版订单)
|
||||
@@ -387,6 +578,7 @@ POST /api/v1/plate-orders/{id}/activate/
|
||||
| default_address | string | 默认地址 |
|
||||
| salesperson | FK | 业务员(外键) |
|
||||
| merchandiser | FK | 跟单员(外键) |
|
||||
| process | integer | 流程ID(无外键约束) |
|
||||
| style_name | string | 款式名称 |
|
||||
| fabric | string | 面料 |
|
||||
| width | string | 幅宽 |
|
||||
@@ -446,7 +638,8 @@ curl -X POST "http://api.example.com/api/v1/plate-orders/" \
|
||||
"style_name": "新款印染",
|
||||
"fabric": "纯棉",
|
||||
"urgency_level": "加急",
|
||||
"required_completion_date": "2025-11-20"
|
||||
"required_completion_date": "2025-11-20",
|
||||
"process": 1
|
||||
}'
|
||||
```
|
||||
|
||||
@@ -464,6 +657,27 @@ curl -X GET "http://api.example.com/api/v1/plate-orders/?plate_date_from=2025-11
|
||||
-H "Authorization: Token your-auth-token"
|
||||
```
|
||||
|
||||
### 示例5: 推进流程状态
|
||||
|
||||
```bash
|
||||
curl -X POST "http://api.example.com/api/v1/plate-orders/1/advance-to-next-state/" \
|
||||
-H "Authorization: Token your-auth-token"
|
||||
```
|
||||
|
||||
### 示例6: 回退流程状态
|
||||
|
||||
```bash
|
||||
curl -X POST "http://api.example.com/api/v1/plate-orders/1/step-back-one-state/" \
|
||||
-H "Authorization: Token your-auth-token"
|
||||
```
|
||||
|
||||
### 示例7: 获取流程时间线
|
||||
|
||||
```bash
|
||||
curl -X GET "http://api.example.com/api/v1/plate-orders/1/timeline/" \
|
||||
-H "Authorization: Token your-auth-token"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误代码
|
||||
@@ -487,11 +701,30 @@ curl -X GET "http://api.example.com/api/v1/plate-orders/?plate_date_from=2025-11
|
||||
3. **日期格式**: 所有日期字段使用 ISO 8601 格式(YYYY-MM-DD 或 YYYY-MM-DDTHH:MM:SSZ)
|
||||
4. **分页**: 列表接口默认支持分页,使用 `limit` 和 `offset` 参数控制
|
||||
5. **文件上传**: `plate_image` 字段需要使用 multipart/form-data 格式上传
|
||||
6. **自动创建流程实例**: 创建开版订单时,如果提供了 `process` 字段,系统会自动创建对应的 BusinessObject 流程实例。如果不提供则使用默认流程
|
||||
7. **流程验证**: 提供的 `process` ID 必须在系统中存在,否则会返回验证错误
|
||||
8. **流程操作**:
|
||||
- 推进/回退操作需要订单已关联 BusinessObject
|
||||
- timeline 接口不包含已撤销的流程记录
|
||||
- completed-states 接口默认不包含已撤销记录,可通过 `include_cancelled=true` 参数包含
|
||||
|
||||
---
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.2.0 (2025-11-14)
|
||||
- 添加流程控制接口:
|
||||
- `advance-to-next-state`: 推进到下一个状态
|
||||
- `step-back-one-state`: 回退一步
|
||||
- `completed-states`: 查询已完成的流程列表
|
||||
- `timeline`: 获取流程时间线
|
||||
|
||||
### v1.1.0 (2025-11-14)
|
||||
- 添加 `process` 字段:支持指定流程ID
|
||||
- 添加 `process_name` 只读字段:自动返回流程名称
|
||||
- 自动创建 BusinessObject:创建订单时根据 process 自动创建流程实例
|
||||
- 流程验证:确保提供的流程ID有效
|
||||
|
||||
### v1.0.0 (2025-11-13)
|
||||
- 初始版本
|
||||
- 支持完整的 CRUD 操作
|
||||
|
||||
@@ -245,3 +245,4 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Printing module settings
|
||||
PRINTING_DEFAULT_PROCESS_ID = 12 # 默认印染流程ID
|
||||
PLATE_ORDER_DEFAULT_PROCESS_ID = 14 # 默认开版流程ID
|
||||
|
||||
@@ -115,17 +115,7 @@ class PlateOrderAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.display(description='进度')
|
||||
def progress_display(self, obj):
|
||||
progress = obj.progress_percentage
|
||||
if progress >= 100:
|
||||
color = 'green'
|
||||
elif progress >= 50:
|
||||
color = 'orange'
|
||||
else:
|
||||
color = 'gray'
|
||||
return format_html(
|
||||
'<span style="color: {}; font-weight: bold;">{:.1f}%</span>',
|
||||
color, progress
|
||||
)
|
||||
return obj.progress_percentage
|
||||
|
||||
@admin.display(description='当前状态')
|
||||
def status_display(self, obj):
|
||||
@@ -139,13 +129,13 @@ class PlateOrderAdmin(admin.ModelAdmin):
|
||||
|
||||
for obj in queryset:
|
||||
if not obj.business_object:
|
||||
messages.append(f"{obj.plate_code}: 没有关联的流程实例,无法推进")
|
||||
messages.append(f"{obj.id}: 没有关联的流程实例,无法推进")
|
||||
continue
|
||||
|
||||
success, message = services.advance_to_next_state(obj.business_object, request.user)
|
||||
if success:
|
||||
success_count += 1
|
||||
messages.append(f"{obj.plate_code}: {message}")
|
||||
messages.append(f"{obj.id}: {message}")
|
||||
|
||||
self.message_user(request, f'成功推进 {success_count}/{queryset.count()} 个开版订单。详情: {"; ".join(messages[:5])}')
|
||||
|
||||
@@ -157,13 +147,13 @@ class PlateOrderAdmin(admin.ModelAdmin):
|
||||
|
||||
for obj in queryset:
|
||||
if not obj.business_object:
|
||||
messages.append(f"{obj.plate_code}: 没有关联的流程实例,无法回退")
|
||||
messages.append(f"{obj.id}: 没有关联的流程实例,无法回退")
|
||||
continue
|
||||
|
||||
success, message = services.step_back_one_state(obj.business_object, request.user)
|
||||
if success:
|
||||
success_count += 1
|
||||
messages.append(f"{obj.plate_code}: {message}")
|
||||
messages.append(f"{obj.id}: {message}")
|
||||
|
||||
self.message_user(request, f'成功回退 {success_count}/{queryset.count()} 个开版订单。详情: {"; ".join(messages[:5])}')
|
||||
|
||||
|
||||
18
printing/migrations/0012_plateorder_process.py
Normal file
18
printing/migrations/0012_plateorder_process.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-14 06:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('printing', '0011_alter_plateorder_options_plateorder_is_invalid'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='plateorder',
|
||||
name='process',
|
||||
field=models.IntegerField(default=14, verbose_name='关联流程'),
|
||||
),
|
||||
]
|
||||
@@ -87,6 +87,7 @@ class PlateOrder(ModelBase):
|
||||
customer_feedback = models.TextField(blank=True, null=True, verbose_name='客户修改意见')
|
||||
|
||||
# 流程管理
|
||||
process = models.IntegerField(default=settings.PLATE_ORDER_DEFAULT_PROCESS_ID, verbose_name='关联流程')
|
||||
business_object = models.OneToOneField(
|
||||
stateflow_models.BusinessObject,
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -97,7 +98,30 @@ class PlateOrder(ModelBase):
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.human_id or f'PlateOrder-{self.id}'
|
||||
return f'PlateOrder-{self.id}'
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""保存时自动创建 BusinessObject"""
|
||||
# 先保存以获取 ID(如果是新建)
|
||||
is_new = self.pk is None
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# 如果有 process 且没有 business_object,则自动创建
|
||||
if self.process and not self.business_object:
|
||||
try:
|
||||
from stateflow.models import Process
|
||||
process_obj = Process.objects.get(id=self.process)
|
||||
|
||||
# 创建 BusinessObject
|
||||
business_object = stateflow_models.BusinessObject.objects.create(
|
||||
process=process_obj
|
||||
)
|
||||
self.business_object = business_object
|
||||
# 再次保存以更新 business_object 字段
|
||||
super().save(update_fields=['business_object'])
|
||||
except Process.DoesNotExist:
|
||||
# 如果流程不存在,忽略错误
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
verbose_name = '开版订单'
|
||||
|
||||
Reference in New Issue
Block a user