From 8af19b8a9a6ae0305d9421b920710b69fe1af997 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 28 Jul 2026 12:15:21 +0800 Subject: [PATCH] Restrict global inventory handovers --- ...0136_alter_handover_global_scope_fields.py | 36 +++++++++ apps/wpm/models.py | 6 +- apps/wpm/serializers.py | 52 +++++++++++-- apps/wpm/tests.py | 75 ++++++++++++++++++- 4 files changed, 160 insertions(+), 9 deletions(-) create mode 100644 apps/wpm/migrations/0136_alter_handover_global_scope_fields.py diff --git a/apps/wpm/migrations/0136_alter_handover_global_scope_fields.py b/apps/wpm/migrations/0136_alter_handover_global_scope_fields.py new file mode 100644 index 00000000..53d7d9be --- /dev/null +++ b/apps/wpm/migrations/0136_alter_handover_global_scope_fields.py @@ -0,0 +1,36 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wpm', '0135_mlog_stored_scope'), + ] + + operations = [ + migrations.AlterField( + model_name='handover', + name='send_dept', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='handover_send_dept', + to='system.dept', + verbose_name='送料部门', + ), + ), + migrations.AlterField( + model_name='handover', + name='recive_dept', + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='handover_recive_dept', + to='system.dept', + verbose_name='接收部门', + ), + ), + ] diff --git a/apps/wpm/models.py b/apps/wpm/models.py index af1ae43d..7407713c 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -698,7 +698,8 @@ class Handover(CommonADModel): send_mgroup = models.ForeignKey( Mgroup, verbose_name='送料工段', on_delete=models.CASCADE, null=True, blank=True) send_dept = models.ForeignKey( - Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept') + Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept', + null=True, blank=True) batch = models.TextField('批次号', null=True, blank=True, db_index=True) material = models.ForeignKey( Material, verbose_name='物料', on_delete=models.CASCADE, related_name='h_ma') @@ -707,7 +708,8 @@ class Handover(CommonADModel): count = models.DecimalField('送料数', default=0, max_digits=11, decimal_places=1) count_eweight = models.FloatField('单数重量', default=0) recive_dept = models.ForeignKey( - Dept, verbose_name='接收部门', on_delete=models.CASCADE, related_name='handover_recive_dept') + Dept, verbose_name='接收部门', on_delete=models.CASCADE, related_name='handover_recive_dept', + null=True, blank=True) recive_mgroup = models.ForeignKey(Mgroup, verbose_name='接收工段', on_delete=models.CASCADE, related_name='handover_recive_mgroup', null=True, blank=True) recive_user = models.ForeignKey( User, verbose_name='接收人', on_delete=models.CASCADE, related_name='handover_recive_user', null=True, blank=True) diff --git a/apps/wpm/serializers.py b/apps/wpm/serializers.py index 73330725..42a5a66c 100644 --- a/apps/wpm/serializers.py +++ b/apps/wpm/serializers.py @@ -1301,20 +1301,62 @@ class HandoverSerializer(CustomModelSerializer): raise ParseError('必须指定车间库存') attrs['material'] = wm.material attrs['send_dept'] = wm.belong_dept - if wm.mgroup: - attrs['send_mgroup'] = wm.mgroup + attrs['send_mgroup'] = wm.mgroup + is_global_source = wm.mgroup_id is None and wm.belong_dept_id is None + + if ( + is_global_source + and attrs['type'] == Handover.H_NORMAL + and mtype == Handover.H_NORMAL + ): + raise ParseError('全局库存无需正常交接') + + is_global_rebatch = ( + is_global_source + and attrs['type'] == Handover.H_NORMAL + and mtype in [Handover.H_DIV, Handover.H_MERGE] + ) + if is_global_rebatch: + # 全局库存的拆批、合批不改变库存归属。 + attrs['recive_mgroup'] = None + attrs['recive_dept'] = None + if new_wm and ( + new_wm.mgroup_id is not None + or new_wm.belong_dept_id is not None + ): + raise ParseError('全局库存合批目标必须是全局库存') + elif ( + is_global_source + and attrs['type'] in [ + Handover.H_REPAIR, + Handover.H_SCRAP, + Handover.H_CHANGE, + ] + and not attrs.get('recive_mgroup') + ): + raise ParseError('全局库存返修、报废或改版必须指定接收工段') + material_process = attrs['material'].process if ( - material_process + not is_global_rebatch + and material_process and WmScope.requires_mgroup(material_process.into_wm_scope) and not attrs.get('recive_mgroup') ): raise ParseError('必须指定交接工段') if 'recive_mgroup' in attrs and attrs['recive_mgroup']: attrs['recive_dept'] = attrs['recive_mgroup'].belong_dept - if not attrs.get('recive_dept', None) and not attrs.get('recive_mgroup', None): + if ( + not is_global_rebatch + and not attrs.get('recive_dept', None) + and not attrs.get('recive_mgroup', None) + ): raise ParseError('必须指定收料车间或收料工段') - if not attrs.get('send_dept', None) and not attrs.get('send_mgroup', None): + if ( + not is_global_source + and not attrs.get('send_dept', None) + and not attrs.get('send_mgroup', None) + ): raise ParseError('必须指定送料车间或送料工段') # if attrs["mtype"] == Handover.H_NORMAL and attrs.get("recive_mgroup", None) == attrs.get("send_mgroup", None): diff --git a/apps/wpm/tests.py b/apps/wpm/tests.py index 5a60d0e2..d935f054 100644 --- a/apps/wpm/tests.py +++ b/apps/wpm/tests.py @@ -5,9 +5,10 @@ from django.test import SimpleTestCase from apps.mtm.models import Material, WmScope from apps.wpm.filters import WMaterialFilter -from apps.wpm.models import WMaterial -from apps.wpm.serializers import WMaterialCreateSerializer +from apps.wpm.models import Handover, WMaterial +from apps.wpm.serializers import HandoverSerializer, WMaterialCreateSerializer from apps.wpm.views import MlogbwViewSet +from rest_framework.exceptions import ParseError class MlogbwViewSetTests(SimpleTestCase): @@ -139,3 +140,73 @@ class WMaterialScopeTests(SimpleTestCase): validated = WMaterialCreateSerializer().validate(attrs) self.assertIs(validated["belong_dept"], dept) + + def test_global_inventory_cannot_enter_normal_handover(self): + wm = WMaterial( + material=Material(tracking=Material.MA_TRACKING_BATCH), + batch="GLOBAL-001", + count=1, + ) + + with self.assertRaisesMessage(ParseError, "全局库存无需正常交接"): + HandoverSerializer().validate({ + "wm": wm, + "count": 1, + "type": Handover.H_NORMAL, + "mtype": Handover.H_NORMAL, + }) + + def test_global_inventory_split_stays_global(self): + wm = WMaterial( + material=Material(tracking=Material.MA_TRACKING_BATCH), + batch="GLOBAL-001", + count=1, + ) + + validated = HandoverSerializer().validate({ + "wm": wm, + "count": 1, + "type": Handover.H_NORMAL, + "mtype": Handover.H_DIV, + }) + + self.assertIsNone(validated["send_dept"]) + self.assertIsNone(validated["recive_dept"]) + self.assertIsNone(validated["recive_mgroup"]) + + def test_global_inventory_cannot_merge_into_scoped_inventory(self): + material = Material(tracking=Material.MA_TRACKING_BATCH) + wm = WMaterial(material=material, batch="GLOBAL-001", count=1) + target = WMaterial( + material=material, + batch="GLOBAL-MERGED", + count=0, + belong_dept_id=20, + ) + + with self.assertRaisesMessage(ParseError, "全局库存合批目标必须是全局库存"): + HandoverSerializer().validate({ + "wm": wm, + "count": 1, + "new_wm": target, + "type": Handover.H_NORMAL, + "mtype": Handover.H_MERGE, + }) + + def test_global_inventory_scrap_requires_receiving_mgroup(self): + wm = WMaterial( + material=Material(tracking=Material.MA_TRACKING_BATCH), + batch="GLOBAL-001", + count=1, + ) + + with self.assertRaisesMessage( + ParseError, + "全局库存返修、报废或改版必须指定接收工段", + ): + HandoverSerializer().validate({ + "wm": wm, + "count": 1, + "type": Handover.H_SCRAP, + "mtype": Handover.H_NORMAL, + })