统一车间库存归属策略

This commit is contained in:
caoqianming 2026-07-28 09:26:25 +08:00
parent dde5b0a6e8
commit 1700a58409
9 changed files with 270 additions and 55 deletions

View File

@ -5,7 +5,7 @@ from apps.mtm.models import Material, Shift, Mgroup, Process
@admin.register(Process) @admin.register(Process)
class ProcessAdmin(admin.ModelAdmin): class ProcessAdmin(admin.ModelAdmin):
list_display = ('id', 'name', 'cate', 'sort', 'into_wm_mgroup', 'store_notok', 'batch_append_equip', 'mlog_need_ticket') list_display = ('id', 'name', 'cate', 'sort', 'into_wm_scope', 'store_notok', 'batch_append_equip', 'mlog_need_ticket')
@admin.register(Material) @admin.register(Material)

View File

@ -0,0 +1,36 @@
from django.db import migrations, models
def migrate_scope_forward(apps, schema_editor):
Process = apps.get_model('mtm', 'Process')
Process.objects.filter(into_wm_mgroup=True).update(into_wm_scope=10)
def migrate_scope_backward(apps, schema_editor):
Process = apps.get_model('mtm', 'Process')
Process.objects.filter(into_wm_scope=10).update(into_wm_mgroup=True)
Process.objects.exclude(into_wm_scope=10).update(into_wm_mgroup=False)
class Migration(migrations.Migration):
dependencies = [
('mtm', '0067_material_test_mode_in'),
]
operations = [
migrations.AddField(
model_name='process',
name='into_wm_scope',
field=models.PositiveSmallIntegerField(
choices=[(10, '工段'), (20, '部门'), (30, '全局')],
default=20,
verbose_name='产出库存归属',
),
),
migrations.RunPython(migrate_scope_forward, migrate_scope_backward),
migrations.RemoveField(
model_name='process',
name='into_wm_mgroup',
),
]

View File

@ -9,6 +9,28 @@ from django.utils import timezone
from django.db.models import Sum, Q, Value, F, ExpressionWrapper, DecimalField from django.db.models import Sum, Q, Value, F, ExpressionWrapper, DecimalField
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
class WmScope(models.IntegerChoices):
MGROUP = 10, '工段'
DEPT = 20, '部门'
GLOBAL = 30, '全局'
@classmethod
def resolve_location(cls, scope, mgroup):
"""将库存归属策略统一解析为 WMaterial 的位置字段。"""
scope = cls(scope)
if scope == cls.GLOBAL:
return {'mgroup': None, 'belong_dept': None}
if mgroup is None:
raise ParseError('工段或部门库存必须提供工段')
if scope == cls.MGROUP:
return {'mgroup': mgroup, 'belong_dept': mgroup.belong_dept}
return {'mgroup': None, 'belong_dept': mgroup.belong_dept}
@classmethod
def requires_mgroup(cls, scope):
return cls(scope) == cls.MGROUP
class Process(CommonBModel): class Process(CommonBModel):
""" """
TN:工序 TN:工序
@ -29,7 +51,8 @@ class Process(CommonBModel):
instruction = models.ForeignKey( instruction = models.ForeignKey(
File, verbose_name='指导书', on_delete=models.SET_NULL, null=True, blank=True) File, verbose_name='指导书', on_delete=models.SET_NULL, null=True, blank=True)
instruction_content = models.TextField('指导书内容', null=True, blank=True) instruction_content = models.TextField('指导书内容', null=True, blank=True)
into_wm_mgroup = models.BooleanField('交接到工段', default=False) into_wm_scope = models.PositiveSmallIntegerField(
'产出库存归属', choices=WmScope.choices, default=WmScope.DEPT)
store_notok = models.BooleanField('不合格品是否入库', default=False) store_notok = models.BooleanField('不合格品是否入库', default=False)
batch_append_equip = models.BooleanField('批号追加设备', default=False) batch_append_equip = models.BooleanField('批号追加设备', default=False)
mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False) mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False)

View File

