from concurrent.futures import ThreadPoolExecutor from decimal import Decimal from threading import Barrier from unittest import skipUnless from django.db import connection, connections, transaction from django.test import SimpleTestCase, TestCase, TransactionTestCase from apps.inm.filters import MaterialBatchFilter from apps.inm.models import MaterialBatch, WareHouse from apps.inm.serializers import MaterialBatchSerializer from apps.mtm.models import Material from apps.qm.models import Defect class MaterialBatchDefectGradeTests(TestCase): @classmethod def setUpTestData(cls): cls.material = Material.objects.create(name='仓库缺陷等级测试物料') cls.warehouse = WareHouse.objects.create( number='GRADE', name='等级测试仓库', place='测试地点', ) cls.defect_b = Defect.objects.create( name='仓库B类缺陷', cate=Defect.cate_list[0], okcate=Defect.DEFECT_OK_B, ) cls.notok_without_defect = MaterialBatch.objects.create( material=cls.material, warehouse=cls.warehouse, batch='MB-NOTOK-NONE', count=1, state=20, ) cls.normal_with_b_defect = MaterialBatch.objects.create( material=cls.material, warehouse=cls.warehouse, batch='MB-NORMAL-B', count=1, state=10, defect=cls.defect_b, ) def test_serializer_uses_defect_or_defaults_to_ok_independent_of_state(self): no_defect_data = MaterialBatchSerializer( self.notok_without_defect ).data b_defect_data = MaterialBatchSerializer( self.normal_with_b_defect ).data self.assertEqual(no_defect_data['defect_grade'], Defect.DEFECT_OK) self.assertEqual(no_defect_data['defect_grade_name'], '合格') self.assertEqual(b_defect_data['defect_grade'], Defect.DEFECT_OK_B) self.assertEqual(b_defect_data['defect_grade_name'], '合格B类') def test_effective_grade_filter_is_independent_of_state(self): ok_items = MaterialBatchFilter( {'defect_grade': Defect.DEFECT_OK}, queryset=MaterialBatch.objects.all(), ).qs b_items = MaterialBatchFilter( {'defect_grade': Defect.DEFECT_OK_B}, queryset=MaterialBatch.objects.all(), ).qs self.assertQuerySetEqual( ok_items, [self.notok_without_defect], transform=lambda item: item, ) self.assertQuerySetEqual( b_items, [self.normal_with_b_defect], transform=lambda item: item, ) class MaterialBatchInventoryKeyTests(SimpleTestCase): def setUp(self): self.material = Material(id='100', name='测试物料') self.warehouse = WareHouse( id='200', number='TEST', name='测试仓库', place='测试地点', ) def test_inventory_key_normalizes_omitted_optional_fields(self): omitted = MaterialBatch._normalize_inventory_lookup( material=self.material, batch='BATCH-001', warehouse=self.warehouse, ) explicit = MaterialBatch._normalize_inventory_lookup( material=self.material, batch='BATCH-001', warehouse=self.warehouse, state=10, defect=None, ) self.assertEqual(omitted, explicit) self.assertEqual( MaterialBatch._inventory_advisory_lock_payload(omitted), MaterialBatch._inventory_advisory_lock_payload(explicit), ) def test_inventory_key_requires_material_batch_and_warehouse(self): required_values = { 'material': self.material, 'batch': 'BATCH-001', 'warehouse': self.warehouse, } for field in required_values: with self.subTest(field=field): lookup = required_values.copy() lookup[field] = None with self.assertRaisesRegex(ValueError, field): MaterialBatch._normalize_inventory_lookup(**lookup) def test_inventory_key_rejects_unknown_fields(self): with self.assertRaisesRegex(TypeError, 'supplier'): MaterialBatch._normalize_inventory_lookup( material=self.material, batch='BATCH-001', warehouse=self.warehouse, supplier=None, ) @skipUnless( connection.vendor == 'postgresql', 'advisory lock concurrency test requires PostgreSQL', ) class MaterialBatchConcurrencyTests(TransactionTestCase): def test_concurrent_first_create_uses_one_inventory_record(self): material = Material.objects.create(name='仓库并发测试物料') warehouse = WareHouse.objects.create( number='CONCURRENT', name='并发测试仓库', place='测试地点', ) barrier = Barrier(2) def create_inventory(): connections.close_all() try: barrier.wait() with transaction.atomic(): mb, created = ( MaterialBatch.locked_get_or_create_inventory( material=material, batch='CONCURRENT-001', warehouse=warehouse, defaults={'count': Decimal('0')}, ) ) mb.count += Decimal('1') mb.save(update_fields=['count']) return created finally: connections.close_all() with ThreadPoolExecutor(max_workers=2) as executor: created_results = list(executor.map( lambda _: create_inventory(), range(2), )) queryset = MaterialBatch.objects.filter( material=material, batch='CONCURRENT-001', warehouse=warehouse, state=10, defect=None, ) self.assertEqual(queryset.count(), 1) self.assertEqual(queryset.get().count, Decimal('2')) self.assertCountEqual(created_results, [True, False])