Compare commits

..

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

151 changed files with 2241 additions and 7991 deletions

View File

@ -20,10 +20,6 @@ class WxCodeSerializer(serializers.Serializer):
code = serializers.CharField(label="code") code = serializers.CharField(label="code")
class UserIdSerializer(serializers.Serializer):
user_id = serializers.CharField(label="用户id")
class PwResetSerializer(serializers.Serializer): class PwResetSerializer(serializers.Serializer):
phone = serializers.CharField(label="手机号") phone = serializers.CharField(label="手机号")
code = serializers.CharField(label="验证码") code = serializers.CharField(label="验证码")

View File

@ -3,8 +3,7 @@ from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView from rest_framework_simplejwt.views import TokenRefreshView
from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView, from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView,
SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView)
TokenLoginView, FaceLoginView, UserIdLogin)
API_BASE_URL = 'api/auth/' API_BASE_URL = 'api/auth/'
urlpatterns = [ urlpatterns = [
@ -19,6 +18,5 @@ urlpatterns = [
path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'), path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'),
path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'), path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'),
path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'), path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'),
path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login'), path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login')
path(API_BASE_URL + 'login_userid/', UserIdLogin.as_view(), name='userid_login'),
] ]

View File

@ -23,8 +23,7 @@ from apps.auth1.serializers import FaceLoginSerializer
from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer, from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer,
PwResetSerializer, SecretLoginSerializer, PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer)
SendCodeSerializer, WxCodeSerializer, UserIdSerializer)
from apps.system.models import User from apps.system.models import User
from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.views import TokenObtainPairView
from apps.auth1.authentication import get_user_by_username_or from apps.auth1.authentication import get_user_by_username_or
@ -235,29 +234,6 @@ class SecretLogin(CreateAPIView):
return Response(ret) return Response(ret)
raise ParseError('登录失败') raise ParseError('登录失败')
class UserIdLogin(CreateAPIView):
"""直接UserId登录(危险操作)
直接UserId登录
"""
authentication_classes = []
permission_classes = []
serializer_class = UserIdSerializer
def post(self, request):
sr = UserIdSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
userid = vdata['user_id']
try:
user = User.objects.get(id=userid)
except Exception as e:
raise ParseError(f'用户不存在-{e}')
if user:
ret = get_tokens_for_user(user)
return Response(ret)
raise ParseError('登录失败')
class PwResetView(CreateAPIView): class PwResetView(CreateAPIView):
"""重置密码 """重置密码

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-25 03:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('bi', '0005_datasetrecord'),
]
operations = [
migrations.AddField(
model_name='dataset',
name='enabled',
field=models.BooleanField(default=True, verbose_name='启用'),
),
]

View File

@ -12,7 +12,6 @@ class Dataset(CommonBDModel):
test_param = models.JSONField('测试查询参数', default=dict, blank=True) test_param = models.JSONField('测试查询参数', default=dict, blank=True)
default_param = models.JSONField('默认查询参数', default=dict, blank=True) default_param = models.JSONField('默认查询参数', default=dict, blank=True)
cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True) cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True)
enabled = models.BooleanField('启用', default=True)
# class Report(CommonBDModel): # class Report(CommonBDModel):

View File

@ -5,19 +5,16 @@ from apps.bi.models import Dataset
import concurrent import concurrent
from apps.utils.sql import execute_raw_sql, format_sqldata from apps.utils.sql import execute_raw_sql, format_sqldata
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE", "INSERT", "CREATE", "ALTER", "GRANT", "REVOKE", "EXEC", "EXECUTE"] forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE"]
def check_sql_safe(sql: str): def check_sql_safe(sql: str):
"""检查sql安全性 """检查sql安全性
""" """
sql_upper = sql.upper() sql_upper = sql.upper()
# 将SQL按空格和分号分割成单词
words = [word for word in sql_upper.replace(';', ' ').split() if word]
for kw in forbidden_keywords: for kw in forbidden_keywords:
# 检查关键字是否作为独立单词出现 if kw in sql_upper:
if kw in words: raise ParseError('sql查询有风险')
raise ParseError(f'sql查询有风险-{kw}')
return sql return sql
def format_json_with_placeholders(json_str, **kwargs): def format_json_with_placeholders(json_str, **kwargs):

View File

@ -64,8 +64,6 @@ class DatasetViewSet(CustomModelViewSet):
执行sql查询支持code 执行sql查询支持code
""" """
dt: Dataset = self.get_object() dt: Dataset = self.get_object()
if not dt.enabled:
raise ParseError(f'{dt.name}-该查询未启用')
rdata = DatasetSerializer(instance=dt).data rdata = DatasetSerializer(instance=dt).data
xquery = request.data.get('query', {}) xquery = request.data.get('query', {})
is_test = request.data.get('is_test', False) is_test = request.data.get('is_test', False)

View File

@ -102,7 +102,7 @@ class LabelTemplateViewSet(CustomModelViewSet):
serializer_class = LabelTemplateSerializer serializer_class = LabelTemplateSerializer
filterset_class = LabelTemplateFilter filterset_class = LabelTemplateFilter
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"}) @action(methods=["post"], detail=False, serializer_class=Tid2Serializer)
def commands(self, request, *args, **kwargs): def commands(self, request, *args, **kwargs):
""" """
获取标签指令 获取标签指令

View File

@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals
from celery import shared_task from celery import shared_task
import subprocess import subprocess
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
from django.conf import settings
import logging import logging
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
@ -15,10 +14,8 @@ def backup_database():
import datetime import datetime
name = datetime.datetime.now().strftime("%Y%m%d%H%M%S") name = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
exclude_tables = getattr(settings, 'EXCLUDE_TABLE_DATA', []) command = 'echo "{}" | sudo -S pg_dump "user={} password={} dbname={}" > {}/bak_{}.sql'.format(
exclude_str = ' '.join([f"--exclude-table-data={table}" for table in exclude_tables]) SD_PWD, DATABASES["default"]["USER"], DATABASES["default"]["PASSWORD"], DATABASES["default"]["NAME"], BACKUP_PATH + "/database", name
command = 'echo "{}" | sudo -S -u postgres pg_dump {} "user={} password={} dbname={}" > {}/bak_{}.sql'.format(
SD_PWD, exclude_str, DATABASES["default"]["USER"], DATABASES["default"]["PASSWORD"], DATABASES["default"]["NAME"], BACKUP_PATH + "/database", name
) )
completed = subprocess.run(command, shell=True, capture_output=True, text=True) completed = subprocess.run(command, shell=True, capture_output=True, text=True)
if completed.returncode != 0: if completed.returncode != 0:

View File

@ -4,19 +4,14 @@ import json
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
import uuid import struct
import logging
import threading
import requests
myLogger = logging.getLogger('log')
def get_checksum(body_msg): def get_checksum(body_msg):
return sum(body_msg) & 0xFF return sum(body_msg) & 0xFF
def handle_bytes(arr): def handle_bytes(arr):
if len(arr) < 8: if len(arr) < 8:
return f"返回数据长度错误-{arr}" return "返回数据长度错误"
if arr[0] != 0xEB or arr[1] != 0x90: if arr[0] != 0xEB or arr[1] != 0x90:
return "数据头不正确" return "数据头不正确"
@ -24,7 +19,7 @@ def handle_bytes(arr):
# 读取长度信息 # 读取长度信息
length_arr = arr[2:4][::-1] # 反转字节 length_arr = arr[2:4][::-1] # 反转字节
length = int.from_bytes(length_arr, byteorder='little', signed=True) # 小端格式 length = struct.unpack('<H', bytes(length_arr))[0] # 小端格式
# 提取内容 # 提取内容
body = arr[4:4 + length - 3] body = arr[4:4 + length - 3]
@ -45,9 +40,8 @@ def handle_bytes(arr):
return res[0] return res[0]
def get_tyy_data_t(host, port, tid): def get_tyy_data_t(host, port):
cd_thread_key_id = f"cd_thread_{host}_{port}_id" cd_thread_key = f"cd_thread_{host}_{port}"
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
sc = None sc = None
def connect_and_send(retry=1): def connect_and_send(retry=1):
nonlocal sc nonlocal sc
@ -57,62 +51,55 @@ def get_tyy_data_t(host, port, tid):
sc.connect((host, int(port))) sc.connect((host, int(port)))
sc.sendall(b"R") sc.sendall(b"R")
except BrokenPipeError: except BrokenPipeError:
sc = None
if retry > 0: if retry > 0:
connect_and_send(retry-1) connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc = None
except OSError as e: except OSError as e:
sc = None sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"}) cache.set(cd_thread_key, {"err_msg": f"采集器连接失败-{str(e)}"})
except ConnectionResetError: except ConnectionResetError:
sc = None sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器重置了连接"}) cache.set(cd_thread_key, {"err_msg": "采集器重置了连接"})
except socket.timeout: except socket.timeout:
sc = None sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器连接超时"}) cache.set(cd_thread_key, {"err_msg": "采集器连接超时"})
except Exception as e: except Exception as e:
sc = None sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"}) cache.set(cd_thread_key, {"err_msg": f"采集器连接失败-{str(e)}"})
while cache.get(cd_thread_key_id) == tid: while True:
if cache.get(cd_thread_key_val) == "get": cd_thread_val = cache.get(cd_thread_key, default=None)
cache.set(cd_thread_key_val, "working") if cd_thread_val is None:
if sc:
try:
sc.close()
except Exception:
pass
break
elif cd_thread_val == "get":
cache.set(cd_thread_key, "working")
connect_and_send() connect_and_send()
if sc is None: if sc is None:
continue continue
resp = sc.recv(1024) resp = sc.recv(1024)
res = handle_bytes(resp) res = handle_bytes(resp)
if isinstance(res, str): if isinstance(res, str):
cache.set(cd_thread_key_val, {"err_msg": f'采集器返回数据错误-{res}'}) cache.set(cd_thread_key, {"err_msg": f'采集器返回数据错误-{res}'})
elif not res:
cache.set(cd_thread_key_val, {"err_msg": f"采集器返回数据为空-{str(res)}"})
else: else:
myLogger.info(f"采集器返回数据-{res}") cache.set(cd_thread_key, res)
cache.set(cd_thread_key_val, res)
time.sleep(0.3) time.sleep(0.3)
if sc: def get_tyy_data(*args, sleep=0):
try: if sleep > 0:
sc.close() time.sleep(sleep)
except Exception:
pass
def get_tyy_data_2(*args, retry=1):
host, port = args[0], int(args[1]) host, port = args[0], int(args[1])
cd_thread_key_id = f"cd_thread_{host}_{port}_id" cd_thread_key = f"cd_thread_{host}_{port}"
cd_thread_key_val = f"cd_thread_{host}_{port}_val" cd_thread_val = cache.get(cd_thread_key, default=None)
cd_thread_val_id = cache.get(cd_thread_key_id, default=None) if cd_thread_val is None:
if cd_thread_val_id is None: cache.set(cd_thread_key, "start")
tid = uuid.uuid4() cd_thread = MyThread(target=get_tyy_data_t, args=(host, port), daemon=True)
cache.set(cd_thread_key_id, tid, timeout=10800)
cd_thread = MyThread(target=get_tyy_data_t, args=(host, port, tid), daemon=True)
cd_thread.start() cd_thread.start()
cache.set(cd_thread_key_val, "get") cache.set(cd_thread_key, "get")
num = 0 num = 0
get_val = False get_val = False
@ -120,7 +107,7 @@ def get_tyy_data_2(*args, retry=1):
num += 1 num += 1
if num > 8: if num > 8:
break break
val = cache.get(cd_thread_key_val) val = cache.get(cd_thread_key)
if isinstance(val, dict): if isinstance(val, dict):
get_val = True get_val = True
if "err_msg" in val: if "err_msg" in val:
@ -128,125 +115,10 @@ def get_tyy_data_2(*args, retry=1):
return val return val
time.sleep(0.3) time.sleep(0.3)
if not get_val and retry > 0: if not get_val:
cache.set(cd_thread_key_id, None) cache.set(cd_thread_key, None)
get_tyy_data_2(*args, retry=retry-1) get_tyy_data(*args, sleep=2)
sc_all = {} if __name__ == '__main__':
sc_lock = threading.Lock() print(get_tyy_data())
def get_tyy_data_1(*args, retry=1):
host, port = args[0], int(args[1])
global sc_all
sc = None
def connect_and_send(retry=1):
nonlocal sc
sc = sc_all.get(f"{host}_{port}", None)
try:
if sc is None:
sc = socket.socket()
sc.settimeout(5) # 设置超时
sc.connect((host, port))
sc_all[f"{host}_{port}"] = sc
else:
# 清空接收缓冲区
sc.settimeout(0.1) # 设置短暂超时
for _ in range(5):
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5) # 恢复原超时设置
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
return connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError("采集器连接失败-管道重置")
except OSError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接失败-{str(e)}")
except TimeoutError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接超时-{str(e)}")
with sc_lock:
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
# myLogger.error(res)
if isinstance(res, str):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data_3(*args, retry=2):
host, port = args[0], int(args[1])
for attempt in range(retry):
try:
# 每次请求都新建连接(确保无共享状态)
with socket.create_connection((host, port), timeout=10) as sc:
sc.sendall(b"R")
# 接收完整响应(避免数据不完整)
# resp = b""
# while True:
# chunk = sc.recv(4096)
# if not chunk:
# break
# resp += chunk
resp = sc.recv(4096)
if not resp:
raise ParseError("设备未启动")
res = handle_bytes(resp)
if isinstance(res, str):
raise ParseError(f"采集器返回数据错误: {res}")
return res
except (socket.timeout, ConnectionError) as e:
if attempt == retry - 1: # 最后一次尝试失败才报错
raise ParseError(f"采集器连接失败: {str(e)}")
time.sleep(0.5) # 失败后等待 1s 再重试
except ParseError:
raise
except Exception as e:
raise ParseError(f"未知错误: {str(e)}")
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}")
res = r.json()
if "err_msg" in res:
raise ParseError(res["err_msg"])
return res

View File

@ -180,9 +180,7 @@ class CdView(MyLoggingMixin, APIView):
执行采集数据方法 执行采集数据方法
""" """
method = request.data.get("method", None) method = request.data.get("method")
if not method:
raise ParseError("请传入method参数")
m = method.split("(")[0] m = method.split("(")[0]
args = method.split("(")[1].split(")")[0].split(",") args = method.split("(")[1].split(")")[0].split(",")
module, func = m.rsplit(".", 1) module, func = m.rsplit(".", 1)

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-28 08:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('enm', '0058_mpoint_save_expr'),
]
operations = [
migrations.AlterField(
model_name='mpoint',
name='cal_related_mgroup_running',
field=models.PositiveSmallIntegerField(blank=True, choices=[(10, '不涉及'), (20, '运行时统计')], default=10, null=True, verbose_name='与工段运行状态的关联'),
),
]

View File

@ -62,7 +62,7 @@ class Mpoint(CommonBModel):
cal_coefficient = models.FloatField("计算系数", null=True, blank=True) cal_coefficient = models.FloatField("计算系数", null=True, blank=True)
mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True) mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True)
cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "不涉及"), (20, "运行时统计")], null=True, blank=True) cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "运行时统计")], null=True, blank=True)
@classmethod @classmethod
def cache_key(cls, code: str): def cache_key(cls, code: str):

View File

@ -86,10 +86,10 @@ def db_ins_mplogx():
if bill_date is None: if bill_date is None:
raise Exception("bill_date is None") raise Exception("bill_date is None")
query = """ query = """
SELECT id, de_real_quantity, inv_code, bill_date SELECT id, de_real_quantity, CONCAT('x', inv_name) AS inv_name, bill_date
FROM sa_weigh_view FROM sa_weigh_view
WHERE bill_date >= %s and de_real_quantity > 0 WHERE bill_date >= %s and de_real_quantity > 0
AND inv_code IN %s AND inv_name IN %s
ORDER BY bill_date ORDER BY bill_date
""" """
cursor.execute(query, (bill_date, tuple(batchs))) cursor.execute(query, (bill_date, tuple(batchs)))
@ -167,11 +167,11 @@ def get_first_stlog_time_from_duration(mgroup:Mgroup, dt_start:datetime, dt_end:
if st: if st:
return st, "ending" return st, "ending"
st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__gte=600).order_by("start_time").last() st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__lte=600).order_by("start_time").last()
if st: if st:
return st, "start" return st, "start"
st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__gte=600).order_by("end_time").first() st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__lte=600).order_by("end_time").first()
if st: if st:
return st, "end" return st, "end"
@ -213,7 +213,7 @@ def cal_mpointstat_hour(mpointId: str, year: int, month: int, day: int, hour: in
val = abs(first_val - last_val) val = abs(first_val - last_val)
else: else:
xtype = "normal" xtype = "normal"
if mpointfrom and mpoint.cal_related_mgroup_running == 20: if mpointfrom and mpoint.cal_related_mgroup_running == 10:
stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n) stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n)

View File

@ -1,19 +1,18 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from apps.enm.views import (MpointViewSet, MpointStatViewSet, from apps.enm.views import (MpointViewSet, MpLogxViewSet, MpointStatViewSet,
EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView) EnStatViewSet, EnStat2ViewSet, XscriptViewSet)
API_BASE_URL = 'api/enm/' API_BASE_URL = 'api/enm/'
HTML_BASE_URL = 'dhtml/enm/' HTML_BASE_URL = 'dhtml/enm/'
router = DefaultRouter() router = DefaultRouter()
router.register('mpoint', MpointViewSet, basename='mpoint') router.register('mpoint', MpointViewSet, basename='mpoint')
# router.register('mplogx', MpLogxViewSet, basename='mplogx') router.register('mplogx', MpLogxViewSet, basename='mplogx')
router.register('mpointstat', MpointStatViewSet, basename='mpointstat') router.register('mpointstat', MpointStatViewSet, basename='mpointstat')
router.register('enstat', EnStatViewSet, basename='enstat') router.register('enstat', EnStatViewSet, basename='enstat')
router.register('enstat2', EnStat2ViewSet, basename='enstat2') router.register('enstat2', EnStat2ViewSet, basename='enstat2')
router.register('xscript', XscriptViewSet, basename='xscript') router.register('xscript', XscriptViewSet, basename='xscript')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
path(f'{API_BASE_URL}mplogx/', MpLogxAPIView.as_view(), name='mplogx_list'),
] ]

View File

@ -12,7 +12,6 @@ from apps.enm.tasks import cal_mpointstat_manual
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.views import APIView
from apps.enm.tasks import cal_mpointstats_duration from apps.enm.tasks import cal_mpointstats_duration
from apps.enm.services import king_sync, MpointCache from apps.enm.services import king_sync, MpointCache
from django.db import transaction from django.db import transaction
@ -22,13 +21,7 @@ from apps.enm.services import get_analyse_data_mgroups_duration
from django.db.models import Sum from django.db.models import Sum
import logging import logging
from django.core.cache import cache from django.core.cache import cache
from apps.utils.sql import query_one_dict, query_all_dict
from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema
from django.utils import timezone
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
class MpointViewSet(CustomModelViewSet): class MpointViewSet(CustomModelViewSet):
""" """
list:测点 list:测点
@ -91,34 +84,6 @@ class MpointViewSet(CustomModelViewSet):
king_sync(getattr(settings, "KING_PROJECTNAME", "")) king_sync(getattr(settings, "KING_PROJECTNAME", ""))
return Response() return Response()
@action(methods=["post"], detail=False, perms_map={"post": "mpoint.create"}, serializer_class=Serializer)
def show_picture(self, request, *args, **kwargs):
import requests
import os
headers = {
"Content-Type": "application/json;charset=utf-8",
}
url = "http://localhost:8093/boxplot"
payload = {
"startTime1": request.data.get("startTime1"),
"endTime1": request.data.get("endTime1"),
"startTime2": request.data.get("startTime2"),
"endTime2": request.data.get("endTime2")
}
try:
response = requests.request("POST", url, json=payload, headers=headers)
except Exception as e:
myLogger.error(e)
pic_dir = os.path.join(settings.MEDIA_ROOT, "box_pic")
os.makedirs(pic_dir, exist_ok=True)
file_name= datetime.now().strftime('%Y%m%d_%H%M%S')+'.png'
pic_path = os.path.join(pic_dir, file_name)
with open(pic_path, 'wb') as f:
f.write(response.content)
rel_path = os.path.join('media/box_pic', file_name)
rel_path = rel_path.replace('\\', '/')
return Response({"rel_path": rel_path})
class XscriptViewSet(CustomModelViewSet): class XscriptViewSet(CustomModelViewSet):
""" """
@ -173,97 +138,6 @@ class XscriptViewSet(CustomModelViewSet):
# select_related_fields = ['mpoint'] # select_related_fields = ['mpoint']
# filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept'] # filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept']
class MpLogxAPIView(APIView):
"""
list:测点采集数据
测点采集数据
"""
perms_map = {"get": "*"}
@swagger_auto_schema(manual_parameters=[
openapi.Parameter('mpoint', openapi.IN_QUERY, description='测点ID', type=openapi.TYPE_STRING),
openapi.Parameter('timex__gte', openapi.IN_QUERY, description='开始时间', type=openapi.TYPE_STRING),
openapi.Parameter('timex__lte', openapi.IN_QUERY, description='结束时间', type=openapi.TYPE_STRING),
openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER),
openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段,如 -timex', type=openapi.TYPE_STRING),
openapi.Parameter('fields', openapi.IN_QUERY, description='返回字段,如 timex,val_float,val_int', type=openapi.TYPE_STRING),
])
def get(self, request, *args, **kwargs):
mpoint = request.query_params.get("mpoint", None)
timex__gte_str = request.query_params.get("timex__gte", None)
timex__lte_str = request.query_params.get("timex__lte", None)
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 20))
fields = request.query_params.get("fields", None)
if page < 0 and page_size < 0:
raise ParseError("page, page_size must be positive")
ordering = request.query_params.get("ordering", "-timex") # 默认倒序
if mpoint is None or timex__gte_str is None:
raise ParseError("mpoint, timex__gte are required")
# 处理时间
timex__gte = timezone.make_aware(datetime.strptime(timex__gte_str, "%Y-%m-%d %H:%M:%S"))
timex__lte = timezone.make_aware(datetime.strptime(timex__lte_str, "%Y-%m-%d %H:%M:%S")) if timex__lte_str else timezone.now()
# 统计总数
count_sql = """SELECT COUNT(*) AS total_count FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s"""
count_data = query_one_dict(count_sql, [mpoint, timex__gte, timex__lte], with_time_format=True)
# 排序白名单
allowed_fields = {"timex", "val_mrs", "val_int", "val_float"} # 根据表字段修改
order_fields = []
for field in ordering.split(","):
field = field.strip()
if not field:
continue
desc = field.startswith("-")
field_name = field[1:] if desc else field
if field_name in allowed_fields:
order_fields.append(f"{field_name} {'DESC' if desc else 'ASC'}")
# 如果没有合法字段,使用默认排序
if not order_fields:
order_fields = ["timex DESC"]
order_clause = "ORDER BY " + ", ".join(order_fields)
# 构造 SQL
if page == 0:
if fields:
# 过滤白名单,避免非法列
fields = [f for f in fields.split(",") if f in allowed_fields]
if not fields:
fields = ["timex", "val_float", "val_int"] # 默认列
select_clause = ", ".join(fields)
else:
select_clause = "timex, val_float, val_int" # 默认列
page_sql = f"""SELECT {select_clause} FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause}"""
page_params = [mpoint, timex__gte, timex__lte]
else:
page_sql = f"""SELECT * FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause} LIMIT %s OFFSET %s"""
page_params = [mpoint, timex__gte, timex__lte, page_size, (page-1)*page_size]
page_data = query_all_dict(page_sql, page_params, with_time_format=True)
if page == 0:
return Response(page_data)
return Response({
"count": count_data["total_count"],
"page": page,
"page_size": page_size,
"results": page_data
})
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet): class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
""" """

View File

@ -1,48 +0,0 @@
# Generated by Django 3.2.12 on 2025-05-21 05:59
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Conversation',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('title', models.CharField(default='新对话', max_length=200, verbose_name='对话标题')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversation_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='conversation_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Message',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('content', models.TextField(verbose_name='消息内容')),
('role', models.CharField(default='user', help_text='system/user', max_length=10, verbose_name='角色')),
('conversation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='ichat.conversation', verbose_name='对话')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,14 +0,0 @@
# 角色
你是一位数据分析专家和前端程序员,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述, 并形成报告。
# 技能
1. 仔细分析用户提供的JSON格式数据分析用户需求。
2. 依据得到的需求, 分别获取JSON数据中的关键信息。
3. 根据2中的关键信息最优化选择表格/饼图/柱状图/折线图等格式绘制报告。
# 回答要求
1. 仅生成完整的HTML代码所有功能都需要实现支持响应式不要输出任何解释或说明。
2. 代码中如需要Echarts等js库请直接使用中国大陆的CDN链接例如bootcdn的链接。
3. 标题为 数据分析报告。
3. 在开始部分请以表格形式简略展示获取的JSON数据。
4. 之后选择最合适的图表方式生成相应的图。
5. 在最后提供可下载该报告的完整PDF的按钮和功能。
6. 在最后提供可下载含有JSON数据的EXCEL文件的按钮和功能。

View File

@ -1,53 +0,0 @@
# 角色
你是一位资深的Postgresql数据库SQL专家具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述并生成准确可执行的SQL语句。
# 技能
1. 仔细分析用户提供的文本描述,明确用户需求。
2. 根据对用户需求的理解生成符合Postgresql数据库语法的准确可执行的SQL语句。
# 回答要求
1. 如果用户的询问未以 查询 开头,请直接回复 "请以 查询 开头,重新描述你的需求"。
2. 生成的SQL语句必须符合Postgresql数据库的语法规范。
3. 不要使用 Markerdown 和 SQL 语法格式输出,禁止添加语法标准、备注、说明等信息。
4. 直接输出符合Postgresql标准的SQL语句用txt纯文本格式展示即可。
5. 如果无法生成符合要求的SQL语句请直接回复 "无法生成"。
# 示例
1. 问:查询 外协白片抛 工段在2025年6月1日到2025年6月15日之间的生产合格数以及合格率等
select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '外协白片抛'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-15'
2. 问:查询 黑化 工段在2025年6月的生产合格数以及合格率等
答: select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '黑化'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
3. 问:查询 各工段 在2025年6月的生产合格数以及合格率等
答: select
mgroup.name as 工段,
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
group by mgroup.id
order by mgroup.sort

View File

@ -2,7 +2,6 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from apps.ichat.views import QueryLLMviewSet, ConversationViewSet from apps.ichat.views import QueryLLMviewSet, ConversationViewSet
from apps.ichat.views2 import WorkChain
API_BASE_URL = 'api/ichat/' API_BASE_URL = 'api/ichat/'
@ -12,5 +11,4 @@ router.register('conversation', ConversationViewSet, basename='conversation')
router.register('message', QueryLLMviewSet, basename='message') router.register('message', QueryLLMviewSet, basename='message')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain')
] ]

View File

@ -1,129 +0,0 @@
import requests
import os
from apps.utils.sql import execute_raw_sql
import json
from apps.utils.tools import MyJSONEncoder
from .utils import is_safe_sql
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from django.conf import settings
from apps.utils.mixins import MyLoggingMixin
from django.core.cache import cache
import uuid
from apps.utils.thread import MyThread
LLM_URL = getattr(settings, "LLM_URL", "")
API_KEY = getattr(settings, "LLM_API_KEY", "")
MODEL = "qwen14b"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
def load_promot(name):
with open(os.path.join(CUR_DIR, f'promot/{name}.md'), 'r') as f:
return f.read()
def ask(input:str, p_name:str, stream=False):
his = [{"role":"system", "content": load_promot(p_name)}]
his.append({"role":"user", "content": input})
payload = {
"model": MODEL,
"messages": his,
"temperature": 0,
"stream": stream
}
response = requests.post(LLM_URL, headers=HEADERS, json=payload, stream=stream)
if not stream:
return response.json()["choices"][0]["message"]["content"]
else:
# 处理流式响应
full_content = ""
for chunk in response.iter_lines():
if chunk:
# 通常流式响应是SSE格式data: {...}
decoded_chunk = chunk.decode('utf-8')
if decoded_chunk.startswith("data:"):
json_str = decoded_chunk[5:].strip()
if json_str == "[DONE]":
break
try:
chunk_data = json.loads(json_str)
if "choices" in chunk_data and chunk_data["choices"]:
delta = chunk_data["choices"][0].get("delta", {})
if "content" in delta:
print(delta["content"])
full_content += delta["content"]
except json.JSONDecodeError:
continue
return full_content
def work_chain(input:str, t_key:str):
pdict = {"state": "progress", "steps": [{"state":"ok", "msg":"正在生成查询语句"}]}
cache.set(t_key, pdict)
res_text = ask(input, 'w_sql')
if res_text == '请以 查询 开头,重新描述你的需求':
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":res_text})
cache.set(t_key, pdict)
return
else:
pdict["steps"].append({"state":"ok", "msg":"查询语句生成成功", "content":res_text})
cache.set(t_key, pdict)
if not is_safe_sql(res_text):
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":"当前查询存在风险,请重新描述你的需求"})
cache.set(t_key, pdict)
return
pdict["steps"].append({"state":"ok", "msg":"正在执行查询语句"})
cache.set(t_key, pdict)
res = execute_raw_sql(res_text)
pdict["steps"].append({"state":"ok", "msg":"查询语句执行成功", "content":res})
cache.set(t_key, pdict)
pdict["steps"].append({"state":"ok", "msg":"正在生成报告"})
cache.set(t_key, pdict)
res2 = ask(json.dumps(res, cls=MyJSONEncoder, ensure_ascii=False), 'w_ana')
content = res2.lstrip('```html ').rstrip('```')
pdict["state"] = "done"
pdict["content"] = content
pdict["steps"].append({"state":"ok", "msg":"报告生成成功", "content": content})
cache.set(t_key, pdict)
return
class InputSerializer(serializers.Serializer):
input = serializers.CharField(label="查询需求")
class WorkChain(MyLoggingMixin, APIView):
@swagger_auto_schema(
operation_summary="提交查询需求",
request_body=InputSerializer)
def post(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
input = request.data.get('input')
t_key = f'ichat_{uuid.uuid4()}'
MyThread(target=work_chain, args=(input, t_key)).start()
return Response({'ichat_tid': t_key})
@swagger_auto_schema(
operation_summary="获取查询进度")
def get(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
ichat_tid = request.GET.get('ichat_tid')
if ichat_tid:
return Response(cache.get(ichat_tid))
if __name__ == "__main__":
print(work_chain("查询 一次超洗 工段在2025年6月的生产合格数等并形成报告"))
from apps.ichat.views2 import work_chain
print(work_chain('查询外观检验工段在2025年6月的生产合格数等并形成报告'))

View File

@ -39,7 +39,8 @@ def correct_mb_count_notok():
count_notok = mi.count_n_zw + mi.count_n_tw + mi.count_n_qp + mi.count_n_wq + mi.count_n_dl + mi.count_n_pb + mi.count_n_dxt + mi.count_n_js + mi.count_n_qx + mi.count_n_zz + mi.count_n_ysq + mi.count_n_hs + mi.count_n_b + mi.count_n_qt count_notok = mi.count_n_zw + mi.count_n_tw + mi.count_n_qp + mi.count_n_wq + mi.count_n_dl + mi.count_n_pb + mi.count_n_dxt + mi.count_n_js + mi.count_n_qx + mi.count_n_zz + mi.count_n_ysq + mi.count_n_hs + mi.count_n_b + mi.count_n_qt
# 先处理库存 # 先处理库存
try: try:
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok) with transaction.atomic():
InmService.update_mb_after_test(mi) MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
InmService.update_mb_after_test(mi)
except ParseError as e: except ParseError as e:
MIOItem.objects.filter(id=mi.id).update(test_date=None) MIOItem.objects.filter(id=mi.id).update(test_date=None)

View File

@ -37,9 +37,7 @@ class MioFilter(filters.FilterSet):
"item_mio__test_user": ["isnull"], "item_mio__test_user": ["isnull"],
"item_mio__w_mioitem__number": ["exact"], "item_mio__w_mioitem__number": ["exact"],
"mgroup": ["exact"], "mgroup": ["exact"],
"item_mio__batch": ["exact"], "item_mio__batch": ["exact"]
"inout_date": ["gte", "lte", "exact"],
"belong_dept": ["exact"]
} }
def filter_materials__type(self, queryset, name, value): def filter_materials__type(self, queryset, name, value):

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-06-19 02:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0030_auto_20250523_0922'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='unit_price',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=14, null=True, verbose_name='单价'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-23 08:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0031_mioitem_unit_price'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='note',
field=models.TextField(blank=True, null=True, verbose_name='备注'),
),
migrations.AlterField(
model_name='mio',
name='type',
field=models.CharField(choices=[('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('other_in', '其他入库'), ('other_out', '其他出库')], default='do_out', help_text="(('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('other_in', '其他入库'), ('other_out', '其他出库'))", max_length=10, verbose_name='出入库类型'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-28 05:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0032_auto_20250723_1639'),
]
operations = [
migrations.AlterField(
model_name='mio',
name='type',
field=models.CharField(choices=[('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('borrow_out', '领用出库'), ('return_in', '退还入库'), ('other_in', '其他入库'), ('other_out', '其他出库')], default='do_out', help_text="(('do_out', '生产领料'), ('sale_out', '销售发货'), ('pur_in', '采购入库'), ('pur_out', '采购退货'), ('do_in', '生产入库'), ('borrow_out', '领用出库'), ('return_in', '退还入库'), ('other_in', '其他入库'), ('other_out', '其他出库'))", max_length=10, verbose_name='出入库类型'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-28 08:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0033_alter_mio_type'),
]
operations = [
migrations.AddField(
model_name='mioitemw',
name='number_out',
field=models.TextField(blank=True, null=True, verbose_name='对外编号'),
),
]

View File

@ -1,34 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-31 06:04
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('inm', '0034_mioitemw_number_out'),
]
operations = [
migrations.CreateModel(
name='Pack',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('index', models.PositiveSmallIntegerField(default=1, verbose_name='序号')),
('mio', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pack_mio', to='inm.mio', verbose_name='关联出入库记录')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='mioitem',
name='pack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mioitem_pack', to='inm.pack', verbose_name='关联装箱单'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-08-01 06:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0035_auto_20250731_1404'),
]
operations = [
migrations.AddField(
model_name='mioitem',
name='pack_index',
field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='装箱序号'),
),
]

View File

@ -5,7 +5,7 @@ from apps.sam.models import Customer, Order
from apps.mtm.models import Material, Mgroup from apps.mtm.models import Material, Mgroup
from apps.system.models import User from apps.system.models import User
from datetime import datetime from datetime import datetime
from django.db.models import Max, Sum from django.db.models import Max
# Create your models here. # Create your models here.
@ -39,10 +39,6 @@ class MaterialBatch(BaseModel):
defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True) defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True)
@property
def count_mioing(self):
return MIOItem.objects.filter(mb=self, mio__submit_time__isnull=True).aggregate(count=Sum('count'))['count'] or 0
class MaterialBatchA(BaseModel): class MaterialBatchA(BaseModel):
""" """
TN:组合件物料批次 TN:组合件物料批次
@ -56,15 +52,12 @@ class MaterialBatchA(BaseModel):
MIO_TYPE_PREFIX = { MIO_TYPE_PREFIX = {
'do_in': 'SCRK', # 生产入库 'do_out': 'SCLL', # 生产领料 (Shēngchǎn Lǐngliào)
'do_out': 'SCLL', # 生产领料 'sale_out': 'XSFH', # 销售发货 (Xiāoshòu Fāhuò)
'sale_out': 'XSFH', # 销售发货 'pur_in': 'CGRK', # 采购入库 (Cǎigòu Rùkù)
'pur_in': 'CGRK', # 采购入库 'do_in': 'SCRK', # 生产入库 (Shēngchǎn Rùkù)
'pur_out': 'CGTH', # 采购退货 'other_in': 'QTRK', # 其他入库 (Qítā Rùkù)
'borrow_out': 'LYCK', # 领用出库 'other_out': 'QTCK' # 其他出库 (Qítā Chūkù)
'return_in': 'THRK', # 退还入库
'other_in': 'QTRK', # 其他入库
'other_out': 'QTCK' # 其他出库
} }
class MIO(CommonBDModel): class MIO(CommonBDModel):
@ -74,21 +67,15 @@ class MIO(CommonBDModel):
MIO_TYPE_DO_OUT = 'do_out' MIO_TYPE_DO_OUT = 'do_out'
MIO_TYPE_SALE_OUT = 'sale_out' MIO_TYPE_SALE_OUT = 'sale_out'
MIO_TYPE_PUR_IN = 'pur_in' MIO_TYPE_PUR_IN = 'pur_in'
MIO_TYPE_PUR_OUT = 'pur_out'
MIO_TYPE_DO_IN = 'do_in' MIO_TYPE_DO_IN = 'do_in'
MIO_TYPE_OTHER_IN = 'other_in' MIO_TYPE_OTHER_IN = 'other_in'
MIO_TYPE_OTHER_OUT = 'other_out' MIO_TYPE_OTHER_OUT = 'other_out'
MIO_TYPE_BORROW_OUT = 'borrow_out'
MIO_TYPE_RETURN_IN = 'return_in'
MIO_TYPES = ( MIO_TYPES = (
(MIO_TYPE_DO_OUT, '生产领料'), (MIO_TYPE_DO_OUT, '生产领料'),
(MIO_TYPE_SALE_OUT, '销售发货'), (MIO_TYPE_SALE_OUT, '销售发货'),
(MIO_TYPE_PUR_IN, '采购入库'), (MIO_TYPE_PUR_IN, '采购入库'),
(MIO_TYPE_PUR_OUT, '采购退货'),
(MIO_TYPE_DO_IN, '生产入库'), (MIO_TYPE_DO_IN, '生产入库'),
(MIO_TYPE_BORROW_OUT, '领用出库'),
(MIO_TYPE_RETURN_IN, '退还入库'),
(MIO_TYPE_OTHER_IN, '其他入库'), (MIO_TYPE_OTHER_IN, '其他入库'),
(MIO_TYPE_OTHER_OUT, '其他出库') (MIO_TYPE_OTHER_OUT, '其他出库')
) )
@ -137,13 +124,6 @@ class MIO(CommonBDModel):
last_number = 1 last_number = 1
return f"{prefix}-{today_str}-{last_number:04d}" return f"{prefix}-{today_str}-{last_number:04d}"
class Pack(BaseModel):
"""
TN:装箱单
"""
index = models.PositiveSmallIntegerField('序号', default=1)
mio = models.ForeignKey(MIO, verbose_name='关联出入库记录', on_delete=models.CASCADE, related_name='pack_mio')
class MIOItem(BaseModel): class MIOItem(BaseModel):
""" """
TN:出入库明细 TN:出入库明细
@ -159,7 +139,6 @@ class MIOItem(BaseModel):
material = models.ForeignKey( material = models.ForeignKey(
Material, verbose_name='物料', on_delete=models.CASCADE) Material, verbose_name='物料', on_delete=models.CASCADE)
batch = models.TextField('批次号', db_index=True) batch = models.TextField('批次号', db_index=True)
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
count = models.DecimalField('出入数量', max_digits=12, decimal_places=3) count = models.DecimalField('出入数量', max_digits=12, decimal_places=3)
count_tested = models.PositiveIntegerField('已检数', null=True, blank=True) count_tested = models.PositiveIntegerField('已检数', null=True, blank=True)
test_date = models.DateField('检验日期', null=True, blank=True) test_date = models.DateField('检验日期', null=True, blank=True)
@ -188,11 +167,6 @@ class MIOItem(BaseModel):
count_n_qt = models.PositiveIntegerField('其他', default=0) count_n_qt = models.PositiveIntegerField('其他', default=0)
is_testok = models.BooleanField('检验是否合格', null=True, blank=True) is_testok = models.BooleanField('检验是否合格', null=True, blank=True)
note = models.TextField('备注', null=True, blank=True)
pack_index = models.PositiveSmallIntegerField('装箱序号', null=True, blank=True)
# 以下字段暂时不用
pack = models.ForeignKey(Pack, verbose_name='关联装箱单', on_delete=models.SET_NULL, related_name='mioitem_pack', null=True, blank=True)
@classmethod @classmethod
def count_fields(cls): def count_fields(cls):
@ -227,11 +201,10 @@ class MIOItemw(BaseModel):
TN:单件记录 TN:单件记录
""" """
number = models.TextField('编号') number = models.TextField('编号')
number_out = models.TextField('对外编号', null=True, blank=True)
wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw' wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw'
, null=True, blank=True) , null=True, blank=True)
mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem') mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem')
ftest = models.ForeignKey("qm.ftest", verbose_name='关联检验记录', on_delete=models.PROTECT, ftest = models.ForeignKey("qm.ftest", verbose_name='关联检验记录', on_delete=models.PROTECT,
related_name='mioitemw_ftest', null=True, blank=True) related_name='mioitemw_ftest', null=True, blank=True)
note = models.TextField('备注', null=True, blank=True) note = models.TextField('备注', null=True, blank=True)

