Compare commits
	
		
			No commits in common. "master" and "2.6.2025060913" have entirely different histories.
		
	
	
		
			master
			...
			2.6.202506
		
	
		|  | @ -20,10 +20,6 @@ class WxCodeSerializer(serializers.Serializer): | |||
|     code = serializers.CharField(label="code") | ||||
| 
 | ||||
| 
 | ||||
| class UserIdSerializer(serializers.Serializer): | ||||
|     user_id = serializers.CharField(label="用户id") | ||||
| 
 | ||||
| 
 | ||||
| class PwResetSerializer(serializers.Serializer): | ||||
|     phone = serializers.CharField(label="手机号") | ||||
|     code = serializers.CharField(label="验证码") | ||||
|  |  | |||
|  | @ -3,8 +3,7 @@ from django.urls import path | |||
| from rest_framework_simplejwt.views import TokenRefreshView | ||||
| 
 | ||||
| from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView, | ||||
|                               SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin,  | ||||
|                               TokenLoginView, FaceLoginView, UserIdLogin) | ||||
|                               SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView) | ||||
| 
 | ||||
| API_BASE_URL = 'api/auth/' | ||||
| urlpatterns = [ | ||||
|  | @ -19,6 +18,5 @@ urlpatterns = [ | |||
|     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 + '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_userid/', UserIdLogin.as_view(), name='userid_login'), | ||||
|     path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login') | ||||
| ] | ||||
|  |  | |||
|  | @ -23,8 +23,7 @@ from apps.auth1.serializers import FaceLoginSerializer | |||
| 
 | ||||
| 
 | ||||
| from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer, | ||||
|                                     PwResetSerializer, SecretLoginSerializer,  | ||||
|                                     SendCodeSerializer, WxCodeSerializer, UserIdSerializer) | ||||
|                                     PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer) | ||||
| from apps.system.models import User | ||||
| from rest_framework_simplejwt.views import TokenObtainPairView | ||||
| from apps.auth1.authentication import get_user_by_username_or | ||||
|  | @ -235,29 +234,6 @@ class SecretLogin(CreateAPIView): | |||
|             return Response(ret) | ||||
|         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): | ||||
|     """重置密码 | ||||
|  |  | |||
|  | @ -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='启用'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -12,7 +12,6 @@ class Dataset(CommonBDModel): | |||
|     test_param = models.JSONField('测试查询参数', default=dict, blank=True) | ||||
|     default_param = models.JSONField('默认查询参数', default=dict, blank=True) | ||||
|     cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True) | ||||
|     enabled = models.BooleanField('启用', default=True) | ||||
| 
 | ||||
| 
 | ||||
| # class Report(CommonBDModel): | ||||
|  |  | |||
|  | @ -5,19 +5,16 @@ from apps.bi.models import Dataset | |||
| import concurrent | ||||
| 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): | ||||
|     """检查sql安全性 | ||||
|     """ | ||||
|     sql_upper = sql.upper() | ||||
|     # 将SQL按空格和分号分割成单词 | ||||
|     words = [word for word in sql_upper.replace(';', ' ').split() if word] | ||||
|     for kw in forbidden_keywords: | ||||
|         # 检查关键字是否作为独立单词出现 | ||||
|         if kw in words: | ||||
|             raise ParseError(f'sql查询有风险-{kw}') | ||||
|         if kw in sql_upper: | ||||
|             raise ParseError('sql查询有风险') | ||||
|     return sql | ||||
| 
 | ||||
| def format_json_with_placeholders(json_str, **kwargs): | ||||
|  |  | |||
|  | @ -64,8 +64,6 @@ class DatasetViewSet(CustomModelViewSet): | |||
|         执行sql查询支持code | ||||
|         """ | ||||
|         dt: Dataset = self.get_object() | ||||
|         if not dt.enabled: | ||||
|             raise ParseError(f'{dt.name}-该查询未启用') | ||||
|         rdata = DatasetSerializer(instance=dt).data | ||||
|         xquery = request.data.get('query', {}) | ||||
|         is_test = request.data.get('is_test', False) | ||||
|  |  | |||
|  | @ -102,7 +102,7 @@ class LabelTemplateViewSet(CustomModelViewSet): | |||
|     serializer_class = LabelTemplateSerializer | ||||
|     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): | ||||
|         """ | ||||
|         获取标签指令 | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals | |||
| from celery import shared_task | ||||
| import subprocess | ||||
| from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD | ||||
| from django.conf import settings | ||||
| import logging | ||||
| myLogger = logging.getLogger('log') | ||||
| 
 | ||||
|  | @ -15,10 +14,8 @@ def backup_database(): | |||
|     import datetime | ||||
| 
 | ||||
|     name = datetime.datetime.now().strftime("%Y%m%d%H%M%S") | ||||
|     exclude_tables = getattr(settings, 'EXCLUDE_TABLE_DATA', []) | ||||
|     exclude_str = ' '.join([f"--exclude-table-data={table}" for table in exclude_tables])                          | ||||
|     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 | ||||
|     command = 'echo "{}" | sudo -S pg_dump "user={} password={} dbname={}" > {}/bak_{}.sql'.format( | ||||
|         SD_PWD, DATABASES["default"]["USER"], DATABASES["default"]["PASSWORD"], DATABASES["default"]["NAME"], BACKUP_PATH + "/database", name | ||||
|     ) | ||||
|     completed = subprocess.run(command, shell=True, capture_output=True, text=True) | ||||
|     if completed.returncode != 0: | ||||
|  |  | |||
							
								
								
									
										212
									
								
								apps/em/cd.py
								
								
								
								
							
							
						
						
									
										212
									
								
								apps/em/cd.py
								
								
								
								
							|  | @ -4,19 +4,14 @@ import json | |||
| import time | ||||
| from django.core.cache import cache | ||||
| from apps.utils.thread import MyThread | ||||
| import uuid | ||||
| import logging | ||||
| import threading | ||||
| import requests | ||||
| 
 | ||||
| myLogger = logging.getLogger('log') | ||||
| import struct | ||||
| 
 | ||||
| def get_checksum(body_msg): | ||||
|     return sum(body_msg) & 0xFF | ||||
| 
 | ||||
| def handle_bytes(arr): | ||||
|     if len(arr) < 8: | ||||
|         return f"返回数据长度错误-{arr}" | ||||
|         return "返回数据长度错误" | ||||
|      | ||||
|     if arr[0] != 0xEB or arr[1] != 0x90: | ||||
|         return "数据头不正确" | ||||
|  | @ -24,7 +19,7 @@ def handle_bytes(arr): | |||
| 
 | ||||
|     # 读取长度信息 | ||||
|     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] | ||||
|  | @ -45,9 +40,8 @@ def handle_bytes(arr): | |||
|      | ||||
|     return res[0] | ||||
| 
 | ||||
| def get_tyy_data_t(host, port, tid): | ||||
|     cd_thread_key_id = f"cd_thread_{host}_{port}_id" | ||||
|     cd_thread_key_val = f"cd_thread_{host}_{port}_val" | ||||
| def get_tyy_data_t(host, port): | ||||
|     cd_thread_key = f"cd_thread_{host}_{port}" | ||||
|     sc = None | ||||
|     def connect_and_send(retry=1): | ||||
|         nonlocal sc | ||||
|  | @ -57,62 +51,55 @@ def get_tyy_data_t(host, port, tid): | |||
|                 sc.connect((host, int(port))) | ||||
|             sc.sendall(b"R") | ||||
|         except BrokenPipeError: | ||||
|             sc = None | ||||
|             if retry > 0: | ||||
|                 connect_and_send(retry-1) | ||||
|             else: | ||||
|         except OSError as e: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key, {"err_msg": f"采集器连接失败-{str(e)}"}) | ||||
|         except ConnectionResetError: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key, {"err_msg": "采集器重置了连接"}) | ||||
|         except socket.timeout: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key, {"err_msg": "采集器连接超时"}) | ||||
|         except Exception as e: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key, {"err_msg": f"采集器连接失败-{str(e)}"}) | ||||
| 
 | ||||
|     while True: | ||||
|         cd_thread_val = cache.get(cd_thread_key, default=None) | ||||
|         if cd_thread_val is None: | ||||
|             if sc: | ||||
|                 try: | ||||
|                     sc.close() | ||||
|                 except Exception: | ||||
|                     pass | ||||
|                 sc = None | ||||
|         except OSError as e: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"}) | ||||
|         except ConnectionResetError: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key_val, {"err_msg": "采集器重置了连接"}) | ||||
|         except socket.timeout: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key_val, {"err_msg": "采集器连接超时"}) | ||||
|         except Exception as e: | ||||
|             sc = None | ||||
|             cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"}) | ||||
| 
 | ||||
|     while cache.get(cd_thread_key_id) == tid: | ||||
|         if cache.get(cd_thread_key_val) == "get": | ||||
|             cache.set(cd_thread_key_val, "working") | ||||
|             break | ||||
|         elif cd_thread_val == "get": | ||||
|             cache.set(cd_thread_key, "working") | ||||
|             connect_and_send() | ||||
|             if sc is None: | ||||
|                 continue | ||||
|             resp = sc.recv(1024) | ||||
|             res = handle_bytes(resp) | ||||
|             if isinstance(res, str): | ||||
|                 cache.set(cd_thread_key_val, {"err_msg": f'采集器返回数据错误-{res}'}) | ||||
|             elif not res: | ||||
|                 cache.set(cd_thread_key_val, {"err_msg": f"采集器返回数据为空-{str(res)}"}) | ||||
|                 cache.set(cd_thread_key, {"err_msg": f'采集器返回数据错误-{res}'}) | ||||
|             else: | ||||
|                 myLogger.info(f"采集器返回数据-{res}") | ||||
|                 cache.set(cd_thread_key_val, res) | ||||
|                 cache.set(cd_thread_key, res) | ||||
|         time.sleep(0.3) | ||||
| 
 | ||||
|     if sc: | ||||
|         try: | ||||
|             sc.close() | ||||
|         except Exception: | ||||
|             pass | ||||
| 
 | ||||
| def get_tyy_data_2(*args, retry=1): | ||||
| def get_tyy_data(*args, sleep=0): | ||||
|     if sleep > 0: | ||||
|         time.sleep(sleep) | ||||
|     host, port = args[0], int(args[1]) | ||||
|     cd_thread_key_id = f"cd_thread_{host}_{port}_id" | ||||
|     cd_thread_key_val = f"cd_thread_{host}_{port}_val" | ||||
|     cd_thread_val_id = cache.get(cd_thread_key_id, default=None) | ||||
|     if cd_thread_val_id is None: | ||||
|         tid = uuid.uuid4() | ||||
|         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_key = f"cd_thread_{host}_{port}" | ||||
|     cd_thread_val = cache.get(cd_thread_key, default=None) | ||||
|     if cd_thread_val is None: | ||||
|         cache.set(cd_thread_key, "start") | ||||
|         cd_thread = MyThread(target=get_tyy_data_t, args=(host, port), daemon=True) | ||||
|         cd_thread.start() | ||||
|     cache.set(cd_thread_key_val, "get") | ||||
|     cache.set(cd_thread_key, "get") | ||||
|     num = 0 | ||||
|     get_val = False | ||||
| 
 | ||||
|  | @ -120,7 +107,7 @@ def get_tyy_data_2(*args, retry=1): | |||
|         num += 1 | ||||
|         if num > 8: | ||||
|             break | ||||
|         val = cache.get(cd_thread_key_val) | ||||
|         val = cache.get(cd_thread_key) | ||||
|         if isinstance(val, dict): | ||||
|             get_val = True | ||||
|             if "err_msg" in val: | ||||
|  | @ -128,125 +115,10 @@ def get_tyy_data_2(*args, retry=1): | |||
|             return val | ||||
|         time.sleep(0.3) | ||||
| 
 | ||||
|     if not get_val and retry > 0: | ||||
|         cache.set(cd_thread_key_id, None) | ||||
|         get_tyy_data_2(*args, retry=retry-1) | ||||
|     if not get_val: | ||||
|         cache.set(cd_thread_key, None) | ||||
|         get_tyy_data(*args, sleep=2) | ||||
| 
 | ||||
| 
 | ||||
| sc_all = {} | ||||
| sc_lock = threading.Lock() | ||||
| 
 | ||||
| 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 | ||||
| if __name__ == '__main__': | ||||
|     print(get_tyy_data()) | ||||
|  | @ -180,9 +180,7 @@ class CdView(MyLoggingMixin, APIView): | |||
| 
 | ||||
|         执行采集数据方法 | ||||
|         """ | ||||
|         method = request.data.get("method", None) | ||||
|         if not method: | ||||
|             raise ParseError("请传入method参数") | ||||
|         method = request.data.get("method") | ||||
|         m = method.split("(")[0] | ||||
|         args = method.split("(")[1].split(")")[0].split(",") | ||||
|         module, func = m.rsplit(".", 1) | ||||
|  |  | |||
|  | @ -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='与工段运行状态的关联'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -62,7 +62,7 @@ class Mpoint(CommonBModel): | |||
|     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) | ||||
|     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 | ||||
|     def cache_key(cls, code: str): | ||||
|  |  | |||
|  | @ -86,10 +86,10 @@ def db_ins_mplogx(): | |||
|         if bill_date is None: | ||||
|             raise Exception("bill_date is None") | ||||
|         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 | ||||
|                     WHERE bill_date >= %s and de_real_quantity > 0 | ||||
|                     AND inv_code IN %s | ||||
|                     AND inv_name IN %s | ||||
|                     ORDER BY bill_date | ||||
|                 """ | ||||
|         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: | ||||
| 
 | ||||
|         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: | ||||
| 
 | ||||
|         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: | ||||
| 
 | ||||
|         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) | ||||
|             else: | ||||
|                 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) | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,19 +1,18 @@ | |||
| from django.urls import path, include | ||||
| from rest_framework.routers import DefaultRouter | ||||
| from apps.enm.views import (MpointViewSet, MpointStatViewSet,  | ||||
|                             EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView) | ||||
| from apps.enm.views import (MpointViewSet, MpLogxViewSet, MpointStatViewSet,  | ||||
|                             EnStatViewSet, EnStat2ViewSet, XscriptViewSet) | ||||
| 
 | ||||
| API_BASE_URL = 'api/enm/' | ||||
| HTML_BASE_URL = 'dhtml/enm/' | ||||
| 
 | ||||
| router = DefaultRouter() | ||||
| 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('enstat', EnStatViewSet, basename='enstat') | ||||
| router.register('enstat2', EnStat2ViewSet, basename='enstat2') | ||||
| router.register('xscript', XscriptViewSet, basename='xscript') | ||||
| urlpatterns = [ | ||||
|     path(API_BASE_URL, include(router.urls)), | ||||
|     path(f'{API_BASE_URL}mplogx/', MpLogxAPIView.as_view(), name='mplogx_list'), | ||||
| ] | ||||
|  | @ -12,7 +12,6 @@ from apps.enm.tasks import cal_mpointstat_manual | |||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import Serializer | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.views import APIView | ||||
| from apps.enm.tasks import cal_mpointstats_duration | ||||
| from apps.enm.services import king_sync, MpointCache | ||||
| 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 | ||||
| import logging | ||||
| 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') | ||||
| 
 | ||||
| class MpointViewSet(CustomModelViewSet): | ||||
|     """ | ||||
|     list:测点 | ||||
|  | @ -91,34 +84,6 @@ class MpointViewSet(CustomModelViewSet): | |||
|         king_sync(getattr(settings, "KING_PROJECTNAME", "")) | ||||
|         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): | ||||
|     """ | ||||
|  | @ -173,97 +138,6 @@ class XscriptViewSet(CustomModelViewSet): | |||
| #     select_related_fields = ['mpoint'] | ||||
| #     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): | ||||
|     """ | ||||
|  |  | |||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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文件的按钮和功能。 | ||||
|  | @ -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 | ||||
|  | @ -2,7 +2,6 @@ | |||
| from django.urls import path, include | ||||
| from rest_framework.routers import DefaultRouter | ||||
| from apps.ichat.views import QueryLLMviewSet, ConversationViewSet | ||||
| from apps.ichat.views2 import WorkChain | ||||
| 
 | ||||
| API_BASE_URL = 'api/ichat/' | ||||
| 
 | ||||
|  | @ -12,5 +11,4 @@ router.register('conversation', ConversationViewSet, basename='conversation') | |||
| router.register('message', QueryLLMviewSet, basename='message') | ||||
| urlpatterns = [ | ||||
|     path(API_BASE_URL, include(router.urls)), | ||||
|     path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain') | ||||
| ] | ||||
|  |  | |||
|  | @ -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月的生产合格数等并形成报告')) | ||||
|  | @ -39,6 +39,7 @@ 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 | ||||
|         # 先处理库存 | ||||
|         try: | ||||
|             with transaction.atomic(): | ||||
|                 MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok) | ||||
|                 InmService.update_mb_after_test(mi) | ||||
|         except ParseError as e: | ||||
|  |  | |||
|  | @ -37,9 +37,7 @@ class MioFilter(filters.FilterSet): | |||
|             "item_mio__test_user": ["isnull"], | ||||
|             "item_mio__w_mioitem__number": ["exact"], | ||||
|             "mgroup": ["exact"], | ||||
|             "item_mio__batch": ["exact"], | ||||
|             "inout_date": ["gte", "lte", "exact"], | ||||
|             "belong_dept": ["exact"] | ||||
|             "item_mio__batch": ["exact"] | ||||
|         } | ||||
| 
 | ||||
|     def filter_materials__type(self, queryset, name, value): | ||||
|  |  | |||
|  | @ -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='单价'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='出入库类型'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='出入库类型'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='对外编号'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='关联装箱单'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='装箱序号'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -5,7 +5,7 @@ from apps.sam.models import Customer, Order | |||
| from apps.mtm.models import Material, Mgroup | ||||
| from apps.system.models import User | ||||
| from datetime import datetime | ||||
| from django.db.models import Max, Sum | ||||
| from django.db.models import Max | ||||
| # 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) | ||||
| 
 | ||||
| 
 | ||||
|     @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): | ||||
|     """ | ||||
|     TN:组合件物料批次 | ||||
|  | @ -56,15 +52,12 @@ class MaterialBatchA(BaseModel): | |||
| 
 | ||||
| 
 | ||||
| MIO_TYPE_PREFIX = { | ||||
|     'do_in': 'SCRK',      # 生产入库 | ||||
|     'do_out': 'SCLL',     # 生产领料 | ||||
|     'sale_out': 'XSFH',   # 销售发货 | ||||
|     'pur_in': 'CGRK',     # 采购入库 | ||||
|     'pur_out': 'CGTH',    # 采购退货 | ||||
|     'borrow_out': 'LYCK',   # 领用出库 | ||||
|     'return_in': 'THRK',    # 退还入库 | ||||
|     'other_in': 'QTRK',   # 其他入库 | ||||
|     'other_out': 'QTCK'   # 其他出库 | ||||
|     'do_out': 'SCLL',     # 生产领料 (Shēngchǎn Lǐngliào) | ||||
|     'sale_out': 'XSFH',   # 销售发货 (Xiāoshòu Fāhuò) | ||||
|     'pur_in': 'CGRK',     # 采购入库 (Cǎigòu Rùkù) | ||||
|     'do_in': 'SCRK',      # 生产入库 (Shēngchǎn Rùkù) | ||||
|     'other_in': 'QTRK',   # 其他入库 (Qítā Rùkù) | ||||
|     'other_out': 'QTCK'   # 其他出库 (Qítā Chūkù) | ||||
| } | ||||
| 
 | ||||
| class MIO(CommonBDModel): | ||||
|  | @ -74,21 +67,15 @@ class MIO(CommonBDModel): | |||
|     MIO_TYPE_DO_OUT = 'do_out' | ||||
|     MIO_TYPE_SALE_OUT = 'sale_out' | ||||
|     MIO_TYPE_PUR_IN = 'pur_in' | ||||
|     MIO_TYPE_PUR_OUT = 'pur_out' | ||||
|     MIO_TYPE_DO_IN = 'do_in' | ||||
|     MIO_TYPE_OTHER_IN = 'other_in' | ||||
|     MIO_TYPE_OTHER_OUT = 'other_out' | ||||
|     MIO_TYPE_BORROW_OUT = 'borrow_out' | ||||
|     MIO_TYPE_RETURN_IN = 'return_in' | ||||
| 
 | ||||
|     MIO_TYPES = ( | ||||
|         (MIO_TYPE_DO_OUT, '生产领料'), | ||||
|         (MIO_TYPE_SALE_OUT, '销售发货'), | ||||
|         (MIO_TYPE_PUR_IN, '采购入库'), | ||||
|         (MIO_TYPE_PUR_OUT, '采购退货'), | ||||
|         (MIO_TYPE_DO_IN, '生产入库'), | ||||
|         (MIO_TYPE_BORROW_OUT, '领用出库'), | ||||
|         (MIO_TYPE_RETURN_IN, '退还入库'), | ||||
|         (MIO_TYPE_OTHER_IN, '其他入库'), | ||||
|         (MIO_TYPE_OTHER_OUT, '其他出库') | ||||
|     ) | ||||
|  | @ -137,13 +124,6 @@ class MIO(CommonBDModel): | |||
|             last_number = 1 | ||||
|         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): | ||||
|     """ | ||||
|     TN:出入库明细 | ||||
|  | @ -159,7 +139,6 @@ class MIOItem(BaseModel): | |||
|     material = models.ForeignKey( | ||||
|         Material, verbose_name='物料', on_delete=models.CASCADE) | ||||
|     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_tested = models.PositiveIntegerField('已检数', 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) | ||||
| 
 | ||||
