1
0
forked from erp-dev/erp
Files
erpnew/api_v2/tests.py
2026-01-07 16:33:33 +08:00

1880 lines
76 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from decimal import Decimal
import datetime
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.test import TestCase
from django.utils import timezone
from rest_framework.test import APIClient, APIRequestFactory
from basic_info import models as basic_models
from printing import models as printing_models
from business import models as business_models
from api_v2.views.printing import PrintingJobByCustomerView
from django.contrib.contenttypes.models import ContentType
from stateflow import models as stateflow_models
class QuickCreateEmployeeUserAPITest(TestCase):
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='测试商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.admin_user = get_user_model().objects.create_user(username='admin', password='pass12345')
self.admin_employee = basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.admin_user,
name='管理员',
)
self.client.force_authenticate(user=self.admin_user)
self.url = '/api/v2/users/quick-create/'
self.payload = {
'username': 'new_user',
'password': 'strongPass#1',
'display_name': '新员工',
'merchant_id': self.merchant.id,
'mobile': '13800000000',
}
def test_quick_create_employee_user_success(self):
response = self.client.post(self.url, self.payload, format='json')
self.assertEqual(response.status_code, 201)
data = response.data
self.assertIn('user', data)
self.assertIn('employee', data)
self.assertEqual(data['user']['username'], self.payload['username'])
created_user = get_user_model().objects.get(username=self.payload['username'])
self.assertEqual(created_user.employee.merchant, self.merchant)
self.assertEqual(created_user.employee.name, self.payload['display_name'])
def test_quick_create_employee_user_duplicate_username(self):
get_user_model().objects.create_user(username=self.payload['username'], password='pass12345')
response = self.client.post(self.url, self.payload, format='json')
self.assertEqual(response.status_code, 400)
self.assertIn('用户名已存在', str(response.data))
def test_quick_create_employee_user_invalid_merchant(self):
payload = {**self.payload, 'merchant_id': 9999}
response = self.client.post(self.url, payload, format='json')
self.assertEqual(response.status_code, 400)
self.assertIn('商户不存在', str(response.data))
def test_quick_create_employee_user_with_role(self):
"""测试创建用户时指定角色"""
role = Group.objects.create(name='管理员')
payload = {**self.payload, 'role_id': role.id}
response = self.client.post(self.url, payload, format='json')
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data['user']['role_id'], role.id)
created_user = get_user_model().objects.get(username=self.payload['username'])
self.assertIn(role, created_user.groups.all())
def test_quick_create_employee_user_with_invalid_role(self):
"""测试创建用户时指定不存在的角色"""
payload = {**self.payload, 'role_id': 99999}
response = self.client.post(self.url, payload, format='json')
self.assertEqual(response.status_code, 400)
self.assertIn('角色不存在', str(response.data))
class RoleListV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(username='test_user', password='pass12345')
self.client.force_authenticate(user=self.user)
self.url = '/api/v2/roles/'
def test_role_list_returns_all_roles(self):
"""测试获取所有角色"""
role1 = Group.objects.create(name='管理员')
role2 = Group.objects.create(name='普通用户')
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.data), 2)
names = [r['name'] for r in response.data]
self.assertIn('管理员', names)
self.assertIn('普通用户', names)
# 检查返回字段只有 id 和 name
for role in response.data:
self.assertEqual(set(role.keys()), {'id', 'name'})
def test_role_list_empty(self):
"""测试没有角色时返回空列表"""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, [])
def test_role_list_requires_authentication(self):
"""测试未认证用户无法访问"""
client = APIClient()
response = client.get(self.url)
self.assertEqual(response.status_code, 401)
class HealthCheckV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
def test_health_check_returns_200(self):
resp = self.client.get('/api/v2/health/')
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.get('status'), 'ok')
class PrintingJobByCustomerAPITest(TestCase):
def setUp(self):
self.factory = APIRequestFactory()
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='印染商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户X',
created_by=None,
)
category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name='品类',
product_prefix='FAB',
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
category=category,
name='产品A',
human_id='FAB-001',
width_size=Decimal('150.00'),
color='红色',
unit=basic_models.ProductUnitEnum.METER,
)
self.other_product = basic_models.Product.objects.create(
merchant=self.merchant,
category=category,
name='产品B',
human_id='FAB-002',
width_size=Decimal('160.00'),
color='蓝色',
unit=basic_models.ProductUnitEnum.METER,
)
self.printing_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='',
width='150cm',
)
self.printing_order_other = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='',
width='160cm',
)
tz = timezone.get_default_timezone()
in_range = timezone.make_aware(datetime.datetime(2025, 12, 5, 10, 0, 0), tz)
out_range = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)
self.job_in_range = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=10,
unit='',
)
printing_models.PrintingJob.objects.filter(id=self.job_in_range.id).update(created_at=in_range)
self.job_out_range = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order_other,
product=self.other_product,
quantity=20,
unit='',
)
printing_models.PrintingJob.objects.filter(id=self.job_out_range.id).update(created_at=out_range)
# 关联销售单,验证 billed_quantity
warehouse = basic_models.WareHouse.objects.create(
merchant=self.merchant,
name='仓库A',
mode=basic_models.WareHouseModeEnum.UNRESTRICTED,
)
operator = basic_models.Employee.objects.create(
merchant=self.merchant,
name='操作员',
)
sales_order = business_models.SalesOrder.objects.create(
merchant=self.merchant,
customer=self.customer,
sales_date=datetime.date(2025, 12, 5),
operator=operator,
warehouse=warehouse,
)
business_models.SalesOrderItem.objects.create(
sales_order=sales_order,
product=self.product,
price=Decimal('10'),
quantity=Decimal('12.5'),
unit='',
empty_diff_percent=Decimal('0'),
num_of_rolls=1,
printing_job=self.job_in_range,
)
business_models.SalesOrderItem.objects.create(
sales_order=sales_order,
product=self.product,
price=Decimal('8'),
quantity=Decimal('7.5'),
unit='',
empty_diff_percent=Decimal('0'),
num_of_rolls=1,
printing_job=self.job_in_range,
)
self.view = PrintingJobByCustomerView.as_view()
def _get(self, params):
request = self.factory.get('/api/v2/printing/jobs/', params)
return self.view(request)
def test_basic_date_and_customer_filter(self):
resp = self._get({
'customer_id': self.customer.id,
'date_from': '2025-12-01',
'date_to': '2025-12-10',
})
self.assertEqual(resp.status_code, 200)
self.assertEqual(len(resp.data), 1)
self.assertEqual(resp.data[0]['id'], self.job_in_range.id)
self.assertEqual(resp.data[0]['billed_quantity'], '20.00')
def test_filter_by_printing_order(self):
resp = self._get({
'customer_id': self.customer.id,
'date_from': '2025-12-01',
'date_to': '2025-12-10',
'printing_order': self.printing_order.id,
})
self.assertEqual(len(resp.data), 1)
self.assertEqual(resp.data[0]['printing_order'], self.printing_order.id)
def test_filter_by_product_fields(self):
resp = self._get({
'customer_id': self.customer.id,
'date_from': '2025-12-01',
'date_to': '2025-12-10',
'product_id': self.product.id,
'product_name': '产品A',
'product_human_id': 'FAB-001',
'product_width_size': '150',
'product_color': '',
})
self.assertEqual(len(resp.data), 1)
data = resp.data[0]
self.assertEqual(data['product']['id'], self.product.id)
self.assertEqual(data['billed_quantity'], '20.00')
class PrintingJobBatchAdvanceV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
# 工厂用户(满足 IsPrintingFactory
self.merchant = basic_models.Merchant.objects.create(
name='印染工厂',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='factory_user', password='pass12345')
self.employee = basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='工厂员工',
)
self.client.force_authenticate(user=self.user)
# 基础客户/产品
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户A',
created_by=None,
)
category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name='品类',
product_prefix='FAB',
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
category=category,
name='产品A',
human_id='FAB-100',
width_size=Decimal('150.00'),
color='红色',
unit=basic_models.ProductUnitEnum.METER,
)
# stateflow流程 + 3个节点 + 节点参数(第一步必填)
self.state1 = stateflow_models.State.objects.create(name='待处理1', description='第一个节点')
self.state2 = stateflow_models.State.objects.create(name='待处理2', description='第二个节点')
self.state3 = stateflow_models.State.objects.create(name='待处理3', description='第三个节点')
self.param_required = stateflow_models.StateParameter.objects.create(
key='temperature',
value='',
is_required=True,
description='温度(必填)',
)
self.state1.parameters.add(self.param_required)
self.process = stateflow_models.Process.objects.create(name='印染流程')
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)
# printing订单 + 两条明细(每条明细都有 business_object
self.printing_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='',
width='150cm',
process=self.process,
)
bo1 = stateflow_models.BusinessObject.objects.create(
name='BO-1',
process=self.process,
)
bo2 = stateflow_models.BusinessObject.objects.create(
name='BO-2',
process=self.process,
)
self.job1 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=10,
unit='',
business_object=bo1,
)
self.job2 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=20,
unit='',
business_object=bo2,
)
def test_preview_success_returns_next_state_and_parameters(self):
resp = self.client.post(
'/api/v2/printing-jobs/batch-advance/preview/',
{'printing_job_ids': [self.job1.id, self.job2.id]},
format='json'
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['printing_order_id'], self.printing_order.id)
self.assertEqual(set(resp.data['printing_job_ids']), {self.job1.id, self.job2.id})
next_state = resp.data['next_state']
self.assertEqual(next_state['id'], self.state1.id)
self.assertEqual(next_state['order'], 0)
self.assertTrue(any(p['key'] == 'temperature' and p['is_required'] for p in next_state['parameters']))
def test_preview_fails_when_jobs_not_same_printing_order(self):
other_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='',
width='160cm',
process=self.process,
)
bo3 = stateflow_models.BusinessObject.objects.create(name='BO-3', process=self.process)
job3 = printing_models.PrintingJob.objects.create(
printing_order=other_order,
product=self.product,
quantity=5,
unit='',
business_object=bo3,
)
resp = self.client.post(
'/api/v2/printing-jobs/batch-advance/preview/',
{'printing_job_ids': [self.job1.id, job3.id]},
format='json'
)
self.assertEqual(resp.status_code, 400)
self.assertIn('printing_order', resp.data.get('detail', ''))
def test_submit_success_advances_all_and_creates_batch_record(self):
resp = self.client.post(
'/api/v2/printing-jobs/batch-advance/',
{
'printing_job_ids': [self.job1.id, self.job2.id],
'parameters': {'temperature': '25.5'}
},
format='json'
)
self.assertEqual(resp.status_code, 200)
self.assertIn('batch_id', resp.data)
# stateflow两条 business_object 都应生成 1 条完成日志,且完成的是 state1
self.job1.refresh_from_db()
self.job2.refresh_from_db()
# 同时应修复绑定BusinessObject.content_type/object_id 指向 PrintingJob
from django.contrib.contenttypes.models import ContentType
ct_job = ContentType.objects.get_for_model(printing_models.PrintingJob)
self.job1.business_object.refresh_from_db()
self.job2.business_object.refresh_from_db()
self.assertEqual(self.job1.business_object.content_type_id, ct_job.id)
self.assertEqual(self.job1.business_object.object_id, self.job1.id)
self.assertEqual(self.job2.business_object.content_type_id, ct_job.id)
self.assertEqual(self.job2.business_object.object_id, self.job2.id)
self.assertEqual(self.job1.business_object.state_logs.filter(is_cancelled=False).count(), 1)
self.assertEqual(self.job2.business_object.state_logs.filter(is_cancelled=False).count(), 1)
self.assertEqual(self.job1.business_object.state_logs.first().state_id, self.state1.id)
self.assertEqual(self.job2.business_object.state_logs.first().state_id, self.state1.id)
# printing批量记录应存在且关联 jobs
record = printing_models.PrintingJobBatchAdvanceRecord.objects.get(id=resp.data['batch_id'])
self.assertEqual(record.created_by_id, self.user.id)
self.assertEqual(record.printing_order_id, self.printing_order.id)
self.assertEqual(record.state_id, self.state1.id)
self.assertEqual(record.parameters.get('temperature'), '25.5')
self.assertEqual(set(record.printing_jobs.values_list('id', flat=True)), {self.job1.id, self.job2.id})
def test_submit_fails_when_missing_required_parameter(self):
resp = self.client.post(
'/api/v2/printing-jobs/batch-advance/',
{
'printing_job_ids': [self.job1.id, self.job2.id],
'parameters': {} # 缺少 temperature
},
format='json'
)
self.assertEqual(resp.status_code, 400)
self.assertIn('缺失必填参数', resp.data.get('detail', ''))
# 未产生任何日志/批量记录
self.assertEqual(self.job1.business_object.state_logs.count(), 0)
self.assertEqual(self.job2.business_object.state_logs.count(), 0)
self.assertEqual(printing_models.PrintingJobBatchAdvanceRecord.objects.count(), 0)
class BusinessObjectCloneV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
self.user = get_user_model().objects.create_user(username='u1', password='pass12345')
self.client.force_authenticate(user=self.user)
self.state1 = stateflow_models.State.objects.create(name='S1')
self.state2 = stateflow_models.State.objects.create(name='S2')
self.process = stateflow_models.Process.objects.create(name='P1')
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)
self.process_src = stateflow_models.Process.objects.create(name='P-src')
self.process_dst = stateflow_models.Process.objects.create(name='P-dst')
ct = ContentType.objects.get_for_model(stateflow_models.Process)
self.bo = stateflow_models.BusinessObject.objects.create(
name='BO',
process=self.process,
content_type=ct,
object_id=self.process_src.id,
)
# 创建一条日志 + 参数记录,确保“克隆包含工艺参数”
log = stateflow_models.StateFlowRecord.objects.create(
business_object=self.bo,
state=self.state1,
completed_by=self.user,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log,
parameters={'temperature': '25.5'},
remark='r1',
)
def test_clone_business_object_api_returns_new_id(self):
new_object_id = self.process_dst.id
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': self.bo.id,
'content_type': self.bo.content_type_id,
'object_id': new_object_id,
},
format='json'
)
self.assertEqual(resp.status_code, 200)
self.assertIn('business_object_id', resp.data)
new_id = resp.data['business_object_id']
self.assertNotEqual(new_id, self.bo.id)
cloned = stateflow_models.BusinessObject.objects.get(id=new_id)
self.assertEqual(cloned.process_id, self.bo.process_id)
self.assertEqual(cloned.state_logs.count(), self.bo.state_logs.count())
self.assertEqual(cloned.content_type_id, self.bo.content_type_id)
self.assertEqual(cloned.object_id, new_object_id)
def test_clone_business_object_api_binds_to_plate_order_business_object(self):
"""
printing 工作流依赖:新建 PlateOrder 后通过 clone API 复制流程进度,
后端应自动把克隆后的 BusinessObject 绑定到目标 PlateOrder.business_object。
"""
merchant = basic_models.Merchant.objects.create(
name='M1',
type=basic_models.MerchantTypeEnum.FACTORY,
)
customer = basic_models.Customer.objects.create(
merchant=merchant,
name='C1',
created_by=None,
)
# source PlateOrder会自动创建 business_object
source_po = printing_models.PlateOrder.objects.create(
customer=customer,
process=self.process.id,
design_code='SRC',
style_name='S',
)
self.assertIsNotNone(source_po.business_object_id)
# 给 source 写一条日志 + 参数,确保克隆后能看到“节点与参数”
log = stateflow_models.StateFlowRecord.objects.create(
business_object=source_po.business_object,
state=self.state1,
completed_by=self.user,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log,
parameters={'temperature': '25.5'},
remark='r1',
)
# target PlateOrder创建时也会自动创建一个“空 business_object”
target_po = printing_models.PlateOrder.objects.create(
customer=customer,
process=self.process.id,
design_code='DST',
style_name='D',
)
self.assertIsNotNone(target_po.business_object_id)
old_target_bo_id = target_po.business_object_id
ct_po = ContentType.objects.get_for_model(printing_models.PlateOrder)
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': source_po.business_object_id,
'content_type': ct_po.id,
'object_id': target_po.id,
},
format='json'
)
self.assertEqual(resp.status_code, 200, resp.data)
new_bo_id = resp.data['business_object_id']
target_po.refresh_from_db()
self.assertEqual(target_po.business_object_id, new_bo_id)
# 目标 PlateOrder 的旧“空 BO”应被安全清理避免残留多份绑定
self.assertFalse(stateflow_models.BusinessObject.objects.filter(id=old_target_bo_id).exists())
# 克隆后的日志与参数应存在
self.assertEqual(target_po.business_object.state_logs.count(), 1)
cloned_log = target_po.business_object.state_logs.first()
self.assertEqual(cloned_log.state_id, self.state1.id)
self.assertEqual(cloned_log.parameter_records.count(), 1)
self.assertEqual(cloned_log.parameter_records.first().parameters.get('temperature'), '25.5')
def test_clone_business_object_api_404(self):
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': 999999,
'content_type': self.bo.content_type_id,
'object_id': 222,
},
format='json'
)
self.assertEqual(resp.status_code, 404)
def test_clone_business_object_api_rejects_mismatched_content_type(self):
# 用另一个 ContentType比如 State
ct_state = ContentType.objects.get_for_model(stateflow_models.State)
self.assertNotEqual(ct_state.id, self.bo.content_type_id)
resp = self.client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': self.bo.id,
'content_type': ct_state.id,
'object_id': 222,
},
format='json'
)
self.assertEqual(resp.status_code, 400)
self.assertIn('content_type', resp.data.get('detail', ''))
def test_clone_business_object_api_unauthorized(self):
client = APIClient()
resp = client.post(
'/api/v2/stateflow/business-objects/clone/',
{
'business_object_id': self.bo.id,
'content_type': self.bo.content_type_id,
'object_id': 222,
},
format='json'
)
self.assertEqual(resp.status_code, 401)
class PlateOrderByProcessNodeV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
# 工厂用户(满足 IsPrintingFactory
self.merchant = basic_models.Merchant.objects.create(
name='开版工厂',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='plate_factory_user', password='pass12345')
basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='开版员工',
)
self.client.force_authenticate(user=self.user)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户P',
created_by=None,
)
# stateflow流程 + 3个节点
self.state1 = stateflow_models.State.objects.create(name='节点1')
self.state2 = stateflow_models.State.objects.create(name='节点2')
self.state3 = stateflow_models.State.objects.create(name='节点3')
self.param2 = stateflow_models.StateParameter.objects.create(key='temperature', value='25')
self.state2.parameters.add(self.param2)
self.process = stateflow_models.Process.objects.create(name='开版流程')
self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# PlateOrder构造不同“下一待执行节点”
self.po_state1 = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-A',
style_name='款式A',
)
self.po_state2_old = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='CODE-X', # 不包含数字,便于测试 search=id
style_name='款式B',
)
stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_state2_old.business_object,
state=self.state1,
completed_by=self.user,
is_cancelled=False,
)
# 订单维度参数:模拟该订单曾在 state2 提交过参数,但该状态记录已撤销(因此当前仍处于 state2 待执行)
cancelled_log = stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_state2_old.business_object,
state=self.state2,
completed_by=self.user,
is_cancelled=True,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=cancelled_log,
parameters={'temperature': '30'},
)
self.po_state2_new = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='ABC-999',
style_name='款式C',
)
stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_state2_new.business_object,
state=self.state1,
completed_by=self.user,
is_cancelled=False,
)
self.po_state3 = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-D',
style_name='款式D',
)
stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_state3.business_object,
state=self.state1,
completed_by=self.user,
is_cancelled=False,
)
stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_state3.business_object,
state=self.state2,
completed_by=self.user,
is_cancelled=False,
)
# 固定 created_at 用于排序断言
tz = timezone.get_default_timezone()
t_old = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
t_new = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
printing_models.PlateOrder.objects.filter(id=self.po_state2_old.id).update(created_at=t_old)
printing_models.PlateOrder.objects.filter(id=self.po_state2_new.id).update(created_at=t_new)
self.url = '/api/v2/plate-orders/by-process-node/'
def test_filter_by_process_node_returns_only_matching_next_pending(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['process_node']['id'], self.node2.id)
self.assertEqual(resp.data['process_node']['state_id'], self.state2.id)
self.assertTrue(any(p['key'] == 'temperature' and p['value'] == '25' for p in resp.data['parameters']))
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(set(ids), {self.po_state2_old.id, self.po_state2_new.id})
# results 必须包含“订单维度”的参数值(同一节点下不同订单可不同)
by_id = {item['id']: item for item in resp.data['results']}
self.assertIn('created_by', by_id[self.po_state2_old.id])
self.assertIn('created_by', by_id[self.po_state2_new.id])
self.assertIn('process_parameters', by_id[self.po_state2_old.id])
self.assertIn('process_parameters', by_id[self.po_state2_new.id])
# po_state2_old来自已撤销的 state2 提交记录
self.assertTrue(
any(p['key'] == 'temperature' and p['value'] == '30' for p in by_id[self.po_state2_old.id]['process_parameters'])
)
# po_state2_new从未提交过 state2 参数value 应为 null
self.assertTrue(
any(p['key'] == 'temperature' and p['value'] is None for p in by_id[self.po_state2_new.id]['process_parameters'])
)
def test_process_parameters_schema_and_keys_align_with_template(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
self.assertEqual(resp.status_code, 200)
template_keys = [p['key'] for p in resp.data['parameters']]
self.assertGreater(len(template_keys), 0)
for item in resp.data['results']:
self.assertIn('process_parameters', item)
self.assertIsInstance(item['process_parameters'], list)
keys = [p.get('key') for p in item['process_parameters']]
self.assertEqual(keys, template_keys)
# 严格 schema每项只能有 key/value 两个字段(便于前端做稳定类型定义)
for p in item['process_parameters']:
self.assertEqual(set(p.keys()), {'key', 'value'})
def test_when_state_has_no_parameters_returns_empty_process_parameters(self):
# node1 对应 state1该测试 setUp 中未配置任何参数
resp = self.client.get(self.url, {'process_node_id': self.node1.id})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['parameters'], [])
self.assertEqual(len(resp.data['results']), 1)
self.assertEqual(resp.data['results'][0]['id'], self.po_state1.id)
self.assertEqual(resp.data['results'][0]['process_parameters'], [])
def test_default_ordering_is_minus_created_at(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id})
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids[0], self.po_state2_new.id)
self.assertEqual(ids[1], self.po_state2_old.id)
def test_ordering_param_created_at_asc(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'ordering': 'created_at'})
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids[0], self.po_state2_old.id)
self.assertEqual(ids[1], self.po_state2_new.id)
def test_search_supports_design_code_icontains(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': 'abc'})
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_state2_new.id])
def test_search_supports_pk_without_extra_param(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'search': str(self.po_state2_old.id)})
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_state2_old.id])
def test_pagination_limit_offset(self):
resp = self.client.get(self.url, {'process_node_id': self.node2.id, 'limit': 1, 'offset': 0})
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['count'], 2)
self.assertEqual(len(resp.data['results']), 1)
class PlateOrderByProcessV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
# 工厂用户(满足 IsPrintingFactory
self.merchant = basic_models.Merchant.objects.create(
name='开版工厂2',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='plate_process_user', password='pass12345')
basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='开版员工2',
)
self.client.force_authenticate(user=self.user)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户PP',
created_by=None,
)
# stateflow流程 + 3个节点其中 2 个节点有参数1 个无参数)
self.state1 = stateflow_models.State.objects.create(name='画图')
self.state2 = stateflow_models.State.objects.create(name='调色')
self.state3 = stateflow_models.State.objects.create(name='套样')
self.param_len = stateflow_models.StateParameter.objects.create(key='length', value=None)
self.param_temp = stateflow_models.StateParameter.objects.create(key='temperature', value=None)
self.state1.parameters.add(self.param_len)
self.state2.parameters.add(self.param_temp)
self.process = stateflow_models.Process.objects.create(name='开版流程2')
self.node1 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=0)
self.node2 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=1)
self.node3 = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state3, order=2)
# PlateOrder同一个 process不同 created_at
self.po_no_exec = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='CODE-X', # 不包含数字,便于测试 plate_order=pk
style_name='款式1',
)
self.po_exec = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-EXEC',
style_name='款式2',
)
# 执行 state1非撤销并提交参数
log1 = stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_exec.business_object,
state=self.state1,
completed_by=self.user,
is_cancelled=False,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log1,
parameters={'length': '5米'},
)
# 执行 state2撤销并提交参数默认不带撤销参数 -> 应视为未执行)
cancelled_log2 = stateflow_models.StateFlowRecord.objects.create(
business_object=self.po_exec.business_object,
state=self.state2,
completed_by=self.user,
is_cancelled=True,
)
stateflow_models.StateLogParameterRecord.objects.create(
state_log=cancelled_log2,
parameters={'temperature': '30'},
)
self.po_out_of_range = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='OUT',
style_name='款式3',
)
# 固定 created_at
tz = timezone.get_default_timezone()
t1 = timezone.make_aware(datetime.datetime(2025, 12, 1, 10, 0, 0), tz)
t2 = timezone.make_aware(datetime.datetime(2025, 12, 2, 10, 0, 0), tz)
t0 = timezone.make_aware(datetime.datetime(2025, 11, 20, 10, 0, 0), tz)
printing_models.PlateOrder.objects.filter(id=self.po_no_exec.id).update(created_at=t1)
printing_models.PlateOrder.objects.filter(id=self.po_exec.id).update(created_at=t2)
printing_models.PlateOrder.objects.filter(id=self.po_out_of_range.id).update(created_at=t0)
self.url = '/api/v2/plate-orders/by-process/'
def test_list_by_process_and_date_range(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['process']['id'], self.process.id)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(set(ids), {self.po_no_exec.id, self.po_exec.id})
# 序列化器字段created_by 必须存在(允许为 null
for item in resp.data['results']:
self.assertIn('created_by', item)
def test_default_ordering_is_minus_created_at(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids[0], self.po_exec.id)
self.assertEqual(ids[1], self.po_no_exec.id)
def test_plate_order_param_supports_design_code_icontains(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'plate_order': 'exec',
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_exec.id])
def test_plate_order_param_supports_pk_without_extra_param(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'plate_order': str(self.po_no_exec.id),
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_no_exec.id])
def test_process_params_includes_all_nodes_and_is_executed(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
}
)
self.assertEqual(resp.status_code, 200)
by_id = {item['id']: item for item in resp.data['results']}
po_exec = by_id[self.po_exec.id]
po_no_exec = by_id[self.po_no_exec.id]
# 每条 PlateOrder 都必须有 process_params且包含所有节点按 order
self.assertIn('process_params', po_exec)
self.assertEqual([n['order'] for n in po_exec['process_params']], [0, 1, 2])
self.assertEqual([n['node_name'] for n in po_exec['process_params']], ['画图', '调色', '套样'])
# po_execstate1 已执行state2 只有撤销记录 -> 默认视为未执行state3 未执行
n1, n2, n3 = po_exec['process_params']
self.assertTrue(n1['is_executed'])
self.assertFalse(n2['is_executed'])
self.assertFalse(n3['is_executed'])
self.assertEqual([p['key'] for p in n1['params']], ['length'])
self.assertEqual(n1['params'][0]['value'], '5米')
self.assertEqual([p['key'] for p in n2['params']], ['temperature'])
self.assertIsNone(n2['params'][0]['value'])
self.assertEqual(n3['params'], [])
# po_no_exec全部未执行参数 value 全为 null
self.assertEqual([n['order'] for n in po_no_exec['process_params']], [0, 1, 2])
self.assertFalse(po_no_exec['process_params'][0]['is_executed'])
self.assertIsNone(po_no_exec['process_params'][0]['params'][0]['value'])
def test_invalid_ordering_returns_400(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'date_from': '2025-12-01',
'date_to': '2025-12-02',
'ordering': 'unknown',
}
)
self.assertEqual(resp.status_code, 400)
class PlateOrderByStateStatusV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='开版工厂3',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='state_status_user', password='pass12345')
basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='开版员工3',
)
self.client.force_authenticate(user=self.user)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户SS',
created_by=None,
)
self.state_prepare = stateflow_models.State.objects.create(name='画图')
self.state_target = stateflow_models.State.objects.create(name='调色')
self.state_unlinked = stateflow_models.State.objects.create(name='未绑定')
self.param_temp = stateflow_models.StateParameter.objects.create(key='temperature', value='25')
self.state_target.parameters.add(self.param_temp)
self.process = stateflow_models.Process.objects.create(name='主流程')
self.node_prepare = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state_prepare, order=0)
self.node_target = stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state_target, order=1)
self.other_process = stateflow_models.Process.objects.create(name='其他流程')
stateflow_models.ProcessNode.objects.create(process=self.other_process, state=self.state_target, order=0)
self.po_not_started = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-NOT',
style_name='款式N',
)
self.po_no_logs = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-ZERO',
style_name='款式Z',
)
self.po_completed_old = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-C-OLD',
style_name='款式CO',
)
self.po_completed_new = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-C-NEW',
style_name='款式CN',
)
self.po_cancelled = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.process.id,
design_code='PO-CAN',
style_name='款式CA',
)
self.po_other_process = printing_models.PlateOrder.objects.create(
customer=self.customer,
process=self.other_process.id,
design_code='PO-OTHER',
style_name='款式OP',
)
self._complete_state(self.po_not_started, self.state_prepare)
self._complete_state(self.po_completed_old, self.state_prepare)
self._complete_state(self.po_completed_new, self.state_prepare)
self._complete_state(self.po_cancelled, self.state_prepare)
self._complete_state(self.po_completed_old, self.state_target, parameters={'temperature': '28'})
self._complete_state(self.po_completed_new, self.state_target, parameters={'temperature': '32'})
self._complete_state(self.po_cancelled, self.state_target, is_cancelled=True, parameters={'temperature': '99'})
self._complete_state(self.po_other_process, self.state_target, parameters={'temperature': '88'})
tz = timezone.get_default_timezone()
t_old = timezone.make_aware(datetime.datetime(2025, 12, 1, 9, 0, 0), tz)
t_new = timezone.make_aware(datetime.datetime(2025, 12, 3, 9, 0, 0), tz)
printing_models.PlateOrder.objects.filter(id=self.po_completed_old.id).update(created_at=t_old)
printing_models.PlateOrder.objects.filter(id=self.po_completed_new.id).update(created_at=t_new)
self.url = '/api/v2/plate-orders/by-state-status/'
def _complete_state(self, plate_order, state, *, is_cancelled=False, parameters=None):
log = stateflow_models.StateFlowRecord.objects.create(
business_object=plate_order.business_object,
state=state,
completed_by=self.user,
is_cancelled=is_cancelled,
)
if parameters is not None:
stateflow_models.StateLogParameterRecord.objects.create(
state_log=log,
parameters=parameters,
)
return log
def test_completed_status_returns_sorted_plate_orders(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
'status': 'completed',
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['state']['id'], self.state_target.id)
self.assertEqual(resp.data['status'], 'completed')
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_completed_new.id, self.po_completed_old.id])
template_keys = [p['key'] for p in resp.data['state_parameters']]
self.assertEqual(template_keys, ['temperature'])
by_id = {item['id']: item for item in resp.data['results']}
self.assertEqual(by_id[self.po_completed_new.id]['state_parameters'][0]['value'], '32')
self.assertEqual(by_id[self.po_completed_old.id]['state_parameters'][0]['value'], '28')
self.assertIsNotNone(by_id[self.po_completed_new.id]['state_log'])
self.assertFalse(by_id[self.po_completed_new.id]['state_log']['is_cancelled'])
def test_default_status_is_completed(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(set(ids), {self.po_completed_new.id, self.po_completed_old.id})
def test_state_id_with_not_started_status_is_invalid(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
'status': 'not_started',
}
)
self.assertEqual(resp.status_code, 400)
def test_cancelled_status_returns_orders_with_cancelled_logs(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
'status': 'cancelled',
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['results'][0]['id'], self.po_cancelled.id)
self.assertTrue(resp.data['results'][0]['state_log']['is_cancelled'])
def test_invalid_process_state_pair_returns_400(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_unlinked.id,
'status': 'completed',
}
)
self.assertEqual(resp.status_code, 400)
self.assertIn('state 不属于该 process', resp.data['detail'])
def test_ordering_created_at_and_pagination(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
'status': 'completed',
'ordering': 'created_at',
'limit': 1,
'offset': 1,
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['count'], 2)
self.assertEqual(len(resp.data['results']), 1)
self.assertEqual(resp.data['results'][0]['id'], self.po_completed_new.id)
def test_other_process_records_are_excluded(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_target.id,
'status': 'completed',
}
)
ids = [item['id'] for item in resp.data['results']]
self.assertNotIn(self.po_other_process.id, ids)
def test_completed_status_only_returns_latest_state(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'state_id': self.state_prepare.id,
'status': 'completed',
}
)
self.assertEqual(resp.status_code, 200)
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(set(ids), {self.po_not_started.id, self.po_cancelled.id})
self.assertNotIn(self.po_completed_old.id, ids)
self.assertNotIn(self.po_completed_new.id, ids)
def test_without_state_id_returns_orders_without_any_logs(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
}
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['status'], 'not_started')
self.assertIsNone(resp.data['state'])
self.assertEqual(resp.data['state_parameters'], [])
ids = [item['id'] for item in resp.data['results']]
self.assertEqual(ids, [self.po_no_logs.id])
self.assertEqual(resp.data['results'][0]['state_parameters'], [])
self.assertIsNone(resp.data['results'][0]['state_log'])
def test_without_state_id_only_allows_not_started_status(self):
resp = self.client.get(
self.url,
{
'process_id': self.process.id,
'status': 'completed',
}
)
self.assertEqual(resp.status_code, 400)
self.assertIn('state_id 为空时仅支持 status=not_started', resp.data['detail'])
class PlateOrderBatchUpdateV2APITest(TestCase):
def setUp(self):
self.client = APIClient()
# 工厂用户(满足 IsPrintingFactory
self.merchant = basic_models.Merchant.objects.create(
name='印染工厂',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='factory_user2', password='pass12345')
basic_models.Employee.objects.create(
merchant=self.merchant,
sys_user=self.user,
name='工厂员工2',
)
self.client.force_authenticate(user=self.user)
# 赋予 change 权限(批量更新必需)
perm_change = Permission.objects.get(codename='change_plateorder')
self.user.user_permissions.add(perm_change)
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='客户A',
created_by=None,
)
self.designer = basic_models.Employee.objects.create(
merchant=self.merchant,
name='设计师',
)
self.po1 = printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='D1',
urgency_level='正常',
style_name='S1',
)
self.po2 = printing_models.PlateOrder.objects.create(
customer=self.customer,
design_code='D2',
urgency_level='正常',
style_name='S2',
)
def test_batch_update_success(self):
resp = self.client.post(
'/api/v2/plate-orders/batch-update/',
{
'plate_order_ids': [self.po1.id, self.po2.id],
'data': {'urgency_level': '加急', 'designer': self.designer.id},
},
format='json',
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data['updated_count'], 2)
self.po1.refresh_from_db()
self.po2.refresh_from_db()
self.assertEqual(self.po1.urgency_level, '加急')
self.assertEqual(self.po2.urgency_level, '加急')
self.assertEqual(self.po1.designer_id, self.designer.id)
self.assertEqual(self.po2.designer_id, self.designer.id)
def test_batch_update_fails_when_missing_ids(self):
resp = self.client.post(
'/api/v2/plate-orders/batch-update/',
{
'plate_order_ids': [self.po1.id, 999999],
'data': {'urgency_level': '加急'},
},
format='json',
)
self.assertEqual(resp.status_code, 400)
self.assertIn('不存在', resp.data.get('detail', ''))
def test_batch_update_fails_when_unknown_field(self):
resp = self.client.post(
'/api/v2/plate-orders/batch-update/',
{
'plate_order_ids': [self.po1.id],
'data': {'process': 123},
},
format='json',
)
self.assertEqual(resp.status_code, 400)
self.assertIn('不支持批量更新字段', resp.data.get('detail', ''))
def test_batch_update_is_invalid_requires_permission(self):
# 未授予 can_invalidate_plateorder
resp = self.client.post(
'/api/v2/plate-orders/batch-update/',
{
'plate_order_ids': [self.po1.id],
'data': {'is_invalid': True},
},
format='json',
)
self.assertEqual(resp.status_code, 403)
perm_invalidate = Permission.objects.get(codename='can_invalidate_plateorder')
self.user.user_permissions.add(perm_invalidate)
resp2 = self.client.post(
'/api/v2/plate-orders/batch-update/',
{
'plate_order_ids': [self.po1.id],
'data': {'is_invalid': True},
},
format='json',
)
self.assertEqual(resp2.status_code, 200)
self.po1.refresh_from_db()
self.assertTrue(self.po1.is_invalid)
class MyVisiblePagesAPITest(TestCase):
"""测试获取当前用户可见页面 API"""
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='测试商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='testuser', password='pass12345')
# 创建前端页面
self.main_menu = basic_models.FrontendPage.objects.create(
key='production',
label='生产管理',
page_type=basic_models.FrontendPageTypeEnum.MAIN,
sort_order=100
)
self.sub_menu1 = basic_models.FrontendPage.objects.create(
key='production-kanban',
label='生产看板',
page_type=basic_models.FrontendPageTypeEnum.SUB,
parent=self.main_menu,
sort_order=101
)
self.sub_menu2 = basic_models.FrontendPage.objects.create(
key='printing-order-list',
label='生产订单',
page_type=basic_models.FrontendPageTypeEnum.SUB,
parent=self.main_menu,
sort_order=102
)
self.other_menu = basic_models.FrontendPage.objects.create(
key='inventory',
label='库存管理',
page_type=basic_models.FrontendPageTypeEnum.MAIN,
sort_order=200
)
# 创建岗位并分配页面
self.emp_type = basic_models.EmployeeType.objects.create(
merchant=self.merchant,
title='生产主管'
)
self.emp_type.visible_pages.add(self.main_menu)
# 创建员工并关联用户
self.employee = basic_models.Employee.objects.create(
merchant=self.merchant,
name='张三',
position=self.emp_type,
sys_user=self.user # 关联到用户
)
self.client.force_authenticate(user=self.user)
def test_get_visible_pages_success(self):
"""测试成功获取当前用户可见页面"""
response = self.client.get('/api/v2/me/visible-pages/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['employee_id'], self.employee.id)
self.assertEqual(data['employee_name'], '张三')
self.assertEqual(data['position_id'], self.emp_type.id)
self.assertEqual(data['position_title'], '生产主管')
# 验证可见页面(主菜单 + 子菜单)
visible_keys = data['visible_keys']
self.assertIn('production', visible_keys)
self.assertIn('production-kanban', visible_keys)
self.assertIn('printing-order-list', visible_keys)
self.assertNotIn('inventory', visible_keys)
# 验证页面详情
self.assertEqual(len(data['visible_pages']), 3)
def test_get_visible_pages_with_sub_menu_only(self):
"""测试仅授予子菜单时只返回该子菜单"""
# 重新配置岗位:只授予子菜单
self.emp_type.visible_pages.clear()
self.emp_type.visible_pages.add(self.sub_menu1)
response = self.client.get('/api/v2/me/visible-pages/')
self.assertEqual(response.status_code, 200)
visible_keys = response.data['visible_keys']
self.assertIn('production-kanban', visible_keys)
self.assertNotIn('production', visible_keys)
self.assertNotIn('printing-order-list', visible_keys)
self.assertEqual(len(visible_keys), 1)
def test_get_visible_pages_employee_no_position(self):
"""测试没有岗位的员工返回空列表"""
# 移除员工岗位
self.employee.position = None
self.employee.save()
response = self.client.get('/api/v2/me/visible-pages/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['employee_id'], self.employee.id)
self.assertEqual(data['position_id'], None)
self.assertEqual(data['visible_pages'], [])
self.assertEqual(data['visible_keys'], [])
def test_get_visible_pages_user_no_employee(self):
"""测试用户未关联员工时返回空列表"""
# 创建一个没有关联员工的用户
user_no_employee = get_user_model().objects.create_user(username='noemployee', password='pass12345')
self.client.force_authenticate(user=user_no_employee)
response = self.client.get('/api/v2/me/visible-pages/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['employee_id'], None)
self.assertEqual(data['employee_name'], None)
self.assertEqual(data['position_id'], None)
self.assertEqual(data['visible_pages'], [])
self.assertEqual(data['visible_keys'], [])
def test_get_visible_pages_unauthenticated(self):
"""测试未登录时返回 401"""
self.client.logout()
response = self.client.get('/api/v2/me/visible-pages/')
self.assertEqual(response.status_code, 401)
class PrintingOrderBatchAdvanceRecordsAPITest(TestCase):
"""测试印染订单批量推进记录查询 API"""
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='测试商户',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='testuser', password='pass12345')
self.client.force_authenticate(user=self.user)
# 创建客户
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='测试客户'
)
# 创建产品类别和产品
self.category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name='测试类别'
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
name='测试产品',
category=self.category,
unit=basic_models.ProductUnitEnum.METER
)
# 创建印染订单
self.printing_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='测试面料',
width='150cm'
)
# 创建印染明细
self.job1 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=100,
unit=''
)
self.job2 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=200,
unit=''
)
# 创建流程状态
self.state = stateflow_models.State.objects.create(
name='染色',
description='染色工序'
)
# 创建批量推进记录
self.record = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
printing_order=self.printing_order,
state=self.state,
created_by=self.user,
parameters={'temperature': '25.5', 'operator': '张三'}
)
self.record.printing_jobs.add(self.job1, self.job2)
def test_get_batch_advance_records_success(self):
"""测试成功获取批量推进记录"""
response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['printing_order_id'], self.printing_order.id)
self.assertEqual(data['total_count'], 1)
self.assertEqual(len(data['records']), 1)
record = data['records'][0]
self.assertEqual(record['id'], self.record.id)
self.assertEqual(record['state_id'], self.state.id)
self.assertEqual(record['state_name'], '染色')
self.assertEqual(record['created_by'], self.user.id)
self.assertEqual(record['created_by_username'], 'testuser')
self.assertEqual(record['parameters'], {'temperature': '25.5', 'operator': '张三'})
self.assertEqual(record['printing_job_count'], 2)
self.assertIn(self.job1.id, record['printing_job_ids'])
self.assertIn(self.job2.id, record['printing_job_ids'])
def test_get_batch_advance_records_empty(self):
"""测试没有批量推进记录时返回空列表"""
# 创建另一个没有记录的订单
another_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='测试面料2',
width='160cm'
)
response = self.client.get(f'/api/v2/printing-orders/{another_order.id}/batch-advance-records/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['printing_order_id'], another_order.id)
self.assertEqual(data['total_count'], 0)
self.assertEqual(data['records'], [])
def test_get_batch_advance_records_order_not_found(self):
"""测试订单不存在时返回 404"""
response = self.client.get('/api/v2/printing-orders/99999/batch-advance-records/')
self.assertEqual(response.status_code, 404)
self.assertIn('error', response.data)
def test_get_batch_advance_records_multiple_records(self):
"""测试多条批量推进记录按时间倒序返回"""
# 创建第二条记录
state2 = stateflow_models.State.objects.create(name='整理', description='整理工序')
record2 = printing_models.PrintingJobBatchAdvanceRecord.objects.create(
printing_order=self.printing_order,
state=state2,
created_by=self.user,
parameters={'notes': '完成'}
)
record2.printing_jobs.add(self.job1)
response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['total_count'], 2)
# 最新的记录应该在前面
self.assertEqual(data['records'][0]['id'], record2.id)
self.assertEqual(data['records'][1]['id'], self.record.id)
def test_get_batch_advance_records_unauthenticated(self):
"""测试未登录时返回 401"""
self.client.logout()
response = self.client.get(f'/api/v2/printing-orders/{self.printing_order.id}/batch-advance-records/')
self.assertEqual(response.status_code, 401)
class PrintingJobBatchAddParametersAPITest(TestCase):
"""测试批量补充工艺参数 API"""
def setUp(self):
self.client = APIClient()
self.merchant = basic_models.Merchant.objects.create(
name='测试工厂',
type=basic_models.MerchantTypeEnum.FACTORY,
)
self.user = get_user_model().objects.create_user(username='factory_user', password='pass12345')
self.employee = basic_models.Employee.objects.create(
merchant=self.merchant,
name='工厂员工',
sys_user=self.user
)
self.client.force_authenticate(user=self.user)
# 创建客户
self.customer = basic_models.Customer.objects.create(
merchant=self.merchant,
name='测试客户'
)
# 创建产品类别和产品
self.category = basic_models.ProductCategory.objects.create(
merchant=self.merchant,
name='测试类别'
)
self.product = basic_models.Product.objects.create(
merchant=self.merchant,
name='测试产品',
category=self.category,
unit=basic_models.ProductUnitEnum.METER
)
# 创建流程状态
self.state1 = stateflow_models.State.objects.create(name='染色', description='染色工序')
self.state2 = stateflow_models.State.objects.create(name='整理', description='整理工序')
# 创建流程
self.process = stateflow_models.Process.objects.create(name='印染流程')
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state1, order=1)
stateflow_models.ProcessNode.objects.create(process=self.process, state=self.state2, order=2)
# 创建印染订单
self.printing_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='测试面料',
width='150cm',
process=self.process
)
# 创建印染明细并绑定流程实例
self.job1 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=100,
unit=''
)
self.job2 = printing_models.PrintingJob.objects.create(
printing_order=self.printing_order,
product=self.product,
quantity=200,
unit=''
)
# 创建业务对象并推进到 state1
from stateflow import services as stateflow_services
from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(printing_models.PrintingJob)
self.bo1 = stateflow_models.BusinessObject.objects.create(
name=f'PrintingJob-{self.job1.id}',
process=self.process,
content_type=ct,
object_id=self.job1.id
)
self.job1.business_object = self.bo1
self.job1.save()
self.bo2 = stateflow_models.BusinessObject.objects.create(
name=f'PrintingJob-{self.job2.id}',
process=self.process,
content_type=ct,
object_id=self.job2.id
)
self.job2.business_object = self.bo2
self.job2.save()
# 推进到 state1创建 state_log
stateflow_services.advance_to_next_state(self.bo1, self.user, temperature='25.0')
stateflow_services.advance_to_next_state(self.bo2, self.user, temperature='25.0')
def test_batch_add_parameters_success(self):
"""测试成功批量补充参数"""
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id, self.job2.id],
'state_id': self.state1.id,
'parameters': {'temperature': '26.0', 'operator': '李四'},
'remark': '补测数据'
},
format='json'
)
self.assertEqual(response.status_code, 200)
data = response.data
self.assertEqual(data['detail'], '批量补充参数成功')
self.assertEqual(data['printing_order_id'], self.printing_order.id)
self.assertEqual(set(data['printing_job_ids']), {self.job1.id, self.job2.id})
self.assertEqual(data['state_id'], self.state1.id)
self.assertEqual(data['state_name'], '染色')
self.assertEqual(data['affected_count'], 2)
# 验证创建了批量操作记录
record = printing_models.PrintingJobBatchAdvanceRecord.objects.get(id=data['batch_record_id'])
self.assertTrue(record.only_parameters)
self.assertEqual(record.parameters, {'temperature': '26.0', 'operator': '李四'})
self.assertEqual(record.printing_jobs.count(), 2)
def test_batch_add_parameters_empty_parameters(self):
"""测试参数为空时返回错误"""
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id],
'state_id': self.state1.id,
'parameters': {},
},
format='json'
)
self.assertEqual(response.status_code, 400)
def test_batch_add_parameters_job_not_found(self):
"""测试 job 不存在时返回错误"""
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [99999],
'state_id': self.state1.id,
'parameters': {'temperature': '26.0'},
},
format='json'
)
self.assertEqual(response.status_code, 400)
self.assertIn('missing_jobs', response.data)
def test_batch_add_parameters_state_not_found(self):
"""测试 state 不存在时返回错误"""
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id],
'state_id': 99999,
'parameters': {'temperature': '26.0'},
},
format='json'
)
self.assertEqual(response.status_code, 400)
def test_batch_add_parameters_missing_state_log(self):
"""测试 job 没有对应 state_log 时返回错误"""
# state2 的 state_log 尚未创建(还没推进到那一步)
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id],
'state_id': self.state2.id, # 还没推进到这个状态
'parameters': {'temperature': '26.0'},
},
format='json'
)
self.assertEqual(response.status_code, 400)
self.assertIn('missing_state_log', response.data)
def test_batch_add_parameters_different_orders(self):
"""测试 jobs 属于不同订单时返回错误"""
# 创建另一个订单和 job
another_order = printing_models.PrintingOrder.objects.create(
customer=self.customer,
fabric='另一面料',
width='160cm'
)
another_job = printing_models.PrintingJob.objects.create(
printing_order=another_order,
product=self.product,
quantity=300,
unit=''
)
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id, another_job.id],
'state_id': self.state1.id,
'parameters': {'temperature': '26.0'},
},
format='json'
)
self.assertEqual(response.status_code, 400)
def test_batch_add_parameters_unauthenticated(self):
"""测试未登录时返回 401"""
self.client.logout()
response = self.client.post(
'/api/v2/printing-jobs/batch-add-parameters/',
{
'printing_job_ids': [self.job1.id],
'state_id': self.state1.id,
'parameters': {'temperature': '26.0'},
},
format='json'
)
self.assertEqual(response.status_code, 401)