View File

@ -8,11 +8,10 @@ from apps.system.models import Dept, User
from apps.utils.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS from apps.utils.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS
from apps.utils.serializers import CustomModelSerializer from apps.utils.serializers import CustomModelSerializer
from apps.mtm.models import Material from apps.mtm.models import Material
from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw, Pack from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw
from django.db import transaction from django.db import transaction
from server.settings import get_sysconfig from server.settings import get_sysconfig
from apps.wpmw.models import Wpr from apps.wpmw.models import Wpr
from decimal import Decimal
class WareHourseSerializer(CustomModelSerializer): class WareHourseSerializer(CustomModelSerializer):
@ -30,15 +29,6 @@ class MaterialBatchAListSerializer(CustomModelSerializer):
fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_'] fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_']
class MaterialBatchAListSerializer2(CustomModelSerializer):
material_name = serializers.StringRelatedField(
source='material', read_only=True)
class Meta:
model = MaterialBatchA
fields = ['material', 'batch', 'rate', 'mb',
'id', 'material_name']
class MaterialBatchSerializer(CustomModelSerializer): class MaterialBatchSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField( warehouse_name = serializers.CharField(
source='warehouse.name', read_only=True) source='warehouse.name', read_only=True)
@ -48,18 +38,11 @@ class MaterialBatchSerializer(CustomModelSerializer):
source='supplier', read_only=True) source='supplier', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True) material_ = MaterialSerializer(source='material', read_only=True)
defect_name = serializers.CharField(source="defect.name", read_only=True) defect_name = serializers.CharField(source="defect.name", read_only=True)
count_mioing = serializers.IntegerField(read_only=True, label='正在出入库数量')
class Meta: class Meta:
model = MaterialBatch model = MaterialBatch
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS_BASE read_only_fields = EXCLUDE_FIELDS_BASE
def to_representation(self, instance):
ret = super().to_representation(instance)
if 'count' in ret and 'count_mioing' in ret:
ret['count_canmio'] = str(Decimal(ret['count']) - Decimal(ret['count_mioing']))
return ret
class MaterialBatchDetailSerializer(CustomModelSerializer): class MaterialBatchDetailSerializer(CustomModelSerializer):
@ -126,15 +109,14 @@ class MIOItemCreateSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MIOItem model = MIOItem
fields = ['mio', 'warehouse', 'material', fields = ['mio', 'warehouse', 'material',
'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price', 'note', "pack_index"] 'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm']
extra_kwargs = { extra_kwargs = {
'mio': {'required': True}, 'warehouse': {'required': False}, 'mio': {'required': True}, 'warehouse': {'required': False},
'material': {'required': False}, 'batch': {'required': False, "allow_null": True, "allow_blank": True}} 'material': {'required': False}, 'batch': {'required': False}}
def create(self, validated_data): def create(self, validated_data):
mio:MIO = validated_data['mio'] mio:MIO = validated_data['mio']
mio_type = mio.type
mb = validated_data.get('mb', None) mb = validated_data.get('mb', None)
wm = validated_data.get('wm', None) wm = validated_data.get('wm', None)
assemb = validated_data.pop('assemb', []) assemb = validated_data.pop('assemb', [])
@ -153,14 +135,9 @@ class MIOItemCreateSerializer(CustomModelSerializer):
validated_data["batch"] = wm.batch validated_data["batch"] = wm.batch
material: Material = validated_data['material'] material: Material = validated_data['material']
batch = validated_data.get("batch", None) batch = validated_data['batch']
if not batch:
batch = ""
if material.is_hidden: if material.is_hidden:
raise ParseError('隐式物料不可出入库') raise ParseError('隐式物料不可出入库')
if mio.type in [MIO.MIO_TYPE_RETURN_IN, MIO.MIO_TYPE_BORROW_OUT]:
if not material.into_wm:
raise ParseError('该物料不可领用或归还')
if mio.state != MIO.MIO_CREATE: if mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可新增') raise ParseError('出入库记录非创建中不可新增')
@ -171,59 +148,50 @@ class MIOItemCreateSerializer(CustomModelSerializer):
mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN]) mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN])
if mis.exists() and (not mis.exclude(test_date=None).exists()): if mis.exists() and (not mis.exclude(test_date=None).exists()):
raise ParseError('该批次的物料未经检验') raise ParseError('该批次的物料未经检验')
with transaction.atomic():
count = validated_data["count"] count = validated_data["count"]
batch = validated_data["batch"] batch = validated_data["batch"]
mioitemw = validated_data.pop('mioitemw', []) mioitemw = validated_data.pop('mioitemw', [])
instance:MIOItem = super().create(validated_data) instance = super().create(validated_data)
assemb_dict = {} assemb_dict = {}
for i in assemb: for i in assemb:
assemb_dict[i['material'].id] = i assemb_dict[i['material'].id] = i
if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级 if material.is_assemb and '_in' in mio.type: # 仅入库且是组合件的时候需要填写下一级
components = material.components components = material.components
for k, v in components.items(): for k, v in components.items():
if k in assemb_dict: if k in assemb_dict:
mia = assemb_dict[k] mia = assemb_dict[k]
MIOItemA.objects.create( MIOItemA.objects.create(
mioitem=instance, rate=v, **mia) mioitem=instance, rate=v, **mia)
else:
raise ParseError('缺少组合件')
if material.tracking == Material.MA_TRACKING_SINGLE:
if len(mioitemw) == 0:
if mb:
wpr_qs = Wpr.get_qs_by_mb(mb)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else: else:
raise ParseError('请提供产品明细编号') raise ParseError('缺少组合件')
elif wm: if material.tracking == Material.MA_TRACKING_SINGLE:
wpr_qs = Wpr.get_qs_by_wm(wm) if len(mioitemw) == 0:
if wpr_qs.count() == validated_data["count"]: if mb:
for item in wpr_qs: wpr_qs = Wpr.get_qs_by_mb(mb)
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item) if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('请提供产品明细编号')
elif wm:
wpr_qs = Wpr.get_qs_by_wm(wm)
if wpr_qs.count() == validated_data["count"]:
for item in wpr_qs:
MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item)
else:
raise ParseError('请提供产品明细编号')
elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1:
MIOItemw.objects.create(mioitem=instance, number=batch)
else: else:
raise ParseError('请提供产品明细编号') raise ParseError('不支持自动生成请提供产品明细')
elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1: elif len(mioitemw) >= 1:
MIOItemw.objects.create(mioitem=instance, number=batch) mio_type = mio.type
else: for item in mioitemw:
raise ParseError('不支持自动生成请提供产品明细') if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
elif len(mioitemw) >= 1: raise ParseError(f'{item["number"]}_请提供产品明细ID')
mio_type = mio.type else:
if mio_type != "pur_in" and mio_type != "other_in": MIOItemw.objects.create(mioitem=instance, **item)
wprIds = [i["wpr"].id for i in mioitemw]
mb_ids = list(Wpr.objects.filter(id__in=wprIds).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == instance.mb.id:
pass
else:
raise ParseError(f'{batch}物料明细中存在{len(mb_ids)}个不同物料批次')
for item in mioitemw:
if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
raise ParseError(f'{item["number"]}_请提供产品明细ID')
elif item.get("number_out", None) is not None and mio_type != MIO.MIO_TYPE_SALE_OUT:
raise ParseError(f'{item["number"]}_非销售出库不可赋予产品对外编号')
else:
MIOItemw.objects.create(mioitem=instance, **item)
return instance return instance
@ -234,14 +202,16 @@ class MIOItemAListSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MIOItemA model = MIOItemA
fields = "__all__" fields = ['material', 'batch', 'rate', 'mioitem',
read_only_fields = EXCLUDE_FIELDS_BASE 'id', 'material_', 'material_name']
class MIOItemSerializer(CustomModelSerializer): class MIOItemSerializer(CustomModelSerializer):
warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) warehouse_name = serializers.CharField(
source='warehouse.name', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True) material_ = MaterialSerializer(source='material', read_only=True)
assemb = serializers.SerializerMethodField(label="组合件信息") assemb = MIOItemAListSerializer(
source='a_mioitem', read_only=True, many=True)
material_name = serializers.StringRelatedField( material_name = serializers.StringRelatedField(
source='material', read_only=True) source='material', read_only=True)
inout_date = serializers.DateField(source='mio.inout_date', read_only=True) inout_date = serializers.DateField(source='mio.inout_date', read_only=True)
@ -252,24 +222,6 @@ class MIOItemSerializer(CustomModelSerializer):
model = MIOItem model = MIOItem
fields = '__all__' fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
ret["price"] = None
if ret["unit_price"] is not None:
ret["price"] = Decimal(ret["count"]) * Decimal(ret["unit_price"])
return ret
def get_assemb(self, obj):
qs = MIOItemA.objects.filter(mioitem=obj)
if qs.exists():
return MIOItemAListSerializer(qs, many=True).data
elif obj.mb and obj.mb.material.is_assemb:
return MaterialBatchAListSerializer2(MaterialBatchA.objects.filter(mb=obj.mb), many=True).data
return None
class MioItemDetailSerializer(MIOItemSerializer):
mio_ = MIOListSerializer(source='mio', read_only=True)
class MIODoSerializer(CustomModelSerializer): class MIODoSerializer(CustomModelSerializer):
@ -283,11 +235,8 @@ class MIODoSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MIO model = MIO
fields = ['id', 'number', 'note', 'do_user', fields = ['id', 'number', 'note', 'do_user',
'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user', 'type'] 'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user']
extra_kwargs = {'inout_date': {'required': True}, extra_kwargs = {'inout_date': {'required': True}, 'do_user': {'required': True}, 'number': {"required": False, "allow_blank": True}}
'do_user': {'required': True},
'number': {"required": False, "allow_blank": True},
'type': {'required': True}}
def validate(self, attrs): def validate(self, attrs):
if 'mgroup' in attrs and attrs['mgroup']: if 'mgroup' in attrs and attrs['mgroup']:
@ -297,13 +246,10 @@ class MIODoSerializer(CustomModelSerializer):
return attrs return attrs
def create(self, validated_data): def create(self, validated_data):
type = validated_data['type']
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]:
pass
else:
raise ValidationError('出入库类型错误')
if not validated_data.get("number", None): if not validated_data.get("number", None):
validated_data["number"] = MIO.get_a_number(validated_data["type"]) validated_data["number"] = MIO.get_a_number(validated_data["type"])
if validated_data['type'] not in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_DO_IN]:
raise ValidationError('出入库类型错误')
return super().create(validated_data) return super().create(validated_data)
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -348,17 +294,11 @@ class MIOPurSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MIO model = MIO
fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user', 'type'] fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user']
extra_kwargs = {'inout_date': {'required': True}, extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}}
'number': {"required": False, "allow_blank": True},
'type': {'required': True}}
def create(self, validated_data): def create(self, validated_data):
type = validated_data["type"] validated_data['type'] = MIO.MIO_TYPE_PUR_IN
if type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]:
pass
else:
raise ValidationError('出入库类型错误')
if not validated_data.get("number", None): if not validated_data.get("number", None):
validated_data["number"] = MIO.get_a_number(validated_data["type"]) validated_data["number"] = MIO.get_a_number(validated_data["type"])
pu_order: PuOrder = validated_data.get('pu_order', None) pu_order: PuOrder = validated_data.get('pu_order', None)
@ -443,23 +383,3 @@ class MIOItemPurInTestSerializer(CustomModelSerializer):
attrs['weight_kgs'] = [float(i) for i in weight_kgs] attrs['weight_kgs'] = [float(i) for i in weight_kgs]
attrs['count_sampling'] = len(attrs['weight_kgs']) attrs['count_sampling'] = len(attrs['weight_kgs'])
return super().validate(attrs) return super().validate(attrs)
class PackSerializer(CustomModelSerializer):
class Meta:
model = Pack
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
def create(self, validated_data):
index = validated_data["index"]
mio = validated_data["mio"]
if Pack.objects.filter(mio=mio, index=index).exists():
raise ParseError('包装箱已存在')
return super().create(validated_data)
class PackMioSerializer(serializers.Serializer):
mioitems = serializers.ListField(child=serializers.CharField(), label="明细ID")
pack_index = serializers.IntegerField(label="包装箱序号")
# pack = serializers.CharField(label="包装箱ID")

View File

@ -5,15 +5,13 @@ from django.db import transaction
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.wpmw.models import Wpr from apps.wpmw.models import Wpr
from apps.mtm.models import Material from apps.mtm.models import Material
from rest_framework import serializers
class MIOItemwCreateUpdateSerializer(CustomModelSerializer): class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
ftest = FtestProcessSerializer(required=False) ftest = FtestProcessSerializer(required=False)
wpr_number_out = serializers.CharField(source="wpr.number_out", read_only=True)
class Meta: class Meta:
model = MIOItemw model = MIOItemw
fields = ["id", "number", "wpr", "note", "mioitem", "ftest", "wpr_number_out"] fields = ["id", "number", "wpr", "note", "mioitem", "ftest"]
def validate(self, attrs): def validate(self, attrs):
mioitem: MIOItem = attrs["mioitem"] mioitem: MIOItem = attrs["mioitem"]
@ -45,6 +43,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
ftest_sr.update(instance=ftest, validated_data=ftest_data) ftest_sr.update(instance=ftest, validated_data=ftest_data)
return mioitemw return mioitemw
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
wpr: Wpr = validated_data.get("wpr", None) wpr: Wpr = validated_data.get("wpr", None)
if wpr: if wpr:
@ -57,6 +56,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
mioitemw = self.save_ftest(mioitemw, ftest_data) mioitemw = self.save_ftest(mioitemw, ftest_data)
return mioitemw return mioitemw
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop("mioitem") validated_data.pop("mioitem")
ftest_data = validated_data.pop("ftest", None) ftest_data = validated_data.pop("ftest", None)

View File

@ -10,7 +10,7 @@ from apps.wpmw.models import Wpr
from apps.qm.models import Ftest, Defect from apps.qm.models import Ftest, Defect
from django.db.models import Count, Q from django.db.models import Count, Q
def do_out(item: MIOItem, is_reverse: bool = False): def do_out(item: MIOItem):
""" """
生产领料到车间 生产领料到车间
""" """
@ -23,6 +23,8 @@ def do_out(item: MIOItem, is_reverse: bool = False):
mgroup = mio.mgroup mgroup = mio.mgroup
do_user = mio.do_user do_user = mio.do_user
material:Material = item.material material:Material = item.material
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
return
# 获取defect # 获取defect
defect:Defect = None defect:Defect = None
@ -92,36 +94,29 @@ def do_out(item: MIOItem, is_reverse: bool = False):
raise ParseError(f"批次错误!{e}") raise ParseError(f"批次错误!{e}")
mb.count = mb.count - xcount mb.count = mb.count - xcount
if mb.count < 0: if mb.count < 0:
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
else: else:
mb.save() mb.save()
if material.into_wm:
# 领到车间库存(或工段) # 领到车间库存(或工段)
wm, new_create = WMaterial.objects.get_or_create( wm, new_create = WMaterial.objects.get_or_create(
batch=xbatch, material=xmaterial, batch=xbatch, material=xmaterial,
belong_dept=belong_dept, mgroup=mgroup, belong_dept=belong_dept, mgroup=mgroup,
state=WMaterial.WM_OK, defect=defect) state=WMaterial.WM_OK, defect=defect)
if new_create: if new_create:
wm.create_by = do_user wm.create_by = do_user
wm.batch_ofrom = mb.batch if mb else None wm.batch_ofrom = mb.batch if mb else None
wm.material_ofrom = mb.material if mb else None wm.material_ofrom = mb.material if mb else None
wm.count = wm.count + item.count wm.count = wm.count + item.count
wm.update_by = do_user wm.update_by = do_user
wm.save() wm.save()
# 开始变动wpr # 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE: if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item) mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count: if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败") raise ParseError("出入库与明细数量不一致,操作失败")
mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == mb.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次')
for mioitemw in mioitemws: for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb) Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
@ -141,7 +136,8 @@ def do_in(item: MIOItem):
mgroup = mio.mgroup mgroup = mio.mgroup
do_user = mio.do_user do_user = mio.do_user
material = item.material material = item.material
if material.into_wm is False: # 根据配置不进行入车间库存的处理
return
action_list = [] action_list = []
mias = MIOItemA.objects.filter(mioitem=item) mias = MIOItemA.objects.filter(mioitem=item)
is_zhj = False # 是否组合件入仓库 is_zhj = False # 是否组合件入仓库
@ -176,39 +172,38 @@ def do_in(item: MIOItem):
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
xbatchs.append(xbatch) xbatchs.append(xbatch)
if material.into_wm:
wm_qs = WMaterial.objects.filter( wm_qs = WMaterial.objects.filter(
batch=xbatch, batch=xbatch,
material=xmaterial, material=xmaterial,
belong_dept=belong_dept, belong_dept=belong_dept,
mgroup=mgroup, mgroup=mgroup,
defect=defect, defect=defect,
state=WMaterial.WM_OK) state=WMaterial.WM_OK)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
wm = wm_qs.first() wm = wm_qs.first()
elif count_x == 0: elif count_x == 0:
raise ParseError( raise ParseError(
f'{str(xmaterial)}-{xbatch}-批次库存不存在!') f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
else: else:
raise ParseError( raise ParseError(
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!') f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 增加mb # 增加mb
if not is_zhj: if not is_zhj:
mb, _ = MaterialBatch.objects.get_or_create( mb, _ = MaterialBatch.objects.get_or_create(
@ -231,16 +226,9 @@ def do_in(item: MIOItem):
# 开始变动wpr # 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE: if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item) mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count: if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败") raise ParseError("出入库与明细数量不一致,操作失败")
wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次')
for mioitemw in mioitemws: for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm) Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
@ -283,9 +271,6 @@ class InmService:
""" """
更新库存, 支持反向操作 更新库存, 支持反向操作
""" """
if not MIOItem.objects.filter(mio=instance).exists():
raise ParseError("出入库记录缺失明细,无法操作")
if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单 if instance.type == MIO.MIO_TYPE_PUR_IN: # 需要更新订单
# 这里还需要对入厂检验进行处理 # 这里还需要对入厂检验进行处理
if is_reverse: if is_reverse:
@ -293,32 +278,25 @@ class InmService:
else: else:
for item in MIOItem.objects.filter(mio=instance): for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create( BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material) batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
from apps.pum.services import PumService from apps.pum.services import PumService
if is_reverse: if is_reverse:
cls.update_mb(instance, -1) cls.update_mb(instance, -1)
else: else:
cls.update_mb(instance, 1) cls.update_mb(instance, 1)
PumService.mio_pur(instance, is_reverse) PumService.mio_purin(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_PUR_OUT:
from apps.pum.services import PumService
if is_reverse:
cls.update_mb(instance, 1)
else:
cls.update_mb(instance, -1)
PumService.mio_pur(instance, is_reverse)
elif instance.type == MIO.MIO_TYPE_OTHER_IN: elif instance.type == MIO.MIO_TYPE_OTHER_IN:
if is_reverse: if is_reverse:
BatchLog.clear(mio=instance) BatchLog.clear(mio=instance)
else: else:
for item in MIOItem.objects.filter(mio=instance): for item in MIOItem.objects.filter(mio=instance):
BatchSt.g_create( BatchSt.g_create(
batch=item.batch, mio=instance, material_start=item.material) batch=item.batch, mio=instance, material_start=item.material, reuse_node=True)
if is_reverse: if is_reverse:
cls.update_mb(instance, -1) cls.update_mb(instance, -1)
else: else:
cls.update_mb(instance, 1) cls.update_mb(instance, 1)
elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]: elif instance.type == MIO.MIO_TYPE_DO_IN:
mioitems = MIOItem.objects.filter(mio=instance) mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse: if is_reverse:
for item in mioitems: for item in mioitems:
@ -326,14 +304,6 @@ class InmService:
else: else:
for item in mioitems: for item in mioitems:
do_in(item) do_in(item)
elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
else:
for item in mioitems:
do_out(item)
elif instance.type == MIO.MIO_TYPE_SALE_OUT: elif instance.type == MIO.MIO_TYPE_SALE_OUT:
from apps.sam.services import SamService from apps.sam.services import SamService
if is_reverse: if is_reverse:
@ -346,6 +316,14 @@ class InmService:
cls.update_mb(instance, 1) cls.update_mb(instance, 1)
else: else:
cls.update_mb(instance, -1) cls.update_mb(instance, -1)
elif instance.type == MIO.MIO_TYPE_DO_OUT:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
do_in(item)
else:
for item in mioitems:
do_out(item)
else: else:
raise ParseError('不支持该出入库操作') raise ParseError('不支持该出入库操作')
@ -371,7 +349,6 @@ class InmService:
out = -1 out = -1
默认使用count字段做加减 默认使用count字段做加减
""" """
mio_type = i.mio.type
material: Material = i.material material: Material = i.material
warehouse = i.warehouse warehouse = i.warehouse
tracking = material.tracking tracking = material.tracking
@ -410,7 +387,7 @@ class InmService:
if change_count < 0: if change_count < 0:
raise ParseError("存在负数!") raise ParseError("存在负数!")
state = WMaterial.WM_OK state = WMaterial.WM_OK
if defect and defect.okcate in [Defect.DEFECT_NOTOK]: if defect:
state = WMaterial.WM_NOTOK state = WMaterial.WM_NOTOK
mb, _ = MaterialBatch.objects.get_or_create( mb, _ = MaterialBatch.objects.get_or_create(
material=material, material=material,
@ -442,7 +419,7 @@ class InmService:
elif in_or_out == -1: elif in_or_out == -1:
mb.count = mb.count - change_count mb.count = mb.count - change_count
if mb.count < 0: if mb.count < 0:
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
else: else:
mb.save() mb.save()
if tracking == Material.MA_TRACKING_SINGLE: if tracking == Material.MA_TRACKING_SINGLE:
@ -453,7 +430,7 @@ class InmService:
if mioitemws.count() != change_count: if mioitemws.count() != change_count:
raise ParseError("出入库与明细数量不一致,操作失败") raise ParseError("出入库与明细数量不一致,操作失败")
for mioitemw in mioitemws: for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out) Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb)
else: else:
raise ParseError("不支持的操作") raise ParseError("不支持的操作")
@ -461,26 +438,3 @@ class InmService:
ana_batch_thread(xbatchs) ana_batch_thread(xbatchs)
@classmethod
def revert_and_del(cls, mioitem: MIOItem):
mio = mioitem.mio
if mio.submit_time is None:
raise ParseError("未提交的出入库明细不允许撤销")
if mioitem.test_date is not None:
raise ParseError("已检验的出入库明细不允许撤销")
if mio.type == MIO.MIO_TYPE_PUR_IN:
from apps.pum.services import PumService
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_OTHER_IN:
cls.update_mb_item(mioitem, -1)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
elif mio.type == MIO.MIO_TYPE_DO_OUT:
do_in(mioitem)
BatchLog.clear(mioitem=mioitem)
mioitem.delete()
else:
raise ParseError("不支持该出入库单明细撤销")

View File

@ -1,6 +1,6 @@
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.mtm.models import Process, Material from apps.mtm.models import Process, Material
from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw
from apps.utils.tools import ranstr from apps.utils.tools import ranstr
from apps.mtm.services_2 import cal_material_count from apps.mtm.services_2 import cal_material_count
@ -67,7 +67,7 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
from openpyxl import load_workbook from openpyxl import load_workbook
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
qct = Qct.get(mioitem.material, tag="inm", type="in") qct = Qct.get(mioitem.material, tag="inm")
if qct is None: if qct is None:
raise ParseError("未找到检验表") raise ParseError("未找到检验表")
@ -84,11 +84,8 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
test_user = mioitem.mio.mio_user test_user = mioitem.mio.mio_user
test_date = mioitem.mio.inout_date test_date = mioitem.mio.inout_date
wb = load_workbook(path, data_only=True) wb = load_workbook(path)
if "Sheet1" in wb.sheetnames: # 检查是否存在 sheet = wb["Sheet1"]
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number") mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
@ -123,39 +120,4 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem):
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id())) ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
FtestItem.objects.bulk_create(ftestitems) FtestItem.objects.bulk_create(ftestitems)
else: else:
break break
def daoru_mioitems(path:str, mio:MIO):
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path, data_only=True)
if "Sheet1" in wb.sheetnames: # 检查是否存在
sheet = wb["Sheet1"] # 获取工作表
else:
raise ParseError("未找到Sheet1")
mioitems = []
ind = 2
while sheet[f"a{ind}"].value:
batch = sheet[f"b{ind}"].value
material_number = sheet[f"a{ind}"].value
try:
material = Material.objects.get(number=material_number)
except Exception as e:
raise ParseError(f"未找到物料:{material_number} {e}")
if batch:
pass
else:
batch = ""
count = sheet[f"c{ind}"].value
warehouse_name = sheet[f"d{ind}"].value
try:
warehouse = WareHouse.objects.get(name=warehouse_name)
except Exception as e:
raise ParseError(f"未找到仓库:{warehouse_name} {e}")
mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id()))
ind = ind + 1
MIOItem.objects.bulk_create(mioitems)