|     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 | ||||
|     def count_fields(cls): | ||||
|  | @ -227,7 +201,6 @@ class MIOItemw(BaseModel): | |||
|     TN:单件记录 | ||||
|     """ | ||||
|     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' | ||||
|                             , null=True, blank=True) | ||||
|     mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem') | ||||
|  |  | |||
|  | @ -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.serializers import CustomModelSerializer | ||||
| 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 server.settings import get_sysconfig | ||||
| from apps.wpmw.models import Wpr | ||||
| from decimal import Decimal | ||||
| 
 | ||||
| 
 | ||||
| class WareHourseSerializer(CustomModelSerializer): | ||||
|  | @ -30,15 +29,6 @@ class MaterialBatchAListSerializer(CustomModelSerializer): | |||
|         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): | ||||
|     warehouse_name = serializers.CharField( | ||||
|         source='warehouse.name', read_only=True) | ||||
|  | @ -48,19 +38,12 @@ class MaterialBatchSerializer(CustomModelSerializer): | |||
|         source='supplier', read_only=True) | ||||
|     material_ = MaterialSerializer(source='material', read_only=True) | ||||
|     defect_name = serializers.CharField(source="defect.name", read_only=True) | ||||
|     count_mioing = serializers.IntegerField(read_only=True, label='正在出入库数量') | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = MaterialBatch | ||||
|         fields = '__all__' | ||||
|         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): | ||||
|     warehouse_name = serializers.CharField( | ||||
|  | @ -126,15 +109,14 @@ class MIOItemCreateSerializer(CustomModelSerializer): | |||
|     class Meta: | ||||
|         model = MIOItem | ||||
|         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 = { | ||||
|             '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): | ||||
|         mio:MIO = validated_data['mio'] | ||||
|         mio_type = mio.type | ||||
|         mb = validated_data.get('mb', None) | ||||
|         wm = validated_data.get('wm', None) | ||||
|         assemb = validated_data.pop('assemb', []) | ||||
|  | @ -153,14 +135,9 @@ class MIOItemCreateSerializer(CustomModelSerializer): | |||
|             validated_data["batch"] = wm.batch | ||||
| 
 | ||||
|         material: Material = validated_data['material'] | ||||
|         batch = validated_data.get("batch", None) | ||||
|         if not batch: | ||||
|             batch = "无" | ||||
|         batch = validated_data['batch'] | ||||
|         if material.is_hidden: | ||||
|             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: | ||||
|             raise ParseError('出入库记录非创建中不可新增') | ||||
|  | @ -171,11 +148,11 @@ 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]) | ||||
|             if mis.exists() and (not mis.exclude(test_date=None).exists()): | ||||
|                 raise ParseError('该批次的物料未经检验') | ||||
| 
 | ||||
|         with transaction.atomic(): | ||||
|             count = validated_data["count"] | ||||
|             batch = validated_data["batch"] | ||||
|             mioitemw = validated_data.pop('mioitemw', []) | ||||
|         instance:MIOItem = super().create(validated_data) | ||||
|             instance = super().create(validated_data) | ||||
|             assemb_dict = {} | ||||
|             for i in assemb: | ||||
|                 assemb_dict[i['material'].id] = i | ||||
|  | @ -210,18 +187,9 @@ class MIOItemCreateSerializer(CustomModelSerializer): | |||
|                         raise ParseError('不支持自动生成请提供产品明细') | ||||
|                 elif len(mioitemw) >= 1: | ||||
|                     mio_type = mio.type | ||||
|                 if mio_type != "pur_in" and mio_type != "other_in": | ||||
|                     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 | ||||
|  | @ -234,14 +202,16 @@ class MIOItemAListSerializer(CustomModelSerializer): | |||
| 
 | ||||
|     class Meta: | ||||
|         model = MIOItemA | ||||
|         fields = "__all__" | ||||
|         read_only_fields = EXCLUDE_FIELDS_BASE | ||||
|         fields = ['material', 'batch', 'rate', 'mioitem', | ||||
|                   'id', 'material_', 'material_name'] | ||||
| 
 | ||||
| 
 | ||||
| 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) | ||||
|     assemb = serializers.SerializerMethodField(label="组合件信息") | ||||
|     assemb = MIOItemAListSerializer( | ||||
|         source='a_mioitem', read_only=True, many=True) | ||||
|     material_name = serializers.StringRelatedField( | ||||
|         source='material', read_only=True) | ||||
|     inout_date = serializers.DateField(source='mio.inout_date', read_only=True) | ||||
|  | @ -252,24 +222,6 @@ class MIOItemSerializer(CustomModelSerializer): | |||
|         model = MIOItem | ||||
|         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): | ||||
|  | @ -283,11 +235,8 @@ class MIODoSerializer(CustomModelSerializer): | |||
|     class Meta: | ||||
|         model = MIO | ||||
|         fields = ['id', 'number', 'note', 'do_user', | ||||
|                   'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user', 'type'] | ||||
|         extra_kwargs = {'inout_date': {'required': True},  | ||||
|                         'do_user': {'required': True},  | ||||
|                         'number': {"required": False, "allow_blank": True}, | ||||
|                         'type': {'required': True}} | ||||
|                   'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user'] | ||||
|         extra_kwargs = {'inout_date': {'required': True}, 'do_user': {'required': True}, 'number': {"required": False, "allow_blank": True}} | ||||
| 
 | ||||
|     def validate(self, attrs): | ||||
|         if 'mgroup' in attrs and attrs['mgroup']: | ||||
|  | @ -297,13 +246,10 @@ class MIODoSerializer(CustomModelSerializer): | |||
|         return attrs | ||||
|      | ||||
|     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): | ||||
|             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) | ||||
| 
 | ||||
|     def update(self, instance, validated_data): | ||||
|  | @ -348,17 +294,11 @@ class MIOPurSerializer(CustomModelSerializer): | |||
| 
 | ||||
|     class Meta: | ||||
|         model = MIO | ||||
|         fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user', 'type'] | ||||
|         extra_kwargs = {'inout_date': {'required': True},  | ||||
|                         'number': {"required": False, "allow_blank": True}, | ||||
|                         'type': {'required': True}} | ||||
|         fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user'] | ||||
|         extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}} | ||||
| 
 | ||||
|     def create(self, validated_data): | ||||
|         type = validated_data["type"] | ||||
|         if type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]: | ||||
|             pass | ||||
|         else: | ||||
|             raise ValidationError('出入库类型错误') | ||||
|         validated_data['type'] = MIO.MIO_TYPE_PUR_IN | ||||
|         if not validated_data.get("number", None): | ||||
|             validated_data["number"] = MIO.get_a_number(validated_data["type"]) | ||||
|         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['count_sampling'] = len(attrs['weight_kgs']) | ||||
|         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") | ||||
|  | @ -5,15 +5,13 @@ from django.db import transaction | |||
| from rest_framework.exceptions import ParseError | ||||
| from apps.wpmw.models import Wpr | ||||
| from apps.mtm.models import Material | ||||
| from rest_framework import serializers | ||||
| 
 | ||||
| class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | ||||
|     ftest = FtestProcessSerializer(required=False) | ||||
|     wpr_number_out = serializers.CharField(source="wpr.number_out", read_only=True) | ||||
| 
 | ||||
|     class Meta: | ||||
|         model = MIOItemw | ||||
|         fields = ["id", "number", "wpr", "note", "mioitem", "ftest", "wpr_number_out"] | ||||
|         fields = ["id", "number", "wpr", "note", "mioitem", "ftest"] | ||||
| 
 | ||||
|     def validate(self, attrs): | ||||
|         mioitem: MIOItem = attrs["mioitem"] | ||||
|  | @ -45,6 +43,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | |||
|             ftest_sr.update(instance=ftest, validated_data=ftest_data) | ||||
|         return mioitemw | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def create(self, validated_data): | ||||
|         wpr: Wpr = validated_data.get("wpr", None) | ||||
|         if wpr: | ||||
|  | @ -57,6 +56,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | |||
|             mioitemw = self.save_ftest(mioitemw, ftest_data) | ||||
|         return mioitemw | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def update(self, instance, validated_data): | ||||
|         validated_data.pop("mioitem") | ||||
|         ftest_data = validated_data.pop("ftest", None) | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ from apps.wpmw.models import Wpr | |||
| from apps.qm.models import Ftest, Defect | ||||
| 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 | ||||
|     do_user = mio.do_user | ||||
|     material:Material = item.material | ||||
|     if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目 | ||||
|         return | ||||
|      | ||||
|     # 获取defect | ||||
|     defect:Defect = None | ||||
|  | @ -92,11 +94,11 @@ def do_out(item: MIOItem, is_reverse: bool = False): | |||
|                 raise ParseError(f"批次错误!{e}") | ||||
|             mb.count = mb.count - xcount | ||||
|             if mb.count < 0: | ||||
|                 raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") | ||||
|                 raise ParseError(f"{mb.batch}-批次库存不足,操作失败") | ||||
|             else: | ||||
|                 mb.save() | ||||
| 
 | ||||
|         if material.into_wm: | ||||
| 
 | ||||
|         # 领到车间库存(或工段) | ||||
|         wm, new_create = WMaterial.objects.get_or_create( | ||||
|             batch=xbatch, material=xmaterial, | ||||
|  | @ -112,16 +114,9 @@ def do_out(item: MIOItem, is_reverse: bool = False): | |||
| 
 | ||||
|         # 开始变动wpr | ||||
|         if xmaterial.tracking == Material.MA_TRACKING_SINGLE: | ||||
|             if material.into_wm is False: | ||||
|                 raise ParseError("追踪单个物料不支持不进行车间库存的操作") | ||||
|             mioitemws = MIOItemw.objects.filter(mioitem=item) | ||||
|             if mioitemws.count() != item.count: | ||||
|                 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: | ||||
|                 Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb) | ||||
| 
 | ||||
|  | @ -141,7 +136,8 @@ def do_in(item: MIOItem): | |||
|     mgroup = mio.mgroup | ||||
|     do_user = mio.do_user | ||||
|     material = item.material | ||||
| 
 | ||||
|     if material.into_wm is False:  # 根据配置不进行入车间库存的处理 | ||||
|         return | ||||
|     action_list = [] | ||||
|     mias = MIOItemA.objects.filter(mioitem=item) | ||||
|     is_zhj = False # 是否组合件入仓库 | ||||
|  | @ -176,7 +172,7 @@ def do_in(item: MIOItem): | |||
|             raise ParseError("存在非正数!") | ||||
|          | ||||
|         xbatchs.append(xbatch) | ||||
|         if material.into_wm: | ||||
| 
 | ||||
|         wm_qs = WMaterial.objects.filter( | ||||
|             batch=xbatch,  | ||||
|             material=xmaterial,  | ||||
|  | @ -208,7 +204,6 @@ def do_in(item: MIOItem): | |||
|             production_dept = wm_production_dept | ||||
|         elif wm_production_dept and production_dept != wm_production_dept: | ||||
|             raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间') | ||||
|          | ||||
|         # 增加mb | ||||
|         if not is_zhj: | ||||
|             mb, _ = MaterialBatch.objects.get_or_create( | ||||
|  | @ -231,16 +226,9 @@ def do_in(item: MIOItem): | |||
| 
 | ||||
|             # 开始变动wpr | ||||
|             if xmaterial.tracking == Material.MA_TRACKING_SINGLE: | ||||
|                 if material.into_wm is False: | ||||
|                     raise ParseError("追踪单个物料不支持不进行车间库存的操作") | ||||
|                 mioitemws = MIOItemw.objects.filter(mioitem=item) | ||||
|                 if mioitemws.count() != item.count: | ||||
|                     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: | ||||
|                     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 is_reverse: | ||||
|  | @ -293,32 +278,25 @@ class InmService: | |||
|             else: | ||||
|                 for item in MIOItem.objects.filter(mio=instance): | ||||
|                     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 | ||||
|             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_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) | ||||
|             PumService.mio_purin(instance, is_reverse) | ||||
|         elif instance.type == MIO.MIO_TYPE_OTHER_IN: | ||||
|             if is_reverse: | ||||
|                 BatchLog.clear(mio=instance) | ||||
|             else: | ||||
|                 for item in MIOItem.objects.filter(mio=instance): | ||||
|                     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: | ||||
|                 cls.update_mb(instance, -1) | ||||
|             else: | ||||
|                 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) | ||||
|             if is_reverse: | ||||
|                 for item in mioitems: | ||||
|  | @ -326,14 +304,6 @@ class InmService: | |||
|             else: | ||||
|                 for item in mioitems: | ||||
|                     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: | ||||
|             from apps.sam.services import SamService | ||||
|             if is_reverse: | ||||
|  | @ -346,6 +316,14 @@ class InmService: | |||
|                 cls.update_mb(instance, 1) | ||||
|             else: | ||||
|                 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: | ||||
|             raise ParseError('不支持该出入库操作') | ||||
| 
 | ||||
|  | @ -371,7 +349,6 @@ class InmService: | |||
|         out = -1 | ||||
|         默认使用count字段做加减 | ||||
|         """ | ||||
|         mio_type = i.mio.type | ||||
|         material: Material = i.material | ||||
|         warehouse = i.warehouse | ||||
|         tracking = material.tracking | ||||
|  | @ -410,7 +387,7 @@ class InmService: | |||
|             if change_count < 0: | ||||
|                 raise ParseError("存在负数!") | ||||
|             state = WMaterial.WM_OK | ||||
|             if defect and defect.okcate in [Defect.DEFECT_NOTOK]: | ||||
|             if defect: | ||||
|                 state = WMaterial.WM_NOTOK | ||||
|             mb, _ = MaterialBatch.objects.get_or_create( | ||||
|                 material=material,  | ||||
|  | @ -442,7 +419,7 @@ class InmService: | |||
|             elif in_or_out == -1: | ||||
|                 mb.count = mb.count - change_count | ||||
|                 if mb.count < 0: | ||||
|                     raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") | ||||
|                     raise ParseError(f"{mb.batch}-批次库存不足,操作失败") | ||||
|                 else: | ||||
|                     mb.save() | ||||
|                     if tracking == Material.MA_TRACKING_SINGLE: | ||||
|  | @ -453,7 +430,7 @@ class InmService: | |||
|                         if mioitemws.count() != change_count: | ||||
|                             raise ParseError("出入库与明细数量不一致,操作失败") | ||||
|                         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: | ||||
|                 raise ParseError("不支持的操作") | ||||
|      | ||||
|  | @ -461,26 +438,3 @@ class InmService: | |||
|         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("不支持该出入库单明细撤销") | ||||
|  | @ -1,6 +1,6 @@ | |||
| from rest_framework.exceptions import ParseError | ||||
| 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.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 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: | ||||
|         raise ParseError("未找到检验表") | ||||
|      | ||||
|  | @ -84,11 +84,8 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem): | |||
|     test_user = mioitem.mio.mio_user | ||||
|     test_date = mioitem.mio.inout_date | ||||
| 
 | ||||
