Compare commits

..

No commits in common. "master" and "3.1.2026042912" have entirely different histories.

36 changed files with 208 additions and 1406 deletions

View File

@ -1,65 +0,0 @@
---
description: 生成 changelog、更新 SYS_VERSION、打 tag 并推到远端
---
发布一个新版本,遵循以下既定流程,**严格按顺序执行**,不要省略或调整步骤。
## 步骤
### 1. 生成 changelog 与版本号
执行:
```bash
bash update_changelog.sh
```
脚本会:
- 在 `changelog.md` 顶部写入 `## 3.1.YYYYMMDDHH`,并附上自上一个 tag 起的 feat / fix / other 提交。
- 终端输出形如 `当前版本号: 3.1.YYYYMMDDHH (请手动修改)`
从输出中**抓取版本号**(即 `3.1.YYYYMMDDHH`),后续步骤记作 `<VER>`
### 2. 更新 `server/settings.py``SYS_VERSION`
`SYS_VERSION = '<旧版本>'` 改为 `SYS_VERSION = '<VER>'`
> 该字段是项目里唯一权威的版本号,前端 `/swagger` 也读它。
### 3. 检查 changelog 顶部内容
`Read` 一下 `changelog.md` 头 ~20 行,确认:
- 顶部是 `## <VER>`
- 自上次 tag 之后的提交都被分类列出。
- 如有内容明显不对(例如重复段、无关分类),先与用户确认,不要直接打 tag。
### 4. 提交 + 打 tag + 推送
参考过往风格commit message 用 `release: <VER>`tag 名直接用版本号、无 `v` 前缀):
```bash
git add changelog.md server/settings.py
git commit -m "release: <VER>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
git tag <VER>
git push
git push origin <VER>
```
可以串成一个 `&&` 链一次执行。
### 5. 汇报
向用户简述:
- 本次版本号 `<VER>`
- changelog 顶部新增了多少行 / 包含哪几类提交。
- commit hash、tag 名、master / tag 都已推到 origin。
## 注意事项
- **不要自己编版本号**:始终用 `update_changelog.sh` 输出里的那一个,不要试图改格式或日期。
- **`SYS_VERSION` 是必须改的**:脚本只更新 `changelog.md`settings 必须手动同步,否则上线后接口返回的版本号还是旧的。
- **CRLF 警告可忽略**Windows 上 `git commit` 会提示 `LF will be replaced by CRLF`,不是错误。
- **打 tag 前确认 working tree 干净**:除了本次 commit 的两个文件外,不应有其他未追踪/未提交的改动混进 release commit。如有先单独 commit 或 stash。
- **失败回滚**:如果 push 失败需要回退,删除本地 tag (`git tag -d <VER>`) 与撤销 commit (`git reset --soft HEAD^`);远端 tag 已 push 的删除需要先与用户确认。

1
.gitignore vendored
View File