View File

@ -19,7 +19,6 @@ router.register('mio/pur', MioPurViewSet)
router.register('mio/other', MioOtherViewSet) router.register('mio/other', MioOtherViewSet)
router.register('mioitem', MIOItemViewSet, basename='mioitem') router.register('mioitem', MIOItemViewSet, basename='mioitem')
router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') router.register('mioitemw', MIOItemwViewSet, basename='mioitemw')
# router.register('pack', PackViewSet, basename='pack')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
] ]

View File

@ -9,25 +9,22 @@ from django.utils import timezone
from rest_framework.response import Response from rest_framework.response import Response
from django.db.models import Sum from django.db.models import Sum
from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw
from apps.inm.serializers import ( from apps.inm.serializers import (
MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer, MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer,
MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer, MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,
MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer, MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer,
MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer) MIOItemwSerializer)
from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.inm.services import InmService from apps.inm.services import InmService
from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test
from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin, from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin,
CustomListModelMixin) CustomListModelMixin)
from apps.utils.permission import has_perm from apps.utils.permission import has_perm
from .filters import MaterialBatchFilter, MioFilter from .filters import MaterialBatchFilter, MioFilter
from apps.qm.serializers import FtestProcessSerializer from apps.qm.serializers import FtestProcessSerializer
from apps.mtm.models import Material from apps.mtm.models import Material
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django.db import connection
# Create your views here. # Create your views here.
@ -148,19 +145,9 @@ class MIOViewSet(CustomModelViewSet):
serializer_class = MIOListSerializer serializer_class = MIOListSerializer
retrieve_serializer_class = MIODetailSerializer retrieve_serializer_class = MIODetailSerializer
filterset_class = MioFilter filterset_class = MioFilter
search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model', search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model']
'item_mio__a_mioitem__batch']
data_filter = True data_filter = True
@classmethod
def lock_and_check_can_update(cls, mio:MIO):
if not connection.in_atomic_block:
raise ParseError("请在事务中调用该方法")
mio:MIO = MIO.objects.select_for_update().get(id=mio.id)
if mio.submit_time is not None:
raise ParseError("该记录已提交无法更改")
return mio
def add_info_for_list(self, data): def add_info_for_list(self, data):
# 获取检验状态 # 获取检验状态
mio_dict = {} mio_dict = {}
@ -181,7 +168,7 @@ class MIOViewSet(CustomModelViewSet):
if self.action in ['create', 'update', 'partial_update']: if self.action in ['create', 'update', 'partial_update']:
type = self.request.data.get('type') type = self.request.data.get('type')
user = self.request.user user = self.request.user
if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]: if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT]:
if not has_perm(user, ['mio.do']): if not has_perm(user, ['mio.do']):
raise PermissionDenied raise PermissionDenied
return MIODoSerializer return MIODoSerializer
@ -193,7 +180,7 @@ class MIOViewSet(CustomModelViewSet):
if not has_perm(user, ['mio.sale']): if not has_perm(user, ['mio.sale']):
raise PermissionDenied raise PermissionDenied
return MIOSaleSerializer return MIOSaleSerializer
elif type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]: elif type == MIO.MIO_TYPE_PUR_IN:
if not has_perm(user, ['mio.pur']): if not has_perm(user, ['mio.pur']):
raise PermissionDenied raise PermissionDenied
return MIOPurSerializer return MIOPurSerializer
@ -205,29 +192,27 @@ class MIOViewSet(CustomModelViewSet):
return super().perform_destroy(instance) return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):
"""提交 """提交
提交 提交
""" """
ins:MIO = self.get_object() ins = self.get_object()
if ins.inout_date is None: if ins.inout_date is None:
raise ParseError('出入库日期未填写') raise ParseError('出入库日期未填写')
if ins.state != MIO.MIO_CREATE: if ins.state != MIO.MIO_CREATE:
raise ParseError('记录状态异常') raise ParseError('记录状态异常')
now = timezone.now() with transaction.atomic():
ins.submit_user = request.user ins.submit_time = timezone.now()
ins.submit_time = now ins.state = MIO.MIO_SUBMITED
ins.update_by = request.user ins.submit_user = request.user
ins.state = MIO.MIO_SUBMITED ins.update_by = request.user
ins.save() ins.save()
InmService.update_inm(ins) InmService.update_inm(ins)
InmService.update_material_count(ins) InmService.update_material_count(ins)
return Response(MIOListSerializer(instance=ins).data) return Response(MIOListSerializer(instance=ins).data)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert(self, request, *args, **kwargs): def revert(self, request, *args, **kwargs):
"""撤回 """撤回
@ -239,84 +224,16 @@ class MIOViewSet(CustomModelViewSet):
raise ParseError('记录状态异常') raise ParseError('记录状态异常')
if ins.submit_user != user: if ins.submit_user != user:
raise ParseError('非提交人不可撤回') raise ParseError('非提交人不可撤回')
ins.submit_user = None with transaction.atomic():
ins.update_by = user ins.submit_time = None
ins.state = MIO.MIO_CREATE ins.state = MIO.MIO_CREATE
ins.submit_time = None ins.update_by = user
ins.save() ins.save()
InmService.update_inm(ins, is_reverse=True) InmService.update_inm(ins, is_reverse=True)
InmService.update_material_count(ins) InmService.update_material_count(ins)
return Response() return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
mio:MIO = self.get_object()
if mio.submit_time is not None:
raise ParseError('该出入库已提交不可装箱')
sr = PackMioSerializer(data=request.data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
pack_index = vdata["pack_index"]
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != mio:
raise ParseError('存在明细不属于该箱')
mioitem.pack_index = pack_index
mioitem.save(update_fields=['pack_index', 'update_time'])
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
def daoru_mioitem(self, request, *args, **kwargs):
"""导入明细
导入明细
"""
daoru_mioitems(settings.BASE_DIR + request.data.get('path', ''), mio=self.get_object())
return Response()
class PackViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
"""
list: 装箱记录
装箱记录
"""
perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = Pack.objects.all()
serializer_class = PackSerializer
filterset_fields = ["mio"]
ordering = ["mio", "index"]
@action(methods=['post'], detail=False, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer)
@transaction.atomic
def pack_mioitem(self, request, *args, **kwargs):
"""装箱
装箱
"""
vdata = PackMioSerializer(data=request.data)
packId = vdata["pack"]
pack:Pack = Pack.objects.get(id=packId)
mioitems = vdata["mioitems"]
if not mioitems:
raise ParseError('未选择明细')
for id in mioitems:
mioitem = MIOItem.objects.get(id=id)
if mioitem.mio != pack.mio:
raise ParseError('存在明细不属于该装箱记录')
mioitem.pack = pack
mioitem.save(update_fields=['pack', 'update_time'])
return Response()
class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
""" """
list: 出入库明细 list: 出入库明细
@ -326,7 +243,6 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
perms_map = {'get': '*', 'post': '*', 'delete': '*'} perms_map = {'get': '*', 'post': '*', 'delete': '*'}
queryset = MIOItem.objects.all() queryset = MIOItem.objects.all()
serializer_class = MIOItemSerializer serializer_class = MIOItemSerializer
retrieve_serializer_class = MioItemDetailSerializer
create_serializer_class = MIOItemCreateSerializer create_serializer_class = MIOItemCreateSerializer
select_related_fields = ['warehouse', 'mio', 'material', 'test_user'] select_related_fields = ['warehouse', 'mio', 'material', 'test_user']
filterset_fields = { filterset_fields = {
@ -336,58 +252,22 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
"mio__type": ["exact", "in"], "mio__type": ["exact", "in"],
"mio__inout_date": ["gte", "lte", "exact"], "mio__inout_date": ["gte", "lte", "exact"],
"material": ["exact"], "material": ["exact"],
"material__type": ["exact"],
"test_date": ["isnull", "exact"] "test_date": ["isnull", "exact"]
} }
ordering = ['create_time'] ordering = ['create_time']
ordering_fields = ['create_time', 'test_date'] ordering_fields = ['create_time', 'test_date']
search_fields =['batch', 'a_mioitem__batch']
def add_info_for_list(self, data): def add_info_for_list(self, data):
with_mio = self.request.query_params.get('with_mio', "no")
if with_mio == "yes" and isinstance(data, list):
mio_ids = [item['mio'] for item in data]
mio_qs = MIO.objects.filter(id__in=mio_ids)
mio_qs_= MIOListSerializer(mio_qs, many=True).data
mio_dict = {mio['id']: mio for mio in mio_qs_}
for item in data:
mioId = item['mio']
item['mio_'] = mio_dict[mioId]
return data return data
@swagger_auto_schema(manual_parameters=[
openapi.Parameter(name="with_mio", in_=openapi.IN_QUERY, description="是否返回出入库记录信息",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="query", in_=openapi.IN_QUERY, description="定制返回数据",
type=openapi.TYPE_STRING, required=False),
openapi.Parameter(name="with_children", in_=openapi.IN_QUERY, description="带有children(yes/no/count)",
type=openapi.TYPE_STRING, required=False)
])
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
return super().perform_create(serializer)
def perform_destroy(self, instance): def perform_destroy(self, instance):
MIOViewSet.lock_and_check_can_update(instance.mio) if instance.mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可删除')
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user: if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
raise PermissionDenied('无权限删除') raise PermissionDenied('无权限删除')
return super().perform_destroy(instance) return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert_and_del(self, request, *args, **kwargs):
"""撤回并删除
撤回并删除
"""
ins:MIOItem = self.get_object()
InmService.revert_and_del(ins)
return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer) @action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer)
@transaction.atomic @transaction.atomic
def test(self, request, *args, **kwargs): def test(self, request, *args, **kwargs):
@ -487,7 +367,7 @@ class MIOItemwViewSet(CustomModelViewSet):
perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'} perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'}
queryset = MIOItemw.objects.all() queryset = MIOItemw.objects.all()
serializer_class = MIOItemwCreateUpdateSerializer serializer_class = MIOItemwCreateUpdateSerializer
filterset_fields = ['mioitem', 'wpr'] filterset_fields = ['mioitem']
ordering = ["number", "create_time"] ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time"] ordering_fields = ["number", "create_time"]
@ -503,20 +383,20 @@ class MIOItemwViewSet(CustomModelViewSet):
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count() mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
mioitem.save() mioitem.save()
@transaction.atomic
def perform_create(self, serializer): def perform_create(self, serializer):
MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio) ins: MIOItemw = serializer.save()
ins:MIOItemw = serializer.save() mioitem: MIOItem = ins.mioitem
self.cal_mioitem_count(ins.mioitem) self.cal_mioitem_count(mioitem)
@transaction.atomic
def perform_update(self, serializer): def perform_update(self, serializer):
ins:MIOItemw = serializer.instance mioitemw = serializer.save()
MIOViewSet.lock_and_check_can_update(ins.mioitem.mio) self.cal_mioitem_count(mioitemw.mioitem)
ins:MIOItemw = serializer.save()
self.cal_mioitem_count(ins.mioitem)
@transaction.atomic
def perform_destroy(self, instance: MIOItemw): def perform_destroy(self, instance: MIOItemw):
mioitem = instance.mioitem mioitem = instance.mioitem
MIOViewSet.lock_and_check_can_update(mioitem.mio)
ftest = instance.ftest ftest = instance.ftest
instance.delete() instance.delete()
if ftest: if ftest:

View File

@ -7,7 +7,6 @@ from celery import shared_task
from django.utils import timezone from django.utils import timezone
from django.conf import settings from django.conf import settings
import os import os
from apps.utils.sql import execute_raw_sql
@shared_task(base=CustomTask) @shared_task(base=CustomTask)
@ -34,44 +33,3 @@ def clear_dbbackup(num: int=7):
for f in files_remove_list: for f in files_remove_list:
filepath = os.path.join(backpath, f) filepath = os.path.join(backpath, f)
os.remove(filepath) os.remove(filepath)
@shared_task(base=CustomTask)
def clean_timescaledb_job_his(num: int = 30):
execute_raw_sql(f"""
DO $$
DECLARE
batch_size INTEGER := 100000; -- 每次删除的行数
deleted_count INTEGER := 0;
BEGIN
IF EXISTS (
SELECT 1
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = '_timescaledb_internal'
AND c.relname = 'bgw_job_stat_history'
) THEN
RAISE NOTICE 'Start cleaning _timescaledb_internal.bgw_job_stat_history ...';
LOOP
WITH del AS (
SELECT ctid
FROM _timescaledb_internal.bgw_job_stat_history
WHERE execution_start < NOW() - INTERVAL '{num} days'
LIMIT batch_size
)
DELETE FROM _timescaledb_internal.bgw_job_stat_history
WHERE ctid IN (SELECT ctid FROM del);
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RAISE NOTICE 'Deleted % rows...', deleted_count;
EXIT WHEN deleted_count = 0;
PERFORM pg_sleep(0.1); -- 稍微休息避免压力过大
END LOOP;
RAISE NOTICE '✅ Data cleanup complete. Run VACUUM FULL manually.';
ELSE
RAISE NOTICE 'Table _timescaledb_internal.bgw_job_stat_history not found.';
END IF;
END$$;
""", timeout=None)

View File