|     wb = load_workbook(path, data_only=True) | ||||
|     if "Sheet1" in wb.sheetnames:  # 检查是否存在 | ||||
|         sheet = wb["Sheet1"]  # 获取工作表 | ||||
|     else: | ||||
|         raise ParseError("未找到Sheet1") | ||||
|     wb = load_workbook(path) | ||||
|     sheet = wb["Sheet1"] | ||||
| 
 | ||||
|     mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number") | ||||
| 
 | ||||
|  | @ -124,38 +121,3 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem): | |||
|             FtestItem.objects.bulk_create(ftestitems) | ||||
|         else: | ||||
|             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) | ||||
|  | @ -19,7 +19,6 @@ router.register('mio/pur', MioPurViewSet) | |||
| router.register('mio/other', MioOtherViewSet) | ||||
| router.register('mioitem', MIOItemViewSet, basename='mioitem') | ||||
| router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') | ||||
| # router.register('pack', PackViewSet, basename='pack') | ||||
| urlpatterns = [ | ||||
|     path(API_BASE_URL, include(router.urls)), | ||||
| ] | ||||
|  |  | |||
|  | @ -9,25 +9,22 @@ from django.utils import timezone | |||
| from rest_framework.response import Response | ||||
| 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 ( | ||||
|     MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer, | ||||
|     MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,  | ||||
|     MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer, | ||||
|     MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer) | ||||
|     MIOItemwSerializer) | ||||
| from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer | ||||
| from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | ||||
| 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, | ||||
|                                CustomListModelMixin) | ||||
| from apps.utils.permission import has_perm | ||||
| from .filters import MaterialBatchFilter, MioFilter | ||||
| from apps.qm.serializers import FtestProcessSerializer | ||||
| 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. | ||||
|  | @ -148,19 +145,9 @@ class MIOViewSet(CustomModelViewSet): | |||
|     serializer_class = MIOListSerializer | ||||
|     retrieve_serializer_class = MIODetailSerializer | ||||
|     filterset_class = MioFilter | ||||
|     search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model', | ||||
|                      'item_mio__a_mioitem__batch'] | ||||
|     search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model'] | ||||
|     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): | ||||
|         # 获取检验状态 | ||||
|         mio_dict = {} | ||||
|  | @ -181,7 +168,7 @@ class MIOViewSet(CustomModelViewSet): | |||
|         if self.action in ['create', 'update', 'partial_update']: | ||||
|             type = self.request.data.get('type') | ||||
|             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']): | ||||
|                     raise PermissionDenied | ||||
|                 return MIODoSerializer | ||||
|  | @ -193,7 +180,7 @@ class MIOViewSet(CustomModelViewSet): | |||
|                 if not has_perm(user, ['mio.sale']): | ||||
|                     raise PermissionDenied | ||||
|                 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']): | ||||
|                     raise PermissionDenied | ||||
|                 return MIOPurSerializer | ||||
|  | @ -205,29 +192,27 @@ class MIOViewSet(CustomModelViewSet): | |||
|         return super().perform_destroy(instance) | ||||
| 
 | ||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) | ||||
|     @transaction.atomic | ||||
|     def submit(self, request, *args, **kwargs): | ||||
|         """提交 | ||||
| 
 | ||||
|         提交 | ||||
|         """ | ||||
|         ins:MIO = self.get_object() | ||||
|         ins = self.get_object() | ||||
|         if ins.inout_date is None: | ||||
|             raise ParseError('出入库日期未填写') | ||||
|         if ins.state != MIO.MIO_CREATE: | ||||
|             raise ParseError('记录状态异常') | ||||
|         now = timezone.now() | ||||
|         ins.submit_user = request.user | ||||
|         ins.submit_time = now | ||||
|         ins.update_by = request.user | ||||
|         with transaction.atomic(): | ||||
|             ins.submit_time = timezone.now() | ||||
|             ins.state = MIO.MIO_SUBMITED | ||||
|             ins.submit_user = request.user | ||||
|             ins.update_by = request.user | ||||
|             ins.save() | ||||
|             InmService.update_inm(ins) | ||||
|         InmService.update_material_count(ins) | ||||
|         return Response(MIOListSerializer(instance=ins).data) | ||||
| 
 | ||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) | ||||
|     @transaction.atomic | ||||
|     def revert(self, request, *args, **kwargs): | ||||
|         """撤回 | ||||
| 
 | ||||
|  | @ -239,84 +224,16 @@ class MIOViewSet(CustomModelViewSet): | |||
|             raise ParseError('记录状态异常') | ||||
|         if ins.submit_user != user: | ||||
|             raise ParseError('非提交人不可撤回') | ||||
|         ins.submit_user = None | ||||
|         ins.update_by = user | ||||
|         ins.state = MIO.MIO_CREATE | ||||
|         with transaction.atomic(): | ||||
|             ins.submit_time = None | ||||
|             ins.state = MIO.MIO_CREATE | ||||
|             ins.update_by = user | ||||
|             ins.save() | ||||
|             InmService.update_inm(ins, is_reverse=True) | ||||
|         InmService.update_material_count(ins) | ||||
|         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): | ||||
|     """ | ||||
|     list: 出入库明细 | ||||
|  | @ -326,7 +243,6 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode | |||
|     perms_map = {'get': '*', 'post': '*', 'delete': '*'} | ||||
|     queryset = MIOItem.objects.all() | ||||
|     serializer_class = MIOItemSerializer | ||||
|     retrieve_serializer_class = MioItemDetailSerializer | ||||
|     create_serializer_class = MIOItemCreateSerializer | ||||
|     select_related_fields = ['warehouse', 'mio', 'material', 'test_user'] | ||||
|     filterset_fields = { | ||||
|  | @ -336,58 +252,22 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode | |||
|         "mio__type": ["exact", "in"], | ||||
|         "mio__inout_date": ["gte", "lte", "exact"], | ||||
|         "material": ["exact"], | ||||
|         "material__type": ["exact"], | ||||
|         "test_date": ["isnull", "exact"] | ||||
|     } | ||||
|     ordering = ['create_time'] | ||||
|     ordering_fields = ['create_time', 'test_date'] | ||||
|     search_fields =['batch', 'a_mioitem__batch'] | ||||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|     @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): | ||||
|         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: | ||||
|             raise PermissionDenied('无权限删除') | ||||
|         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) | ||||
|     @transaction.atomic | ||||
|     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'} | ||||
|     queryset = MIOItemw.objects.all() | ||||
|     serializer_class = MIOItemwCreateUpdateSerializer | ||||
|     filterset_fields = ['mioitem', 'wpr'] | ||||
|     filterset_fields = ['mioitem'] | ||||
|     ordering = ["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.save() | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_create(self, serializer): | ||||
|         MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio) | ||||
|         ins:MIOItemw = serializer.save() | ||||
|         self.cal_mioitem_count(ins.mioitem) | ||||
|         ins: MIOItemw = serializer.save() | ||||
|         mioitem: MIOItem = ins.mioitem | ||||
|         self.cal_mioitem_count(mioitem) | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_update(self, serializer): | ||||
|         ins:MIOItemw = serializer.instance | ||||
|         MIOViewSet.lock_and_check_can_update(ins.mioitem.mio) | ||||
|         ins:MIOItemw = serializer.save() | ||||
|         self.cal_mioitem_count(ins.mioitem) | ||||
|         mioitemw = serializer.save() | ||||
|         self.cal_mioitem_count(mioitemw.mioitem) | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_destroy(self, instance: MIOItemw): | ||||
|         mioitem = instance.mioitem | ||||
|         MIOViewSet.lock_and_check_can_update(mioitem.mio) | ||||
|         ftest = instance.ftest | ||||
|         instance.delete() | ||||
|         if ftest: | ||||
|  |  | |||
|  | @ -7,7 +7,6 @@ from celery import shared_task | |||
| from django.utils import timezone | ||||
| from django.conf import settings | ||||
| import os | ||||
| from apps.utils.sql import execute_raw_sql | ||||
| 
 | ||||
| 
 | ||||
| @shared_task(base=CustomTask) | ||||
|  | @ -34,44 +33,3 @@ def clear_dbbackup(num: int=7): | |||
|     for f in files_remove_list: | ||||
|         filepath = os.path.join(backpath, f) | ||||
|         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) | ||||
|  | @ -1,7 +1,6 @@ | |||
| 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 rest_framework.exceptions import ParseError | ||||
| 
 | ||||
| 
 | ||||