@ -123,7 +123,7 @@ class WMaterialFilter(filters.FilterSet):
"material__name": ["exact", "in", "contains"], "material__name": ["exact", "in", "contains"],
"material__process": ["exact", "in"], "material__process": ["exact", "in"],
"material__process__name": ["exact", "contains", "in"], "material__process__name": ["exact", "contains", "in"],
"belong_dept": ["exact"], "belong_dept": ["exact", "isnull"],
"belong_dept__name": ["exact", "in"], "belong_dept__name": ["exact", "in"],
"batch": ["exact", "contains"], "batch": ["exact", "contains"],
"mgroup": ["exact", "in", "isnull"], "mgroup": ["exact", "in", "isnull"],

View File

@ -0,0 +1,37 @@
from django.db import migrations, models
def migrate_scope_forward(apps, schema_editor):
Mlog = apps.get_model('wpm', 'Mlog')
Mlog.objects.filter(stored_mgroup=True).update(stored_scope=10)
def migrate_scope_backward(apps, schema_editor):
Mlog = apps.get_model('wpm', 'Mlog')
Mlog.objects.filter(stored_scope=10).update(stored_mgroup=True)
Mlog.objects.exclude(stored_scope=10).update(stored_mgroup=False)
class Migration(migrations.Migration):
dependencies = [
('mtm', '0068_process_into_wm_scope'),
('wpm', '0134_alter_mlog_clear_defect'),
]
operations = [
migrations.AddField(
model_name='mlog',
name='stored_scope',
field=models.PositiveSmallIntegerField(
choices=[(10, '工段'), (20, '部门'), (30, '全局')],
default=20,
verbose_name='产出库存归属',
),
),
migrations.RunPython(migrate_scope_forward, migrate_scope_backward),
migrations.RemoveField(
model_name='mlog',
name='stored_mgroup',
),
]

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from apps.utils.models import CommonADModel, CommonBDModel, BaseModel from apps.utils.models import CommonADModel, CommonBDModel, BaseModel
from apps.mtm.models import Mgroup, Team, Shift, Material, Route, Process from apps.mtm.models import Mgroup, Team, Shift, Material, Route, Process, WmScope
from apps.pm.models import Mtask, Mtaskb from apps.pm.models import Mtask, Mtaskb
from apps.system.models import User from apps.system.models import User
from django.utils.timezone import localtime from django.utils.timezone import localtime
@ -103,6 +103,9 @@ class WmStateOption(models.IntegerChoices):
class WMaterial(CommonBDModel): class WMaterial(CommonBDModel):
""" """
TN: 车间库存 TN: 车间库存
归属范围分三级mgroup 指定工段只有 belong_dept 指定部门
两者都为空表示全局库存
""" """
WM_OK = 10 WM_OK = 10
WM_NOTOK = 20 WM_NOTOK = 20
@ -144,7 +147,11 @@ class WMaterial(CommonBDModel):
@property @property
def belong_dept_or_mgroup_id(self): def belong_dept_or_mgroup_id(self):
return self.mgroup.id if self.mgroup else self.belong_dept.id if self.mgroup_id:
return ("mgroup", self.mgroup_id)
if self.belong_dept_id:
return ("belong_dept", self.belong_dept_id)
return ("global", None)
@property @property
def count_working(self): def count_working(self):
@ -171,7 +178,6 @@ class WMaterial(CommonBDModel):
if qs is None: if qs is None:
qs = cls.objects qs = cls.objects
return WMaterial.ava_qs(mgroup=mtask.mgroup, qs=qs).filter( return WMaterial.ava_qs(mgroup=mtask.mgroup, qs=qs).filter(
mgroup=mtask.mgroup,
material=mtask.material_in, material=mtask.material_in,
batch__in=Subquery( batch__in=Subquery(
Mlogb.objects.filter( Mlogb.objects.filter(
@ -240,7 +246,8 @@ class Mlog(CommonADModel):
work_end_time = models.DateTimeField('生产结束时间', null=True, blank=True) work_end_time = models.DateTimeField('生产结束时间', null=True, blank=True)
hour_work = models.FloatField('预计工时', null=True, blank=True) hour_work = models.FloatField('预计工时', null=True, blank=True)
reminder_interval_list = models.JSONField('提醒间隔', default=list, blank=True) reminder_interval_list = models.JSONField('提醒间隔', default=list, blank=True)
stored_mgroup = models.BooleanField('入库到工段', default=False) stored_scope = models.PositiveSmallIntegerField(
'产出库存归属', choices=WmScope.choices, default=WmScope.DEPT)
stored_notok = models.BooleanField('不合格品是否已入库', default=False) stored_notok = models.BooleanField('不合格品是否已入库', default=False)
clear_defect = models.BooleanField('合格B类记为合格', default=False) clear_defect = models.BooleanField('合格B类记为合格', default=False)
route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True) route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True)

View File

@ -12,7 +12,7 @@ from apps.system.serializers import UserSimpleSerializer
from apps.pm.models import Mtask, Mtaskb from apps.pm.models import Mtask, Mtaskb
from apps.wpm.tasks import cal_enstat_when_pcoal_heat_change, cal_enstat_when_team_change, cal_exp_duration_sec from apps.wpm.tasks import cal_enstat_when_pcoal_heat_change, cal_enstat_when_team_change, cal_exp_duration_sec
from apps.wpm.services import get_sflog, find_material_can_change, generate_new_batch from apps.wpm.services import get_sflog, find_material_can_change, generate_new_batch
from apps.mtm.models import Mgroup, TeamMember, Shift, Material, Route, Process from apps.mtm.models import Mgroup, TeamMember, Shift, Material, Route, Process, WmScope
from apps.mtm.serializers import MaterialSimpleSerializer from apps.mtm.serializers import MaterialSimpleSerializer
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
@ -232,16 +232,19 @@ class WMaterialSerializer(CustomModelSerializer):
class WMaterialCreateSerializer(CustomModelSerializer): class WMaterialCreateSerializer(CustomModelSerializer):
class Meta: class Meta:
model = WMaterial model = WMaterial
fields = ['material', 'count', 'batch', 'mgroup'] fields = ['material', 'count', 'batch', 'mgroup', 'belong_dept']
extra_kwargs = { extra_kwargs = {
'material': {'required': True}, 'material': {'required': True},
'count': {'required': True}, 'count': {'required': True},
'batch': {'required': True}, 'batch': {'required': True},
'mgroup': {'required': True, 'allow_null': False}, 'mgroup': {'required': False, 'allow_null': True},
'belong_dept': {'required': False, 'allow_null': True},
} }
def validate(self, attrs): def validate(self, attrs):
attrs['belong_dept'] = attrs['mgroup'].belong_dept mgroup = attrs.get('mgroup')
if mgroup:
attrs['belong_dept'] = mgroup.belong_dept
return attrs return attrs
@ -1300,7 +1303,12 @@ class HandoverSerializer(CustomModelSerializer):
attrs['send_dept'] = wm.belong_dept attrs['send_dept'] = wm.belong_dept
if wm.mgroup: if wm.mgroup:
attrs['send_mgroup'] = wm.mgroup attrs['send_mgroup'] = wm.mgroup
if attrs['material'].process and attrs['material'].process.into_wm_mgroup and 'recive_mgroup' not in attrs: material_process = attrs['material'].process
if (
material_process
and WmScope.requires_mgroup(material_process.into_wm_scope)
and not attrs.get('recive_mgroup')
):
raise ParseError('必须指定交接工段') raise ParseError('必须指定交接工段')
if 'recive_mgroup' in attrs and attrs['recive_mgroup']: if 'recive_mgroup' in attrs and attrs['recive_mgroup']:
attrs['recive_dept'] = attrs['recive_mgroup'].belong_dept attrs['recive_dept'] = attrs['recive_mgroup'].belong_dept

View File

@ -21,7 +21,7 @@ from apps.wpmw.models import Wpr, WprDefect
from ..qm.models import Defect, Ftest from ..qm.models import Defect, Ftest
from django.db.models import Count, Q from django.db.models import Count, Q
from apps.utils.tasks import ctask_run from apps.utils.tasks import ctask_run
from apps.mtm.models import Process from apps.mtm.models import Process, WmScope
from django.db.models import F from django.db.models import F
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
@ -187,7 +187,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mgroup = mlog.mgroup mgroup = mlog.mgroup
process = mgroup.process process = mgroup.process
stored_mgroup = process.into_wm_mgroup stored_scope = process.into_wm_scope
stored_notok = process.store_notok stored_notok = process.store_notok
belong_dept = mgroup.belong_dept belong_dept = mgroup.belong_dept
material_out: Material = mlog.material_out material_out: Material = mlog.material_out
@ -195,7 +195,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
supplier = mlog.supplier # 外协 supplier = mlog.supplier # 外协
is_fix = mlog.is_fix is_fix = mlog.is_fix
if is_fix: # 如果是返工,直接放到工段下 if is_fix: # 如果是返工,直接放到工段下
stored_mgroup = True stored_scope = WmScope.MGROUP
stored_location = WmScope.resolve_location(stored_scope, mgroup)
m_ins_list = [] m_ins_list = []
m_ins_bl_list = [] m_ins_bl_list = []
@ -237,15 +238,23 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
continue continue
if mi_count <= 0: if mi_count <= 0:
raise ParseError('存在非正数!') raise ParseError('存在非正数!')
# 需要判断领用数是否合理 # 优先使用日志已选库存;未选择时按工段、部门、全局依次匹配并记录实际来源
# 优先使用工段库存 wm_in = getattr(mlog_or_b, 'wm_in', None)
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: if wm_in:
wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id) wm = WMaterial.objects.select_for_update().get(id=wm_in.id)
else: else:
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK) wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, mgroup=mgroup, state=WMaterial.WM_OK)
if not wm_qs.exists(): if not wm_qs.exists():
wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma, wm_qs = WMaterial.objects.filter(batch=mi_batch, material=mi_ma,
belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK) belong_dept=belong_dept, mgroup=None, state=WMaterial.WM_OK)
if not wm_qs.exists():
wm_qs = WMaterial.objects.filter(
batch=mi_batch,
material=mi_ma,
belong_dept=None,
mgroup=None,
state=WMaterial.WM_OK,
)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
wm = WMaterial.objects.select_for_update().get(id=wm_qs.first().id) wm = WMaterial.objects.select_for_update().get(id=wm_qs.first().id)
@ -255,6 +264,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
else: else:
raise ParseError( raise ParseError(
f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!') f'{str(mi_ma)}-{mi_batch}-存在多个相同批次!')
mlog_or_b.wm_in = wm
mlog_or_b.save(update_fields=['wm_in'])
if mi_count > wm.count: if mi_count > wm.count:
raise ParseError( raise ParseError(
@ -280,8 +291,14 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
c_state = WMaterial.WM_NOTOK c_state = WMaterial.WM_NOTOK
if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B: if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B:
c_state = WMaterial.WM_OK c_state = WMaterial.WM_OK
lookup = {'batch': batch, 'material': material, 'mgroup': mgroup, 'defect': defect, 'state': c_state} lookup = {
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) 'batch': batch,
'material': material,
'defect': defect,
'state': c_state,
**stored_location,
}
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={})
wm.count = wm.count + count wm.count = wm.count + count
if is_create: if is_create:
wm.create_by = user wm.create_by = user
@ -359,18 +376,19 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or ( wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B] isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
) else WMaterial.WM_NOTOK ) else WMaterial.WM_NOTOK
lookup = {'batch': mo_batch, 'material': mo_ma, 'mgroup': None, lookup = {
'notok_sign': None, 'defect': None, 'state': wm_state} 'batch': mo_batch,
'material': mo_ma,
'notok_sign': None,
'defect': None,
'state': wm_state,
**stored_location,
}
if isinstance(notok_sign_or_defect, Defect): if isinstance(notok_sign_or_defect, Defect):
lookup['defect'] = notok_sign_or_defect lookup['defect'] = notok_sign_or_defect
elif notok_sign_or_defect is not None: elif notok_sign_or_defect is not None:
lookup['notok_sign'] = notok_sign_or_defect lookup['notok_sign'] = notok_sign_or_defect
if stored_mgroup: wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={})
lookup['mgroup'] = mgroup
else:
lookup['belong_dept'] = belong_dept
wm, is_create2 = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count + mo_count wm.count = wm.count + mo_count
wm.count_eweight = mo_count_eweight wm.count_eweight = mo_count_eweight
wm.update_by = user wm.update_by = user
@ -415,7 +433,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlog.submit_time = now mlog.submit_time = now
mlog.submit_user = user mlog.submit_user = user
mlog.stored_notok = stored_notok mlog.stored_notok = stored_notok
mlog.stored_mgroup = stored_mgroup mlog.stored_scope = stored_scope
if mlog.work_end_time is None and mlog.work_start_time is not None: if mlog.work_end_time is None and mlog.work_start_time is not None:
mlog.work_end_time = now mlog.work_end_time = now
mlog.save() mlog.save()
@ -448,7 +466,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
material_out:Material = mlog.material_out material_out:Material = mlog.material_out
material_in:Material = mlog.material_in material_in:Material = mlog.material_in
stored_notok = mlog.stored_notok stored_notok = mlog.stored_notok
stored_mgroup = mlog.stored_mgroup stored_scope = mlog.stored_scope
stored_location = WmScope.resolve_location(stored_scope, mgroup)
is_fix = mlog.is_fix is_fix = mlog.is_fix
if is_fix: if is_fix:
@ -523,15 +542,18 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or ( wm_state = WMaterial.WM_OK if notok_sign_or_defect is None or (
isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B] isinstance(notok_sign_or_defect, Defect) and notok_sign_or_defect.okcate in [Defect.DEFECT_OK, Defect.DEFECT_OK_B]
) else WMaterial.WM_NOTOK ) else WMaterial.WM_NOTOK
lookup = {'batch': mo_batch, 'material': mo_ma, 'mgroup': None, 'notok_sign': None, 'defect': None, 'state': wm_state} lookup = {
'batch': mo_batch,
'material': mo_ma,
'notok_sign': None,
'defect': None,
'state': wm_state,
**stored_location,
}
if isinstance(notok_sign_or_defect, Defect): if isinstance(notok_sign_or_defect, Defect):
lookup['defect'] = notok_sign_or_defect lookup['defect'] = notok_sign_or_defect
else: else:
lookup['notok_sign'] = notok_sign_or_defect lookup['notok_sign'] = notok_sign_or_defect
if stored_mgroup:
lookup['mgroup'] = mgroup
else:
lookup['belong_dept'] = belong_dept
wm_qs = WMaterial.objects.filter(**lookup) wm_qs = WMaterial.objects.filter(**lookup)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
@ -584,18 +606,18 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
continue continue
if mi_count <= 0: if mi_count <= 0:
raise ParseError('存在非正数!') raise ParseError('存在非正数!')
if isinstance(mlog_or_b, Mlogb) and mlog_or_b.wm_in: wm_in = getattr(mlog_or_b, 'wm_in', None)
wm = WMaterial.objects.select_for_update().get(id=mlog_or_b.wm_in.id) if wm_in:
wm = WMaterial.objects.select_for_update().get(id=wm_in.id)
else: else:
# 针对光子的情况实际上必须需要wm_in # 针对光子的情况实际上必须需要wm_in
lookup = {'batch': mi_batch, 'material': mi_ma, 'mgroup': None, 'state': WMaterial.WM_OK} lookup = {
if stored_mgroup: 'batch': mi_batch,
# 退回到本工段 'material': mi_ma,
lookup['mgroup'] = mgroup 'state': WMaterial.WM_OK,
else: **stored_location,
lookup['belong_dept'] = belong_dept }
wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={})
wm, _ = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept})
wm.count = wm.count + mi_count wm.count = wm.count + mi_count
wm.update_by = user wm.update_by = user
wm.save() wm.save()
@ -615,12 +637,14 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
c_state = WMaterial.WM_NOTOK c_state = WMaterial.WM_NOTOK
if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B: if isinstance(defect, Defect) and defect.okcate == Defect.DEFECT_OK_B:
c_state = WMaterial.WM_OK c_state = WMaterial.WM_OK
lookup = {'batch': batch, 'material': material, 'mgroup': None, 'defect': defect, 'state': c_state} lookup = {
if stored_mgroup: 'batch': batch,
lookup['mgroup'] = mgroup 'material': material,
else: 'defect': defect,
lookup['belong_dept'] = belong_dept 'state': c_state,
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={"belong_dept": belong_dept}) **stored_location,
}
wm, is_create = WMaterial.locked_get_or_create(**lookup, defaults={})
wm.count = wm.count - count wm.count = wm.count - count
if wm.count < 0: if wm.count < 0:
raise ParseError('加工前不良数量大于库存量') raise ParseError('加工前不良数量大于库存量')