@ -27,4 +27,3 @@ temp/*
nohup.out nohup.out
*.zip *.zip
scripts/*.py scripts/*.py
.claude/settings.local.json

View File

@ -1,262 +0,0 @@
"""
伟迪捷 1880 喷码机 ASCII 协议封装
帧格式: ASCII, 字段以 '|' 分隔, 整帧以 <CR>(0x0D) 结尾
- 写指令(SLA/JDA/CQI/JDI/SST/SNO) 通常无回包
- 查询指令(GST/GFT/GJD) 回包同样以 <CR> 结尾
"""
import socket
from rest_framework.exceptions import ParseError
CR = b"\x0d"
class CoderError(ParseError):
pass
class CoderClient:
def __init__(self, ip: str, port: int = 3100, timeout: float = 2.0):
if not ip:
raise CoderError("喷码IP未配置")
self.ip = ip
self.port = port
self.timeout = timeout
def _send(self, frame: bytes, expect_reply: bool = False) -> bytes:
try:
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
s.sendall(frame)
if expect_reply:
buf = b""
while CR not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
return buf
# 写指令: 短暂等待, 捕获机器拒绝(ERR/!); 超时则视为成功
s.settimeout(0.3)
try:
buf = b""
while CR not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
except socket.timeout:
buf = b""
if buf:
text = buf.rstrip(b"\r\n").decode("latin-1", errors="replace")
if text.startswith("ERR") or text.startswith("!"):
raise CoderError(f"喷码机拒绝指令 {frame!r}: {text}")
return b""
except CoderError:
raise
except (socket.timeout, OSError) as e:
raise CoderError(f"喷码通讯失败 {self.ip}:{self.port} - {e}")
@staticmethod
def _encode(text: str, label: str) -> bytes:
if "\r" in text or "\n" in text:
raise CoderError(f"{label}含 CR/LF, 会破坏帧结构")
if "|" in text:
raise CoderError(f"{label}'|', 与 1880 分隔符冲突")
try:
return text.encode("latin-1")
except UnicodeEncodeError as e:
raise CoderError(f"{label}含喷码机不支持的字符: {e}")
def send_command(self, command: str, expect_reply: bool = False) -> bytes:
"""直接下发一条已拼装好的 1880 指令(不含末尾 CR), 由本方法统一补 CR。"""
if "\r" in command or "\n" in command:
raise CoderError("指令含 CR/LF, 由 send_command 统一补 CR, 不要手动拼")
return self._send(command.encode("latin-1") + CR, expect_reply=expect_reply)
def select_job(self, job_name: str):
"""SLA|<jobname>|<CR> 选择信息模板"""
self._send(b"SLA|" + self._encode(job_name, "模板名") + b"|" + CR)
def update_fields(self, fields: dict):
"""JDA|<field>=<value>|...|<CR> 一帧更新多个用户区"""
if not fields:
return
parts = [b"JDA"]
for k, v in fields.items():
parts.append(self._encode(str(k), "用户区名") + b"=" + self._encode(str(v), "喷印内容"))
self._send(b"|".join(parts) + b"|" + CR)
def clear_queue(self):
"""CQI<CR> 清空喷码机内部缓存队列"""
self._send(b"CQI" + CR)
def push_queue(self, fields: dict):
"""JDI|1|<field>=<value>|...|<CR> 入队列数据"""
if not fields:
return
parts = [b"JDI", b"1"]
for k, v in fields.items():
parts.append(self._encode(str(k), "用户区名") + b"=" + self._encode(str(v), "喷印内容"))
self._send(b"|".join(parts) + b"|" + CR)
def set_state(self, state: int):
"""SST|<State>|<CR> 0关机 1开机 3运行 4离线"""
self._send(b"SST|" + str(int(state)).encode("ascii") + b"|" + CR)
def register_print_callback(self):
"""SNO|PRC|1|<CR> 注册打印完成反馈, 之后喷码机每打印一次会回 PRC<CR>"""
self._send(b"SNO|PRC|1|" + CR)
def get_status(self) -> dict:
"""GST<CR> -> STS|<overall>|<error>|<currentjob>|<batchcount>|<totalcount>|<CR>"""
resp = self._send(b"GST" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "STS" or len(parts) < 6:
raise CoderError(f"GST 响应不识别: {resp!r}")
try:
return {
"overall": int(parts[1]),
"error": int(parts[2]),
"current_job": parts[3],
"batch_count": int(parts[4]),
"total_count": int(parts[5]),
}
except ValueError as e:
raise CoderError(f"GST 响应解析失败: {resp!r} - {e}")
def get_faults(self) -> list:
"""GFT<CR> -> FLT|<count>|[<code>|<clearable>|<title>|]*<CR>"""
resp = self._send(b"GFT" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "FLT" or len(parts) < 2:
raise CoderError(f"GFT 响应不识别: {resp!r}")
try:
count = int(parts[1])
except ValueError as e:
raise CoderError(f"GFT 响应解析失败: {resp!r} - {e}")
faults = []
for i in range(count):
base = 2 + i * 3
if base + 2 >= len(parts):
break
faults.append({
"code": parts[base],
"clearable": parts[base + 1] == "1",
"title": parts[base + 2],
})
return faults
def get_current_job(self) -> dict:
"""GJD<CR> -> JDL|<count>|<field>=<value>|...|<CR>"""
resp = self._send(b"GJD" + CR, expect_reply=True)
parts = resp.rstrip(b"\r\n").decode("latin-1").split("|")
if not parts or parts[0] != "JDL" or len(parts) < 2:
raise CoderError(f"GJD 响应不识别: {resp!r}")
try:
count = int(parts[1])
except ValueError as e:
raise CoderError(f"GJD 响应解析失败: {resp!r} - {e}")
fields = {}
for i in range(count):
idx = 2 + i
if idx >= len(parts):
break
pair = parts[idx]
if "=" not in pair:
continue
k, v = pair.split("=", 1)
fields[k] = v
return fields
# ============================================================================
# Hanslaser(大族激光) 老款打标机 标准通讯协议(Winsocket/TCP)
# 帧格式: STX(0x02) + 内容 + ETX(0x03), 每条命令下位机都回一帧
# 单件打标流程: $Initialize_<文件> -> $Data_<v1,v2,...> -> $MarkStart_
# 该协议无队列/批量能力, 只能逐件: 一组 Data 对应一次 MarkStart
# ============================================================================
STX = b"\x02"
ETX = b"\x03"
class HanslaserClient:
encoding = "gbk" # 大族激光为国产机, 文档 code page 936
def __init__(self, ip: str, port: int, timeout: float = 5.0):
if not ip:
raise CoderError("打标机IP未配置")
if not port:
raise CoderError("打标机端口未配置")
self.ip = ip
self.port = port
self.timeout = timeout
def _encode(self, text: str, label: str) -> bytes:
if "\x02" in text or "\x03" in text:
raise CoderError(f"{label}含 STX/ETX, 会破坏帧结构")
if "," in text:
raise CoderError(f"{label}',', 与 Hanslaser $Data 分隔符冲突")
if "\r" in text or "\n" in text:
raise CoderError(f"{label}含 CR/LF")
try:
return text.encode(self.encoding)
except UnicodeEncodeError as e:
raise CoderError(f"{label}含打标机不支持的字符: {e}")
def _send_recv(self, body: bytes) -> str:
"""发 STX+body+ETX, 读回一整帧, 返回去掉 STX/ETX 后的内容字符串。"""
frame = STX + body + ETX
try:
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
s.sendall(frame)
buf = b""
while ETX not in buf:
chunk = s.recv(256)
if not chunk:
break
buf += chunk
except (socket.timeout, OSError) as e:
raise CoderError(f"打标通讯失败 {self.ip}:{self.port} - {e}")
if ETX not in buf:
raise CoderError(f"打标机无完整回包(缺 ETX): {buf!r}")
start = buf.find(STX)
end = buf.find(ETX, start + 1 if start >= 0 else 0)
content = buf[(start + 1 if start >= 0 else 0):end]
return content.decode(self.encoding, errors="replace")
def initialize(self, filename: str):
"""$Initialize_<文件名> -> $Initialize_OK / $Initialize_FALSE"""
resp = self._send_recv(b"$Initialize_" + self._encode(filename, "打标文件名"))
if resp != "$Initialize_OK":
raise CoderError(f"打标文件初始化失败: {resp}")
def send_data(self, values: list):
"""$Data_<v1,v2,...> -> $Receive_OK / $Receive_ERROR / $SysNoReady"""
if not values:
raise CoderError("打标数据为空")
payload = b",".join(self._encode(str(v), "打标内容") for v in values)
resp = self._send_recv(b"$Data_" + payload)
if resp == "$Receive_OK":
return
if resp == "$SysNoReady":
raise CoderError("打标机未准备好($SysNoReady)")
raise CoderError(f"打标数据被拒绝: {resp}")
def mark_start(self):
"""$MarkStart_ -> $MarkStart_OK / $MarkStart_OK_<次数> / $MarkStart_ERROR
下位机开始打标即回 $MarkStart_OK; 打标完成会再回 $MarkStart_OK_<次数>
单件场景只确认开始成功, 不阻塞等待打标完成
"""
resp = self._send_recv(b"$MarkStart_")
if not resp.startswith("$MarkStart_OK"):
raise CoderError(f"启动打标失败: {resp}")
return resp
def mark_stop(self):
"""$MarkStop_ -> $MarkStop_OK / $MarkStop_ERROR"""
resp = self._send_recv(b"$MarkStop_")
if not resp.startswith("$MarkStop_OK"):
raise CoderError(f"停止打标失败: {resp}")
return resp

View File

@ -17,21 +17,6 @@ class Tid2Serializer(serializers.Serializer):
data = serializers.JSONField(label='数据', allow_null=True, required=False) data = serializers.JSONField(label='数据', allow_null=True, required=False)
class CoderSendSerializer(serializers.Serializer):
tdata_list = serializers.ListField(child=serializers.JSONField(), label='模板数据列表', allow_empty=False)
coder_ip = serializers.IPAddressField(label='喷码IP')
coder_port = serializers.IntegerField(label='喷码端口', required=False, allow_null=True)
coder_field = serializers.CharField(label='默认用户区名', required=False, allow_null=True)
coder_jobname = serializers.CharField(label='信息模板名', required=False, allow_null=True)
class HanslaserSendSerializer(serializers.Serializer):
tdata_list = serializers.ListField(child=serializers.JSONField(), label='模板数据列表', allow_empty=False)
coder_ip = serializers.IPAddressField(label='打标机IP')
coder_port = serializers.IntegerField(label='打标机端口')
coder_jobname = serializers.CharField(label='打标文件名', required=False, allow_null=True)
class LabelMatSerializer(serializers.ModelSerializer): class LabelMatSerializer(serializers.ModelSerializer):
material_name = serializers.StringRelatedField(source='material', read_only=True) material_name = serializers.StringRelatedField(source='material', read_only=True)
material_origin_name = serializers.StringRelatedField(source='material_origin', read_only=True) material_origin_name = serializers.StringRelatedField(source='material_origin', read_only=True)
@ -51,7 +36,6 @@ class LabelMatSerializer(serializers.ModelSerializer):
class LabelTemplateSerializer(CustomModelSerializer): class LabelTemplateSerializer(CustomModelSerializer):
class Meta: class Meta:
model = LabelTemplate model = LabelTemplate
fields = '__all__' fields = '__all__'

View File

@ -1,7 +1,6 @@
from apps.cm.models import LableMat, LabelTemplate from apps.cm.models import LableMat, LabelTemplate
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer, CoderSendSerializer, HanslaserSendSerializer from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer
from apps.cm.coder import CoderClient, HanslaserClient
from apps.inm.models import MaterialBatch, MIOItem from apps.inm.models import MaterialBatch, MIOItem
from apps.wpm.models import WMaterial from apps.wpm.models import WMaterial
from rest_framework.exceptions import ParseError, NotFound from rest_framework.exceptions import ParseError, NotFound
@ -115,87 +114,3 @@ class LabelTemplateViewSet(CustomModelViewSet):
data = request.data.get("data", {}) data = request.data.get("data", {})
return Response({"commands": LabelTemplate.gen_commands(label_template, label_template_name, data)}) return Response({"commands": LabelTemplate.gen_commands(label_template, label_template_name, data)})
@action(methods=["post"], detail=True, serializer_class=CoderSendSerializer, perms_map={"post": "*"})
def send_to_coder(self, request, pk=None, *args, **kwargs):
"""
下发指令到伟迪捷 1880 喷码机 (统一走 CQI + JDI 队列模式)
body tdata_list (列表, 至少一条), 每条对应一个产品
每次调用先 CQI 清空队列, 再按顺序 JDI 入队 N ;
喷码机每个外部触发(光电)消费一条, 喷一个产品
每条 tdata 走模板 commands, 解析为 <字段>=<内容> 合并成一帧 JDI;
commands 中不含 '=' 的元素, 会绑定到默认 coder_field
"""
sr = CoderSendSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
lt: LabelTemplate = self.get_object()
ip = vdata.get("coder_ip")
port = vdata.get("coder_port") or 3100
field = vdata.get("coder_field") or "1"
batched = []
for tdata in vdata["tdata_list"]:
commands = LabelTemplate.gen_commands(lt.id, None, tdata)
if not commands:
raise ParseError("模板未生成任何指令")
fields = {}
for item in commands:
text = str(item)
if "=" in text:
k, v = text.split("=", 1)
fields[k.strip()] = v
else:
fields[field] = text
batched.append(fields)
client = CoderClient(ip=ip, port=port)
coder_jobname = vdata.get("coder_jobname")
client.clear_queue()
if coder_jobname:
client.select_job(coder_jobname)
client.clear_queue()
for fields in batched:
client.push_queue(fields)
return Response({"queued": len(batched), "fields": batched, "ip": ip, "port": port, "jobname": coder_jobname})
@action(methods=["post"], detail=True, serializer_class=HanslaserSendSerializer, perms_map={"post": "*"})
def send_to_hanslaser(self, request, pk=None, *args, **kwargs):
"""
下发指令到大族激光(Hanslaser)老款打标机 (单件打标, 协议无队列能力)
body tdata_list, 可选 coder_jobname 先选打标文件
tdata_list 逐件执行 $Data_<逗号值> -> $MarkStart_, 每步等下位机回包;
模板 commands 取每个元素作为位置值( '=' 时取等号右侧), 按顺序逗号拼接
传多条会同步逐件打完, 单件场景传 1 条即可
"""
sr = HanslaserSendSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
lt: LabelTemplate = self.get_object()
ip = vdata.get("coder_ip")
port = vdata.get("coder_port")
coder_jobname = vdata.get("coder_jobname")
batched = []
for tdata in vdata["tdata_list"]:
commands = LabelTemplate.gen_commands(lt.id, None, tdata)
if not commands:
raise ParseError("模板未生成任何指令")
values = []
for item in commands:
text = str(item)
values.append(text.split("=", 1)[1] if "=" in text else text)
batched.append(values)
client = HanslaserClient(ip=ip, port=port)
if coder_jobname:
client.initialize(coder_jobname)
marked = 0
for values in batched:
client.send_data(values)
client.mark_start()
marked += 1
return Response({"marked": marked, "values": batched, "ip": ip, "port": port, "jobname": coder_jobname})

View File

@ -1,7 +1,6 @@
import socket import socket
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
import json import json
import re
import time import time
from django.core.cache import cache from django.core.cache import cache
from apps.utils.thread import MyThread from apps.utils.thread import MyThread
@ -244,75 +243,6 @@ def get_tyy_data_3(*args, retry=2):
raise ParseError(f"未知错误: {str(e)}") raise ParseError(f"未知错误: {str(e)}")
def _parse_vtfp_line(line):
"""解析 SVS 的 VTFP 应答行,返回 (识别字符, 字段列表)。
返回格式形如 ``VTFP 0 <用户自定义数据...>``
- 1 个字段固定为 ``VTFP``
- 2 个字段为状态码0 成功 0 失败失败时后面无其他数据
- 其余字段为用户在程序中自定义输出的数据即识别出的字符
分隔符按协议同时兼容空格和逗号
"""
parts = [p for p in re.split(r"[,\s]+", line.strip()) if p != ""]
if not parts or parts[0].upper() != "VTFP":
raise ParseError(f"SVS返回格式错误-{line}")
if len(parts) < 2:
raise ParseError(f"SVS返回缺少状态码-{line}")
status = parts[1]
if status != "0":
raise ParseError(f"SVS触发失败,状态码-{status}")
fields = parts[2:] # 用户自定义数据(识别字符),可能含可选的流程单元号回显
char = " ".join(fields)
return char, fields
def get_svs_char(host, port, flow_unit=0, timeout=10):
"""连接 SVS维视智造 VisionBank用 VTFP 触发拍照并取回识别字符。
返回 ``{"char": 识别字符, "fields": [...], "raw": 原始应答行}``
不同流程的输出配置可能不一样有的回 ``VTFP 0 <数据>`` 完整应答有的只回
用户自定义的识别内容( ``1``)这里读到第一行完整数据就返回 ``VTFP``
前缀的按协议解析否则整行当作识别内容
"""
cmd = f"VTFP {flow_unit}\r".encode("ascii") # 该设备只认单 CR 结尾, 带 \n 会无响应
try:
with socket.create_connection((host, int(port)), timeout=timeout) as sc:
buf = bytearray()
sc.sendall(cmd)
# 第一次用完整超时等设备响应; 设备返回的识别内容(如 b'1')末尾可能没有
# 换行符, 因此拿到数据后只用很短的超时把剩余字节收干净(兼容多位数分包),
# 不再死等换行, 否则会卡到超时
chunk = sc.recv(1024)
if chunk:
buf.extend(chunk)
sc.settimeout(0.3)
while b"\n" not in buf and b"\r" not in buf:
try:
more = sc.recv(1024)
except socket.timeout:
break
if not more:
break
buf.extend(more)
text = buf.decode("utf-8", errors="replace")
for ln in re.split(r"[\r\n]+", text):
ln = ln.strip()
if not ln:
continue
if ln.upper().startswith("VTFP"):
char, fields = _parse_vtfp_line(ln)
return {"char": char, "fields": fields, "raw": ln}
# 无 VTFP 前缀,设备直接回识别内容
return {"char": ln, "fields": ln.split(), "raw": ln}
except ParseError:
raise
except (socket.timeout, ConnectionError, OSError) as e:
raise ParseError(f"SVS连接失败-{str(e)}")
raise ParseError("SVS未返回有效数据")
def get_tyy_data(*args): def get_tyy_data(*args):
host, port = args[0], int(args[1]) host, port = args[0], int(args[1])
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}") r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")

View File

@ -1,72 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-26 07:52
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('em', '0023_repair'),
]
operations = [
migrations.AlterField(
model_name='ecate',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='ecate',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='echeckrecord',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='echeckrecord',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='einspect',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='einspect',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='equipment',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='equipment',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='equipment',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='repair',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='repair',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
]

View File

@ -14,7 +14,6 @@ from rest_framework.decorators import action
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from django.db import transaction from django.db import transaction
from apps.em.services import daoru_equipment from apps.em.services import daoru_equipment
from apps.em import cd
from rest_framework.response import Response from rest_framework.response import Response
from django.conf import settings from django.conf import settings
from django.db.models import Count, Case, When, IntegerField from django.db.models import Count, Case, When, IntegerField
@ -128,26 +127,6 @@ class EquipmentViewSet(CustomModelViewSet):
return Response(json_result) return Response(json_result)
@swagger_auto_schema(
manual_parameters=[
openapi.Parameter(name="host", in_=openapi.IN_QUERY, description="SVS IP", type=openapi.TYPE_STRING, required=True),
openapi.Parameter(name="port", in_=openapi.IN_QUERY, description="SVS 端口", type=openapi.TYPE_STRING, required=True),
openapi.Parameter(name="flow_unit", in_=openapi.IN_QUERY, description="识别码(流程单元号), 多台 SVS 共用 IP 时用于区分设备", type=openapi.TYPE_STRING, required=True),
]
)
@action(methods=["get"], detail=False, perms_map={"get": "*"})
def get_svs_char(self, request, *args, **kwargs):
"""获取 SVS 识别字符
触发 SVS 拍照并取回识别字符, flow_unit 为识别码用于区分共用 IP 的不同设备
"""
host = request.query_params.get("host", None)
port = request.query_params.get("port", None)
if not host or not port:
raise ParseError("请传入host和port参数")
flow_unit = request.query_params.get("flow_unit", 0)
return Response(cd.get_svs_char(host, port, flow_unit))
class EcheckRecordViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet): class EcheckRecordViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
""" """

View File

@ -1,27 +0,0 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0001_initial'),
('hrm', '0035_rename_band_card_employee_bank_card'),
]
operations = [
migrations.AlterField(
model_name='empcontract',
name='employee',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL,
to='hrm.employee', verbose_name='人员信息'),
),
migrations.AlterField(
model_name='empcontract',
name='ticket',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE,
related_name='contract_ticket', to='wf.ticket', verbose_name='关联工单'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2026-05-13 03:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0036_empcontract_batch'),
]
operations = [
migrations.AlterField(
model_name='empcontract',
name='counts',
field=models.PositiveSmallIntegerField(default=0, verbose_name='合同变更次数'),
),
]

View File

@ -354,8 +354,8 @@ class EmpContract(CommonAModel):
""" """
TN:劳动合同 TN:劳动合同
""" """
employee = models.ForeignKey(Employee, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True) employee = models.OneToOneField(Employee, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单', ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='contract_ticket', null=True, blank=True) on_delete=models.CASCADE, related_name='contract_ticket', null=True, blank=True)
counts = models.PositiveSmallIntegerField('合同变更次数', default=0) counts = models.PositiveSmallIntegerField('合同变更次数', default=0)
plan_renewal = models.DateField('应续签', null=True, blank=True) plan_renewal = models.DateField('应续签', null=True, blank=True)

View File

@ -425,22 +425,6 @@ class EmpContractSerializer(CustomModelSerializer):
gender = serializers.CharField(source="employee.gender", read_only=True) gender = serializers.CharField(source="employee.gender", read_only=True)
join_date = serializers.CharField(source="employee.start_date", read_only=True) join_date = serializers.CharField(source="employee.start_date", read_only=True)
end_contract = serializers.CharField(source="employee.contract_end_date", read_only=True) end_contract = serializers.CharField(source="employee.contract_end_date", read_only=True)
batch_employees = serializers.SerializerMethodField()
def get_batch_employees(self, obj):
if obj.ticket_id:
siblings = EmpContract.objects.filter(ticket_id=obj.ticket_id).select_related(
'employee', 'employee__post', 'employee__belong_dept')
if siblings.count() > 1:
return [{
'employee_name': s.employee.name if s.employee else '',
'post_name': s.employee.post.name if s.employee and s.employee.post else '',
'dept_name': s.employee.belong_dept.name if s.employee and s.employee.belong_dept else '',
'join_date': str(s.employee.start_date or '') if s.employee else '',
'end_contract': str(s.employee.contract_end_date or '') if s.employee else '',
} for s in siblings]
return []
class Meta: class Meta:
model = EmpContract model = EmpContract
fields = '__all__' fields = '__all__'

View File

@ -33,9 +33,8 @@ from datetime import datetime
from apps.utils.export import export_excel from apps.utils.export import export_excel
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, RetrieveModelMixin from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, RetrieveModelMixin
from apps.wf.models import Ticket, Workflow, State from apps.wf.models import Ticket
from apps.wf.mixins import TicketMixin from apps.wf.mixins import TicketMixin
from apps.wf.services import WfService
from apps.system.models import Post, Dept from apps.system.models import Post, Dept
from django.db.models import DateField from django.db.models import DateField
from datetime import datetime, date from datetime import datetime, date
@ -290,70 +289,23 @@ class EmployeeViewSet(CustomModelViewSet):
"""导出excel """导出excel
导出excel 导出excel
""" """
# 导出列定义: (表头, 取值函数)。覆盖人员的全部业务字段。 field_data = ['人员类型', '人员', '手机号', '身份证号', '所属部门', '在职状态', '定位卡号']
def _cell(v): queryset = self.filter_queryset(self.get_queryset())
return '' if v is None else v if queryset.count() > 1000:
columns = [ raise ParseError('数据量超过1000,请筛选后导出')
('人员类型', lambda i: epTypeOptions.get(i['type'], i['type'])),
('姓名', lambda i: i['name']),
('性别', lambda i: i['gender']),
('人员编号', lambda i: i['number']),
('手机号', lambda i: i['phone']),
('个人邮箱', lambda i: i['email']),
('身份证号', lambda i: i['id_number']),
('所属部门', lambda i: i.get('belong_dept_name', '')),
('所属岗位', lambda i: i.get('post_name', '')),
('车间', lambda i: i['workshop']),
('职务', lambda i: i['position']),
('职务聘任日期', lambda i: i['office_date']),
('在职状态', lambda i: epStateOptions.get(i['job_state'], i['job_state'])),
('入职日期', lambda i: i['start_date']),
('转正日期', lambda i: i['regular_date']),
('合同到期日', lambda i: i['contract_end_date']),
('离职日期', lambda i: i['end_date']),
('银行卡号', lambda i: i['bank_card']),
('出生年月日', lambda i: i['birthday']),
('年龄', lambda i: i['age']),
('工龄', lambda i: i['work_years']),
('学历', lambda i: i['qualification']),
('政治面貌', lambda i: i['partisan']),
('入党时间', lambda i: i['join_partisan_date']),
('民族', lambda i: i['nation']),
('婚姻状况', lambda i: i['marriage']),
('户口性质', lambda i: i['hukou_type']),
('籍贯', lambda i: i['birthplace']),
('户籍地址', lambda i: i['hukou_address']),
('现住地址', lambda i: i['address']),
('枣庄市职称', lambda i: i['zhuanzhi']),
('获得枣庄市职称日期', lambda i: i['zhuanzhi_date']),
('总院/集团职称', lambda i: i['zyjt_zhuanzhi']),
('获得职称日期', lambda i: i['zyjt_zhuanzhi_date']),
('职业技能等级', lambda i: i['skill_rank']),
('获得技能等级证书日期', lambda i: i['skill_rank_date']),
('全日制最高学历', lambda i: i['full_edu']),
('全日制最高学历学校名称', lambda i: i['full_edu_school']),
('全日制最高学历专业', lambda i: i['full_edu_major']),
('全日制最高学历毕业时间', lambda i: i['full_edu_time']),
('非全日制最高学历', lambda i: i['part_edu']),
('非全日制最高学历学校名称', lambda i: i['part_edu_school']),
('非全日制最高学历专业', lambda i: i['part_edu_major']),
('非全日制最高学历毕业时间', lambda i: i['part_edu_time']),
('紧急联系人姓名', lambda i: i['emergency_contact']),
('紧急联系人电话', lambda i: i['emergency_phone']),
('所获荣誉', lambda i: i['honor']),
('首次缴纳社保日期', lambda i: i['first_social_security_date']),
('是否为退役军人', lambda i: '' if i['is_veteran'] else ''),
('定位卡号', lambda i: i['blt_'].get('code', '') if i.get('blt_') else ''),
('创建时间', lambda i: (i.get('create_time') or '')[:19].replace('T', ' ')),
]
field_data = [c[0] for c in columns]
queryset = self.filter_queryset(self.get_queryset()).select_related(
'belong_dept', 'post', 'blt')
odata = EmployeeSerializer(queryset, many=True).data odata = EmployeeSerializer(queryset, many=True).data
# 处理数据 # 处理数据
data = [] data = []
for i in odata: for i in odata:
data.append([_cell(getter(i)) for _, getter in columns]) data.append(
[epTypeOptions[i['type']],
i['name'],
i['phone'],
i['id_number'],
i.get('belong_dept_name', ''),
epStateOptions[i['job_state']],
i['blt_'].get('code', '') if 'blt_' in i and i['blt_'] else '']
)
return Response({'path': export_excel(field_data, data, '人员信息')}) return Response({'path': export_excel(field_data, data, '人员信息')})
@action(methods=['post'], detail=False, perms_map={'post': 'employee.import_excel'}, @action(methods=['post'], detail=False, perms_map={'post': 'employee.import_excel'},
@ -408,8 +360,8 @@ class EmployeeViewSet(CustomModelViewSet):
id_number = data.get("id_number") id_number = data.get("id_number")
name = data.get("name") name = data.get("name")
if not name: if not id_number or not name:
raise ParseError(f'{row_num}行,姓名为空') raise ParseError(f'{row_num}行,身份证号或姓名为空')
myLogger.info(f"处理第{row_num}行:{name} - {id_number}") myLogger.info(f"处理第{row_num}行:{name} - {id_number}")
# 处理人员类型 # 处理人员类型
@ -419,9 +371,9 @@ class EmployeeViewSet(CustomModelViewSet):
data['type'] = TYPE_MAPPING[excel_type] data['type'] = TYPE_MAPPING[excel_type]
else: else:
raise ParseError(f'{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}') raise ParseError(f'{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
# 处理部门外键:填了就校验是否存在并赋值;为空时不动(新增场景的必填在下方创建处校验) # 处理部门外键
dept_name = data.pop('belong_dept', None) if 'belong_dept' in data and data['belong_dept']:
if dept_name: dept_name = data.pop('belong_dept')
if dept_name not in dept_map: if dept_name not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_name}"不存在') raise ParseError(f'{row_num}行,部门"{dept_name}"不存在')
data['belong_dept_id'] = dept_map[dept_name] data['belong_dept_id'] = dept_map[dept_name]
@ -435,44 +387,34 @@ class EmployeeViewSet(CustomModelViewSet):
if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']): if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']):
raise ParseError(f'{row_num}行,身份证号格式不正确') raise ParseError(f'{row_num}行,身份证号格式不正确')
# 按姓名匹配并更新:姓名唯一则直接更新(身份证号可为空); # 查找或创建/补全
# 存在重名则必须用身份证号定位具体记录,否则报错。
try: try:
with transaction.atomic(): with transaction.atomic():
name_matches = list(Employee.objects.filter(name=name)) # 优先按身份证号匹配,匹配不到再按姓名匹配
if len(name_matches) <= 1:
existing = name_matches[0] if name_matches else None
# 同名但身份证号不一致,视为不同人员,新建
if existing and id_number and existing.id_number \
and existing.id_number != id_number:
existing = None existing = None
else: if id_number:
# 存在重名,必须用身份证号区分 existing = Employee.objects.filter(id_number=id_number).first()
if not id_number: if not existing and name:
raise ParseError(f'{row_num}行,姓名"{name}"存在重名,必须填写身份证号') existing = Employee.objects.filter(name=name, id_number__isnull=True).first() or \
existing = next( Employee.objects.filter(name=name, id_number='').first()
(e for e in name_matches if e.id_number == id_number), None)
if existing: if existing:
# 用 Excel 中填写了值的列覆盖数据库已有数据;空单元格保持原值不变 # 只用 Excel 非空值填补数据库中为空的字段
updated_fields = [] updated_fields = []
for field_name, value in data.items(): for field_name, value in data.items():
if value in [None, '']: if value in [None, '']:
continue continue
if getattr(existing, field_name, None) != value: current_value = getattr(existing, field_name, None)
if current_value in [None, '']:
setattr(existing, field_name, value) setattr(existing, field_name, value)
updated_fields.append(field_name) updated_fields.append(field_name)
if updated_fields: if updated_fields:
existing.save(update_fields=updated_fields + ['update_time']) existing.save(update_fields=updated_fields + ['update_time'])
myLogger.info(f"✅ 第{row_num}更新成功:{name},更新字段:{updated_fields}") myLogger.info(f"✅ 第{row_num}补全成功:{name},更新字段:{updated_fields}")
else: else:
myLogger.info(f"⏭️ 第{row_num}行无变化{name}") myLogger.info(f"⏭️ 第{row_num}行无需补全{name}")
created = False created = False
else: else:
# 新增人员时所属部门必填 Employee.objects.create(id_number=id_number, name=name, **data)
if 'belong_dept_id' not in data:
raise ParseError(f'{row_num}行,新增人员时所属部门不能为空')
Employee.objects.create(**data)
created = True created = True
except Exception as e: except Exception as e:
raise raise
@ -883,49 +825,3 @@ class EmpContractViewSet(TicketMixin, EuModelViewSet):
def gen_other_ticket_data(self, instance): def gen_other_ticket_data(self, instance):
return {"name": instance.employee.name if instance.employee.name else None} return {"name": instance.employee.name if instance.employee.name else None}
def perform_create(self, serializer):
result = serializer.save()
is_batch = isinstance(result, list)
instances = result if is_batch else [result]
handler = self.request.user
workflow_key = self.get_workflow_key(instances[0])
if not workflow_key:
raise ParseError('工作流异常:必须赋值workflow_key')
try:
wf = Workflow.objects.get(key=workflow_key)
except Exception as e:
raise ParseError(f'工作流{workflow_key}异常:{e}')
# 批量时创建一个工单,所有记录共享
if is_batch:
names = [ins.employee.name for ins in instances if ins.employee]
ticket_data = {
"t_model": "EmpContract",
"t_id": str(instances[0].id),
"batch": True,
"count": len(instances),
"name": f"批量合同变更({len(instances)}人)",
"employees": [
{"name": ins.employee.name, "id": str(ins.id)} for ins in instances if ins.employee
],
}
else:
ticket_data = self.gen_ticket_data(instances[0])
ticket = WfService.handle_ticket(ticket=None, transition=None, workflow=wf, new_ticket_data=ticket_data,
handler=handler, oinfo={})
for ins in instances:
ins.ticket = ticket
ins.save(update_fields=['ticket'])
if self.ticket_auto_submit_on_create:
source_state = WfService.get_workflow_start_state(wf)
transitions = WfService.get_state_transitions(source_state)
if transitions.count() == 1:
transition = transitions.first()
WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
handler=handler, oinfo={})
else:
raise ParseError(f'工作流{workflow_key}异常:有多个或无后续状态;不可处理')

View File

@ -1,7 +1,7 @@
import re import re
import psycopg2 import psycopg2
import threading import threading
from django.db import transaction, connections from django.db import transaction
from .models import Message from .models import Message
# 数据库连接 # 数据库连接
@ -190,10 +190,6 @@ def strip_sql_markdown(content: str) -> str:
# ORM 写入包装函数 # ORM 写入包装函数
def save_message_thread_safe(**kwargs): def save_message_thread_safe(**kwargs):
def _save(): def _save():
try:
with transaction.atomic(): with transaction.atomic():
Message.objects.create(**kwargs) Message.objects.create(**kwargs)
finally:
# 子线程退出前关闭本线程的 Django DB 连接,避免 PG 连接泄漏
connections.close_all()
threading.Thread(target=_save).start() threading.Thread(target=_save).start()

View File

@ -264,21 +264,6 @@ class MIOItemListSimpleSerializer(CustomModelSerializer):
model = MIOItem model = MIOItem
fields = ["id", "mio", "material", "warehouse", "material_name", "warehouse_name", "batch", "count", "test_date", "count_notok"] fields = ["id", "mio", "material", "warehouse", "material_name", "warehouse_name", "batch", "count", "test_date", "count_notok"]
class _MioDetailItemMaterialSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ["id", "name", "model"]
class MIOItemForMioDetailSerializer(CustomModelSerializer):
"""MIO 详情下挂的 items 用,仅含前端实际消费的字段"""
material_ = _MioDetailItemMaterialSerializer(source='material', read_only=True)
class Meta:
model = MIOItem
fields = ["id", "mio", "material", "material_", "batch", "count", "pack_index"]
class MIOItemSerializer(CustomModelSerializer): class MIOItemSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(source='warehouse.name', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True) material_ = MaterialSerializer(source='material', read_only=True)
@ -288,33 +273,11 @@ class MIOItemSerializer(CustomModelSerializer):
inout_date = serializers.DateField(source='mio.inout_date', read_only=True) inout_date = serializers.DateField(source='mio.inout_date', read_only=True)
test_user_name = serializers.CharField(source='test_user.name', read_only=True) test_user_name = serializers.CharField(source='test_user.name', read_only=True)
mioitemw = MIOItemwSerializer(many=True, required=False) mioitemw = MIOItemwSerializer(many=True, required=False)
defect = serializers.SerializerMethodField(label="缺陷")
defect_name = serializers.SerializerMethodField(label="缺陷名称")
okcate = serializers.SerializerMethodField(label="缺陷分类")
class Meta: class Meta:
model = MIOItem model = MIOItem
fields = '__all__' fields = '__all__'
def _resolve_defect(self, obj):
if obj.wm_id and obj.wm.defect_id:
return obj.wm.defect
if obj.mb_id and obj.mb.defect_id:
return obj.mb.defect
return None
def get_defect(self, obj):
d = self._resolve_defect(obj)
return d.id if d else None
def get_defect_name(self, obj):
d = self._resolve_defect(obj)
return d.name if d else None
def get_okcate(self, obj):
d = self._resolve_defect(obj)
return d.okcate if d else None
def to_representation(self, instance): def to_representation(self, instance):
ret = super().to_representation(instance) ret = super().to_representation(instance)
ret["price"] = None ret["price"] = None
@ -461,7 +424,7 @@ class MIOOtherSerializer(CustomModelSerializer):
class MIODetailSerializer(MIOListSerializer): class MIODetailSerializer(MIOListSerializer):
items = MIOItemForMioDetailSerializer(source='item_mio', many=True, read_only=True) items = MIOItemSerializer(source='item_mio', many=True, read_only=True)
class MIOItemTestSerializer(CustomModelSerializer): class MIOItemTestSerializer(CustomModelSerializer):

View File

@ -13,6 +13,9 @@ def do_out(item: MIOItem, is_reverse: bool = False):
""" """
生产领料到车间 生产领料到车间
""" """
if item.mb and item.mb.defect is not None:
raise ParseError("生产领料不支持不合格品")
from apps.inm.models import MaterialBatch from apps.inm.models import MaterialBatch
mio:MIO = item.mio mio:MIO = item.mio
belong_dept = mio.belong_dept belong_dept = mio.belong_dept
@ -72,10 +75,6 @@ def do_out(item: MIOItem, is_reverse: bool = False):
xcount:str = al[2] xcount:str = al[2]
defect:Defect = al[3] defect:Defect = al[3]
state = WMaterial.WM_OK
if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
state = WMaterial.WM_NOTOK
xbatches.append(xbatch) xbatches.append(xbatch)
if xcount <= 0: if xcount <= 0:
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
@ -86,7 +85,7 @@ def do_out(item: MIOItem, is_reverse: bool = False):
material=xmaterial, material=xmaterial,
warehouse=item.warehouse, warehouse=item.warehouse,
batch=xbatch, batch=xbatch,
state=state, state=WMaterial.WM_OK,
defect=defect defect=defect
) )
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e: except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
@ -102,7 +101,7 @@ def do_out(item: MIOItem, is_reverse: bool = False):
wm, new_create = WMaterial.objects.get_or_create( wm, new_create = WMaterial.objects.get_or_create(
batch=xbatch, material=xmaterial, batch=xbatch, material=xmaterial,
belong_dept=belong_dept, mgroup=mgroup, belong_dept=belong_dept, mgroup=mgroup,
state=state, defect=defect) state=WMaterial.WM_OK, defect=defect)
if new_create: if new_create:
wm.create_by = do_user wm.create_by = do_user
wm.batch_ofrom = mb.batch if mb else None wm.batch_ofrom = mb.batch if mb else None
@ -177,10 +176,6 @@ def do_in(item: MIOItem):
if xcount <= 0: if xcount <= 0:
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
state = WMaterial.WM_OK
if defect and defect.okcate in [Defect.DEFECT_NOTOK]:
state = WMaterial.WM_NOTOK
xbatchs.append(xbatch) xbatchs.append(xbatch)
if xmaterial.into_wm: if xmaterial.into_wm:
if xwm: if xwm:
@ -192,7 +187,7 @@ def do_in(item: MIOItem):
belong_dept=belong_dept, belong_dept=belong_dept,
mgroup=mgroup, mgroup=mgroup,
defect=defect, defect=defect,
state=state) state=WMaterial.WM_OK)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
wm = wm_qs.first() wm = wm_qs.first()
@ -224,7 +219,7 @@ def do_in(item: MIOItem):
material=xmaterial, material=xmaterial,
warehouse=item.warehouse, warehouse=item.warehouse,
batch=xbatch, batch=xbatch,
state=state, state=WMaterial.WM_OK,
defect=defect, defect=defect,
defaults={ defaults={
"count": 0, "count": 0,
@ -463,8 +458,7 @@ class InmService:
@classmethod @classmethod
def revert_and_del(cls, mioitem: MIOItem): def revert_and_del(cls, mioitem: MIOItem):
# 锁定 MIO 行,防止多人同时撤销不同明细时并发漏删 MIO mio = mioitem.mio
mio = MIO.objects.select_for_update().get(id=mioitem.mio_id)
if mio.submit_time is None: if mio.submit_time is None:
raise ParseError("未提交的出入库明细不允许撤销") raise ParseError("未提交的出入库明细不允许撤销")
if mioitem.test_date is not None: if mioitem.test_date is not None:
@ -485,7 +479,4 @@ class InmService:
mioitem.delete() mioitem.delete()
else: else:
raise ParseError("不支持该出入库单明细撤销") raise ParseError("不支持该出入库单明细撤销")
# 若该出入库记录已无明细,自动删除
if not MIOItem.objects.filter(mio=mio).exists():
mio.delete()

View File

@ -7,7 +7,7 @@ from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
from django.utils import timezone from django.utils import timezone
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Sum, Prefetch from django.db.models import Sum
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack
from apps.inm.serializers import ( from apps.inm.serializers import (
@ -153,15 +153,6 @@ class MIOViewSet(CustomModelViewSet):
'item_mio__a_mioitem__batch'] 'item_mio__a_mioitem__batch']
data_filter = True data_filter = True
def get_queryset(self):
queryset = super().get_queryset()
if self.action == 'retrieve':
queryset = queryset.prefetch_related(Prefetch(
'item_mio',
queryset=MIOItem.objects.select_related('material'),
))
return queryset
@classmethod @classmethod
def lock_and_check_can_update(cls, mio:MIO): def lock_and_check_can_update(cls, mio:MIO):
if not connection.in_atomic_block: if not connection.in_atomic_block:
@ -355,7 +346,7 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
serializer_class = MIOItemSerializer serializer_class = MIOItemSerializer
retrieve_serializer_class = MioItemDetailSerializer retrieve_serializer_class = MioItemDetailSerializer
create_serializer_class = MIOItemCreateSerializer create_serializer_class = MIOItemCreateSerializer
select_related_fields = ['warehouse', 'mio', 'material', 'test_user', 'wm__defect', 'mb__defect'] select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
filterset_fields = { filterset_fields = {
"warehouse": ["exact"], "warehouse": ["exact"],
"mio": ["exact"], "mio": ["exact"],
@ -394,14 +385,6 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
def perform_create(self, serializer): def perform_create(self, serializer):
if isinstance(serializer.validated_data, list):
mio_cache = {}
for item in serializer.validated_data:
mio = item['mio']
if mio.id not in mio_cache:
mio_cache[mio.id] = MIOViewSet.lock_and_check_can_update(mio)
item['mio'] = mio_cache[mio.id]
else:
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio']) serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
return super().perform_create(serializer) return super().perform_create(serializer)

View File

@ -1,137 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-15 03:25
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('mtm', '0063_auto_20251106_2339'),
]
operations = [
migrations.AddField(
model_name='process',
name='clear_defect',
field=models.BooleanField(default=False, verbose_name='合批清除缺陷'),
),
migrations.AlterField(
model_name='goal',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='goal',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='material',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='material',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='mgroup',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='mgroup',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='mgroup',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='process',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='process',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='process',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='route',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='route',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='routepack',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='routepack',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='shift',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='shift',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='shift',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='srule',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='srule',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='srule',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AlterField(
model_name='team',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AlterField(
model_name='team',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AlterField(
model_name='team',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
]

View File

@ -37,7 +37,7 @@ class Process(CommonBModel):
parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True) parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True)
number_to_batch = models.BooleanField('个号转批号', default=False) number_to_batch = models.BooleanField('个号转批号', default=False)
wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True) wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True)
clear_defect = models.BooleanField('合批清除缺陷', default=False)
class Meta: class Meta:
verbose_name = '工序' verbose_name = '工序'
ordering = ['sort', 'create_time'] ordering = ['sort', 'create_time']

View File

@ -1,7 +1,6 @@
import threading import threading
from apps.utils.decorators import auto_log from apps.utils.decorators import auto_log
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from django.db import connections
# 创建全局线程池 # 创建全局线程池
global_executor = ThreadPoolExecutor(max_workers=20) global_executor = ThreadPoolExecutor(max_workers=20)
@ -9,12 +8,7 @@ class MyThread(threading.Thread):
@auto_log('MyThread', raise_exception=True, send_mail=True) @auto_log('MyThread', raise_exception=True, send_mail=True)
def run(self) -> None: def run(self) -> None:
# 子线程退出 / 池内 worker 跑完一次任务后必须关闭本线程的 Django DB 连接,
# 否则 psycopg2 连接会一直驻留在线程的 thread-local导致 PG "too many clients"
try:
return super().run() return super().run()
finally:
connections.close_all()
def start_p(self): def start_p(self):
""" """

View File

@ -1,16 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpm', '0129_mlogbdefect_is_inherited'),
]
operations = [
migrations.AddField(
model_name='handover',
name='clear_defect',
field=models.BooleanField(default=False, verbose_name='合批清除缺陷'),
),
]

View File

@ -1,16 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpm', '0130_handover_clear_defect'),
]
operations = [
migrations.AddField(
model_name='mlog',
name='clear_defect',
field=models.BooleanField(default=False, verbose_name='合格B类缺陷不拆批'),
),
]

View File

@ -1,25 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-26 07:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('em', '0024_alter_ecate_create_by_alter_ecate_update_by_and_more'),
('wpm', '0131_mlog_clear_defect'),
]
operations = [
migrations.AddField(
model_name='mlogbw',
name='tooling',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mlogbw_tooling', to='em.equipment', verbose_name='工装'),
),
migrations.AlterField(
model_name='mlogbw',
name='equip',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mlogbw_equip', to='em.equipment', verbose_name='设备'),
),
]

View File

@ -242,7 +242,6 @@ class Mlog(CommonADModel):
reminder_interval_list = models.JSONField('提醒间隔', default=list, blank=True) reminder_interval_list = models.JSONField('提醒间隔', default=list, blank=True)
stored_mgroup = models.BooleanField('入库到工段', default=False) stored_mgroup = models.BooleanField('入库到工段', default=False)
stored_notok = models.BooleanField('不合格品是否已入库', default=False) stored_notok = models.BooleanField('不合格品是否已入库', default=False)
clear_defect = models.BooleanField('合格B类缺陷不拆批', default=False)
route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True) route = models.ForeignKey(Route, verbose_name='生产路线', on_delete=models.SET_NULL, null=True, blank=True)
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')
@ -511,7 +510,7 @@ class Mlogb(BaseModel):
@property @property
def mlogbdefect(self): def mlogbdefect(self):
return self.mlogbdefect_set.all() return MlogbDefect.objects.filter(mlogb=self)
def cal_count_pn_jgqbl(self, cal_mlog=False): def cal_count_pn_jgqbl(self, cal_mlog=False):
mqs = MlogbDefect.get_defect_qs_from_mlogb(self, ftype="in") mqs = MlogbDefect.get_defect_qs_from_mlogb(self, ftype="in")
@ -539,8 +538,6 @@ class Mlogb(BaseModel):
def get_default_inherited_defect(self): def get_default_inherited_defect(self):
if self.material_out is None or self.material_out.tracking != Material.MA_TRACKING_BATCH: if self.material_out is None or self.material_out.tracking != Material.MA_TRACKING_BATCH:
return None return None
if self.mlog_id and self.mlog and self.mlog.is_fix:
return None
if self.mlogb_from_id and self.mlogb_from and self.mlogb_from.wm_in_id: if self.mlogb_from_id and self.mlogb_from and self.mlogb_from.wm_in_id:
return self.mlogb_from.wm_in.defect return self.mlogb_from.wm_in.defect
if self.wm_in_id and self.wm_in: if self.wm_in_id and self.wm_in:
@ -554,18 +551,13 @@ class Mlogb(BaseModel):
def sync_inherited_defect(self, cal_count=True): def sync_inherited_defect(self, cal_count=True):
inherited_qs = MlogbDefect.objects.filter(mlogb=self, is_inherited=True) inherited_qs = MlogbDefect.objects.filter(mlogb=self, is_inherited=True)
had_inherited = inherited_qs.exists()
if MlogbDefect.objects.filter(mlogb=self, is_inherited=False).exists() or self.has_legacy_defect_count(): if MlogbDefect.objects.filter(mlogb=self, is_inherited=False).exists() or self.has_legacy_defect_count():
inherited_qs.delete() inherited_qs.delete()
if had_inherited and cal_count:
self.cal_count_notok(cal_mlog=False)
return return
defect = self.get_default_inherited_defect() defect = self.get_default_inherited_defect()
if defect is None: if defect is None:
inherited_qs.delete() inherited_qs.delete()
if had_inherited and cal_count:
self.cal_count_notok(cal_mlog=False)
return return
count = self.count_real count = self.count_real
@ -614,8 +606,7 @@ class Mlogbw(BaseModel):
mlogbw_from = models.ForeignKey("self", verbose_name='来源个', on_delete=models.CASCADE, null=True, blank=True, related_name="w_mlogbw_from") 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 wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL
, related_name='wpr_mlogbw', null=True, blank=True) , related_name='wpr_mlogbw', null=True, blank=True)
equip = models.ForeignKey(Equipment, verbose_name='设备', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlogbw_equip') equip = models.ForeignKey(Equipment, verbose_name='设备', on_delete=models.SET_NULL, null=True, blank=True)
tooling = models.ForeignKey(Equipment, verbose_name='工装', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlogbw_tooling')
work_start_time = models.DateTimeField('开始加工时间', null=True, blank=True) work_start_time = models.DateTimeField('开始加工时间', null=True, blank=True)
work_end_time = models.DateTimeField('结束加工时间', null=True, blank=True) work_end_time = models.DateTimeField('结束加工时间', null=True, blank=True)
ftest = models.OneToOneField("qm.ftest", verbose_name='关联检验', ftest = models.OneToOneField("qm.ftest", verbose_name='关联检验',
@ -679,7 +670,6 @@ class Handover(CommonADModel):
H_DIV = 20 H_DIV = 20
new_batch = models.TextField('新批次号', null=True, blank=True, db_index=True) new_batch = models.TextField('新批次号', null=True, blank=True, db_index=True)
new_wm = models.ForeignKey(WMaterial, on_delete=models.SET_NULL, null=True, blank=True) new_wm = models.ForeignKey(WMaterial, on_delete=models.SET_NULL, null=True, blank=True)
clear_defect = models.BooleanField('合批清除缺陷', default=False)
mtype = models.PositiveSmallIntegerField("合并类型", default=H_NORMAL, choices= mtype = models.PositiveSmallIntegerField("合并类型", default=H_NORMAL, choices=
[(H_NORMAL, '正常'), (H_DIV, '分批'), (H_MERGE, '合批')]) [(H_NORMAL, '正常'), (H_DIV, '分批'), (H_MERGE, '合批')])
type = models.PositiveSmallIntegerField('交接类型', choices=[ type = models.PositiveSmallIntegerField('交接类型', choices=[

View File

@ -1,7 +1,7 @@
from apps.wpm.models import BatchSt from apps.wpm.models import BatchSt
import logging import logging
from apps.qm.models import Defect, FtestWork, FtestworkDefect from apps.qm.models import Defect, FtestWork, FtestworkDefect
from apps.wpm.models import Mlogb, MlogbDefect, Mlog from apps.wpm.models import Mlogb, MlogbDefect
from apps.mtm.models import Mgroup from apps.mtm.models import Mgroup
import decimal import decimal
from django.db.models import Sum from django.db.models import Sum
@ -87,44 +87,35 @@ def main(batch: str, mgroup_obj):
data[f"{mgroup_name}_班次"] = ";".join([item for item in data[f"{mgroup_name}_班次"]]) data[f"{mgroup_name}_班次"] = ";".join([item for item in data[f"{mgroup_name}_班次"]])
# 按 mlog__submit_time, id 排序,每条 Mlogb 记录独立为一次返修 mlogb2_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
# (同一 Mlog 下有多条同批次 Mlogb 时也能正确拆分为多次返修)
_all_fix_qs = Mlogb.objects.filter(
mlog__submit_time__isnull=False,
material_out__isnull=False, material_out__isnull=False,
mlog__mgroup__name="外观检验", mlog__mgroup__name="外观检验",
mlog__is_fix=True, mlog__is_fix=True, batch=batch, need_inout=True)
batch=batch, if mlogb2_qs.exists():
need_inout=True, data["外观检验_返修_日期"] = []
).order_by("mlog__submit_time", "id") data["外观检验_返修_操作人"] = []
_fix_prefixes = [] data["外观检验_返修_count_real"] = 0
for fix_idx, fix_mlogb in enumerate(_all_fix_qs): data["外观检验_返修_count_ok"] = 0
suffix = "" if fix_idx == 0 else str(fix_idx + 1) data["外观检验_返修_count_ok_full"] = 0
prefix = f"外观检验_返修{suffix}_" for item in mlogb2_qs:
_fix_prefixes.append(prefix) if item.mlog.handle_user:
mlog = fix_mlogb.mlog data["外观检验_返修_操作人"].append(item.mlog.handle_user)
handle_date = mlog.handle_date if item.mlog.handle_date:
data[f"{prefix}count_real"] = fix_mlogb.count_real data["外观检验_返修_日期"].append(item.mlog.handle_date)
data[f"{prefix}count_ok"] = fix_mlogb.count_ok data["外观检验_返修_count_real"] += item.count_real
data[f"{prefix}count_ok_full"] = fix_mlogb.count_ok_full or 0 data["外观检验_返修_count_ok"] += item.count_ok
data[f"{prefix}count_notok"] = fix_mlogb.count_notok or 0 data["外观检验_返修_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
try:
data[f"{prefix}合格率"] = round((fix_mlogb.count_ok / fix_mlogb.count_real) * 100, 2) data["外观检验_返修_日期"] = list(set(data["外观检验_返修_日期"]))
except (decimal.InvalidOperation, ZeroDivisionError): data["外观检验_返修_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_返修_日期"]])
data[f"{prefix}合格率"] = 0 data["外观检验_返修_操作人"] = list(set(data["外观检验_返修_操作人"]))
try: data["外观检验_返修_操作人"] = ";".join([item.name for item in data["外观检验_返修_操作人"]])
data[f"{prefix}完全合格率"] = round(((fix_mlogb.count_ok_full or 0) / fix_mlogb.count_real) * 100, 2)
except (decimal.InvalidOperation, ZeroDivisionError): mlogbd2_qs = MlogbDefect.objects.filter(mlogb__in=mlogb2_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
data[f"{prefix}完全合格率"] = 0
data[f"{prefix}日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else "" for item in mlogbd2_qs:
data[f"{prefix}小日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else "" data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
data[f"{prefix}大日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else "" data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
data[f"{prefix}操作人"] = mlog.handle_user.name if mlog.handle_user else ""
data[f"{prefix}班次"] = mlog.shift.name if mlog.shift else ""
fix_defect_qs = MlogbDefect.objects.filter(mlogb=fix_mlogb, count__gt=0).values("defect__name").annotate(total=Sum("count"))
for item in fix_defect_qs:
data[f"{prefix}缺陷_{item['defect__name']}"] = item["total"]
data[f"{prefix}缺陷_{item['defect__name']}_比例"] = round((item["total"] / fix_mlogb.count_real) * 100, 2)
# 车间库存抽检 # 车间库存抽检
ft_qs = FtestWork.objects.filter(type2=FtestWork.TYPE2_SOME, wm__mgroup__name="外观检验", batch=batch, submit_time__isnull=False) ft_qs = FtestWork.objects.filter(type2=FtestWork.TYPE2_SOME, wm__mgroup__name="外观检验", batch=batch, submit_time__isnull=False)
@ -150,13 +141,13 @@ def main(batch: str, mgroup_obj):
data[f"外观检验_车间库存抽检_缺陷_{item['defect__name']}"] = item["total"] data[f"外观检验_车间库存抽检_缺陷_{item['defect__name']}"] = item["total"]
if "外观检验_count_ok" in data: if "外观检验_count_ok" in data:
data["外观检验_总合格数"] = data["外观检验_count_ok"] + sum(data.get(f"{p}count_ok", 0) for p in _fix_prefixes) data["外观检验_总合格数"] = data["外观检验_count_ok"] + data.get("外观检验_返修_count_ok", 0)
try: try:
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2) data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
except (decimal.InvalidOperation, ZeroDivisionError): except (decimal.InvalidOperation, ZeroDivisionError):
data["外观检验_总合格率"] = 0 data["外观检验_总合格率"] = 0
data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + sum(data.get(f"{p}count_ok_full", 0) for p in _fix_prefixes) data["外观检验_完全总合格数"] = data["外观检验_count_ok_full"] + data.get("外观检验_返修_count_ok_full", 0)
try: try:
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2) data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
except (decimal.InvalidOperation, ZeroDivisionError): except (decimal.InvalidOperation, ZeroDivisionError):

View File

@ -39,12 +39,10 @@ def main(wprId, mgroup:Mgroup=None):
# 子工序操作人和日期 # 子工序操作人和日期
mlogusers = MlogUser.objects.filter(mlog=mlog) mlogusers = MlogUser.objects.filter(mlog=mlog)
if mlogusers.exists(): if mlogusers.exists():
datab = mlogusers.values("handle_user__name", "process__name", "handle_date", "equipment__name") datab = mlogusers.values("handle_user__name", "process__name", "handle_date")
for ind, item in enumerate(datab): for ind, item in enumerate(datab):
data[f"{mgroup_name}_{item['process__name']}_操作人"] = item["handle_user__name"] data[f"{mgroup_name}_{item['process__name']}_操作人"] = item["handle_user__name"]
data[f"{mgroup_name}_{item['process__name']}_日期"] = item["handle_date"].strftime("%Y-%m-%d") data[f"{mgroup_name}_{item['process__name']}_日期"] = item["handle_date"].strftime("%Y-%m-%d")
if item["equipment__name"]:
data[f"{mgroup_name}_{item['process__name']}_使用设备"] = item["equipment__name"]
# 检测数据 # 检测数据
ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr, ftestitems = FtestItem.objects.filter(ftest__mlogbw_ftest__wpr=wpr,
ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup, ftest__mlogbw_ftest__mlogb__mlog__mgroup=mgroup,

View File

@ -301,9 +301,6 @@ class MlogListSerializer(CustomModelSerializer):
handle_users_ = UserSimpleSerializer( handle_users_ = UserSimpleSerializer(
source='handle_users', many=True, read_only=True) source='handle_users', many=True, read_only=True)
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', read_only=True)
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
class Meta: class Meta:
model = Mlog model = Mlog
@ -318,9 +315,6 @@ class MlogbDetailSerializer(CustomModelSerializer):
material_out_tracking = serializers.IntegerField(source="material_out.tracking", read_only=True) material_out_tracking = serializers.IntegerField(source="material_out.tracking", read_only=True)
mlogbdefect = MlogbDefectSerializer(many=True, read_only=True) mlogbdefect = MlogbDefectSerializer(many=True, read_only=True)
test_user_name = serializers.CharField(source='test_user.name', read_only=True) test_user_name = serializers.CharField(source='test_user.name', read_only=True)
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', read_only=True)
wm_in_state = serializers.IntegerField(source='wm_in.state', read_only=True)
class Meta: class Meta:
model = Mlogb model = Mlogb
@ -374,9 +368,6 @@ class MlogSerializer(CustomModelSerializer):
mlogdefect = MlogbDefectSerializer(many=True, required=False) mlogdefect = MlogbDefectSerializer(many=True, required=False)
mlogindefect = MlogbDefectSerializer(many=True, label="前道不良", required=False) mlogindefect = MlogbDefectSerializer(many=True, label="前道不良", required=False)
wm_in_defect = serializers.PrimaryKeyRelatedField(source='wm_in.defect', read_only=True)
wm_in_defect_name = serializers.CharField(source='wm_in.defect.name', 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__' fields = '__all__'
@ -680,8 +671,6 @@ class MlogSerializer(CustomModelSerializer):
handle_user = attrs.get('handle_user', None) handle_user = attrs.get('handle_user', None)
if handle_user is None and hasattr(self, "request"): if handle_user is None and hasattr(self, "request"):
handle_user = self.request.user handle_user = self.request.user
if self.instance is None and mgroup.process:
attrs['clear_defect'] = mgroup.process.clear_defect
return attrs return attrs
@ -728,14 +717,12 @@ class MlogInitSerializer(CustomModelSerializer):
attrs["qct"] = Qct.get(attrs["material_out"], "process", "out") attrs["qct"] = Qct.get(attrs["material_out"], "process", "out")
attrs["handle_date"], attrs["shift"] = mgroup.get_shift(attrs['work_start_time']) attrs["handle_date"], attrs["shift"] = mgroup.get_shift(attrs['work_start_time'])
if mgroup.process:
attrs['clear_defect'] = mgroup.process.clear_defect
return attrs return attrs
class MlogChangeSerializer(CustomModelSerializer): class MlogChangeSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Mlog model = Mlog
fields = ['id', 'work_start_time', 'work_end_time', 'handle_user', 'note', 'oinfo_json', 'test_file', 'test_user', 'test_time', 'equipment', "team", "clear_defect"] fields = ['id', 'work_start_time', 'work_end_time', 'handle_user', 'note', 'oinfo_json', 'test_file', 'test_user', 'test_time', 'equipment', "team"]
def update(self, instance, validated_data): def update(self, instance, validated_data):
work_start_time = validated_data.get('work_start_time', None) work_start_time = validated_data.get('work_start_time', None)
@ -878,17 +865,13 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
ftest = FtestProcessSerializer(required=False, allow_null=True) ftest = FtestProcessSerializer(required=False, allow_null=True)
equip_name = serializers.CharField(source='equip.name', read_only=True) equip_name = serializers.CharField(source='equip.name', read_only=True)
equip_number = serializers.CharField(source='equip.number', read_only=True) equip_number = serializers.CharField(source='equip.number', read_only=True)
tooling_name = serializers.CharField(source='tooling.name', read_only=True)
tooling_number = serializers.CharField(source='tooling.number', read_only=True)
wpr_number_out = serializers.CharField(source='wpr.number_out', read_only=True) wpr_number_out = serializers.CharField(source='wpr.number_out', read_only=True)
wpr_pre_info = serializers.JSONField(source='wpr.pre_info', read_only=True)
mlogb__batch = serializers.CharField(source='mlogb.batch', read_only=True) mlogb__batch = serializers.CharField(source='mlogb.batch', read_only=True)
class Meta: class Meta:
model = Mlogbw model = Mlogbw
fields = ["id", "number", "wpr", "note", fields = ["id", "number", "wpr", "note",
"mlogb", "ftest", "equip", "tooling", "work_start_time", "mlogb", "ftest", "equip", "work_start_time",
"work_end_time", "mlogbw_from", "equip_name", "equip_number", "work_end_time", "mlogbw_from", "equip_name", "equip_number", "wpr_number_out", "mlogb__batch"]
"tooling_name", "tooling_number", "wpr_number_out", "wpr_pre_info", "mlogb__batch"]
read_only_fields = ["mlogbw_from"] read_only_fields = ["mlogbw_from"]
def validate(self, attrs): def validate(self, attrs):
@ -1312,21 +1295,16 @@ 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:
raise ParseError('清除缺陷合批的目标批次不能带缺陷')
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
next_defect = None if clear_defect else wm.defect next_defect = wm.defect
else: else:
if next_mat != wm.material: if next_mat != wm.material:
raise ParseError(f'{ind+1}行-合并的物料不一致') raise ParseError(f'{ind+1}行-合并的物料不一致')
if next_state != wm.state: if next_state != wm.state:
raise ParseError(f'{ind+1}行-合并的物料状态不一致') raise ParseError(f'{ind+1}行-合并的物料状态不一致')
if not clear_defect and next_defect != wm.defect: if next_defect != wm.defect:
raise ParseError(f'{ind+1}行-合并的物料缺陷不一致') raise ParseError(f'{ind+1}行-合并的物料缺陷不一致')
if tracking == Material.MA_TRACKING_SINGLE: if tracking == Material.MA_TRACKING_SINGLE:
handoverbw = item.get("handoverbw", []) handoverbw = item.get("handoverbw", [])
@ -1562,7 +1540,6 @@ class MlogUserSerializer(CustomModelSerializer):
handle_user_name = serializers.CharField(source='handle_user.name', read_only=True) handle_user_name = serializers.CharField(source='handle_user.name', read_only=True)
process_name = serializers.CharField(source='process.name', read_only=True) process_name = serializers.CharField(source='process.name', read_only=True)
shift_name = serializers.CharField(source='shift.name', read_only=True) shift_name = serializers.CharField(source='shift.name', read_only=True)
equipment_name = serializers.CharField(source='equipment.name', read_only=True, default=None)
class Meta: class Meta:
model = MlogUser model = MlogUser
fields = "__all__" fields = "__all__"

View File

@ -282,12 +282,8 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False) mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
clear_defect = bool(mlog.clear_defect)
if mlogb_out_qs.exists(): if mlogb_out_qs.exists():
mlogb_out_qs = mlogb_out_qs.filter(need_inout=True) mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
if clear_defect:
m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
else:
m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()] m_outs_list = [(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) for mo in mlogb_out_qs.all()]
if stored_notok: if stored_notok:
for item in mlogb_out_qs: for item in mlogb_out_qs:
@ -297,8 +293,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# Mlogbw.cal_count_notok(item) # Mlogbw.cal_count_notok(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.count > 0: if itemx.count > 0:
if clear_defect and itemx.defect and itemx.defect.okcate == Defect.DEFECT_OK_B:
continue
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item)) m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# # 获取所有主要的不合格项/先暂时保留 # # 获取所有主要的不合格项/先暂时保留
# bw_qs = Mlogbw.objects.filter(mlogb=item) # bw_qs = Mlogbw.objects.filter(mlogb=item)
@ -441,14 +435,8 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 有多个产物的情况 # 有多个产物的情况
# 需要考虑不合格品退回的情况 # 需要考虑不合格品退回的情况
mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False) mlogb_out_qs = Mlogb.objects.filter(mlog=mlog, material_out__isnull=False)
clear_defect = bool(mlog.clear_defect)
if mlogb_out_qs.exists(): if mlogb_out_qs.exists():
mlogb_out_qs = mlogb_out_qs.filter(need_inout=True) mlogb_out_qs = mlogb_out_qs.filter(need_inout=True)
if clear_defect:
m_outs_list = [
(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok, mlog.count_real_eweight, None, mo)
for mo in mlogb_out_qs.all()]
else:
m_outs_list = [ m_outs_list = [
(mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo) (mo.material_out, mo.batch if mo.batch else mlog.batch, mo.count_ok_full if mo.count_ok_full is not None else mo.count_ok, mlog.count_real_eweight, None, mo)
for mo in mlogb_out_qs.all()] for mo in mlogb_out_qs.all()]
@ -460,8 +448,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# Mlogbw.cal_count_notok(item) # Mlogbw.cal_count_notok(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.count > 0: if itemx.count > 0:
if clear_defect and itemx.defect and itemx.defect.okcate == Defect.DEFECT_OK_B:
continue
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item)) m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE: # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# # 获取所有主要的不合格项 # # 获取所有主要的不合格项
@ -624,18 +610,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlog.submit_user = None mlog.submit_user = None
mlog.save() mlog.save()
# 兜底清理历史 is_fix 错误带入的继承缺陷
if is_fix:
legacy_mlogbouts = list(Mlogb.objects.filter(
mlog=mlog,
material_out__isnull=False,
mlogbdefect__is_inherited=True,
).distinct())
if legacy_mlogbouts:
for mlogbout in legacy_mlogbouts:
mlogbout.sync_inherited_defect(cal_count=True)
mlog.cal_mlog_count_from_mlogb()
# mtask变更状态 # mtask变更状态
update_mtaskIds = [] update_mtaskIds = []
if mlog.mtask: if mlog.mtask:
@ -700,8 +674,6 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
utask.state = Utask.UTASK_WORKING utask.state = Utask.UTASK_WORKING
if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0: if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
utask.state = Utask.UTASK_SUBMIT utask.state = Utask.UTASK_SUBMIT
elif utask.state == Utask.UTASK_SUBMIT:
utask.state = Utask.UTASK_WORKING if utask.count_ok > 0 else Utask.UTASK_ASSGINED
utask.save() utask.save()
elif fill_way in [Mlog.MLOG_23, Mlog.MLOG_12]: elif fill_way in [Mlog.MLOG_23, Mlog.MLOG_12]:
# 已经提交的日志 # 已经提交的日志
@ -733,8 +705,6 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0 utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
# if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0: # if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
# utask.state = Utask.UTASK_SUBMIT # utask.state = Utask.UTASK_SUBMIT
if utask.state == Utask.UTASK_SUBMIT and Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).exists():
utask.state = Utask.UTASK_WORKING if utask.count_ok > 0 else Utask.UTASK_ASSGINED
utask.save() utask.save()
def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]): def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime, None]):
@ -771,8 +741,6 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
mids = [] mids = []
exclude_batchst_ids = [] exclude_batchst_ids = []
check_batch_exist = False check_batch_exist = False
merge_target_defect = None
merge_clear_defect = False
if mtype == Handover.H_MERGE: if mtype == Handover.H_MERGE:
if new_batch: if new_batch:
batches = [new_batch] batches = [new_batch]
@ -782,12 +750,6 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
check_batch_exist = True check_batch_exist = True
target_b, _ = BatchSt.g_create(batch=new_batch, handover=handover, material_start=material, check_batch_exist=check_batch_exist) target_b, _ = BatchSt.g_create(batch=new_batch, handover=handover, material_start=material, check_batch_exist=check_batch_exist)
exclude_batchst_ids.append(target_b.id) exclude_batchst_ids.append(target_b.id)
merge_clear_defect = bool(handover.clear_defect)
if merge_clear_defect:
merge_target_defect = None
else:
source_wm_ids = [hb[0] for hb in handoverb_list]
merge_target_defect = WMaterial.objects.filter(id__in=source_wm_ids).first().defect
elif mtype == Handover.H_DIV: elif mtype == Handover.H_DIV:
if handover.wm is None: if handover.wm is None:
raise ParseError('拆批请选择车间库存') raise ParseError('拆批请选择车间库存')
@ -832,10 +794,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
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 wm_to.state != wm_from.state or wm_to.material != wm_from.material or wm_to.defect != wm_from.defect:
merge_clear_defect and wm_to.defect is None and wm_from.state == WMaterial.WM_OK
)
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( wm_to, _ = WMaterial.locked_get_or_create(
@ -845,7 +804,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=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,
@ -941,7 +900,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=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,

View File

@ -274,7 +274,6 @@ class MlogViewSet(CustomModelViewSet):
"material_out__process", "material_out__process",
"mgroup__process", "mgroup__process",
"submit_user", "submit_user",
"wm_in__defect",
] ]
# select_related_fields = ['create_by', 'update_by', 'mtask', 'mtaskb', 'mgroup', # select_related_fields = ['create_by', 'update_by', 'mtask', 'mtaskb', 'mgroup',
# 'handle_user', 'handle_user_2', 'equipment', 'mgroup__belong_dept', # 'handle_user', 'handle_user_2', 'equipment', 'mgroup__belong_dept',
@ -765,10 +764,7 @@ class MlogbViewSet(CustomListModelMixin, CustomGenericViewSet):
perms_map = {"get": "*"} perms_map = {"get": "*"}
queryset = Mlogb.objects.all() queryset = Mlogb.objects.all()
serializer_class = MlogbDetailSerializer serializer_class = MlogbDetailSerializer
select_related_fields = ["material_out", "material_in", "test_user", "wm_in__defect"] select_related_fields = ["material_out", "material_in", "test_user"]
prefetch_related_fields = [
Prefetch("mlogbdefect_set", queryset=MlogbDefect.objects.select_related("defect")),
]
filterset_class = MlogbFilter filterset_class = MlogbFilter
ordering = ["create_time"] ordering = ["create_time"]
@ -1112,7 +1108,7 @@ class MlogbwViewSet(CustomModelViewSet):
list_serializer_class = MlogbwListSerializer list_serializer_class = MlogbwListSerializer
retrieve_serializer_class = MlogbwListSerializer retrieve_serializer_class = MlogbwListSerializer
filterset_class = MlogbwFilter filterset_class = MlogbwFilter
select_related_fields = ["ftest", "equip", "tooling", "wpr", "mlogb"] select_related_fields = ["ftest", "equip", "wpr", "mlogb"]
prefetch_related_fields = [ prefetch_related_fields = [
Prefetch( Prefetch(
"ftest__items_ftest", "ftest__items_ftest",
@ -1273,7 +1269,7 @@ class MlogUserViewSet(BulkCreateModelMixin, CustomListModelMixin, BulkDestroyMod
perms_map = {"get": "*", "post": "mlog.update", "delete": "mlog.update"} perms_map = {"get": "*", "post": "mlog.update", "delete": "mlog.update"}
queryset = MlogUser.objects.all() queryset = MlogUser.objects.all()
serializer_class = MlogUserSerializer serializer_class = MlogUserSerializer
select_related_fields = ["handle_user", "shift", "process", "equipment"] select_related_fields = ["handle_user", "shift", "process"]
filterset_fields = ["mlog"] filterset_fields = ["mlog"]
def get_queryset(self): def get_queryset(self):

View File

@ -1,16 +0,0 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpmw', '0009_alter_wpr_number'),
]
operations = [
migrations.AddField(
model_name='wpr',
name='pre_info',
field=models.JSONField(blank=True, default=dict, verbose_name='预处理信息'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.27 on 2026-05-28 03:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpmw', '0010_wpr_pre_info'),
]
operations = [
migrations.AlterField(
model_name='wpr',
name='pre_info',
field=models.JSONField(blank=True, default=dict, null=True, verbose_name='预处理信息'),
),
]

View File

@ -30,7 +30,6 @@ class Wpr(BaseModel):
oinfo = models.JSONField(verbose_name="其他信息", default=dict, blank=True) oinfo = models.JSONField(verbose_name="其他信息", default=dict, blank=True)
wpr_from = models.ForeignKey("self", verbose_name="来源于", on_delete=models.CASCADE, null=True, blank=True) wpr_from = models.ForeignKey("self", verbose_name="来源于", on_delete=models.CASCADE, null=True, blank=True)
data = models.JSONField(verbose_name="数据", default=dict, blank=True) data = models.JSONField(verbose_name="数据", default=dict, blank=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, ftest=None, wpr_from=None, add_version=True, number_out=None):

View File

@ -3,7 +3,6 @@ from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers from rest_framework import serializers
from apps.inm.serializers import MaterialBatchSerializer from apps.inm.serializers import MaterialBatchSerializer
from apps.wpm.serializers import WMaterialSerializer from apps.wpm.serializers import WMaterialSerializer
from apps.qm.models import FtestItem
class WprDefectSerializer(CustomModelSerializer): class WprDefectSerializer(CustomModelSerializer):
defect_name = serializers.CharField(source="defect.name", read_only=True) defect_name = serializers.CharField(source="defect.name", read_only=True)
@ -56,7 +55,3 @@ class WproutListSerializer(serializers.Serializer):
class WprChangeNumberSerializer(serializers.Serializer): class WprChangeNumberSerializer(serializers.Serializer):
old_number = serializers.CharField(label="原编号") old_number = serializers.CharField(label="原编号")
new_number = serializers.CharField(label="新编号") new_number = serializers.CharField(label="新编号")
class WprPreInfoSerializer(serializers.Serializer):
tooling = serializers.CharField(label="工装ID")

View File

@ -1,10 +1,9 @@
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from apps.utils.mixins import CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, BulkUpdateModelMixin from apps.utils.mixins import CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin
from apps.wpmw.models import Wpr, WprDefect from apps.wpmw.models import Wpr, WprDefect
from apps.qm.models import FtestItem from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer, WprPreInfoSerializer
from rest_framework.response import Response from rest_framework.response import Response
from apps.mtm.models import Material from apps.mtm.models import Material
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
@ -14,13 +13,13 @@ from apps.utils.sql import query_one_dict
from django.db.models.expressions import RawSQL from django.db.models.expressions import RawSQL
class WprViewSet(BulkUpdateModelMixin, CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet): class WprViewSet(CustomListModelMixin, CustomRetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
"""动态产品 """动态产品
动态产品 动态产品
""" """
perms_map = {"get": "*", "put": "*", "patch": "*"} perms_map = {"get": "*"}
select_related_fields = ["wm", "mb", "material", "wm__material_ofrom"] select_related_fields = ["wm", "mb", "material", "wm__material_ofrom"]
prefetch_related_fields = ["defects"] prefetch_related_fields = ["defects"]
queryset = Wpr.objects.all() queryset = Wpr.objects.all()
@ -29,7 +28,7 @@ class WprViewSet(BulkUpdateModelMixin, CustomListModelMixin, CustomRetrieveModel
filterset_class = WprFilter filterset_class = WprFilter
ordering = ["number", "create_time"] ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time", "update_time"] ordering_fields = ["number", "create_time", "update_time"]
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out","wm__batch"] search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
annotate_dict = { annotate_dict = {
"number_prefix": RawSQL("regexp_replace(wpmw_wpr.number, '(\\d+)$', '')", []), "number_prefix": RawSQL("regexp_replace(wpmw_wpr.number, '(\\d+)$', '')", []),
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(wpmw_wpr.number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []), "number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(wpmw_wpr.number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
@ -149,18 +148,6 @@ class WprViewSet(BulkUpdateModelMixin, CustomListModelMixin, CustomRetrieveModel
else: else:
return Response({"number_out_last": None}) return Response({"number_out_last": None})
@action(methods=["patch"], detail=True, perms_map={"patch": "*"}, serializer_class=WprPreInfoSerializer)
def update_pre_info(self, request, *args, **kwargs):
"""更新预处理信息"""
wpr = self.get_object()
sr = WprPreInfoSerializer(data=request.data)
sr.is_valid(raise_exception=True)
raw_info = sr.validated_data
from apps.utils.tools import update_dict
wpr.pre_info = update_dict(wpr.pre_info or {}, raw_info)
wpr.save(update_fields=["pre_info"])
return Response({"pre_info": wpr.pre_info})
@action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WproutListSerializer) @action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WproutListSerializer)
@transaction.atomic @transaction.atomic
def assgin_number_out(self, request, *args, **kwargs): def assgin_number_out(self, request, *args, **kwargs):

View File

@ -1,24 +1,19 @@
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import json import json
from urllib.parse import urlparse, parse_qs from urllib.parse import urlparse, parse_qs
import socket import socket
import threading import traceback
import threading # 新增:引入线程锁
sc_all = {} # 全局 Socket 连接字典
sc_locks = {} sc_all = {
registry_lock = threading.Lock() "192.168.1.220_6000": None,
"192.168.1.235_6000": None,
"192.168.1.225_6000": None,
"192.168.1.230_6000": None,
}
sc_lock = threading.Lock() # 全局锁,保护 sc_all
def get_lock(addr):
lock = sc_locks.get(addr)
if lock is None:
with registry_lock:
lock = sc_locks.get(addr)
if lock is None:
lock = threading.Lock()
sc_locks[addr] = lock
sc_all.setdefault(addr, None)
return lock
def get_checksum(body_msg): def get_checksum(body_msg):
return sum(body_msg) & 0xFF return sum(body_msg) & 0xFF
@ -51,45 +46,6 @@ def handle_bytes(arr):
except Exception as e: except Exception as e:
return f"数据解析错误: {str(e)}" return f"数据解析错误: {str(e)}"
def drain_socket(sc):
sc.setblocking(False)
try:
while True:
data = sc.recv(65536)
if not data:
break
except BlockingIOError:
pass
finally:
sc.settimeout(5)
def recv_full_message(sc):
data = bytearray()
while len(data) < 4:
chunk = sc.recv(1024)
if not chunk:
raise ConnectionError("连接中断")
data.extend(chunk)
if data[0] != 0xEB or data[1] != 0x90:
return bytes(data)
length = int.from_bytes(bytes(data[2:4])[::-1], byteorder='little', signed=True)
if length <= 0 or length > 65536:
return bytes(data)
total_needed = 4 + length
while len(data) < total_needed:
chunk = sc.recv(1024)
if not chunk:
raise ConnectionError("连接中断")
data.extend(chunk)
return bytes(data)
class JSONRequestHandler(BaseHTTPRequestHandler): class JSONRequestHandler(BaseHTTPRequestHandler):
def ok(self, data): def ok(self, data):
self.send_response(200) self.send_response(200)
@ -104,31 +60,48 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
data = {"err_msg": err_msg} data = {"err_msg": err_msg}
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8')) self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
def log_message(self, format, *args):
return
def do_GET(self): def do_GET(self):
parsed_url = urlparse(self.path) parsed_url = urlparse(self.path)
query_params = parse_qs(parsed_url.query) query_params = parse_qs(parsed_url.query)
host = query_params.get('host', ['127.0.0.1'])[0] host = query_params.get('host', ['127.0.0.1'])[0]
port = query_params.get('port', ['6000'])[0] port = query_params.get('port', ['6000'])[0]
addr = f'{host}_{port}' addr = f'{host}_{port}'
lock = get_lock(addr)
def request_once(): if addr not in sc_all:
with lock: self.error(f'{addr} 未找到')
return
def connect_and_send():
with sc_lock: # 加锁,防止竞争
sc = sc_all[addr] sc = sc_all[addr]
try: try:
if sc is None: if sc is None:
sc = socket.socket() sc = socket.socket()
sc.settimeout(5) sc.settimeout(5)
sc.connect((host, int(port))) sc.connect((host, int(port)))
sc_all[addr] = sc sc_all[addr] = sc
else: sc.settimeout(0.5)
drain_socket(sc) while True:
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5)
sc.sendall(b"R") sc.sendall(b"R")
return recv_full_message(sc)
data = bytearray()
while len(data) < 8:
chunk = sc.recv(1024)
if not chunk:
raise ConnectionError("连接中断")
data.extend(chunk)
return sc, bytes(data)
except Exception as e: except Exception as e:
if sc is not None: if sc is not None:
try: try:
@ -136,30 +109,30 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
except Exception: except Exception:
pass pass
sc_all[addr] = None sc_all[addr] = None
raise e
try:
resp = request_once()
except Exception as e:
self.error(f'采集器通信失败: {e}') self.error(f'采集器通信失败: {e}')
return None, None
sc, resp = connect_and_send()
if sc is None or resp is None:
return return
res = handle_bytes(resp) res = handle_bytes(resp)
if isinstance(res, str) and res == "数据头不正确": if isinstance(res, str):
try: if res == "数据头不正确":
resp = request_once() sc, resp = connect_and_send()
except Exception as e: if sc is None or resp is None:
self.error(f'采集器通信失败: {e}')
return return
res = handle_bytes(resp) res = handle_bytes(resp)
if isinstance(res, str): if isinstance(res, str):
self.error(res) self.error(res)
else: else:
self.ok(res) self.ok(res)
else:
self.error(res)
else:
self.ok(res)
def run(server_class=HTTPServer, handler_class=JSONRequestHandler, port=2300):
def run(server_class=ThreadingHTTPServer, handler_class=JSONRequestHandler, port=2300):
server_address = ('', port) server_address = ('', port)
httpd = server_class(server_address, handler_class) httpd = server_class(server_address, handler_class)
print(f'Starting httpd server on port {port}...') print(f'Starting httpd server on port {port}...')

View File

@ -16,7 +16,6 @@ django.setup()
from apps.enm.services import insert_mplogx_item from apps.enm.services import insert_mplogx_item
from django.utils import timezone from django.utils import timezone
from django.db import connections
from apps.utils.tasks import send_mail_task from apps.utils.tasks import send_mail_task
from datetime import datetime, timedelta from datetime import datetime, timedelta
CUR_DIR = os.path.dirname(os.path.abspath(__file__)) CUR_DIR = os.path.dirname(os.path.abspath(__file__))
@ -93,7 +92,6 @@ def fetch_data(timex, enp_mpoint_dict, path):
""" """
从数据库转存到超表 从数据库转存到超表
""" """
try:
response = None response = None
try: try:
response = requests.get(path, timeout=5) response = requests.get(path, timeout=5)
@ -120,9 +118,6 @@ def fetch_data(timex, enp_mpoint_dict, path):
current_object = [] # 重置,准备处理下一个对象 current_object = [] # 重置,准备处理下一个对象
except Exception as e: except Exception as e:
send_error_notification(e) send_error_notification(e)
finally:
# 子线程必须主动关闭 Django DB 连接,否则每分钟泄漏 2 条 PG 连接
connections.close_all()
def get_data(): def get_data():
last_triggered = None last_triggered = None