from io import StringIO from django.core.management import call_command from django.test import TestCase from django.core.exceptions import ValidationError from .models import ( Merchant, MerchantTypeEnum, WareHouse, WarehouseTypeEnum, WareHouseModeEnum, EmployeeType, Employee, EmployeeStatusEnum, FrontendPage, FrontendPageTypeEnum, TransportVehicle, TransportVehicleMaterialCapacity ) class WarehouseTypeTestCase(TestCase): """测试仓库类型字段""" def setUp(self): """设置测试数据""" self.merchant = Merchant.objects.create( name='测试商户', type=MerchantTypeEnum.STORE ) def test_warehouse_default_type(self): """测试仓库类型的默认值""" warehouse = WareHouse.objects.create( merchant=self.merchant, name='默认仓库', ) self.assertEqual(warehouse.type, WarehouseTypeEnum.WHOLE) def test_warehouse_type_whole(self): """测试创建整仓""" warehouse = WareHouse.objects.create( merchant=self.merchant, name='整仓1', type=WarehouseTypeEnum.WHOLE ) self.assertEqual(warehouse.type, WarehouseTypeEnum.WHOLE) self.assertEqual(warehouse.get_type_display(), '整仓') def test_warehouse_type_scattered(self): """测试创建散仓""" warehouse = WareHouse.objects.create( merchant=self.merchant, name='散仓1', type=WarehouseTypeEnum.SCATTERED ) self.assertEqual(warehouse.type, WarehouseTypeEnum.SCATTERED) self.assertEqual(warehouse.get_type_display(), '散仓') def test_warehouse_type_enum_values(self): """测试仓库类型枚举的所有值""" self.assertEqual(WarehouseTypeEnum.WHOLE.value, 1) self.assertEqual(WarehouseTypeEnum.WHOLE.label, '整仓') self.assertEqual(WarehouseTypeEnum.SCATTERED.value, 2) self.assertEqual(WarehouseTypeEnum.SCATTERED.label, '散仓') def test_warehouse_with_all_fields(self): """测试带有所有字段的仓库""" warehouse = WareHouse.objects.create( merchant=self.merchant, name='完整仓库', type=WarehouseTypeEnum.SCATTERED, mode=WareHouseModeEnum.RESTRICT_IN_OUT, location='测试地址', area='测试区域', contact='张三', mobile='13800138000', description='测试描述' ) self.assertEqual(warehouse.type, WarehouseTypeEnum.SCATTERED) self.assertEqual(warehouse.mode, WareHouseModeEnum.RESTRICT_IN_OUT) self.assertEqual(warehouse.name, '完整仓库') class EmployeeTypeTestCase(TestCase): """测试员工职位类型""" def setUp(self): """设置测试数据""" self.merchant = Merchant.objects.create( name='测试商户', type=MerchantTypeEnum.STORE ) def test_create_employee_type(self): """测试创建员工职位类型""" emp_type = EmployeeType.objects.create( merchant=self.merchant, title='测试职位', description='职位描述' ) self.assertEqual(emp_type.title, '测试职位') self.assertEqual(emp_type.description, '职位描述') self.assertEqual(str(emp_type), '测试职位') def test_employee_type_unique_together(self): """测试商户+职位名称的唯一性约束""" EmployeeType.objects.create( merchant=self.merchant, title='打纸' ) # 同一商户不能创建重复职位 with self.assertRaises(Exception): EmployeeType.objects.create( merchant=self.merchant, title='打纸' ) def test_employee_with_position(self): """测试员工关联职位""" emp_type = EmployeeType.objects.create( merchant=self.merchant, title='滚筒工' ) employee = Employee.objects.create( merchant=self.merchant, name='张三', position=emp_type, status=EmployeeStatusEnum.ACTIVE ) self.assertEqual(employee.position, emp_type) self.assertEqual(employee.job_type, '滚筒工') # 测试 job_type 属性 def test_employee_without_position(self): """测试员工没有职位时 job_type 返回空字符串""" employee = Employee.objects.create( merchant=self.merchant, name='李四', status=EmployeeStatusEnum.ACTIVE ) self.assertIsNone(employee.position) self.assertEqual(employee.job_type, '') def test_employee_type_reverse_field(self): """测试预留字段""" emp_type = EmployeeType.objects.create( merchant=self.merchant, title='仓库管理员', reverse='预留数据' ) self.assertEqual(emp_type.reverse, '预留数据') def test_multiple_employees_same_position(self): """测试多个员工可以有相同职位""" emp_type = EmployeeType.objects.create( merchant=self.merchant, title='打纸工' ) emp1 = Employee.objects.create( merchant=self.merchant, name='员工1', position=emp_type ) emp2 = Employee.objects.create( merchant=self.merchant, name='员工2', position=emp_type ) self.assertEqual(emp1.job_type, '打纸工') self.assertEqual(emp2.job_type, '打纸工') self.assertEqual(emp_type.employees.count(), 2) class TransportVehicleTestCase(TestCase): def setUp(self): self.merchant = Merchant.objects.create( name='运输商户', type=MerchantTypeEnum.STORE ) def test_create_transport_vehicle_with_material_capacity(self): vehicle = TransportVehicle.objects.create( merchant=self.merchant, name='9.6米厢车', license_plate='浙A12345' ) capacity = TransportVehicleMaterialCapacity.objects.create( merchant=self.merchant, transport_vehicle=vehicle, material_name='棉布', capacity=120 ) self.assertEqual(vehicle.license_plate, '浙A12345') self.assertEqual(str(vehicle), '9.6米厢车 (浙A12345)') self.assertEqual(capacity.capacity, 120) self.assertEqual(vehicle.material_capacities.count(), 1) def test_transport_vehicle_license_plate_unique_per_merchant(self): TransportVehicle.objects.create( merchant=self.merchant, name='一号车', license_plate='浙A99999' ) with self.assertRaises(Exception): TransportVehicle.objects.create( merchant=self.merchant, name='二号车', license_plate='浙A99999' ) class FrontendPageTestCase(TestCase): """测试前端页面模型""" def test_create_main_menu(self): """测试创建主菜单""" page = FrontendPage.objects.create( key='plate-opening', label='开版管理', page_type=FrontendPageTypeEnum.MAIN, sort_order=0 ) self.assertEqual(page.key, 'plate-opening') self.assertEqual(page.label, '开版管理') self.assertEqual(page.page_type, FrontendPageTypeEnum.MAIN) self.assertIsNone(page.parent) self.assertTrue(page.is_active) self.assertEqual(str(page), '开版管理 (plate-opening)') def test_create_sub_menu_with_parent(self): """测试创建子菜单并关联父级""" parent = FrontendPage.objects.create( key='plate-opening', label='开版管理', page_type=FrontendPageTypeEnum.MAIN ) child = FrontendPage.objects.create( key='plate-order', label='开版订单', page_type=FrontendPageTypeEnum.SUB, parent=parent ) self.assertEqual(child.parent, parent) self.assertEqual(parent.children.count(), 1) self.assertEqual(parent.children.first(), child) def test_key_unique_constraint(self): """测试 key 字段唯一性约束""" FrontendPage.objects.create(key='test-page', label='测试页面') with self.assertRaises(Exception): FrontendPage.objects.create(key='test-page', label='重复页面') def test_cascade_delete(self): """测试删除父级时子级也被删除""" parent = FrontendPage.objects.create( key='parent', label='父级', page_type=FrontendPageTypeEnum.MAIN ) FrontendPage.objects.create( key='child1', label='子级1', parent=parent ) FrontendPage.objects.create( key='child2', label='子级2', parent=parent ) self.assertEqual(FrontendPage.objects.count(), 3) parent.delete() self.assertEqual(FrontendPage.objects.count(), 0) class EmployeeTypeVisiblePagesTestCase(TestCase): """测试员工职位类型的可见页面功能""" def setUp(self): """设置测试数据""" self.merchant = Merchant.objects.create( name='测试商户', type=MerchantTypeEnum.STORE ) # 创建页面 self.main_menu = FrontendPage.objects.create( key='production', label='生产管理', page_type=FrontendPageTypeEnum.MAIN ) self.sub_menu1 = FrontendPage.objects.create( key='production-kanban', label='生产看板', page_type=FrontendPageTypeEnum.SUB, parent=self.main_menu ) self.sub_menu2 = FrontendPage.objects.create( key='printing-order-list', label='生产订单', page_type=FrontendPageTypeEnum.SUB, parent=self.main_menu ) # 创建岗位 self.emp_type = EmployeeType.objects.create( merchant=self.merchant, title='生产主管' ) def test_assign_visible_pages(self): """测试为岗位分配可见页面""" self.emp_type.visible_pages.add(self.main_menu) self.assertEqual(self.emp_type.visible_pages.count(), 1) self.assertIn(self.main_menu, self.emp_type.visible_pages.all()) def test_get_visible_page_keys_with_main_menu(self): """测试授予主菜单时获取所有可见页面(含子菜单)""" self.emp_type.visible_pages.add(self.main_menu) # 模拟查询逻辑 visible_keys = set() for page in self.emp_type.visible_pages.filter(is_active=True): visible_keys.add(page.key) if page.page_type == FrontendPageTypeEnum.MAIN: children_keys = page.children.filter(is_active=True).values_list('key', flat=True) visible_keys.update(children_keys) self.assertIn('production', visible_keys) self.assertIn('production-kanban', visible_keys) self.assertIn('printing-order-list', visible_keys) self.assertEqual(len(visible_keys), 3) def test_get_visible_page_keys_with_sub_menu_only(self): """测试仅授予子菜单时只能看到该子菜单""" self.emp_type.visible_pages.add(self.sub_menu1) visible_keys = set() for page in self.emp_type.visible_pages.filter(is_active=True): visible_keys.add(page.key) if page.page_type == FrontendPageTypeEnum.MAIN: children_keys = page.children.filter(is_active=True).values_list('key', flat=True) visible_keys.update(children_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_employee_visible_pages_through_position(self): """测试通过员工岗位查询可见页面""" self.emp_type.visible_pages.add(self.main_menu) employee = Employee.objects.create( merchant=self.merchant, name='测试员工', position=self.emp_type ) # 通过员工 -> 岗位 -> 可见页面 visible_pages = employee.position.visible_pages.all() self.assertEqual(visible_pages.count(), 1) self.assertIn(self.main_menu, visible_pages) class SyncFrontendPagesCommandTestCase(TestCase): """测试 sync_frontend_pages 管理命令""" def test_sync_command_dry_run(self): """测试 dry-run 模式不写入数据""" out = StringIO() call_command('sync_frontend_pages', '--dry-run', stdout=out) # 验证没有实际写入 self.assertEqual(FrontendPage.objects.count(), 0) self.assertIn('DRY-RUN', out.getvalue()) def test_sync_command_creates_pages(self): """测试命令创建页面数据""" out = StringIO() call_command('sync_frontend_pages', stdout=out) # 验证创建了页面 self.assertGreater(FrontendPage.objects.count(), 0) # 验证主菜单 main_menus = FrontendPage.objects.filter(page_type=FrontendPageTypeEnum.MAIN) self.assertEqual(main_menus.count(), 9) # 9 个主菜单 # 验证子菜单 sub_menus = FrontendPage.objects.filter(page_type=FrontendPageTypeEnum.SUB) self.assertEqual(sub_menus.count(), 43) # 43 个子菜单 # 验证父子关系 plate_opening = FrontendPage.objects.get(key='plate-opening') self.assertEqual(plate_opening.children.count(), 5) def test_sync_command_idempotent(self): """测试命令幂等性(多次运行结果一致)""" call_command('sync_frontend_pages', stdout=StringIO()) first_count = FrontendPage.objects.count() call_command('sync_frontend_pages', stdout=StringIO()) second_count = FrontendPage.objects.count() self.assertEqual(first_count, second_count) def test_sync_command_clear_option(self): """测试 --clear 选项""" # 先创建数据 call_command('sync_frontend_pages', stdout=StringIO()) self.assertGreater(FrontendPage.objects.count(), 0) # 使用 --clear 重新同步 out = StringIO() call_command('sync_frontend_pages', '--clear', stdout=out) self.assertIn('已清空', out.getvalue()) # 数据应该重新创建 self.assertEqual(FrontendPage.objects.count(), 52) # 9 主菜单 + 43 子菜单