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 协议封装
|
1000 系列喷码机网口通讯封装
|
||||||
|
协议: STX(0x02) + 指令 + 数据 + ETX(0x03)
|
||||||
帧格式: 纯 ASCII, 字段以 '|' 分隔, 整帧以 <CR>(0x0D) 结尾
|
反馈: $XX 成功 / !XX 失败 (XX 为两位校验)
|
||||||
- 写指令(SLA/JDA/CQI/JDI/SST/SNO) 通常无回包
|
|
||||||
- 查询指令(GST/GFT/GJD) 回包同样以 <CR> 结尾
|
|
||||||
"""
|
"""
|
||||||
import socket
|
import socket
|
||||||
from rest_framework.exceptions import ParseError
|
from rest_framework.exceptions import ParseError
|
||||||
|
|
||||||
CR = b"\x0d"
|
STX = b"\x02"
|
||||||
|
ETX = b"\x03"
|
||||||
|
LF = b"\x0a"
|
||||||
|
|
||||||
|
|
||||||
class CoderError(ParseError):
|
class CoderError(ParseError):
|
||||||
|
|
@ -18,245 +18,44 @@ class CoderError(ParseError):
|
||||||
class CoderClient:
|
class CoderClient:
|
||||||
def __init__(self, ip: str, port: int = 3100, timeout: float = 2.0):
|
def __init__(self, ip: str, port: int = 3100, timeout: float = 2.0):
|
||||||
if not ip:
|
if not ip:
|
||||||
raise CoderError("喷码IP未配置")
|
raise CoderError("打码器IP未配置")
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.port = port
|
self.port = port
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
|
|
||||||
def _send(self, frame: bytes, expect_reply: bool = False) -> bytes:
|
def _send(self, frame: bytes) -> bytes:
|
||||||
try:
|
try:
|
||||||
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
|
with socket.create_connection((self.ip, self.port), timeout=self.timeout) as s:
|
||||||
s.sendall(frame)
|
s.sendall(frame)
|
||||||
if expect_reply:
|
return s.recv(64)
|
||||||
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:
|
except (socket.timeout, OSError) as e:
|
||||||
raise CoderError(f"喷码通讯失败 {self.ip}:{self.port} - {e}")
|
raise CoderError(f"打码器通讯失败 {self.ip}:{self.port} - {e}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode(text: str, label: str) -> bytes:
|
def _check_ack(resp: bytes):
|
||||||
if "\r" in text or "\n" in text:
|
if not resp:
|
||||||
raise CoderError(f"{label}含 CR/LF, 会破坏帧结构")
|
raise CoderError("打码器无响应")
|
||||||
if "|" in text:
|
head = resp[:1]
|
||||||
raise CoderError(f"{label}含 '|', 与 1880 分隔符冲突")
|
if head == b"$":
|
||||||
try:
|
return True
|
||||||
return text.encode("latin-1")
|
if head == b"!":
|
||||||
except UnicodeEncodeError as e:
|
raise CoderError(f"打码器返回失败: {resp!r}")
|
||||||
raise CoderError(f"{label}含喷码机不支持的字符: {e}")
|
raise CoderError(f"打码器响应不识别: {resp!r}")
|
||||||
|
|
||||||
def send_command(self, command: str, expect_reply: bool = False) -> bytes:
|
def update_field(self, field_name: str, content: str) -> bool:
|
||||||
"""直接下发一条已拼装好的 1880 指令(不含末尾 CR), 由本方法统一补 CR。"""
|
"""更新用户区: 02 55 <field> 0A <content> 03"""
|
||||||
if "\r" in command or "\n" in command:
|
frame = STX + b"\x55" + field_name.encode("ascii") + LF + content.encode("ascii") + ETX
|
||||||
raise CoderError("指令含 CR/LF, 由 send_command 统一补 CR, 不要手动拼")
|
return self._check_ack(self._send(frame))
|
||||||
return self._send(command.encode("latin-1") + CR, expect_reply=expect_reply)
|
|
||||||
|
|
||||||
def select_job(self, job_name: str):
|
def select_message(self, message_name: str) -> bool:
|
||||||
"""SLA|<jobname>|<CR> 选择信息模板"""
|
"""选择信息: 02 4D <name> 03"""
|
||||||
self._send(b"SLA|" + self._encode(job_name, "模板名") + b"|" + CR)
|
frame = STX + b"\x4d" + message_name.encode("ascii") + ETX
|
||||||
|
return self._check_ack(self._send(frame))
|
||||||
|
|
||||||
def update_fields(self, fields: dict):
|
def get_status(self) -> bytes:
|
||||||
"""JDA|<field>=<value>|...|<CR> 一帧更新多个用户区"""
|
"""获取喷码机状态: 02 45 03"""
|
||||||
if not fields:
|
return self._send(STX + b"\x45" + ETX)
|
||||||
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):
|
def send_raw(self, frame_str: str) -> bytes:
|
||||||
"""CQI<CR> 清空喷码机内部缓存队列"""
|
"""发送任意已拼好的帧字符串(不含 STX/ETX), 自动补头尾"""
|
||||||
self._send(b"CQI" + CR)
|
return self._send(STX + frame_str.encode("ascii") + ETX)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -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("名称")
|
name = models.TextField("名称")
|
||||||
commands = models.JSONField("指令模板", default=list, blank=True)
|
commands = models.JSONField("指令模板", default=list, blank=True)
|
||||||
process_json = 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
|
@classmethod
|
||||||
def gen_commands(cls, label_template, label_template_name, tdata):
|
def gen_commands(cls, label_template, label_template_name, tdata):
|
||||||
|
|
|
||||||
|
|
@ -18,18 +18,10 @@ class Tid2Serializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class CoderSendSerializer(serializers.Serializer):
|
class CoderSendSerializer(serializers.Serializer):
|
||||||
tdata_list = serializers.ListField(child=serializers.JSONField(), label='模板数据列表', allow_empty=False)
|
tdata = serializers.JSONField(label='模板数据', required=False, default=dict)
|
||||||
coder_ip = serializers.IPAddressField(label='喷码IP')
|
coder_ip = serializers.IPAddressField(label='打码器IP(可覆盖模板)', required=False, allow_null=True)
|
||||||
coder_port = serializers.IntegerField(label='喷码端口', 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)
|
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):
|
||||||
|
|
@ -51,7 +43,11 @@ class LabelMatSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
|
|
||||||
class LabelTemplateSerializer(CustomModelSerializer):
|
class LabelTemplateSerializer(CustomModelSerializer):
|
||||||
|
coder_ip = serializers.IPAddressField(required=False, allow_null=True, allow_blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = LabelTemplate
|
model = LabelTemplate
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
def validate_coder_ip(self, value):
|
||||||
|
return value or None
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
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, CoderSendSerializer
|
||||||
from apps.cm.coder import CoderClient, HanslaserClient
|
from apps.cm.coder import CoderClient
|
||||||
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
|
||||||
|
|
@ -118,84 +118,29 @@ class LabelTemplateViewSet(CustomModelViewSet):
|
||||||
@action(methods=["post"], detail=True, serializer_class=CoderSendSerializer, perms_map={"post": "*"})
|
@action(methods=["post"], detail=True, serializer_class=CoderSendSerializer, perms_map={"post": "*"})
|
||||||
def send_to_coder(self, request, pk=None, *args, **kwargs):
|
def send_to_coder(self, request, pk=None, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
下发指令到伟迪捷 1880 喷码机 (统一走 CQI + JDI 队列模式)
|
下发指令到打码器
|
||||||
|
|
||||||
body 传 tdata_list (列表, 至少一条), 每条对应一个产品。
|
按模板 commands 用 tdata 格式化后, 通过模板上配置的 IP/端口下发到 1000 系列喷码机用户区。
|
||||||
每次调用先 CQI 清空队列, 再按顺序 JDI 入队 N 条;
|
body 中的 coder_ip/coder_port/coder_field 可临时覆盖模板配置。
|
||||||
喷码机每个外部触发(光电)消费一条, 喷一个产品。
|
|
||||||
每条 tdata 走模板 commands, 解析为 <字段>=<内容> 合并成一帧 JDI;
|
|
||||||
commands 中不含 '=' 的元素, 会绑定到默认 coder_field 上。
|
|
||||||
"""
|
"""
|
||||||
sr = CoderSendSerializer(data=request.data)
|
sr = CoderSendSerializer(data=request.data)
|
||||||
sr.is_valid(raise_exception=True)
|
sr.is_valid(raise_exception=True)
|
||||||
vdata = sr.validated_data
|
vdata = sr.validated_data
|
||||||
lt: LabelTemplate = self.get_object()
|
lt: LabelTemplate = self.get_object()
|
||||||
|
|
||||||
ip = vdata.get("coder_ip")
|
ip = vdata.get("coder_ip") or lt.coder_ip
|
||||||
port = vdata.get("coder_port") or 3100
|
port = vdata.get("coder_port") or lt.coder_port
|
||||||
field = vdata.get("coder_field") or "1"
|
field = vdata.get("coder_field") or "1"
|
||||||
|
if not ip:
|
||||||
|
raise ParseError("模板未配置打码器IP, 也未在请求中提供 coder_ip")
|
||||||
|
|
||||||
batched = []
|
commands = LabelTemplate.gen_commands(lt.id, None, vdata.get("tdata") or {})
|
||||||
for tdata in vdata["tdata_list"]:
|
if not commands:
|
||||||
commands = LabelTemplate.gen_commands(lt.id, None, tdata)
|
raise ParseError("模板未生成任何指令")
|
||||||
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)
|
client = CoderClient(ip=ip, port=port)
|
||||||
coder_jobname = vdata.get("coder_jobname")
|
results = []
|
||||||
client.clear_queue()
|
for content in commands:
|
||||||
if coder_jobname:
|
client.update_field(field, content)
|
||||||
client.select_job(coder_jobname)
|
results.append(content)
|
||||||
client.clear_queue()
|
return Response({"sent": results, "ip": ip, "port": port, "field": field})
|
||||||
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})
|
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
|
|
||||||
|
|
@ -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 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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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:劳动合同
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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__'
|
||||||
|
|
@ -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 = None
|
||||||
existing = name_matches[0] if name_matches else None
|
if id_number:
|
||||||
# 同名但身份证号不一致,视为不同人员,新建
|
existing = Employee.objects.filter(id_number=id_number).first()
|
||||||
if existing and id_number and existing.id_number \
|
if not existing and name:
|
||||||
and existing.id_number != id_number:
|
existing = Employee.objects.filter(name=name, id_number__isnull=True).first() or \
|
||||||
existing = None
|
Employee.objects.filter(name=name, id_number='').first()
|
||||||
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 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}异常:有多个或无后续状态;不可处理')
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -463,8 +463,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 +484,4 @@ class InmService:
|
||||||
mioitem.delete()
|
mioitem.delete()
|
||||||
else:
|
else:
|
||||||
raise ParseError("不支持该出入库单明细撤销")
|
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)
|
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']
|
||||||
|
|
|
||||||
|
|
@ -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")
|
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='关联检验',
|
||||||
|
|
|
||||||
|
|
@ -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 时也能正确拆分为多次返修)
|
material_out__isnull=False,
|
||||||
_all_fix_qs = Mlogb.objects.filter(
|
mlog__mgroup__name="外观检验",
|
||||||
mlog__submit_time__isnull=False,
|
mlog__is_fix=True, batch=batch, need_inout=True)
|
||||||
material_out__isnull=False,
|
if mlogb2_qs.exists():
|
||||||
mlog__mgroup__name="外观检验",
|
data["外观检验_返修_日期"] = []
|
||||||
mlog__is_fix=True,
|
data["外观检验_返修_操作人"] = []
|
||||||
batch=batch,
|
data["外观检验_返修_count_real"] = 0
|
||||||
need_inout=True,
|
data["外观检验_返修_count_ok"] = 0
|
||||||
).order_by("mlog__submit_time", "id")
|
data["外观检验_返修_count_ok_full"] = 0
|
||||||
_fix_prefixes = []
|
for item in mlogb2_qs:
|
||||||
for fix_idx, fix_mlogb in enumerate(_all_fix_qs):
|
if item.mlog.handle_user:
|
||||||
suffix = "" if fix_idx == 0 else str(fix_idx + 1)
|
data["外观检验_返修_操作人"].append(item.mlog.handle_user)
|
||||||
prefix = f"外观检验_返修{suffix}_"
|
if item.mlog.handle_date:
|
||||||
_fix_prefixes.append(prefix)
|
data["外观检验_返修_日期"].append(item.mlog.handle_date)
|
||||||
mlog = fix_mlogb.mlog
|
data["外观检验_返修_count_real"] += item.count_real
|
||||||
handle_date = mlog.handle_date
|
data["外观检验_返修_count_ok"] += item.count_ok
|
||||||
data[f"{prefix}count_real"] = fix_mlogb.count_real
|
data["外观检验_返修_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
|
||||||
data[f"{prefix}count_ok"] = fix_mlogb.count_ok
|
|
||||||
data[f"{prefix}count_ok_full"] = fix_mlogb.count_ok_full or 0
|
data["外观检验_返修_日期"] = list(set(data["外观检验_返修_日期"]))
|
||||||
data[f"{prefix}count_notok"] = fix_mlogb.count_notok or 0
|
data["外观检验_返修_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data["外观检验_返修_日期"]])
|
||||||
try:
|
data["外观检验_返修_操作人"] = list(set(data["外观检验_返修_操作人"]))
|
||||||
data[f"{prefix}合格率"] = round((fix_mlogb.count_ok / fix_mlogb.count_real) * 100, 2)
|
data["外观检验_返修_操作人"] = ";".join([item.name for item in data["外观检验_返修_操作人"]])
|
||||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
|
||||||
data[f"{prefix}合格率"] = 0
|
mlogbd2_qs = MlogbDefect.objects.filter(mlogb__in=mlogb2_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
|
||||||
try:
|
|
||||||
data[f"{prefix}完全合格率"] = round(((fix_mlogb.count_ok_full or 0) / fix_mlogb.count_real) * 100, 2)
|
for item in mlogbd2_qs:
|
||||||
except (decimal.InvalidOperation, ZeroDivisionError):
|
data[f"外观检验_返修_缺陷_{item['defect__name']}"] = item["total"]
|
||||||
data[f"{prefix}完全合格率"] = 0
|
data[f"外观检验_返修_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data["外观检验_返修_count_real"])*100, 2)
|
||||||
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)
|
|
||||||
|
|
||||||
# 车间库存抽检
|
# 车间库存抽检
|
||||||
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):
|
||||||
|
|
|
||||||
|
|
@ -680,8 +680,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,8 +726,6 @@ 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):
|
||||||
|
|
@ -878,17 +874,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):
|
||||||
|
|
@ -1562,7 +1554,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__"
|
||||||
|
|
|
||||||
|
|
@ -1112,7 +1112,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 +1273,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):
|
||||||
|
|
|
||||||
|
|
@ -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)
|
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):
|
||||||
|
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
self.error(f'采集器通信失败: {e}')
|
||||||
|
return None, None
|
||||||
|
|
||||||
try:
|
sc, resp = connect_and_send()
|
||||||
resp = request_once()
|
if sc is None or resp is None:
|
||||||
except Exception as e:
|
|
||||||
self.error(f'采集器通信失败: {e}')
|
|
||||||
return
|
return
|
||||||
|
|
||||||
res = handle_bytes(resp)
|
res = handle_bytes(resp)
|
||||||
if isinstance(res, str) and res == "数据头不正确":
|
|
||||||
try:
|
|
||||||
resp = request_once()
|
|
||||||
except Exception as e:
|
|
||||||
self.error(f'采集器通信失败: {e}')
|
|
||||||
return
|
|
||||||
res = handle_bytes(resp)
|
|
||||||
|
|
||||||
if isinstance(res, str):
|
if isinstance(res, str):
|
||||||
self.error(res)
|
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:
|
else:
|
||||||
self.ok(res)
|
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}...')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue