This commit is contained in:
shijing 2026-08-04 09:55:18 +08:00
commit cfcd6c2ba3
14 changed files with 502 additions and 33 deletions

View File

@ -1,7 +1,8 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.enm.views import (MpointViewSet, MpointStatViewSet,
EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView)
from apps.enm.views import (MpointViewSet, MpointStatViewSet,
EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView,
WengfuMpLogxAPIView, WengfuMpointListAPIView)
API_BASE_URL = 'api/enm/'
HTML_BASE_URL = 'dhtml/enm/'
@ -16,4 +17,6 @@ router.register('xscript', XscriptViewSet, basename='xscript')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
path(f'{API_BASE_URL}mplogx/', MpLogxAPIView.as_view(), name='mplogx_list'),
path(f'{API_BASE_URL}wengfu_mplogx/', WengfuMpLogxAPIView.as_view(), name='wengfu_mplogx'),
path(f'{API_BASE_URL}wengfu_mpoints/', WengfuMpointListAPIView.as_view(), name='wengfu_mpoints'),
]

View File

@ -265,6 +265,134 @@ class MpLogxAPIView(APIView):
class WengfuMpLogxAPIView(APIView):
perms_map = {"get": "*", "post": "*"}
def _get_conn(self):
from apps.utils.sql import DbConnection
return DbConnection(
host='10.0.11.52', user='postgres',
password='zcDsj@2024', database='hfnf', dbtype='pg'
)
def get(self, request, *args, **kwargs):
mpoint_ids = request.query_params.get("mpoint_ids", None)
timex_gte = request.query_params.get("timex__gte", None)
timex_lte = request.query_params.get("timex__lte", None)
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 20))
conditions = []
params = []
if mpoint_ids:
ids = [x.strip() for x in mpoint_ids.split(",") if x.strip()]
placeholders = ",".join(["%s"] * len(ids))
conditions.append(f"m.mpoint_id IN ({placeholders})")
params.extend(ids)
if timex_gte:
conditions.append("m.timex >= %s")
params.append(timex_gte)
if timex_lte:
conditions.append("m.timex <= %s")
params.append(timex_lte)
where = ("WHERE " + " AND ".join(conditions)) if conditions else ""
with self._get_conn() as cur:
count_sql = f"""SELECT COUNT(*) FROM mplogx_wengfu m {where}"""
cur.execute(count_sql, params)
total = cur.fetchone()[0]
data_sql = f"""
SELECT m.mpoint_id, n.point_name AS mpoint_name, m.timex,
m.val_float AS val, m.val_bool
FROM mplogx_wengfu m
LEFT JOIN cj_mpoint_name n ON n.mpoint_id = m.mpoint_id AND n.company = '瓮福'
{where}
ORDER BY m.timex DESC
LIMIT %s OFFSET %s
"""
cur.execute(data_sql, params + [page_size, (page - 1) * page_size])
columns = [desc[0] for desc in cur.description]
results = []
for row in cur.fetchall():
d = dict(zip(columns, row))
if d.get("timex"):
d["timex"] = d["timex"].strftime("%Y-%m-%d %H:%M:%S")
results.append(d)
return Response({"count": total, "results": results})
def post(self, request, *args, **kwargs):
"""导出或图表数据(不分页)"""
mpoint_ids = request.data.get("mpoint_ids", [])
timex_gte = request.data.get("timex__gte")
timex_lte = request.data.get("timex__lte")
if not mpoint_ids or not timex_gte or not timex_lte:
raise ParseError("mpoint_ids, timex__gte, timex__lte are required")
conditions = []
params = []
placeholders = ",".join(["%s"] * len(mpoint_ids))
conditions.append(f"m.mpoint_id IN ({placeholders})")
params.extend(mpoint_ids)
conditions.append("m.timex >= %s")
params.append(timex_gte)
conditions.append("m.timex <= %s")
params.append(timex_lte)
where = "WHERE " + " AND ".join(conditions)
with self._get_conn() as cur:
sql = f"""
SELECT m.mpoint_id, n.point_name AS mpoint_name, m.timex, m.val_float AS val, m.val_bool
FROM mplogx_wengfu m
LEFT JOIN cj_mpoint_name n ON n.mpoint_id = m.mpoint_id AND n.company = '瓮福'
{where}
ORDER BY m.timex ASC
"""
cur.execute(sql, params)
columns = [desc[0] for desc in cur.description]
results = []
for row in cur.fetchall():
d = dict(zip(columns, row))
if d.get("timex"):
d["timex"] = d["timex"].strftime("%Y-%m-%d %H:%M:%S")
results.append(d)
return Response(results)
class WengfuMpointListAPIView(APIView):
"""瓮福测点列表(供下拉选择)"""
perms_map = {"get": "*"}
def get(self, request, *args, **kwargs):
keyword = request.query_params.get("keyword", "")
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 50))
from apps.utils.sql import DbConnection
with DbConnection(host='10.0.11.52', user='postgres',
password='zcDsj@2024', database='hfnf', dbtype='pg') as cur:
conditions = ["company = '瓮福'"]
params = []
if keyword:
conditions.append("(mpoint_id ILIKE %s OR point_name ILIKE %s)")
params.extend([f"%{keyword}%", f"%{keyword}%"])
where = "WHERE " + " AND ".join(conditions)
cur.execute(f"SELECT COUNT(*) FROM cj_mpoint_name {where}", params)
total = cur.fetchone()[0]
cur.execute(f"""
SELECT mpoint_id, point_name FROM cj_mpoint_name
{where} ORDER BY mpoint_id LIMIT %s OFFSET %s
""", params + [page_size, (page - 1) * page_size])
columns = [desc[0] for desc in cur.description]
results = [dict(zip(columns, row)) for row in cur.fetchall()]
return Response({"count": total, "results": results})
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
"""
list: 测点采集数据

View File

@ -390,7 +390,24 @@ class EmployeeViewSet(CustomModelViewSet):
# 获取模型字段
model_fields = {f.name: f for f in Employee._meta.fields}
from django.db.models import CharField, TextField
_ABSENT = object() # 区分“列没出现”与“列为空”
_KEEP = object() # 该字段不可清空时,保留原值
def empty_value(field_name):
"""全量覆盖时,空单元格应写入的“清空值”;不可清空的字段返回 _KEEP。"""
if field_name == 'belong_dept_id':
return None # 部门 FK 可空
mf = model_fields.get(field_name)
if mf is None:
return _KEEP
if getattr(mf, 'null', False):
return None
if isinstance(mf, (CharField, TextField)) and getattr(mf, 'blank', False):
return ''
return _KEEP # 非空且不可为空(如布尔/性别/类别)-> 保留原值
for row_num, row in enumerate(rows[1:], start=2):
try:
data = {}
@ -419,12 +436,15 @@ class EmployeeViewSet(CustomModelViewSet):
data['type'] = TYPE_MAPPING[excel_type]
else:
raise ParseError(f'{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
# 处理部门外键:填了就校验是否存在并赋值;为空时不动(新增场景的必填在下方创建处校验)
dept_name = data.pop('belong_dept', None)
if dept_name:
if dept_name not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_name}"不存在')
data['belong_dept_id'] = dept_map[dept_name]
# 处理部门外键:填了校验并赋值;列存在但为空 -> 清空(新增场景的必填在创建处校验)
dept_raw = data.pop('belong_dept', _ABSENT)
if dept_raw is not _ABSENT:
if dept_raw:
if dept_raw not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_raw}"不存在')
data['belong_dept_id'] = dept_map[dept_raw]
else:
data['belong_dept_id'] = None
# 数据验证
if data.get('phone'):
@ -454,11 +474,14 @@ class EmployeeViewSet(CustomModelViewSet):
(e for e in name_matches if e.id_number == id_number), None)
if existing:
# 用 Excel 中填写了值的列覆盖数据库已有数据;空单元格保持原值不变
# 全量覆盖:以本次 Excel 为准。填了值的列覆盖原值;
# 空单元格清空原值(不可为空的字段如布尔/性别/类别则保留原值)。
updated_fields = []
for field_name, value in data.items():
if value in [None, '']:
continue
value = empty_value(field_name)
if value is _KEEP:
continue
if getattr(existing, field_name, None) != value:
setattr(existing, field_name, value)
updated_fields.append(field_name)
@ -470,9 +493,11 @@ class EmployeeViewSet(CustomModelViewSet):
created = False
else:
# 新增人员时所属部门必填
if 'belong_dept_id' not in data:
if not data.get('belong_dept_id'):
raise ParseError(f'{row_num}行,新增人员时所属部门不能为空')
Employee.objects.create(**data)
# 新增时丢弃空值,交给模型默认值/允许为空
create_data = {k: v for k, v in data.items() if v is not None}
Employee.objects.create(**create_data)
created = True
except Exception as e:
raise
@ -501,18 +526,23 @@ def convert_field_value(model_field, value, row_num):
# ===== 日期字段 =====
if isinstance(model_field, CharField):
sval = str(value)
if isinstance(value, str):
if 'e' in value.lower():
try:
return str(int(float(value)))
sval = str(int(float(value)))
except ValueError:
pass
if '.' in value:
try:
return str(int(float(value)))
sval = str(int(float(value)))
except ValueError:
pass
return str(value)
# 超长字段给出明确提示,避免数据库报 value too long
if model_field.max_length and len(sval) > model_field.max_length:
raise ParseError(
f'{row_num}行,字段 {model_field.verbose_name} 长度超过 {model_field.max_length} 个字符(当前 {len(sval)}')
return sval
if isinstance(model_field, DateField):

View File

@ -31,7 +31,27 @@ class SoftDeletableQuerySetMixin(object):
return super(SoftDeletableQuerySetMixin, self).delete()
class SoftDeletableQuerySet(SoftDeletableQuerySetMixin, QuerySet):
class SnowflakeQuerySet(QuerySet):
"""为 BaseModel 的批量创建统一准备雪花主键。"""
def bulk_create(self, objs, *args, **kwargs):
objs = list(objs)
for obj in objs:
if not obj.pk:
obj.pk = str(idWorker.get_id())
else:
obj.pk = str(obj.pk)
return super().bulk_create(objs, *args, **kwargs)
class SnowflakeManager(models.Manager.from_queryset(SnowflakeQuerySet)):
pass
class SoftDeletableQuerySet(
SoftDeletableQuerySetMixin,
SnowflakeQuerySet,
):
pass
@ -111,6 +131,8 @@ class BaseModel(models.Model):
is_deleted = models.BooleanField(
default=False, verbose_name='删除标记', help_text='删除标记')
objects = SnowflakeManager()
class Meta:
abstract = True

116
apps/utils/tests.py Normal file
View File

@ -0,0 +1,116 @@
from unittest.mock import patch
from django.db.models.query import QuerySet
from django.test import SimpleTestCase, TestCase
from apps.develop.models import Project
from apps.mtm.models import Material
from apps.system.models import User
from apps.wpmw.models import WprDefect
class SnowflakeBulkCreateTests(SimpleTestCase):
@patch.object(QuerySet, "bulk_create")
@patch(
"apps.utils.models.idWorker.get_id",
side_effect=[100, 101],
)
def test_base_model_bulk_create_assigns_missing_ids(
self,
get_id,
bulk_create,
):
objs = [
WprDefect(defect_id="10", wpr_id="20"),
WprDefect(defect_id="11", wpr_id="20"),
]
WprDefect.objects.bulk_create(objs)
self.assertEqual([obj.id for obj in objs], ["100", "101"])
self.assertEqual(get_id.call_count, 2)
bulk_create.assert_called_once()
@patch.object(QuerySet, "bulk_create")
@patch("apps.utils.models.idWorker.get_id")
def test_base_model_bulk_create_preserves_existing_ids(
self,
get_id,
bulk_create,
):
objs = [
WprDefect(id=100, defect_id="10", wpr_id="20"),
WprDefect(id="101", defect_id="11", wpr_id="20"),
]
WprDefect.objects.bulk_create(objs)
self.assertEqual([obj.id for obj in objs], ["100", "101"])
get_id.assert_not_called()
bulk_create.assert_called_once()
@patch.object(QuerySet, "bulk_create")
@patch(
"apps.utils.models.idWorker.get_id",
side_effect=[200, 201],
)
def test_soft_delete_manager_uses_snowflake_bulk_create(
self,
get_id,
bulk_create,
):
objs = [
Material(name="物料1"),
Material(name="物料2"),
]
Material.objects.bulk_create(objs)
self.assertEqual([obj.id for obj in objs], ["200", "201"])
self.assertEqual(get_id.call_count, 2)
bulk_create.assert_called_once()
@patch.object(QuerySet, "bulk_create")
@patch(
"apps.utils.models.idWorker.get_id",
side_effect=[300, 301],
)
def test_custom_user_manager_uses_snowflake_bulk_create(
self,
get_id,
bulk_create,
):
objs = [
User(username="bulk-user-1"),
User(username="bulk-user-2"),
]
User.objects.bulk_create(objs)
self.assertEqual([obj.id for obj in objs], ["300", "301"])
self.assertEqual(get_id.call_count, 2)
bulk_create.assert_called_once()
class SnowflakeBulkCreateDatabaseTests(TestCase):
@patch(
"apps.utils.models.idWorker.get_id",
side_effect=[900000000000000001, 900000000000000002],
)
def test_bulk_create_persists_generated_ids(self, get_id):
objs = [
Project(name="批量项目1", code="bulk-project-1"),
Project(name="批量项目2", code="bulk-project-2"),
]
Project.objects.bulk_create(objs)
self.assertEqual(
[obj.id for obj in objs],
["900000000000000001", "900000000000000002"],
)
self.assertEqual(
set(Project.objects.values_list("id", flat=True)),
{"900000000000000001", "900000000000000002"},
)
self.assertEqual(get_id.call_count, 2)

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.27 on 2026-08-03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("system", "0007_alter_dept_create_by_alter_dept_third_info_and_more"),
("wpm", "0137_mlog_wpr_defect_snapshot"),
]
operations = [
migrations.AddField(
model_name="mlogbw",
name="files",
field=models.ManyToManyField(
blank=True,
related_name="mlogbw_outputs",
to="system.file",
verbose_name="产出附件",
),
),
]

View File

@ -740,6 +740,12 @@ class Mlogbw(BaseModel):
"""TN: 单个产品生产/检验日志
"""
number = models.TextField('单个编号', db_index=True)
files = models.ManyToManyField(
"system.File",
verbose_name="产出附件",
blank=True,
related_name="mlogbw_outputs",
)
mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE, related_name="w_mlogb")
mlogbw_from = models.ForeignKey("self", verbose_name='来源个', on_delete=models.CASCADE, null=True, blank=True, related_name="w_mlogbw_from")
wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL

View File

@ -8,7 +8,7 @@ from .models import (SfLog, StLog, SfLogExp, WMaterial, Mlog,
Handover, Handoverb, Mlogb, AttLog,
OtherLog, Fmlog, BatchSt, Mlogbw, Handoverbw, MlogbDefect, MlogUser, BatchLog)
from apps.system.models import Dept, User
from apps.system.serializers import UserSimpleSerializer
from apps.system.serializers import FileSerializer, UserSimpleSerializer
from apps.pm.models import Mtask, Mtaskb
from apps.wpm.tasks import cal_enstat_when_pcoal_heat_change, cal_enstat_when_team_change, cal_exp_duration_sec
from apps.wpm.services import get_sflog, find_material_can_change, generate_new_batch
@ -923,6 +923,8 @@ class MlogbInUpdateSerializer(CustomModelSerializer):
return ins
class MlogbwCreateUpdateSerializer(CustomModelSerializer):
files = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
files_ = FileSerializer(source="files", many=True, read_only=True)
ftest = FtestProcessSerializer(required=False, allow_null=True)
equip_name = serializers.CharField(source='equip.name', read_only=True)
equip_number = serializers.CharField(source='equip.number', read_only=True)
@ -936,7 +938,8 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
fields = ["id", "number", "wpr", "note",
"mlogb", "ftest", "equip", "tooling", "work_start_time",
"work_end_time", "mlogbw_from", "equip_name", "equip_number",
"tooling_name", "tooling_number", "wpr_number_out", "wpr_pre_info", "mlogb__batch"]
"tooling_name", "tooling_number", "wpr_number_out", "wpr_pre_info", "mlogb__batch",
"files", "files_"]
read_only_fields = ["mlogbw_from"]
def validate(self, attrs):
@ -998,6 +1001,15 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
class MlogbwListSerializer(MlogbwCreateUpdateSerializer):
ftest = FtestProcessListSerializer(read_only=True)
class MlogbwFilesSerializer(CustomModelSerializer):
files_ = FileSerializer(source="files", many=True, read_only=True)
class Meta:
model = Mlogbw
fields = ["id", "files", "files_"]
read_only_fields = ["id", "files_"]
class MlogbwStartTestSerializer(serializers.Serializer):
mlogbw_ids = serializers.ListField(child=serializers.CharField(), label="mlogbwId列表")
test_equip = serializers.CharField(label="测试设备", allow_null=True, required=False, allow_blank=True)
@ -1398,6 +1410,7 @@ class HandoverSerializer(CustomModelSerializer):
next_mat = None
next_state = None
next_defect = None
next_defect_grade = None
if new_wm and attrs["type"] != Handover.H_CHANGE:
next_mat = new_wm.material
next_state = new_wm.state
@ -1418,13 +1431,15 @@ class HandoverSerializer(CustomModelSerializer):
if clear_defect and new_wm is not None and new_wm.defect is not None:
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
):
if wm.defect is None:
raise ParseError(
f'{ind+1}行-批次追踪物料仅合格B类可清除批次缺陷'
f'{ind+1}行-批次追踪物料仅同缺陷等级可清除批次缺陷'
)
if next_defect_grade is None:
next_defect_grade = wm.defect.okcate
elif next_defect_grade != wm.defect.okcate:
raise ParseError(
f'{ind+1}行-批次追踪物料仅同缺陷等级可清除批次缺陷'
)
if next_mat is None:
next_mat = wm.material

View File

@ -21,6 +21,7 @@ from apps.wpmw.models import Wpr, WprDefect
from ..qm.models import Defect, Ftest
from django.db.models import Count, Q
from apps.utils.tasks import ctask_run
from apps.utils.snowflake import idWorker
from apps.mtm.models import Process, WmScope
from django.db.models import F
@ -214,6 +215,7 @@ def restore_mlog_wpr_defects(mlog: Mlog):
continue
restored.extend(
WprDefect(
id=idWorker.get_id(),
wpr=wpr,
defect_id=item["defect_id"],
is_main=item.get("is_main", False),

View File

@ -462,22 +462,58 @@ class WMaterialScopeTests(SimpleTestCase):
self.assertTrue(validated["clear_defect"])
self.assertEqual(validated["count"], 2)
def test_batch_tracking_merge_cannot_clear_notok_defect(self):
def test_batch_tracking_merge_can_clear_same_grade_notok_defects(self):
material = Material(tracking=Material.MA_TRACKING_BATCH)
defect = Defect(id="1", okcate=Defect.DEFECT_NOTOK)
wm = WMaterial(
defect_a = Defect(id="1", okcate=Defect.DEFECT_NOTOK)
defect_b = Defect(id="2", okcate=Defect.DEFECT_NOTOK)
wm_a = WMaterial(
id="10", material=material, batch="N-001", count=1,
state=WMaterial.WM_NOTOK, defect=defect,
state=WMaterial.WM_NOTOK, defect=defect_a,
)
wm_b = WMaterial(
id="20", material=material, batch="N-002", count=1,
state=WMaterial.WM_NOTOK, defect=defect_b,
)
validated = HandoverSerializer().validate({
"wm": wm_a,
"handoverb": [
{"wm": wm_a, "count": 1},
{"wm": wm_b, "count": 1},
],
"new_batch": "N-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_mixed_defect_grades(self):
material = Material(tracking=Material.MA_TRACKING_BATCH)
defect_b = Defect(id="1", okcate=Defect.DEFECT_OK_B)
defect_notok = Defect(id="2", okcate=Defect.DEFECT_NOTOK)
wm_a = WMaterial(
id="10", material=material, batch="B-001", count=1,
state=WMaterial.WM_OK, defect=defect_b,
)
wm_b = WMaterial(
id="20", material=material, batch="N-001", count=1,
state=WMaterial.WM_OK, defect=defect_notok,
)
with self.assertRaisesMessage(
ParseError,
"批次追踪物料仅合格B类可清除批次缺陷",
"批次追踪物料仅同缺陷等级可清除批次缺陷",
):
HandoverSerializer().validate({
"wm": wm,
"handoverb": [{"wm": wm, "count": 1}],
"new_batch": "N-MERGED",
"wm": wm_a,
"handoverb": [
{"wm": wm_a, "count": 1},
{"wm": wm_b, "count": 1},
],
"new_batch": "MIXED-MERGED",
"clear_defect": True,
"type": Handover.H_NORMAL,
"mtype": Handover.H_MERGE,
@ -512,6 +548,30 @@ class WMaterialScopeTests(SimpleTestCase):
class WprDefectLifecycleTests(SimpleTestCase):
@patch("apps.wpmw.models.WprDefect.objects.bulk_create")
@patch("apps.wpmw.models.WprDefect.objects.filter")
@patch(
"apps.wpmw.models.idWorker.get_id",
side_effect=["100", "101"],
)
def test_replace_defects_assigns_unique_ids_before_bulk_create(
self,
get_id,
defect_filter,
bulk_create,
):
wpr = Wpr(id="1")
wpr.replace_defects([
{"defect_id": "10", "is_main": True},
{"defect_id": "11", "is_main": False},
])
defect_filter.return_value.delete.assert_called_once_with()
created = bulk_create.call_args.args[0]
self.assertEqual([str(item.id) for item in created], ["100", "101"])
self.assertEqual(get_id.call_count, 2)
@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)
@ -635,8 +695,13 @@ class MlogWprDefectSnapshotTests(SimpleTestCase):
@patch("apps.wpm.services.WprDefect.objects.bulk_create")
@patch("apps.wpm.services.WprDefect.objects.filter")
@patch("apps.wpm.services.Wpr.objects.select_for_update")
@patch(
"apps.wpm.services.idWorker.get_id",
side_effect=["100", "101"],
)
def test_restore_replaces_current_defects_exactly(
self,
get_id,
select_for_update,
defect_filter,
bulk_create,
@ -662,6 +727,8 @@ class MlogWprDefectSnapshotTests(SimpleTestCase):
[(str(item.defect_id), item.is_main) for item in created],
[("10", True), ("11", False)],
)
self.assertEqual([str(item.id) for item in created], ["100", "101"])
self.assertEqual(get_id.call_count, 2)
@skipUnless(

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,36 @@
from django.test import SimpleTestCase
from django.urls import reverse
from apps.system.models import File
from apps.wpm.models import Mlogbw
from apps.wpm.serializers import MlogbwFilesSerializer, MlogbwListSerializer
from apps.wpm.views import MlogbwViewSet
class MlogbwFilesDefinitionTests(SimpleTestCase):
def test_files_field_links_system_file(self):
field = Mlogbw._meta.get_field("files")
self.assertTrue(field.blank)
self.assertIs(field.remote_field.model, File)
self.assertEqual(field.remote_field.related_name, "mlogbw_outputs")
def test_list_serializer_exposes_file_ids_and_details(self):
fields = MlogbwListSerializer().fields
self.assertTrue(fields["files"].read_only)
self.assertTrue(fields["files_"].read_only)
def test_files_serializer_only_writes_file_relations(self):
fields = MlogbwFilesSerializer().fields
self.assertEqual(set(fields), {"id", "files", "files_"})
self.assertFalse(fields["files"].read_only)
def test_files_action_supports_read_and_update(self):
self.assertEqual(MlogbwViewSet.files.mapping["get"], "files")
self.assertEqual(MlogbwViewSet.files.mapping["put"], "files")
self.assertEqual(
reverse("mlogbw-files", kwargs={"pk": "single-id"}),
"/api/wpm/mlogbw/single-id/files/",
)

View File

@ -46,6 +46,7 @@ from .serializers import (
BatchStSerializer,
MlogbwCreateUpdateSerializer,
MlogbwListSerializer,
MlogbwFilesSerializer,
HandoverMgroupSerializer,
MlogListSerializer,
MlogbSerializer,
@ -1147,6 +1148,7 @@ class MlogbwViewSet(CustomModelViewSet):
filterset_class = MlogbwFilter
select_related_fields = ["ftest", "equip", "tooling", "wpr", "mlogb"]
prefetch_related_fields = [
"files",
Prefetch(
"ftest__items_ftest",
queryset=FtestItem.objects.select_related(
@ -1166,6 +1168,21 @@ class MlogbwViewSet(CustomModelViewSet):
ordering = ["mlogb", "number", "create_time"]
ordering_fields = ["number", "create_time", "mlogb"]
@action(detail=True, methods=["get", "put"], url_path="files")
@transaction.atomic
def files(self, request, *args, **kwargs):
instance = self.get_object()
if instance.mlogb.material_out_id is None:
raise ParseError("只能给输出单件上传附件")
if request.method == "PUT":
MlogViewSet.lock_and_check_can_update(instance.mlogb.mlog)
serializer = MlogbwFilesSerializer(instance, data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
else:
serializer = MlogbwFilesSerializer(instance)
return Response(serializer.data)
def perform_create(self, serializer):
ins: Mlogbw = serializer.save()
mlog: Mlog = None

View File

@ -5,6 +5,7 @@ from apps.utils.models import BaseModel
from apps.mtm.models import Material
from rest_framework.exceptions import ParseError
from apps.wpm.models import WmStateOption, Mlogbw, Handoverbw
from apps.utils.snowflake import idWorker
from apps.utils.tools import update_dict
from apps.inm.models import MIOItemw
from django.db.models import F, Value
@ -130,6 +131,7 @@ class Wpr(BaseModel):
WprDefect.objects.filter(wpr=self).delete()
WprDefect.objects.bulk_create([
WprDefect(
id=idWorker.get_id(),
wpr=self,
defect_id=item["defect_id"],
is_main=item.get("is_main", False),