Compare commits
3 Commits
94215df0b6
...
d1693799b9
| Author | SHA1 | Date |
|---|---|---|
|
|
d1693799b9 | |
|
|
596f187d3d | |
|
|
fa16694f9f |
|
|
@ -10,5 +10,6 @@
|
|||
- [项目 Python 虚拟环境](reference_python_venv.md):运行 Django、pytest 和脚本时使用。
|
||||
- [项目测试数据库](reference_test_database.md):测试与可切换的生产查询连接解耦,始终使用固定的 `test_ehs_develop`。
|
||||
- [两个 WebView 套壳 App](reference_wrapper_apps.md):修改 h5x 与扫码、返回键交互时使用。
|
||||
- [前端验证时机](feedback_frontend_validation.md):日常修改先跑 check,完整 build 留到 push 前执行。
|
||||
|
||||
这些文件记录的是长期约定或历史上下文。执行任务前应结合当前代码和数据重新验证,尤其不要把历史缺陷结论直接当成当前故障原因。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# 前端验证时机
|
||||
|
||||
- 修改配套前端 `../ehs_web` 时,日常开发和中间验证优先运行项目已有的 `check`,不要每次修改后都运行完整 `build`。
|
||||
- 准备 push 前运行一次完整 `build`,用于发现生产构建阶段的问题。
|
||||
- 若当前前端尚未配置 `check` 脚本,应先说明现状,不得把其他命令擅自当作 `check`。
|
||||
|
|
@ -877,7 +877,7 @@ class Handoverb(BaseModel):
|
|||
|
||||
@property
|
||||
def handoverbw(self):
|
||||
return Handoverbw.objects.filter(handoverb=self)
|
||||
return self.w_handoverb.all()
|
||||
|
||||
class Handoverbw(BaseModel):
|
||||
"""TN: 单个产品交接记录
|
||||
|
|
|
|||
|
|
@ -31,6 +31,28 @@ from apps.em.models import Equipment
|
|||
from django.db.models import Q
|
||||
mylogger = logging.getLogger("log")
|
||||
|
||||
WM_STATE_NAMES = {
|
||||
WMaterial.WM_OK: "合格",
|
||||
WMaterial.WM_NOTOK: "不合格",
|
||||
WMaterial.WM_REPAIR: "返修",
|
||||
WMaterial.WM_REPAIRED: "返修完成",
|
||||
WMaterial.WM_TEST: "检验",
|
||||
WMaterial.WM_SCRAP: "报废",
|
||||
}
|
||||
|
||||
|
||||
class MlogEquipmentOptionSerializer(serializers.ModelSerializer):
|
||||
mgroup_name = serializers.CharField(source="mgroup.name", read_only=True)
|
||||
full_name = serializers.SerializerMethodField()
|
||||
|
||||
def get_full_name(self, obj):
|
||||
return f"{obj.number}|{obj.name}|{obj.model}"
|
||||
|
||||
class Meta:
|
||||
model = Equipment
|
||||
fields = ["id", "name", "number", "model", "mgroup_name", "full_name"]
|
||||
|
||||
|
||||
class OtherLogSerializer(CustomModelSerializer):
|
||||
class Meta:
|
||||
model = OtherLog
|
||||
|
|
@ -1286,8 +1308,35 @@ class HandoverbSerializer(CustomModelSerializer):
|
|||
)
|
||||
)
|
||||
notok_sign = serializers.CharField(source='wm.notok_sign', read_only=True)
|
||||
notok_sign_name = serializers.SerializerMethodField()
|
||||
defect_name = serializers.CharField(source="wm.defect.name", read_only=True)
|
||||
defect_grade = serializers.SerializerMethodField()
|
||||
defect_grade_name = serializers.SerializerMethodField()
|
||||
material_name = serializers.StringRelatedField(source="wm.material", read_only=True)
|
||||
state_name = serializers.SerializerMethodField()
|
||||
mgroup_name = serializers.CharField(source="wm.mgroup.name", read_only=True)
|
||||
belong_dept_name = serializers.CharField(source="wm.belong_dept.name", read_only=True)
|
||||
count_available = serializers.SerializerMethodField()
|
||||
handoverbw = Handoverbwserializer(many=True, required=False)
|
||||
|
||||
def get_notok_sign_name(self, obj):
|
||||
return getattr(NotOkOption, obj.wm.notok_sign, NotOkOption.qt).label if obj.wm.notok_sign else None
|
||||
|
||||
def get_defect_grade(self, obj):
|
||||
return effective_defect_grade(obj.wm, "notok_sign")
|
||||
|
||||
def get_defect_grade_name(self, obj):
|
||||
return DEFECT_GRADE_NAMES[self.get_defect_grade(obj)]
|
||||
|
||||
def get_state_name(self, obj):
|
||||
return WM_STATE_NAMES.get(obj.wm.state, str(obj.wm.state))
|
||||
|
||||
def get_count_available(self, obj):
|
||||
# 编辑未提交交接时,当前明细占用的数量仍应允许重新填写。
|
||||
if obj.handover.submit_time is not None:
|
||||
return obj.count
|
||||
return obj.wm.count - obj.wm.count_handovering + obj.count
|
||||
|
||||
class Meta:
|
||||
model = Handoverb
|
||||
fields = "__all__"
|
||||
|
|
@ -1310,9 +1359,14 @@ class HandoverSerializer(CustomModelSerializer):
|
|||
recive_user_name = serializers.CharField(
|
||||
source='recive_user.name', read_only=True)
|
||||
recive_dept_name = serializers.CharField(
|
||||
source='recive_dept', read_only=True)
|
||||
source='recive_dept.name', read_only=True)
|
||||
send_dept_name = serializers.CharField(source='send_dept.name', read_only=True)
|
||||
send_mgroup_name = serializers.CharField(source='send_mgroup.name', read_only=True)
|
||||
recive_mgroup_name = serializers.CharField(source='recive_mgroup.name', read_only=True)
|
||||
submit_user_name = serializers.CharField(source='submit_user.name', read_only=True)
|
||||
type_name = serializers.CharField(source='get_type_display', read_only=True)
|
||||
mtype_name = serializers.CharField(source='get_mtype_display', read_only=True)
|
||||
state_changed_name = serializers.SerializerMethodField()
|
||||
material_ = MaterialSimpleSerializer(source='material', read_only=True)
|
||||
material_name = serializers.StringRelatedField(
|
||||
source='material', read_only=True)
|
||||
|
|
@ -1321,6 +1375,9 @@ class HandoverSerializer(CustomModelSerializer):
|
|||
handoverb = HandoverbSerializer(many=True, required=False)
|
||||
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
|
||||
|
||||
def get_state_changed_name(self, obj):
|
||||
return WM_STATE_NAMES.get(obj.state_changed) if obj.state_changed is not None else None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
handoverb = data.get("handoverb", []) if hasattr(data, "get") else []
|
||||
wm_ids = {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Sum
|
||||
|
|
@ -27,6 +28,44 @@ from django.db.models import F
|
|||
|
||||
myLogger = logging.getLogger('log')
|
||||
|
||||
RECENT_EQUIPMENT_LOG_LIMIT = 50
|
||||
|
||||
|
||||
def get_recent_mgroup_equipment_ids(
|
||||
mgroup_id, log_limit=RECENT_EQUIPMENT_LOG_LIMIT
|
||||
):
|
||||
"""按日志时间倒序返回工段最近使用过的设备 ID,空值和重复值忽略。"""
|
||||
recent_logs = list(
|
||||
Mlog.objects.filter(mgroup_id=mgroup_id)
|
||||
.order_by("-create_time", "-id")
|
||||
.values_list("id", "equipment_id", "equipment_2_id")[:log_limit]
|
||||
)
|
||||
if not recent_logs:
|
||||
return []
|
||||
|
||||
log_ids = [log_id for log_id, _, _ in recent_logs]
|
||||
multiple_equipment_ids = defaultdict(list)
|
||||
for log_id, equipment_id in (
|
||||
Mlog.equipments.through.objects.filter(mlog_id__in=log_ids)
|
||||
.order_by("id")
|
||||
.values_list("mlog_id", "equipment_id")
|
||||
):
|
||||
multiple_equipment_ids[log_id].append(equipment_id)
|
||||
|
||||
result = []
|
||||
seen = set()
|
||||
for log_id, equipment_id, equipment_2_id in recent_logs:
|
||||
candidate_ids = [
|
||||
equipment_id,
|
||||
equipment_2_id,
|
||||
*multiple_equipment_ids[log_id],
|
||||
]
|
||||
for candidate_id in candidate_ids:
|
||||
if candidate_id and candidate_id not in seen:
|
||||
seen.add(candidate_id)
|
||||
result.append(candidate_id)
|
||||
return result
|
||||
|
||||
def inherit_zt_batch(source: BatchSt, target: BatchSt):
|
||||
"""拆批/报工改号时目标批继承来源批的直通统计大批归属(纯继承, 不做判定)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
|
||||
from apps.wpm.services import get_recent_mgroup_equipment_ids
|
||||
|
||||
|
||||
class RecentMgroupEquipmentTests(SimpleTestCase):
|
||||
@patch("apps.wpm.services.Mlog")
|
||||
def test_collects_all_equipment_fields_in_log_order_without_duplicates(
|
||||
self, mlog
|
||||
):
|
||||
log_queryset = MagicMock()
|
||||
mlog.objects.filter.return_value = log_queryset
|
||||
log_queryset.order_by.return_value.values_list.return_value.__getitem__.return_value = [
|
||||
("log-new", "equipment-a", None),
|
||||
("log-middle", "equipment-b", "equipment-a"),
|
||||
("log-empty", None, None),
|
||||
]
|
||||
through_queryset = MagicMock()
|
||||
mlog.equipments.through.objects.filter.return_value = through_queryset
|
||||
through_queryset.order_by.return_value.values_list.return_value = [
|
||||
("log-new", "equipment-c"),
|
||||
("log-middle", "equipment-c"),
|
||||
("log-middle", "equipment-d"),
|
||||
]
|
||||
|
||||
result = get_recent_mgroup_equipment_ids("mgroup-1", log_limit=50)
|
||||
|
||||
self.assertEqual(
|
||||
result,
|
||||
["equipment-a", "equipment-c", "equipment-b", "equipment-d"],
|
||||
)
|
||||
log_queryset.order_by.return_value.values_list.return_value.__getitem__.assert_called_once_with(
|
||||
slice(None, 50, None)
|
||||
)
|
||||
|
||||
@patch("apps.wpm.services.Mlog")
|
||||
def test_returns_empty_when_recent_logs_have_no_equipment(self, mlog):
|
||||
log_queryset = MagicMock()
|
||||
mlog.objects.filter.return_value = log_queryset
|
||||
log_queryset.order_by.return_value.values_list.return_value.__getitem__.return_value = [
|
||||
("log-1", None, None),
|
||||
("log-2", None, None),
|
||||
]
|
||||
through_queryset = MagicMock()
|
||||
mlog.equipments.through.objects.filter.return_value = through_queryset
|
||||
through_queryset.order_by.return_value.values_list.return_value = []
|
||||
|
||||
result = get_recent_mgroup_equipment_ids("mgroup-1")
|
||||
|
||||
self.assertEqual(result, [])
|
||||
|
|
@ -5,7 +5,7 @@ from django.test.utils import CaptureQueriesContext
|
|||
from apps.mtm.models import Material
|
||||
from apps.qm.models import Defect
|
||||
from apps.system.models import Dept, User
|
||||
from apps.wpm.models import Handover, WMaterial
|
||||
from apps.wpm.models import Handover, Handoverb, WMaterial
|
||||
from apps.wpm.serializers import HandoverSerializer
|
||||
|
||||
|
||||
|
|
@ -56,3 +56,32 @@ class HandoverSerializerQueryTests(TestCase):
|
|||
if 'FROM "wpm_wmaterial"' in query["sql"]
|
||||
]
|
||||
self.assertEqual(len(inventory_queries), 1, inventory_queries)
|
||||
|
||||
def test_detail_contains_display_fields_for_handover_and_items(self):
|
||||
handover = Handover.objects.create(
|
||||
send_date="2026-08-07",
|
||||
send_user=self.user,
|
||||
send_dept=self.dept,
|
||||
recive_dept=self.dept,
|
||||
material=self.material,
|
||||
wm=self.inventories[0],
|
||||
count=1,
|
||||
type=Handover.H_NORMAL,
|
||||
mtype=Handover.H_NORMAL,
|
||||
)
|
||||
Handoverb.objects.create(
|
||||
handover=handover,
|
||||
wm=self.inventories[0],
|
||||
batch=self.inventories[0].batch,
|
||||
count=1,
|
||||
)
|
||||
|
||||
data = HandoverSerializer(handover).data
|
||||
|
||||
self.assertEqual(data["send_dept_name"], self.dept.name)
|
||||
self.assertEqual(data["recive_dept_name"], self.dept.name)
|
||||
self.assertEqual(data["type_name"], "正常交接")
|
||||
self.assertEqual(data["mtype_name"], "正常")
|
||||
self.assertEqual(data["handoverb"][0]["material_name"], str(self.material))
|
||||
self.assertEqual(data["handoverb"][0]["state_name"], "合格")
|
||||
self.assertEqual(data["handoverb"][0]["count_available"], 1)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from rest_framework.decorators import action
|
|||
from rest_framework.exceptions import ParseError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
from django.db.models import Sum
|
||||
from django.db.models import Case, IntegerField, Sum, When
|
||||
from django.utils import timezone
|
||||
from apps.system.models import User
|
||||
|
||||
|
|
@ -54,13 +54,23 @@ from .serializers import (
|
|||
MlogUserSerializer,
|
||||
BatchLogSerializer,
|
||||
MlogQuickSerializer,
|
||||
MlogEquipmentOptionSerializer,
|
||||
MlogbwStartTestSerializer,
|
||||
HandoverListSerializer,
|
||||
BatchChangeSerializer,
|
||||
MlogbOutPatchUpdateSerializer
|
||||
)
|
||||
from .services import mlog_submit, handover_submit, mlog_revert, get_batch_dag, handover_revert
|
||||
from apps.wpm.services import mlog_submit_validate, generate_new_batch
|
||||
from .services import (
|
||||
RECENT_EQUIPMENT_LOG_LIMIT,
|
||||
generate_new_batch,
|
||||
get_batch_dag,
|
||||
get_recent_mgroup_equipment_ids,
|
||||
handover_revert,
|
||||
handover_submit,
|
||||
mlog_revert,
|
||||
mlog_submit,
|
||||
mlog_submit_validate,
|
||||
)
|
||||
from apps.wf.models import State, Ticket
|
||||
from apps.wpmw.models import Wpr
|
||||
from apps.qm.models import Qct, Ftest, TestItem
|
||||
|
|
@ -332,6 +342,92 @@ class MlogViewSet(CustomModelViewSet):
|
|||
]
|
||||
ordering_fields = ["create_time", "update_time"]
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[
|
||||
openapi.Parameter(
|
||||
name="mgroup",
|
||||
in_=openapi.IN_QUERY,
|
||||
description="日志所属工段",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=True,
|
||||
),
|
||||
openapi.Parameter(
|
||||
name="search",
|
||||
in_=openapi.IN_QUERY,
|
||||
description="按设备名称或编号搜索全部生产设备",
|
||||
type=openapi.TYPE_STRING,
|
||||
required=False,
|
||||
),
|
||||
]
|
||||
)
|
||||
@action(
|
||||
methods=["get"],
|
||||
detail=False,
|
||||
perms_map={"get": "*"},
|
||||
serializer_class=MlogEquipmentOptionSerializer,
|
||||
)
|
||||
def equipment_options(self, request, *args, **kwargs):
|
||||
"""返回本工段设备、最近 50 条日志用过的设备或搜索结果。"""
|
||||
mgroup_id = request.query_params.get("mgroup")
|
||||
if not mgroup_id:
|
||||
raise ParseError("请传入mgroup参数")
|
||||
|
||||
search = request.query_params.get("search", "").strip()
|
||||
owned_ids = list(
|
||||
Equipment.objects.filter(
|
||||
type=Equipment.EQUIP_TYPE_PRO,
|
||||
mgroup_id=mgroup_id,
|
||||
)
|
||||
.order_by("name", "number")
|
||||
.values_list("id", flat=True)
|
||||
)
|
||||
owned_id_set = set(owned_ids)
|
||||
|
||||
if search:
|
||||
queryset = (
|
||||
Equipment.objects.filter(type=Equipment.EQUIP_TYPE_PRO)
|
||||
.filter(Q(name__icontains=search) | Q(number__icontains=search))
|
||||
.order_by("name", "number")
|
||||
)
|
||||
option_group = "搜索结果"
|
||||
else:
|
||||
recent_ids = get_recent_mgroup_equipment_ids(
|
||||
mgroup_id, RECENT_EQUIPMENT_LOG_LIMIT
|
||||
)
|
||||
option_ids = list(dict.fromkeys([*owned_ids, *recent_ids]))
|
||||
if option_ids:
|
||||
order = Case(
|
||||
*[
|
||||
When(id=equipment_id, then=position)
|
||||
for position, equipment_id in enumerate(option_ids)
|
||||
],
|
||||
output_field=IntegerField(),
|
||||
)
|
||||
queryset = Equipment.objects.filter(
|
||||
id__in=option_ids,
|
||||
type=Equipment.EQUIP_TYPE_PRO,
|
||||
).order_by(order)
|
||||
else:
|
||||
queryset = Equipment.objects.none()
|
||||
option_group = None
|
||||
|
||||
queryset = queryset.select_related("mgroup")
|
||||
page = self.paginate_queryset(queryset)
|
||||
equipment_list = page if page is not None else queryset
|
||||
data = MlogEquipmentOptionSerializer(
|
||||
equipment_list,
|
||||
many=True,
|
||||
context=self.get_serializer_context(),
|
||||
).data
|
||||
for item in data:
|
||||
item["option_group"] = option_group or (
|
||||
"本工段设备" if item["id"] in owned_id_set else "近期使用"
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(data)
|
||||
return Response(data)
|
||||
|
||||
def add_info_for_item(self, data):
|
||||
if data.get("oinfo_json", {}):
|
||||
czx_dict = dict(TestItem.objects.filter(id__in=data.get("oinfo_json", {}).keys()).values_list("id", "name"))
|
||||
|
|
@ -592,7 +688,20 @@ class HandoverViewSet(CustomModelViewSet):
|
|||
select_related_fields = ["send_user", "send_mgroup", "send_dept", "recive_user", "recive_mgroup", "recive_dept", "wm", "material_changed", "material", "material__process"]
|
||||
filterset_class = HandoverFilter
|
||||
search_fields = ["material__name", "material__number", "material__specification", "batch", "material__model", "b_handover__batch", "new_batch", "wm__batch"]
|
||||
prefetch_related_fields = [Prefetch("b_handover", queryset=Handoverb.objects.select_related("wm__defect")), "ticket__state"]
|
||||
prefetch_related_fields = ["ticket__state"]
|
||||
|
||||
def get_queryset_custom(self, queryset):
|
||||
if self.action not in ["list", "retrieve"]:
|
||||
return queryset
|
||||
|
||||
detail_queryset = Handoverb.objects.select_related(
|
||||
"handover", "wm__defect", "wm__material", "wm__mgroup", "wm__belong_dept"
|
||||
)
|
||||
if self.action == "retrieve":
|
||||
detail_queryset = detail_queryset.prefetch_related("w_handoverb")
|
||||
return queryset.prefetch_related(
|
||||
Prefetch("b_handover", queryset=detail_queryset)
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance: Handover):
|
||||
user = self.request.user
|
||||
|
|
@ -680,7 +789,7 @@ class HandoverViewSet(CustomModelViewSet):
|
|||
m_qs = m_qs.filter(process__route_p__material_in__id=materialInId) | m_qs.filter(process__route_p__routemat_route__material__id=materialInId)
|
||||
elif type in [Handover.H_SCRAP]:
|
||||
m_qs = m_qs.filter(process=None)
|
||||
return Response(list(m_qs.values("id", "name").distinct()))
|
||||
return Response(list(m_qs.values("id", "name", "belong_dept").distinct()))
|
||||
|
||||
@action(methods=["post"], detail=False, perms_map={"post": "handover.create"}, serializer_class=GenHandoverWmSerializer)
|
||||
@transaction.atomic
|
||||
|
|
|
|||
Loading…
Reference in New Issue