@ -1,7 +1,6 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from apps.mtm.models import Goal, Material, Route, RoutePack from apps.mtm.models import Goal, Material, Route
from django.db.models.expressions import F from django.db.models.expressions import F
from rest_framework.exceptions import ParseError
class MaterialFilter(filters.FilterSet): class MaterialFilter(filters.FilterSet):
@ -46,8 +45,6 @@ class GoalFilter(filters.FilterSet):
class RouteFilter(filters.FilterSet): class RouteFilter(filters.FilterSet):
nprocess_name = filters.CharFilter(method='filter_nprocess_name', label="nprocess_name")
material_in_has = filters.CharFilter(method='filter_material_in_has', label="material_in_has ID")
class Meta: class Meta:
model = Route model = Route
fields = { fields = {
@ -61,18 +58,5 @@ class RouteFilter(filters.FilterSet):
"mgroup": ["exact", "in", "isnull"], "mgroup": ["exact", "in", "isnull"],
"mgroup__name": ["exact", "contains"], "mgroup__name": ["exact", "contains"],
"mgroup__belong_dept": ["exact"], "mgroup__belong_dept": ["exact"],
"mgroup__belong_dept__name": ["exact", "contains"], "mgroup__belong_dept__name": ["exact", "contains"]
"from_route": ["exact", "isnull"],
} }
def filter_nprocess_name(self, queryset, name, value):
return queryset
def filter_material_in_has(self, queryset, name, value):
nprocess_name = self.data.get('nprocess_name', None)
if nprocess_name:
routepack_qs = queryset.filter(material_in__id=value, routepack__isnull=False, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True)
qs = queryset.filter(routepack__in=routepack_qs, process__name=nprocess_name)
return qs
raise ParseError("nprocess_name is required")

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-06-18 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0058_process_wpr_number_rule'),
]
operations = [
migrations.AddField(
model_name='material',
name='bin_number_main',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='主库位号'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-08-07 02:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0059_material_bin_number_main'),
]
operations = [
migrations.AddField(
model_name='route',
name='params_json',
field=models.JSONField(blank=True, default=dict, verbose_name='工艺参数'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-08-21 09:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0060_route_params_json'),
]
operations = [
migrations.AddField(
model_name='material',
name='img',
field=models.TextField(blank=True, null=True, verbose_name='图片'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-02 03:22
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mtm', '0061_material_img'),
]
operations = [
migrations.AddField(
model_name='route',
name='from_route',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='route_f', to='mtm.route', verbose_name='来源路线'),
),
migrations.AlterField(
model_name='route',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='route_parent', to='mtm.route', verbose_name='上级路线'),
),
]

View File

@ -4,8 +4,6 @@ from rest_framework.exceptions import ParseError
from apps.utils.models import CommonBDModel from apps.utils.models import CommonBDModel
from collections import defaultdict, deque from collections import defaultdict, deque
from django.db.models import Q from django.db.models import Q
from datetime import datetime, timedelta
from django.utils import timezone
class Process(CommonBModel): class Process(CommonBModel):
""" """
@ -44,12 +42,6 @@ class Process(CommonBModel):
"""获取可产出的materialIds """获取可产出的materialIds
""" """
return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct()) return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct())
def get_canin_mat_ids(self):
"""获取可输入的materialIds
"""
return list(RouteMat.objects.filter(route__process=self).values_list("material__id", flat=True).distinct()) + \
list(Route.objects.filter(process=self).values_list("material_in__id", flat=True).distinct())
# Create your models here. # Create your models here.
class Material(CommonAModel): class Material(CommonAModel):
@ -109,8 +101,6 @@ class Material(CommonAModel):
brothers = models.JSONField('兄弟件', default=list, null=False, blank=True) brothers = models.JSONField('兄弟件', default=list, null=False, blank=True)
unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True) unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True)
into_wm = models.BooleanField('是否进入车间库存', default=True) into_wm = models.BooleanField('是否进入车间库存', default=True)
bin_number_main = models.CharField('主库位号', max_length=50, null=True, blank=True)
img = models.TextField('图片', null=True, blank=True)
class Meta: class Meta:
verbose_name = '物料表' verbose_name = '物料表'
@ -179,32 +169,6 @@ class Mgroup(CommonBModel):
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
def get_shift(self, w_s_time:datetime):
# 如果没有时区信息,使用默认时区(东八区)
if not timezone.is_aware(w_s_time):
w_s_time = timezone.make_aware(w_s_time)
else:
w_s_time = timezone.localtime(w_s_time)
shifts = Shift.objects.filter(rule=self.shift_rule).order_by('sort')
if not shifts:
raise ParseError(f"工段{self.name}未配置班次")
# 处理跨天班次的情况
for shift in shifts:
# 如果开始时间小于结束时间,表示班次在同一天内
if shift.start_time_o < shift.end_time_o:
if shift.start_time_o <= w_s_time.time() < shift.end_time_o:
return w_s_time.date(), shift
else: # 班次跨天(如夜班从当天晚上到次日凌晨)
if w_s_time.time() >= shift.start_time_o or w_s_time.time() < shift.end_time_o:
# 如果当前时间在开始时间之后,属于当天
if w_s_time.time() >= shift.start_time_o:
return w_s_time.date(), shift
# 如果当前时间在结束时间之前,属于前一天
else:
return (w_s_time - timedelta(days=1)).date(), shift
# return w_s_time.date(), None
class TeamMember(BaseModel): class TeamMember(BaseModel):
@ -360,8 +324,7 @@ class RoutePack(CommonADModel):
route_dict[r.id] = { route_dict[r.id] = {
"label": r.process.name if r.process else "", "label": r.process.name if r.process else "",
"source": r.material_in.id, "source": r.material_in.id,
"target": r.material_out.id, "target": r.material_out.id
"id": r.id
} }
# 获取所有物料信息 # 获取所有物料信息
@ -417,9 +380,7 @@ class Route(CommonADModel):
batch_bind = models.BooleanField('是否绑定批次', default=True) batch_bind = models.BooleanField('是否绑定批次', default=True)
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials", materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
through="mtm.routemat", blank=True) through="mtm.routemat", blank=True)
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent") parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
params_json = models.JSONField('工艺参数', default=dict, blank=True)
from_route = models.ForeignKey('self', verbose_name='来源路线', on_delete=models.SET_NULL, null=True, blank=True, related_name="route_f")
def __str__(self): def __str__(self):
x = "" x = ""
@ -549,7 +510,6 @@ class Route(CommonADModel):
'source': source, 'source': source,
'target': target, 'target': target,
'label': rq.process.name, 'label': rq.process.name,
'id': rq.id
}) })
# 将批次号排序 # 将批次号排序
nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time") nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time")

View File

@ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Material model = Material
fields = ['id', 'name', 'number', 'model', fields = ['id', 'name', 'number', 'model',
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking", "bin_number_main"] 'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking"]
def get_full_name(self, obj): def get_full_name(self, obj):
return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}' return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}'
@ -246,34 +246,30 @@ class RouteSerializer(CustomModelSerializer):
# material = validated_data.get('material', None) # material = validated_data.get('material', None)
# if material and process and Route.objects.filter(material=material, process=process).exists(): # if material and process and Route.objects.filter(material=material, process=process).exists():
# raise ValidationError('已选择该工序!!') # raise ValidationError('已选择该工序!!')
with transaction.atomic():
instance:Route = super().create(validated_data) instance = super().create(validated_data)
material_out = instance.material_out material_out = instance.material_out
if material_out: if material_out:
if material_out.process is None: if material_out.process is None:
material_out.process = process material_out.process = process
if material_out_tracking != material_out.tracking: if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改") raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material: if material_out.parent is None and instance.material:
material_out.parent = instance.material material_out.parent = instance.material
material_out.save() material_out.save()
# elif material_out.process != process: # elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择') # raise ParseError('物料工序错误!请重新选择')
else: else:
if instance.material: if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save() instance.save()
rx = Route.objects.filter( rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
material_in=instance.material_in, material_out=instance.material_out, if rx:
process=process).exclude(id=instance.id).order_by("create_time").first() msg = ""
if rx: if rx.routepack:
instance.from_route = rx msg = rx.routepack.name
instance.save() raise ParseError(f"该工艺步骤已存在-{msg}")
# msg = "" return instance
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop('material', None) validated_data.pop('material', None)
@ -281,34 +277,30 @@ class RouteSerializer(CustomModelSerializer):
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH) material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH)
if material_out_tracking is None: if material_out_tracking is None:
material_out_tracking = Material.MA_TRACKING_BATCH material_out_tracking = Material.MA_TRACKING_BATCH
with transaction.atomic():
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
material_out = instance.material_out material_out = instance.material_out
if material_out: if material_out:
if material_out.process is None: if material_out.process is None:
material_out.process = process material_out.process = process
if material_out_tracking != material_out.tracking: if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改") raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material: if material_out.parent is None and instance.material:
material_out.parent = instance.material material_out.parent = instance.material
material_out.save() material_out.save()
# elif material_out.process != process: # elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择') # raise ParseError('物料工序错误!请重新选择')
else: else:
if instance.material: if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save() instance.save()
rx = Route.objects.filter( rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
material_in=instance.material_in, material_out=instance.material_out, if rx:
process=process).exclude(id=instance.id).order_by("create_time").first() msg = ""
if rx: if rx.routepack:
instance.from_route = rx msg = rx.routepack.name
instance.save() raise ParseError(f"该工艺步骤已存在-{msg}")
# msg = "" return instance
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def to_representation(self, instance): def to_representation(self, instance):
res = super().to_representation(instance) res = super().to_representation(instance)
@ -337,16 +329,4 @@ class RouteMatSerializer(CustomModelSerializer):
class Meta: class Meta:
model = RouteMat model = RouteMat
fields = "__all__" fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE read_only_fields = EXCLUDE_FIELDS_BASE
def validate(self, attrs):
route:Route = attrs["route"]
if route.from_route is not None:
raise ParseError("该工艺步骤引用其他步骤,无法修改")
return attrs
class MaterialExportSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]

View File

@ -51,35 +51,33 @@ def daoru_material(path: str):
'辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70} '辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70}
from apps.utils.snowflake import idWorker from apps.utils.snowflake import idWorker
from openpyxl import load_workbook from openpyxl import load_workbook
wb = load_workbook(path, read_only=True) wb = load_workbook(path)
sheet = wb.active sheet = wb['物料']
process_l = Process.objects.all() process_l = Process.objects.all()
process_d = {p.name: p for p in process_l} process_d = {p.name: p for p in process_l}
i = 3 i = 3
if sheet['a2'].value != '物料编号': if sheet['a2'].value != '物料编号':
raise ParseError('列错误导入失败') raise ParseError('列错误导入失败')
while sheet[f'b{i}'].value is not None or sheet[f'd{i}'].value is not None: while sheet[f'b{i}'].value is not None:
type_str = sheet[f'b{i}'].value.replace(' ', '') type_str = sheet[f'b{i}'].value.replace(' ', '')
try: try:
type = type_dict[type_str] type = type_dict[type_str]
cate = sheet[f'c{i}'].value.replace(' ', '') if sheet[f'c{i}'].value else ""
number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None
if sheet[f'd{i}'].value: if sheet[f'c{i}'].value:
name = str(sheet[f'd{i}'].value).replace(' ', '') name = str(sheet[f'c{i}'].value).replace(' ', '')
else: else:
raise ParseError(f'{i}行物料信息错误: 物料名称必填') raise ParseError(f'{i}行物料信息错误: 物料名称必填')
specification = str(sheet[f'e{i}'].value).replace( specification = str(sheet[f'd{i}'].value).replace(
'×', '*').replace(' ', '') if sheet[f'e{i}'].value else None '×', '*').replace(' ', '') if sheet[f'd{i}'].value else None
model = str(sheet[f'f{i}'].value).replace(' ', '') if sheet[f'f{i}'].value else None model = str(sheet[f'e{i}'].value).replace(' ', '') if sheet[f'e{i}'].value else None
unit = sheet[f'g{i}'].value.replace(' ', '') unit = sheet[f'f{i}'].value.replace(' ', '')
count_safe = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None count_safe = float(sheet[f'h{i}'].value) if sheet[f'h{i}'].value else None
unit_price = float(sheet[f'j{i}'].value) if sheet[f'j{i}'].value else None unit_price = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None
bin_number_main = sheet[f'k{i}'].value.replace(' ', '') if sheet[f'k{i}'].value else None
except Exception as e: except Exception as e:
raise ParseError(f'{i}行物料信息错误: {e}') raise ParseError(f'{i}行物料信息错误: {e}')
if type in [20, 30]: if type in [20, 30]:
try: try:
process = process_d[sheet[f'h{i}'].value.replace(' ', '')] process = process_d[sheet[f'g{i}'].value.replace(' ', '')]
except Exception as e: except Exception as e:
raise ParseError(f'{i}行物料信息错误: {e}') raise ParseError(f'{i}行物料信息错误: {e}')
try: try:
@ -89,7 +87,7 @@ def daoru_material(path: str):
filters['process'] = process filters['process'] = process
default = {'type': type, 'name': name, 'specification': specification, default = {'type': type, 'name': name, 'specification': specification,
'model': model, 'unit': unit, 'number': number if number else f'm{type}_{ranstr(6)}', 'id': idWorker.get_id(), 'model': model, 'unit': unit, 'number': number if number else f'm{type}_{ranstr(6)}', 'id': idWorker.get_id(),
'count_safe': count_safe, 'unit_price': unit_price, 'cate': cate, 'bin_number_main': bin_number_main} 'count_safe': count_safe, 'unit_price': unit_price}
material, is_created = Material.objects.get_or_create( material, is_created = Material.objects.get_or_create(
**filters, defaults=default) **filters, defaults=default)
if not is_created: if not is_created:
@ -155,12 +153,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
raise ParseError('缺少步骤') raise ParseError('缺少步骤')
r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
first_route = r_qs.first() first_route = r_qs.first()
last_route = r_qs.last()
if first_route.batch_bind: if first_route.batch_bind:
first_route.batch_bind = False first_route.batch_bind = False
first_route.save(update_fields=['batch_bind']) first_route.save(update_fields=['batch_bind'])
# last_route = r_qs.last() if last_route.material_out != routepack.material:
# if last_route.material_out != routepack.material: raise ParseError('最后一步产出与工艺包不一致')
# raise ParseError('最后一步产出与工艺包不一致')
ticket_data = ticket.ticket_data ticket_data = ticket.ticket_data
ticket_data.update({ ticket_data.update({
't_model': 'routepack', 't_model': 'routepack',
@ -171,8 +169,8 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
ticket.save() ticket.save()
if routepack.ticket is None: if routepack.ticket is None:
routepack.ticket = ticket routepack.ticket = ticket
routepack.state = RoutePack.RP_S_AUDIT routepack.state = RoutePack.RP_S_AUDIT
routepack.save() routepack.save()
def routepack_audit_end(ticket: Ticket): def routepack_audit_end(ticket: Ticket):
@ -182,7 +180,7 @@ def routepack_audit_end(ticket: Ticket):
def routepack_ticket_change(ticket: Ticket): def routepack_ticket_change(ticket: Ticket):
routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id']) routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id'])
if ticket.act_state in [Ticket.TICKET_ACT_STATE_DRAFT, Ticket.TICKET_ACT_STATE_BACK, Ticket.TICKET_ACT_STATE_RETREAT]: if ticket.act_state == Ticket.TICKET_ACT_STATE_DRAFT:
routepack.state = RoutePack.RP_S_CREATE routepack.state = RoutePack.RP_S_CREATE
routepack.save() routepack.save()

View File

@ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer,
MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer, MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer,
ShiftSerializer, TeamSerializer, ProcessSerializer, ShiftSerializer, TeamSerializer, ProcessSerializer,
RouteSerializer, TeamMemberSerializer, RoutePackSerializer, RouteSerializer, TeamMemberSerializer, RoutePackSerializer,
SruleSerializer, RouteMatSerializer, RoutePackCopySerializer, MaterialExportSerializer) SruleSerializer, RouteMatSerializer, RoutePackCopySerializer)
from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin
@ -21,8 +21,6 @@ from django.db.models import Q
from apps.wf.models import Ticket from apps.wf.models import Ticket
from django.utils import timezone from django.utils import timezone
from rest_framework.permissions import IsAdminUser from rest_framework.permissions import IsAdminUser
from apps.utils.export import export_excel
from operator import itemgetter
# Create your views here. # Create your views here.
class MaterialViewSet(CustomModelViewSet): class MaterialViewSet(CustomModelViewSet):
@ -34,13 +32,14 @@ class MaterialViewSet(CustomModelViewSet):
queryset = Material.objects.all() queryset = Material.objects.all()
serializer_class = MaterialSerializer serializer_class = MaterialSerializer
filterset_class = MaterialFilter filterset_class = MaterialFilter
search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main'] search_fields = ['name', 'code', 'number', 'specification', 'model']
select_related_fields = ['process'] select_related_fields = ['process']
ordering = ['name', 'model', 'specification', ordering = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number'] 'type', 'process', 'process__sort', 'sort', 'id', 'number']
ordering_fields = ['name', 'model', 'specification', ordering_fields = ['name', 'model', 'specification',
'type', 'process', 'process__sort', 'sort', 'id', 'number', 'create_time'] 'type', 'process', 'process__sort', 'sort', 'id', 'number']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
from apps.inm.models import MaterialBatch from apps.inm.models import MaterialBatch
if MaterialBatch.objects.filter(material=instance).exists(): if MaterialBatch.objects.filter(material=instance).exists():
@ -89,23 +88,6 @@ class MaterialViewSet(CustomModelViewSet):
def cates(self, request, *args, **kwargs): def cates(self, request, *args, **kwargs):
res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct() res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct()
return Response(set(res)) return Response(set(res))
@action(methods=['get'], detail=False, perms_map={'get': '*'})
def export_excel(self, request, pk=None):
"""导出excel
导出excel
"""
field_data = ['大类', '物料编号', '名称', '规格', '型号', '计量单位', '仓库位号', "安全库存", "单价"]
queryset = self.filter_queryset(self.get_queryset())
if queryset.count() > 1000:
raise ParseError('数据量超过1000,请筛选后导出')
odata = MaterialExportSerializer(queryset, many=True).data
# 处理数据
field_keys = ['cate', 'number', 'name', 'specification', 'model', 'unit',
'bin_number_main', 'count_safe', 'unit_price']
getter = itemgetter(*field_keys)
data = [list(getter(item)) for item in odata]
return Response({'path': export_excel(field_data, data, '物料清单')})
class ShiftViewSet(ListModelMixin, CustomGenericViewSet): class ShiftViewSet(ListModelMixin, CustomGenericViewSet):
""" """
@ -311,7 +293,7 @@ class RoutePackViewSet(CustomModelViewSet):
return Response({"id": route_new.id}) return Response({"id": route_new.id})
@transaction.atomic @transaction.atomic
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer) @action(methods=['post'], detail=True, permission_classes = [IsAdminUser], serializer_class=Serializer)
def toggle_state(self, request, *args, **kwargs): def toggle_state(self, request, *args, **kwargs):
"""变更工艺路线状态 """变更工艺路线状态
@ -367,22 +349,12 @@ class RouteViewSet(CustomModelViewSet):
select_related_fields = ['material', select_related_fields = ['material',
'process', 'material_in', 'material_out', 'mgroup', 'routepack'] 'process', 'material_in', 'material_out', 'mgroup', 'routepack']
def perform_update(self, serializer): def update(self, request, *args, **kwargs):
ins:Route = serializer.instance obj:Route = self.get_object()
if ins.from_route is not None: routepack = obj.routepack
raise ParseError('该工艺步骤引用其他步骤, 无法编辑')
old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process
routepack = ins.routepack
if routepack and routepack.state != RoutePack.RP_S_CREATE: if routepack and routepack.state != RoutePack.RP_S_CREATE:
raise ParseError('该工艺路线非创建中不可编辑') raise ParseError('该状态下不可编辑')
ins_n:Route = serializer.save() return super().update(request, *args, **kwargs)
if Route.objects.filter(from_route__id=ins.id).exists() and (ins_n.material_in != old_m_in or ins_n.material_out != old_m_out or ins_n.process != process):
raise ParseError("该工艺步骤被其他步骤引用, 无法修改关键信息")
def perform_destroy(self, instance:Route):
if Route.objects.filter(from_route=instance).exists():
raise ParseError('该工艺步骤被其他步骤引用,无法删除')
return super().perform_destroy(instance)
class SruleViewSet(CustomModelViewSet): class SruleViewSet(CustomModelViewSet):

View File

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class OfmConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.ofm'

View File

@ -1,32 +0,0 @@
from django_filters import rest_framework as filters
from apps.ofm.models import MroomBooking, BorrowRecord
from .models import LendingSeal
from apps.utils.filters import MyJsonListFilter
class MroomBookingFilterset(filters.FilterSet):
class Meta:
model = MroomBooking
fields = {
'slot_b__mroom': ['exact', 'in'],
'slot_b__booking': ['exact'],
'slot_b__mdate': ['exact', 'gte', 'lte'],
'create_by': ['exact'],
"id": ["exact"]
}
class SealFilter(filters.FilterSet):
seal = MyJsonListFilter(label='按印章名称查询', field_name="seal")
class Meta:
model = LendingSeal
fields = ['seal']
class BorrowRecordFilter(filters.FilterSet):
file_name = filters.CharFilter(label='按文件名称查询', field_name="borrow_file__name", lookup_expr='icontains')
borrow_user = filters.CharFilter(label='按借阅人查询', field_name="create_by__name", lookup_expr='icontains')
class Meta:
model = BorrowRecord
fields = ['file_name', 'borrow_user']

View File

@ -1,66 +0,0 @@
# Generated by Django 3.2.12 on 2025-06-25 09:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Mroom',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=50, unique=True, verbose_name='会议室名称')),
('location', models.CharField(max_length=100, verbose_name='位置')),
('capacity', models.PositiveIntegerField(verbose_name='容纳人数')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroom_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='MroomBooking',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('title', models.CharField(max_length=100, verbose_name='会议主题')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='MroomSlot',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('mdate', models.DateField(db_index=True, verbose_name='会议日期')),
('slot', models.PositiveIntegerField(help_text='0-47', verbose_name='时段')),
('booking', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_b', to='ofm.mroombooking')),
('mroom', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='slot_m', to='ofm.mroom')),
],
options={
'unique_together': {('mroom', 'mdate', 'slot')},
},
),
]

