184 lines
6.3 KiB
Python
184 lines
6.3 KiB
Python
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])
|