| class MaterialFilter(filters.FilterSet): | ||||
|  | @ -46,8 +45,6 @@ class GoalFilter(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: | ||||
|         model = Route | ||||
|         fields = {  | ||||
|  | @ -61,18 +58,5 @@ class RouteFilter(filters.FilterSet): | |||
|             "mgroup": ["exact", "in", "isnull"], | ||||
|             "mgroup__name": ["exact", "contains"], | ||||
|             "mgroup__belong_dept": ["exact"], | ||||
|             "mgroup__belong_dept__name": ["exact", "contains"], | ||||
|             "from_route": ["exact", "isnull"], | ||||
|             "mgroup__belong_dept__name": ["exact", "contains"] | ||||
|         } | ||||
| 
 | ||||
|     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") | ||||
|  | @ -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='主库位号'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='工艺参数'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='图片'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='上级路线'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -4,8 +4,6 @@ from rest_framework.exceptions import ParseError | |||
| from apps.utils.models import CommonBDModel | ||||
| from collections import defaultdict, deque | ||||
| from django.db.models import Q | ||||
| from datetime import datetime, timedelta | ||||
| from django.utils import timezone | ||||
| 
 | ||||
| class Process(CommonBModel): | ||||
|     """ | ||||
|  | @ -45,12 +43,6 @@ class Process(CommonBModel): | |||
|         """ | ||||
|         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. | ||||
| class Material(CommonAModel): | ||||
|     """TN:物料""" | ||||
|  | @ -109,8 +101,6 @@ class Material(CommonAModel): | |||
|     brothers = models.JSONField('兄弟件', default=list, null=False, blank=True) | ||||
|     unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=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: | ||||
|         verbose_name = '物料表' | ||||
|  | @ -180,32 +170,6 @@ class Mgroup(CommonBModel): | |||
|     def __str__(self) -> str: | ||||
|         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): | ||||
|     team = models.ForeignKey(Team, verbose_name='关联班组', | ||||
|  | @ -360,8 +324,7 @@ class RoutePack(CommonADModel): | |||
|                 route_dict[r.id] = { | ||||
|                     "label": r.process.name if r.process else "", | ||||
|                     "source": r.material_in.id, | ||||
|                     "target": r.material_out.id, | ||||
|                     "id": r.id | ||||
|                     "target": r.material_out.id | ||||
|                 } | ||||
|          | ||||
|         # 获取所有物料信息 | ||||
|  | @ -417,9 +380,7 @@ class Route(CommonADModel): | |||
|     batch_bind = models.BooleanField('是否绑定批次', default=True) | ||||
|     materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials", | ||||
|                                   through="mtm.routemat", blank=True) | ||||
|     parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent") | ||||
|     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") | ||||
|     parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True) | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         x = "" | ||||
|  | @ -549,7 +510,6 @@ class Route(CommonADModel): | |||
|                     'source': source, | ||||
|                     'target': target, | ||||
|                     'label': rq.process.name,  | ||||
|                     'id': rq.id | ||||
|                 }) | ||||
|         # 将批次号排序 | ||||
|         nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time") | ||||
|  |  | |||
|  | @ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer): | |||
|     class Meta: | ||||
|         model = Material | ||||
|         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): | ||||
|         return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}' | ||||
|  | @ -246,8 +246,8 @@ class RouteSerializer(CustomModelSerializer): | |||
|             # material = validated_data.get('material', None) | ||||
|             # if material and process and Route.objects.filter(material=material, process=process).exists(): | ||||
|             #     raise ValidationError('已选择该工序!!') | ||||
| 
 | ||||
|         instance:Route = super().create(validated_data) | ||||
|         with transaction.atomic(): | ||||
|             instance = super().create(validated_data) | ||||
|             material_out = instance.material_out | ||||
|             if material_out: | ||||
|                 if material_out.process is None: | ||||
|  | @ -263,16 +263,12 @@ class RouteSerializer(CustomModelSerializer): | |||
|                 if instance.material: | ||||
|                     instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) | ||||
|                     instance.save() | ||||
|         rx = Route.objects.filter( | ||||
|             material_in=instance.material_in, material_out=instance.material_out,  | ||||
|             process=process).exclude(id=instance.id).order_by("create_time").first() | ||||
|             rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first() | ||||
|             if rx: | ||||
|             instance.from_route = rx | ||||
|             instance.save() | ||||
|             # msg = "" | ||||
|             # if rx.routepack: | ||||
|             #     msg = rx.routepack.name | ||||
|             # raise ParseError(f"该工艺步骤已存在-{msg}") | ||||
|                 msg = "" | ||||
|                 if rx.routepack: | ||||
|                     msg = rx.routepack.name | ||||
|                 raise ParseError(f"该工艺步骤已存在-{msg}") | ||||
|             return instance | ||||
| 
 | ||||
|     def update(self, instance, validated_data): | ||||
|  | @ -281,7 +277,7 @@ class RouteSerializer(CustomModelSerializer): | |||
|         material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH) | ||||
|         if material_out_tracking is None: | ||||
|             material_out_tracking = Material.MA_TRACKING_BATCH | ||||
| 
 | ||||
|         with transaction.atomic(): | ||||
|             instance = super().update(instance, validated_data) | ||||
|             material_out = instance.material_out | ||||
|             if material_out: | ||||
|  | @ -298,16 +294,12 @@ class RouteSerializer(CustomModelSerializer): | |||
|                 if instance.material: | ||||
|                     instance.material_out = RouteSerializer.gen_material_out(instance,  material_out_tracking, user=self.request.user) | ||||
|                     instance.save() | ||||
|         rx = Route.objects.filter( | ||||
|             material_in=instance.material_in, material_out=instance.material_out,  | ||||
|             process=process).exclude(id=instance.id).order_by("create_time").first() | ||||
|             rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first() | ||||
|             if rx: | ||||
|             instance.from_route = rx | ||||
|             instance.save() | ||||
|             # msg = "" | ||||
|             # if rx.routepack: | ||||
|             #     msg = rx.routepack.name | ||||
|             # raise ParseError(f"该工艺步骤已存在-{msg}") | ||||
|                 msg = "" | ||||
|                 if rx.routepack: | ||||
|                     msg = rx.routepack.name | ||||
|                 raise ParseError(f"该工艺步骤已存在-{msg}") | ||||
|             return instance | ||||
| 
 | ||||
|     def to_representation(self, instance): | ||||
|  | @ -338,15 +330,3 @@ class RouteMatSerializer(CustomModelSerializer): | |||
|         model = RouteMat | ||||
|         fields = "__all__" | ||||
|         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"] | ||||
|  | @ -51,35 +51,33 @@ def daoru_material(path: str): | |||
|                  '辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70} | ||||
|     from apps.utils.snowflake import idWorker | ||||
|     from openpyxl import load_workbook | ||||
|     wb = load_workbook(path, read_only=True) | ||||
|     sheet = wb.active | ||||
|     wb = load_workbook(path) | ||||
|     sheet = wb['物料'] | ||||
|     process_l = Process.objects.all() | ||||
|     process_d = {p.name: p for p in process_l} | ||||
|     i = 3 | ||||
|     if sheet['a2'].value != '物料编号': | ||||
|         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(' ', '') | ||||
|         try: | ||||
|             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 | ||||
|             if sheet[f'd{i}'].value: | ||||
|                 name = str(sheet[f'd{i}'].value).replace(' ', '') | ||||
|             if sheet[f'c{i}'].value: | ||||
|                 name = str(sheet[f'c{i}'].value).replace(' ', '') | ||||
|             else: | ||||
|                 raise ParseError(f'{i}行物料信息错误: 物料名称必填') | ||||
|             specification = str(sheet[f'e{i}'].value).replace( | ||||
|                 '×', '*').replace(' ', '') if sheet[f'e{i}'].value else None | ||||
|             model = str(sheet[f'f{i}'].value).replace(' ', '') if sheet[f'f{i}'].value else None | ||||
|             unit = sheet[f'g{i}'].value.replace(' ', '') | ||||
|             count_safe = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None | ||||
|             unit_price = float(sheet[f'j{i}'].value) if sheet[f'j{i}'].value else None | ||||
|             bin_number_main = sheet[f'k{i}'].value.replace(' ', '') if sheet[f'k{i}'].value else None | ||||
|             specification = str(sheet[f'd{i}'].value).replace( | ||||
|                 '×', '*').replace(' ', '') if sheet[f'd{i}'].value else None | ||||
|             model = str(sheet[f'e{i}'].value).replace(' ', '') if sheet[f'e{i}'].value else None | ||||
|             unit = sheet[f'f{i}'].value.replace(' ', '') | ||||
|             count_safe = float(sheet[f'h{i}'].value) if sheet[f'h{i}'].value else None | ||||
|             unit_price = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None | ||||
|         except Exception as e: | ||||
|             raise ParseError(f'{i}行物料信息错误: {e}') | ||||
|         if type in [20, 30]: | ||||
|             try: | ||||
|                 process = process_d[sheet[f'h{i}'].value.replace(' ', '')] | ||||
|                 process = process_d[sheet[f'g{i}'].value.replace(' ', '')] | ||||
|             except Exception as e: | ||||
|                 raise ParseError(f'{i}行物料信息错误: {e}') | ||||
|         try: | ||||
|  | @ -89,7 +87,7 @@ def daoru_material(path: str): | |||
|                 filters['process'] = process | ||||
|             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(),  | ||||
|                        '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( | ||||
|                 **filters, defaults=default) | ||||
|             if not is_created: | ||||
|  | @ -155,12 +153,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict): | |||
|         raise ParseError('缺少步骤') | ||||
|     r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') | ||||
|     first_route = r_qs.first() | ||||
|     last_route = r_qs.last() | ||||
|     if first_route.batch_bind: | ||||
|         first_route.batch_bind = False | ||||
|         first_route.save(update_fields=['batch_bind']) | ||||
|     # last_route = r_qs.last() | ||||
|     # if last_route.material_out != routepack.material: | ||||
|     #     raise ParseError('最后一步产出与工艺包不一致') | ||||
|     if last_route.material_out != routepack.material: | ||||
|         raise ParseError('最后一步产出与工艺包不一致') | ||||
|     ticket_data = ticket.ticket_data | ||||
|     ticket_data.update({ | ||||
|         't_model': 'routepack', | ||||
|  | @ -182,7 +180,7 @@ def routepack_audit_end(ticket: Ticket): | |||
| 
 | ||||
| def routepack_ticket_change(ticket: Ticket): | ||||
|     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.save() | ||||
| 
 | ||||
|  |  | |||
|  | @ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer, | |||
|                                   MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer, | ||||
|                                   ShiftSerializer, TeamSerializer, ProcessSerializer,  | ||||
|                                   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.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | ||||
| 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 django.utils import timezone | ||||
| from rest_framework.permissions import IsAdminUser | ||||
| from apps.utils.export import export_excel | ||||
| from operator import itemgetter | ||||
| 
 | ||||
| # Create your views here. | ||||
| class MaterialViewSet(CustomModelViewSet): | ||||
|  | @ -34,13 +32,14 @@ class MaterialViewSet(CustomModelViewSet): | |||
|     queryset = Material.objects.all() | ||||
|     serializer_class = MaterialSerializer | ||||
|     filterset_class = MaterialFilter | ||||
|     search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main'] | ||||
|     search_fields = ['name', 'code', 'number', 'specification', 'model'] | ||||
|     select_related_fields = ['process'] | ||||
|     ordering = ['name', 'model', 'specification', | ||||
|                 'type', 'process', 'process__sort', 'sort', 'id', 'number'] | ||||
|     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): | ||||
|         from apps.inm.models import MaterialBatch | ||||
|         if MaterialBatch.objects.filter(material=instance).exists(): | ||||
|  | @ -90,23 +89,6 @@ class MaterialViewSet(CustomModelViewSet): | |||
|         res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct() | ||||
|         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): | ||||
|     """ | ||||
|     list:班次 | ||||
|  | @ -311,7 +293,7 @@ class RoutePackViewSet(CustomModelViewSet): | |||
|         return Response({"id": route_new.id}) | ||||
|      | ||||
|     @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): | ||||
|         """变更工艺路线状态 | ||||
| 
 | ||||