View File

@ -1,48 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-05 03:07
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ofm', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='LendingSeal',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('seal', models.JSONField(default=list, help_text='{"seal_name": "印章名称"}', verbose_name='印章信息')),
('filename', models.TextField(verbose_name='文件名称')),
('file', models.TextField(verbose_name='文件内容')),
('file_count', models.PositiveIntegerField(verbose_name='用印份数')),
('is_lending', models.BooleanField(default=False, verbose_name='是否借出')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='联系方式')),
('lending_date', models.DateField(blank=True, null=True, verbose_name='借出日期')),
('return_date', models.DateField(blank=True, null=True, verbose_name='拟归还日期')),
('actual_return_date', models.DateField(blank=True, null=True, verbose_name='实际归还日期')),
('reason', models.CharField(blank=True, max_length=100, null=True, verbose_name='借用理由')),
('note', models.TextField(blank=True, null=True, verbose_name='备注')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('submit_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='seal_submit_user', to=settings.AUTH_USER_MODEL, verbose_name='提交人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='seal_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lendingseal_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-08 03:11
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0002_lendingseal'),
]
operations = [
migrations.RemoveField(
model_name='lendingseal',
name='submit_user',
),
]

View File

@ -1,42 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-10 06:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0002_alter_state_filter_dept'),
('ofm', '0003_remove_lendingseal_submit_user'),
]
operations = [
migrations.CreateModel(
name='Vehicle',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('start_time', models.DateField(blank=True, null=True, verbose_name='出车时间')),
('end_time', models.DateField(blank=True, null=True, verbose_name='还车时间')),
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='出发地点')),
('destination', models.CharField(blank=True, max_length=100, null=True, verbose_name='到达地点')),
('start_km', models.PositiveIntegerField(verbose_name='出发公里数')),
('end_km', models.PositiveIntegerField(verbose_name='归还公里数')),
('actual_km', models.PositiveIntegerField(editable=False, verbose_name='实际行驶公里数')),
('is_city', models.BooleanField(default=True, verbose_name='是否市内用车')),
('reason', models.CharField(max_length=100, verbose_name='用车事由')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-10 06:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0004_vehicle'),
]
operations = [
migrations.AddField(
model_name='vehicle',
name='via',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='途经地点'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-11 01:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
('ofm', '0005_vehicle_via'),
]
operations = [
migrations.AddField(
model_name='vehicle',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='vehicle_belong_dept', to='system.dept', verbose_name='所属部门'),
),
]

View File

