Compare commits
2 Commits
bcbb27374f
...
6068f2315d
| Author | SHA1 | Date |
|---|---|---|
|
|
6068f2315d | |
|
|
298a32f744 |
|
|
@ -436,11 +436,18 @@ class InmService:
|
||||||
raise ParseError("出入库与明细数量不一致,操作失败")
|
raise ParseError("出入库与明细数量不一致,操作失败")
|
||||||
for mioitemw in mioitemws:
|
for mioitemw in mioitemws:
|
||||||
if mioitemw.wpr:
|
if mioitemw.wpr:
|
||||||
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, ftest=mioitemw.ftest)
|
wpr = Wpr.change_or_new(
|
||||||
|
wpr=mioitemw.wpr,
|
||||||
|
mb=mb,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
wpr = Wpr.change_or_new(number=mioitemw.number, mb=mb, ftest=mioitemw.ftest)
|
wpr = Wpr.change_or_new(
|
||||||
|
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,7 +221,9 @@ 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 ParseError('选择已有输出物料和新建半成品不可同时提交')
|
raise serializers.ValidationError(
|
||||||
|
'选择已有输出物料和新建半成品不可同时提交'
|
||||||
|
)
|
||||||
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,7 +189,10 @@ 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 = idWorker.get_id()
|
self.id = str(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
|
||||||
|
|
@ -209,7 +212,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 = idWorker.get_id()
|
self.id = str(idWorker.get_id())
|
||||||
ins = super().save(*args, **kwargs)
|
ins = super().save(*args, **kwargs)
|
||||||
raise e
|
raise e
|
||||||
# 处理父级
|
# 处理父级
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,38 @@
|
||||||
|
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):
|
||||||
def test_audit_end(self):
|
@patch.object(HrmService, 'sync_dahua_employee')
|
||||||
v = Visit.objects.get(id='1547401054033940480')
|
def test_sync_employee_uses_visit_time_window(self, sync_employee):
|
||||||
ep = Employee.objects.get(id='1547406632055607296')
|
visit_time = timezone.now()
|
||||||
HrmService.sync_dahua_employee(ep, '', v.visit_time, v.leave_time)
|
leave_time = visit_time + timedelta(hours=2)
|
||||||
|
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,7 +132,8 @@ 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):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 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,6 +367,12 @@ 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,12 +189,21 @@ 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 会截断在途量, 导致可用/可交接量偏大)
|
||||||
|
|
@ -347,8 +356,7 @@ class MlogListSerializer(CustomModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Mlog
|
model = Mlog
|
||||||
fields = '__all__'
|
exclude = ["wpr_defect_snapshot"]
|
||||||
# exclude = ["equipments","handle_users", "material_outs"]
|
|
||||||
|
|
||||||
|
|
||||||
class MlogbDetailSerializer(CustomModelSerializer):
|
class MlogbDetailSerializer(CustomModelSerializer):
|
||||||
|
|
@ -419,7 +427,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
|
||||||
fields = '__all__'
|
exclude = ["wpr_defect_snapshot"]
|
||||||
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 = {
|
||||||
|
|
@ -1280,6 +1288,9 @@ 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:
|
||||||
|
|
@ -1404,11 +1415,17 @@ 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
|
||||||
|
|
@ -1533,7 +1550,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=handoverb,
|
Handoverbw.objects.get_or_create(wpr=item, handoverb=hb,
|
||||||
defaults={"number": item.number})
|
defaults={"number": item.number})
|
||||||
else:
|
else:
|
||||||
raise ParseError(f'第{ind+1}行-请提供交接物料明细')
|
raise ParseError(f'第{ind+1}行-请提供交接物料明细')
|
||||||
|
|
|
||||||
|
|
@ -165,6 +165,73 @@ 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]):
|
||||||
"""
|
"""
|
||||||
生产日志提交后需要执行的操作
|
生产日志提交后需要执行的操作
|
||||||
|
|
@ -200,6 +267,8 @@ 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()
|
||||||
# 建立关系链
|
# 建立关系链
|
||||||
|
|
@ -279,7 +348,8 @@ 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.change_or_new(wpr=item.wpr, old_wm=wm, ftest=item.ftest)
|
wpr = Wpr.change_or_new(wpr=item.wpr, old_wm=wm)
|
||||||
|
wpr.apply_ftest(item.ftest)
|
||||||
|
|
||||||
# 针对加工前不良的暂时额外处理
|
# 针对加工前不良的暂时额外处理
|
||||||
# 合格B类的需要当合格品
|
# 合格B类的需要当合格品
|
||||||
|
|
@ -312,7 +382,12 @@ 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.change_or_new(wpr=item.wpr, wm=wm, old_wm=wm, ftest=item.ftest)
|
wpr = Wpr.change_or_new(
|
||||||
|
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)
|
||||||
|
|
@ -419,18 +494,35 @@ 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.change_or_new(wpr=item.wpr, wm=wm, ftest=item.ftest)
|
wpr = Wpr.change_or_new(
|
||||||
|
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(number=item.number,
|
wpr = Wpr.change_or_new(
|
||||||
wm=wm, ftest=item.ftest,
|
number=item.number,
|
||||||
wpr_from=wpr_from)
|
wm=wm,
|
||||||
|
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
|
||||||
|
|
@ -438,6 +530,7 @@ 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()
|
||||||
|
|
||||||
# 更新任务进度
|
# 更新任务进度
|
||||||
|
|
@ -471,6 +564,7 @@ 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()
|
||||||
|
|
@ -585,7 +679,8 @@ 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.change_or_new(wpr=item.wpr, old_wm=wm)
|
wpr = Wpr.change_or_new(wpr=item.wpr, old_wm=wm)
|
||||||
|
wpr.revert_ftest_info(item.ftest)
|
||||||
|
|
||||||
# 再生成消耗
|
# 再生成消耗
|
||||||
m_ins_list = []
|
m_ins_list = []
|
||||||
|
|
@ -629,7 +724,9 @@ 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.change_or_new(wpr=item.wpr, wm=wm)
|
wpr = 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:
|
||||||
|
|
@ -662,7 +759,18 @@ 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.change_or_new(wpr=item.wpr, wm=wm)
|
wpr = 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
|
||||||
|
|
@ -873,14 +981,20 @@ 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)
|
||||||
defect_ok = wm_to.defect == wm_from.defect or (
|
if (
|
||||||
merge_clear_defect and wm_to.defect is None and wm_from.state == WMaterial.WM_OK
|
wm_to.state != wm_from.state
|
||||||
)
|
or wm_to.material != wm_from.material
|
||||||
if wm_to.state != wm_from.state or wm_to.material != wm_from.material or not defect_ok:
|
or wm_to.defect != target_defect
|
||||||
|
):
|
||||||
raise ParseError("正常合并到的车间库存状态或物料异常")
|
raise ParseError("正常合并到的车间库存状态或物料异常")
|
||||||
else:
|
else:
|
||||||
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
wm_to, _ = WMaterial.locked_get_or_create_inventory(
|
||||||
|
|
@ -890,7 +1004,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=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
|
defect=target_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,
|
||||||
|
|
@ -904,7 +1018,11 @@ 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 wm_to.state != WMaterial.WM_REPAIR or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
if (
|
||||||
|
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(
|
||||||
|
|
@ -913,7 +1031,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=wm_from.defect,
|
defect=target_defect,
|
||||||
material_origin=material,
|
material_origin=material,
|
||||||
state=wm_state,
|
state=wm_state,
|
||||||
defaults={
|
defaults={
|
||||||
|
|
@ -928,7 +1046,11 @@ 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 wm_to.state != WMaterial.WM_SCRAP or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
|
if (
|
||||||
|
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(
|
||||||
|
|
@ -937,7 +1059,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=wm_from.defect,
|
defect=target_defect,
|
||||||
state=WMaterial.WM_SCRAP,
|
state=WMaterial.WM_SCRAP,
|
||||||
defaults={
|
defaults={
|
||||||
"batch_ofrom": wm_from.batch_ofrom,
|
"batch_ofrom": wm_from.batch_ofrom,
|
||||||
|
|
@ -951,7 +1073,11 @@ 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 wm_to.material != handover.material_changed or wm_to.state != handover.state_changed:
|
if (
|
||||||
|
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(
|
||||||
|
|
@ -961,7 +1087,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=wm_from.defect,
|
defect=target_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={
|
||||||
|
|
@ -978,6 +1104,12 @@ 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,
|
||||||
|
|
@ -986,7 +1118,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=merge_target_defect if mtype == Handover.H_MERGE else wm_from.defect,
|
defect=target_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,
|
||||||
|
|
@ -1011,7 +1143,12 @@ 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=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
|
Wpr.change_or_new(
|
||||||
|
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,操作失败")
|
||||||
|
|
@ -1066,7 +1203,13 @@ 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=wpr, wm=wm, old_wm=wpr.wm, old_mb=wpr.mb, add_version=False)
|
Wpr.change_or_new(
|
||||||
|
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,11 +9,18 @@ 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 HandoverSerializer, WMaterialCreateSerializer
|
from apps.wpm.serializers import (
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -97,6 +104,87 @@ 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")
|
||||||
|
|
@ -346,6 +434,235 @@ 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,7 +33,10 @@ 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(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):
|
def change_or_new(
|
||||||
|
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不能同时为空")
|
||||||
|
|
@ -109,25 +112,9 @@ 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:
|
||||||
|
|
@ -138,6 +125,62 @@ 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)
|
||||||
|
|
@ -180,4 +223,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