|  | @ -367,22 +349,12 @@ class RouteViewSet(CustomModelViewSet): | |||
|     select_related_fields = ['material', | ||||
|                              'process', 'material_in', 'material_out', 'mgroup', 'routepack'] | ||||
| 
 | ||||
|     def perform_update(self, serializer): | ||||
|         ins:Route = serializer.instance | ||||
|         if ins.from_route is not None: | ||||
|             raise ParseError('该工艺步骤引用其他步骤, 无法编辑') | ||||
|         old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process | ||||
|         routepack = ins.routepack | ||||
|     def update(self, request, *args, **kwargs): | ||||
|         obj:Route = self.get_object() | ||||
|         routepack = obj.routepack | ||||
|         if routepack and routepack.state != RoutePack.RP_S_CREATE: | ||||
|             raise ParseError('该工艺路线非创建中不可编辑') | ||||
|         ins_n:Route = serializer.save() | ||||
|         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) | ||||
|             raise ParseError('该状态下不可编辑') | ||||
|         return super().update(request, *args, **kwargs) | ||||
| 
 | ||||
| 
 | ||||
| class SruleViewSet(CustomModelViewSet): | ||||
|  |  | |||
|  | @ -1,3 +0,0 @@ | |||
| from django.contrib import admin | ||||
| 
 | ||||
| # Register your models here. | ||||
|  | @ -1,6 +0,0 @@ | |||
| from django.apps import AppConfig | ||||
| 
 | ||||
| 
 | ||||
| class OfmConfig(AppConfig): | ||||
|     default_auto_field = 'django.db.models.BigAutoField' | ||||
|     name = 'apps.ofm' | ||||
|  | @ -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'] | ||||
|  | @ -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')}, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='途经地点'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='所属部门'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='关联工单'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='部室/研究院'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='关联工单'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='关联会议室'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='归还公里数'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='参会人数'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='涉密等级'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='第一撰稿人自审'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='第一撰稿人自审'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='第一撰稿人自审'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='宣传报道意见'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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(), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='第一撰稿人自审'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='拟申请地域'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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, | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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', | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='所属部门'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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) | ||||
| 
 | ||||
|  | @ -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 | ||||
|          | ||||
|  | @ -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() | ||||
|  | @ -1,3 +0,0 @@ | |||
| from django.test import TestCase | ||||
| 
 | ||||
| # Create your tests here. | ||||
|  | @ -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)), | ||||
| ] | ||||
|  | @ -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"] | ||||
|  | @ -50,7 +50,7 @@ class MtaskFilter(filters.FilterSet): | |||
|             "is_count_utask": ["exact"], | ||||
|             "start_date": ["exact", "gte", "lte"], | ||||
|             "end_date": ["exact", "gte", "lte"], | ||||
|             "mgroup": ["exact", "in"], | ||||
|             "mgroup": ["exact"], | ||||
|             "mgroup__name": ["exact"], | ||||
|             "mgroup__cate": ["exact"], | ||||
|             "mgroup__process": ["exact"], | ||||
|  |  | |||
|  | @ -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='优先级'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -37,7 +37,6 @@ class Utask(CommonBDModel): | |||
|     type = models.CharField('任务类型', max_length=10, | ||||
|                             help_text=str(TASK_TYPE), default='mass') | ||||
|     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( | ||||
|         '状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES)) | ||||
|     number = models.CharField('编号', max_length=50, unique=True) | ||||
|  |  | |||
|  | @ -27,10 +27,10 @@ class UtaskSerializer(CustomModelSerializer): | |||
|         model = Utask | ||||
|         fields = '__all__' | ||||
|         extra_kwargs = { | ||||
|             'number': {"required": False, "allow_blank": True}, | ||||
|             "priority": {"required": False, "allow_null": True}, | ||||
|             'number': {"required": False, "allow_blank": True} | ||||
|         } | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def create(self, validated_data): | ||||
|         if not validated_data.get('number', None): | ||||
|             validated_data["number"] = Utask.get_a_number() | ||||
|  | @ -52,7 +52,6 @@ class UtaskSerializer(CustomModelSerializer): | |||
|             attrs['count_day'] = math.ceil(attrs['count']/rela_days) | ||||
|         except Exception: | ||||
|             raise ParseError('日均任务数计划失败') | ||||
|         attrs["priority"] = attrs.get("priority", 20) | ||||
|         return attrs | ||||
| 
 | ||||
|     def update(self, instance, validated_data): | ||||
|  |  | |||
|  | @ -408,14 +408,11 @@ class PmService: | |||
|             mtask.submit_time = now | ||||
|             mtask.submit_user = user | ||||
|             mtask.save() | ||||
|             utask = mtask.utask | ||||
|             if utask: | ||||
|                 cls.utask_submit(utask, raise_e=False) | ||||
|         else: | ||||
|             raise ParseError('该任务状态不可提交') | ||||
| 
 | ||||
|     @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.save() | ||||
|         else: | ||||
|             if raise_e: | ||||
|             raise ParseError('存在子任务未提交') | ||||
|  | @ -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) | ||||
|  | @ -29,8 +29,7 @@ class UtaskViewSet(CustomModelViewSet): | |||
|     serializer_class = UtaskSerializer | ||||
|     filterset_class = UtaskFilter | ||||
|     select_related_fields = ['material'] | ||||
|     ordering_fields = ['priority', 'start_date'] | ||||
|     ordering = ["priority", '-start_date'] | ||||
|     ordering = ['-start_date'] | ||||
| 
 | ||||