@ -1,67 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-11 06:41
from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('ofm', '0006_vehicle_belong_dept'),
]
operations = [
migrations.AlterField(
model_name='lendingseal',
name='seal',
field=models.JSONField(default=list, help_text='[公章,法人章,财务章,合同章,业务章,其他章]', verbose_name='印章信息'),
),
migrations.CreateModel(
name='FileRecord',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=100, verbose_name='资料名称')),
('number', models.CharField(blank=True, max_length=50, null=True, verbose_name='档案编号')),
('counts', models.CharField(blank=True, max_length=10, null=True, verbose_name='文件份数')),
('location', models.CharField(blank=True, max_length=100, null=True, verbose_name='存放位置')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='存档人电话')),
('reciver', models.CharField(blank=True, max_length=50, null=True, verbose_name='接收人(综合办)')),
('remark', models.TextField(blank=True, max_length=200, null=True, verbose_name='备注')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='filerecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='BorrowRecord',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('borrow_date', models.DateField(blank=True, null=True, verbose_name='借阅日期')),
('return_date', models.DateField(blank=True, null=True, verbose_name='归还日期')),
('contacts', models.CharField(blank=True, max_length=50, null=True, validators=[django.core.validators.RegexValidator('^1[3456789]\\d{9}$', '手机号码格式不正确')], verbose_name='借阅人电话')),
('remark', models.JSONField(default=list, help_text=['借阅', '复印', '查阅'], verbose_name='用途')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_belong_dept', to='system.dept', verbose_name='所属部门')),
('borrow_file', models.ManyToManyField(related_name='borrow_records', to='ofm.FileRecord')),
('borrow_user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='borrow_user', to=settings.AUTH_USER_MODEL)),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrowrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-12 06:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0007_auto_20250911_1441'),
]
operations = [
migrations.RemoveField(
model_name='borrowrecord',
name='borrow_user',
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-12 07:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
('ofm', '0008_remove_borrowrecord_borrow_user'),
]
operations = [
migrations.AddField(
model_name='borrowrecord',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='borrow_ticket', to='wf.ticket', verbose_name='关联工单'),
),
]

View File

@ -1,53 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-19 01:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('system', '0006_auto_20241213_1249'),
('ofm', '0009_borrowrecord_ticket'),
]
operations = [
migrations.AddField(
model_name='lendingseal',
name='seal_other',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='其他印章'),
),
migrations.CreateModel(
name='Publicity',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('number', models.CharField(max_length=50, verbose_name='记录编号')),
('title', models.CharField(max_length=100, verbose_name='送审稿件标题')),
('participants', models.CharField(max_length=50, verbose_name='所有撰稿人')),
('level', models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='用途')),
('content', models.JSONField(default=list, help_text=['武器装备科研生产综合事项', '其它'], verbose_name='稿件内容涉及')),
('other_content', models.CharField(blank=True, max_length=100, null=True, verbose_name='其它内容')),
('report_purpose', models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道目的')),
('channel', models.JSONField(default=list, help_text=['互联网', '信息平台', '官微', '公开发行物', '其它'], verbose_name='发布渠道')),
('channel_other', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
('other_channel', models.CharField(blank=True, max_length=50, null=True, verbose_name='其它渠道')),
('report_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='报道名称')),
('review', models.JSONField(default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审')),
('dept_opinion', models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见')),
('dept_opinion_review', models.CharField(blank=True, max_length=100, null=True, verbose_name='部门审查意见')),
('publicity_opinion', models.JSONField(default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-24 05:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0010_auto_20250919_0921'),
]
operations = [
migrations.RemoveField(
model_name='publicity',
name='channel_other',
),
migrations.AddField(
model_name='publicity',
name='pfile',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='稿件路径'),
),
migrations.AddField(
model_name='publicity',
name='pub_dept',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='部室/研究院'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-24 06:07
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0011_auto_20250924_1359'),
]
operations = [
migrations.AddField(
model_name='publicity',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='publicity_ticket', to='wf.ticket', verbose_name='关联工单'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-25 07:41
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0012_publicity_ticket'),
]
operations = [
migrations.AddField(
model_name='mroomslot',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mrooms_ticket', to='wf.ticket', verbose_name='关联会议室'),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-28 02:23
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
('ofm', '0013_mroomslot_ticket'),
]
operations = [
migrations.AddField(
model_name='mroombooking',
name='ticket',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mrooms_ticket', to='wf.ticket', verbose_name='关联会议室'),
),
migrations.AddField(
model_name='mroomslot',
name='is_inuse',
field=models.BooleanField(default=True, verbose_name='是否占用'),
),
migrations.AlterUniqueTogether(
name='mroomslot',
unique_together={('mroom', 'mdate', 'slot', 'is_inuse')},
),
migrations.RemoveField(
model_name='mroomslot',
name='ticket',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-28 06:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0014_auto_20250928_1023'),
]
operations = [
migrations.AlterField(
model_name='vehicle',
name='end_km',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='归还公里数'),
),
]

View File

@ -1,35 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-29 07:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
('ofm', '0015_alter_vehicle_end_km'),
]
operations = [
migrations.AddField(
model_name='mroombooking',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='mroombooking_belong_dept', to='system.dept', verbose_name='所属部门'),
),
migrations.AddField(
model_name='mroombooking',
name='key_participants',
field=models.TextField(blank=True, null=True, verbose_name='主要参会领导'),
),
migrations.AddField(
model_name='mroombooking',
name='note',
field=models.TextField(blank=True, null=True, verbose_name='备注'),
),
migrations.AddField(
model_name='mroombooking',
name='participant_count',
field=models.PositiveIntegerField(default=0, verbose_name='参会人数'),
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-10 08:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0016_auto_20250929_1551'),
]
operations = [
migrations.AddField(
model_name='publicity',
name='secret_period',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='秘密期限'),
),
migrations.AlterField(
model_name='publicity',
name='level',
field=models.JSONField(default=list, help_text=['重要', '一般', '非涉密'], verbose_name='涉密等级'),
),
]

View File

@ -1,33 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-11 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0017_auto_20251010_1631'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='number',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='记录编号'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-11 03:28
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0018_auto_20251011_0922'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(default=list, help_text=['同意', '不同意'], verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], verbose_name='第一撰稿人自审'),
),
]

View File

@ -1,28 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-11 06:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0019_auto_20251011_1128'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意', '不同意'], null=True, verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.JSONField(blank=True, default=list, help_text=['同意公开宣传报道', '不同意任何渠道的宣传报道'], null=True, verbose_name='宣传统战部审查意见'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True, verbose_name='第一撰稿人自审'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-13 01:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0020_auto_20251011_1427'),
]
operations = [
migrations.AlterField(
model_name='publicity',
name='publicity_opinion',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='宣传报道意见'),
),
]

View File

@ -1,17 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-17 06:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('ofm', '0021_alter_publicity_publicity_opinion'),
]
operations = [
migrations.AlterUniqueTogether(
name='mroomslot',
unique_together=set(),
),
]

View File

@ -1,49 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-21 06:08
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wf', '0004_workflow_view_path2'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('system', '0006_auto_20241213_1249'),
('ofm', '0022_alter_mroomslot_unique_together'),
]
operations = [
migrations.CreateModel(
name='PatentInfo',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('name', models.CharField(max_length=100, verbose_name='拟申请专利名称')),
('author', models.CharField(max_length=100, verbose_name='发明人(设计人)')),
('type', models.CharField(choices=[('invention', '发明专利'), ('utility', '实用新型专利'), ('design', '外观设计专利')], default='invention', max_length=50, verbose_name='专利类型')),
('is_public', models.BooleanField(default=False, verbose_name='是否公开')),
('area', models.CharField(choices=[('Domestic', '国内申请'), ('Foreign', '国外申请'), (' PCT', 'PCT申请')], default='Domestic', max_length=50, verbose_name='拟申请地域')),
('identified', models.BooleanField(default=False, verbose_name='是否进行过科技成果鉴定')),
('published_article', models.BooleanField(default=False, verbose_name='是否发表过文章')),
('exhibited', models.BooleanField(default=False, verbose_name='是否参与过展会展出')),
('applied_to_production', models.BooleanField(default=False, verbose_name='是否参与应用于生产/销售')),
('participated_in_exchange', models.BooleanField(default=False, verbose_name='是否参与过技术交流')),
('tech_background_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术背景材料页数')),
('tech_disclosure_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='技术交底材料页数')),
('novelty_report_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='查新检索报告页数')),
('diagrams_or_photos_pages', models.PositiveIntegerField(blank=True, null=True, verbose_name='图/照片页数或张数')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_belong_dept', to='system.dept', verbose_name='所属部门')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentInfo_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='patentinfo_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,95 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-22 02:05
import apps.ofm.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0023_patentinfo'),
]
operations = [
migrations.RemoveField(
model_name='patentinfo',
name='applied_to_production',
),
migrations.RemoveField(
model_name='patentinfo',
name='diagrams_or_photos_pages',
),
migrations.RemoveField(
model_name='patentinfo',
name='exhibited',
),
migrations.RemoveField(
model_name='patentinfo',
name='identified',
),
migrations.RemoveField(
model_name='patentinfo',
name='novelty_report_pages',
),
migrations.RemoveField(
model_name='patentinfo',
name='participated_in_exchange',
),
migrations.RemoveField(
model_name='patentinfo',
name='published_article',
),
migrations.RemoveField(
model_name='patentinfo',
name='tech_background_pages',
),
migrations.RemoveField(
model_name='patentinfo',
name='tech_disclosure_pages',
),
migrations.AddField(
model_name='patentinfo',
name='other_area',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='其它申请地域'),
),
migrations.AddField(
model_name='patentinfo',
name='tech_file',
field=models.JSONField(default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段', verbose_name='技术文件'),
),
migrations.AddField(
model_name='patentinfo',
name='tech_status',
field=models.JSONField(blank=True, default=list, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段', verbose_name='技术状态'),
),
migrations.AlterField(
model_name='borrowrecord',
name='remark',
field=models.JSONField(default=list, help_text="['借阅', '复印', '查阅']", verbose_name='用途'),
),
migrations.AlterField(
model_name='publicity',
name='channel',
field=models.JSONField(default=list, help_text="['互联网', '信息平台', '官微', '公开发行物', '其它']", verbose_name='发布渠道'),
),
migrations.AlterField(
model_name='publicity',
name='content',
field=models.JSONField(default=list, help_text="['武器装备科研生产综合事项', '其它']", verbose_name='稿件内容涉及'),
),
migrations.AlterField(
model_name='publicity',
name='dept_opinion',
field=models.JSONField(blank=True, default=list, help_text="['同意', '不同意']", null=True, verbose_name='部门负责人意见'),
),
migrations.AlterField(
model_name='publicity',
name='level',
field=models.JSONField(default=list, help_text="['重要', '一般', '非涉密']", verbose_name='涉密等级'),
),
migrations.AlterField(
model_name='publicity',
name='review',
field=models.JSONField(blank=True, default=list, help_text="['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布']", null=True, verbose_name='第一撰稿人自审'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-24 05:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ofm', '0024_auto_20251022_1005'),
]
operations = [
migrations.AlterField(
model_name='patentinfo',
name='area',
field=models.CharField(choices=[('Domestic', '国内申请'), ('Foreign', '国外申请'), ('PCT', 'PCT申请')], default='Domestic', max_length=50, verbose_name='拟申请地域'),
),
]

View File

@ -1,41 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-29 03:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0004_workflow_view_path2'),
('ofm', '0025_alter_patentinfo_area'),
]
operations = [
migrations.CreateModel(
name='PaperSe',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('paper_name', models.CharField(max_length=100, verbose_name='拟发表论文名称')),
('publication_name', models.CharField(max_length=100, verbose_name='拟投期刊名称')),
('author', models.CharField(max_length=100, verbose_name='作者')),
('paper_type', models.CharField(max_length=100, verbose_name='拟发表文章类型')),
('is_chinese_core', models.BooleanField(default=False, verbose_name='是否为中文核心')),
('is_sci', models.BooleanField(default=False, verbose_name='是否被SCI/EI收录')),
('tech_status', models.JSONField(blank=True, default=list, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段', verbose_name='技术状态')),
('tech_file', models.JSONField(default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段', verbose_name='技术文件')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paperse_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paperse_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paperse_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,44 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-29 06:26
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0004_workflow_view_path2'),
('ofm', '0026_paperse'),
]
operations = [
migrations.CreateModel(
name='Papersecret',
fields=[
('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')),
('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')),
('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')),
('paper_name', models.CharField(max_length=100, verbose_name='拟发表论文名称')),
('publication_name', models.CharField(max_length=100, verbose_name='拟投期刊名称')),
('author', models.CharField(max_length=100, verbose_name='作者')),
('paper_type', models.CharField(max_length=100, verbose_name='拟发表文章类型')),
('is_chinese_core', models.BooleanField(default=False, verbose_name='是否为中文核心')),
('is_sci', models.BooleanField(default=False, verbose_name='是否被SCI/EI收录')),
('tech_status', models.JSONField(blank=True, default=list, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段', verbose_name='技术状态')),
('tech_file', models.JSONField(default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段', verbose_name='技术文件')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='papersecret_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('ticket', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='paperse_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='papersecret_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.DeleteModel(
name='PaperSe',
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-30 05:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0006_auto_20241213_1249'),
('ofm', '0027_auto_20251029_1426'),
]
operations = [
migrations.AddField(
model_name='papersecret',
name='belong_dept',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='papersecret_belong_dept', to='system.dept', verbose_name='所属部门'),
),
]

View File

@ -1,311 +0,0 @@
from django.db import models, transaction
from apps.utils.models import CommonADModel, BaseModel, CommonBDModel
from apps.system.models import User
from django.core.validators import RegexValidator
from datetime import datetime
from rest_framework.exceptions import ParseError
# Create your models here.
MTASK_CREATED = 10
MTASK_ASSGINED = 20
MTASK_STOP = 34
MTASK_SUBMIT = 40
MTASK_STATES = (
(MTASK_CREATED, '创建中'),
(MTASK_ASSGINED, '已下达'),
(MTASK_STOP, '已停止'),
(MTASK_SUBMIT, '已提交')
)
phone_validator = RegexValidator(r'^1[3456789]\d{9}$', '手机号码格式不正确')
class Mroom(CommonADModel):
"""TN: 会议室基本信息"""
name = models.CharField('会议室名称', max_length=50, unique=True)
location = models.CharField('位置', max_length=100)
capacity = models.PositiveIntegerField('容纳人数')
class MroomBooking(CommonBDModel):
"""TN: 会议室预定信息"""
# belong_dept 是预定部门
title = models.CharField('会议主题', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联会议室',
on_delete=models.SET_NULL, related_name='mrooms_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
participant_count = models.PositiveIntegerField('参会人数', default=0)
key_participants = models.TextField("主要参会领导", null=True, blank=True)
class MroomSlot(BaseModel):
"""TN: 会议室时段"""
mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m")
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
mdate = models.DateField('会议日期', db_index=True)
slot = models.PositiveIntegerField('时段', help_text='0-47')
is_inuse = models.BooleanField('是否占用', default=True)
# class Seal(BaseModel):
# """TN: 印章类型"""
# name = models.CharField('印章名称', max_length=50, unique=True)
class LendingSeal(CommonBDModel):
"""TN: 印章外出用印信息"""
seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]')
seal_other = models.CharField('其他印章', max_length=50, blank=True, null=True)
filename = models.TextField('文件名称')
file = models.TextField('文件内容')
file_count = models.PositiveIntegerField('用印份数')
is_lending= models.BooleanField('是否借出', default=False)
contacts = models.CharField('联系方式', max_length=50, validators=[phone_validator], blank=True, null=True)
lending_date = models.DateField('借出日期', blank=True, null=True)
return_date = models.DateField('拟归还日期', blank=True, null=True)
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
class Vehicle(CommonBDModel):
"""TN: 用车申请"""
start_time = models.DateField('出车时间', blank=True, null=True)
end_time = models.DateField('还车时间', blank=True, null=True)
location = models.CharField('出发地点', null=True, blank=True, max_length=100)
via = models.CharField('途经地点', null=True, blank=True, max_length=100)
destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
start_km = models.PositiveIntegerField('出发公里数')
end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True)
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
is_city = models.BooleanField('是否市内用车', default=True)
reason = models.CharField('用车事由', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False)
def save(self, *args, **kwargs):
if self.end_km:
if self.start_km <= self.end_km:
self.actual_km = self.end_km - self.start_km
else:
raise ParseError('归还公里数不能小于出发公里数')
else:
self.actual_km = 0
return super().save(*args, **kwargs)
class FileRecord(CommonBDModel):
"""TN: 档案台账"""
name = models.CharField('资料名称', max_length=100)
number = models.CharField('档案编号', max_length=50, null=True, blank=True)
counts = models.CharField('文件份数', max_length=10, null=True, blank=True)
location = models.CharField('存放位置', max_length=100, null=True, blank=True)
contacts = models.CharField('存档人电话', max_length=50, validators=[phone_validator], blank=True, null=True)
reciver = models.CharField('接收人(综合办)', max_length=50, null=True, blank=True)
remark = models.TextField('备注', max_length=200, null=True, blank=True)
class BorrowRecord(CommonBDModel):
"""TN: 借阅、复印、查阅记录"""
borrow_file = models.ManyToManyField(FileRecord, related_name="borrow_records")
borrow_date = models.DateField('借阅日期', null=True, blank=True)
return_date = models.DateField('归还日期', null=True, blank=True)
contacts = models.CharField('借阅人电话', max_length=50, validators=[phone_validator], null=True, blank=True)
remark = models.JSONField('用途', default=list, help_text=str(['借阅', '复印', '查阅']))
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False)
class Publicity(CommonBDModel):
"""TN: 公示栏"""
number = models.CharField('记录编号', max_length=50, blank=True, null=True)
title = models.CharField('送审稿件标题', max_length=100)
participants = models.CharField('所有撰稿人', max_length=50)
pub_dept = models.CharField('部室/研究院', null=True, blank=True, max_length=50)
pfile = models.CharField('稿件路径', null=True, blank=True, max_length=100)
level = models.JSONField('涉密等级', default=list, help_text=str(['重要', '一般', '非涉密']))
content = models.JSONField('稿件内容涉及', default=list, help_text=str([
"武器装备科研生产综合事项",
"其它"
]))
other_content = models.CharField('其它内容', max_length=100, blank=True, null=True)
report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True)
channel = models.JSONField('发布渠道', default=list, help_text=str(['互联网', '信息平台', '官微', '公开发行物', '其它']))
other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True)
report_name = models.CharField('报道名称', max_length=50, blank=True, null=True)
review = models.JSONField('第一撰稿人自审', default=list, help_text=str(['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布']), null=True,blank=True)
dept_opinion = models.JSONField('部门负责人意见', default=list, help_text=str(['同意', '不同意']), null=True, blank=True)
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
# 记录编号自动生成
def save(self, *args, **kwargs):
if not self.number:
last_number = self.__class__.objects.filter(number__startswith=f"GXKG-{datetime.now().year}-").order_by('-number').first()
if last_number:
try:
last_num = int(last_number.number.split('-')[-1])
except ValueError:
last_num = 0
else:
last_num =0
# 格式化编号,带补零
self.number = f"GXKG-{datetime.now().year}-{last_num+1:02d}"
super().save(*args, **kwargs)
class PatentInfo(CommonBDModel):
"""TN: 专利申密审批表单样式"""
PATENT_TYPE_CHOICES = (
('invention', '发明专利'),
('utility', '实用新型专利'),
('design', '外观设计专利'),
)
APPLY_AREAS = (
('Domestic', '国内申请'),
('Foreign', '国外申请'),
('PCT', 'PCT申请'),
)
name = models.CharField('拟申请专利名称', max_length=100)
author = models.CharField('发明人(设计人)', max_length=100)
type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention')
is_public = models.BooleanField('是否公开', default=False)
area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic')
other_area = models.CharField('其它申请地域', max_length=50, blank=True, null=True)
tech_status = models.JSONField('技术状态', default=list, blank=True, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段')
tech_file = models.JSONField('技术文件', default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段')
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='patentInfo_ticket', null=True, blank=True, db_constraint=False)
class Papersecret(CommonBDModel):
"""TN: 论文申密审批表单"""
paper_name = models.CharField('拟发表论文名称', max_length=100)
publication_name = models.CharField('拟投期刊名称', max_length=100)
author = models.CharField('作者', max_length=100)
paper_type = models.CharField('拟发表文章类型', max_length=100)
is_chinese_core = models.BooleanField('是否为中文核心', default=False)
is_sci = models.BooleanField('是否被SCI/EI收录', default=False)
tech_status = models.JSONField('技术状态', default=list, blank=True, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段')
tech_file = models.JSONField('技术文件', default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段')
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='paperse_ticket', null=True, blank=True, db_constraint=False)
# class PatentRecord(CommonADModel):
# """TN: 专利台账登记"""
# volume_number = models.CharField(max_length=50, null=True, blank=True, verbose_name="卷号")
# application_number = models.CharField(max_length=50, verbose_name="申请号(交局后补登)")
# title = models.CharField(max_length=255, verbose_name="名称")
# patent_type = models.CharField(
# max_length=20,
# choices=[
# ("invention", "发明"),
# ("utility_model", "实用新型"),
# ("design", "外观设计")
# ],
# verbose_name="专利类型"
# )
# organization = models.CharField(max_length=100, verbose_name="单位")
# inventors = models.CharField(max_length=255, verbose_name="发明人")
# agent = models.CharField(max_length=255, null=True, blank=True, verbose_name="代理人")
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
# application_date = models.DateField(null=True, blank=True, verbose_name="申请日")
# authorization_date = models.DateField(null=True, blank=True, verbose_name="授权日")
# validity_years = models.IntegerField(null=True, blank=True, verbose_name="有效年限(年)")
# annuity_paid = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="年费缴纳")
# status = models.CharField(
# max_length=20,
# choices=[
# ("not_disclosed", "未公开"),
# ("under_examination", "实审中"),
# ("first_office_action", "一通"),
# ("second_office_action", "二通"),
# ("rejected", "驳回"),
# ("reexamination", "复审"),
# ("authorized", "授权")
# ],
# verbose_name="状态"
# )
# award_info = models.TextField(null=True, blank=True, verbose_name="报奖情况")
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2, null=True,blank=True,verbose_name="奖金金额(元)")
# class PaperRecord(models.Model):
# """TN: 论文台账登记"""
# index = models.PositiveIntegerField(verbose_name="序号")
# paper_code = models.CharField(max_length=100, blank=True, null=True, verbose_name="论文编号(投稿后补登)")
# title = models.CharField(max_length=255, verbose_name="名称")
# paper_type = models.CharField(max_length=100, verbose_name="论文类型")
# affiliation = models.CharField(max_length=255, verbose_name="单位")
# authors = models.CharField(max_length=255, verbose_name="作者")
# corresponding_author = models.CharField(max_length=255, blank=True, null=True, verbose_name="通讯作者")
# affiliated_platforms = models.ManyToManyField('Platform', blank=True, verbose_name="归属平台")
# affiliated_projects = models.ManyToManyField('Project', blank=True, verbose_name="归属项目")
# acceptance_date = models.DateField(blank=True, null=True, verbose_name="接受日期")
# publication_date = models.DateField(blank=True, null=True, verbose_name="发表日期")
# page_fee_paid = models.DecimalField(
# max_digits=10,
# decimal_places=2,
# blank=True,
# null=True,
# verbose_name="版面费缴纳"
# )
# status = models.CharField(
# max_length=50,
# choices=[
# ("under_review", "审稿中"),
# ("revise_1", "一修"),
# ("revise_2", "二修"),
# ("accepted", "接收"),
# ("published", "发表")
# ],
# default="under_review", verbose_name="状态"
# )
# award_status = models.CharField(max_length=255, blank=True, null=True, verbose_name="报奖情况")
# bonus_amount = models.DecimalField(max_digits=10,decimal_places=2,blank=True,null=True,verbose_name="奖金发放")
# class ProjectApproval(CommonBDModel):
# """TN: 立项审批表"""
# project_start_date = models.DateField("立项日期", null=True, blank=True)
# is_self_initiated = models.BooleanField("自立项目", default=False)
# is_city_level = models.BooleanField("市级项目", default=False)
# is_province_level = models.BooleanField("省级项目", default=False)
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
# project_members = models.TextField("项目组员", null=True, blank=True)
# project_budget = models.DecimalField("项目预算(万元)", max_digits=12, decimal_places=2, null=True, blank=True)
# project_description = models.TextField("项目基本情况", null=True, blank=True)
# project_performance = models.TextField("目标绩效", null=True, blank=True)
# class ProjectInfo(CommonBDModel):
# """TN: 项目信息表
# """
# serial_number = models.CharField("序号", max_length=50, null=True, blank=True)
# red_head_doc_no = models.CharField("红头发文号/公示页", max_length=100, null=True, blank=True)
# name = models.CharField("名称", max_length=200, null=True, blank=True)
# project_type = models.CharField("项目类型", max_length=100, null=True, blank=True)
# platform = models.CharField("所属平台", max_length=100, null=True, blank=True)
# project_source = models.CharField("项目来源", max_length=100, null=True, blank=True)
# construction_period = models.CharField("建设期", max_length=100, null=True, blank=True)
# project_funding = models.DecimalField("项目资金(财政与自筹)", max_digits=15, decimal_places=2, null=True, blank=True)
# support_period = models.CharField("项目支持期", max_length=100, null=True, blank=True)
# undertaking_unit = models.CharField("承担单位", max_length=200, null=True, blank=True)
# responsible_person = models.CharField("负责人", max_length=50, null=True, blank=True)
# project_members = models.TextField("项目人员", null=True, blank=True)
# milestone = models.TextField("里程碑节点", null=True, blank=True)
# mid_term_status = models.TextField("项目中期情况", null=True, blank=True)
# acceptance_status = models.TextField("项目验收情况", null=True, blank=True)
# sci_tech_achievements = models.TextField("科技成果", null=True, blank=True)

View File

@ -1,199 +0,0 @@
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo, Papersecret)
# Publicity, PatetInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers
from django.db import transaction
from rest_framework.exceptions import ParseError
from apps.utils.constants import EXCLUDE_FIELDS
from apps.wf.serializers import TicketSimpleSerializer
class MroomSerializer(CustomModelSerializer):
class Meta:
model = Mroom
fields = '__all__'
class MroomBookingSerializer(CustomModelSerializer):
mroom = serializers.PrimaryKeyRelatedField(queryset=Mroom.objects.all(), write_only=True, label="会议室")
mdate = serializers.DateField(write_only=True, label="预订日期")
slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引")
create_by_name = serializers.CharField(source='create_by.username', read_only=True)
create_by_phone = serializers.CharField(source='create_by.phone', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = MroomBooking
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
extra_kwargs = {'belong_dept': {'required': True}}
def create(self, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().create(validated_data)
MroomSlot.objects.filter(booking=booking).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
def update(self, instance, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().update(instance, validated_data)
MroomSlot.objects.filter(booking=instance).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
class MroomSlotSerializer(CustomModelSerializer):
booking_title = serializers.CharField(source='booking.title', read_only=True)
class Meta:
model = MroomSlot
fields = '__all__'
class LendingSealSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = LendingSeal
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class VehicleSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Vehicle
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
class FileRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = FileRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class BorrowRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件")
file_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
file_name = serializers.SerializerMethodField()
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = BorrowRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
def get_file_name(self, obj):
return [file.name for file in obj.borrow_file.all()]
class PublicitySerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Publicity
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class PatentInfoSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = PatentInfo
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class PaperSeSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Papersecret
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
# class PlatformSerializer(serializers.ModelSerializer):
# class Meta:
# model = Platform
# fields = ['id', 'name']
# class ProjectSerializer(serializers.ModelSerializer):
# class Meta:
# model = Project
# fields = ['id', 'name']
# class ProjectMemberSerializer(CustomModelSerializer):
# affiliated_platforms = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Platform.objects.all(),
# write_only=True
# )
# affiliated_platforms_detail = PlatformSerializer(
# source='affiliated_platforms', many=True, read_only=True
# )
# affiliated_projects = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Project.objects.all(),
# write_only=True
# )
# affiliated_projects_detail = ProjectSerializer(
# source='affiliated_projects', many=True, read_only=True
# )
# class Meta:
# model = PatentRecord
# fields = '__all__'
# class PaperRecordSerializer(CustomModelSerializer):
# class Meta:
# model = PaperRecord
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectApprovalSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectApproval
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectInfoSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectInfo
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS

View File

@ -1,192 +0,0 @@
from apps.wf.models import Ticket
# TicketFlow, Transition, Workflow, CustomField, State,
from apps.ofm.models import LendingSeal, Vehicle, BorrowRecord, Publicity, MroomBooking, MroomSlot, PatentInfo, Papersecret
from rest_framework.exceptions import ParseError
def seal_submit_validate(ins: LendingSeal):
if ins.submit_time:
raise ParseError('该日志已提交!')
if ins.mtask and ins.mtask.state == LendingSeal.MTASK_STOP:
raise ParseError('该任务已停止!')
def bind_mroombooking(ticket: Ticket, transition, new_ticket_data: dict):
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'mroombooking',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def mroombooking_reject(ticket: Ticket, transition, new_ticket_data: dict):
ins = MroomBooking.objects.get(id=new_ticket_data['t_id'])
MroomSlot.objects.filter(booking=ins).update(is_inuse=False)
def bind_lendingseal(ticket: Ticket, transition, new_ticket_data: dict):
ins = LendingSeal.objects.get(id=new_ticket_data['t_id'])
ins.actual_return_date = None
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'LendingSeal',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
# 如果驳回到开始状态
def lending_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = LendingSeal.objects.get(id=new_ticket_data['t_id'])
except LendingSeal.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
ins = Vehicle.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'Vehicle',
't_id': ins.id,
})
ins.actual_km = None
ins.end_time = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def vehicle_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = Vehicle.objects.get(id=new_ticket_data['t_id'])
except Vehicle.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_file(ticket: Ticket, transition, new_ticket_data: dict):
ins = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'BorrowRecord',
't_id': ins.id,
})
ins.return_date = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def file_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
except BorrowRecord.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_publicity(ticket: Ticket, transition, new_ticket_data: dict):
ins = Publicity.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'publicity',
't_id': ins.id,
})
ins.dept_opinion = None
ins.secret_period = None
ins.dept_opinion_review = None
ins.publicity_opinion = None
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = Publicity.objects.get(id=new_ticket_data['t_id'])
except Publicity.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def bind_patent(ticket: Ticket, transition, new_ticket_data: dict):
ins = PatentInfo.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'patent',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def patent_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = PatentInfo.objects.get(id=new_ticket_data['t_id'])
except PatentInfo.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()
def paperse_patent(ticket: Ticket, transition, new_ticket_data: dict):
ins = Papersecret.objects.get(id=new_ticket_data['t_id'])
ticket_data = ticket.ticket_data
ticket_data.update({
't_model': 'paperse',
't_id': ins.id,
})
ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by
ticket.save()
if ins.ticket is None:
ins.ticket = ticket
ins.save()
def paperse_save_ticket_data(ticket: Ticket, new_ticket_data: dict, **kwargs):
try:
obj = Papersecret.objects.get(id=new_ticket_data['t_id'])
except Papersecret.DoesNotExist:
raise ParseError("Publicity t_id 不存在")
data_save = {k: v for k, v in new_ticket_data.items() if k not in ['t_model', 't_id']}
for k, v in data_save.items():
setattr(obj, k, v)
obj.save()

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,32 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet,
FileborrowViewSet, PublicityViewSet, PatentInfoViewSet, PapersecretViewSet)
# SealModelViewSet,
# , PublicityViewSet, , PaperViewSet, PlatformViewSet,
# ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet)
API_BASE_URL = 'api/ofm/'
HTML_BASE_URL = 'dhtml/ofm/'
router = DefaultRouter()
router.register('mroom', MroomViewSet, basename='mroom')
router.register('mroombooking', MroomBookingViewSet, basename='mroombooking')
router.register('mroomslot', MroomSlotViewSet, basename='mroomslot')
# router.register('sealmanage', SealManageViewSet, basename='sealmanage')
router.register('lendingseal', LendingSealViewSet, basename='lendingseal')
router.register('vehicle', VehicleViewSet, basename='vehicle')
router.register('filerecord', FilerecordViewSet, basename='filerecord')
router.register('fileborrow', FileborrowViewSet, basename='fileborrow')
router.register('publicity', PublicityViewSet, basename='publicity')
router.register('patentinfo', PatentInfoViewSet, basename='patentinfo')
router.register('paperse', PapersecretViewSet, basename='PaperSe')
# router.register('platform', PlatformViewSet, basename='platform')
# router.register('project', ProjectViewSet, basename='project')
# router.register('patentrecord', PatentRecordViewSet, basename='patentrecord')
# router.register('paperrecord', PaperRecordViewSet, basename='paperrecord')
# router.register('projectapproval', ProjectApprovalViewSet, basename='projectapproval')
# router.register('projectinfo', ProjectInfoViewSet, basename='projectinfo')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,259 +0,0 @@
from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo, Papersecret
# Publicity, , PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer,
VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, PatentInfoSerializer, PaperSeSerializer)
# ,SealSerializer,
# LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer,
# PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer)
from rest_framework.decorators import action
from apps.utils.mixins import CustomListModelMixin
from rest_framework.exceptions import ParseError
from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter
class MroomViewSet(CustomModelViewSet):
"""list: 会议室
会议室
"""
queryset = Mroom.objects.all()
serializer_class = MroomSerializer
class MroomBookingViewSet(CustomModelViewSet):
"""list: 会议室预订
会议室预订
"""
queryset = MroomBooking.objects.all()
serializer_class = MroomBookingSerializer
select_related_fields = ["create_by", "ticket", "belong_dept"]
filterset_class = MroomBookingFilterset
def add_info_for_list(self, data):
booking_ids = [d["id"] for d in data]
slots = MroomSlot.objects.filter(booking__in=booking_ids).order_by("booking", "mroom", "mdate", "slot")
booking_info = {}
for slot in slots:
booking_id = slot.booking.id
if booking_id not in booking_info:
booking_info[booking_id] = {
"mdate": slot.mdate.strftime("%Y-%m-%d"), # 格式化日期
"mroom": slot.mroom.id,
"mroom_name": slot.mroom.name, # 会议室名称
"time_ranges": [], # 存储时间段(如 ["8:00-9:00", "10:00-11:30"]
"current_slots": [], # 临时存储连续的slot用于合并
}
# 检查是否连续当前slot是否紧接上一个slot
current_slots = booking_info[booking_id]["current_slots"]
if not current_slots or slot.slot == current_slots[-1] + 1:
current_slots.append(slot.slot)
else:
# 如果不连续先把当前连续的slot转换成时间段
if current_slots:
start_time = self._slot_to_time(current_slots[0])
end_time = self._slot_to_time(current_slots[-1] + 1)
booking_info[booking_id]["time_ranges"].append(f"{start_time}-{end_time}")
current_slots.clear()
current_slots.append(slot.slot)
# 处理最后剩余的连续slot
for info in booking_info.values():
if info["current_slots"]:
start_time = self._slot_to_time(info["current_slots"][0])
end_time = self._slot_to_time(info["current_slots"][-1] + 1)
info["time_ranges"].append(f"{start_time}-{end_time}")
info["slots"] = info.pop("current_slots") # 清理临时数据
for item in data:
item.update(booking_info.get(item["id"], {}))
return data
@staticmethod
def _slot_to_time(slot):
"""将slot (0-47) 转换为 HH:MM 格式的时间字符串"""
hours = slot // 2
minutes = (slot % 2) * 30
return f"{hours:02d}:{minutes:02d}"
def perform_update(self, serializer):
ins:MroomBooking = self.get_object()
ticket = ins.ticket
if ticket is None or ticket.state.type == 1:
pass
else:
raise ParseError("存在审批单,不允许修改")
if ins.create_by and ins.create_by != self.request.user:
raise ParseError("只允许创建者修改")
return super().perform_update(serializer)
def perform_destroy(self, instance):
if instance.create_by and instance.create_by != self.request.user:
raise ParseError("只允许创建者删除")
ticket = instance.ticket
if ticket is None or ticket.state.type == 1:
pass
else:
raise ParseError("存在审批单,不允许删除")
if ticket:
ticket.delete()
instance.delete()
class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet):
"""list:
会议室预订时段
"""
queryset = MroomSlot.objects.all()
serializer_class = MroomSlotSerializer
filterset_fields = ["mroom", "mdate", "booking", "is_inuse"]
class LendingSealViewSet(CustomModelViewSet):
"""list: 印章外出
印章外出
"""
perms_map = {'get': '*', 'post': 'seal.update',
'put': 'seal.update', 'delete': 'seal.delete'}
queryset = LendingSeal.objects.all()
serializer_class = LendingSealSerializer
filterset_class = SealFilter
ordering = ["-create_time"]
data_filter = True
class VehicleViewSet(CustomModelViewSet):
"""list: 车辆
车辆
"""
queryset = Vehicle.objects.all()
serializer_class = VehicleSerializer
ordering = ["-create_time"]
class FilerecordViewSet(CustomModelViewSet):
"""list: 文件
文件
"""
queryset = FileRecord.objects.all()
serializer_class = FileRecordSerializer
filterset_fields = [ "name", "number"]
ordering = ["-create_time", "number", "name"]
class FileborrowViewSet(CustomModelViewSet):
"""list: 文件借阅
文件借阅
"""
queryset = BorrowRecord.objects.all()
serializer_class = BorrowRecordSerializer
filterset_class = BorrowRecordFilter
ordering = ["-create_time"]
class PublicityViewSet(CustomModelViewSet):
"""list: 公告
公告
"""
queryset = Publicity.objects.all()
serializer_class = PublicitySerializer
filterset_fields = ["title","number"]
ordering = ["-create_time", "number"]
class PatentInfoViewSet(CustomModelViewSet):
"""list: 专利
专利
"""
queryset = PatentInfo.objects.all()
serializer_class = PatentInfoSerializer
filterset_fields = ["name", "author", "type"]
ordering = ["-create_time", "name", "author", "type"]
class PapersecretViewSet(CustomModelViewSet):
"""list: 论文申密审批
论文申密审批
"""
queryset = Papersecret.objects.all()
serializer_class = PaperSeSerializer
filterset_fields = ["paper_name", "author"]
ordering = ["create_time", "paper_name"]
# class PlatformViewSet(CustomModelViewSet):
# """list: 平台
# 平台
# """
# queryset = Platform.objects.all()
# serializer_class = PlatformSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class ProjectViewSet(CustomModelViewSet):
# """list: 项目
# 项目
# """
# queryset = Project.objects.all()
# serializer_class = ProjectSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class PatentRecordViewSet(CustomModelViewSet):
# """list: 专利台账登记
# 专利台账登记
# """
# queryset = PatentRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["patent", "type"]
# ordering = ["create_time", "patent", "type"]
# class PaperRecordViewSet(CustomModelViewSet):
# """list: 论文台账登记
# 论文台账登记
# """
# queryset = PaperRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["index", "title", "paper_code","paper_type", "authors"]
# ordering = ["create_time", "paper", "type"]
# class ProjectApprovalViewSet(CustomModelViewSet):
# """list: 立项审批表
# 立项审批表
# """
# queryset = ProjectApproval.objects.all()
# serializer_class = ProjectApprovalSerializer
# filterset_fields = ["project_start_date"]
# ordering = ["project_start_date"]
# class ProjectInfoViewSet(CustomModelViewSet):
# """list: 项目信息
# 项目信息
# """
# queryset = ProjectInfo.objects.all()
# serializer_class = ProjectInfoSerializer
# filterset_fields = ["serial_number", "name", "platform", "project_source"]
# ordering = ["serial_number", "name"]

View File

@ -50,7 +50,7 @@ class MtaskFilter(filters.FilterSet):
"is_count_utask": ["exact"], "is_count_utask": ["exact"],
"start_date": ["exact", "gte", "lte"], "start_date": ["exact", "gte", "lte"],
"end_date": ["exact", "gte", "lte"], "end_date": ["exact", "gte", "lte"],
"mgroup": ["exact", "in"], "mgroup": ["exact"],
"mgroup__name": ["exact"], "mgroup__name": ["exact"],
"mgroup__cate": ["exact"], "mgroup__cate": ["exact"],
"mgroup__process": ["exact"], "mgroup__process": ["exact"],

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-06-11 03:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pm', '0021_auto_20250317_1040'),
]
operations = [
migrations.AddField(
model_name='utask',
name='priority',
field=models.PositiveIntegerField(default=20, help_text='10:低;20:中;30:高', verbose_name='优先级'),
),
]

View File

@ -37,7 +37,6 @@ class Utask(CommonBDModel):
type = models.CharField('任务类型', max_length=10, type = models.CharField('任务类型', max_length=10,
help_text=str(TASK_TYPE), default='mass') help_text=str(TASK_TYPE), default='mass')
routepack = models.ForeignKey(RoutePack, verbose_name='关联工艺包', on_delete=models.SET_NULL, null=True, blank=True) routepack = models.ForeignKey(RoutePack, verbose_name='关联工艺包', on_delete=models.SET_NULL, null=True, blank=True)
priority = models.PositiveIntegerField('优先级', default=20, help_text="10:低;20:中;30:高")
state = models.PositiveIntegerField( state = models.PositiveIntegerField(
'状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES)) '状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES))
number = models.CharField('编号', max_length=50, unique=True) number = models.CharField('编号', max_length=50, unique=True)

View File

@ -27,10 +27,10 @@ class UtaskSerializer(CustomModelSerializer):
model = Utask model = Utask
fields = '__all__' fields = '__all__'
extra_kwargs = { extra_kwargs = {
'number': {"required": False, "allow_blank": True}, 'number': {"required": False, "allow_blank": True}
"priority": {"required": False, "allow_null": True},
} }
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
if not validated_data.get('number', None): if not validated_data.get('number', None):
validated_data["number"] = Utask.get_a_number() validated_data["number"] = Utask.get_a_number()
@ -52,7 +52,6 @@ class UtaskSerializer(CustomModelSerializer):
attrs['count_day'] = math.ceil(attrs['count']/rela_days) attrs['count_day'] = math.ceil(attrs['count']/rela_days)
except Exception: except Exception:
raise ParseError('日均任务数计划失败') raise ParseError('日均任务数计划失败')
attrs["priority"] = attrs.get("priority", 20)
return attrs return attrs
def update(self, instance, validated_data): def update(self, instance, validated_data):

View File

@ -408,14 +408,11 @@ class PmService:
mtask.submit_time = now mtask.submit_time = now
mtask.submit_user = user mtask.submit_user = user
mtask.save() mtask.save()
utask = mtask.utask
if utask:
cls.utask_submit(utask, raise_e=False)
else: else:
raise ParseError('该任务状态不可提交') raise ParseError('该任务状态不可提交')
@classmethod @classmethod
def utask_submit(cls, utask: Utask, raise_e=True): def utask_submit(cls, utask: Utask):
""" """
生产大任务提交 生产大任务提交
""" """
@ -423,5 +420,4 @@ class PmService:
utask.state = Utask.UTASK_SUBMIT utask.state = Utask.UTASK_SUBMIT
utask.save() utask.save()
else: else:
if raise_e: raise ParseError('存在子任务未提交')
raise ParseError('存在子任务未提交')

View File

@ -1,26 +0,0 @@
# Create your tasks here
from __future__ import absolute_import, unicode_literals
from apps.pm.models import Mtask, Utask
from apps.utils.tasks import CustomTask
from celery import shared_task
from datetime import datetime, timedelta
from django.db.models import F
from apps.pm.services import PmService
@shared_task(base=CustomTask)
def complete_mtask():
"""
将2天前未提交的任务且数量已达标的任务标记为已完成
"""
now = datetime.now().date()
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
end_date__lte=now-timedelta(days=2),
count_ok__gte=F('count')).update(state=Mtask.MTASK_SUBMIT)
Mtask.objects.filter(state=Mtask.MTASK_ASSGINED,
end_date__lte=now-timedelta(days=7)).update(state=Mtask.MTASK_SUBMIT)
utasks = Utask.objects.filter(state__in=[Utask.UTASK_ASSGINED, Utask.UTASK_WORKING],
end_date__lte=now-timedelta(days=2))
for utask in utasks:
PmService.utask_submit(utask=utask, raise_e=False)

View File

@ -29,8 +29,7 @@ class UtaskViewSet(CustomModelViewSet):
serializer_class = UtaskSerializer serializer_class = UtaskSerializer
filterset_class = UtaskFilter filterset_class = UtaskFilter
select_related_fields = ['material'] select_related_fields = ['material']
ordering_fields = ['priority', 'start_date'] ordering = ['-start_date']
ordering = ["priority", '-start_date']
def perform_destroy(self, instance): def perform_destroy(self, instance):
if instance.state >= Utask.UTASK_WORKING: if instance.state >= Utask.UTASK_WORKING:
@ -144,8 +143,8 @@ class MtaskViewSet(CustomModelViewSet):
filterset_class = MtaskFilter filterset_class = MtaskFilter
select_related_fields = ['material_in', 'material_out', 'mgroup'] select_related_fields = ['material_in', 'material_out', 'mgroup']
prefetch_related_fields = ['mlog_mtask', 'b_mtask'] prefetch_related_fields = ['mlog_mtask', 'b_mtask']
ordering_fields = ["utask__priority", 'start_date', 'mgroup__process__sort', 'create_time'] ordering_fields = ['start_date', 'mgroup__process__sort', 'create_time']
ordering = ["utask__priority", '-start_date', 'route__sort', 'mgroup__process__sort', '-create_time'] ordering = ['-start_date', 'route__sort', 'mgroup__process__sort', '-create_time']
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MtaskDaySerializer) @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MtaskDaySerializer)
@transaction.atomic @transaction.atomic

View File

@ -43,6 +43,7 @@ class PuPlanItemSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['pu_order'] read_only_fields = EXCLUDE_FIELDS + ['pu_order']
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
pu_plan = validated_data['pu_plan'] pu_plan = validated_data['pu_plan']
if pu_plan.state != PuPlan.PUPLAN_CREATE: if pu_plan.state != PuPlan.PUPLAN_CREATE:
@ -64,6 +65,7 @@ class PuPlanItemSerializer(CustomModelSerializer):
else: else:
validated_data['belong_dept'] = belong_dept validated_data['belong_dept'] = belong_dept
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop('pu_plan') validated_data.pop('pu_plan')
pu_plan = instance.pu_plan pu_plan = instance.pu_plan
@ -112,6 +114,7 @@ class PuOrderItemSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count'] read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count']
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
pu_order = validated_data['pu_order'] pu_order = validated_data['pu_order']
material = validated_data['material'] material = validated_data['material']
@ -123,6 +126,7 @@ class PuOrderItemSerializer(CustomModelSerializer):
PumService.cal_pu_order_total_price(pu_order) PumService.cal_pu_order_total_price(pu_order)
return ins return ins
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop('material') validated_data.pop('material')
validated_data.pop('pu_order') validated_data.pop('pu_order')

