Compare commits
No commits in common. "6068f2315d0e4c333ddc62dad7b11136000d6d2d" and "bcbb27374fc1c796e16cf2f34dcee73af56e5975" have entirely different histories.
6068f2315d
...
bcbb27374f
|
|
@ -436,18 +436,11 @@ class InmService:
|
||||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||||
for mioitemw in mioitemws:
|
for mioitemw in mioitemws:
|
||||||
if mioitemw.wpr:
|
if mioitemw.wpr:
|
||||||
wpr = Wpr.change_or_new(
|
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, ftest=mioitemw.ftest)
|
||||||
wpr=mioitemw.wpr,
|
|
||||||
mb=mb,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
wpr = Wpr.change_or_new(
|
wpr = Wpr.change_or_new(number=mioitemw.number, mb=mb, ftest=mioitemw.ftest)
|
||||||
number=mioitemw.number,
|
|
||||||
mb=mb,
|
|
||||||
)
|
|
||||||
mioitemw.wpr = wpr
|
mioitemw.wpr = wpr
|
||||||
mioitemw.save()
|
mioitemw.save()
|
||||||
wpr.apply_ftest(mioitemw.ftest)
|
|
||||||
|
|
||||||
elif in_or_out == -1:
|
elif in_or_out == -1:
|
||||||
mb.count = mb.count - change_count
|
mb.count = mb.count - change_count
|
||||||
|
|
|
||||||
|
|
@ -221,9 +221,7 @@ class RouteSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
if attrs.get('material_out') and attrs.get('material_out_create'):
|
if attrs.get('material_out') and attrs.get('material_out_create'):
|
||||||
raise serializers.ValidationError(
|
raise ParseError('选择已有输出物料和新建半成品不可同时提交')
|
||||||
'选择已有输出物料和新建半成品不可同时提交'
|
|
||||||
)
|
|
||||||
if attrs.get('routepack', None):
|
if attrs.get('routepack', None):
|
||||||
attrs['material'] = attrs['routepack'].material
|
attrs['material'] = attrs['routepack'].material
|
||||||
if 'mgroup' in attrs and attrs['mgroup']:
|
if 'mgroup' in attrs and attrs['mgroup']:
|
||||||
|
|
|
||||||
|
|
@ -189,10 +189,7 @@ class BaseModel(models.Model):
|
||||||
is_create = False
|
is_create = False
|
||||||
if not self.id:
|
if not self.id:
|
||||||
is_create = True
|
is_create = True
|
||||||
self.id = str(idWorker.get_id())
|
self.id = idWorker.get_id()
|
||||||
else:
|
|
||||||
# BaseModel 的主键是 CharField,保证新建后内存值与数据库回读值类型一致。
|
|
||||||
self.id = str(self.id)
|
|
||||||
|
|
||||||
old_parent = None
|
old_parent = None
|
||||||
need_handle_parent = False
|
need_handle_parent = False
|
||||||
|
|
@ -212,7 +209,7 @@ class BaseModel(models.Model):
|
||||||
except IntegrityError as e:
|
except IntegrityError as e:
|
||||||
if is_create:
|
if is_create:
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
self.id = str(idWorker.get_id())
|
self.id = idWorker.get_id()
|
||||||
ins = super().save(*args, **kwargs)
|
ins = super().save(*args, **kwargs)
|
||||||
raise e
|
raise e
|
||||||
# 处理父级
|
# 处理父级
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,14 @@
|
||||||
from datetime import timedelta
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
from apps.hrm.models import Employee
|
from apps.hrm.models import Employee
|
||||||
from apps.hrm.services import HrmService
|
from apps.hrm.services import HrmService
|
||||||
|
|
||||||
from apps.vm.models import Visit
|
from apps.vm.models import Visit
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
|
|
||||||
|
|
||||||
class VisitTest(TestCase):
|
class VisitTest(TestCase):
|
||||||
@patch.object(HrmService, 'sync_dahua_employee')
|
def test_audit_end(self):
|
||||||
def test_sync_employee_uses_visit_time_window(self, sync_employee):
|
v = Visit.objects.get(id='1547401054033940480')
|
||||||
visit_time = timezone.now()
|
ep = Employee.objects.get(id='1547406632055607296')
|
||||||
leave_time = visit_time + timedelta(hours=2)
|
HrmService.sync_dahua_employee(ep, '', v.visit_time, v.leave_time)
|
||||||
visit = Visit.objects.create(
|
|
||||||
purpose=Visit.V_PURPOSE_CHOICES[0][0],
|
|
||||||
level=Visit.V_LEVEL_CHOICES[0][0],
|
|
||||||
name='访客同步测试',
|
|
||||||
visit_time=visit_time,
|
|
||||||
leave_time=leave_time,
|
|
||||||
)
|
|
||||||
employee = Employee.objects.create(name='测试访客', type='visitor')
|
|
||||||
|
|
||||||
HrmService.sync_dahua_employee(
|
|
||||||
employee,
|
|
||||||
'',
|
|
||||||
visit.visit_time,
|
|
||||||
visit.leave_time,
|
|
||||||
)
|
|
||||||
|
|
||||||
sync_employee.assert_called_once_with(
|
|
||||||
employee,
|
|
||||||
'',
|
|
||||||
visit_time,
|
|
||||||
leave_time,
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -132,8 +132,7 @@ class WMaterialFilter(filters.FilterSet):
|
||||||
"notok_sign": ["exact", "in", "isnull"],
|
"notok_sign": ["exact", "in", "isnull"],
|
||||||
"count_xtest": ["gte", "isnull"],
|
"count_xtest": ["gte", "isnull"],
|
||||||
"supplier": ["exact"],
|
"supplier": ["exact"],
|
||||||
"defect": ["exact", "isnull"],
|
"defect": ["exact", "isnull"]
|
||||||
"defect__okcate": ["exact", "in"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MlogFilter(filters.FilterSet):
|
class MlogFilter(filters.FilterSet):
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# Generated by Django 4.2.27 on 2026-07-30 01:06
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wpm', '0136_alter_handover_global_scope_fields'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='mlog',
|
|
||||||
name='wpr_defect_snapshot',
|
|
||||||
field=models.JSONField(blank=True, editable=False, null=True, verbose_name='单件缺陷提交前快照'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
@ -367,12 +367,6 @@ class Mlog(CommonADModel):
|
||||||
'产出库存归属', choices=WmScope.choices, default=WmScope.DEPT)
|
'产出库存归属', 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)
|
||||||
wpr_defect_snapshot = models.JSONField(
|
|
||||||
'单件缺陷提交前快照',
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
editable=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)
|
||||||
mtask = models.ForeignKey(
|
mtask = models.ForeignKey(
|
||||||
Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask')
|
Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask')
|
||||||
|
|
|
||||||
|
|
@ -189,21 +189,12 @@ class WMaterialSerializer(CustomModelSerializer):
|
||||||
material_origin_name = serializers.StringRelatedField(source='material_origin', read_only=True)
|
material_origin_name = serializers.StringRelatedField(source='material_origin', read_only=True)
|
||||||
notok_sign_name = serializers.SerializerMethodField()
|
notok_sign_name = serializers.SerializerMethodField()
|
||||||
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
||||||
defect_okcate = serializers.IntegerField(source="defect.okcate", read_only=True)
|
|
||||||
defect_grade = serializers.SerializerMethodField()
|
|
||||||
defect_grade_name = serializers.SerializerMethodField()
|
|
||||||
count_working = serializers.SerializerMethodField(label='在制数量')
|
count_working = serializers.SerializerMethodField(label='在制数量')
|
||||||
count_handovering = serializers.SerializerMethodField(label='正在交送的数量')
|
count_handovering = serializers.SerializerMethodField(label='正在交送的数量')
|
||||||
|
|
||||||
def get_notok_sign_name(self, obj):
|
def get_notok_sign_name(self, obj):
|
||||||
return getattr(NotOkOption, obj.notok_sign, NotOkOption.qt).label if obj.notok_sign else None
|
return getattr(NotOkOption, obj.notok_sign, NotOkOption.qt).label if obj.notok_sign else None
|
||||||
|
|
||||||
def get_defect_grade(self, obj):
|
|
||||||
return obj.defect.okcate if obj.defect else None
|
|
||||||
|
|
||||||
def get_defect_grade_name(self, obj):
|
|
||||||
return obj.defect.get_okcate_display() if obj.defect else None
|
|
||||||
|
|
||||||
def get_count_working(self, obj):
|
def get_count_working(self, obj):
|
||||||
# 列表接口 queryset 已注解(单次聚合); 嵌套等无注解场景回退模型属性
|
# 列表接口 queryset 已注解(单次聚合); 嵌套等无注解场景回退模型属性
|
||||||
# 保留 decimal 精度(原 IntegerField 会截断在途量, 导致可用/可交接量偏大)
|
# 保留 decimal 精度(原 IntegerField 会截断在途量, 导致可用/可交接量偏大)
|
||||||
|
|
@ -356,7 +347,8 @@ class MlogListSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mlog
|
model = Mlog
|
||||||
exclude = ["wpr_defect_snapshot"]
|
fields = '__all__'
|
||||||
|
# exclude = ["equipments","handle_users", "material_outs"]
|
||||||
|
|
||||||
|
|
||||||
class MlogbDetailSerializer(CustomModelSerializer):
|
class MlogbDetailSerializer(CustomModelSerializer):
|
||||||
|
|
@ -427,7 +419,7 @@ class MlogSerializer(CustomModelSerializer):
|
||||||
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
|
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mlog
|
model = Mlog
|
||||||
exclude = ["wpr_defect_snapshot"]
|
fields = '__all__'
|
||||||
read_only_fields = EXCLUDE_FIELDS + \
|
read_only_fields = EXCLUDE_FIELDS + \
|
||||||
['submit_time', 'submit_user', 'material_outs']
|
['submit_time', 'submit_user', 'material_outs']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
|
|
@ -1288,9 +1280,6 @@ class HandoverSerializer(CustomModelSerializer):
|
||||||
attrs['type'] = Handover.H_NORMAL
|
attrs['type'] = Handover.H_NORMAL
|
||||||
|
|
||||||
mtype = attrs["mtype"]
|
mtype = attrs["mtype"]
|
||||||
clear_defect = bool(attrs.get('clear_defect', False))
|
|
||||||
if clear_defect and mtype != Handover.H_MERGE:
|
|
||||||
raise ParseError('仅合批时可清除批次缺陷')
|
|
||||||
|
|
||||||
new_wm:WMaterial = attrs.get("new_wm", None)
|
new_wm:WMaterial = attrs.get("new_wm", None)
|
||||||
if new_wm:
|
if new_wm:
|
||||||
|
|
@ -1415,17 +1404,11 @@ class HandoverSerializer(CustomModelSerializer):
|
||||||
elif deptOrmgroupId != current_mdept_id:
|
elif deptOrmgroupId != current_mdept_id:
|
||||||
raise ParseError(f'第{ind+1}行-交接物料所属工段/车间不一致')
|
raise ParseError(f'第{ind+1}行-交接物料所属工段/车间不一致')
|
||||||
if mtype == Handover.H_MERGE:
|
if mtype == Handover.H_MERGE:
|
||||||
|
clear_defect = attrs.get('clear_defect', False)
|
||||||
|
if clear_defect and wm.state != WMaterial.WM_OK:
|
||||||
|
raise ParseError(f'第{ind+1}行-清除缺陷合批仅支持合格品')
|
||||||
if clear_defect and new_wm is not None and new_wm.defect is not None:
|
if clear_defect and new_wm is not None and new_wm.defect is not None:
|
||||||
raise ParseError('清除批次缺陷时目标批次不能带缺陷')
|
raise ParseError('清除缺陷合批的目标批次不能带缺陷')
|
||||||
if clear_defect and tracking == Material.MA_TRACKING_BATCH:
|
|
||||||
if (
|
|
||||||
wm.state != WMaterial.WM_OK
|
|
||||||
or wm.defect is None
|
|
||||||
or wm.defect.okcate != Defect.DEFECT_OK_B
|
|
||||||
):
|
|
||||||
raise ParseError(
|
|
||||||
f'第{ind+1}行-批次追踪物料仅合格B类可清除批次缺陷'
|
|
||||||
)
|
|
||||||
if next_mat is None:
|
if next_mat is None:
|
||||||
next_mat = wm.material
|
next_mat = wm.material
|
||||||
next_state = wm.state
|
next_state = wm.state
|
||||||
|
|
@ -1550,7 +1533,7 @@ class HandoverSerializer(CustomModelSerializer):
|
||||||
elif count == wm.count:
|
elif count == wm.count:
|
||||||
wpr_qs = Wpr.get_qs_by_wm(wm)
|
wpr_qs = Wpr.get_qs_by_wm(wm)
|
||||||
for item in wpr_qs:
|
for item in wpr_qs:
|
||||||
Handoverbw.objects.get_or_create(wpr=item, handoverb=hb,
|
Handoverbw.objects.get_or_create(wpr=item, handoverb=handoverb,
|
||||||
defaults={"number": item.number})
|
defaults={"number": item.number})
|
||||||
else:
|
else:
|
||||||
raise ParseError(f'第{ind+1}行-请提供交接物料明细')
|
raise ParseError(f'第{ind+1}行-请提供交接物料明细')
|
||||||
|
|
|
||||||
|
|
@ -165,73 +165,6 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
|
||||||
return 25000
|
return 25000
|
||||||
|
|
||||||
|
|
||||||
def snapshot_mlog_wpr_defects(mlog: Mlog):
|
|
||||||
"""锁定并保存日志提交前已有单件的缺陷状态。"""
|
|
||||||
wpr_ids = list(Mlogbw.objects.filter(
|
|
||||||
mlogb__mlog=mlog,
|
|
||||||
wpr__isnull=False,
|
|
||||||
).values_list("wpr_id", flat=True).distinct())
|
|
||||||
list(Wpr.objects.select_for_update().filter(
|
|
||||||
id__in=wpr_ids,
|
|
||||||
).values_list("id", flat=True))
|
|
||||||
|
|
||||||
snapshot = {str(wpr_id): [] for wpr_id in wpr_ids}
|
|
||||||
for defect in WprDefect.objects.filter(wpr_id__in=wpr_ids).values(
|
|
||||||
"wpr_id",
|
|
||||||
"defect_id",
|
|
||||||
"is_main",
|
|
||||||
):
|
|
||||||
snapshot[str(defect["wpr_id"])].append({
|
|
||||||
"defect_id": str(defect["defect_id"]),
|
|
||||||
"is_main": defect["is_main"],
|
|
||||||
})
|
|
||||||
mlog.wpr_defect_snapshot = snapshot
|
|
||||||
mlog.save(update_fields=["wpr_defect_snapshot", "update_time"])
|
|
||||||
return snapshot
|
|
||||||
|
|
||||||
|
|
||||||
def restore_mlog_wpr_defects(mlog: Mlog):
|
|
||||||
"""按快照精确恢复日志提交前的单件缺陷。"""
|
|
||||||
snapshot = mlog.wpr_defect_snapshot
|
|
||||||
if snapshot is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
wpr_ids = list(snapshot)
|
|
||||||
wprs = {
|
|
||||||
str(wpr.id): wpr
|
|
||||||
for wpr in Wpr.objects.select_for_update().filter(id__in=wpr_ids)
|
|
||||||
}
|
|
||||||
WprDefect.objects.filter(wpr_id__in=wpr_ids).delete()
|
|
||||||
restored = []
|
|
||||||
for wpr_id, defects in snapshot.items():
|
|
||||||
wpr = wprs.get(str(wpr_id))
|
|
||||||
if wpr is None:
|
|
||||||
myLogger.warning(
|
|
||||||
"生产日志%s测回时未找到快照中的单件%s",
|
|
||||||
mlog.id,
|
|
||||||
wpr_id,
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
restored.extend(
|
|
||||||
WprDefect(
|
|
||||||
wpr=wpr,
|
|
||||||
defect_id=item["defect_id"],
|
|
||||||
is_main=item.get("is_main", False),
|
|
||||||
)
|
|
||||||
for item in defects
|
|
||||||
)
|
|
||||||
WprDefect.objects.bulk_create(restored)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def restore_legacy_wpr_defects_from_wm(wpr: Wpr, wm: WMaterial):
|
|
||||||
"""兼容无快照历史日志的旧测回行为。"""
|
|
||||||
defects = []
|
|
||||||
if wm.defect_id:
|
|
||||||
defects.append({"defect_id": wm.defect_id, "is_main": True})
|
|
||||||
wpr.replace_defects(defects)
|
|
||||||
|
|
||||||
|
|
||||||
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
"""
|
"""
|
||||||
生产日志提交后需要执行的操作
|
生产日志提交后需要执行的操作
|
||||||
|
|
@ -267,8 +200,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
m_ins_list = []
|
m_ins_list = []
|
||||||
m_ins_bl_list = []
|
m_ins_bl_list = []
|
||||||
|
|
||||||
wpr_defect_snapshot = snapshot_mlog_wpr_defects(mlog)
|
|
||||||
|
|
||||||
if is_fix:
|
if is_fix:
|
||||||
can_matoutIds = process.get_canout_mat_ids()
|
can_matoutIds = process.get_canout_mat_ids()
|
||||||
# 建立关系链
|
# 建立关系链
|
||||||
|
|
@ -348,8 +279,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mlogbws.count() != mi_count:
|
if mlogbws.count() != mi_count:
|
||||||
raise ParseError("日志与实际使用明细数量不一致,操作失败")
|
raise ParseError("日志与实际使用明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
wpr = Wpr.change_or_new(wpr=item.wpr, old_wm=wm)
|
Wpr.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)
|
||||||
wpr.apply_ftest(item.ftest)
|
|
||||||
|
|
||||||
# 针对加工前不良的暂时额外处理
|
# 针对加工前不良的暂时额外处理
|
||||||
# 合格B类的需要当合格品
|
# 合格B类的需要当合格品
|
||||||
|
|
@ -382,12 +312,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mlogbws.count() != count:
|
if mlogbws.count() != count:
|
||||||
raise ParseError("日志与加工前不良明细数量不一致,操作失败")
|
raise ParseError("日志与加工前不良明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
wpr = Wpr.change_or_new(
|
Wpr.change_or_new(wpr=item.wpr, wm=wm, old_wm=wm, ftest=item.ftest)
|
||||||
wpr=item.wpr,
|
|
||||||
wm=wm,
|
|
||||||
old_wm=wm,
|
|
||||||
)
|
|
||||||
wpr.apply_ftest(item.ftest)
|
|
||||||
|
|
||||||
|
|
||||||
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
|
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
|
||||||
|
|
@ -494,35 +419,18 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
raise ParseError("日志与明细数量不一致,操作失败")
|
raise ParseError("日志与明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
if item.wpr:
|
if item.wpr:
|
||||||
wpr = Wpr.change_or_new(
|
Wpr.change_or_new(wpr=item.wpr, wm=wm, ftest=item.ftest)
|
||||||
wpr=item.wpr,
|
|
||||||
wm=wm,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
if not check_wpr_number(item.number):
|
if not check_wpr_number(item.number):
|
||||||
raise ParseError(f'{item.number} 该编号格式有误')
|
raise ParseError(f'{item.number} 该编号格式有误')
|
||||||
wpr_from = None
|
wpr_from = None
|
||||||
if item.mlogbw_from:
|
if item.mlogbw_from:
|
||||||
wpr_from = item.mlogbw_from.wpr
|
wpr_from = item.mlogbw_from.wpr
|
||||||
wpr = Wpr.change_or_new(
|
wpr = Wpr.change_or_new(number=item.number,
|
||||||
number=item.number,
|
wm=wm, ftest=item.ftest,
|
||||||
wm=wm,
|
wpr_from=wpr_from)
|
||||||
wpr_from=wpr_from,
|
|
||||||
)
|
|
||||||
item.wpr = wpr
|
item.wpr = wpr
|
||||||
item.save()
|
item.save()
|
||||||
wpr_id = str(wpr.id)
|
|
||||||
if wpr_id not in wpr_defect_snapshot:
|
|
||||||
wpr_defect_snapshot[wpr_id] = [
|
|
||||||
{
|
|
||||||
"defect_id": str(defect["defect_id"]),
|
|
||||||
"is_main": defect["is_main"],
|
|
||||||
}
|
|
||||||
for defect in WprDefect.objects.filter(
|
|
||||||
wpr=wpr,
|
|
||||||
).values("defect_id", "is_main")
|
|
||||||
]
|
|
||||||
wpr.apply_ftest(item.ftest)
|
|
||||||
|
|
||||||
mlog.submit_time = now
|
mlog.submit_time = now
|
||||||
mlog.submit_user = user
|
mlog.submit_user = user
|
||||||
|
|
@ -530,7 +438,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
mlog.stored_scope = stored_scope
|
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.wpr_defect_snapshot = wpr_defect_snapshot
|
|
||||||
mlog.save()
|
mlog.save()
|
||||||
|
|
||||||
# 更新任务进度
|
# 更新任务进度
|
||||||
|
|
@ -564,7 +471,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
stored_scope = mlog.stored_scope
|
stored_scope = mlog.stored_scope
|
||||||
stored_location = WmScope.resolve_location(stored_scope, mgroup)
|
stored_location = WmScope.resolve_location(stored_scope, mgroup)
|
||||||
is_fix = mlog.is_fix
|
is_fix = mlog.is_fix
|
||||||
has_wpr_defect_snapshot = mlog.wpr_defect_snapshot is not None
|
|
||||||
|
|
||||||
if is_fix:
|
if is_fix:
|
||||||
can_matoutIds = process.get_canout_mat_ids()
|
can_matoutIds = process.get_canout_mat_ids()
|
||||||
|
|
@ -679,8 +585,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mlogbws.count() != mo_count:
|
if mlogbws.count() != mo_count:
|
||||||
raise ParseError("日志与明细数量不一致,操作失败")
|
raise ParseError("日志与明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
wpr = Wpr.change_or_new(wpr=item.wpr, old_wm=wm)
|
Wpr.change_or_new(wpr=item.wpr, old_wm=wm)
|
||||||
wpr.revert_ftest_info(item.ftest)
|
|
||||||
|
|
||||||
# 再生成消耗
|
# 再生成消耗
|
||||||
m_ins_list = []
|
m_ins_list = []
|
||||||
|
|
@ -724,9 +629,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mlogbws.count() != mi_count:
|
if mlogbws.count() != mi_count:
|
||||||
raise ParseError("日志与实际使用明细数量不一致,操作失败")
|
raise ParseError("日志与实际使用明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
wpr = Wpr.change_or_new(wpr=item.wpr, wm=wm)
|
Wpr.change_or_new(wpr=item.wpr, wm=wm)
|
||||||
if not has_wpr_defect_snapshot:
|
|
||||||
restore_legacy_wpr_defects_from_wm(wpr, wm)
|
|
||||||
|
|
||||||
# 针对加工前不良的暂时额外处理
|
# 针对加工前不良的暂时额外处理
|
||||||
if stored_notok:
|
if stored_notok:
|
||||||
|
|
@ -759,18 +662,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
|
||||||
if mlogbws.count() != count:
|
if mlogbws.count() != count:
|
||||||
raise ParseError("日志与加工前不良明细数量不一致,操作失败")
|
raise ParseError("日志与加工前不良明细数量不一致,操作失败")
|
||||||
for item in mlogbws:
|
for item in mlogbws:
|
||||||
wpr = Wpr.change_or_new(wpr=item.wpr, wm=wm)
|
Wpr.change_or_new(wpr=item.wpr, wm=wm)
|
||||||
if not has_wpr_defect_snapshot:
|
|
||||||
restore_legacy_wpr_defects_from_wm(wpr, wm)
|
|
||||||
|
|
||||||
if has_wpr_defect_snapshot:
|
|
||||||
restore_mlog_wpr_defects(mlog)
|
|
||||||
mlog.wpr_defect_snapshot = None
|
|
||||||
else:
|
|
||||||
myLogger.warning(
|
|
||||||
"生产日志%s无单件缺陷快照,测回按历史兼容逻辑处理,无法精确恢复提交前缺陷",
|
|
||||||
mlog.id,
|
|
||||||
)
|
|
||||||
|
|
||||||
mlog.submit_time = None
|
mlog.submit_time = None
|
||||||
mlog.submit_user = None
|
mlog.submit_user = None
|
||||||
|
|
@ -981,20 +873,14 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount)
|
WMaterial.decrease(wm_id=wm_from.id, user=user, count=xcount)
|
||||||
|
|
||||||
if need_add:
|
if need_add:
|
||||||
target_defect = (
|
|
||||||
merge_target_defect
|
|
||||||
if mtype == Handover.H_MERGE
|
|
||||||
else wm_from.defect
|
|
||||||
)
|
|
||||||
# 开始变动
|
# 开始变动
|
||||||
if handover.type == Handover.H_NORMAL:
|
if handover.type == Handover.H_NORMAL:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if (
|
defect_ok = wm_to.defect == wm_from.defect or (
|
||||||
wm_to.state != wm_from.state
|
merge_clear_defect and wm_to.defect is None and wm_from.state == WMaterial.WM_OK
|
||||||
or wm_to.material != wm_from.material
|
)
|
||||||
or wm_to.defect != target_defect
|
if wm_to.state != wm_from.state or wm_to.material != wm_from.material or not defect_ok:
|
||||||
):
|
|
||||||
raise ParseError("正常合并到的车间库存状态或物料异常")
|
raise ParseError("正常合并到的车间库存状态或物料异常")
|
||||||
else:
|
else:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
|
|
@ -1004,7 +890,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
belong_dept=recive_dept,
|
belong_dept=recive_dept,
|
||||||
state=wm_from.state,
|
state=wm_from.state,
|
||||||
notok_sign=wm_from.notok_sign,
|
notok_sign=wm_from.notok_sign,
|
||||||
defect=target_defect,
|
defect=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
|
||||||
defaults={
|
defaults={
|
||||||
"batch_ofrom": wm_from.batch_ofrom,
|
"batch_ofrom": wm_from.batch_ofrom,
|
||||||
"material_ofrom": wm_from.material_ofrom,
|
"material_ofrom": wm_from.material_ofrom,
|
||||||
|
|
@ -1018,11 +904,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
wm_state = WMaterial.WM_REPAIR
|
wm_state = WMaterial.WM_REPAIR
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if (
|
if wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
||||||
wm_to.state != WMaterial.WM_REPAIR
|
|
||||||
or wm_to.material != wm_from.material
|
|
||||||
or wm_to.defect != target_defect
|
|
||||||
):
|
|
||||||
raise ParseError("返修合并到的车间库存状态或物料异常")
|
raise ParseError("返修合并到的车间库存状态或物料异常")
|
||||||
elif recive_mgroup:
|
elif recive_mgroup:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
|
|
@ -1031,7 +913,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
belong_dept=recive_dept,
|
belong_dept=recive_dept,
|
||||||
notok_sign=wm_from.notok_sign,
|
notok_sign=wm_from.notok_sign,
|
||||||
defect=target_defect,
|
defect=wm_from.defect,
|
||||||
material_origin=material,
|
material_origin=material,
|
||||||
state=wm_state,
|
state=wm_state,
|
||||||
defaults={
|
defaults={
|
||||||
|
|
@ -1046,11 +928,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
elif handover.type == Handover.H_SCRAP:
|
elif handover.type == Handover.H_SCRAP:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if (
|
if wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
||||||
wm_to.state != WMaterial.WM_SCRAP
|
|
||||||
or wm_to.material != wm_from.material
|
|
||||||
or wm_to.defect != target_defect
|
|
||||||
):
|
|
||||||
raise ParseError("报废合并到的车间库存状态或物料异常")
|
raise ParseError("报废合并到的车间库存状态或物料异常")
|
||||||
elif recive_mgroup:
|
elif recive_mgroup:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
|
|
@ -1059,7 +937,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
belong_dept=recive_dept,
|
belong_dept=recive_dept,
|
||||||
notok_sign=wm_from.notok_sign,
|
notok_sign=wm_from.notok_sign,
|
||||||
defect=target_defect,
|
defect=wm_from.defect,
|
||||||
state=WMaterial.WM_SCRAP,
|
state=WMaterial.WM_SCRAP,
|
||||||
defaults={
|
defaults={
|
||||||
"batch_ofrom": wm_from.batch_ofrom,
|
"batch_ofrom": wm_from.batch_ofrom,
|
||||||
|
|
@ -1073,11 +951,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
elif handover.type == Handover.H_CHANGE:
|
elif handover.type == Handover.H_CHANGE:
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if (
|
if wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
|
||||||
wm_to.material != handover.material_changed
|
|
||||||
or wm_to.state != handover.state_changed
|
|
||||||
or wm_to.defect != target_defect
|
|
||||||
):
|
|
||||||
raise ParseError("改版合并到的车间库存状态或物料异常")
|
raise ParseError("改版合并到的车间库存状态或物料异常")
|
||||||
elif handover.recive_mgroup:
|
elif handover.recive_mgroup:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
|
|
@ -1087,7 +961,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
mgroup=recive_mgroup,
|
mgroup=recive_mgroup,
|
||||||
belong_dept=recive_dept,
|
belong_dept=recive_dept,
|
||||||
notok_sign=wm_from.notok_sign,
|
notok_sign=wm_from.notok_sign,
|
||||||
defect=target_defect,
|
defect=wm_from.defect,
|
||||||
material_origin=None if handover.material_changed == material else material,
|
material_origin=None if handover.material_changed == material else material,
|
||||||
state_origin=None if handover.state_changed == wm_from.state else wm_from.state,
|
state_origin=None if handover.state_changed == wm_from.state else wm_from.state,
|
||||||
defaults={
|
defaults={
|
||||||
|
|
@ -1104,12 +978,6 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
raise ParseError("仅合格品支持退回")
|
raise ParseError("仅合格品支持退回")
|
||||||
if mtype == Handover.H_MERGE and handover.new_wm:
|
if mtype == Handover.H_MERGE and handover.new_wm:
|
||||||
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
wm_to = WMaterial.objects.select_for_update().get(id=handover.new_wm.id)
|
||||||
if (
|
|
||||||
wm_to.state != wm_from.state
|
|
||||||
or wm_to.material != wm_from.material
|
|
||||||
or wm_to.defect != target_defect
|
|
||||||
):
|
|
||||||
raise ParseError("退料合并到的车间库存状态或物料异常")
|
|
||||||
else:
|
else:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
batch=batch,
|
batch=batch,
|
||||||
|
|
@ -1118,7 +986,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
belong_dept=recive_dept,
|
belong_dept=recive_dept,
|
||||||
state=wm_from.state,
|
state=wm_from.state,
|
||||||
notok_sign=wm_from.notok_sign,
|
notok_sign=wm_from.notok_sign,
|
||||||
defect=target_defect,
|
defect=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
|
||||||
defaults={
|
defaults={
|
||||||
"batch_ofrom": wm_from.batch_ofrom,
|
"batch_ofrom": wm_from.batch_ofrom,
|
||||||
"material_ofrom": wm_from.material_ofrom,
|
"material_ofrom": wm_from.material_ofrom,
|
||||||
|
|
@ -1143,12 +1011,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
|
||||||
raise ParseError(f'{batch}物料明细中存在{len(wm_ids)}个不同物料批次')
|
raise ParseError(f'{batch}物料明细中存在{len(wm_ids)}个不同物料批次')
|
||||||
for item in handoverbws:
|
for item in handoverbws:
|
||||||
wpr:Wpr = item.wpr
|
wpr:Wpr = item.wpr
|
||||||
Wpr.change_or_new(
|
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
|
||||||
wpr=wpr,
|
|
||||||
wm=wm_to,
|
|
||||||
old_wm=wpr.wm,
|
|
||||||
old_mb=wpr.mb,
|
|
||||||
)
|
|
||||||
db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get()
|
db_count = WMaterial.objects.filter(id=wm_to.id).values_list("count", flat=True).get()
|
||||||
if db_count != Wpr.objects.filter(wm=wm_to).count():
|
if db_count != Wpr.objects.filter(wm=wm_to).count():
|
||||||
raise ParseError("交接与明细数量不一致2,操作失败")
|
raise ParseError("交接与明细数量不一致2,操作失败")
|
||||||
|
|
@ -1203,13 +1066,7 @@ def handover_revert(handover:Handover, handler:User=None):
|
||||||
wm = WMaterial.objects.get(id=wm.id)
|
wm = WMaterial.objects.get(id=wm.id)
|
||||||
for item in handoverbws:
|
for item in handoverbws:
|
||||||
wpr:Wpr = item.wpr
|
wpr:Wpr = item.wpr
|
||||||
Wpr.change_or_new(
|
Wpr.change_or_new(wpr=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)
|
||||||
wpr=wpr,
|
|
||||||
wm=wm,
|
|
||||||
old_wm=wpr.wm,
|
|
||||||
old_mb=wpr.mb,
|
|
||||||
add_version=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
handover.submit_time = None
|
handover.submit_time = None
|
||||||
handover.submit_user = None
|
handover.submit_user = None
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,11 @@ from django.test import SimpleTestCase, TestCase, TransactionTestCase
|
||||||
from unittest import skipUnless
|
from unittest import skipUnless
|
||||||
|
|
||||||
from apps.mtm.models import Material, Mgroup, WmScope
|
from apps.mtm.models import Material, Mgroup, WmScope
|
||||||
from apps.qm.models import Defect
|
|
||||||
from apps.system.models import Dept, User
|
from apps.system.models import Dept, User
|
||||||
from apps.wpm.filters import HandoverFilter, WMaterialFilter
|
from apps.wpm.filters import HandoverFilter, WMaterialFilter
|
||||||
from apps.wpm.models import Handover, WMaterial
|
from apps.wpm.models import Handover, WMaterial
|
||||||
from apps.wpm.serializers import (
|
from apps.wpm.serializers import HandoverSerializer, WMaterialCreateSerializer
|
||||||
HandoverSerializer,
|
|
||||||
WMaterialCreateSerializer,
|
|
||||||
WMaterialSerializer,
|
|
||||||
)
|
|
||||||
from apps.wpm.services import restore_mlog_wpr_defects, snapshot_mlog_wpr_defects
|
|
||||||
from apps.wpm.views import MlogbwViewSet
|
from apps.wpm.views import MlogbwViewSet
|
||||||
from apps.wpmw.models import Wpr, WprDefect
|
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -104,87 +97,6 @@ class HandoverFilterTests(TestCase):
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
class WMaterialDefectGradeTests(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.material = Material.objects.create(name="缺陷等级测试物料")
|
|
||||||
cls.defect_b = Defect.objects.create(
|
|
||||||
name="B类缺陷",
|
|
||||||
cate=Defect.cate_list[0],
|
|
||||||
okcate=Defect.DEFECT_OK_B,
|
|
||||||
)
|
|
||||||
cls.defect_notok = Defect.objects.create(
|
|
||||||
name="不合格缺陷",
|
|
||||||
cate=Defect.cate_list[0],
|
|
||||||
okcate=Defect.DEFECT_NOTOK,
|
|
||||||
)
|
|
||||||
cls.normal_with_notok_defect = WMaterial.objects.create(
|
|
||||||
material=cls.material,
|
|
||||||
batch="NORMAL-NOTOK",
|
|
||||||
count=1,
|
|
||||||
state=WMaterial.WM_OK,
|
|
||||||
defect=cls.defect_notok,
|
|
||||||
)
|
|
||||||
cls.notok_with_b_defect = WMaterial.objects.create(
|
|
||||||
material=cls.material,
|
|
||||||
batch="NOTOK-B",
|
|
||||||
count=1,
|
|
||||||
state=WMaterial.WM_NOTOK,
|
|
||||||
defect=cls.defect_b,
|
|
||||||
)
|
|
||||||
cls.normal_without_defect = WMaterial.objects.create(
|
|
||||||
material=cls.material,
|
|
||||||
batch="NORMAL-NONE",
|
|
||||||
count=1,
|
|
||||||
state=WMaterial.WM_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_serializer_exposes_nullable_defect_grade_without_using_state(self):
|
|
||||||
normal_notok_data = WMaterialSerializer(
|
|
||||||
self.normal_with_notok_defect
|
|
||||||
).data
|
|
||||||
notok_b_data = WMaterialSerializer(self.notok_with_b_defect).data
|
|
||||||
no_defect_data = WMaterialSerializer(self.normal_without_defect).data
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
normal_notok_data["defect_grade"], Defect.DEFECT_NOTOK
|
|
||||||
)
|
|
||||||
self.assertEqual(normal_notok_data["defect_grade_name"], "不合格")
|
|
||||||
self.assertEqual(
|
|
||||||
notok_b_data["defect_grade"], Defect.DEFECT_OK_B
|
|
||||||
)
|
|
||||||
self.assertEqual(notok_b_data["defect_grade_name"], "合格B类")
|
|
||||||
self.assertIsNone(no_defect_data["defect_grade"])
|
|
||||||
self.assertIsNone(no_defect_data["defect_grade_name"])
|
|
||||||
|
|
||||||
def test_filtering_state_and_defect_grade_are_independent(self):
|
|
||||||
normal_notok = WMaterialFilter(
|
|
||||||
{
|
|
||||||
"state": WMaterial.WM_OK,
|
|
||||||
"defect__okcate": Defect.DEFECT_NOTOK,
|
|
||||||
},
|
|
||||||
queryset=WMaterial.objects.all(),
|
|
||||||
).qs
|
|
||||||
notok_b = WMaterialFilter(
|
|
||||||
{
|
|
||||||
"state": WMaterial.WM_NOTOK,
|
|
||||||
"defect__okcate": Defect.DEFECT_OK_B,
|
|
||||||
},
|
|
||||||
queryset=WMaterial.objects.all(),
|
|
||||||
).qs
|
|
||||||
|
|
||||||
self.assertQuerySetEqual(
|
|
||||||
normal_notok,
|
|
||||||
[self.normal_with_notok_defect],
|
|
||||||
transform=lambda item: item,
|
|
||||||
)
|
|
||||||
self.assertQuerySetEqual(
|
|
||||||
notok_b,
|
|
||||||
[self.notok_with_b_defect],
|
|
||||||
transform=lambda item: item,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MlogbwViewSetTests(SimpleTestCase):
|
class MlogbwViewSetTests(SimpleTestCase):
|
||||||
@patch("apps.wpm.views.MlogViewSet.lock_and_check_can_update")
|
@patch("apps.wpm.views.MlogViewSet.lock_and_check_can_update")
|
||||||
@patch("apps.wpm.views.Mlogbw.cal_count_notok")
|
@patch("apps.wpm.views.Mlogbw.cal_count_notok")
|
||||||
|
|
@ -434,235 +346,6 @@ class WMaterialScopeTests(SimpleTestCase):
|
||||||
self.assertIs(validated["send_mgroup"], mgroup)
|
self.assertIs(validated["send_mgroup"], mgroup)
|
||||||
self.assertIs(validated["send_dept"], sending_dept)
|
self.assertIs(validated["send_dept"], sending_dept)
|
||||||
|
|
||||||
def test_batch_tracking_merge_can_clear_ok_b_batch_defects(self):
|
|
||||||
material = Material(tracking=Material.MA_TRACKING_BATCH)
|
|
||||||
defect_a = Defect(id="1", okcate=Defect.DEFECT_OK_B)
|
|
||||||
defect_b = Defect(id="2", okcate=Defect.DEFECT_OK_B)
|
|
||||||
wm_a = WMaterial(
|
|
||||||
id="10", material=material, batch="B-001", count=1,
|
|
||||||
state=WMaterial.WM_OK, defect=defect_a,
|
|
||||||
)
|
|
||||||
wm_b = WMaterial(
|
|
||||||
id="20", material=material, batch="B-002", count=1,
|
|
||||||
state=WMaterial.WM_OK, defect=defect_b,
|
|
||||||
)
|
|
||||||
|
|
||||||
validated = HandoverSerializer().validate({
|
|
||||||
"wm": wm_a,
|
|
||||||
"handoverb": [
|
|
||||||
{"wm": wm_a, "count": 1},
|
|
||||||
{"wm": wm_b, "count": 1},
|
|
||||||
],
|
|
||||||
"new_batch": "B-MERGED",
|
|
||||||
"clear_defect": True,
|
|
||||||
"type": Handover.H_NORMAL,
|
|
||||||
"mtype": Handover.H_MERGE,
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertTrue(validated["clear_defect"])
|
|
||||||
self.assertEqual(validated["count"], 2)
|
|
||||||
|
|
||||||
def test_batch_tracking_merge_cannot_clear_notok_defect(self):
|
|
||||||
material = Material(tracking=Material.MA_TRACKING_BATCH)
|
|
||||||
defect = Defect(id="1", okcate=Defect.DEFECT_NOTOK)
|
|
||||||
wm = WMaterial(
|
|
||||||
id="10", material=material, batch="N-001", count=1,
|
|
||||||
state=WMaterial.WM_NOTOK, defect=defect,
|
|
||||||
)
|
|
||||||
|
|
||||||
with self.assertRaisesMessage(
|
|
||||||
ParseError,
|
|
||||||
"批次追踪物料仅合格B类可清除批次缺陷",
|
|
||||||
):
|
|
||||||
HandoverSerializer().validate({
|
|
||||||
"wm": wm,
|
|
||||||
"handoverb": [{"wm": wm, "count": 1}],
|
|
||||||
"new_batch": "N-MERGED",
|
|
||||||
"clear_defect": True,
|
|
||||||
"type": Handover.H_NORMAL,
|
|
||||||
"mtype": Handover.H_MERGE,
|
|
||||||
})
|
|
||||||
|
|
||||||
def test_single_tracking_merge_can_clear_notok_batch_defect(self):
|
|
||||||
material = Material(tracking=Material.MA_TRACKING_SINGLE)
|
|
||||||
defect = Defect(id="1", okcate=Defect.DEFECT_NOTOK)
|
|
||||||
wm = WMaterial(
|
|
||||||
id="10", material=material, batch="S-001", count=1,
|
|
||||||
state=WMaterial.WM_NOTOK, defect=defect,
|
|
||||||
)
|
|
||||||
wm_b = WMaterial(
|
|
||||||
id="20", material=material, batch="S-002", count=1,
|
|
||||||
state=WMaterial.WM_NOTOK, defect=defect,
|
|
||||||
)
|
|
||||||
|
|
||||||
validated = HandoverSerializer().validate({
|
|
||||||
"wm": wm,
|
|
||||||
"handoverb": [
|
|
||||||
{"wm": wm, "count": 1},
|
|
||||||
{"wm": wm_b, "count": 1},
|
|
||||||
],
|
|
||||||
"new_batch": "S-MERGED",
|
|
||||||
"clear_defect": True,
|
|
||||||
"type": Handover.H_NORMAL,
|
|
||||||
"mtype": Handover.H_MERGE,
|
|
||||||
})
|
|
||||||
|
|
||||||
self.assertTrue(validated["clear_defect"])
|
|
||||||
self.assertEqual(validated["count"], 2)
|
|
||||||
|
|
||||||
|
|
||||||
class WprDefectLifecycleTests(SimpleTestCase):
|
|
||||||
@patch("apps.wpmw.models.WprDefect.objects.filter")
|
|
||||||
def test_change_inventory_never_changes_individual_defects(self, defect_filter):
|
|
||||||
material = Material(id="1", tracking=Material.MA_TRACKING_SINGLE)
|
|
||||||
old_wm = WMaterial(
|
|
||||||
id="10", material=material, state=WMaterial.WM_OK,
|
|
||||||
)
|
|
||||||
target_wm = WMaterial(
|
|
||||||
id="20", material=material, state=WMaterial.WM_OK,
|
|
||||||
defect=None,
|
|
||||||
)
|
|
||||||
wpr = SimpleNamespace(
|
|
||||||
wm=old_wm,
|
|
||||||
mb=None,
|
|
||||||
material=material,
|
|
||||||
state=WMaterial.WM_OK,
|
|
||||||
version=1,
|
|
||||||
save=MagicMock(),
|
|
||||||
)
|
|
||||||
|
|
||||||
Wpr.change_or_new(
|
|
||||||
wpr=wpr,
|
|
||||||
wm=target_wm,
|
|
||||||
old_wm=old_wm,
|
|
||||||
)
|
|
||||||
|
|
||||||
defect_filter.assert_not_called()
|
|
||||||
self.assertIs(wpr.wm, target_wm)
|
|
||||||
|
|
||||||
@patch.object(Wpr, "replace_defects")
|
|
||||||
@patch("apps.wpmw.models.FtestDefect.objects.filter")
|
|
||||||
@patch("apps.wpmw.models.FtestItem.objects.filter")
|
|
||||||
def test_apply_ftest_replaces_defects_and_carries_info(
|
|
||||||
self,
|
|
||||||
ftest_item_filter,
|
|
||||||
ftest_defect_filter,
|
|
||||||
replace_defects,
|
|
||||||
):
|
|
||||||
ftest = object()
|
|
||||||
ftest_defect_filter.return_value.values.return_value = [
|
|
||||||
{"defect_id": "30", "is_main": False},
|
|
||||||
{"defect_id": "31", "is_main": True},
|
|
||||||
]
|
|
||||||
ftest_item_filter.return_value.select_related.return_value = [
|
|
||||||
SimpleNamespace(
|
|
||||||
id="40",
|
|
||||||
test_val_json={"value": 1},
|
|
||||||
testitem=SimpleNamespace(name="尺寸"),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
wpr = Wpr(
|
|
||||||
oinfo={"existing": {"value": 2}},
|
|
||||||
)
|
|
||||||
wpr.save = MagicMock()
|
|
||||||
|
|
||||||
wpr.apply_ftest(ftest)
|
|
||||||
|
|
||||||
ftest_defect_filter.assert_called_once_with(ftest=ftest, has=True)
|
|
||||||
replace_defects.assert_called_once_with([
|
|
||||||
{"defect_id": "30", "is_main": False},
|
|
||||||
{"defect_id": "31", "is_main": True},
|
|
||||||
])
|
|
||||||
self.assertEqual(
|
|
||||||
wpr.oinfo["40"],
|
|
||||||
{"name": "尺寸", "val": {"value": 1}},
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch.object(Wpr, "replace_defects")
|
|
||||||
def test_apply_no_ftest_clears_defects(self, replace_defects):
|
|
||||||
wpr = Wpr(oinfo={"existing": True})
|
|
||||||
|
|
||||||
wpr.apply_ftest(None)
|
|
||||||
|
|
||||||
replace_defects.assert_called_once_with([])
|
|
||||||
self.assertEqual(wpr.oinfo, {"existing": True})
|
|
||||||
|
|
||||||
@patch("apps.wpmw.models.FtestItem.objects.filter")
|
|
||||||
def test_revert_ftest_info_only_removes_matching_keys(self, item_filter):
|
|
||||||
item_filter.return_value.values_list.return_value = ["10", "11"]
|
|
||||||
wpr = Wpr(oinfo={"10": {"v": 1}, "other": {"v": 2}})
|
|
||||||
wpr.save = MagicMock()
|
|
||||||
|
|
||||||
wpr.revert_ftest_info(object())
|
|
||||||
|
|
||||||
self.assertEqual(wpr.oinfo, {"other": {"v": 2}})
|
|
||||||
wpr.save.assert_called_once()
|
|
||||||
|
|
||||||
|
|
||||||
class MlogWprDefectSnapshotTests(SimpleTestCase):
|
|
||||||
@patch("apps.wpm.services.WprDefect.objects.filter")
|
|
||||||
@patch("apps.wpm.services.Wpr.objects.select_for_update")
|
|
||||||
@patch("apps.wpm.services.Mlogbw.objects.filter")
|
|
||||||
def test_snapshot_keeps_multiple_defects_and_empty_wpr(
|
|
||||||
self,
|
|
||||||
mlogbw_filter,
|
|
||||||
select_for_update,
|
|
||||||
defect_filter,
|
|
||||||
):
|
|
||||||
mlogbw_filter.return_value.values_list.return_value.distinct.return_value = [
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
]
|
|
||||||
select_for_update.return_value.filter.return_value.values_list.return_value = [
|
|
||||||
"1",
|
|
||||||
"2",
|
|
||||||
]
|
|
||||||
defect_filter.return_value.values.return_value = [
|
|
||||||
{"wpr_id": "1", "defect_id": "10", "is_main": True},
|
|
||||||
{"wpr_id": "1", "defect_id": "11", "is_main": False},
|
|
||||||
]
|
|
||||||
mlog = SimpleNamespace(id="100", save=MagicMock())
|
|
||||||
|
|
||||||
snapshot = snapshot_mlog_wpr_defects(mlog)
|
|
||||||
|
|
||||||
self.assertEqual(snapshot["2"], [])
|
|
||||||
self.assertEqual(len(snapshot["1"]), 2)
|
|
||||||
select_for_update.return_value.filter.return_value.values_list.assert_called_once_with(
|
|
||||||
"id",
|
|
||||||
flat=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
@patch("apps.wpm.services.WprDefect.objects.bulk_create")
|
|
||||||
@patch("apps.wpm.services.WprDefect.objects.filter")
|
|
||||||
@patch("apps.wpm.services.Wpr.objects.select_for_update")
|
|
||||||
def test_restore_replaces_current_defects_exactly(
|
|
||||||
self,
|
|
||||||
select_for_update,
|
|
||||||
defect_filter,
|
|
||||||
bulk_create,
|
|
||||||
):
|
|
||||||
wpr = Wpr(id="1")
|
|
||||||
select_for_update.return_value.filter.return_value = [wpr]
|
|
||||||
mlog = SimpleNamespace(
|
|
||||||
id="100",
|
|
||||||
wpr_defect_snapshot={
|
|
||||||
"1": [
|
|
||||||
{"defect_id": "10", "is_main": True},
|
|
||||||
{"defect_id": "11", "is_main": False},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
restored = restore_mlog_wpr_defects(mlog)
|
|
||||||
|
|
||||||
self.assertTrue(restored)
|
|
||||||
defect_filter.return_value.delete.assert_called_once_with()
|
|
||||||
created = bulk_create.call_args.args[0]
|
|
||||||
self.assertEqual(
|
|
||||||
[(str(item.defect_id), item.is_main) for item in created],
|
|
||||||
[("10", True), ("11", False)],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@skipUnless(
|
@skipUnless(
|
||||||
connection.vendor == "postgresql",
|
connection.vendor == "postgresql",
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,7 @@ class Wpr(BaseModel):
|
||||||
pre_info = models.JSONField(verbose_name="预处理信息", default=dict, blank=True, null=True)
|
pre_info = models.JSONField(verbose_name="预处理信息", default=dict, blank=True, null=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def change_or_new(
|
def change_or_new(cls, wpr=None, number=None, mb=None, wm=None, old_mb=None, old_wm=None, ftest=None, wpr_from=None, add_version=True, number_out=None):
|
||||||
cls, wpr=None, number=None, mb=None, wm=None, old_mb=None,
|
|
||||||
old_wm=None, wpr_from=None, add_version=True, number_out=None,
|
|
||||||
):
|
|
||||||
is_create = False
|
is_create = False
|
||||||
if wpr is None and number is None:
|
if wpr is None and number is None:
|
||||||
raise ParseError("id和number不能同时为空")
|
raise ParseError("id和number不能同时为空")
|
||||||
|
|
@ -112,9 +109,25 @@ class Wpr(BaseModel):
|
||||||
ins.state = wm.state
|
ins.state = wm.state
|
||||||
if is_create:
|
if is_create:
|
||||||
ins.material_start = wm.material
|
ins.material_start = wm.material
|
||||||
|
WprDefect.objects.filter(wpr=ins).delete()
|
||||||
if add_version:
|
if add_version:
|
||||||
ins.version = ins.version + 1
|
ins.version = ins.version + 1
|
||||||
ins.save()
|
ins.save()
|
||||||
|
if wm:
|
||||||
|
if wm.defect:
|
||||||
|
WprDefect.objects.create(wpr=ins, defect=wm.defect, is_main=True)
|
||||||
|
if ftest:
|
||||||
|
# 通过检验变更其缺陷项
|
||||||
|
WprDefect.objects.filter(wpr=ins).delete()
|
||||||
|
for ftestdefect in FtestDefect.objects.filter(ftest=ftest, has=True):
|
||||||
|
WprDefect.objects.create(wpr=ins, defect=ftestdefect.defect, is_main=ftestdefect.is_main)
|
||||||
|
# 携带某些检测信息
|
||||||
|
oinfo = {}
|
||||||
|
for ftestitem in FtestItem.objects.filter(ftest=ftest, addto_wpr=True):
|
||||||
|
if ftestitem.test_val_json:
|
||||||
|
oinfo[ftestitem.id] = {"name": ftestitem.testitem.name, "val": ftestitem.test_val_json}
|
||||||
|
ins.oinfo = update_dict(ins.oinfo, oinfo)
|
||||||
|
ins.save()
|
||||||
# if ins.mb and ins.wm:
|
# if ins.mb and ins.wm:
|
||||||
# raise ParseError("所属仓库批次和车间批次不可同时存在2")
|
# raise ParseError("所属仓库批次和车间批次不可同时存在2")
|
||||||
if number_out:
|
if number_out:
|
||||||
|
|
@ -125,62 +138,6 @@ class Wpr(BaseModel):
|
||||||
raise ParseError(f"{ins.number}-出库编号-{number_out}-已存在")
|
raise ParseError(f"{ins.number}-出库编号-{number_out}-已存在")
|
||||||
return ins
|
return ins
|
||||||
|
|
||||||
def replace_defects(self, defects):
|
|
||||||
"""用给定缺陷列表完整替换单件缺陷。"""
|
|
||||||
WprDefect.objects.filter(wpr=self).delete()
|
|
||||||
WprDefect.objects.bulk_create([
|
|
||||||
WprDefect(
|
|
||||||
wpr=self,
|
|
||||||
defect_id=item["defect_id"],
|
|
||||||
is_main=item.get("is_main", False),
|
|
||||||
)
|
|
||||||
for item in defects
|
|
||||||
])
|
|
||||||
|
|
||||||
def apply_ftest(self, ftest):
|
|
||||||
"""应用一次明确的单件检验;无检验结果时缺陷为空。"""
|
|
||||||
defects = []
|
|
||||||
if ftest is not None:
|
|
||||||
defects = list(FtestDefect.objects.filter(
|
|
||||||
ftest=ftest,
|
|
||||||
has=True,
|
|
||||||
).values("defect_id", "is_main"))
|
|
||||||
self.replace_defects(defects)
|
|
||||||
|
|
||||||
if ftest is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
oinfo = {}
|
|
||||||
for ftestitem in FtestItem.objects.filter(
|
|
||||||
ftest=ftest,
|
|
||||||
addto_wpr=True,
|
|
||||||
).select_related("testitem"):
|
|
||||||
if ftestitem.test_val_json:
|
|
||||||
oinfo[str(ftestitem.id)] = {
|
|
||||||
"name": ftestitem.testitem.name,
|
|
||||||
"val": ftestitem.test_val_json,
|
|
||||||
}
|
|
||||||
if oinfo:
|
|
||||||
self.oinfo = update_dict(self.oinfo, oinfo)
|
|
||||||
self.save(update_fields=["oinfo", "update_time"])
|
|
||||||
|
|
||||||
def revert_ftest_info(self, ftest):
|
|
||||||
"""仅撤销指定检验写入单件携带信息的字段。"""
|
|
||||||
if ftest is None or not self.oinfo:
|
|
||||||
return
|
|
||||||
item_ids = FtestItem.objects.filter(
|
|
||||||
ftest=ftest,
|
|
||||||
addto_wpr=True,
|
|
||||||
).values_list("id", flat=True)
|
|
||||||
changed = False
|
|
||||||
oinfo = dict(self.oinfo)
|
|
||||||
for item_id in item_ids:
|
|
||||||
if oinfo.pop(str(item_id), None) is not None:
|
|
||||||
changed = True
|
|
||||||
if changed:
|
|
||||||
self.oinfo = oinfo
|
|
||||||
self.save(update_fields=["oinfo", "update_time"])
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear(cls, number_list):
|
def clear(cls, number_list):
|
||||||
cls.objects.filter(number__in=number_list).update(mb=None, wm=None)
|
cls.objects.filter(number__in=number_list).update(mb=None, wm=None)
|
||||||
|
|
@ -223,4 +180,4 @@ class WprDefect(BaseModel):
|
||||||
"""TN:产物缺陷项关联表"""
|
"""TN:产物缺陷项关联表"""
|
||||||
wpr = models.ForeignKey(Wpr, verbose_name="关联产物", on_delete=models.CASCADE)
|
wpr = models.ForeignKey(Wpr, verbose_name="关联产物", on_delete=models.CASCADE)
|
||||||
defect = models.ForeignKey("qm.defect", verbose_name="关联缺陷项", on_delete=models.CASCADE)
|
defect = models.ForeignKey("qm.defect", verbose_name="关联缺陷项", on_delete=models.CASCADE)
|
||||||
is_main = models.BooleanField("是否主要缺陷", default=False)
|
is_main = models.BooleanField("是否主要缺陷", default=False)
|
||||||
Loading…
Reference in New Issue