|     def perform_destroy(self, instance): | ||||
|         if instance.state >= Utask.UTASK_WORKING: | ||||
|  | @ -144,8 +143,8 @@ class MtaskViewSet(CustomModelViewSet): | |||
|     filterset_class = MtaskFilter | ||||
|     select_related_fields = ['material_in', 'material_out', 'mgroup'] | ||||
|     prefetch_related_fields = ['mlog_mtask', 'b_mtask'] | ||||
|     ordering_fields = ["utask__priority", 'start_date', 'mgroup__process__sort', 'create_time'] | ||||
|     ordering = ["utask__priority", '-start_date', 'route__sort', 'mgroup__process__sort', '-create_time'] | ||||
|     ordering_fields = ['start_date', '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) | ||||
|     @transaction.atomic | ||||
|  |  | |||
|  | @ -43,6 +43,7 @@ class PuPlanItemSerializer(CustomModelSerializer): | |||
|         fields = '__all__' | ||||
|         read_only_fields = EXCLUDE_FIELDS + ['pu_order'] | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def create(self, validated_data): | ||||
|         pu_plan = validated_data['pu_plan'] | ||||
|         if pu_plan.state != PuPlan.PUPLAN_CREATE: | ||||
|  | @ -64,6 +65,7 @@ class PuPlanItemSerializer(CustomModelSerializer): | |||
|             else: | ||||
|                 validated_data['belong_dept'] = belong_dept | ||||
|      | ||||
|     @transaction.atomic | ||||
|     def update(self, instance, validated_data): | ||||
|         validated_data.pop('pu_plan') | ||||
|         pu_plan = instance.pu_plan | ||||
|  | @ -112,6 +114,7 @@ class PuOrderItemSerializer(CustomModelSerializer): | |||
|         fields = '__all__' | ||||
|         read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count'] | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def create(self, validated_data): | ||||
|         pu_order = validated_data['pu_order'] | ||||
|         material = validated_data['material'] | ||||
|  | @ -123,6 +126,7 @@ class PuOrderItemSerializer(CustomModelSerializer): | |||
|         PumService.cal_pu_order_total_price(pu_order) | ||||
|         return ins | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def update(self, instance, validated_data): | ||||
|         validated_data.pop('material') | ||||
|         validated_data.pop('pu_order') | ||||
|  |  | |||
|  | @ -2,24 +2,20 @@ from rest_framework.exceptions import ValidationError | |||
| from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder | ||||
| from django.db.models import F, Sum | ||||
| from apps.inm.models import MIO, MIOItem | ||||
| from rest_framework.exceptions import ParseError | ||||
| 
 | ||||
| 
 | ||||
| class PumService: | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def cal_pu_order_total_price(puorder: PuOrder): | ||||
|         total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0 | ||||
|         puorder.total_price = total_price | ||||
|         puorder.save() | ||||
|      | ||||
|     @staticmethod | ||||
|     def cal_pu_plan_total_price(puplan: PuPlan): | ||||
|         total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0 | ||||
|         puplan.total_price = total_price | ||||
|         puplan.save() | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def change_puplan_state_when_puorder_sumbit(puorder: PuOrder): | ||||
|         puplanIds = PuPlanItem.objects.filter( | ||||
|             pu_order=puorder).values_list('pu_plan', flat=True) | ||||
|  | @ -33,45 +29,24 @@ class PumService: | |||
|             puplan.state = state | ||||
|             puplan.save() | ||||
| 
 | ||||
|     @staticmethod | ||||
|     def mio_pur(mio: MIO, is_reverse: bool = False, mioitem: MIOItem = None): | ||||
|     def mio_purin(mio: MIO, is_reverse: bool = False): | ||||
|         """ | ||||
|         采购入库成功后的操作 | ||||
|         """ | ||||
|         pu_order = mio.pu_order | ||||
|         if pu_order is None: | ||||
|             return | ||||
|         if mioitem is None: | ||||
|             qs = MIOItem.objects.filter(mio=mio) | ||||
|         else: | ||||
|             qs = MIOItem.objects.filter(id=mioitem.id) | ||||
|          | ||||
| 
 | ||||
|         if mio.type == MIO.MIO_TYPE_PUR_IN: | ||||
|             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: | ||||
|         for i in MIOItem.objects.filter(mio=mio): | ||||
|             pu_orderitem = PuOrderItem.objects.get( | ||||
|                 material=i.material, pu_order=pu_order) | ||||
|             except PuOrderItem.DoesNotExist: | ||||
|                 raise ParseError(f'{str(i.material)}-采购订单中不存在该物料') | ||||
|             if xtype == "out": | ||||
|             if is_reverse: | ||||
|                 delivered_count = pu_orderitem.delivered_count - i.count | ||||
|             else: | ||||
|                 delivered_count = pu_orderitem.delivered_count + i.count | ||||
|             if delivered_count > pu_orderitem.count: | ||||
|                 raise ValidationError(f'{str(i.material)}-超出采购订单所需数量') | ||||
|                 raise ValidationError(f'{i.material.name}-超出采购订单所需数量') | ||||
|             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.save() | ||||
|         pu_order_state = PuOrder.PUORDER_SHIP | ||||
|  | @ -94,4 +69,3 @@ class PumService: | |||
|             if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE: | ||||
|                 puplan.state = PuPlan.PUPLAN_DONE | ||||
|                 puplan.save() | ||||
| 
 | ||||
|  |  | |||
|  | @ -80,6 +80,7 @@ class PuPlanItemViewSet(CustomModelViewSet): | |||
|     ordering_fields = ['create_time', 'material', 'need_date', 'need_count'] | ||||
|     ordering = ['create_time'] | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_destroy(self, instance): | ||||
|         user = self.request.user | ||||
|         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'] | ||||
|     select_related_fields = ['create_by', 'update_by', 'supplier'] | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_destroy(self, instance): | ||||
|         if instance.state != PuOrder.PUORDER_CREATE: | ||||
|             raise ParseError('采购订单非创建中不可删除') | ||||
|  | @ -143,6 +145,7 @@ class PuOrderItemViewSet(CustomModelViewSet): | |||
|     filterset_fields = ['material', 'pu_order'] | ||||
|     ordering = ['create_time'] | ||||
| 
 | ||||
|     @transaction.atomic | ||||
|     def perform_destroy(self, instance): | ||||
|         pu_order = instance.pu_order | ||||
|         if pu_order.state != PuOrder.PUORDER_CREATE: | ||||
|  |  | |||
|  | @ -25,8 +25,6 @@ class QctFilter(filters.FilterSet): | |||
|             "testitems": ["exact"], | ||||
|             "defects": ["exact"], | ||||
|             "qctmat__material": ["exact"], | ||||
|             "qctmat__use_for_in": ["exact"], | ||||
|             "qctmat__use_for_out": ["exact"], | ||||
|             "qctmat__tracing": ["exact"], | ||||
|         } | ||||
| 
 | ||||
|  | @ -40,25 +38,19 @@ class TestItemFilter(filters.FilterSet): | |||
| 
 | ||||
| 
 | ||||
| class FtestWorkFilter(filters.FilterSet): | ||||
|     cbatch = filters.CharFilter(label='批次号', method='filter_cbatch') | ||||
|     class Meta: | ||||
|         model = FtestWork | ||||
|         fields = { | ||||
|             "material__process__name": ["exact", "contains"], | ||||
|             "material": ["exact"], | ||||
|             "wm": ["exact", "isnull"], | ||||
|             "mb": ["exact", "isnull"], | ||||
|             "wm": ["exact"], | ||||
|             "mb": ["exact"], | ||||
|             "batch": ["exact"], | ||||
|             "type": ["exact"], | ||||
|             "type2": ["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): | ||||
|     wpr = filters.CharFilter(label="wprId", method="filter_wpr") | ||||
|     class Meta: | ||||
|  |  | |||
|  | @ -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='可用于产出'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='是否合格'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -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='析晶'), | ||||
|         ), | ||||
|     ] | ||||
|  | @ -172,13 +172,8 @@ class Qct(CommonAModel): | |||
|         return QctMat.objects.filter(qct=self) | ||||
| 
 | ||||
|     @classmethod | ||||
|     def get(cls, material:Material, tag:str, type:str=None): | ||||
|     def get(cls, material:Material, tag:str): | ||||
|         try: | ||||
|             if type == "in": | ||||
|                 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: | ||||
|             try: | ||||
|  | @ -200,12 +195,8 @@ class Qct(CommonAModel): | |||
|             return None | ||||
|      | ||||
|     @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) | ||||
|         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():  | ||||
|             qct_qs = Qct.objects.filter(name="默认检验表") | ||||
|         return qct_qs | ||||
|  | @ -233,8 +224,6 @@ class QctMat(BaseModel): | |||
|     material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE) | ||||
|     tracing = models.CharField('追溯层级', default=QC_T, choices=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) | ||||
|      | ||||
| 
 | ||||
|  | @ -337,7 +326,7 @@ class Ftest(CommonBDModel): | |||
|         User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user') | ||||
|     check_user = models.ForeignKey( | ||||
|         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) | ||||
|     ftest_work = models.ForeignKey( | ||||
|         FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True) | ||||
|  | @ -353,6 +342,7 @@ class Ftest(CommonBDModel): | |||
| 
 | ||||
|     @classmethod | ||||
|     def init_by_qct(cls, qct, test_user, test_date): | ||||
|         with transaction.atomic(): | ||||
|             ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date) | ||||
|             for testitem in qct.testitems.all(): | ||||
|                 FtestItem.objects.create(ftest=ftest, testitem=testitem) | ||||
|  | @ -445,7 +435,7 @@ class Ptest(CommonAModel): | |||
|     val_tg = models.FloatField("Tg", help_text='℃', null=True, blank=True) | ||||
|     val_tf = models.FloatField("Tf", help_text='℃', null=True, blank=True) | ||||
|     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( | ||||
|         '膨胀系数', help_text='30-300℃', 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
		Loading…
	
		Reference in New Issue