View File

@ -2,24 +2,20 @@ from rest_framework.exceptions import ValidationError
from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder
from django.db.models import F, Sum from django.db.models import F, Sum
from apps.inm.models import MIO, MIOItem from apps.inm.models import MIO, MIOItem
from rest_framework.exceptions import ParseError
class PumService: class PumService:
@staticmethod
def cal_pu_order_total_price(puorder: PuOrder): def cal_pu_order_total_price(puorder: PuOrder):
total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0 total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0
puorder.total_price = total_price puorder.total_price = total_price
puorder.save() puorder.save()
@staticmethod
def cal_pu_plan_total_price(puplan: PuPlan): def cal_pu_plan_total_price(puplan: PuPlan):
total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0 total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0
puplan.total_price = total_price puplan.total_price = total_price
puplan.save() puplan.save()
@staticmethod
def change_puplan_state_when_puorder_sumbit(puorder: PuOrder): def change_puplan_state_when_puorder_sumbit(puorder: PuOrder):
puplanIds = PuPlanItem.objects.filter( puplanIds = PuPlanItem.objects.filter(
pu_order=puorder).values_list('pu_plan', flat=True) pu_order=puorder).values_list('pu_plan', flat=True)
@ -33,45 +29,24 @@ class PumService:
puplan.state = state puplan.state = state
puplan.save() puplan.save()
@staticmethod def mio_purin(mio: MIO, is_reverse: bool = False):
def mio_pur(mio: MIO, is_reverse: bool = False, mioitem: MIOItem = None):
""" """
采购入库成功后的操作 采购入库成功后的操作
""" """
pu_order = mio.pu_order pu_order = mio.pu_order
if pu_order is None: if pu_order is None:
return return
if mioitem is None: for i in MIOItem.objects.filter(mio=mio):
qs = MIOItem.objects.filter(mio=mio) pu_orderitem = PuOrderItem.objects.get(
else: material=i.material, pu_order=pu_order)
qs = MIOItem.objects.filter(id=mioitem.id)
if mio.type == MIO.MIO_TYPE_PUR_IN:
if is_reverse: if is_reverse:
xtype = "out"
else:
xtype = "in"
elif mio.type == MIO.MIO_TYPE_PUR_OUT:
if is_reverse:
xtype = "in"
else:
xtype = "out"
for i in qs:
try:
pu_orderitem = PuOrderItem.objects.get(
material=i.material, pu_order=pu_order)
except PuOrderItem.DoesNotExist:
raise ParseError(f'{str(i.material)}-采购订单中不存在该物料')
if xtype == "out":
delivered_count = pu_orderitem.delivered_count - i.count delivered_count = pu_orderitem.delivered_count - i.count
else: else:
delivered_count = pu_orderitem.delivered_count + i.count delivered_count = pu_orderitem.delivered_count + i.count
if delivered_count > pu_orderitem.count: if delivered_count > pu_orderitem.count:
raise ValidationError(f'{str(i.material)}-超出采购订单所需数量') raise ValidationError(f'{i.material.name}-超出采购订单所需数量')
elif delivered_count < 0: elif delivered_count < 0:
raise ValidationError(f'{str(i.material)}-数量小于0') raise ValidationError(f'{i.material.name}-数量小于0')
pu_orderitem.delivered_count = delivered_count pu_orderitem.delivered_count = delivered_count
pu_orderitem.save() pu_orderitem.save()
pu_order_state = PuOrder.PUORDER_SHIP pu_order_state = PuOrder.PUORDER_SHIP
@ -94,4 +69,3 @@ class PumService:
if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE: if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE:
puplan.state = PuPlan.PUPLAN_DONE puplan.state = PuPlan.PUPLAN_DONE
puplan.save() puplan.save()

View File

@ -80,6 +80,7 @@ class PuPlanItemViewSet(CustomModelViewSet):
ordering_fields = ['create_time', 'material', 'need_date', 'need_count'] ordering_fields = ['create_time', 'material', 'need_date', 'need_count']
ordering = ['create_time'] ordering = ['create_time']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
user = self.request.user user = self.request.user
pu_plan = instance.pu_plan pu_plan = instance.pu_plan
@ -103,6 +104,7 @@ class PuOrderViewSet(CustomModelViewSet):
search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model'] search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model']
select_related_fields = ['create_by', 'update_by', 'supplier'] select_related_fields = ['create_by', 'update_by', 'supplier']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
if instance.state != PuOrder.PUORDER_CREATE: if instance.state != PuOrder.PUORDER_CREATE:
raise ParseError('采购订单非创建中不可删除') raise ParseError('采购订单非创建中不可删除')
@ -143,6 +145,7 @@ class PuOrderItemViewSet(CustomModelViewSet):
filterset_fields = ['material', 'pu_order'] filterset_fields = ['material', 'pu_order']
ordering = ['create_time'] ordering = ['create_time']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
pu_order = instance.pu_order pu_order = instance.pu_order
if pu_order.state != PuOrder.PUORDER_CREATE: if pu_order.state != PuOrder.PUORDER_CREATE:

View File

@ -25,8 +25,6 @@ class QctFilter(filters.FilterSet):
"testitems": ["exact"], "testitems": ["exact"],
"defects": ["exact"], "defects": ["exact"],
"qctmat__material": ["exact"], "qctmat__material": ["exact"],
"qctmat__use_for_in": ["exact"],
"qctmat__use_for_out": ["exact"],
"qctmat__tracing": ["exact"], "qctmat__tracing": ["exact"],
} }
@ -40,24 +38,18 @@ class TestItemFilter(filters.FilterSet):
class FtestWorkFilter(filters.FilterSet): class FtestWorkFilter(filters.FilterSet):
cbatch = filters.CharFilter(label='批次号', method='filter_cbatch')
class Meta: class Meta:
model = FtestWork model = FtestWork
fields = { fields = {
"material__process__name": ["exact", "contains"], "material__process__name": ["exact", "contains"],
"material": ["exact"], "material": ["exact"],
"wm": ["exact", "isnull"], "wm": ["exact"],
"mb": ["exact", "isnull"], "mb": ["exact"],
"batch": ["exact"], "batch": ["exact"],
"type": ["exact"], "type": ["exact"],
"type2": ["exact"], "type2": ["exact"],
"shift": ["exact"] "shift": ["exact"]
} }
def filter_cbatch(self, queryset, name, value):
qs1 = queryset.filter(wm__batch=value)
qs2 = queryset.filter(mb__batch=value)
return qs1.union(qs2)
class FtestFilter(filters.FilterSet): class FtestFilter(filters.FilterSet):
wpr = filters.CharFilter(label="wprId", method="filter_wpr") wpr = filters.CharFilter(label="wprId", method="filter_wpr")

View File

@ -1,23 +0,0 @@
# Generated by Django 3.2.12 on 2025-07-18 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0051_alter_ftestwork_batch'),
]
operations = [
migrations.AddField(
model_name='qctmat',
name='use_for_in',
field=models.BooleanField(default=True, verbose_name='可用于消耗'),
),
migrations.AddField(
model_name='qctmat',
name='use_for_out',
field=models.BooleanField(default=True, verbose_name='可用于产出'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-05 01:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0052_auto_20250718_1558'),
]
operations = [
migrations.AlterField(
model_name='ftest',
name='is_ok',
field=models.BooleanField(default=True, verbose_name='是否合格'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-10 01:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qm', '0053_alter_ftest_is_ok'),
]
operations = [
migrations.AlterField(
model_name='ptest',
name='val_xj',
field=models.CharField(blank=True, choices=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], help_text=[('S', '析晶'), ('R', '不析晶'), ('θ', '未化')], max_length=10, null=True, verbose_name='析晶'),
),
]

View File

@ -172,14 +172,9 @@ class Qct(CommonAModel):
return QctMat.objects.filter(qct=self) return QctMat.objects.filter(qct=self)
@classmethod @classmethod
def get(cls, material:Material, tag:str, type:str=None): def get(cls, material:Material, tag:str):
try: try:
if type == "in": qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_in=True)
elif type == "out":
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_out=True)
else:
qct = Qct.objects.get(qctmat__material=material, tags__contains=tag)
except Qct.DoesNotExist: except Qct.DoesNotExist:
try: try:
qct = Qct.objects.get(name="默认检验表") qct = Qct.objects.get(name="默认检验表")
@ -200,12 +195,8 @@ class Qct(CommonAModel):
return None return None
@classmethod @classmethod
def get_qs(cls, materialId:str, tag:str, type:str): def get_qs(cls, materialId:str, tag:str):
qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag) qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag)
if type == "in":
qct_qs = qct_qs.filter(qctmat__use_for_in=True)
elif type == "out":
qct_qs = qct_qs.filter(qctmat__use_for_out=True)
if not qct_qs.exists(): if not qct_qs.exists():
qct_qs = Qct.objects.filter(name="默认检验表") qct_qs = Qct.objects.filter(name="默认检验表")
return qct_qs return qct_qs
@ -233,8 +224,6 @@ class QctMat(BaseModel):
material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE) material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE)
tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES, tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES,
max_length=20, help_text=str(QC_TRACE_CHOICES)) max_length=20, help_text=str(QC_TRACE_CHOICES))
use_for_in = models.BooleanField("可用于消耗", default=True)
use_for_out = models.BooleanField("可用于产出", default=True)
max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True) max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True)
@ -337,7 +326,7 @@ class Ftest(CommonBDModel):
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user') User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user')
check_user = models.ForeignKey( check_user = models.ForeignKey(
User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True) User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True)
is_ok = models.BooleanField('是否合格', default=True) is_ok = models.BooleanField('是否合格', default=False)
note = models.TextField('备注', default='', blank=True) note = models.TextField('备注', default='', blank=True)
ftest_work = models.ForeignKey( ftest_work = models.ForeignKey(
FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True) FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True)
@ -353,12 +342,13 @@ class Ftest(CommonBDModel):
@classmethod @classmethod
def init_by_qct(cls, qct, test_user, test_date): def init_by_qct(cls, qct, test_user, test_date):
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date) with transaction.atomic():
for testitem in qct.testitems.all(): ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
FtestItem.objects.create(ftest=ftest, testitem=testitem) for testitem in qct.testitems.all():
for defect in qct.defects.all(): FtestItem.objects.create(ftest=ftest, testitem=testitem)
FtestDefect.objects.create(ftest=ftest, defect=defect) for defect in qct.defects.all():
return ftest FtestDefect.objects.create(ftest=ftest, defect=defect)
return ftest
# @classmethod # @classmethod
# def cal_count_notok(cls, ftestwork: FtestWork): # def cal_count_notok(cls, ftestwork: FtestWork):
@ -445,7 +435,7 @@ class Ptest(CommonAModel):
val_tg = models.FloatField("Tg", help_text='', null=True, blank=True) val_tg = models.FloatField("Tg", help_text='', null=True, blank=True)
val_tf = models.FloatField("Tf", help_text='', null=True, blank=True) val_tf = models.FloatField("Tf", help_text='', null=True, blank=True)
val_xj = models.CharField( val_xj = models.CharField(
'析晶', max_length=10, null=True, blank=True, choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS)) '析晶', max_length=10, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
val_pzxs = models.FloatField( val_pzxs = models.FloatField(
'膨胀系数', help_text='30-300℃', null=True, blank=True) '膨胀系数', help_text='30-300℃', null=True, blank=True)
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True) val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)

Some files were not shown because too many files have changed in this diff Show More