View File

@ -3,7 +3,10 @@ from unittest.mock import MagicMock, patch
from django.test import SimpleTestCase from django.test import SimpleTestCase
from apps.mtm.models import Material 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.views import MlogbwViewSet from apps.wpm.views import MlogbwViewSet
@ -59,3 +62,80 @@ class MlogbwViewSetTests(SimpleTestCase):
) )
self.assertEqual(cal_count_notok.call_count, 2) self.assertEqual(cal_count_notok.call_count, 2)
mlog.cal_mlog_count_from_mlogb.assert_called_once_with() mlog.cal_mlog_count_from_mlogb.assert_called_once_with()
class WMaterialScopeTests(SimpleTestCase):
def test_scope_resolver_returns_consistent_location_fields(self):
dept = object()
mgroup = SimpleNamespace(belong_dept=dept)
self.assertEqual(
WmScope.resolve_location(WmScope.MGROUP, mgroup),
{"mgroup": mgroup, "belong_dept": dept},
)
self.assertEqual(
WmScope.resolve_location(WmScope.DEPT, mgroup),
{"mgroup": None, "belong_dept": dept},
)
self.assertEqual(
WmScope.resolve_location(WmScope.GLOBAL, mgroup),
{"mgroup": None, "belong_dept": None},
)
def test_only_mgroup_scope_requires_handover_mgroup(self):
self.assertTrue(WmScope.requires_mgroup(WmScope.MGROUP))
self.assertFalse(WmScope.requires_mgroup(WmScope.DEPT))
self.assertFalse(WmScope.requires_mgroup(WmScope.GLOBAL))
def test_global_scope_key_does_not_access_missing_relations(self):
wm = WMaterial()
self.assertEqual(wm.belong_dept_or_mgroup_id, ("global", None))
def test_department_and_mgroup_scope_keys_cannot_collide(self):
dept_wm = WMaterial(belong_dept_id=10)
mgroup_wm = WMaterial(mgroup_id=10)
self.assertNotEqual(
dept_wm.belong_dept_or_mgroup_id,
mgroup_wm.belong_dept_or_mgroup_id,
)
def test_manual_create_allows_global_scope(self):
serializer = WMaterialCreateSerializer()
attrs = {
"material": Material(),
"count": 1,
"batch": "GLOBAL-001",
}
validated = serializer.validate(attrs)
self.assertFalse(serializer.fields["mgroup"].required)
self.assertFalse(serializer.fields["belong_dept"].required)
self.assertNotIn("mgroup", validated)
self.assertNotIn("belong_dept", validated)
def test_global_scope_can_be_filtered_explicitly(self):
self.assertIn(
"isnull",
WMaterialFilter.Meta.fields["belong_dept"],
)
self.assertIn(
"isnull",
WMaterialFilter.Meta.fields["mgroup"],
)
def test_manual_create_derives_department_from_mgroup(self):
dept = SimpleNamespace(id=20)
mgroup = SimpleNamespace(id=10, belong_dept=dept)
attrs = {
"material": Material(),
"count": 1,
"batch": "MGROUP-001",
"mgroup": mgroup,
}
validated = WMaterialCreateSerializer().validate(attrs)
self.assertIs(validated["belong_dept"], dept)