Compare commits
No commits in common. "master" and "dev_cqm" have entirely different histories.
267
apps/cm/coder.py
267
apps/cm/coder.py
|
|
@ -1,14 +1,14 @@
|
|||
"""
|
||||
伟迪捷 1880 喷码机 ASCII 协议封装
|
||||
|
||||
帧格式: 纯 ASCII, 字段以 '|' 分隔, 整帧以 <CR>(0x0D) 结尾
|
||||
- 写指令(SLA/JDA/CQI/JDI/SST/SNO) 通常无回包
|
||||
- 查询指令(GST/GFT/GJD) 回包同样以 <CR> 结尾
|
||||
1000 系列喷码机网口通讯封装
|
||||
协议: STX(0x02) + 指令 + 数据 + ETX(0x03)
|
||||
反馈: $XX 成功 / !XX 失败 (XX 为两位校验)
|
||||
"""
|
||||
import socket
|
||||
from rest_framework.exceptions import ParseError
|
||||
|
||||
CR = b"\x0d"
|
||||
STX = b"\x02"
|
||||
ETX = b"\x03"
|
||||
LF = b"\x0a"
|
||||
|
||||
|
||||
class CoderError(ParseError):
|
||||
|
|
@ -18,245 +18,44 @@ class CoderError(ParseError):
|
|||
class CoderClient:
|
||||
def __init__(self, ip: str, port: int = 3100, timeout: float = 2.0):
|
||||
if not ip:
|
||||
raise CoderError("喷码IP未配置")
|
||||
raise CoderError("打码器IP未配置")
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
|
||||
def _send(self, frame: bytes, expect_reply: bool = False) -> bytes:
|
||||
def _send(self, frame: bytes) -> 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
|
||||
return s.recv(64)
|
||||
except (socket.timeout, OSError) as e:
|
||||
raise CoderError(f"喷码通讯失败 {self.ip}:{self.port} - {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 _check_ack(resp: bytes):
|
||||
if not resp:
|
||||
raise CoderError("打码器无响应")
|
||||
head = resp[:1]
|
||||
if head == b"$":
|
||||
return True
|
||||
if head == b"!":
|
||||
raise CoderError(f"打码器返回失败: {resp!r}")
|
||||
raise CoderError(f"打码器响应不识别: {resp!r}")
|
||||
|
||||
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 update_field(self, field_name: str, content: str) -> bool:
|
||||
"""更新用户区: 02 55 <field> 0A <content> 03"""
|
||||
frame = STX + b"\x55" + field_name.encode("ascii") + LF + content.encode("ascii") + ETX
|
||||
return self._check_ack(self._send(frame))
|
||||
|
||||
def select_job(self, job_name: str):
|
||||
"""SLA|<jobname>|<CR> 选择信息模板"""
|
||||
self._send(b"SLA|" + self._encode(job_name, "模板名") + b"|" + CR)
|
||||
def select_message(self, message_name: str) -> bool:
|
||||
"""选择信息: 02 4D <name> 03"""
|
||||
frame = STX + b"\x4d" + message_name.encode("ascii") + ETX
|
||||
return self._check_ack(self._send(frame))
|
||||
|
||||
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 get_status(self) -> bytes:
|
||||
"""获取喷码机状态: 02 45 03"""
|
||||
return self._send(STX + b"\x45" + ETX)
|
||||
|
||||
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
|
||||
def send_raw(self, frame_str: str) -> bytes:
|
||||
"""发送任意已拼好的帧字符串(不含 STX/ETX), 自动补头尾"""
|
||||
return self._send(STX + frame_str.encode("ascii") + ETX)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# Generated for coder fields on LabelTemplate
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('cm', '0006_alter_lablemat_batch'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='labeltemplate',
|
||||
name='coder_ip',
|
||||
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='打码器IP'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='labeltemplate',
|
||||
name='coder_port',
|
||||
field=models.PositiveIntegerField(default=3100, verbose_name='打码器端口'),
|
||||
),
|
||||
]
|
||||
|
|
@ -22,6 +22,8 @@ class LabelTemplate(BaseModel):
|
|||
name = models.TextField("名称")
|
||||
commands = models.JSONField("指令模板", default=list, blank=True)
|
||||
process_json = models.JSONField("工序", default=list, blank=True)
|
||||
coder_ip = models.GenericIPAddressField("打码器IP", null=True, blank=True)
|
||||
coder_port = models.PositiveIntegerField("打码器端口", default=3100)
|
||||
|
||||
@classmethod
|
||||
def gen_commands(cls, label_template, label_template_name, tdata):
|
||||
|
|
|
|||
|
|
@ -18,18 +18,10 @@ class Tid2Serializer(serializers.Serializer):
|
|||
|
||||
|
||||
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)
|
||||
tdata = serializers.JSONField(label='模板数据', required=False, default=dict)
|
||||
coder_ip = serializers.IPAddressField(label='打码器IP(可覆盖模板)', required=False, allow_null=True)
|
||||
coder_port = serializers.IntegerField(label='打码器端口(可覆盖模板)', required=False, allow_null=True)
|
||||
coder_field = serializers.CharField(label='用户区名(可覆盖模板)', required=False, allow_null=True)
|
||||
|
||||
|
||||
class LabelMatSerializer(serializers.ModelSerializer):
|
||||
|
|
@ -51,7 +43,11 @@ class LabelMatSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class LabelTemplateSerializer(CustomModelSerializer):
|
||||
coder_ip = serializers.IPAddressField(required=False, allow_null=True, allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
model = LabelTemplate
|
||||
fields = '__all__'
|
||||
|
||||
def validate_coder_ip(self, value):
|
||||
return value or None
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from apps.cm.models import LableMat, LabelTemplate
|
||||
from rest_framework.decorators import action
|
||||
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer, CoderSendSerializer, HanslaserSendSerializer
|
||||
from apps.cm.coder import CoderClient, HanslaserClient
|
||||
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer, CoderSendSerializer
|
||||
from apps.cm.coder import CoderClient
|
||||
from apps.inm.models import MaterialBatch, MIOItem
|
||||
from apps.wpm.models import WMaterial
|
||||
from rest_framework.exceptions import ParseError, NotFound
|
||||
|
|
@ -118,84 +118,29 @@ class LabelTemplateViewSet(CustomModelViewSet):
|
|||
@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 上。
|
||||
按模板 commands 用 tdata 格式化后, 通过模板上配置的 IP/端口下发到 1000 系列喷码机用户区。
|
||||
body 中的 coder_ip/coder_port/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
|
||||
ip = vdata.get("coder_ip") or lt.coder_ip
|
||||
port = vdata.get("coder_port") or lt.coder_port
|
||||
field = vdata.get("coder_field") or "1"
|
||||
if not ip:
|
||||
raise ParseError("模板未配置打码器IP, 也未在请求中提供 coder_ip")
|
||||
|
||||
batched = []
|
||||
for tdata in vdata["tdata_list"]:
|
||||
commands = LabelTemplate.gen_commands(lt.id, None, tdata)
|
||||
commands = LabelTemplate.gen_commands(lt.id, None, vdata.get("tdata") or {})
|
||||
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})
|
||||
results = []
|
||||
for content in commands:
|
||||
client.update_field(field, content)
|
||||
results.append(content)
|
||||
return Response({"sent": results, "ip": ip, "port": port, "field": field})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import socket
|
||||
from rest_framework.exceptions import ParseError
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from django.core.cache import cache
|
||||
from apps.utils.thread import MyThread
|
||||
|
|
@ -244,75 +243,6 @@ def get_tyy_data_3(*args, retry=2):
|
|||
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):
|
||||
host, port = args[0], int(args[1])
|
||||
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
|
||||
|
|
|
|||
|
|
@ -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='最后编辑人'),
|
||||
),
|
||||
]
|
||||
|
|
@ -14,7 +14,6 @@ from rest_framework.decorators import action
|
|||
from rest_framework.serializers import Serializer
|
||||
from django.db import transaction
|
||||
from apps.em.services import daoru_equipment
|
||||
from apps.em import cd
|
||||
from rest_framework.response import Response
|
||||
from django.conf import settings
|
||||
from django.db.models import Count, Case, When, IntegerField
|
||||
|
|
@ -128,26 +127,6 @@ class EquipmentViewSet(CustomModelViewSet):
|
|||
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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='关联工单'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='合同变更次数'),
|
||||
),
|
||||
]
|
||||
|
|
@ -354,8 +354,8 @@ class EmpContract(CommonAModel):
|
|||
"""
|
||||
TN:劳动合同
|
||||
"""
|
||||
employee = models.ForeignKey(Employee, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
|
||||
employee = models.OneToOneField(Employee, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
|
||||
on_delete=models.CASCADE, related_name='contract_ticket', null=True, blank=True)
|
||||
counts = models.PositiveSmallIntegerField('合同变更次数', default=0)
|
||||
plan_renewal = models.DateField('应续签', null=True, blank=True)
|
||||
|
|
|
|||
|
|
@ -425,22 +425,6 @@ class EmpContractSerializer(CustomModelSerializer):
|
|||
gender = serializers.CharField(source="employee.gender", 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)
|
||||
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:
|
||||
model = EmpContract
|
||||
fields = '__all__'
|
||||
|
|
@ -33,9 +33,8 @@ from datetime import datetime
|
|||
from apps.utils.export import export_excel
|
||||
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
|
||||
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.services import WfService
|
||||
from apps.system.models import Post, Dept
|
||||
from django.db.models import DateField
|
||||
from datetime import datetime, date
|
||||
|
|
@ -290,70 +289,23 @@ class EmployeeViewSet(CustomModelViewSet):
|
|||
"""导出excel
|
||||
导出excel
|
||||
"""
|
||||
# 导出列定义: (表头, 取值函数)。覆盖人员的全部业务字段。
|
||||
def _cell(v):
|
||||
return '' if v is None else v
|
||||
columns = [
|
||||
('人员类型', 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')
|
||||
field_data = ['人员类型', '人员', '手机号', '身份证号', '所属部门', '在职状态', '定位卡号']
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
if queryset.count() > 1000:
|
||||
raise ParseError('数据量超过1000,请筛选后导出')
|
||||
odata = EmployeeSerializer(queryset, many=True).data
|
||||
# 处理数据
|
||||
data = []
|
||||
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, '人员信息')})
|
||||
|
||||
@action(methods=['post'], detail=False, perms_map={'post': 'employee.import_excel'},
|
||||
|
|
@ -408,8 +360,8 @@ class EmployeeViewSet(CustomModelViewSet):
|
|||
id_number = data.get("id_number")
|
||||
name = data.get("name")
|
||||
|
||||
if not name:
|
||||
raise ParseError(f'第{row_num}行,姓名为空')
|
||||
if not id_number or not name:
|
||||
raise ParseError(f'第{row_num}行,身份证号或姓名为空')
|
||||
|
||||
myLogger.info(f"处理第{row_num}行:{name} - {id_number}")
|
||||
# 处理人员类型
|
||||
|
|
@ -419,9 +371,9 @@ class EmployeeViewSet(CustomModelViewSet):
|
|||
data['type'] = TYPE_MAPPING[excel_type]
|
||||
else:
|
||||
raise ParseError(f'第{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
|
||||
# 处理部门外键:填了就校验是否存在并赋值;为空时不动(新增场景的必填在下方创建处校验)
|
||||
dept_name = data.pop('belong_dept', None)
|
||||
if dept_name:
|
||||
# 处理部门外键
|
||||
if 'belong_dept' in data and data['belong_dept']:
|
||||
dept_name = data.pop('belong_dept')
|
||||
if dept_name not in dept_map:
|
||||
raise ParseError(f'第{row_num}行,部门"{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']):
|
||||
raise ParseError(f'第{row_num}行,身份证号格式不正确')
|
||||
|
||||
# 按姓名匹配并更新:姓名唯一则直接更新(身份证号可为空);
|
||||
# 存在重名则必须用身份证号定位具体记录,否则报错。
|
||||
# 查找或创建/补全
|
||||
try:
|
||||
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
|
||||
else:
|
||||
# 存在重名,必须用身份证号区分
|
||||
if not id_number:
|
||||
raise ParseError(f'第{row_num}行,姓名"{name}"存在重名,必须填写身份证号')
|
||||
existing = next(
|
||||
(e for e in name_matches if e.id_number == id_number), None)
|
||||
|
||||
if id_number:
|
||||
existing = Employee.objects.filter(id_number=id_number).first()
|
||||
if not existing and name:
|
||||
existing = Employee.objects.filter(name=name, id_number__isnull=True).first() or \
|
||||
Employee.objects.filter(name=name, id_number='').first()
|
||||
if existing:
|
||||
# 用 Excel 中填写了值的列覆盖数据库已有数据;空单元格保持原值不变
|
||||
# 只用 Excel 非空值填补数据库中为空的字段
|
||||
updated_fields = []
|
||||
for field_name, value in data.items():
|
||||
if value in [None, '']:
|
||||
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)
|
||||
updated_fields.append(field_name)
|
||||
if updated_fields:
|
||||
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:
|
||||
myLogger.info(f"⏭️ 第{row_num}行无变化:{name}")
|
||||
myLogger.info(f"⏭️ 第{row_num}行无需补全:{name}")
|
||||
created = False
|
||||
else:
|
||||
# 新增人员时所属部门必填
|
||||
if 'belong_dept_id' not in data:
|
||||
raise ParseError(f'第{row_num}行,新增人员时所属部门不能为空')
|
||||
Employee.objects.create(**data)
|
||||
Employee.objects.create(id_number=id_number, name=name, **data)
|
||||
created = True
|
||||
except Exception as e:
|
||||
raise
|
||||
|
|
@ -883,49 +825,3 @@ class EmpContractViewSet(TicketMixin, EuModelViewSet):
|
|||
def gen_other_ticket_data(self, instance):
|
||||
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}异常:有多个或无后续状态;不可处理')
|
||||
|
||||
|
|
|
|||
|
|
@ -463,8 +463,7 @@ class InmService:
|
|||
|
||||
@classmethod
|
||||
def revert_and_del(cls, mioitem: MIOItem):
|
||||
# 锁定 MIO 行,防止多人同时撤销不同明细时并发漏删 MIO
|
||||
mio = MIO.objects.select_for_update().get(id=mioitem.mio_id)
|
||||
mio = mioitem.mio
|
||||
if mio.submit_time is None:
|
||||
raise ParseError("未提交的出入库明细不允许撤销")
|
||||
if mioitem.test_date is not None:
|
||||
|
|
@ -485,7 +484,4 @@ class InmService:
|
|||
mioitem.delete()
|
||||
else:
|
||||
raise ParseError("不支持该出入库单明细撤销")
|
||||
# 若该出入库记录已无明细,自动删除
|
||||
if not MIOItem.objects.filter(mio=mio).exists():
|
||||
mio.delete()
|
||||
|
||||
|
|
@ -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='最后编辑人'),
|
||||
),
|
||||
]
|
||||
|
|
@ -37,7 +37,7 @@ class Process(CommonBModel):
|
|||
parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True)
|
||||
number_to_batch = models.BooleanField('个号转批号', default=False)
|
||||
wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True)
|
||||
clear_defect = models.BooleanField('合批清除缺陷', default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = '工序'
|
||||
ordering = ['sort', 'create_time']
|
||||
|
|
|
|||
|
|
@ -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='设备'),
|
||||
),
|
||||
]
|
||||
|
|
@ -614,8 +614,7 @@ class Mlogbw(BaseModel):
|
|||
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
|
||||
, 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')
|
||||
tooling = models.ForeignKey(Equipment, verbose_name='工装', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlogbw_tooling')
|
||||
equip = models.ForeignKey(Equipment, verbose_name='设备', on_delete=models.SET_NULL, null=True, blank=True)
|
||||
work_start_time = models.DateTimeField('开始加工时间', null=True, blank=True)
|
||||
work_end_time = models.DateTimeField('结束加工时间', null=True, blank=True)
|
||||
ftest = models.OneToOneField("qm.ftest", verbose_name='关联检验',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from apps.wpm.models import BatchSt
|
||||
import logging
|
||||
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
|
||||
import decimal
|
||||
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}_班次"]])
|
||||
|
||||
|
||||
# 按 mlog__submit_time, id 排序,每条 Mlogb 记录独立为一次返修
|
||||
# (同一 Mlog 下有多条同批次 Mlogb 时也能正确拆分为多次返修)
|
||||
_all_fix_qs = Mlogb.objects.filter(
|
||||
mlog__submit_time__isnull=False,
|
||||
mlogb2_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,
|
||||
material_out__isnull=False,
|
||||
mlog__mgroup__name="外观检验",
|
||||
mlog__is_fix=True,
|
||||
batch=batch,
|
||||
need_inout=True,
|
||||
).order_by("mlog__submit_time", "id")
|
||||
_fix_prefixes = []
|
||||
for fix_idx, fix_mlogb in enumerate(_all_fix_qs):
|
||||
suffix = "" if fix_idx == 0 else str(fix_idx + 1)
|
||||
prefix = f"外观检验_返修{suffix}_"
|
||||
_fix_prefixes.append(prefix)
|
||||
mlog = fix_mlogb.mlog
|
||||
handle_date = mlog.handle_date
|
||||
data[f"{prefix}count_real"] = fix_mlogb.count_real
|
||||
data[f"{prefix}count_ok"] = fix_mlogb.count_ok
|
||||
data[f"{prefix}count_ok_full"] = fix_mlogb.count_ok_full or 0
|
||||
data[f"{prefix}count_notok"] = fix_mlogb.count_notok or 0
|
||||
try:
|
||||
data[f"{prefix}合格率"] = round((fix_mlogb.count_ok / fix_mlogb.count_real) * 100, 2)
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{prefix}合格率"] = 0
|
||||
try:
|
||||
data[f"{prefix}完全合格率"] = round(((fix_mlogb.count_ok_full or 0) / fix_mlogb.count_real) * 100, 2)
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
data[f"{prefix}完全合格率"] = 0
|
||||
data[f"{prefix}日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
||||
data[f"{prefix}小日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
||||
data[f"{prefix}大日期"] = handle_date.strftime("%Y-%m-%d") if handle_date else ""
|
||||
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)
|
||||
mlog__is_fix=True, batch=batch, need_inout=True)
|
||||
if mlogb2_qs.exists():
|
||||
data["外观检验_返修_日期"] = []
|
||||
data["外观检验_返修_操作人"] = []
|
||||
data["外观检验_返修_count_real"] = 0
|
||||
data["外观检验_返修_count_ok"] = 0
|
||||
data["外观检验_返修_count_ok_full"] = 0
|
||||
for item in mlogb2_qs:
|
||||
if item.mlog.handle_user:
|
||||
data["外观检验_返修_操作人"].append(item.mlog.handle_user)
|
||||
if item.mlog.handle_date:
|
||||
data["外观检验_返修_日期"].append(item.mlog.handle_date)
|
||||
data["外观检验_返修_count_real"] += item.count_real
|
||||
data["外观检验_返修_count_ok"] += item.count_ok
|
||||
data["外观检验_返修_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
|
||||
|
||||
data["外观检验_返修_日期"] = list(set(data["外观检验_返修_日期"]))
|
||||
data["外观检验_返修_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_返修_日期"]])
|
||||
data["外观检验_返修_操作人"] = list(set(data["外观检验_返修_操作人"]))
|
||||
data["外观检验_返修_操作人"] = ";".join([item.name for item in data["外观检验_返修_操作人"]])
|
||||
|
||||
mlogbd2_qs = MlogbDefect.objects.filter(mlogb__in=mlogb2_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
||||
|
||||
for item in mlogbd2_qs:
|
||||
data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
|
||||
data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
|
||||
|
||||
# 车间库存抽检
|
||||
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"]
|
||||
|
||||
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:
|
||||
data["外观检验_总合格率"] = round((data["外观检验_总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
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:
|
||||
data["外观检验_完全总合格率"] = round((data["外观检验_完全总合格数"] / data["外观检验_count_real"])*100, 2)
|
||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
||||
|
|
|
|||
|
|
@ -680,8 +680,6 @@ class MlogSerializer(CustomModelSerializer):
|
|||
handle_user = attrs.get('handle_user', None)
|
||||
if handle_user is None and hasattr(self, "request"):
|
||||
handle_user = self.request.user
|
||||
if self.instance is None and mgroup.process:
|
||||
attrs['clear_defect'] = mgroup.process.clear_defect
|
||||
return attrs
|
||||
|
||||
|
||||
|
|
@ -728,8 +726,6 @@ class MlogInitSerializer(CustomModelSerializer):
|
|||
attrs["qct"] = Qct.get(attrs["material_out"], "process", "out")
|
||||
|
||||
attrs["handle_date"], attrs["shift"] = mgroup.get_shift(attrs['work_start_time'])
|
||||
if mgroup.process:
|
||||
attrs['clear_defect'] = mgroup.process.clear_defect
|
||||
return attrs
|
||||
|
||||
class MlogChangeSerializer(CustomModelSerializer):
|
||||
|
|
@ -878,17 +874,13 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
|
|||
ftest = FtestProcessSerializer(required=False, allow_null=True)
|
||||
equip_name = serializers.CharField(source='equip.name', read_only=True)
|
||||
equip_number = serializers.CharField(source='equip.number', read_only=True)
|
||||
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_pre_info = serializers.JSONField(source='wpr.pre_info', read_only=True)
|
||||
mlogb__batch = serializers.CharField(source='mlogb.batch', read_only=True)
|
||||
class Meta:
|
||||
model = Mlogbw
|
||||
fields = ["id", "number", "wpr", "note",
|
||||
"mlogb", "ftest", "equip", "tooling", "work_start_time",
|
||||
"work_end_time", "mlogbw_from", "equip_name", "equip_number",
|
||||
"tooling_name", "tooling_number", "wpr_number_out", "wpr_pre_info", "mlogb__batch"]
|
||||
"mlogb", "ftest", "equip", "work_start_time",
|
||||
"work_end_time", "mlogbw_from", "equip_name", "equip_number", "wpr_number_out", "mlogb__batch"]
|
||||
read_only_fields = ["mlogbw_from"]
|
||||
|
||||
def validate(self, attrs):
|
||||
|
|
@ -1562,7 +1554,6 @@ class MlogUserSerializer(CustomModelSerializer):
|
|||
handle_user_name = serializers.CharField(source='handle_user.name', read_only=True)
|
||||
process_name = serializers.CharField(source='process.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:
|
||||
model = MlogUser
|
||||
fields = "__all__"
|
||||
|
|
|
|||
|
|
@ -1112,7 +1112,7 @@ class MlogbwViewSet(CustomModelViewSet):
|
|||
list_serializer_class = MlogbwListSerializer
|
||||
retrieve_serializer_class = MlogbwListSerializer
|
||||
filterset_class = MlogbwFilter
|
||||
select_related_fields = ["ftest", "equip", "tooling", "wpr", "mlogb"]
|
||||
select_related_fields = ["ftest", "equip", "wpr", "mlogb"]
|
||||
prefetch_related_fields = [
|
||||
Prefetch(
|
||||
"ftest__items_ftest",
|
||||
|
|
@ -1273,7 +1273,7 @@ class MlogUserViewSet(BulkCreateModelMixin, CustomListModelMixin, BulkDestroyMod
|
|||
perms_map = {"get": "*", "post": "mlog.update", "delete": "mlog.update"}
|
||||
queryset = MlogUser.objects.all()
|
||||
serializer_class = MlogUserSerializer
|
||||
select_related_fields = ["handle_user", "shift", "process", "equipment"]
|
||||
select_related_fields = ["handle_user", "shift", "process"]
|
||||
filterset_fields = ["mlog"]
|
||||
|
||||
def get_queryset(self):
|
||||
|
|
|
|||
|
|
@ -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='预处理信息'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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='预处理信息'),
|
||||
),
|
||||
]
|
||||
|
|
@ -30,7 +30,6 @@ class Wpr(BaseModel):
|
|||
oinfo = models.JSONField(verbose_name="其他信息", default=dict, 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)
|
||||
pre_info = models.JSONField(verbose_name="预处理信息", default=dict, blank=True, null=True)
|
||||
|
||||
@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):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from apps.utils.serializers import CustomModelSerializer
|
|||
from rest_framework import serializers
|
||||
from apps.inm.serializers import MaterialBatchSerializer
|
||||
from apps.wpm.serializers import WMaterialSerializer
|
||||
from apps.qm.models import FtestItem
|
||||
|
||||
class WprDefectSerializer(CustomModelSerializer):
|
||||
defect_name = serializers.CharField(source="defect.name", read_only=True)
|
||||
|
|
@ -56,7 +55,3 @@ class WproutListSerializer(serializers.Serializer):
|
|||
class WprChangeNumberSerializer(serializers.Serializer):
|
||||
old_number = serializers.CharField(label="原编号")
|
||||
new_number = serializers.CharField(label="新编号")
|
||||
|
||||
|
||||
class WprPreInfoSerializer(serializers.Serializer):
|
||||
tooling = serializers.CharField(label="工装ID")
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
from rest_framework.decorators import action
|
||||
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.qm.models import FtestItem
|
||||
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer, WprPreInfoSerializer
|
||||
from apps.wpmw.serializers import WprSerializer, WprNewSerializer, WprDetailSerializer, WproutListSerializer, WprChangeNumberSerializer
|
||||
from rest_framework.response import Response
|
||||
from apps.mtm.models import Material
|
||||
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
|
||||
|
||||
|
||||
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"]
|
||||
prefetch_related_fields = ["defects"]
|
||||
queryset = Wpr.objects.all()
|
||||
|
|
@ -149,18 +148,6 @@ class WprViewSet(BulkUpdateModelMixin, CustomListModelMixin, CustomRetrieveModel
|
|||
else:
|
||||
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)
|
||||
@transaction.atomic
|
||||
def assgin_number_out(self, request, *args, **kwargs):
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import json
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
import socket
|
||||
import threading
|
||||
import traceback
|
||||
import threading # 新增:引入线程锁
|
||||
|
||||
sc_all = {}
|
||||
sc_locks = {}
|
||||
registry_lock = threading.Lock()
|
||||
# 全局 Socket 连接字典
|
||||
sc_all = {
|
||||
"192.168.1.220_6000": None,
|
||||
"192.168.1.235_6000": None,
|
||||
"192.168.1.225_6000": None,
|
||||
"192.168.1.230_6000": None,
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
sc_lock = threading.Lock() # 全局锁,保护 sc_all
|
||||
|
||||
def get_checksum(body_msg):
|
||||
return sum(body_msg) & 0xFF
|
||||
|
|
@ -51,45 +46,6 @@ def handle_bytes(arr):
|
|||
except Exception as 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):
|
||||
def ok(self, data):
|
||||
self.send_response(200)
|
||||
|
|
@ -104,31 +60,48 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
|
|||
data = {"err_msg": err_msg}
|
||||
self.wfile.write(json.dumps(data, ensure_ascii=False).encode('utf-8'))
|
||||
|
||||
def log_message(self, format, *args):
|
||||
return
|
||||
|
||||
def do_GET(self):
|
||||
parsed_url = urlparse(self.path)
|
||||
query_params = parse_qs(parsed_url.query)
|
||||
host = query_params.get('host', ['127.0.0.1'])[0]
|
||||
port = query_params.get('port', ['6000'])[0]
|
||||
addr = f'{host}_{port}'
|
||||
lock = get_lock(addr)
|
||||
|
||||
def request_once():
|
||||
with lock:
|
||||
if addr not in sc_all:
|
||||
self.error(f'{addr} 未找到')
|
||||
return
|
||||
|
||||
def connect_and_send():
|
||||
with sc_lock: # 加锁,防止竞争
|
||||
sc = sc_all[addr]
|
||||
|
||||
try:
|
||||
if sc is None:
|
||||
sc = socket.socket()
|
||||
sc.settimeout(5)
|
||||
sc.connect((host, int(port)))
|
||||
sc_all[addr] = sc
|
||||
else:
|
||||
drain_socket(sc)
|
||||
sc.settimeout(0.5)
|
||||
while True:
|
||||
try:
|
||||
data = sc.recv(65536)
|
||||
if not data:
|
||||
break
|
||||
except (socket.timeout, BlockingIOError):
|
||||
break
|
||||
|
||||
sc.settimeout(5)
|
||||
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:
|
||||
if sc is not None:
|
||||
try:
|
||||
|
|
@ -136,30 +109,30 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
|
|||
except Exception:
|
||||
pass
|
||||
sc_all[addr] = None
|
||||
raise e
|
||||
|
||||
try:
|
||||
resp = request_once()
|
||||
except Exception as e:
|
||||
self.error(f'采集器通信失败: {e}')
|
||||
return None, None
|
||||
|
||||
sc, resp = connect_and_send()
|
||||
if sc is None or resp is None:
|
||||
return
|
||||
|
||||
res = handle_bytes(resp)
|
||||
if isinstance(res, str) and res == "数据头不正确":
|
||||
try:
|
||||
resp = request_once()
|
||||
except Exception as e:
|
||||
self.error(f'采集器通信失败: {e}')
|
||||
if isinstance(res, str):
|
||||
if res == "数据头不正确":
|
||||
sc, resp = connect_and_send()
|
||||
if sc is None or resp is None:
|
||||
return
|
||||
res = handle_bytes(resp)
|
||||
|
||||
if isinstance(res, str):
|
||||
self.error(res)
|
||||
else:
|
||||
self.ok(res)
|
||||
else:
|
||||
self.error(res)
|
||||
else:
|
||||
self.ok(res)
|
||||
|
||||
|
||||
def run(server_class=ThreadingHTTPServer, handler_class=JSONRequestHandler, port=2300):
|
||||
def run(server_class=HTTPServer, handler_class=JSONRequestHandler, port=2300):
|
||||
server_address = ('', port)
|
||||
httpd = server_class(server_address, handler_class)
|
||||
print(f'Starting httpd server on port {port}...')
|
||||
|
|
|
|||
Loading…
Reference in New Issue