Compare commits
	
		
			No commits in common. "master" and "2.7.2025062414" have entirely different histories.
		
	
	
		
			master
			...
			2.7.202506
		
	
		|  | @ -20,10 +20,6 @@ class WxCodeSerializer(serializers.Serializer): | ||||||
|     code = serializers.CharField(label="code") |     code = serializers.CharField(label="code") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class UserIdSerializer(serializers.Serializer): |  | ||||||
|     user_id = serializers.CharField(label="用户id") |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PwResetSerializer(serializers.Serializer): | class PwResetSerializer(serializers.Serializer): | ||||||
|     phone = serializers.CharField(label="手机号") |     phone = serializers.CharField(label="手机号") | ||||||
|     code = serializers.CharField(label="验证码") |     code = serializers.CharField(label="验证码") | ||||||
|  |  | ||||||
|  | @ -3,8 +3,7 @@ from django.urls import path | ||||||
| from rest_framework_simplejwt.views import TokenRefreshView | from rest_framework_simplejwt.views import TokenRefreshView | ||||||
| 
 | 
 | ||||||
| from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView, | from apps.auth1.views import (CodeLogin, LoginView, LogoutView, PwResetView, | ||||||
|                               SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin,  |                               SecretLogin, SendCode, TokenBlackView, WxLogin, WxmpLogin, TokenLoginView, FaceLoginView) | ||||||
|                               TokenLoginView, FaceLoginView, UserIdLogin) |  | ||||||
| 
 | 
 | ||||||
| API_BASE_URL = 'api/auth/' | API_BASE_URL = 'api/auth/' | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|  | @ -19,6 +18,5 @@ urlpatterns = [ | ||||||
|     path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'), |     path(API_BASE_URL + 'sms_code/', SendCode.as_view(), name='sms_code_send'), | ||||||
|     path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'), |     path(API_BASE_URL + 'logout/', LogoutView.as_view(), name='session_logout'), | ||||||
|     path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'), |     path(API_BASE_URL + 'reset_password/', PwResetView.as_view(), name='reset_password'), | ||||||
|     path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login'), |     path(API_BASE_URL + 'login_face/', FaceLoginView.as_view(), name='face_login') | ||||||
|     path(API_BASE_URL + 'login_userid/', UserIdLogin.as_view(), name='userid_login'), |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -23,8 +23,7 @@ from apps.auth1.serializers import FaceLoginSerializer | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer, | from apps.auth1.serializers import (CodeLoginSerializer, LoginSerializer, | ||||||
|                                     PwResetSerializer, SecretLoginSerializer,  |                                     PwResetSerializer, SecretLoginSerializer, SendCodeSerializer, WxCodeSerializer) | ||||||
|                                     SendCodeSerializer, WxCodeSerializer, UserIdSerializer) |  | ||||||
| from apps.system.models import User | from apps.system.models import User | ||||||
| from rest_framework_simplejwt.views import TokenObtainPairView | from rest_framework_simplejwt.views import TokenObtainPairView | ||||||
| from apps.auth1.authentication import get_user_by_username_or | from apps.auth1.authentication import get_user_by_username_or | ||||||
|  | @ -235,29 +234,6 @@ class SecretLogin(CreateAPIView): | ||||||
|             return Response(ret) |             return Response(ret) | ||||||
|         raise ParseError('登录失败') |         raise ParseError('登录失败') | ||||||
| 
 | 
 | ||||||
| class UserIdLogin(CreateAPIView): |  | ||||||
|     """直接UserId登录(危险操作) |  | ||||||
| 
 |  | ||||||
|     直接UserId登录 |  | ||||||
|     """ |  | ||||||
|     authentication_classes = [] |  | ||||||
|     permission_classes = [] |  | ||||||
|     serializer_class = UserIdSerializer |  | ||||||
| 
 |  | ||||||
|     def post(self, request): |  | ||||||
|         sr = UserIdSerializer(data=request.data) |  | ||||||
|         sr.is_valid(raise_exception=True) |  | ||||||
|         vdata = sr.validated_data |  | ||||||
|         userid = vdata['user_id'] |  | ||||||
|         try: |  | ||||||
|             user = User.objects.get(id=userid) |  | ||||||
|         except Exception as e: |  | ||||||
|             raise ParseError(f'用户不存在-{e}') |  | ||||||
|         if user: |  | ||||||
|             ret = get_tokens_for_user(user) |  | ||||||
|             return Response(ret) |  | ||||||
|         raise ParseError('登录失败') |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class PwResetView(CreateAPIView): | class PwResetView(CreateAPIView): | ||||||
|     """重置密码 |     """重置密码 | ||||||
|  |  | ||||||
|  | @ -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) |     test_param = models.JSONField('测试查询参数', default=dict, blank=True) | ||||||
|     default_param = models.JSONField('默认查询参数', default=dict, blank=True) |     default_param = models.JSONField('默认查询参数', default=dict, blank=True) | ||||||
|     cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True) |     cache_seconds = models.PositiveIntegerField('缓存秒数', default=10, blank=True) | ||||||
|     enabled = models.BooleanField('启用', default=True) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # class Report(CommonBDModel): | # class Report(CommonBDModel): | ||||||
|  |  | ||||||
|  | @ -12,12 +12,9 @@ def check_sql_safe(sql: str): | ||||||
|     """检查sql安全性 |     """检查sql安全性 | ||||||
|     """ |     """ | ||||||
|     sql_upper = sql.upper() |     sql_upper = sql.upper() | ||||||
|     # 将SQL按空格和分号分割成单词 |  | ||||||
|     words = [word for word in sql_upper.replace(';', ' ').split() if word] |  | ||||||
|     for kw in forbidden_keywords: |     for kw in forbidden_keywords: | ||||||
|         # 检查关键字是否作为独立单词出现 |         if kw in sql_upper: | ||||||
|         if kw in words: |             raise ParseError('sql查询有风险') | ||||||
|             raise ParseError(f'sql查询有风险-{kw}') |  | ||||||
|     return sql |     return sql | ||||||
| 
 | 
 | ||||||
| def format_json_with_placeholders(json_str, **kwargs): | def format_json_with_placeholders(json_str, **kwargs): | ||||||
|  |  | ||||||
|  | @ -64,8 +64,6 @@ class DatasetViewSet(CustomModelViewSet): | ||||||
|         执行sql查询支持code |         执行sql查询支持code | ||||||
|         """ |         """ | ||||||
|         dt: Dataset = self.get_object() |         dt: Dataset = self.get_object() | ||||||
|         if not dt.enabled: |  | ||||||
|             raise ParseError(f'{dt.name}-该查询未启用') |  | ||||||
|         rdata = DatasetSerializer(instance=dt).data |         rdata = DatasetSerializer(instance=dt).data | ||||||
|         xquery = request.data.get('query', {}) |         xquery = request.data.get('query', {}) | ||||||
|         is_test = request.data.get('is_test', False) |         is_test = request.data.get('is_test', False) | ||||||
|  |  | ||||||
|  | @ -102,7 +102,7 @@ class LabelTemplateViewSet(CustomModelViewSet): | ||||||
|     serializer_class = LabelTemplateSerializer |     serializer_class = LabelTemplateSerializer | ||||||
|     filterset_class = LabelTemplateFilter |     filterset_class = LabelTemplateFilter | ||||||
| 
 | 
 | ||||||
|     @action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"}) |     @action(methods=["post"], detail=False, serializer_class=Tid2Serializer) | ||||||
|     def commands(self, request, *args, **kwargs): |     def commands(self, request, *args, **kwargs): | ||||||
|         """ |         """ | ||||||
|         获取标签指令 |         获取标签指令 | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals | ||||||
| from celery import shared_task | from celery import shared_task | ||||||
| import subprocess | import subprocess | ||||||
| from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD | from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD | ||||||
| from django.conf import settings |  | ||||||
| import logging | import logging | ||||||
| myLogger = logging.getLogger('log') | myLogger = logging.getLogger('log') | ||||||
| 
 | 
 | ||||||
|  | @ -15,10 +14,8 @@ def backup_database(): | ||||||
|     import datetime |     import datetime | ||||||
| 
 | 
 | ||||||
|     name = datetime.datetime.now().strftime("%Y%m%d%H%M%S") |     name = datetime.datetime.now().strftime("%Y%m%d%H%M%S") | ||||||
|     exclude_tables = getattr(settings, 'EXCLUDE_TABLE_DATA', []) |     command = 'echo "{}" | sudo -S pg_dump "user={} password={} dbname={}" > {}/bak_{}.sql'.format( | ||||||
|     exclude_str = ' '.join([f"--exclude-table-data={table}" for table in exclude_tables])                          |         SD_PWD, DATABASES["default"]["USER"], DATABASES["default"]["PASSWORD"], DATABASES["default"]["NAME"], BACKUP_PATH + "/database", name | ||||||
|     command = 'echo "{}" | sudo -S -u postgres pg_dump {} "user={} password={} dbname={}" > {}/bak_{}.sql'.format( |  | ||||||
|         SD_PWD, exclude_str, DATABASES["default"]["USER"], DATABASES["default"]["PASSWORD"], DATABASES["default"]["NAME"], BACKUP_PATH + "/database", name |  | ||||||
|     ) |     ) | ||||||
|     completed = subprocess.run(command, shell=True, capture_output=True, text=True) |     completed = subprocess.run(command, shell=True, capture_output=True, text=True) | ||||||
|     if completed.returncode != 0: |     if completed.returncode != 0: | ||||||
|  |  | ||||||
|  | @ -4,10 +4,10 @@ import json | ||||||
| import time | import time | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from apps.utils.thread import MyThread | from apps.utils.thread import MyThread | ||||||
|  | import struct | ||||||
| import uuid | import uuid | ||||||
| import logging | import logging | ||||||
| import threading | import threading | ||||||
| import requests |  | ||||||
| 
 | 
 | ||||||
| myLogger = logging.getLogger('log') | myLogger = logging.getLogger('log') | ||||||
| 
 | 
 | ||||||
|  | @ -16,7 +16,7 @@ def get_checksum(body_msg): | ||||||
| 
 | 
 | ||||||
| def handle_bytes(arr): | def handle_bytes(arr): | ||||||
|     if len(arr) < 8: |     if len(arr) < 8: | ||||||
|         return f"返回数据长度错误-{arr}" |         return "返回数据长度错误" | ||||||
|      |      | ||||||
|     if arr[0] != 0xEB or arr[1] != 0x90: |     if arr[0] != 0xEB or arr[1] != 0x90: | ||||||
|         return "数据头不正确" |         return "数据头不正确" | ||||||
|  | @ -136,7 +136,7 @@ def get_tyy_data_2(*args, retry=1): | ||||||
| sc_all = {} | sc_all = {} | ||||||
| sc_lock = threading.Lock() | sc_lock = threading.Lock() | ||||||
| 
 | 
 | ||||||
| def get_tyy_data_1(*args, retry=1): | def get_tyy_data(*args, retry=1): | ||||||
|     host, port = args[0], int(args[1]) |     host, port = args[0], int(args[1]) | ||||||
|     global sc_all |     global sc_all | ||||||
|     sc = None |     sc = None | ||||||
|  | @ -208,45 +208,3 @@ def get_tyy_data_1(*args, retry=1): | ||||||
|             raise ParseError(f'采集器返回数据错误-{res}') |             raise ParseError(f'采集器返回数据错误-{res}') | ||||||
|         else: |         else: | ||||||
|             return res |             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 |  | ||||||
|  | @ -180,9 +180,7 @@ class CdView(MyLoggingMixin, APIView): | ||||||
| 
 | 
 | ||||||
|         执行采集数据方法 |         执行采集数据方法 | ||||||
|         """ |         """ | ||||||
|         method = request.data.get("method", None) |         method = request.data.get("method") | ||||||
|         if not method: |  | ||||||
|             raise ParseError("请传入method参数") |  | ||||||
|         m = method.split("(")[0] |         m = method.split("(")[0] | ||||||
|         args = method.split("(")[1].split(")")[0].split(",") |         args = method.split("(")[1].split(")")[0].split(",") | ||||||
|         module, func = m.rsplit(".", 1) |         module, func = m.rsplit(".", 1) | ||||||
|  |  | ||||||
|  | @ -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) |     cal_coefficient = models.FloatField("计算系数", null=True, blank=True) | ||||||
| 
 | 
 | ||||||
|     mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True) |     mpoint_from = models.ForeignKey("self", verbose_name="来源自采测点", related_name="mp_mpoint_from", on_delete=models.SET_NULL, null=True, blank=True) | ||||||
|     cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "不涉及"), (20, "运行时统计")], null=True, blank=True) |     cal_related_mgroup_running = models.PositiveSmallIntegerField("与工段运行状态的关联", default=10, choices=[(10, "运行时统计")], null=True, blank=True) | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def cache_key(cls, code: str): |     def cache_key(cls, code: str): | ||||||
|  |  | ||||||
|  | @ -86,10 +86,10 @@ def db_ins_mplogx(): | ||||||
|         if bill_date is None: |         if bill_date is None: | ||||||
|             raise Exception("bill_date is None") |             raise Exception("bill_date is None") | ||||||
|         query = """ |         query = """ | ||||||
|                     SELECT id, de_real_quantity, inv_code, bill_date |                     SELECT id, de_real_quantity, CONCAT('x', inv_name) AS inv_name, bill_date | ||||||
|                     FROM sa_weigh_view |                     FROM sa_weigh_view | ||||||
|                     WHERE bill_date >= %s and de_real_quantity > 0 |                     WHERE bill_date >= %s and de_real_quantity > 0 | ||||||
|                     AND inv_code IN %s |                     AND inv_name IN %s | ||||||
|                     ORDER BY bill_date |                     ORDER BY bill_date | ||||||
|                 """ |                 """ | ||||||
|         cursor.execute(query, (bill_date, tuple(batchs))) |         cursor.execute(query, (bill_date, tuple(batchs))) | ||||||
|  | @ -167,11 +167,11 @@ def get_first_stlog_time_from_duration(mgroup:Mgroup, dt_start:datetime, dt_end: | ||||||
|     if st: |     if st: | ||||||
| 
 | 
 | ||||||
|         return st, "ending" |         return st, "ending" | ||||||
|     st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__gte=600).order_by("start_time").last() |     st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__lte=600).order_by("start_time").last() | ||||||
|     if st: |     if st: | ||||||
| 
 | 
 | ||||||
|         return st, "start" |         return st, "start" | ||||||
|     st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__gte=600).order_by("end_time").first() |     st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__lte=600).order_by("end_time").first() | ||||||
|     if st: |     if st: | ||||||
| 
 | 
 | ||||||
|         return st, "end" |         return st, "end" | ||||||
|  | @ -213,7 +213,7 @@ def cal_mpointstat_hour(mpointId: str, year: int, month: int, day: int, hour: in | ||||||
|                         val = abs(first_val - last_val) |                         val = abs(first_val - last_val) | ||||||
|             else: |             else: | ||||||
|                 xtype = "normal" |                 xtype = "normal" | ||||||
|                 if mpointfrom and mpoint.cal_related_mgroup_running == 20: |                 if mpointfrom and mpoint.cal_related_mgroup_running == 10: | ||||||
| 
 | 
 | ||||||
|                     stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n) |                     stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,19 +1,18 @@ | ||||||
| from django.urls import path, include | from django.urls import path, include | ||||||
| from rest_framework.routers import DefaultRouter | from rest_framework.routers import DefaultRouter | ||||||
| from apps.enm.views import (MpointViewSet, MpointStatViewSet,  | from apps.enm.views import (MpointViewSet, MpLogxViewSet, MpointStatViewSet,  | ||||||
|                             EnStatViewSet, EnStat2ViewSet, XscriptViewSet, MpLogxAPIView) |                             EnStatViewSet, EnStat2ViewSet, XscriptViewSet) | ||||||
| 
 | 
 | ||||||
| API_BASE_URL = 'api/enm/' | API_BASE_URL = 'api/enm/' | ||||||
| HTML_BASE_URL = 'dhtml/enm/' | HTML_BASE_URL = 'dhtml/enm/' | ||||||
| 
 | 
 | ||||||
| router = DefaultRouter() | router = DefaultRouter() | ||||||
| router.register('mpoint', MpointViewSet, basename='mpoint') | router.register('mpoint', MpointViewSet, basename='mpoint') | ||||||
| # router.register('mplogx', MpLogxViewSet, basename='mplogx') | router.register('mplogx', MpLogxViewSet, basename='mplogx') | ||||||
| router.register('mpointstat', MpointStatViewSet, basename='mpointstat') | router.register('mpointstat', MpointStatViewSet, basename='mpointstat') | ||||||
| router.register('enstat', EnStatViewSet, basename='enstat') | router.register('enstat', EnStatViewSet, basename='enstat') | ||||||
| router.register('enstat2', EnStat2ViewSet, basename='enstat2') | router.register('enstat2', EnStat2ViewSet, basename='enstat2') | ||||||
| router.register('xscript', XscriptViewSet, basename='xscript') | router.register('xscript', XscriptViewSet, basename='xscript') | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path(API_BASE_URL, include(router.urls)), |     path(API_BASE_URL, include(router.urls)), | ||||||
|     path(f'{API_BASE_URL}mplogx/', MpLogxAPIView.as_view(), name='mplogx_list'), |  | ||||||
| ] | ] | ||||||
|  | @ -12,7 +12,6 @@ from apps.enm.tasks import cal_mpointstat_manual | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from rest_framework.serializers import Serializer | from rest_framework.serializers import Serializer | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.views import APIView |  | ||||||
| from apps.enm.tasks import cal_mpointstats_duration | from apps.enm.tasks import cal_mpointstats_duration | ||||||
| from apps.enm.services import king_sync, MpointCache | from apps.enm.services import king_sync, MpointCache | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
|  | @ -22,13 +21,7 @@ from apps.enm.services import get_analyse_data_mgroups_duration | ||||||
| from django.db.models import Sum | from django.db.models import Sum | ||||||
| import logging | import logging | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from apps.utils.sql import query_one_dict, query_all_dict |  | ||||||
| from drf_yasg import openapi |  | ||||||
| from drf_yasg.utils import swagger_auto_schema |  | ||||||
| from django.utils import timezone |  | ||||||
| 
 |  | ||||||
| myLogger = logging.getLogger('log') | myLogger = logging.getLogger('log') | ||||||
| 
 |  | ||||||
| class MpointViewSet(CustomModelViewSet): | class MpointViewSet(CustomModelViewSet): | ||||||
|     """ |     """ | ||||||
|     list:测点 |     list:测点 | ||||||
|  | @ -91,34 +84,6 @@ class MpointViewSet(CustomModelViewSet): | ||||||
|         king_sync(getattr(settings, "KING_PROJECTNAME", "")) |         king_sync(getattr(settings, "KING_PROJECTNAME", "")) | ||||||
|         return Response() |         return Response() | ||||||
| 
 | 
 | ||||||
|     @action(methods=["post"], detail=False, perms_map={"post": "mpoint.create"}, serializer_class=Serializer) |  | ||||||
|     def show_picture(self, request, *args, **kwargs): |  | ||||||
|         import requests |  | ||||||
|         import os |  | ||||||
|         headers = { |  | ||||||
|             "Content-Type": "application/json;charset=utf-8", |  | ||||||
|         } |  | ||||||
|         url = "http://localhost:8093/boxplot" |  | ||||||
|         payload = { |  | ||||||
|             "startTime1": request.data.get("startTime1"), |  | ||||||
|             "endTime1": request.data.get("endTime1"), |  | ||||||
|             "startTime2": request.data.get("startTime2"), |  | ||||||
|             "endTime2": request.data.get("endTime2") |  | ||||||
|         } |  | ||||||
|         try: |  | ||||||
|             response = requests.request("POST", url, json=payload, headers=headers) |  | ||||||
|         except Exception as e: |  | ||||||
|             myLogger.error(e) |  | ||||||
|         pic_dir = os.path.join(settings.MEDIA_ROOT, "box_pic") |  | ||||||
|         os.makedirs(pic_dir, exist_ok=True) |  | ||||||
|         file_name= datetime.now().strftime('%Y%m%d_%H%M%S')+'.png' |  | ||||||
|         pic_path = os.path.join(pic_dir, file_name) |  | ||||||
|         with open(pic_path, 'wb') as f: |  | ||||||
|             f.write(response.content) |  | ||||||
|         rel_path = os.path.join('media/box_pic', file_name) |  | ||||||
|         rel_path = rel_path.replace('\\', '/') |  | ||||||
|         return Response({"rel_path": rel_path}) |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class XscriptViewSet(CustomModelViewSet): | class XscriptViewSet(CustomModelViewSet): | ||||||
|     """ |     """ | ||||||
|  | @ -173,97 +138,6 @@ class XscriptViewSet(CustomModelViewSet): | ||||||
| #     select_related_fields = ['mpoint'] | #     select_related_fields = ['mpoint'] | ||||||
| #     filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept'] | #     filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept'] | ||||||
| 
 | 
 | ||||||
| class MpLogxAPIView(APIView): |  | ||||||
|     """ |  | ||||||
|     list:测点采集数据 |  | ||||||
| 
 |  | ||||||
|     测点采集数据 |  | ||||||
|     """ |  | ||||||
|     perms_map = {"get": "*"} |  | ||||||
| 
 |  | ||||||
|     @swagger_auto_schema(manual_parameters=[ |  | ||||||
|         openapi.Parameter('mpoint', openapi.IN_QUERY, description='测点ID', type=openapi.TYPE_STRING), |  | ||||||
|         openapi.Parameter('timex__gte', openapi.IN_QUERY, description='开始时间', type=openapi.TYPE_STRING), |  | ||||||
|         openapi.Parameter('timex__lte', openapi.IN_QUERY, description='结束时间', type=openapi.TYPE_STRING), |  | ||||||
|         openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER), |  | ||||||
|         openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER), |  | ||||||
|         openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段,如 -timex', type=openapi.TYPE_STRING), |  | ||||||
|         openapi.Parameter('fields', openapi.IN_QUERY, description='返回字段,如 timex,val_float,val_int', type=openapi.TYPE_STRING), |  | ||||||
|     ]) |  | ||||||
|     def get(self, request, *args, **kwargs): |  | ||||||
|         mpoint = request.query_params.get("mpoint", None) |  | ||||||
|         timex__gte_str = request.query_params.get("timex__gte", None) |  | ||||||
|         timex__lte_str = request.query_params.get("timex__lte", None) |  | ||||||
|         page = int(request.query_params.get("page", 1)) |  | ||||||
|         page_size = int(request.query_params.get("page_size", 20)) |  | ||||||
|         fields = request.query_params.get("fields", None) |  | ||||||
|         if page < 0 and page_size < 0: |  | ||||||
|             raise ParseError("page, page_size must be positive") |  | ||||||
|         ordering = request.query_params.get("ordering", "-timex")  # 默认倒序 |  | ||||||
| 
 |  | ||||||
|         if mpoint is None or timex__gte_str is None: |  | ||||||
|             raise ParseError("mpoint, timex__gte are required") |  | ||||||
| 
 |  | ||||||
|         # 处理时间 |  | ||||||
|         timex__gte = timezone.make_aware(datetime.strptime(timex__gte_str, "%Y-%m-%d %H:%M:%S")) |  | ||||||
|         timex__lte = timezone.make_aware(datetime.strptime(timex__lte_str, "%Y-%m-%d %H:%M:%S")) if timex__lte_str else timezone.now() |  | ||||||
| 
 |  | ||||||
|         # 统计总数 |  | ||||||
|         count_sql = """SELECT COUNT(*) AS total_count FROM enm_mplogx  |  | ||||||
|                     WHERE mpoint_id=%s AND timex >= %s AND timex <= %s""" |  | ||||||
|         count_data = query_one_dict(count_sql, [mpoint, timex__gte, timex__lte], with_time_format=True) |  | ||||||
| 
 |  | ||||||
|         # 排序白名单 |  | ||||||
|         allowed_fields = {"timex", "val_mrs", "val_int", "val_float"}  # 根据表字段修改 |  | ||||||
|         order_fields = [] |  | ||||||
|         for field in ordering.split(","): |  | ||||||
|             field = field.strip() |  | ||||||
|             if not field: |  | ||||||
|                 continue |  | ||||||
|             desc = field.startswith("-") |  | ||||||
|             field_name = field[1:] if desc else field |  | ||||||
|             if field_name in allowed_fields: |  | ||||||
|                 order_fields.append(f"{field_name} {'DESC' if desc else 'ASC'}") |  | ||||||
| 
 |  | ||||||
|         # 如果没有合法字段,使用默认排序 |  | ||||||
|         if not order_fields: |  | ||||||
|             order_fields = ["timex DESC"] |  | ||||||
| 
 |  | ||||||
|         order_clause = "ORDER BY " + ", ".join(order_fields) |  | ||||||
| 
 |  | ||||||
|         # 构造 SQL |  | ||||||
|         if page == 0: |  | ||||||
|             if fields: |  | ||||||
|                 # 过滤白名单,避免非法列 |  | ||||||
|                 fields = [f for f in fields.split(",") if f in allowed_fields] |  | ||||||
|                 if not fields: |  | ||||||
|                     fields = ["timex", "val_float", "val_int"]  # 默认列 |  | ||||||
|                 select_clause = ", ".join(fields) |  | ||||||
|             else: |  | ||||||
|                 select_clause = "timex, val_float, val_int"  # 默认列 |  | ||||||
|             page_sql = f"""SELECT {select_clause} FROM enm_mplogx  |  | ||||||
|                         WHERE mpoint_id=%s AND timex >= %s AND timex <= %s |  | ||||||
|                         {order_clause}""" |  | ||||||
|             page_params = [mpoint, timex__gte, timex__lte] |  | ||||||
|         else: |  | ||||||
|             page_sql = f"""SELECT * FROM enm_mplogx  |  | ||||||
|                         WHERE mpoint_id=%s AND timex >= %s AND timex <= %s |  | ||||||
|                         {order_clause} LIMIT %s OFFSET %s""" |  | ||||||
|             page_params = [mpoint, timex__gte, timex__lte, page_size, (page-1)*page_size] |  | ||||||
| 
 |  | ||||||
|         page_data = query_all_dict(page_sql, page_params, with_time_format=True) |  | ||||||
| 
 |  | ||||||
|         if page == 0: |  | ||||||
|             return Response(page_data) |  | ||||||
| 
 |  | ||||||
|         return Response({ |  | ||||||
|             "count": count_data["total_count"], |  | ||||||
|             "page": page, |  | ||||||
|             "page_size": page_size, |  | ||||||
|             "results": page_data |  | ||||||
|         }) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet): | class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet): | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|  | @ -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, |  | ||||||
|             }, |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
|  | @ -39,7 +39,8 @@ def correct_mb_count_notok(): | ||||||
|         count_notok = mi.count_n_zw + mi.count_n_tw + mi.count_n_qp + mi.count_n_wq + mi.count_n_dl + mi.count_n_pb + mi.count_n_dxt + mi.count_n_js + mi.count_n_qx + mi.count_n_zz + mi.count_n_ysq + mi.count_n_hs + mi.count_n_b + mi.count_n_qt |         count_notok = mi.count_n_zw + mi.count_n_tw + mi.count_n_qp + mi.count_n_wq + mi.count_n_dl + mi.count_n_pb + mi.count_n_dxt + mi.count_n_js + mi.count_n_qx + mi.count_n_zz + mi.count_n_ysq + mi.count_n_hs + mi.count_n_b + mi.count_n_qt | ||||||
|         # 先处理库存 |         # 先处理库存 | ||||||
|         try: |         try: | ||||||
|             MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok) |             with transaction.atomic(): | ||||||
|             InmService.update_mb_after_test(mi) |                 MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok) | ||||||
|  |                 InmService.update_mb_after_test(mi) | ||||||
|         except ParseError as e: |         except ParseError as e: | ||||||
|             MIOItem.objects.filter(id=mi.id).update(test_date=None) |             MIOItem.objects.filter(id=mi.id).update(test_date=None) | ||||||
|  |  | ||||||
|  | @ -37,9 +37,7 @@ class MioFilter(filters.FilterSet): | ||||||
|             "item_mio__test_user": ["isnull"], |             "item_mio__test_user": ["isnull"], | ||||||
|             "item_mio__w_mioitem__number": ["exact"], |             "item_mio__w_mioitem__number": ["exact"], | ||||||
|             "mgroup": ["exact"], |             "mgroup": ["exact"], | ||||||
|             "item_mio__batch": ["exact"], |             "item_mio__batch": ["exact"] | ||||||
|             "inout_date": ["gte", "lte", "exact"], |  | ||||||
|             "belong_dept": ["exact"] |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     def filter_materials__type(self, queryset, name, value): |     def filter_materials__type(self, queryset, name, value): | ||||||
|  |  | ||||||
|  | @ -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.mtm.models import Material, Mgroup | ||||||
| from apps.system.models import User | from apps.system.models import User | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from django.db.models import Max, Sum | from django.db.models import Max | ||||||
| # Create your models here. | # Create your models here. | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -39,10 +39,6 @@ class MaterialBatch(BaseModel): | ||||||
|     defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True) |     defect = models.ForeignKey('qm.defect', verbose_name='缺陷', on_delete=models.PROTECT, null=True, blank=True) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     @property |  | ||||||
|     def count_mioing(self):  |  | ||||||
|         return  MIOItem.objects.filter(mb=self, mio__submit_time__isnull=True).aggregate(count=Sum('count'))['count'] or 0 |  | ||||||
| 
 |  | ||||||
| class MaterialBatchA(BaseModel): | class MaterialBatchA(BaseModel): | ||||||
|     """ |     """ | ||||||
|     TN:组合件物料批次 |     TN:组合件物料批次 | ||||||
|  | @ -56,15 +52,12 @@ class MaterialBatchA(BaseModel): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| MIO_TYPE_PREFIX = { | MIO_TYPE_PREFIX = { | ||||||
|     'do_in': 'SCRK',      # 生产入库 |     'do_out': 'SCLL',     # 生产领料 (Shēngchǎn Lǐngliào) | ||||||
|     'do_out': 'SCLL',     # 生产领料 |     'sale_out': 'XSFH',   # 销售发货 (Xiāoshòu Fāhuò) | ||||||
|     'sale_out': 'XSFH',   # 销售发货 |     'pur_in': 'CGRK',     # 采购入库 (Cǎigòu Rùkù) | ||||||
|     'pur_in': 'CGRK',     # 采购入库 |     'do_in': 'SCRK',      # 生产入库 (Shēngchǎn Rùkù) | ||||||
|     'pur_out': 'CGTH',    # 采购退货 |     'other_in': 'QTRK',   # 其他入库 (Qítā Rùkù) | ||||||
|     'borrow_out': 'LYCK',   # 领用出库 |     'other_out': 'QTCK'   # 其他出库 (Qítā Chūkù) | ||||||
|     'return_in': 'THRK',    # 退还入库 |  | ||||||
|     'other_in': 'QTRK',   # 其他入库 |  | ||||||
|     'other_out': 'QTCK'   # 其他出库 |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| class MIO(CommonBDModel): | class MIO(CommonBDModel): | ||||||
|  | @ -74,21 +67,15 @@ class MIO(CommonBDModel): | ||||||
|     MIO_TYPE_DO_OUT = 'do_out' |     MIO_TYPE_DO_OUT = 'do_out' | ||||||
|     MIO_TYPE_SALE_OUT = 'sale_out' |     MIO_TYPE_SALE_OUT = 'sale_out' | ||||||
|     MIO_TYPE_PUR_IN = 'pur_in' |     MIO_TYPE_PUR_IN = 'pur_in' | ||||||
|     MIO_TYPE_PUR_OUT = 'pur_out' |  | ||||||
|     MIO_TYPE_DO_IN = 'do_in' |     MIO_TYPE_DO_IN = 'do_in' | ||||||
|     MIO_TYPE_OTHER_IN = 'other_in' |     MIO_TYPE_OTHER_IN = 'other_in' | ||||||
|     MIO_TYPE_OTHER_OUT = 'other_out' |     MIO_TYPE_OTHER_OUT = 'other_out' | ||||||
|     MIO_TYPE_BORROW_OUT = 'borrow_out' |  | ||||||
|     MIO_TYPE_RETURN_IN = 'return_in' |  | ||||||
| 
 | 
 | ||||||
|     MIO_TYPES = ( |     MIO_TYPES = ( | ||||||
|         (MIO_TYPE_DO_OUT, '生产领料'), |         (MIO_TYPE_DO_OUT, '生产领料'), | ||||||
|         (MIO_TYPE_SALE_OUT, '销售发货'), |         (MIO_TYPE_SALE_OUT, '销售发货'), | ||||||
|         (MIO_TYPE_PUR_IN, '采购入库'), |         (MIO_TYPE_PUR_IN, '采购入库'), | ||||||
|         (MIO_TYPE_PUR_OUT, '采购退货'), |  | ||||||
|         (MIO_TYPE_DO_IN, '生产入库'), |         (MIO_TYPE_DO_IN, '生产入库'), | ||||||
|         (MIO_TYPE_BORROW_OUT, '领用出库'), |  | ||||||
|         (MIO_TYPE_RETURN_IN, '退还入库'), |  | ||||||
|         (MIO_TYPE_OTHER_IN, '其他入库'), |         (MIO_TYPE_OTHER_IN, '其他入库'), | ||||||
|         (MIO_TYPE_OTHER_OUT, '其他出库') |         (MIO_TYPE_OTHER_OUT, '其他出库') | ||||||
|     ) |     ) | ||||||
|  | @ -137,13 +124,6 @@ class MIO(CommonBDModel): | ||||||
|             last_number = 1 |             last_number = 1 | ||||||
|         return f"{prefix}-{today_str}-{last_number:04d}" |         return f"{prefix}-{today_str}-{last_number:04d}" | ||||||
| 
 | 
 | ||||||
| class Pack(BaseModel): |  | ||||||
|     """ |  | ||||||
|     TN:装箱单 |  | ||||||
|     """ |  | ||||||
|     index = models.PositiveSmallIntegerField('序号', default=1) |  | ||||||
|     mio = models.ForeignKey(MIO, verbose_name='关联出入库记录', on_delete=models.CASCADE, related_name='pack_mio') |  | ||||||
| 
 |  | ||||||
| class MIOItem(BaseModel): | class MIOItem(BaseModel): | ||||||
|     """ |     """ | ||||||
|     TN:出入库明细 |     TN:出入库明细 | ||||||
|  | @ -188,11 +168,6 @@ class MIOItem(BaseModel): | ||||||
|     count_n_qt = models.PositiveIntegerField('其他', default=0) |     count_n_qt = models.PositiveIntegerField('其他', default=0) | ||||||
| 
 | 
 | ||||||
|     is_testok = models.BooleanField('检验是否合格', null=True, blank=True) |     is_testok = models.BooleanField('检验是否合格', null=True, blank=True) | ||||||
|     note = models.TextField('备注', null=True, blank=True) |  | ||||||
|     pack_index = models.PositiveSmallIntegerField('装箱序号', null=True, blank=True) |  | ||||||
|      |  | ||||||
|     # 以下字段暂时不用 |  | ||||||
|     pack = models.ForeignKey(Pack, verbose_name='关联装箱单', on_delete=models.SET_NULL, related_name='mioitem_pack', null=True, blank=True) |  | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def count_fields(cls): |     def count_fields(cls): | ||||||
|  | @ -227,7 +202,6 @@ class MIOItemw(BaseModel): | ||||||
|     TN:单件记录 |     TN:单件记录 | ||||||
|     """ |     """ | ||||||
|     number = models.TextField('编号') |     number = models.TextField('编号') | ||||||
|     number_out = models.TextField('对外编号', null=True, blank=True) |  | ||||||
|     wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw' |     wpr = models.ForeignKey("wpmw.wpr", verbose_name='关联产品', on_delete=models.SET_NULL, related_name='wpr_mioitemw' | ||||||
|                             , null=True, blank=True) |                             , null=True, blank=True) | ||||||
|     mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem') |     mioitem = models.ForeignKey(MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='w_mioitem') | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ from apps.system.models import Dept, User | ||||||
| from apps.utils.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS | from apps.utils.constants import EXCLUDE_FIELDS_BASE, EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS | ||||||
| from apps.utils.serializers import CustomModelSerializer | from apps.utils.serializers import CustomModelSerializer | ||||||
| from apps.mtm.models import Material | from apps.mtm.models import Material | ||||||
| from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw, Pack | from .models import MIO, MaterialBatch, MIOItem, WareHouse, MIOItemA, MaterialBatchA, MIOItemw | ||||||
| from django.db import transaction | from django.db import transaction | ||||||
| from server.settings import get_sysconfig | from server.settings import get_sysconfig | ||||||
| from apps.wpmw.models import Wpr | from apps.wpmw.models import Wpr | ||||||
|  | @ -30,15 +30,6 @@ class MaterialBatchAListSerializer(CustomModelSerializer): | ||||||
|         fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_'] |         fields = ['material', 'batch', 'rate', 'mb', 'id', 'material_'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MaterialBatchAListSerializer2(CustomModelSerializer): |  | ||||||
|     material_name = serializers.StringRelatedField( |  | ||||||
|         source='material', read_only=True) |  | ||||||
| 
 |  | ||||||
|     class Meta: |  | ||||||
|         model = MaterialBatchA |  | ||||||
|         fields = ['material', 'batch', 'rate', 'mb', |  | ||||||
|                   'id', 'material_name'] |  | ||||||
| 
 |  | ||||||
| class MaterialBatchSerializer(CustomModelSerializer): | class MaterialBatchSerializer(CustomModelSerializer): | ||||||
|     warehouse_name = serializers.CharField( |     warehouse_name = serializers.CharField( | ||||||
|         source='warehouse.name', read_only=True) |         source='warehouse.name', read_only=True) | ||||||
|  | @ -48,19 +39,12 @@ class MaterialBatchSerializer(CustomModelSerializer): | ||||||
|         source='supplier', read_only=True) |         source='supplier', read_only=True) | ||||||
|     material_ = MaterialSerializer(source='material', read_only=True) |     material_ = MaterialSerializer(source='material', read_only=True) | ||||||
|     defect_name = serializers.CharField(source="defect.name", read_only=True) |     defect_name = serializers.CharField(source="defect.name", read_only=True) | ||||||
|     count_mioing = serializers.IntegerField(read_only=True, label='正在出入库数量') |  | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MaterialBatch |         model = MaterialBatch | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|         read_only_fields = EXCLUDE_FIELDS_BASE |         read_only_fields = EXCLUDE_FIELDS_BASE | ||||||
| 
 | 
 | ||||||
|     def to_representation(self, instance): |  | ||||||
|         ret = super().to_representation(instance) |  | ||||||
|         if 'count' in ret and 'count_mioing' in ret: |  | ||||||
|             ret['count_canmio'] = str(Decimal(ret['count']) - Decimal(ret['count_mioing'])) |  | ||||||
|         return ret |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class MaterialBatchDetailSerializer(CustomModelSerializer): | class MaterialBatchDetailSerializer(CustomModelSerializer): | ||||||
|     warehouse_name = serializers.CharField( |     warehouse_name = serializers.CharField( | ||||||
|  | @ -126,15 +110,14 @@ class MIOItemCreateSerializer(CustomModelSerializer): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MIOItem |         model = MIOItem | ||||||
|         fields = ['mio', 'warehouse', 'material', |         fields = ['mio', 'warehouse', 'material', | ||||||
|                   'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price', 'note', "pack_index"] |                   'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price'] | ||||||
|         extra_kwargs = { |         extra_kwargs = { | ||||||
|             'mio': {'required': True}, 'warehouse': {'required': False}, |             'mio': {'required': True}, 'warehouse': {'required': False}, | ||||||
|             'material': {'required': False}, 'batch': {'required': False, "allow_null": True, "allow_blank": True}} |             'material': {'required': False}, 'batch': {'required': False}} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         mio:MIO = validated_data['mio'] |         mio:MIO = validated_data['mio'] | ||||||
|         mio_type = mio.type |  | ||||||
|         mb = validated_data.get('mb', None) |         mb = validated_data.get('mb', None) | ||||||
|         wm = validated_data.get('wm', None) |         wm = validated_data.get('wm', None) | ||||||
|         assemb = validated_data.pop('assemb', []) |         assemb = validated_data.pop('assemb', []) | ||||||
|  | @ -153,14 +136,9 @@ class MIOItemCreateSerializer(CustomModelSerializer): | ||||||
|             validated_data["batch"] = wm.batch |             validated_data["batch"] = wm.batch | ||||||
| 
 | 
 | ||||||
|         material: Material = validated_data['material'] |         material: Material = validated_data['material'] | ||||||
|         batch = validated_data.get("batch", None) |         batch = validated_data['batch'] | ||||||
|         if not batch: |  | ||||||
|             batch = "无" |  | ||||||
|         if material.is_hidden: |         if material.is_hidden: | ||||||
|             raise ParseError('隐式物料不可出入库') |             raise ParseError('隐式物料不可出入库') | ||||||
|         if mio.type in [MIO.MIO_TYPE_RETURN_IN, MIO.MIO_TYPE_BORROW_OUT]: |  | ||||||
|             if not material.into_wm: |  | ||||||
|                 raise ParseError('该物料不可领用或归还') |  | ||||||
| 
 | 
 | ||||||
|         if mio.state != MIO.MIO_CREATE: |         if mio.state != MIO.MIO_CREATE: | ||||||
|             raise ParseError('出入库记录非创建中不可新增') |             raise ParseError('出入库记录非创建中不可新增') | ||||||
|  | @ -171,59 +149,50 @@ class MIOItemCreateSerializer(CustomModelSerializer): | ||||||
|             mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN]) |             mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN]) | ||||||
|             if mis.exists() and (not mis.exclude(test_date=None).exists()): |             if mis.exists() and (not mis.exclude(test_date=None).exists()): | ||||||
|                 raise ParseError('该批次的物料未经检验') |                 raise ParseError('该批次的物料未经检验') | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         count = validated_data["count"] |             count = validated_data["count"] | ||||||
|         batch = validated_data["batch"] |             batch = validated_data["batch"] | ||||||
|         mioitemw = validated_data.pop('mioitemw', []) |             mioitemw = validated_data.pop('mioitemw', []) | ||||||
|         instance:MIOItem = super().create(validated_data) |             instance = super().create(validated_data) | ||||||
|         assemb_dict = {} |             assemb_dict = {} | ||||||
|         for i in assemb: |             for i in assemb: | ||||||
|             assemb_dict[i['material'].id] = i |                 assemb_dict[i['material'].id] = i | ||||||
|         if material.is_assemb and '_in' in mio.type:  # 仅入库且是组合件的时候需要填写下一级 |             if material.is_assemb and '_in' in mio.type:  # 仅入库且是组合件的时候需要填写下一级 | ||||||
|             components = material.components |                 components = material.components | ||||||
|             for k, v in components.items(): |                 for k, v in components.items(): | ||||||
|                 if k in assemb_dict: |                     if k in assemb_dict: | ||||||
|                     mia = assemb_dict[k] |                         mia = assemb_dict[k] | ||||||
|                     MIOItemA.objects.create( |                         MIOItemA.objects.create( | ||||||
|                         mioitem=instance, rate=v, **mia) |                             mioitem=instance, rate=v, **mia) | ||||||
|                 else: |  | ||||||
|                     raise ParseError('缺少组合件') |  | ||||||
|         if material.tracking == Material.MA_TRACKING_SINGLE: |  | ||||||
|             if len(mioitemw) == 0: |  | ||||||
|                 if mb: |  | ||||||
|                     wpr_qs = Wpr.get_qs_by_mb(mb) |  | ||||||
|                     if wpr_qs.count() == validated_data["count"]: |  | ||||||
|                         for item in wpr_qs: |  | ||||||
|                             MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item) |  | ||||||
|                     else: |                     else: | ||||||
|                         raise ParseError('请提供产品明细编号') |                         raise ParseError('缺少组合件') | ||||||
|                 elif wm: |             if material.tracking == Material.MA_TRACKING_SINGLE: | ||||||
|                     wpr_qs = Wpr.get_qs_by_wm(wm) |                 if len(mioitemw) == 0: | ||||||
|                     if wpr_qs.count() == validated_data["count"]: |                     if mb: | ||||||
|                         for item in wpr_qs: |                         wpr_qs = Wpr.get_qs_by_mb(mb) | ||||||
|                             MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item) |                         if wpr_qs.count() == validated_data["count"]: | ||||||
|  |                             for item in wpr_qs: | ||||||
|  |                                 MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item) | ||||||
|  |                         else: | ||||||
|  |                             raise ParseError('请提供产品明细编号') | ||||||
|  |                     elif wm: | ||||||
|  |                         wpr_qs = Wpr.get_qs_by_wm(wm) | ||||||
|  |                         if wpr_qs.count() == validated_data["count"]: | ||||||
|  |                             for item in wpr_qs: | ||||||
|  |                                 MIOItemw.objects.create(mioitem=instance, number=item.number, wpr=item) | ||||||
|  |                         else: | ||||||
|  |                             raise ParseError('请提供产品明细编号') | ||||||
|  |                     elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1: | ||||||
|  |                         MIOItemw.objects.create(mioitem=instance, number=batch) | ||||||
|                     else: |                     else: | ||||||
|                         raise ParseError('请提供产品明细编号') |                         raise ParseError('不支持自动生成请提供产品明细') | ||||||
|                 elif mio.type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_OTHER_IN] and count==1: |                 elif len(mioitemw) >= 1: | ||||||
|                     MIOItemw.objects.create(mioitem=instance, number=batch) |                     mio_type = mio.type | ||||||
|                 else: |                     for item in mioitemw: | ||||||
|                     raise ParseError('不支持自动生成请提供产品明细') |                         if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in": | ||||||
|             elif len(mioitemw) >= 1: |                             raise ParseError(f'{item["number"]}_请提供产品明细ID') | ||||||
|                 mio_type = mio.type |                         else: | ||||||
|                 if mio_type != "pur_in" and mio_type != "other_in": |                             MIOItemw.objects.create(mioitem=instance, **item) | ||||||
|                     wprIds = [i["wpr"].id for i in mioitemw] |  | ||||||
|                     mb_ids = list(Wpr.objects.filter(id__in=wprIds).values_list("mb__id", flat=True).distinct()) |  | ||||||
|                     if len(mb_ids) == 1 and mb_ids[0] == instance.mb.id: |  | ||||||
|                         pass |  | ||||||
|                     else: |  | ||||||
|                         raise ParseError(f'{batch}物料明细中存在{len(mb_ids)}个不同物料批次') |  | ||||||
|                 for item in mioitemw: |  | ||||||
|                     if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in": |  | ||||||
|                         raise ParseError(f'{item["number"]}_请提供产品明细ID') |  | ||||||
|                     elif item.get("number_out", None) is not None and mio_type != MIO.MIO_TYPE_SALE_OUT: |  | ||||||
|                         raise ParseError(f'{item["number"]}_非销售出库不可赋予产品对外编号') |  | ||||||
|                     else: |  | ||||||
|                         MIOItemw.objects.create(mioitem=instance, **item) |  | ||||||
|         return instance |         return instance | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -234,14 +203,15 @@ class MIOItemAListSerializer(CustomModelSerializer): | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MIOItemA |         model = MIOItemA | ||||||
|         fields = "__all__" |         fields = ['material', 'batch', 'rate', 'mioitem', | ||||||
|         read_only_fields = EXCLUDE_FIELDS_BASE |                   'id', 'material_', 'material_name'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MIOItemSerializer(CustomModelSerializer): | class MIOItemSerializer(CustomModelSerializer): | ||||||
|     warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) |     warehouse_name = serializers.CharField(source='warehouse.name', read_only=True) | ||||||
|     material_ = MaterialSerializer(source='material', read_only=True) |     material_ = MaterialSerializer(source='material', read_only=True) | ||||||
|     assemb = serializers.SerializerMethodField(label="组合件信息") |     assemb = MIOItemAListSerializer( | ||||||
|  |         source='a_mioitem', read_only=True, many=True) | ||||||
|     material_name = serializers.StringRelatedField( |     material_name = serializers.StringRelatedField( | ||||||
|         source='material', read_only=True) |         source='material', read_only=True) | ||||||
|     inout_date = serializers.DateField(source='mio.inout_date', read_only=True) |     inout_date = serializers.DateField(source='mio.inout_date', read_only=True) | ||||||
|  | @ -259,15 +229,6 @@ class MIOItemSerializer(CustomModelSerializer): | ||||||
|             ret["price"] = Decimal(ret["count"]) * Decimal(ret["unit_price"]) |             ret["price"] = Decimal(ret["count"]) * Decimal(ret["unit_price"]) | ||||||
|         return ret |         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): | class MioItemDetailSerializer(MIOItemSerializer): | ||||||
|     mio_ = MIOListSerializer(source='mio', read_only=True) |     mio_ = MIOListSerializer(source='mio', read_only=True) | ||||||
| 
 | 
 | ||||||
|  | @ -283,11 +244,8 @@ class MIODoSerializer(CustomModelSerializer): | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MIO |         model = MIO | ||||||
|         fields = ['id', 'number', 'note', 'do_user', |         fields = ['id', 'number', 'note', 'do_user', | ||||||
|                   'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user', 'type'] |                   'belong_dept', 'type', 'inout_date', 'mgroup', 'mio_user'] | ||||||
|         extra_kwargs = {'inout_date': {'required': True},  |         extra_kwargs = {'inout_date': {'required': True}, 'do_user': {'required': True}, 'number': {"required": False, "allow_blank": True}} | ||||||
|                         'do_user': {'required': True},  |  | ||||||
|                         'number': {"required": False, "allow_blank": True}, |  | ||||||
|                         'type': {'required': True}} |  | ||||||
| 
 | 
 | ||||||
|     def validate(self, attrs): |     def validate(self, attrs): | ||||||
|         if 'mgroup' in attrs and attrs['mgroup']: |         if 'mgroup' in attrs and attrs['mgroup']: | ||||||
|  | @ -297,13 +255,10 @@ class MIODoSerializer(CustomModelSerializer): | ||||||
|         return attrs |         return attrs | ||||||
|      |      | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         type = validated_data['type'] |  | ||||||
|         if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]: |  | ||||||
|             pass |  | ||||||
|         else: |  | ||||||
|             raise ValidationError('出入库类型错误') |  | ||||||
|         if not validated_data.get("number", None): |         if not validated_data.get("number", None): | ||||||
|             validated_data["number"] = MIO.get_a_number(validated_data["type"]) |             validated_data["number"] = MIO.get_a_number(validated_data["type"]) | ||||||
|  |         if validated_data['type'] not in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_DO_IN]: | ||||||
|  |             raise ValidationError('出入库类型错误') | ||||||
|         return super().create(validated_data) |         return super().create(validated_data) | ||||||
| 
 | 
 | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|  | @ -348,17 +303,11 @@ class MIOPurSerializer(CustomModelSerializer): | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MIO |         model = MIO | ||||||
|         fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user', 'type'] |         fields = ['id', 'number', 'note', 'pu_order', 'inout_date', 'supplier', 'mio_user'] | ||||||
|         extra_kwargs = {'inout_date': {'required': True},  |         extra_kwargs = {'inout_date': {'required': True}, 'number': {"required": False, "allow_blank": True}} | ||||||
|                         'number': {"required": False, "allow_blank": True}, |  | ||||||
|                         'type': {'required': True}} |  | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         type = validated_data["type"] |         validated_data['type'] = MIO.MIO_TYPE_PUR_IN | ||||||
|         if type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]: |  | ||||||
|             pass |  | ||||||
|         else: |  | ||||||
|             raise ValidationError('出入库类型错误') |  | ||||||
|         if not validated_data.get("number", None): |         if not validated_data.get("number", None): | ||||||
|             validated_data["number"] = MIO.get_a_number(validated_data["type"]) |             validated_data["number"] = MIO.get_a_number(validated_data["type"]) | ||||||
|         pu_order: PuOrder = validated_data.get('pu_order', None) |         pu_order: PuOrder = validated_data.get('pu_order', None) | ||||||
|  | @ -443,23 +392,3 @@ class MIOItemPurInTestSerializer(CustomModelSerializer): | ||||||
|             attrs['weight_kgs'] = [float(i) for i in weight_kgs] |             attrs['weight_kgs'] = [float(i) for i in weight_kgs] | ||||||
|             attrs['count_sampling'] = len(attrs['weight_kgs']) |             attrs['count_sampling'] = len(attrs['weight_kgs']) | ||||||
|         return super().validate(attrs) |         return super().validate(attrs) | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PackSerializer(CustomModelSerializer): |  | ||||||
|     class Meta: |  | ||||||
|         model = Pack |  | ||||||
|         fields = "__all__" |  | ||||||
|         read_only_fields = EXCLUDE_FIELDS_BASE |  | ||||||
|      |  | ||||||
|     def create(self, validated_data): |  | ||||||
|         index = validated_data["index"] |  | ||||||
|         mio = validated_data["mio"] |  | ||||||
|         if Pack.objects.filter(mio=mio, index=index).exists(): |  | ||||||
|             raise ParseError('包装箱已存在') |  | ||||||
|         return super().create(validated_data) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class PackMioSerializer(serializers.Serializer): |  | ||||||
|     mioitems = serializers.ListField(child=serializers.CharField(), label="明细ID") |  | ||||||
|     pack_index = serializers.IntegerField(label="包装箱序号") |  | ||||||
|     # pack = serializers.CharField(label="包装箱ID") |  | ||||||
|  | @ -5,15 +5,13 @@ from django.db import transaction | ||||||
| from rest_framework.exceptions import ParseError | from rest_framework.exceptions import ParseError | ||||||
| from apps.wpmw.models import Wpr | from apps.wpmw.models import Wpr | ||||||
| from apps.mtm.models import Material | from apps.mtm.models import Material | ||||||
| from rest_framework import serializers |  | ||||||
| 
 | 
 | ||||||
| class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | ||||||
|     ftest = FtestProcessSerializer(required=False) |     ftest = FtestProcessSerializer(required=False) | ||||||
|     wpr_number_out = serializers.CharField(source="wpr.number_out", read_only=True) |  | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = MIOItemw |         model = MIOItemw | ||||||
|         fields = ["id", "number", "wpr", "note", "mioitem", "ftest", "wpr_number_out"] |         fields = ["id", "number", "wpr", "note", "mioitem", "ftest"] | ||||||
| 
 | 
 | ||||||
|     def validate(self, attrs): |     def validate(self, attrs): | ||||||
|         mioitem: MIOItem = attrs["mioitem"] |         mioitem: MIOItem = attrs["mioitem"] | ||||||
|  | @ -45,6 +43,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | ||||||
|             ftest_sr.update(instance=ftest, validated_data=ftest_data) |             ftest_sr.update(instance=ftest, validated_data=ftest_data) | ||||||
|         return mioitemw |         return mioitemw | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         wpr: Wpr = validated_data.get("wpr", None) |         wpr: Wpr = validated_data.get("wpr", None) | ||||||
|         if wpr: |         if wpr: | ||||||
|  | @ -57,6 +56,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer): | ||||||
|             mioitemw = self.save_ftest(mioitemw, ftest_data) |             mioitemw = self.save_ftest(mioitemw, ftest_data) | ||||||
|         return mioitemw |         return mioitemw | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         validated_data.pop("mioitem") |         validated_data.pop("mioitem") | ||||||
|         ftest_data = validated_data.pop("ftest", None) |         ftest_data = validated_data.pop("ftest", None) | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ from apps.wpmw.models import Wpr | ||||||
| from apps.qm.models import Ftest, Defect | from apps.qm.models import Ftest, Defect | ||||||
| from django.db.models import Count, Q | from django.db.models import Count, Q | ||||||
| 
 | 
 | ||||||
| def do_out(item: MIOItem, is_reverse: bool = False): | def do_out(item: MIOItem): | ||||||
|     """ |     """ | ||||||
|     生产领料到车间 |     生产领料到车间 | ||||||
|     """ |     """ | ||||||
|  | @ -23,6 +23,8 @@ def do_out(item: MIOItem, is_reverse: bool = False): | ||||||
|     mgroup = mio.mgroup |     mgroup = mio.mgroup | ||||||
|     do_user = mio.do_user |     do_user = mio.do_user | ||||||
|     material:Material = item.material |     material:Material = item.material | ||||||
|  |     if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目 | ||||||
|  |         return | ||||||
|      |      | ||||||
|     # 获取defect |     # 获取defect | ||||||
|     defect:Defect = None |     defect:Defect = None | ||||||
|  | @ -92,36 +94,29 @@ def do_out(item: MIOItem, is_reverse: bool = False): | ||||||
|                 raise ParseError(f"批次错误!{e}") |                 raise ParseError(f"批次错误!{e}") | ||||||
|             mb.count = mb.count - xcount |             mb.count = mb.count - xcount | ||||||
|             if mb.count < 0: |             if mb.count < 0: | ||||||
|                 raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") |                 raise ParseError(f"{mb.batch}-批次库存不足,操作失败") | ||||||
|             else: |             else: | ||||||
|                 mb.save() |                 mb.save() | ||||||
| 
 | 
 | ||||||
|         if material.into_wm: | 
 | ||||||
|             # 领到车间库存(或工段) |         # 领到车间库存(或工段) | ||||||
|             wm, new_create = WMaterial.objects.get_or_create( |         wm, new_create = WMaterial.objects.get_or_create( | ||||||
|                 batch=xbatch, material=xmaterial, |             batch=xbatch, material=xmaterial, | ||||||
|                 belong_dept=belong_dept, mgroup=mgroup,  |             belong_dept=belong_dept, mgroup=mgroup,  | ||||||
|                 state=WMaterial.WM_OK, defect=defect) |             state=WMaterial.WM_OK, defect=defect) | ||||||
|             if new_create: |         if new_create: | ||||||
|                 wm.create_by = do_user |             wm.create_by = do_user | ||||||
|                 wm.batch_ofrom = mb.batch if mb else None |             wm.batch_ofrom = mb.batch if mb else None | ||||||
|                 wm.material_ofrom = mb.material if mb else None |             wm.material_ofrom = mb.material if mb else None | ||||||
|             wm.count = wm.count + item.count |         wm.count = wm.count + item.count | ||||||
|             wm.update_by = do_user |         wm.update_by = do_user | ||||||
|             wm.save() |         wm.save() | ||||||
| 
 | 
 | ||||||
|         # 开始变动wpr |         # 开始变动wpr | ||||||
|         if xmaterial.tracking == Material.MA_TRACKING_SINGLE: |         if xmaterial.tracking == Material.MA_TRACKING_SINGLE: | ||||||
|             if material.into_wm is False: |  | ||||||
|                 raise ParseError("追踪单个物料不支持不进行车间库存的操作") |  | ||||||
|             mioitemws = MIOItemw.objects.filter(mioitem=item) |             mioitemws = MIOItemw.objects.filter(mioitem=item) | ||||||
|             if mioitemws.count() != item.count: |             if mioitemws.count() != item.count: | ||||||
|                 raise ParseError("出入库与明细数量不一致,操作失败") |                 raise ParseError("出入库与明细数量不一致,操作失败") | ||||||
|             mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct()) |  | ||||||
|             if len(mb_ids) == 1 and mb_ids[0] == mb.id: |  | ||||||
|                 pass |  | ||||||
|             else: |  | ||||||
|                 raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次') |  | ||||||
|             for mioitemw in mioitemws: |             for mioitemw in mioitemws: | ||||||
|                 Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb) |                 Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb) | ||||||
| 
 | 
 | ||||||
|  | @ -141,7 +136,8 @@ def do_in(item: MIOItem): | ||||||
|     mgroup = mio.mgroup |     mgroup = mio.mgroup | ||||||
|     do_user = mio.do_user |     do_user = mio.do_user | ||||||
|     material = item.material |     material = item.material | ||||||
| 
 |     if material.into_wm is False:  # 根据配置不进行入车间库存的处理 | ||||||
|  |         return | ||||||
|     action_list = [] |     action_list = [] | ||||||
|     mias = MIOItemA.objects.filter(mioitem=item) |     mias = MIOItemA.objects.filter(mioitem=item) | ||||||
|     is_zhj = False # 是否组合件入仓库 |     is_zhj = False # 是否组合件入仓库 | ||||||
|  | @ -176,39 +172,38 @@ def do_in(item: MIOItem): | ||||||
|             raise ParseError("存在非正数!") |             raise ParseError("存在非正数!") | ||||||
|          |          | ||||||
|         xbatchs.append(xbatch) |         xbatchs.append(xbatch) | ||||||
|         if material.into_wm: |  | ||||||
|             wm_qs = WMaterial.objects.filter( |  | ||||||
|                 batch=xbatch,  |  | ||||||
|                 material=xmaterial,  |  | ||||||
|                 belong_dept=belong_dept,  |  | ||||||
|                 mgroup=mgroup, |  | ||||||
|                 defect=defect,  |  | ||||||
|                 state=WMaterial.WM_OK) |  | ||||||
|             count_x = wm_qs.count() |  | ||||||
|             if count_x == 1: |  | ||||||
|                 wm = wm_qs.first() |  | ||||||
|             elif count_x == 0: |  | ||||||
|                 raise ParseError( |  | ||||||
|                     f'{str(xmaterial)}-{xbatch}-批次库存不存在!') |  | ||||||
|             else: |  | ||||||
|                 raise ParseError( |  | ||||||
|                     f'{str(xmaterial)}-{xbatch}-存在多个相同批次!') |  | ||||||
| 
 | 
 | ||||||
|             # 扣减车间库存 |         wm_qs = WMaterial.objects.filter( | ||||||
|             new_count = wm.count - xcount |             batch=xbatch,  | ||||||
|             if new_count >= 0: |             material=xmaterial,  | ||||||
|                 wm.count = new_count |             belong_dept=belong_dept,  | ||||||
|                 wm.update_by = do_user |             mgroup=mgroup, | ||||||
|                 wm.save() |             defect=defect,  | ||||||
|             else: |             state=WMaterial.WM_OK) | ||||||
|                 raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足') |         count_x = wm_qs.count() | ||||||
|  |         if count_x == 1: | ||||||
|  |             wm = wm_qs.first() | ||||||
|  |         elif count_x == 0: | ||||||
|  |             raise ParseError( | ||||||
|  |                 f'{str(xmaterial)}-{xbatch}-批次库存不存在!') | ||||||
|  |         else: | ||||||
|  |             raise ParseError( | ||||||
|  |                 f'{str(xmaterial)}-{xbatch}-存在多个相同批次!') | ||||||
|          |          | ||||||
|             wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept |         # 扣减车间库存 | ||||||
|             if production_dept is None: |         new_count = wm.count - xcount | ||||||
|                 production_dept = wm_production_dept |         if new_count >= 0: | ||||||
|             elif wm_production_dept and production_dept != wm_production_dept: |             wm.count = new_count | ||||||
|                 raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间') |             wm.update_by = do_user | ||||||
|  |             wm.save() | ||||||
|  |         else: | ||||||
|  |             raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足') | ||||||
|          |          | ||||||
|  |         wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept | ||||||
|  |         if production_dept is None: | ||||||
|  |             production_dept = wm_production_dept | ||||||
|  |         elif wm_production_dept and production_dept != wm_production_dept: | ||||||
|  |             raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间') | ||||||
|         # 增加mb |         # 增加mb | ||||||
|         if not is_zhj: |         if not is_zhj: | ||||||
|             mb, _ = MaterialBatch.objects.get_or_create( |             mb, _ = MaterialBatch.objects.get_or_create( | ||||||
|  | @ -231,16 +226,9 @@ def do_in(item: MIOItem): | ||||||
| 
 | 
 | ||||||
|             # 开始变动wpr |             # 开始变动wpr | ||||||
|             if xmaterial.tracking == Material.MA_TRACKING_SINGLE: |             if xmaterial.tracking == Material.MA_TRACKING_SINGLE: | ||||||
|                 if material.into_wm is False: |  | ||||||
|                     raise ParseError("追踪单个物料不支持不进行车间库存的操作") |  | ||||||
|                 mioitemws = MIOItemw.objects.filter(mioitem=item) |                 mioitemws = MIOItemw.objects.filter(mioitem=item) | ||||||
|                 if mioitemws.count() != item.count: |                 if mioitemws.count() != item.count: | ||||||
|                     raise ParseError("出入库与明细数量不一致,操作失败") |                     raise ParseError("出入库与明细数量不一致,操作失败") | ||||||
|                 wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct()) |  | ||||||
|                 if len(wm_ids) == 1 and wm_ids[0] == wm.id: |  | ||||||
|                     pass |  | ||||||
|                 else: |  | ||||||
|                     raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次') |  | ||||||
|                 for mioitemw in mioitemws: |                 for mioitemw in mioitemws: | ||||||
|                     Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm) |                     Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm) | ||||||
| 
 | 
 | ||||||
|  | @ -283,9 +271,6 @@ class InmService: | ||||||
|         """ |         """ | ||||||
|         更新库存, 支持反向操作 |         更新库存, 支持反向操作 | ||||||
|         """ |         """ | ||||||
|         if not MIOItem.objects.filter(mio=instance).exists(): |  | ||||||
|             raise ParseError("出入库记录缺失明细,无法操作") |  | ||||||
|          |  | ||||||
|         if instance.type == MIO.MIO_TYPE_PUR_IN:  # 需要更新订单 |         if instance.type == MIO.MIO_TYPE_PUR_IN:  # 需要更新订单 | ||||||
|             # 这里还需要对入厂检验进行处理 |             # 这里还需要对入厂检验进行处理 | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|  | @ -293,32 +278,25 @@ class InmService: | ||||||
|             else: |             else: | ||||||
|                 for item in MIOItem.objects.filter(mio=instance): |                 for item in MIOItem.objects.filter(mio=instance): | ||||||
|                     BatchSt.g_create( |                     BatchSt.g_create( | ||||||
|                         batch=item.batch, mio=instance, material_start=item.material) |                         batch=item.batch, mio=instance, material_start=item.material, reuse_node=True) | ||||||
|             from apps.pum.services import PumService |             from apps.pum.services import PumService | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|                 cls.update_mb(instance, -1) |                 cls.update_mb(instance, -1) | ||||||
|             else: |             else: | ||||||
|                 cls.update_mb(instance, 1) |                 cls.update_mb(instance, 1) | ||||||
|             PumService.mio_pur(instance, is_reverse) |             PumService.mio_purin(instance, is_reverse) | ||||||
|         elif instance.type == MIO.MIO_TYPE_PUR_OUT: |  | ||||||
|             from apps.pum.services import PumService |  | ||||||
|             if is_reverse: |  | ||||||
|                 cls.update_mb(instance, 1) |  | ||||||
|             else: |  | ||||||
|                 cls.update_mb(instance, -1) |  | ||||||
|             PumService.mio_pur(instance, is_reverse) |  | ||||||
|         elif instance.type == MIO.MIO_TYPE_OTHER_IN: |         elif instance.type == MIO.MIO_TYPE_OTHER_IN: | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|                 BatchLog.clear(mio=instance) |                 BatchLog.clear(mio=instance) | ||||||
|             else: |             else: | ||||||
|                 for item in MIOItem.objects.filter(mio=instance): |                 for item in MIOItem.objects.filter(mio=instance): | ||||||
|                     BatchSt.g_create( |                     BatchSt.g_create( | ||||||
|                         batch=item.batch, mio=instance, material_start=item.material) |                         batch=item.batch, mio=instance, material_start=item.material, reuse_node=True) | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|                 cls.update_mb(instance, -1) |                 cls.update_mb(instance, -1) | ||||||
|             else: |             else: | ||||||
|                 cls.update_mb(instance, 1) |                 cls.update_mb(instance, 1) | ||||||
|         elif instance.type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_RETURN_IN]: |         elif instance.type == MIO.MIO_TYPE_DO_IN: | ||||||
|             mioitems = MIOItem.objects.filter(mio=instance) |             mioitems = MIOItem.objects.filter(mio=instance) | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|                 for item in mioitems: |                 for item in mioitems: | ||||||
|  | @ -326,14 +304,6 @@ class InmService: | ||||||
|             else: |             else: | ||||||
|                 for item in mioitems: |                 for item in mioitems: | ||||||
|                     do_in(item) |                     do_in(item) | ||||||
|         elif instance.type in [MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT]: |  | ||||||
|             mioitems = MIOItem.objects.filter(mio=instance) |  | ||||||
|             if is_reverse: |  | ||||||
|                 for item in mioitems: |  | ||||||
|                     do_in(item) |  | ||||||
|             else: |  | ||||||
|                 for item in mioitems: |  | ||||||
|                     do_out(item) |  | ||||||
|         elif instance.type == MIO.MIO_TYPE_SALE_OUT: |         elif instance.type == MIO.MIO_TYPE_SALE_OUT: | ||||||
|             from apps.sam.services import SamService |             from apps.sam.services import SamService | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|  | @ -346,6 +316,14 @@ class InmService: | ||||||
|                 cls.update_mb(instance, 1) |                 cls.update_mb(instance, 1) | ||||||
|             else: |             else: | ||||||
|                 cls.update_mb(instance, -1) |                 cls.update_mb(instance, -1) | ||||||
|  |         elif instance.type == MIO.MIO_TYPE_DO_OUT: | ||||||
|  |             mioitems = MIOItem.objects.filter(mio=instance) | ||||||
|  |             if is_reverse: | ||||||
|  |                 for item in mioitems: | ||||||
|  |                     do_in(item) | ||||||
|  |             else: | ||||||
|  |                 for item in mioitems: | ||||||
|  |                     do_out(item) | ||||||
|         else: |         else: | ||||||
|             raise ParseError('不支持该出入库操作') |             raise ParseError('不支持该出入库操作') | ||||||
| 
 | 
 | ||||||
|  | @ -371,7 +349,6 @@ class InmService: | ||||||
|         out = -1 |         out = -1 | ||||||
|         默认使用count字段做加减 |         默认使用count字段做加减 | ||||||
|         """ |         """ | ||||||
|         mio_type = i.mio.type |  | ||||||
|         material: Material = i.material |         material: Material = i.material | ||||||
|         warehouse = i.warehouse |         warehouse = i.warehouse | ||||||
|         tracking = material.tracking |         tracking = material.tracking | ||||||
|  | @ -410,7 +387,7 @@ class InmService: | ||||||
|             if change_count < 0: |             if change_count < 0: | ||||||
|                 raise ParseError("存在负数!") |                 raise ParseError("存在负数!") | ||||||
|             state = WMaterial.WM_OK |             state = WMaterial.WM_OK | ||||||
|             if defect and defect.okcate in [Defect.DEFECT_NOTOK]: |             if defect: | ||||||
|                 state = WMaterial.WM_NOTOK |                 state = WMaterial.WM_NOTOK | ||||||
|             mb, _ = MaterialBatch.objects.get_or_create( |             mb, _ = MaterialBatch.objects.get_or_create( | ||||||
|                 material=material,  |                 material=material,  | ||||||
|  | @ -442,7 +419,7 @@ class InmService: | ||||||
|             elif in_or_out == -1: |             elif in_or_out == -1: | ||||||
|                 mb.count = mb.count - change_count |                 mb.count = mb.count - change_count | ||||||
|                 if mb.count < 0: |                 if mb.count < 0: | ||||||
|                     raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") |                     raise ParseError(f"{mb.batch}-批次库存不足,操作失败") | ||||||
|                 else: |                 else: | ||||||
|                     mb.save() |                     mb.save() | ||||||
|                     if tracking == Material.MA_TRACKING_SINGLE: |                     if tracking == Material.MA_TRACKING_SINGLE: | ||||||
|  | @ -453,7 +430,7 @@ class InmService: | ||||||
|                         if mioitemws.count() != change_count: |                         if mioitemws.count() != change_count: | ||||||
|                             raise ParseError("出入库与明细数量不一致,操作失败") |                             raise ParseError("出入库与明细数量不一致,操作失败") | ||||||
|                         for mioitemw in mioitemws: |                         for mioitemw in mioitemws: | ||||||
|                             Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb, number_out=mioitemw.number_out) |                             Wpr.change_or_new(wpr=mioitemw.wpr, old_mb=mb) | ||||||
|             else: |             else: | ||||||
|                 raise ParseError("不支持的操作") |                 raise ParseError("不支持的操作") | ||||||
|      |      | ||||||
|  | @ -461,26 +438,3 @@ class InmService: | ||||||
|         ana_batch_thread(xbatchs) |         ana_batch_thread(xbatchs) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     @classmethod |  | ||||||
|     def revert_and_del(cls, mioitem: MIOItem): |  | ||||||
|         mio = mioitem.mio |  | ||||||
|         if mio.submit_time is None: |  | ||||||
|             raise ParseError("未提交的出入库明细不允许撤销") |  | ||||||
|         if mioitem.test_date is not None: |  | ||||||
|             raise ParseError("已检验的出入库明细不允许撤销") |  | ||||||
|         if mio.type == MIO.MIO_TYPE_PUR_IN: |  | ||||||
|             from apps.pum.services import PumService |  | ||||||
|             cls.update_mb_item(mioitem, -1) |  | ||||||
|             BatchLog.clear(mioitem=mioitem) |  | ||||||
|             PumService.mio_pur(mio=mio, is_reverse=True, mioitem=mioitem) |  | ||||||
|             mioitem.delete() |  | ||||||
|         elif mio.type == MIO.MIO_TYPE_OTHER_IN: |  | ||||||
|             cls.update_mb_item(mioitem, -1) |  | ||||||
|             BatchLog.clear(mioitem=mioitem) |  | ||||||
|             mioitem.delete() |  | ||||||
|         elif mio.type == MIO.MIO_TYPE_DO_OUT: |  | ||||||
|             do_in(mioitem) |  | ||||||
|             BatchLog.clear(mioitem=mioitem) |  | ||||||
|             mioitem.delete() |  | ||||||
|         else: |  | ||||||
|             raise ParseError("不支持该出入库单明细撤销") |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| from rest_framework.exceptions import ParseError | from rest_framework.exceptions import ParseError | ||||||
| from apps.mtm.models import Process, Material | from apps.mtm.models import Process, Material | ||||||
| from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw, MIO | from apps.inm.models import WareHouse, MaterialBatch, MIOItem, MIOItemw | ||||||
| from apps.utils.tools import ranstr | from apps.utils.tools import ranstr | ||||||
| from apps.mtm.services_2 import cal_material_count | from apps.mtm.services_2 import cal_material_count | ||||||
| 
 | 
 | ||||||
|  | @ -67,7 +67,7 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem): | ||||||
|     from openpyxl import load_workbook |     from openpyxl import load_workbook | ||||||
|     from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect |     from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect | ||||||
| 
 | 
 | ||||||
|     qct = Qct.get(mioitem.material, tag="inm", type="in") |     qct = Qct.get(mioitem.material, tag="inm") | ||||||
|     if qct is None: |     if qct is None: | ||||||
|         raise ParseError("未找到检验表") |         raise ParseError("未找到检验表") | ||||||
|      |      | ||||||
|  | @ -84,11 +84,8 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem): | ||||||
|     test_user = mioitem.mio.mio_user |     test_user = mioitem.mio.mio_user | ||||||
|     test_date = mioitem.mio.inout_date |     test_date = mioitem.mio.inout_date | ||||||
| 
 | 
 | ||||||
|     wb = load_workbook(path, data_only=True) |     wb = load_workbook(path) | ||||||
|     if "Sheet1" in wb.sheetnames:  # 检查是否存在 |     sheet = wb["Sheet1"] | ||||||
|         sheet = wb["Sheet1"]  # 获取工作表 |  | ||||||
|     else: |  | ||||||
|         raise ParseError("未找到Sheet1") |  | ||||||
| 
 | 
 | ||||||
|     mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number") |     mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number") | ||||||
| 
 | 
 | ||||||
|  | @ -124,38 +121,3 @@ def daoru_mioitem_test(path:str, mioitem:MIOItem): | ||||||
|             FtestItem.objects.bulk_create(ftestitems) |             FtestItem.objects.bulk_create(ftestitems) | ||||||
|         else: |         else: | ||||||
|             break |             break | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def daoru_mioitems(path:str, mio:MIO): |  | ||||||
|     from apps.utils.snowflake import idWorker |  | ||||||
|     from openpyxl import load_workbook |  | ||||||
| 
 |  | ||||||
|     wb = load_workbook(path, data_only=True) |  | ||||||
|     if "Sheet1" in wb.sheetnames:  # 检查是否存在 |  | ||||||
|         sheet = wb["Sheet1"]  # 获取工作表 |  | ||||||
|     else: |  | ||||||
|         raise ParseError("未找到Sheet1") |  | ||||||
|      |  | ||||||
|     mioitems = [] |  | ||||||
|     ind = 2 |  | ||||||
|     while sheet[f"a{ind}"].value: |  | ||||||
|         batch = sheet[f"b{ind}"].value |  | ||||||
|         material_number = sheet[f"a{ind}"].value |  | ||||||
|         try: |  | ||||||
|             material = Material.objects.get(number=material_number) |  | ||||||
|         except Exception as e: |  | ||||||
|             raise ParseError(f"未找到物料:{material_number} {e}") |  | ||||||
|         if batch: |  | ||||||
|             pass |  | ||||||
|         else: |  | ||||||
|             batch = "无" |  | ||||||
|         count = sheet[f"c{ind}"].value |  | ||||||
|         warehouse_name = sheet[f"d{ind}"].value |  | ||||||
|         try: |  | ||||||
|             warehouse = WareHouse.objects.get(name=warehouse_name) |  | ||||||
|         except Exception as e: |  | ||||||
|             raise ParseError(f"未找到仓库:{warehouse_name} {e}") |  | ||||||
|         mioitems.append(MIOItem(mio=mio, warehouse=warehouse, material=material, batch=batch, count=count, id=idWorker.get_id())) |  | ||||||
|         ind = ind + 1 |  | ||||||
|      |  | ||||||
|     MIOItem.objects.bulk_create(mioitems) |  | ||||||
|  | @ -19,7 +19,6 @@ router.register('mio/pur', MioPurViewSet) | ||||||
| router.register('mio/other', MioOtherViewSet) | router.register('mio/other', MioOtherViewSet) | ||||||
| router.register('mioitem', MIOItemViewSet, basename='mioitem') | router.register('mioitem', MIOItemViewSet, basename='mioitem') | ||||||
| router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') | router.register('mioitemw', MIOItemwViewSet, basename='mioitemw') | ||||||
| # router.register('pack', PackViewSet, basename='pack') |  | ||||||
| urlpatterns = [ | urlpatterns = [ | ||||||
|     path(API_BASE_URL, include(router.urls)), |     path(API_BASE_URL, include(router.urls)), | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | @ -9,16 +9,16 @@ from django.utils import timezone | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
| from django.db.models import Sum | from django.db.models import Sum | ||||||
| 
 | 
 | ||||||
| from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw, Pack | from apps.inm.models import WareHouse, MaterialBatch, MIO, MIOItem, MIOItemw | ||||||
| from apps.inm.serializers import ( | from apps.inm.serializers import ( | ||||||
|     MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer, |     MaterialBatchSerializer, WareHourseSerializer, MIOListSerializer, MIOItemSerializer, MioItemAnaSerializer, | ||||||
|     MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,  |     MIODoSerializer, MIOSaleSerializer, MIOPurSerializer, MIOOtherSerializer, MIOItemCreateSerializer,  | ||||||
|     MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer, |     MaterialBatchDetailSerializer, MIODetailSerializer, MIOItemTestSerializer, MIOItemPurInTestSerializer, | ||||||
|     MIOItemwSerializer, MioItemDetailSerializer, PackSerializer, PackMioSerializer) |     MIOItemwSerializer, MioItemDetailSerializer) | ||||||
| from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer | from apps.inm.serializers2 import MIOItemwCreateUpdateSerializer | ||||||
| from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | ||||||
| from apps.inm.services import InmService | from apps.inm.services import InmService | ||||||
| from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test, daoru_mioitems | from apps.inm.services_daoru import daoru_mb, daoru_mioitem_test | ||||||
| from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin, | from apps.utils.mixins import (BulkCreateModelMixin, BulkDestroyModelMixin, BulkUpdateModelMixin, | ||||||
|                                CustomListModelMixin) |                                CustomListModelMixin) | ||||||
| from apps.utils.permission import has_perm | from apps.utils.permission import has_perm | ||||||
|  | @ -27,7 +27,6 @@ from apps.qm.serializers import FtestProcessSerializer | ||||||
| from apps.mtm.models import Material | from apps.mtm.models import Material | ||||||
| from drf_yasg.utils import swagger_auto_schema | from drf_yasg.utils import swagger_auto_schema | ||||||
| from drf_yasg import openapi | from drf_yasg import openapi | ||||||
| from django.db import connection |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| # Create your views here. | # Create your views here. | ||||||
|  | @ -148,19 +147,9 @@ class MIOViewSet(CustomModelViewSet): | ||||||
|     serializer_class = MIOListSerializer |     serializer_class = MIOListSerializer | ||||||
|     retrieve_serializer_class = MIODetailSerializer |     retrieve_serializer_class = MIODetailSerializer | ||||||
|     filterset_class = MioFilter |     filterset_class = MioFilter | ||||||
|     search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model', |     search_fields = ['id', 'number', 'item_mio__batch', 'item_mio__material__name', 'item_mio__material__specification', 'item_mio__material__model'] | ||||||
|                      'item_mio__a_mioitem__batch'] |  | ||||||
|     data_filter = True |     data_filter = True | ||||||
| 
 | 
 | ||||||
|     @classmethod |  | ||||||
|     def lock_and_check_can_update(cls, mio:MIO): |  | ||||||
|         if not connection.in_atomic_block: |  | ||||||
|             raise ParseError("请在事务中调用该方法") |  | ||||||
|         mio:MIO = MIO.objects.select_for_update().get(id=mio.id) |  | ||||||
|         if mio.submit_time is not None: |  | ||||||
|             raise ParseError("该记录已提交无法更改") |  | ||||||
|         return mio |  | ||||||
|      |  | ||||||
|     def add_info_for_list(self, data): |     def add_info_for_list(self, data): | ||||||
|         # 获取检验状态 |         # 获取检验状态 | ||||||
|         mio_dict = {} |         mio_dict = {} | ||||||
|  | @ -181,7 +170,7 @@ class MIOViewSet(CustomModelViewSet): | ||||||
|         if self.action in ['create', 'update', 'partial_update']: |         if self.action in ['create', 'update', 'partial_update']: | ||||||
|             type = self.request.data.get('type') |             type = self.request.data.get('type') | ||||||
|             user = self.request.user |             user = self.request.user | ||||||
|             if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT, MIO.MIO_TYPE_BORROW_OUT, MIO.MIO_TYPE_RETURN_IN]: |             if type in [MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_DO_OUT]: | ||||||
|                 if not has_perm(user, ['mio.do']): |                 if not has_perm(user, ['mio.do']): | ||||||
|                     raise PermissionDenied |                     raise PermissionDenied | ||||||
|                 return MIODoSerializer |                 return MIODoSerializer | ||||||
|  | @ -193,7 +182,7 @@ class MIOViewSet(CustomModelViewSet): | ||||||
|                 if not has_perm(user, ['mio.sale']): |                 if not has_perm(user, ['mio.sale']): | ||||||
|                     raise PermissionDenied |                     raise PermissionDenied | ||||||
|                 return MIOSaleSerializer |                 return MIOSaleSerializer | ||||||
|             elif type in [MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_PUR_OUT]: |             elif type == MIO.MIO_TYPE_PUR_IN: | ||||||
|                 if not has_perm(user, ['mio.pur']): |                 if not has_perm(user, ['mio.pur']): | ||||||
|                     raise PermissionDenied |                     raise PermissionDenied | ||||||
|                 return MIOPurSerializer |                 return MIOPurSerializer | ||||||
|  | @ -205,29 +194,27 @@ class MIOViewSet(CustomModelViewSet): | ||||||
|         return super().perform_destroy(instance) |         return super().perform_destroy(instance) | ||||||
| 
 | 
 | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) |     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) | ||||||
|     @transaction.atomic |  | ||||||
|     def submit(self, request, *args, **kwargs): |     def submit(self, request, *args, **kwargs): | ||||||
|         """提交 |         """提交 | ||||||
| 
 | 
 | ||||||
|         提交 |         提交 | ||||||
|         """ |         """ | ||||||
|         ins:MIO = self.get_object() |         ins = self.get_object() | ||||||
|         if ins.inout_date is None: |         if ins.inout_date is None: | ||||||
|             raise ParseError('出入库日期未填写') |             raise ParseError('出入库日期未填写') | ||||||
|         if ins.state != MIO.MIO_CREATE: |         if ins.state != MIO.MIO_CREATE: | ||||||
|             raise ParseError('记录状态异常') |             raise ParseError('记录状态异常') | ||||||
|         now = timezone.now() |         with transaction.atomic(): | ||||||
|         ins.submit_user = request.user |             ins.submit_time = timezone.now() | ||||||
|         ins.submit_time = now |             ins.state = MIO.MIO_SUBMITED | ||||||
|         ins.update_by = request.user |             ins.submit_user = request.user | ||||||
|         ins.state = MIO.MIO_SUBMITED |             ins.update_by = request.user | ||||||
|         ins.save() |             ins.save() | ||||||
|         InmService.update_inm(ins) |             InmService.update_inm(ins) | ||||||
|         InmService.update_material_count(ins) |         InmService.update_material_count(ins) | ||||||
|         return Response(MIOListSerializer(instance=ins).data) |         return Response(MIOListSerializer(instance=ins).data) | ||||||
| 
 | 
 | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) |     @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) | ||||||
|     @transaction.atomic |  | ||||||
|     def revert(self, request, *args, **kwargs): |     def revert(self, request, *args, **kwargs): | ||||||
|         """撤回 |         """撤回 | ||||||
| 
 | 
 | ||||||
|  | @ -239,84 +226,16 @@ class MIOViewSet(CustomModelViewSet): | ||||||
|             raise ParseError('记录状态异常') |             raise ParseError('记录状态异常') | ||||||
|         if ins.submit_user != user: |         if ins.submit_user != user: | ||||||
|             raise ParseError('非提交人不可撤回') |             raise ParseError('非提交人不可撤回') | ||||||
|         ins.submit_user = None |         with transaction.atomic(): | ||||||
|         ins.update_by = user |             ins.submit_time = None | ||||||
|         ins.state = MIO.MIO_CREATE |             ins.state = MIO.MIO_CREATE | ||||||
|         ins.submit_time = None |             ins.update_by = user | ||||||
|         ins.save() |             ins.save() | ||||||
|         InmService.update_inm(ins, is_reverse=True) |             InmService.update_inm(ins, is_reverse=True) | ||||||
|         InmService.update_material_count(ins) |         InmService.update_material_count(ins) | ||||||
|         return Response() |         return Response() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer) |  | ||||||
|     @transaction.atomic |  | ||||||
|     def pack_mioitem(self, request, *args, **kwargs): |  | ||||||
|         """装箱 |  | ||||||
| 
 |  | ||||||
|         装箱 |  | ||||||
|         """ |  | ||||||
|         mio:MIO = self.get_object() |  | ||||||
|         if mio.submit_time is not None: |  | ||||||
|             raise ParseError('该出入库已提交不可装箱') |  | ||||||
|         sr = PackMioSerializer(data=request.data) |  | ||||||
|         sr.is_valid(raise_exception=True) |  | ||||||
|         vdata = sr.validated_data |  | ||||||
|         pack_index = vdata["pack_index"] |  | ||||||
|         mioitems = vdata["mioitems"] |  | ||||||
|         if not mioitems: |  | ||||||
|             raise ParseError('未选择明细') |  | ||||||
|         for id in mioitems: |  | ||||||
|             mioitem = MIOItem.objects.get(id=id) |  | ||||||
|             if mioitem.mio != mio: |  | ||||||
|                 raise ParseError('存在明细不属于该箱') |  | ||||||
|             mioitem.pack_index = pack_index |  | ||||||
|             mioitem.save(update_fields=['pack_index', 'update_time']) |  | ||||||
|         return Response() |  | ||||||
| 
 |  | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer) |  | ||||||
|     def daoru_mioitem(self, request, *args, **kwargs): |  | ||||||
|         """导入明细 |  | ||||||
| 
 |  | ||||||
|         导入明细 |  | ||||||
|         """ |  | ||||||
|         daoru_mioitems(settings.BASE_DIR + request.data.get('path', ''), mio=self.get_object()) |  | ||||||
|         return Response() |  | ||||||
| class PackViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): |  | ||||||
|     """ |  | ||||||
|     list: 装箱记录 |  | ||||||
| 
 |  | ||||||
|     装箱记录 |  | ||||||
|     """ |  | ||||||
|     perms_map = {'get': '*', 'post': '*', 'delete': '*'} |  | ||||||
|     queryset = Pack.objects.all() |  | ||||||
|     serializer_class = PackSerializer |  | ||||||
|     filterset_fields = ["mio"] |  | ||||||
|     ordering = ["mio", "index"] |  | ||||||
| 
 |  | ||||||
|     @action(methods=['post'], detail=False, perms_map={'post': 'mio.update'}, serializer_class=PackMioSerializer) |  | ||||||
|     @transaction.atomic |  | ||||||
|     def pack_mioitem(self, request, *args, **kwargs): |  | ||||||
|         """装箱 |  | ||||||
| 
 |  | ||||||
|         装箱 |  | ||||||
|         """ |  | ||||||
|         vdata = PackMioSerializer(data=request.data) |  | ||||||
|         packId = vdata["pack"] |  | ||||||
|         pack:Pack = Pack.objects.get(id=packId) |  | ||||||
|         mioitems = vdata["mioitems"] |  | ||||||
|         if not mioitems: |  | ||||||
|             raise ParseError('未选择明细') |  | ||||||
|         for id in mioitems: |  | ||||||
|             mioitem = MIOItem.objects.get(id=id) |  | ||||||
|             if mioitem.mio != pack.mio: |  | ||||||
|                 raise ParseError('存在明细不属于该装箱记录') |  | ||||||
|             mioitem.pack = pack |  | ||||||
|             mioitem.save(update_fields=['pack', 'update_time']) |  | ||||||
|         return Response() |  | ||||||
|              |  | ||||||
|      |  | ||||||
| 
 |  | ||||||
| class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): | class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): | ||||||
|     """ |     """ | ||||||
|     list: 出入库明细 |     list: 出入库明细 | ||||||
|  | @ -341,7 +260,6 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode | ||||||
|     } |     } | ||||||
|     ordering = ['create_time'] |     ordering = ['create_time'] | ||||||
|     ordering_fields = ['create_time', 'test_date'] |     ordering_fields = ['create_time', 'test_date'] | ||||||
|     search_fields =['batch', 'a_mioitem__batch'] |  | ||||||
| 
 | 
 | ||||||
|     def add_info_for_list(self, data): |     def add_info_for_list(self, data): | ||||||
|         with_mio = self.request.query_params.get('with_mio', "no") |         with_mio = self.request.query_params.get('with_mio', "no") | ||||||
|  | @ -366,28 +284,13 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode | ||||||
|     def list(self, request, *args, **kwargs): |     def list(self, request, *args, **kwargs): | ||||||
|         return super().list(request, *args, **kwargs) |         return super().list(request, *args, **kwargs) | ||||||
| 
 | 
 | ||||||
|     def perform_create(self, serializer): |  | ||||||
|         serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio']) |  | ||||||
|         return super().perform_create(serializer) |  | ||||||
| 
 |  | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         MIOViewSet.lock_and_check_can_update(instance.mio) |         if instance.mio.state != MIO.MIO_CREATE: | ||||||
|  |             raise ParseError('出入库记录非创建中不可删除') | ||||||
|         if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user: |         if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user: | ||||||
|             raise PermissionDenied('无权限删除') |             raise PermissionDenied('无权限删除') | ||||||
|         return super().perform_destroy(instance) |         return super().perform_destroy(instance) | ||||||
| 
 | 
 | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer) |  | ||||||
|     @transaction.atomic |  | ||||||
|     def revert_and_del(self, request, *args, **kwargs): |  | ||||||
|         """撤回并删除 |  | ||||||
| 
 |  | ||||||
|         撤回并删除 |  | ||||||
|         """ |  | ||||||
|         ins:MIOItem = self.get_object() |  | ||||||
|         InmService.revert_and_del(ins) |  | ||||||
|         return Response() |  | ||||||
|          |  | ||||||
| 
 |  | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer) |     @action(methods=['post'], detail=True, perms_map={'post': 'mioitem.test'}, serializer_class=MIOItemTestSerializer) | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     def test(self, request, *args, **kwargs): |     def test(self, request, *args, **kwargs): | ||||||
|  | @ -487,7 +390,7 @@ class MIOItemwViewSet(CustomModelViewSet): | ||||||
|     perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'} |     perms_map = {'get': '*', 'post': 'mio.update', 'put': 'mio.update', 'delete': 'mio.update'} | ||||||
|     queryset = MIOItemw.objects.all() |     queryset = MIOItemw.objects.all() | ||||||
|     serializer_class = MIOItemwCreateUpdateSerializer |     serializer_class = MIOItemwCreateUpdateSerializer | ||||||
|     filterset_fields = ['mioitem', 'wpr'] |     filterset_fields = ['mioitem'] | ||||||
|     ordering = ["number", "create_time"] |     ordering = ["number", "create_time"] | ||||||
|     ordering_fields = ["number", "create_time"] |     ordering_fields = ["number", "create_time"] | ||||||
| 
 | 
 | ||||||
|  | @ -503,20 +406,20 @@ class MIOItemwViewSet(CustomModelViewSet): | ||||||
|         mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count() |         mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count() | ||||||
|         mioitem.save() |         mioitem.save() | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio) |         ins: MIOItemw = serializer.save() | ||||||
|         ins:MIOItemw = serializer.save() |         mioitem: MIOItem = ins.mioitem | ||||||
|         self.cal_mioitem_count(ins.mioitem) |         self.cal_mioitem_count(mioitem) | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_update(self, serializer): |     def perform_update(self, serializer): | ||||||
|         ins:MIOItemw = serializer.instance |         mioitemw = serializer.save() | ||||||
|         MIOViewSet.lock_and_check_can_update(ins.mioitem.mio) |         self.cal_mioitem_count(mioitemw.mioitem) | ||||||
|         ins:MIOItemw = serializer.save() |  | ||||||
|         self.cal_mioitem_count(ins.mioitem) |  | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance: MIOItemw): |     def perform_destroy(self, instance: MIOItemw): | ||||||
|         mioitem = instance.mioitem |         mioitem = instance.mioitem | ||||||
|         MIOViewSet.lock_and_check_can_update(mioitem.mio) |  | ||||||
|         ftest = instance.ftest |         ftest = instance.ftest | ||||||
|         instance.delete() |         instance.delete() | ||||||
|         if ftest: |         if ftest: | ||||||
|  |  | ||||||
|  | @ -7,7 +7,6 @@ from celery import shared_task | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| import os | import os | ||||||
| from apps.utils.sql import execute_raw_sql |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @shared_task(base=CustomTask) | @shared_task(base=CustomTask) | ||||||
|  | @ -34,44 +33,3 @@ def clear_dbbackup(num: int=7): | ||||||
|     for f in files_remove_list: |     for f in files_remove_list: | ||||||
|         filepath = os.path.join(backpath, f) |         filepath = os.path.join(backpath, f) | ||||||
|         os.remove(filepath) |         os.remove(filepath) | ||||||
| 
 |  | ||||||
| @shared_task(base=CustomTask) |  | ||||||
| def clean_timescaledb_job_his(num: int = 30): |  | ||||||
|     execute_raw_sql(f""" |  | ||||||
|         DO $$ |  | ||||||
|         DECLARE |  | ||||||
|             batch_size INTEGER := 100000; -- 每次删除的行数 |  | ||||||
|             deleted_count INTEGER := 0; |  | ||||||
|         BEGIN |  | ||||||
|             IF EXISTS ( |  | ||||||
|                 SELECT 1 |  | ||||||
|                 FROM pg_class c |  | ||||||
|                 JOIN pg_namespace n ON n.oid = c.relnamespace |  | ||||||
|                 WHERE n.nspname = '_timescaledb_internal' |  | ||||||
|                 AND c.relname = 'bgw_job_stat_history' |  | ||||||
|             ) THEN |  | ||||||
|                 RAISE NOTICE 'Start cleaning _timescaledb_internal.bgw_job_stat_history ...'; |  | ||||||
| 
 |  | ||||||
|                 LOOP |  | ||||||
|                     WITH del AS ( |  | ||||||
|                     SELECT ctid |  | ||||||
|                     FROM _timescaledb_internal.bgw_job_stat_history |  | ||||||
|                     WHERE execution_start < NOW() - INTERVAL '{num} days' |  | ||||||
|                     LIMIT batch_size |  | ||||||
|                     ) |  | ||||||
|                     DELETE FROM _timescaledb_internal.bgw_job_stat_history |  | ||||||
|                     WHERE ctid IN (SELECT ctid FROM del); |  | ||||||
|              |  | ||||||
|                     GET DIAGNOSTICS deleted_count = ROW_COUNT; |  | ||||||
|                     RAISE NOTICE 'Deleted % rows...', deleted_count; |  | ||||||
|                     EXIT WHEN deleted_count = 0; |  | ||||||
| 
 |  | ||||||
|                      |  | ||||||
|                     PERFORM pg_sleep(0.1);  -- 稍微休息,避免压力过大 |  | ||||||
|                 END LOOP; |  | ||||||
|                 RAISE NOTICE '✅ Data cleanup complete. Run VACUUM FULL manually.'; |  | ||||||
|             ELSE |  | ||||||
|                 RAISE NOTICE 'Table _timescaledb_internal.bgw_job_stat_history not found.'; |  | ||||||
|             END IF; |  | ||||||
|         END$$; |  | ||||||
|     """, timeout=None) |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| from django_filters import rest_framework as filters | from django_filters import rest_framework as filters | ||||||
| from apps.mtm.models import Goal, Material, Route, RoutePack | from apps.mtm.models import Goal, Material, Route | ||||||
| from django.db.models.expressions import F | from django.db.models.expressions import F | ||||||
| from rest_framework.exceptions import ParseError |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MaterialFilter(filters.FilterSet): | class MaterialFilter(filters.FilterSet): | ||||||
|  | @ -46,8 +45,6 @@ class GoalFilter(filters.FilterSet): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class RouteFilter(filters.FilterSet): | class RouteFilter(filters.FilterSet): | ||||||
|     nprocess_name = filters.CharFilter(method='filter_nprocess_name', label="nprocess_name") |  | ||||||
|     material_in_has = filters.CharFilter(method='filter_material_in_has', label="material_in_has ID") |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = Route |         model = Route | ||||||
|         fields = {  |         fields = {  | ||||||
|  | @ -61,18 +58,5 @@ class RouteFilter(filters.FilterSet): | ||||||
|             "mgroup": ["exact", "in", "isnull"], |             "mgroup": ["exact", "in", "isnull"], | ||||||
|             "mgroup__name": ["exact", "contains"], |             "mgroup__name": ["exact", "contains"], | ||||||
|             "mgroup__belong_dept": ["exact"], |             "mgroup__belong_dept": ["exact"], | ||||||
|             "mgroup__belong_dept__name": ["exact", "contains"], |             "mgroup__belong_dept__name": ["exact", "contains"] | ||||||
|             "from_route": ["exact", "isnull"], |  | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|     def filter_nprocess_name(self, queryset, name, value): |  | ||||||
|         return queryset |  | ||||||
|      |  | ||||||
| 
 |  | ||||||
|     def filter_material_in_has(self, queryset, name, value): |  | ||||||
|         nprocess_name = self.data.get('nprocess_name', None) |  | ||||||
|         if nprocess_name: |  | ||||||
|             routepack_qs = queryset.filter(material_in__id=value, routepack__isnull=False, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True) |  | ||||||
|             qs = queryset.filter(routepack__in=routepack_qs, process__name=nprocess_name) |  | ||||||
|             return qs |  | ||||||
|         raise ParseError("nprocess_name is required") |  | ||||||
|  | @ -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 apps.utils.models import CommonBDModel | ||||||
| from collections import defaultdict, deque | from collections import defaultdict, deque | ||||||
| from django.db.models import Q | from django.db.models import Q | ||||||
| from datetime import datetime, timedelta |  | ||||||
| from django.utils import timezone |  | ||||||
| 
 | 
 | ||||||
| class Process(CommonBModel): | class Process(CommonBModel): | ||||||
|     """ |     """ | ||||||
|  | @ -45,12 +43,6 @@ class Process(CommonBModel): | ||||||
|         """ |         """ | ||||||
|         return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct()) |         return list(Route.objects.filter(process=self).values_list("material_out__id", flat=True).distinct()) | ||||||
| 
 | 
 | ||||||
|     def get_canin_mat_ids(self): |  | ||||||
|         """获取可输入的materialIds |  | ||||||
|         """ |  | ||||||
|         return list(RouteMat.objects.filter(route__process=self).values_list("material__id", flat=True).distinct()) + \ |  | ||||||
|                 list(Route.objects.filter(process=self).values_list("material_in__id", flat=True).distinct()) |  | ||||||
| 
 |  | ||||||
| # Create your models here. | # Create your models here. | ||||||
| class Material(CommonAModel): | class Material(CommonAModel): | ||||||
|     """TN:物料""" |     """TN:物料""" | ||||||
|  | @ -110,7 +102,6 @@ class Material(CommonAModel): | ||||||
|     unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True) |     unit_price = models.DecimalField('单价', max_digits=14, decimal_places=2, null=True, blank=True) | ||||||
|     into_wm = models.BooleanField('是否进入车间库存', default=True) |     into_wm = models.BooleanField('是否进入车间库存', default=True) | ||||||
|     bin_number_main = models.CharField('主库位号', max_length=50, null=True, blank=True) |     bin_number_main = models.CharField('主库位号', max_length=50, null=True, blank=True) | ||||||
|     img = models.TextField('图片', null=True, blank=True) |  | ||||||
| 
 | 
 | ||||||
|     class Meta: |     class Meta: | ||||||
|         verbose_name = '物料表' |         verbose_name = '物料表' | ||||||
|  | @ -180,32 +171,6 @@ class Mgroup(CommonBModel): | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         return self.name |         return self.name | ||||||
| 
 | 
 | ||||||
|     def get_shift(self, w_s_time:datetime): |  | ||||||
|         # 如果没有时区信息,使用默认时区(东八区) |  | ||||||
|         if not timezone.is_aware(w_s_time): |  | ||||||
|             w_s_time = timezone.make_aware(w_s_time) |  | ||||||
|         else: |  | ||||||
|             w_s_time = timezone.localtime(w_s_time) |  | ||||||
| 
 |  | ||||||
|         shifts = Shift.objects.filter(rule=self.shift_rule).order_by('sort') |  | ||||||
|         if not shifts: |  | ||||||
|             raise ParseError(f"工段{self.name}未配置班次") |  | ||||||
|         # 处理跨天班次的情况 |  | ||||||
|         for shift in shifts: |  | ||||||
|             # 如果开始时间小于结束时间,表示班次在同一天内 |  | ||||||
|             if shift.start_time_o < shift.end_time_o: |  | ||||||
|                 if shift.start_time_o <= w_s_time.time() < shift.end_time_o: |  | ||||||
|                     return w_s_time.date(), shift |  | ||||||
|             else:  # 班次跨天(如夜班从当天晚上到次日凌晨) |  | ||||||
|                 if w_s_time.time() >= shift.start_time_o or w_s_time.time() < shift.end_time_o: |  | ||||||
|                     # 如果当前时间在开始时间之后,属于当天 |  | ||||||
|                     if w_s_time.time() >= shift.start_time_o: |  | ||||||
|                         return w_s_time.date(), shift |  | ||||||
|                     # 如果当前时间在结束时间之前,属于前一天 |  | ||||||
|                     else: |  | ||||||
|                         return (w_s_time - timedelta(days=1)).date(), shift |  | ||||||
|         # return w_s_time.date(), None |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class TeamMember(BaseModel): | class TeamMember(BaseModel): | ||||||
|     team = models.ForeignKey(Team, verbose_name='关联班组', |     team = models.ForeignKey(Team, verbose_name='关联班组', | ||||||
|  | @ -360,8 +325,7 @@ class RoutePack(CommonADModel): | ||||||
|                 route_dict[r.id] = { |                 route_dict[r.id] = { | ||||||
|                     "label": r.process.name if r.process else "", |                     "label": r.process.name if r.process else "", | ||||||
|                     "source": r.material_in.id, |                     "source": r.material_in.id, | ||||||
|                     "target": r.material_out.id, |                     "target": r.material_out.id | ||||||
|                     "id": r.id |  | ||||||
|                 } |                 } | ||||||
|          |          | ||||||
|         # 获取所有物料信息 |         # 获取所有物料信息 | ||||||
|  | @ -417,9 +381,7 @@ class Route(CommonADModel): | ||||||
|     batch_bind = models.BooleanField('是否绑定批次', default=True) |     batch_bind = models.BooleanField('是否绑定批次', default=True) | ||||||
|     materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials", |     materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials", | ||||||
|                                   through="mtm.routemat", blank=True) |                                   through="mtm.routemat", blank=True) | ||||||
|     parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent") |     parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True) | ||||||
|     params_json = models.JSONField('工艺参数', default=dict, blank=True) |  | ||||||
|     from_route = models.ForeignKey('self', verbose_name='来源路线', on_delete=models.SET_NULL, null=True, blank=True, related_name="route_f") |  | ||||||
| 
 | 
 | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         x = "" |         x = "" | ||||||
|  | @ -549,7 +511,6 @@ class Route(CommonADModel): | ||||||
|                     'source': source, |                     'source': source, | ||||||
|                     'target': target, |                     'target': target, | ||||||
|                     'label': rq.process.name,  |                     'label': rq.process.name,  | ||||||
|                     'id': rq.id |  | ||||||
|                 }) |                 }) | ||||||
|         # 将批次号排序 |         # 将批次号排序 | ||||||
|         nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time") |         nodes_qs = Material.objects.filter(id__in=nodes_set).order_by("process__sort", "create_time") | ||||||
|  |  | ||||||
|  | @ -246,34 +246,30 @@ class RouteSerializer(CustomModelSerializer): | ||||||
|             # material = validated_data.get('material', None) |             # material = validated_data.get('material', None) | ||||||
|             # if material and process and Route.objects.filter(material=material, process=process).exists(): |             # if material and process and Route.objects.filter(material=material, process=process).exists(): | ||||||
|             #     raise ValidationError('已选择该工序!!') |             #     raise ValidationError('已选择该工序!!') | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance:Route = super().create(validated_data) |             instance = super().create(validated_data) | ||||||
|         material_out = instance.material_out |             material_out = instance.material_out | ||||||
|         if material_out: |             if material_out: | ||||||
|             if material_out.process is None: |                 if material_out.process is None: | ||||||
|                 material_out.process = process |                     material_out.process = process | ||||||
|                 if material_out_tracking != material_out.tracking: |                     if material_out_tracking != material_out.tracking: | ||||||
|                     raise ParseError("物料跟踪类型不一致!请前往物料处修改") |                         raise ParseError("物料跟踪类型不一致!请前往物料处修改") | ||||||
|                 if material_out.parent is None and instance.material: |                     if material_out.parent is None and instance.material: | ||||||
|                     material_out.parent = instance.material |                         material_out.parent = instance.material | ||||||
|                 material_out.save() |                     material_out.save() | ||||||
|             # elif material_out.process != process: |                 # elif material_out.process != process: | ||||||
|             #     raise ParseError('物料工序错误!请重新选择') |                 #     raise ParseError('物料工序错误!请重新选择') | ||||||
|         else: |             else: | ||||||
|             if instance.material: |                 if instance.material: | ||||||
|                 instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) |                     instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) | ||||||
|                 instance.save() |                     instance.save() | ||||||
|         rx = Route.objects.filter( |             rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first() | ||||||
|             material_in=instance.material_in, material_out=instance.material_out,  |             if rx: | ||||||
|             process=process).exclude(id=instance.id).order_by("create_time").first() |                 msg = "" | ||||||
|         if rx: |                 if rx.routepack: | ||||||
|             instance.from_route = rx |                     msg = rx.routepack.name | ||||||
|             instance.save() |                 raise ParseError(f"该工艺步骤已存在-{msg}") | ||||||
|             # msg = "" |             return instance | ||||||
|             # if rx.routepack: |  | ||||||
|             #     msg = rx.routepack.name |  | ||||||
|             # raise ParseError(f"该工艺步骤已存在-{msg}") |  | ||||||
|         return instance |  | ||||||
| 
 | 
 | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         validated_data.pop('material', None) |         validated_data.pop('material', None) | ||||||
|  | @ -281,34 +277,30 @@ class RouteSerializer(CustomModelSerializer): | ||||||
|         material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH) |         material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH) | ||||||
|         if material_out_tracking is None: |         if material_out_tracking is None: | ||||||
|             material_out_tracking = Material.MA_TRACKING_BATCH |             material_out_tracking = Material.MA_TRACKING_BATCH | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance = super().update(instance, validated_data) |             instance = super().update(instance, validated_data) | ||||||
|         material_out = instance.material_out |             material_out = instance.material_out | ||||||
|         if material_out: |             if material_out: | ||||||
|             if material_out.process is None: |                 if material_out.process is None: | ||||||
|                 material_out.process = process |                     material_out.process = process | ||||||
|                 if material_out_tracking != material_out.tracking: |                     if material_out_tracking != material_out.tracking: | ||||||
|                     raise ParseError("物料跟踪类型不一致!请前往物料处修改") |                         raise ParseError("物料跟踪类型不一致!请前往物料处修改") | ||||||
|                 if material_out.parent is None and instance.material: |                     if material_out.parent is None and instance.material: | ||||||
|                     material_out.parent = instance.material |                         material_out.parent = instance.material | ||||||
|                 material_out.save() |                     material_out.save() | ||||||
|             # elif material_out.process != process: |                 # elif material_out.process != process: | ||||||
|             #     raise ParseError('物料工序错误!请重新选择') |                 #     raise ParseError('物料工序错误!请重新选择') | ||||||
|         else: |             else: | ||||||
|             if instance.material: |                 if instance.material: | ||||||
|                 instance.material_out = RouteSerializer.gen_material_out(instance,  material_out_tracking, user=self.request.user) |                     instance.material_out = RouteSerializer.gen_material_out(instance,  material_out_tracking, user=self.request.user) | ||||||
|                 instance.save() |                     instance.save() | ||||||
|         rx = Route.objects.filter( |             rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first() | ||||||
|             material_in=instance.material_in, material_out=instance.material_out,  |             if rx: | ||||||
|             process=process).exclude(id=instance.id).order_by("create_time").first() |                 msg = "" | ||||||
|         if rx: |                 if rx.routepack: | ||||||
|             instance.from_route = rx |                     msg = rx.routepack.name | ||||||
|             instance.save() |                 raise ParseError(f"该工艺步骤已存在-{msg}") | ||||||
|             # msg = "" |             return instance | ||||||
|             # if rx.routepack: |  | ||||||
|             #     msg = rx.routepack.name |  | ||||||
|             # raise ParseError(f"该工艺步骤已存在-{msg}") |  | ||||||
|         return instance |  | ||||||
| 
 | 
 | ||||||
|     def to_representation(self, instance): |     def to_representation(self, instance): | ||||||
|         res = super().to_representation(instance) |         res = super().to_representation(instance) | ||||||
|  | @ -338,15 +330,3 @@ class RouteMatSerializer(CustomModelSerializer): | ||||||
|         model = RouteMat |         model = RouteMat | ||||||
|         fields = "__all__" |         fields = "__all__" | ||||||
|         read_only_fields = EXCLUDE_FIELDS_BASE |         read_only_fields = EXCLUDE_FIELDS_BASE | ||||||
|      |  | ||||||
|     def validate(self, attrs): |  | ||||||
|         route:Route = attrs["route"] |  | ||||||
|         if route.from_route is not None: |  | ||||||
|             raise ParseError("该工艺步骤引用其他步骤,无法修改") |  | ||||||
|         return attrs |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class MaterialExportSerializer(CustomModelSerializer): |  | ||||||
|     class Meta: |  | ||||||
|         model = Material |  | ||||||
|         fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"] |  | ||||||
|  | @ -51,35 +51,33 @@ def daoru_material(path: str): | ||||||
|                  '辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70} |                  '辅助材料': 40, '加工工具': 50, '辅助工装': 60, '办公用品': 70} | ||||||
|     from apps.utils.snowflake import idWorker |     from apps.utils.snowflake import idWorker | ||||||
|     from openpyxl import load_workbook |     from openpyxl import load_workbook | ||||||
|     wb = load_workbook(path, read_only=True) |     wb = load_workbook(path) | ||||||
|     sheet = wb.active |     sheet = wb['物料'] | ||||||
|     process_l = Process.objects.all() |     process_l = Process.objects.all() | ||||||
|     process_d = {p.name: p for p in process_l} |     process_d = {p.name: p for p in process_l} | ||||||
|     i = 3 |     i = 3 | ||||||
|     if sheet['a2'].value != '物料编号': |     if sheet['a2'].value != '物料编号': | ||||||
|         raise ParseError('列错误导入失败') |         raise ParseError('列错误导入失败') | ||||||
|     while sheet[f'b{i}'].value is not None or sheet[f'd{i}'].value is not None: |     while sheet[f'b{i}'].value is not None: | ||||||
|         type_str = sheet[f'b{i}'].value.replace(' ', '') |         type_str = sheet[f'b{i}'].value.replace(' ', '') | ||||||
|         try: |         try: | ||||||
|             type = type_dict[type_str] |             type = type_dict[type_str] | ||||||
|             cate = sheet[f'c{i}'].value.replace(' ', '') if sheet[f'c{i}'].value else "" |  | ||||||
|             number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None |             number = str(sheet[f'a{i}'].value).replace(' ', '') if sheet[f'a{i}'].value else None | ||||||
|             if sheet[f'd{i}'].value: |             if sheet[f'c{i}'].value: | ||||||
|                 name = str(sheet[f'd{i}'].value).replace(' ', '') |                 name = str(sheet[f'c{i}'].value).replace(' ', '') | ||||||
|             else: |             else: | ||||||
|                 raise ParseError(f'{i}行物料信息错误: 物料名称必填') |                 raise ParseError(f'{i}行物料信息错误: 物料名称必填') | ||||||
|             specification = str(sheet[f'e{i}'].value).replace( |             specification = str(sheet[f'd{i}'].value).replace( | ||||||
|                 '×', '*').replace(' ', '') if sheet[f'e{i}'].value else None |                 '×', '*').replace(' ', '') if sheet[f'd{i}'].value else None | ||||||
|             model = str(sheet[f'f{i}'].value).replace(' ', '') if sheet[f'f{i}'].value else None |             model = str(sheet[f'e{i}'].value).replace(' ', '') if sheet[f'e{i}'].value else None | ||||||
|             unit = sheet[f'g{i}'].value.replace(' ', '') |             unit = sheet[f'f{i}'].value.replace(' ', '') | ||||||
|             count_safe = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None |             count_safe = float(sheet[f'h{i}'].value) if sheet[f'h{i}'].value else None | ||||||
|             unit_price = float(sheet[f'j{i}'].value) if sheet[f'j{i}'].value else None |             unit_price = float(sheet[f'i{i}'].value) if sheet[f'i{i}'].value else None | ||||||
|             bin_number_main = sheet[f'k{i}'].value.replace(' ', '') if sheet[f'k{i}'].value else None |  | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             raise ParseError(f'{i}行物料信息错误: {e}') |             raise ParseError(f'{i}行物料信息错误: {e}') | ||||||
|         if type in [20, 30]: |         if type in [20, 30]: | ||||||
|             try: |             try: | ||||||
|                 process = process_d[sheet[f'h{i}'].value.replace(' ', '')] |                 process = process_d[sheet[f'g{i}'].value.replace(' ', '')] | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 raise ParseError(f'{i}行物料信息错误: {e}') |                 raise ParseError(f'{i}行物料信息错误: {e}') | ||||||
|         try: |         try: | ||||||
|  | @ -89,7 +87,7 @@ def daoru_material(path: str): | ||||||
|                 filters['process'] = process |                 filters['process'] = process | ||||||
|             default = {'type': type, 'name': name, 'specification': specification, |             default = {'type': type, 'name': name, 'specification': specification, | ||||||
|                        'model': model, 'unit': unit, 'number': number if number else f'm{type}_{ranstr(6)}', 'id': idWorker.get_id(),  |                        'model': model, 'unit': unit, 'number': number if number else f'm{type}_{ranstr(6)}', 'id': idWorker.get_id(),  | ||||||
|                        'count_safe': count_safe, 'unit_price': unit_price, 'cate': cate, 'bin_number_main': bin_number_main} |                        'count_safe': count_safe, 'unit_price': unit_price} | ||||||
|             material, is_created = Material.objects.get_or_create( |             material, is_created = Material.objects.get_or_create( | ||||||
|                 **filters, defaults=default) |                 **filters, defaults=default) | ||||||
|             if not is_created: |             if not is_created: | ||||||
|  | @ -155,12 +153,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict): | ||||||
|         raise ParseError('缺少步骤') |         raise ParseError('缺少步骤') | ||||||
|     r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') |     r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') | ||||||
|     first_route = r_qs.first() |     first_route = r_qs.first() | ||||||
|  |     last_route = r_qs.last() | ||||||
|     if first_route.batch_bind: |     if first_route.batch_bind: | ||||||
|         first_route.batch_bind = False |         first_route.batch_bind = False | ||||||
|         first_route.save(update_fields=['batch_bind']) |         first_route.save(update_fields=['batch_bind']) | ||||||
|     # last_route = r_qs.last() |     if last_route.material_out != routepack.material: | ||||||
|     # if last_route.material_out != routepack.material: |         raise ParseError('最后一步产出与工艺包不一致') | ||||||
|     #     raise ParseError('最后一步产出与工艺包不一致') |  | ||||||
|     ticket_data = ticket.ticket_data |     ticket_data = ticket.ticket_data | ||||||
|     ticket_data.update({ |     ticket_data.update({ | ||||||
|         't_model': 'routepack', |         't_model': 'routepack', | ||||||
|  | @ -171,8 +169,8 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict): | ||||||
|     ticket.save() |     ticket.save() | ||||||
|     if routepack.ticket is None: |     if routepack.ticket is None: | ||||||
|         routepack.ticket = ticket |         routepack.ticket = ticket | ||||||
|     routepack.state = RoutePack.RP_S_AUDIT |         routepack.state = RoutePack.RP_S_AUDIT | ||||||
|     routepack.save() |         routepack.save() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def routepack_audit_end(ticket: Ticket): | def routepack_audit_end(ticket: Ticket): | ||||||
|  | @ -182,7 +180,7 @@ def routepack_audit_end(ticket: Ticket): | ||||||
| 
 | 
 | ||||||
| def routepack_ticket_change(ticket: Ticket): | def routepack_ticket_change(ticket: Ticket): | ||||||
|     routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id']) |     routepack = RoutePack.objects.get(id=ticket.ticket_data['t_id']) | ||||||
|     if ticket.act_state in [Ticket.TICKET_ACT_STATE_DRAFT, Ticket.TICKET_ACT_STATE_BACK, Ticket.TICKET_ACT_STATE_RETREAT]: |     if ticket.act_state == Ticket.TICKET_ACT_STATE_DRAFT: | ||||||
|         routepack.state = RoutePack.RP_S_CREATE |         routepack.state = RoutePack.RP_S_CREATE | ||||||
|         routepack.save() |         routepack.save() | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ from apps.mtm.serializers import (GoalSerializer, MaterialSerializer, | ||||||
|                                   MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer, |                                   MgroupGoalYearSerializer, MgroupSerializer, MgroupDaysSerializer, | ||||||
|                                   ShiftSerializer, TeamSerializer, ProcessSerializer,  |                                   ShiftSerializer, TeamSerializer, ProcessSerializer,  | ||||||
|                                   RouteSerializer, TeamMemberSerializer, RoutePackSerializer, |                                   RouteSerializer, TeamMemberSerializer, RoutePackSerializer, | ||||||
|                                   SruleSerializer, RouteMatSerializer, RoutePackCopySerializer, MaterialExportSerializer) |                                   SruleSerializer, RouteMatSerializer, RoutePackCopySerializer) | ||||||
| from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days | from apps.mtm.services import get_mgroup_goals, daoru_material, get_mgroup_days | ||||||
| from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | ||||||
| from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin | from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin | ||||||
|  | @ -21,8 +21,6 @@ from django.db.models import Q | ||||||
| from apps.wf.models import Ticket | from apps.wf.models import Ticket | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
| from rest_framework.permissions import IsAdminUser | from rest_framework.permissions import IsAdminUser | ||||||
| from apps.utils.export import export_excel |  | ||||||
| from operator import itemgetter |  | ||||||
| 
 | 
 | ||||||
| # Create your views here. | # Create your views here. | ||||||
| class MaterialViewSet(CustomModelViewSet): | class MaterialViewSet(CustomModelViewSet): | ||||||
|  | @ -34,13 +32,14 @@ class MaterialViewSet(CustomModelViewSet): | ||||||
|     queryset = Material.objects.all() |     queryset = Material.objects.all() | ||||||
|     serializer_class = MaterialSerializer |     serializer_class = MaterialSerializer | ||||||
|     filterset_class = MaterialFilter |     filterset_class = MaterialFilter | ||||||
|     search_fields = ['name', 'code', 'number', 'specification', 'model', 'bin_number_main'] |     search_fields = ['name', 'code', 'number', 'specification', 'model'] | ||||||
|     select_related_fields = ['process'] |     select_related_fields = ['process'] | ||||||
|     ordering = ['name', 'model', 'specification', |     ordering = ['name', 'model', 'specification', | ||||||
|                 'type', 'process', 'process__sort', 'sort', 'id', 'number'] |                 'type', 'process', 'process__sort', 'sort', 'id', 'number'] | ||||||
|     ordering_fields = ['name', 'model', 'specification', |     ordering_fields = ['name', 'model', 'specification', | ||||||
|                        'type', 'process', 'process__sort', 'sort', 'id', 'number', 'create_time'] |                        'type', 'process', 'process__sort', 'sort', 'id', 'number'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         from apps.inm.models import MaterialBatch |         from apps.inm.models import MaterialBatch | ||||||
|         if MaterialBatch.objects.filter(material=instance).exists(): |         if MaterialBatch.objects.filter(material=instance).exists(): | ||||||
|  | @ -90,23 +89,6 @@ class MaterialViewSet(CustomModelViewSet): | ||||||
|         res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct() |         res = Material.objects.exclude(cate='').exclude(cate=None).values_list('cate', flat=True).distinct() | ||||||
|         return Response(set(res)) |         return Response(set(res)) | ||||||
| 
 | 
 | ||||||
|     @action(methods=['get'], detail=False, perms_map={'get': '*'}) |  | ||||||
|     def export_excel(self, request, pk=None): |  | ||||||
|         """导出excel |  | ||||||
|         导出excel |  | ||||||
|         """ |  | ||||||
|         field_data = ['大类', '物料编号', '名称', '规格', '型号', '计量单位', '仓库位号', "安全库存", "单价"] |  | ||||||
|         queryset = self.filter_queryset(self.get_queryset()) |  | ||||||
|         if queryset.count() > 1000: |  | ||||||
|             raise ParseError('数据量超过1000,请筛选后导出') |  | ||||||
|         odata = MaterialExportSerializer(queryset, many=True).data |  | ||||||
|         # 处理数据 |  | ||||||
|         field_keys = ['cate', 'number', 'name', 'specification', 'model', 'unit',  |  | ||||||
|               'bin_number_main', 'count_safe', 'unit_price'] |  | ||||||
|         getter = itemgetter(*field_keys) |  | ||||||
|         data = [list(getter(item)) for item in odata] |  | ||||||
|         return Response({'path': export_excel(field_data, data, '物料清单')}) |  | ||||||
| 
 |  | ||||||
| class ShiftViewSet(ListModelMixin, CustomGenericViewSet): | class ShiftViewSet(ListModelMixin, CustomGenericViewSet): | ||||||
|     """ |     """ | ||||||
|     list:班次 |     list:班次 | ||||||
|  | @ -311,7 +293,7 @@ class RoutePackViewSet(CustomModelViewSet): | ||||||
|         return Response({"id": route_new.id}) |         return Response({"id": route_new.id}) | ||||||
|      |      | ||||||
|     @transaction.atomic |     @transaction.atomic | ||||||
|     @action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer) |     @action(methods=['post'], detail=True, permission_classes = [IsAdminUser], serializer_class=Serializer) | ||||||
|     def toggle_state(self, request, *args, **kwargs): |     def toggle_state(self, request, *args, **kwargs): | ||||||
|         """变更工艺路线状态 |         """变更工艺路线状态 | ||||||
| 
 | 
 | ||||||
|  | @ -367,22 +349,12 @@ class RouteViewSet(CustomModelViewSet): | ||||||
|     select_related_fields = ['material', |     select_related_fields = ['material', | ||||||
|                              'process', 'material_in', 'material_out', 'mgroup', 'routepack'] |                              'process', 'material_in', 'material_out', 'mgroup', 'routepack'] | ||||||
| 
 | 
 | ||||||
|     def perform_update(self, serializer): |     def update(self, request, *args, **kwargs): | ||||||
|         ins:Route = serializer.instance |         obj:Route = self.get_object() | ||||||
|         if ins.from_route is not None: |         routepack = obj.routepack | ||||||
|             raise ParseError('该工艺步骤引用其他步骤, 无法编辑') |  | ||||||
|         old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process |  | ||||||
|         routepack = ins.routepack |  | ||||||
|         if routepack and routepack.state != RoutePack.RP_S_CREATE: |         if routepack and routepack.state != RoutePack.RP_S_CREATE: | ||||||
|             raise ParseError('该工艺路线非创建中不可编辑') |             raise ParseError('该状态下不可编辑') | ||||||
|         ins_n:Route = serializer.save() |         return super().update(request, *args, **kwargs) | ||||||
|         if Route.objects.filter(from_route__id=ins.id).exists() and (ins_n.material_in != old_m_in or ins_n.material_out != old_m_out or ins_n.process != process): |  | ||||||
|             raise ParseError("该工艺步骤被其他步骤引用, 无法修改关键信息") |  | ||||||
| 
 |  | ||||||
|     def perform_destroy(self, instance:Route): |  | ||||||
|         if Route.objects.filter(from_route=instance).exists(): |  | ||||||
|             raise ParseError('该工艺步骤被其他步骤引用,无法删除') |  | ||||||
|         return super().perform_destroy(instance) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SruleViewSet(CustomModelViewSet): | class SruleViewSet(CustomModelViewSet): | ||||||
|  |  | ||||||
|  | @ -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 +1,25 @@ | ||||||
| from django.db import models, transaction | from django.db import models | ||||||
| from apps.utils.models import CommonADModel, BaseModel, CommonBDModel | from apps.utils.models import CommonADModel, BaseModel | ||||||
| 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. | # 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): | class Mroom(CommonADModel): | ||||||
|     """TN: 会议室基本信息""" |     """TN: 会议室基本信息""" | ||||||
|     name = models.CharField('会议室名称', max_length=50, unique=True) |     name = models.CharField('会议室名称', max_length=50, unique=True) | ||||||
|     location = models.CharField('位置', max_length=100) |     location = models.CharField('位置', max_length=100) | ||||||
|     capacity = models.PositiveIntegerField('容纳人数') |     capacity = models.PositiveIntegerField('容纳人数') | ||||||
| 
 | 
 | ||||||
| class MroomBooking(CommonBDModel): | class MroomBooking(CommonADModel): | ||||||
|     """TN: 会议室预定信息""" |     """TN: 会议室预定信息""" | ||||||
|     # belong_dept 是预定部门 |  | ||||||
|     title = models.CharField('会议主题', max_length=100) |     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): | class MroomSlot(BaseModel): | ||||||
|     """TN: 会议室时段""" |     """TN: 会议室时段""" | ||||||
|     mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m") |     mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE) | ||||||
|     booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b") |     booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE) | ||||||
|     mdate = models.DateField('会议日期', db_index=True) |     mdate = models.DateField('会议日期', db_index=True) | ||||||
|     slot = models.PositiveIntegerField('时段', help_text='0-47') |     slot = models.PositiveIntegerField('时段', help_text='0-47') | ||||||
|     is_inuse = models.BooleanField('是否占用', default=True) |  | ||||||
|      |      | ||||||
| 
 |     class Meta: | ||||||
| # class Seal(BaseModel): |         unique_together = ('mroom', 'mdate', 'slot') | ||||||
| #     """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,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 +1,4 @@ | ||||||
| from django.shortcuts import render | from django.shortcuts import render | ||||||
| from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet | from apps.utils.viewsets import CustomModelViewSet | ||||||
| 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"] |  | ||||||
|  |  | ||||||
|  | @ -31,6 +31,7 @@ class UtaskSerializer(CustomModelSerializer): | ||||||
|             "priority": {"required": False, "allow_null": True}, |             "priority": {"required": False, "allow_null": True}, | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         if not validated_data.get('number', None): |         if not validated_data.get('number', None): | ||||||
|             validated_data["number"] = Utask.get_a_number() |             validated_data["number"] = Utask.get_a_number() | ||||||
|  |  | ||||||
|  | @ -408,14 +408,11 @@ class PmService: | ||||||
|             mtask.submit_time = now |             mtask.submit_time = now | ||||||
|             mtask.submit_user = user |             mtask.submit_user = user | ||||||
|             mtask.save() |             mtask.save() | ||||||
|             utask = mtask.utask |  | ||||||
|             if utask: |  | ||||||
|                 cls.utask_submit(utask, raise_e=False) |  | ||||||
|         else: |         else: | ||||||
|             raise ParseError('该任务状态不可提交') |             raise ParseError('该任务状态不可提交') | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def utask_submit(cls, utask: Utask, raise_e=True): |     def utask_submit(cls, utask: Utask): | ||||||
|         """ |         """ | ||||||
|         生产大任务提交 |         生产大任务提交 | ||||||
|         """ |         """ | ||||||
|  | @ -423,5 +420,4 @@ class PmService: | ||||||
|             utask.state = Utask.UTASK_SUBMIT |             utask.state = Utask.UTASK_SUBMIT | ||||||
|             utask.save() |             utask.save() | ||||||
|         else: |         else: | ||||||
|             if raise_e: |             raise ParseError('存在子任务未提交') | ||||||
|                 raise ParseError('存在子任务未提交') |  | ||||||
|  | @ -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) |  | ||||||
|  | @ -43,6 +43,7 @@ class PuPlanItemSerializer(CustomModelSerializer): | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|         read_only_fields = EXCLUDE_FIELDS + ['pu_order'] |         read_only_fields = EXCLUDE_FIELDS + ['pu_order'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         pu_plan = validated_data['pu_plan'] |         pu_plan = validated_data['pu_plan'] | ||||||
|         if pu_plan.state != PuPlan.PUPLAN_CREATE: |         if pu_plan.state != PuPlan.PUPLAN_CREATE: | ||||||
|  | @ -64,6 +65,7 @@ class PuPlanItemSerializer(CustomModelSerializer): | ||||||
|             else: |             else: | ||||||
|                 validated_data['belong_dept'] = belong_dept |                 validated_data['belong_dept'] = belong_dept | ||||||
|      |      | ||||||
|  |     @transaction.atomic | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         validated_data.pop('pu_plan') |         validated_data.pop('pu_plan') | ||||||
|         pu_plan = instance.pu_plan |         pu_plan = instance.pu_plan | ||||||
|  | @ -112,6 +114,7 @@ class PuOrderItemSerializer(CustomModelSerializer): | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
|         read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count'] |         read_only_fields = EXCLUDE_FIELDS_BASE + ['delivered_count'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         pu_order = validated_data['pu_order'] |         pu_order = validated_data['pu_order'] | ||||||
|         material = validated_data['material'] |         material = validated_data['material'] | ||||||
|  | @ -123,6 +126,7 @@ class PuOrderItemSerializer(CustomModelSerializer): | ||||||
|         PumService.cal_pu_order_total_price(pu_order) |         PumService.cal_pu_order_total_price(pu_order) | ||||||
|         return ins |         return ins | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         validated_data.pop('material') |         validated_data.pop('material') | ||||||
|         validated_data.pop('pu_order') |         validated_data.pop('pu_order') | ||||||
|  |  | ||||||
|  | @ -2,24 +2,20 @@ from rest_framework.exceptions import ValidationError | ||||||
| from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder | from apps.pum.models import PuOrderItem, PuPlan, PuPlanItem, PuOrder | ||||||
| from django.db.models import F, Sum | from django.db.models import F, Sum | ||||||
| from apps.inm.models import MIO, MIOItem | from apps.inm.models import MIO, MIOItem | ||||||
| from rest_framework.exceptions import ParseError |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PumService: | class PumService: | ||||||
| 
 | 
 | ||||||
|     @staticmethod |  | ||||||
|     def cal_pu_order_total_price(puorder: PuOrder): |     def cal_pu_order_total_price(puorder: PuOrder): | ||||||
|         total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0 |         total_price = PuOrderItem.objects.filter(pu_order=puorder).aggregate(total=Sum("total_price"))["total"] or 0 | ||||||
|         puorder.total_price = total_price |         puorder.total_price = total_price | ||||||
|         puorder.save() |         puorder.save() | ||||||
|      |      | ||||||
|     @staticmethod |  | ||||||
|     def cal_pu_plan_total_price(puplan: PuPlan): |     def cal_pu_plan_total_price(puplan: PuPlan): | ||||||
|         total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0 |         total_price = PuPlanItem.objects.filter(pu_plan=puplan).aggregate(total=Sum("total_price"))["total"] or 0 | ||||||
|         puplan.total_price = total_price |         puplan.total_price = total_price | ||||||
|         puplan.save() |         puplan.save() | ||||||
| 
 | 
 | ||||||
|     @staticmethod |  | ||||||
|     def change_puplan_state_when_puorder_sumbit(puorder: PuOrder): |     def change_puplan_state_when_puorder_sumbit(puorder: PuOrder): | ||||||
|         puplanIds = PuPlanItem.objects.filter( |         puplanIds = PuPlanItem.objects.filter( | ||||||
|             pu_order=puorder).values_list('pu_plan', flat=True) |             pu_order=puorder).values_list('pu_plan', flat=True) | ||||||
|  | @ -33,45 +29,24 @@ class PumService: | ||||||
|             puplan.state = state |             puplan.state = state | ||||||
|             puplan.save() |             puplan.save() | ||||||
| 
 | 
 | ||||||
|     @staticmethod |     def mio_purin(mio: MIO, is_reverse: bool = False): | ||||||
|     def mio_pur(mio: MIO, is_reverse: bool = False, mioitem: MIOItem = None): |  | ||||||
|         """ |         """ | ||||||
|         采购入库成功后的操作 |         采购入库成功后的操作 | ||||||
|         """ |         """ | ||||||
|         pu_order = mio.pu_order |         pu_order = mio.pu_order | ||||||
|         if pu_order is None: |         if pu_order is None: | ||||||
|             return |             return | ||||||
|         if mioitem is None: |         for i in MIOItem.objects.filter(mio=mio): | ||||||
|             qs = MIOItem.objects.filter(mio=mio) |             pu_orderitem = PuOrderItem.objects.get( | ||||||
|         else: |                 material=i.material, pu_order=pu_order) | ||||||
|             qs = MIOItem.objects.filter(id=mioitem.id) |  | ||||||
|          |  | ||||||
| 
 |  | ||||||
|         if mio.type == MIO.MIO_TYPE_PUR_IN: |  | ||||||
|             if is_reverse: |             if is_reverse: | ||||||
|                 xtype = "out" |  | ||||||
|             else: |  | ||||||
|                 xtype = "in" |  | ||||||
|         elif mio.type == MIO.MIO_TYPE_PUR_OUT: |  | ||||||
|             if is_reverse: |  | ||||||
|                 xtype = "in" |  | ||||||
|             else: |  | ||||||
|                 xtype = "out" |  | ||||||
| 
 |  | ||||||
|         for i in qs: |  | ||||||
|             try: |  | ||||||
|                 pu_orderitem = PuOrderItem.objects.get( |  | ||||||
|                     material=i.material, pu_order=pu_order) |  | ||||||
|             except PuOrderItem.DoesNotExist: |  | ||||||
|                 raise ParseError(f'{str(i.material)}-采购订单中不存在该物料') |  | ||||||
|             if xtype == "out": |  | ||||||
|                 delivered_count = pu_orderitem.delivered_count - i.count |                 delivered_count = pu_orderitem.delivered_count - i.count | ||||||
|             else: |             else: | ||||||
|                 delivered_count = pu_orderitem.delivered_count + i.count |                 delivered_count = pu_orderitem.delivered_count + i.count | ||||||
|             if delivered_count > pu_orderitem.count: |             if delivered_count > pu_orderitem.count: | ||||||
|                 raise ValidationError(f'{str(i.material)}-超出采购订单所需数量') |                 raise ValidationError(f'{i.material.name}-超出采购订单所需数量') | ||||||
|             elif delivered_count < 0: |             elif delivered_count < 0: | ||||||
|                 raise ValidationError(f'{str(i.material)}-数量小于0') |                 raise ValidationError(f'{i.material.name}-数量小于0') | ||||||
|             pu_orderitem.delivered_count = delivered_count |             pu_orderitem.delivered_count = delivered_count | ||||||
|             pu_orderitem.save() |             pu_orderitem.save() | ||||||
|         pu_order_state = PuOrder.PUORDER_SHIP |         pu_order_state = PuOrder.PUORDER_SHIP | ||||||
|  | @ -94,4 +69,3 @@ class PumService: | ||||||
|             if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE: |             if len(states) == 1 and list(states)[0] == PuOrder.PUORDER_DONE: | ||||||
|                 puplan.state = PuPlan.PUPLAN_DONE |                 puplan.state = PuPlan.PUPLAN_DONE | ||||||
|                 puplan.save() |                 puplan.save() | ||||||
| 
 |  | ||||||
|  |  | ||||||
|  | @ -80,6 +80,7 @@ class PuPlanItemViewSet(CustomModelViewSet): | ||||||
|     ordering_fields = ['create_time', 'material', 'need_date', 'need_count'] |     ordering_fields = ['create_time', 'material', 'need_date', 'need_count'] | ||||||
|     ordering = ['create_time'] |     ordering = ['create_time'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         user = self.request.user |         user = self.request.user | ||||||
|         pu_plan = instance.pu_plan |         pu_plan = instance.pu_plan | ||||||
|  | @ -103,6 +104,7 @@ class PuOrderViewSet(CustomModelViewSet): | ||||||
|     search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model'] |     search_fields = ['number', 'supplier__name', 'item_puorder__material__name', 'item_puorder__material__specification', 'item_puorder__material__model'] | ||||||
|     select_related_fields = ['create_by', 'update_by', 'supplier'] |     select_related_fields = ['create_by', 'update_by', 'supplier'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         if instance.state != PuOrder.PUORDER_CREATE: |         if instance.state != PuOrder.PUORDER_CREATE: | ||||||
|             raise ParseError('采购订单非创建中不可删除') |             raise ParseError('采购订单非创建中不可删除') | ||||||
|  | @ -143,6 +145,7 @@ class PuOrderItemViewSet(CustomModelViewSet): | ||||||
|     filterset_fields = ['material', 'pu_order'] |     filterset_fields = ['material', 'pu_order'] | ||||||
|     ordering = ['create_time'] |     ordering = ['create_time'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         pu_order = instance.pu_order |         pu_order = instance.pu_order | ||||||
|         if pu_order.state != PuOrder.PUORDER_CREATE: |         if pu_order.state != PuOrder.PUORDER_CREATE: | ||||||
|  |  | ||||||
|  | @ -25,8 +25,6 @@ class QctFilter(filters.FilterSet): | ||||||
|             "testitems": ["exact"], |             "testitems": ["exact"], | ||||||
|             "defects": ["exact"], |             "defects": ["exact"], | ||||||
|             "qctmat__material": ["exact"], |             "qctmat__material": ["exact"], | ||||||
|             "qctmat__use_for_in": ["exact"], |  | ||||||
|             "qctmat__use_for_out": ["exact"], |  | ||||||
|             "qctmat__tracing": ["exact"], |             "qctmat__tracing": ["exact"], | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  | @ -40,25 +38,19 @@ class TestItemFilter(filters.FilterSet): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FtestWorkFilter(filters.FilterSet): | class FtestWorkFilter(filters.FilterSet): | ||||||
|     cbatch = filters.CharFilter(label='批次号', method='filter_cbatch') |  | ||||||
|     class Meta: |     class Meta: | ||||||
|         model = FtestWork |         model = FtestWork | ||||||
|         fields = { |         fields = { | ||||||
|             "material__process__name": ["exact", "contains"], |             "material__process__name": ["exact", "contains"], | ||||||
|             "material": ["exact"], |             "material": ["exact"], | ||||||
|             "wm": ["exact", "isnull"], |             "wm": ["exact"], | ||||||
|             "mb": ["exact", "isnull"], |             "mb": ["exact"], | ||||||
|             "batch": ["exact"], |             "batch": ["exact"], | ||||||
|             "type": ["exact"], |             "type": ["exact"], | ||||||
|             "type2": ["exact"], |             "type2": ["exact"], | ||||||
|             "shift": ["exact"]            |             "shift": ["exact"]            | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|     def filter_cbatch(self, queryset, name, value): |  | ||||||
|         qs1 = queryset.filter(wm__batch=value) |  | ||||||
|         qs2 = queryset.filter(mb__batch=value) |  | ||||||
|         return qs1.union(qs2) |  | ||||||
| 
 |  | ||||||
| class FtestFilter(filters.FilterSet): | class FtestFilter(filters.FilterSet): | ||||||
|     wpr = filters.CharFilter(label="wprId", method="filter_wpr") |     wpr = filters.CharFilter(label="wprId", method="filter_wpr") | ||||||
|     class Meta: |     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,14 +172,9 @@ class Qct(CommonAModel): | ||||||
|         return QctMat.objects.filter(qct=self) |         return QctMat.objects.filter(qct=self) | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get(cls, material:Material, tag:str, type:str=None): |     def get(cls, material:Material, tag:str): | ||||||
|         try: |         try: | ||||||
|             if type == "in": |             qct = Qct.objects.get(qctmat__material=material, tags__contains=tag) | ||||||
|                 qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_in=True) |  | ||||||
|             elif type == "out": |  | ||||||
|                 qct = Qct.objects.get(qctmat__material=material, tags__contains=tag, qctmat__use_for_out=True) |  | ||||||
|             else: |  | ||||||
|                 qct = Qct.objects.get(qctmat__material=material, tags__contains=tag) |  | ||||||
|         except Qct.DoesNotExist: |         except Qct.DoesNotExist: | ||||||
|             try: |             try: | ||||||
|                 qct = Qct.objects.get(name="默认检验表") |                 qct = Qct.objects.get(name="默认检验表") | ||||||
|  | @ -200,12 +195,8 @@ class Qct(CommonAModel): | ||||||
|             return None |             return None | ||||||
|      |      | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get_qs(cls, materialId:str, tag:str, type:str): |     def get_qs(cls, materialId:str, tag:str): | ||||||
|         qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag) |         qct_qs = Qct.objects.filter(qctmat__material__id=materialId, tags__contains=tag) | ||||||
|         if type == "in": |  | ||||||
|             qct_qs = qct_qs.filter(qctmat__use_for_in=True) |  | ||||||
|         elif type == "out": |  | ||||||
|             qct_qs = qct_qs.filter(qctmat__use_for_out=True) |  | ||||||
|         if not qct_qs.exists():  |         if not qct_qs.exists():  | ||||||
|             qct_qs = Qct.objects.filter(name="默认检验表") |             qct_qs = Qct.objects.filter(name="默认检验表") | ||||||
|         return qct_qs |         return qct_qs | ||||||
|  | @ -233,8 +224,6 @@ class QctMat(BaseModel): | ||||||
|     material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE) |     material = models.ForeignKey(Material, verbose_name="物料", on_delete=models.CASCADE) | ||||||
|     tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES, |     tracing = models.CharField('追溯层级', default=QC_T, choices=QC_TRACE_CHOICES, | ||||||
|                                max_length=20, help_text=str(QC_TRACE_CHOICES)) |                                max_length=20, help_text=str(QC_TRACE_CHOICES)) | ||||||
|     use_for_in = models.BooleanField("可用于消耗", default=True) |  | ||||||
|     use_for_out = models.BooleanField("可用于产出", default=True) |  | ||||||
|     max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True) |     max_defect_rate = models.FloatField('最大不合格率', default=0.5, null=True, blank=True) | ||||||
|      |      | ||||||
| 
 | 
 | ||||||
|  | @ -337,7 +326,7 @@ class Ftest(CommonBDModel): | ||||||
|         User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user') |         User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user') | ||||||
|     check_user = models.ForeignKey( |     check_user = models.ForeignKey( | ||||||
|         User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True) |         User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True) | ||||||
|     is_ok = models.BooleanField('是否合格', default=True) |     is_ok = models.BooleanField('是否合格', default=False) | ||||||
|     note = models.TextField('备注', default='', blank=True) |     note = models.TextField('备注', default='', blank=True) | ||||||
|     ftest_work = models.ForeignKey( |     ftest_work = models.ForeignKey( | ||||||
|         FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True) |         FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True) | ||||||
|  | @ -353,12 +342,13 @@ class Ftest(CommonBDModel): | ||||||
| 
 | 
 | ||||||
|     @classmethod |     @classmethod | ||||||
|     def init_by_qct(cls, qct, test_user, test_date): |     def init_by_qct(cls, qct, test_user, test_date): | ||||||
|         ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date) |         with transaction.atomic(): | ||||||
|         for testitem in qct.testitems.all(): |             ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date) | ||||||
|             FtestItem.objects.create(ftest=ftest, testitem=testitem) |             for testitem in qct.testitems.all(): | ||||||
|         for defect in qct.defects.all(): |                 FtestItem.objects.create(ftest=ftest, testitem=testitem) | ||||||
|             FtestDefect.objects.create(ftest=ftest, defect=defect) |             for defect in qct.defects.all(): | ||||||
|         return ftest |                 FtestDefect.objects.create(ftest=ftest, defect=defect) | ||||||
|  |             return ftest | ||||||
| 
 | 
 | ||||||
|     # @classmethod |     # @classmethod | ||||||
|     # def cal_count_notok(cls, ftestwork: FtestWork): |     # def cal_count_notok(cls, ftestwork: FtestWork): | ||||||
|  | @ -445,7 +435,7 @@ class Ptest(CommonAModel): | ||||||
|     val_tg = models.FloatField("Tg", help_text='℃', null=True, blank=True) |     val_tg = models.FloatField("Tg", help_text='℃', null=True, blank=True) | ||||||
|     val_tf = models.FloatField("Tf", help_text='℃', null=True, blank=True) |     val_tf = models.FloatField("Tf", help_text='℃', null=True, blank=True) | ||||||
|     val_xj = models.CharField( |     val_xj = models.CharField( | ||||||
|         '析晶', max_length=10, null=True, blank=True, choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS)) |         '析晶', max_length=10, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS)) | ||||||
|     val_pzxs = models.FloatField( |     val_pzxs = models.FloatField( | ||||||
|         '膨胀系数', help_text='30-300℃', null=True, blank=True) |         '膨胀系数', help_text='30-300℃', null=True, blank=True) | ||||||
|     val_zgwd = models.FloatField('升至最高温度', null=True, blank=True) |     val_zgwd = models.FloatField('升至最高温度', null=True, blank=True) | ||||||
|  |  | ||||||
|  | @ -37,7 +37,6 @@ class DefectSerializer(CustomModelSerializer): | ||||||
| class QctGetSerializer(serializers.Serializer): | class QctGetSerializer(serializers.Serializer): | ||||||
|     material = serializers.CharField(label="物料ID") |     material = serializers.CharField(label="物料ID") | ||||||
|     tag = serializers.CharField(label="标签") |     tag = serializers.CharField(label="标签") | ||||||
|     type = serializers.CharField(label="类型(in/out)", required=False) |  | ||||||
| 
 | 
 | ||||||
| class TestItemSerializer(CustomModelSerializer): | class TestItemSerializer(CustomModelSerializer): | ||||||
|     process_name = serializers.CharField(source="process.name", read_only=True) |     process_name = serializers.CharField(source="process.name", read_only=True) | ||||||
|  | @ -54,7 +53,6 @@ class QctSerializer(CustomModelSerializer): | ||||||
| 
 | 
 | ||||||
| class QctTestItemSerializer(CustomModelSerializer): | class QctTestItemSerializer(CustomModelSerializer): | ||||||
|     testitem_name = serializers.CharField(source='testitem.name', read_only=True) |     testitem_name = serializers.CharField(source='testitem.name', read_only=True) | ||||||
|     testitem_type = serializers.CharField(source='testitem.type', read_only=True) |  | ||||||
|     testitem_description = serializers.CharField(source='testitem.description', read_only=True) |     testitem_description = serializers.CharField(source='testitem.description', read_only=True) | ||||||
|     testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True) |     testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True) | ||||||
|     testitem_choices = serializers.CharField(source='testitem.choices', read_only=True) |     testitem_choices = serializers.CharField(source='testitem.choices', read_only=True) | ||||||
|  | @ -63,11 +61,11 @@ class QctTestItemSerializer(CustomModelSerializer): | ||||||
|         model = QctTestItem |         model = QctTestItem | ||||||
|         fields = '__all__' |         fields = '__all__' | ||||||
| 
 | 
 | ||||||
|     # def validate(self, attrs): |     def validate(self, attrs): | ||||||
|     #     testitem:TestItem = attrs.get("testitem") |         testitem:TestItem = attrs.get("testitem") | ||||||
|     #     if testitem.type != TestItem.T_TEST: |         if testitem.type != TestItem.T_TEST: | ||||||
|     #         raise ParseError("只可选择检测项") |             raise ParseError("只可选择检测项") | ||||||
|     #     return attrs |         return attrs | ||||||
| 
 | 
 | ||||||
| class QctDefectSerializer(CustomModelSerializer): | class QctDefectSerializer(CustomModelSerializer): | ||||||
|     defect_name = serializers.CharField(source='defect.name', read_only=True) |     defect_name = serializers.CharField(source='defect.name', read_only=True) | ||||||
|  | @ -215,33 +213,34 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer): | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         ftestworkdefect = validated_data.pop("ftestworkdefect", []) |         ftestworkdefect = validated_data.pop("ftestworkdefect", []) | ||||||
|         ins: FtestWork = super().create(validated_data) |         with transaction.atomic(): | ||||||
|         for ftestworkdefect in ftestworkdefect: |             ins: FtestWork = super().create(validated_data) | ||||||
|             if ftestworkdefect['count'] > 0: |             for ftestworkdefect in ftestworkdefect: | ||||||
|                 FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect) |                 if ftestworkdefect['count'] > 0: | ||||||
|         if ins.qct or ftestworkdefect: |                     FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect) | ||||||
|             ins.cal_count() |             if ins.qct or ftestworkdefect: | ||||||
|  |                 ins.cal_count() | ||||||
|         return ins |         return ins | ||||||
| 
 | 
 | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         ftestworkdefect = validated_data.pop("ftestworkdefect", []) |         ftestworkdefect = validated_data.pop("ftestworkdefect", []) | ||||||
|         ins:FtestWork = super().update(instance, validated_data) |         ins:FtestWork = super().update(instance, validated_data) | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         fd_ids = [] |             fd_ids = [] | ||||||
|         for item in ftestworkdefect: |             for item in ftestworkdefect: | ||||||
|             if item["count"] > 0: |                 if item["count"] > 0: | ||||||
|                 try: |                     try: | ||||||
|                     ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"]) |                         ins = FtestworkDefect.objects.get(ftestwork=ins, defect=item["defect"]) | ||||||
|                 except FtestworkDefect.DoesNotExist: |                     except FtestworkDefect.DoesNotExist: | ||||||
|                     raise ParseError("新的缺陷项!") |                         raise ParseError("新的缺陷项!") | ||||||
|                 except FtestworkDefect.MultipleObjectsReturned: |                     except FtestworkDefect.MultipleObjectsReturned: | ||||||
|                     raise ParseError("缺陷项重复!") |                         raise ParseError("缺陷项重复!") | ||||||
|                 ins.count = item["count"] |                     ins.count = item["count"] | ||||||
|                 ins.save() |                     ins.save() | ||||||
|                 fd_ids.append(ins.id) |                     fd_ids.append(ins.id) | ||||||
|         FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete() |             FtestworkDefect.objects.filter(ftestwork=ins).exclude(id__in=fd_ids).delete() | ||||||
|         if ins.qct or ftestworkdefect: |             if ins.qct or ftestworkdefect: | ||||||
|             ins.cal_count() |                 ins.cal_count() | ||||||
|         return ins |         return ins | ||||||
| 
 | 
 | ||||||
| class FtestWorkSerializer(CustomModelSerializer): | class FtestWorkSerializer(CustomModelSerializer): | ||||||
|  | @ -291,24 +290,24 @@ class FtestSerializer(CustomModelSerializer): | ||||||
| 
 | 
 | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         ftestitems = validated_data.pop('ftestitems', []) |         ftestitems = validated_data.pop('ftestitems', []) | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance = super().create(validated_data) |             instance = super().create(validated_data) | ||||||
|         for item in ftestitems: |             for item in ftestitems: | ||||||
|             FtestItem.objects.create(ftest=instance, **item) |                 FtestItem.objects.create(ftest=instance, **item) | ||||||
|         return instance |         return instance | ||||||
| 
 | 
 | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         validated_data.pop('ftest_work', None) |         validated_data.pop('ftest_work', None) | ||||||
|         ftestitems = validated_data.pop('ftestitems', []) |         ftestitems = validated_data.pop('ftestitems', []) | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance = super().update(instance, validated_data) |             instance = super().update(instance, validated_data) | ||||||
|         for item in ftestitems: |             for item in ftestitems: | ||||||
|             id = item.get('id', None) |                 id = item.get('id', None) | ||||||
|             if id: |                 if id: | ||||||
|                 ftestitem = FtestItem.objects.get(id=id) |                     ftestitem = FtestItem.objects.get(id=id) | ||||||
|                 ftestitem.test_val = item['test_val'] |                     ftestitem.test_val = item['test_val'] | ||||||
|                 ftestitem.check_val = item['check_val'] |                     ftestitem.check_val = item['check_val'] | ||||||
|                 ftestitem.save() |                     ftestitem.save() | ||||||
|         return instance |         return instance | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -363,75 +362,75 @@ class FtestProcessSerializer(CustomModelSerializer): | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         ftestitems = validated_data.pop('ftestitems', []) |         ftestitems = validated_data.pop('ftestitems', []) | ||||||
|         ftestdefects = validated_data.pop('ftestdefects', []) |         ftestdefects = validated_data.pop('ftestdefects', []) | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance = super().create(validated_data) |             instance = super().create(validated_data) | ||||||
|         for item in ftestitems: |             for item in ftestitems: | ||||||
|             FtestItem.objects.create(ftest=instance, **item) |                 FtestItem.objects.create(ftest=instance, **item) | ||||||
|         is_ok = True |             is_ok = True | ||||||
|         defect_main = None |             defect_main = None | ||||||
|         has_is_main = False |             has_is_main = False | ||||||
|         for item2 in ftestdefects: |             for item2 in ftestdefects: | ||||||
|             defect:Defect = item2["defect"] |                 defect:Defect = item2["defect"] | ||||||
|             if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]: |                 if defect.okcate in [Defect.DEFECT_NOTOK] and item2["has"]: | ||||||
|                 is_ok = False |                     is_ok = False | ||||||
|                 if not has_is_main: |                     if not has_is_main: | ||||||
|                     item2["is_main"] = True |                         item2["is_main"] = True | ||||||
|                     has_is_main = True |                         has_is_main = True | ||||||
|                     defect_main = defect |                         defect_main = defect | ||||||
|                 else: |                     else: | ||||||
|                     item2["is_main"] = False |                         item2["is_main"] = False | ||||||
|             FtestDefect.objects.create(ftest=instance, **item2) |                 FtestDefect.objects.create(ftest=instance, **item2) | ||||||
|         if not is_ok: |             if not is_ok: | ||||||
|             instance.defect_main = defect_main |                 instance.defect_main = defect_main | ||||||
|         else: |             else: | ||||||
|             instance.defect_main = None |                 instance.defect_main = None | ||||||
|         instance.is_ok = is_ok |             instance.is_ok = is_ok | ||||||
|         instance.save() |             instance.save() | ||||||
|         return instance |         return instance | ||||||
| 
 | 
 | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         ftestitems = validated_data.pop('ftestitems', []) |         ftestitems = validated_data.pop('ftestitems', []) | ||||||
|         ftestdefects = validated_data.pop('ftestdefects', []) |         ftestdefects = validated_data.pop('ftestdefects', []) | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         instance = super().update(instance, validated_data) |             instance = super().update(instance, validated_data) | ||||||
|         for item in ftestitems: |             for item in ftestitems: | ||||||
|             try: |                 try: | ||||||
|                 ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance) |                     ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance) | ||||||
|             except FtestItem.DoesNotExist: |                 except FtestItem.DoesNotExist: | ||||||
|                 ins = FtestItem.objects.create(ftest=instance, **item) |                     raise ParseError("新的检测项!") | ||||||
|             for k, v in item.items(): |                 for k, v in item.items(): | ||||||
|                 setattr(ins, k, v) |                     setattr(ins, k, v) | ||||||
|             ins.save() |                 ins.save() | ||||||
|         is_ok = True |             is_ok = True | ||||||
|         defect_main = None |             defect_main = None | ||||||
|         has_is_main = False |             has_is_main = False | ||||||
|         for item2 in ftestdefects: |             for item2 in ftestdefects: | ||||||
|             try: |                 try: | ||||||
|                 ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"]) |                     ins:FtestDefect = FtestDefect.objects.get(ftest=instance, defect=item2["defect"]) | ||||||
|             except FtestDefect.MultipleObjectsReturned: |                 except FtestDefect.MultipleObjectsReturned: | ||||||
|                 myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}") |                     myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}") | ||||||
|                 raise ParseError("获取到重复的缺陷项!") |                     raise ParseError("获取到重复的缺陷项!") | ||||||
|             except FtestDefect.DoesNotExist: |                 except FtestDefect.DoesNotExist: | ||||||
|                 ins = FtestDefect.objects.create(ftest=instance, **item2) |                     raise ParseError("新的缺陷项!") | ||||||
|             for k, v in item2.items(): |                 for k, v in item2.items(): | ||||||
|                 setattr(ins, k, v) |                     setattr(ins, k, v) | ||||||
|             ins.save() |                 ins.save() | ||||||
|             if ins.is_main: |                 if ins.is_main: | ||||||
|                 has_is_main = True |  | ||||||
|                 defect_main = ins.defect |  | ||||||
|             if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]: |  | ||||||
|                 is_ok = False |  | ||||||
|                 if not has_is_main: |  | ||||||
|                     ins.is_main = True |  | ||||||
|                     has_is_main = True |                     has_is_main = True | ||||||
|                     defect_main = ins.defect |                     defect_main = ins.defect | ||||||
|                 else: |                 if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]: | ||||||
|                     ins.is_main = False |                     is_ok = False | ||||||
|                 ins.save() |                     if not has_is_main: | ||||||
|         if not is_ok: |                         ins.is_main = True | ||||||
|             instance.defect_main = defect_main |                         has_is_main = True | ||||||
|         else: |                         defect_main = ins.defect | ||||||
|             instance.defect_main = None |                     else: | ||||||
|         instance.is_ok = is_ok |                         ins.is_main = False | ||||||
|         instance.save() |                     ins.save() | ||||||
|  |             if not is_ok: | ||||||
|  |                 instance.defect_main = defect_main | ||||||
|  |             else: | ||||||
|  |                 instance.defect_main = None | ||||||
|  |             instance.is_ok = is_ok | ||||||
|  |             instance.save() | ||||||
|         return instance |         return instance | ||||||
|  | @ -6,7 +6,6 @@ from django.utils import timezone | ||||||
| from apps.wf.models import Ticket | from apps.wf.models import Ticket | ||||||
| from apps.qm.models import NotOkOption, Defect | from apps.qm.models import NotOkOption, Defect | ||||||
| from apps.wpm.services_2 import ana_batch_thread | from apps.wpm.services_2 import ana_batch_thread | ||||||
| from apps.inm.models import MaterialBatch |  | ||||||
| 
 | 
 | ||||||
| def ftestwork_submit_validate(ins: FtestWork): | def ftestwork_submit_validate(ins: FtestWork): | ||||||
|     wm:WMaterial = ins.wm |     wm:WMaterial = ins.wm | ||||||
|  | @ -24,8 +23,8 @@ def ftestwork_submit_validate(ins: FtestWork): | ||||||
| def ftestwork_submit(ins:FtestWork, user: User): | def ftestwork_submit(ins:FtestWork, user: User): | ||||||
|     wm:WMaterial = ins.wm |     wm:WMaterial = ins.wm | ||||||
|     fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins) |     fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins) | ||||||
|     if wm and ins.need_update_wm: |     if ins.need_update_wm: | ||||||
|         if ins.qct is None and not fwd_qs.exists(): |         if ins.qct is None or not fwd_qs.exists(): | ||||||
|             if wm.state == WMaterial.WM_TEST: |             if wm.state == WMaterial.WM_TEST: | ||||||
|                 # 更新对应的车间库存 |                 # 更新对应的车间库存 | ||||||
|                 wm.count = wm.count - ins.count |                 wm.count = wm.count - ins.count | ||||||
|  | @ -100,7 +99,7 @@ def ftestwork_submit(ins:FtestWork, user: User): | ||||||
|         else: |         else: | ||||||
|             wm:WMaterial = ins.wm |             wm:WMaterial = ins.wm | ||||||
|             # 此时调用了qct表 |             # 此时调用了qct表 | ||||||
|             for item in fwd_qs: |             for item in FtestworkDefect.objects.filter(ftestwork=ins): | ||||||
|                 item:FtestworkDefect = item |                 item:FtestworkDefect = item | ||||||
|                 if item.count > 0: |                 if item.count > 0: | ||||||
|                     wm.count = wm.count - item.count |                     wm.count = wm.count - item.count | ||||||
|  | @ -125,37 +124,9 @@ def ftestwork_submit(ins:FtestWork, user: User): | ||||||
|                     if not new_create: |                     if not new_create: | ||||||
|                         wmx.count = wmx.count + item.count |                         wmx.count = wmx.count + item.count | ||||||
|                         wmx.save() |                         wmx.save() | ||||||
|      |  | ||||||
|     if ins.mb: |  | ||||||
|         mb:MaterialBatch = ins.mb |  | ||||||
|         for item in fwd_qs: |  | ||||||
|             item:FtestworkDefect = item |  | ||||||
|             if item.count > 0: |  | ||||||
|                 mb.count = mb.count - item.count |  | ||||||
|                 if mb.count < 0: |  | ||||||
|                     raise ParseError("数量不足,扣减失败") |  | ||||||
|                 mb.save() |  | ||||||
|                 mbstate = WMaterial.WM_OK |  | ||||||
|                 if item.defect.okcate == Defect.DEFECT_NOTOK: |  | ||||||
|                     mbstate = WMaterial.WM_NOTOK |  | ||||||
|                 mbx, new_create = MaterialBatch.objects.get_or_create( |  | ||||||
|                     material=mb.material,  |  | ||||||
|                     warehouse=mb.warehouse,  |  | ||||||
|                     batch=mb.batch,  |  | ||||||
|                     defect=item.defect, |  | ||||||
|                     state=mbstate, |  | ||||||
|                     defaults={ |  | ||||||
|                         'count': item.count, |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
|                 if not new_create: |  | ||||||
|                     mbx.count = mbx.count + item.count |  | ||||||
|                     mbx.save() |  | ||||||
|     ins.submit_user = user |     ins.submit_user = user | ||||||
|     ins.submit_time = timezone.now() |     ins.submit_time = timezone.now() | ||||||
|     ins.save() |     ins.save() | ||||||
|     # 触发批次统计分析 |  | ||||||
|     ana_batch_thread(xbatchs=[ins.batch]) |  | ||||||
| 
 | 
 | ||||||
| def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict): | def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict): | ||||||
|     ins = FtestWork.objects.get(id=new_ticket_data['t_id']) |     ins = FtestWork.objects.get(id=new_ticket_data['t_id']) | ||||||
|  | @ -177,3 +148,4 @@ def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict): | ||||||
| def ftestwork_audit_end(ticket: Ticket): | def ftestwork_audit_end(ticket: Ticket): | ||||||
|     ins = FtestWork.objects.get(id=ticket.ticket_data['t_id']) |     ins = FtestWork.objects.get(id=ticket.ticket_data['t_id']) | ||||||
|     ftestwork_submit(ins, ticket.create_by) |     ftestwork_submit(ins, ticket.create_by) | ||||||
|  |     ana_batch_thread(xbatchs=[ins.batch]) | ||||||
|  | @ -33,6 +33,7 @@ class DefectViewSet(CustomModelViewSet): | ||||||
|     filterset_fields = ["cate", "okcate"] |     filterset_fields = ["cate", "okcate"] | ||||||
|     search_fields = ["name", "code"] |     search_fields = ["name", "code"] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         QctDefect.objects.filter(defect=instance).delete() |         QctDefect.objects.filter(defect=instance).delete() | ||||||
|         instance.delete() |         instance.delete() | ||||||
|  | @ -62,10 +63,8 @@ class QctViewSet(CustomModelViewSet): | ||||||
|         sr = QctGetSerializer(data=request.data) |         sr = QctGetSerializer(data=request.data) | ||||||
|         sr.is_valid(raise_exception=True) |         sr.is_valid(raise_exception=True) | ||||||
|         vdata = sr.validated_data |         vdata = sr.validated_data | ||||||
|         qct = Qct.get(vdata["material"], vdata["tag"], vdata.get("type", None)) |         qct = Qct.get(vdata["material"], vdata["tag"]) | ||||||
|         if qct: |         return Response(QctDetailSerializer(instance=qct).data) | ||||||
|             return Response(QctDetailSerializer(instance=qct).data) |  | ||||||
|         return Response() |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class QctTestItemViewSet(CustomModelViewSet): | class QctTestItemViewSet(CustomModelViewSet): | ||||||
|  | @ -92,6 +91,7 @@ class QctDefectViewSet(CustomModelViewSet): | ||||||
|     filterset_fields = ["qct", "defect"] |     filterset_fields = ["qct", "defect"] | ||||||
|     ordering = ["qct", "sort"] |     ordering = ["qct", "sort"] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         ins: QctDefect = serializer.save() |         ins: QctDefect = serializer.save() | ||||||
|         if ins.is_default: |         if ins.is_default: | ||||||
|  | @ -149,6 +149,7 @@ class TestItemViewSet(CustomModelViewSet): | ||||||
|             item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects]) |             item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects]) | ||||||
|         return data |         return data | ||||||
|      |      | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         QctTestItem.objects.filter(testitem=instance).delete() |         QctTestItem.objects.filter(testitem=instance).delete() | ||||||
|         instance.delete() |         instance.delete() | ||||||
|  | @ -235,16 +236,19 @@ class FtestViewSet(CustomModelViewSet): | ||||||
|             ftest_work.count_notok = all_count - ok_count |             ftest_work.count_notok = all_count - ok_count | ||||||
|         ftest_work.save() |         ftest_work.save() | ||||||
|      |      | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         ins: Ftest = serializer.save() |         ins: Ftest = serializer.save() | ||||||
|         if ins.ftest_work: |         if ins.ftest_work: | ||||||
|             self.count_sampling(ins.ftest_work) |             self.count_sampling(ins.ftest_work) | ||||||
|      |      | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_update(self, serializer): |     def perform_update(self, serializer): | ||||||
|         ins: Ftest = serializer.save() |         ins: Ftest = serializer.save() | ||||||
|         if ins.ftest_work: |         if ins.ftest_work: | ||||||
|             self.count_sampling(ins.ftest_work) |             self.count_sampling(ins.ftest_work) | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         ftest_work = instance.ftest_work |         ftest_work = instance.ftest_work | ||||||
|         instance.delete() |         instance.delete() | ||||||
|  | @ -278,32 +282,27 @@ class FtestWorkViewSet(CustomModelViewSet): | ||||||
|     select_related_fields = ['material', 'mb', 'mb__material'] |     select_related_fields = ['material', 'mb', 'mb__material'] | ||||||
|     filterset_class = FtestWorkFilter |     filterset_class = FtestWorkFilter | ||||||
| 
 | 
 | ||||||
|     @transaction.atomic |  | ||||||
|     def update(self, request, *args, **kwargs): |     def update(self, request, *args, **kwargs): | ||||||
|         ins:FtestWork = self.get_object() |         ins:FtestWork = self.get_object() | ||||||
|         partial = kwargs.pop('partial', False) |  | ||||||
|         if ins.submit_time is not None: |         if ins.submit_time is not None: | ||||||
|             raise ParseError('已提交无法修改') |             raise ParseError('已提交无法修改') | ||||||
|         if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START: |         if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START: | ||||||
|             raise ParseError('审批单已进行,无法修改') |             raise ParseError('审批单已进行,无法修改') | ||||||
|         serializer = self.get_serializer(ins, data=request.data, partial=partial) |         x =  super().update(request, *args, **kwargs) | ||||||
|         serializer.is_valid(raise_exception=True) |  | ||||||
|         self.perform_update(serializer) |  | ||||||
|         # 触发批次统计分析 |         # 触发批次统计分析 | ||||||
|         ana_batch_thread(xbatchs=[ins.batch]) |         ana_batch_thread(xbatchs=[ins.batch]) | ||||||
|         return Response(serializer.data) |         return x | ||||||
| 
 | 
 | ||||||
|     @transaction.atomic |  | ||||||
|     def destroy(self, request, *args, **kwargs): |     def destroy(self, request, *args, **kwargs): | ||||||
|         ins:FtestWork = self.get_object() |         ins:FtestWork = self.get_object() | ||||||
|         if ins.submit_time is not None: |         if ins.submit_time is not None: | ||||||
|             raise ParseError('已提交无法删除') |             raise ParseError('已提交无法删除') | ||||||
|         if ins.ticket: |         if ins.ticket: | ||||||
|             raise ParseError('存在审批, 无法删除') |             raise ParseError('存在审批, 无法删除') | ||||||
|         self.perform_destroy(ins) |         x = super().destroy(request, *args, **kwargs) | ||||||
|         # 触发批次统计分析 |         # 触发批次统计分析 | ||||||
|         ana_batch_thread(xbatchs=[ins.batch]) |         ana_batch_thread(xbatchs=[ins.batch]) | ||||||
|         return Response(status=204) |         return x | ||||||
|      |      | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         ins = serializer.save() |         ins = serializer.save() | ||||||
|  | @ -321,8 +320,8 @@ class FtestWorkViewSet(CustomModelViewSet): | ||||||
|         ins:FtestWork = self.get_object() |         ins:FtestWork = self.get_object() | ||||||
|         if ins.ticket: |         if ins.ticket: | ||||||
|             raise ParseError('该检验工作存在审批!') |             raise ParseError('该检验工作存在审批!') | ||||||
|         if ins.wm is None and ins.mb is None: |         if ins.wm is None: | ||||||
|             raise ParseError('该检验工作未关联库存') |             raise ParseError('该检验工作未关联车间库存') | ||||||
|         if ins.submit_time is None: |         if ins.submit_time is None: | ||||||
|             ftestwork_submit(ins, request.user) |             ftestwork_submit(ins, request.user) | ||||||
|         else: |         else: | ||||||
|  |  | ||||||
|  | @ -65,6 +65,7 @@ class OrderViewSet(CustomModelViewSet): | ||||||
|         "state": ["exact", "in"], |         "state": ["exact", "in"], | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_destroy(self, instance): |     def perform_destroy(self, instance): | ||||||
|         if instance.state != Order.ORDER_CREATE: |         if instance.state != Order.ORDER_CREATE: | ||||||
|             raise ParseError('订单非创建中不可删除') |             raise ParseError('订单非创建中不可删除') | ||||||
|  |  | ||||||
|  | @ -275,11 +275,13 @@ class DeptCreateUpdateSerializer(CustomModelSerializer): | ||||||
|         model = Dept |         model = Dept | ||||||
|         exclude = EXCLUDE_FIELDS + ['third_info'] |         exclude = EXCLUDE_FIELDS + ['third_info'] | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def create(self, validated_data): |     def create(self, validated_data): | ||||||
|         ins = super().create(validated_data) |         ins = super().create(validated_data) | ||||||
|         sync_dahua_dept(ins) |         sync_dahua_dept(ins) | ||||||
|         return ins |         return ins | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def update(self, instance, validated_data): |     def update(self, instance, validated_data): | ||||||
|         ins = super().update(instance, validated_data) |         ins = super().update(instance, validated_data) | ||||||
|         sync_dahua_dept(ins) |         sync_dahua_dept(ins) | ||||||
|  |  | ||||||
|  | @ -8,7 +8,8 @@ from django_celery_beat.models import (CrontabSchedule, IntervalSchedule, | ||||||
| from django_celery_results.models import TaskResult | from django_celery_results.models import TaskResult | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied | from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied | ||||||
| from rest_framework.mixins import RetrieveModelMixin | from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin, | ||||||
|  |                                    ListModelMixin, RetrieveModelMixin) | ||||||
| from rest_framework.parsers import (JSONParser, | from rest_framework.parsers import (JSONParser, | ||||||
|                                     MultiPartParser) |                                     MultiPartParser) | ||||||
| from rest_framework.serializers import Serializer | from rest_framework.serializers import Serializer | ||||||
|  | @ -19,7 +20,7 @@ from apps.hrm.models import Employee | ||||||
| from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG | from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG | ||||||
| from apps.system.filters import DeptFilterSet, UserFilterSet | from apps.system.filters import DeptFilterSet, UserFilterSet | ||||||
| # from django_q.models import Task as QTask, Schedule as QSchedule | # from django_q.models import Task as QTask, Schedule as QSchedule | ||||||
| from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin) | from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin) | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from apps.utils.permission import ALL_PERMS | from apps.utils.permission import ALL_PERMS | ||||||
| from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet | ||||||
|  | @ -228,7 +229,7 @@ class PTaskViewSet(CustomModelViewSet): | ||||||
|         return Response() |         return Response() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet): | class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet): | ||||||
|     """ |     """ | ||||||
|     list:任务执行结果列表 |     list:任务执行结果列表 | ||||||
| 
 | 
 | ||||||
|  | @ -372,7 +373,7 @@ class RoleViewSet(CustomModelViewSet): | ||||||
|     ordering = ['create_time'] |     ordering = ['create_time'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet): | class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): | ||||||
|     """岗位/角色关系 |     """岗位/角色关系 | ||||||
| 
 | 
 | ||||||
|     岗位/角色关系 |     岗位/角色关系 | ||||||
|  | @ -384,7 +385,7 @@ class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod | ||||||
|     filterset_fields = ['post', 'role'] |     filterset_fields = ['post', 'role'] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet): | class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): | ||||||
|     """用户/岗位关系 |     """用户/岗位关系 | ||||||
| 
 | 
 | ||||||
|     用户/岗位关系 |     用户/岗位关系 | ||||||
|  | @ -397,13 +398,37 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod | ||||||
|     ordering = ['sort', 'create_time'] |     ordering = ['sort', 'create_time'] | ||||||
| 
 | 
 | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         instance = serializer.save() |         with transaction.atomic(): | ||||||
|         user = instance.user |             instance = serializer.save() | ||||||
|         up = UserPost.objects.filter(user=user).order_by( |             user = instance.user | ||||||
|             'sort', 'create_time').first() |             up = UserPost.objects.filter(user=user).order_by( | ||||||
|         if up: |                 'sort', 'create_time').first() | ||||||
|             user.belong_dept = up.dept |             if up: | ||||||
|             user.post = up.post |                 user.belong_dept = up.dept | ||||||
|  |                 user.post = up.post | ||||||
|  |                 user.update_by = self.request.user | ||||||
|  |                 user.save() | ||||||
|  |                 # 更新人员表 | ||||||
|  |                 ep = Employee.objects.get_queryset( | ||||||
|  |                     all=True).filter(user=user).first() | ||||||
|  |                 if ep: | ||||||
|  |                     ep.belong_dept = user.belong_dept | ||||||
|  |                     ep.post = user.post | ||||||
|  |                     ep.is_deleted = False | ||||||
|  |                     ep.save() | ||||||
|  | 
 | ||||||
|  |     def perform_destroy(self, instance): | ||||||
|  |         with transaction.atomic(): | ||||||
|  |             user = instance.user | ||||||
|  |             instance.delete() | ||||||
|  |             up = UserPost.objects.filter(user=user).order_by( | ||||||
|  |                 'sort', 'create_time').first() | ||||||
|  |             if up: | ||||||
|  |                 user.belong_dept = up.dept | ||||||
|  |                 user.post = up.post | ||||||
|  |             else: | ||||||
|  |                 user.belong_dept = None | ||||||
|  |                 user.post = None | ||||||
|             user.update_by = self.request.user |             user.update_by = self.request.user | ||||||
|             user.save() |             user.save() | ||||||
|             # 更新人员表 |             # 更新人员表 | ||||||
|  | @ -415,28 +440,6 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod | ||||||
|                 ep.is_deleted = False |                 ep.is_deleted = False | ||||||
|                 ep.save() |                 ep.save() | ||||||
| 
 | 
 | ||||||
|     def perform_destroy(self, instance): |  | ||||||
|         user = instance.user |  | ||||||
|         instance.delete() |  | ||||||
|         up = UserPost.objects.filter(user=user).order_by( |  | ||||||
|             'sort', 'create_time').first() |  | ||||||
|         if up: |  | ||||||
|             user.belong_dept = up.dept |  | ||||||
|             user.post = up.post |  | ||||||
|         else: |  | ||||||
|             user.belong_dept = None |  | ||||||
|             user.post = None |  | ||||||
|         user.update_by = self.request.user |  | ||||||
|         user.save() |  | ||||||
|         # 更新人员表 |  | ||||||
|         ep = Employee.objects.get_queryset( |  | ||||||
|             all=True).filter(user=user).first() |  | ||||||
|         if ep: |  | ||||||
|             ep.belong_dept = user.belong_dept |  | ||||||
|             ep.post = user.post |  | ||||||
|             ep.is_deleted = False |  | ||||||
|             ep.save() |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| class UserViewSet(CustomModelViewSet): | class UserViewSet(CustomModelViewSet): | ||||||
|     queryset = User.objects.get_queryset(all=True) |     queryset = User.objects.get_queryset(all=True) | ||||||
|  | @ -607,7 +610,7 @@ class UserViewSet(CustomModelViewSet): | ||||||
|         return Response() |         return Response() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet): | class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet): | ||||||
|     """文件上传 |     """文件上传 | ||||||
| 
 | 
 | ||||||
|     list: |     list: | ||||||
|  | @ -648,7 +651,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin | ||||||
|         instance.save() |         instance.save() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet): | class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet): | ||||||
|     perms_map = {'get': '*', 'post': 'apk.upload'} |     perms_map = {'get': '*', 'post': 'apk.upload'} | ||||||
|     serializer_class = ApkSerializer |     serializer_class = ApkSerializer | ||||||
| 
 | 
 | ||||||
|  | @ -689,7 +692,7 @@ class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, Gen | ||||||
|         return Response() |         return Response() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): | class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet): | ||||||
|     perms_map = {'get': '*', 'post': '*', |     perms_map = {'get': '*', 'post': '*', | ||||||
|                  'delete': 'myschedule.delete'} |                  'delete': 'myschedule.delete'} | ||||||
|     serializer_class = MyScheduleSerializer |     serializer_class = MyScheduleSerializer | ||||||
|  | @ -714,6 +717,7 @@ class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyM | ||||||
|             return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}") |             return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}") | ||||||
|         return '' |         return '' | ||||||
| 
 | 
 | ||||||
|  |     @transaction.atomic | ||||||
|     def perform_create(self, serializer): |     def perform_create(self, serializer): | ||||||
|         vdata = serializer.validated_data |         vdata = serializer.validated_data | ||||||
|         vdata['create_by'] = self.request.user  # 不可少 |         vdata['create_by'] = self.request.user  # 不可少 | ||||||
|  |  | ||||||
|  | @ -1,51 +0,0 @@ | ||||||
| from contextlib import contextmanager |  | ||||||
| from rest_framework.exceptions import ParseError |  | ||||||
| from functools import wraps |  | ||||||
| from django.db import transaction |  | ||||||
| 
 |  | ||||||
| @contextmanager |  | ||||||
| def lock_model_record(model_class, pk): |  | ||||||
|     """ |  | ||||||
|     Locks a model instance and returns it. |  | ||||||
|     """ |  | ||||||
|     try: |  | ||||||
|         instance = model_class.objects.select_for_update().get(pk=pk) |  | ||||||
|         yield instance |  | ||||||
|     except model_class.DoesNotExist: |  | ||||||
|         raise ParseError("该记录不存在或已被删除") |  | ||||||
| 
 |  | ||||||
| def lock_model_record_d_func(model_class, pk_attr='id'): |  | ||||||
|     """ |  | ||||||
|     通用模型锁装饰器(内置事务),用于装饰函数 |  | ||||||
|     """ |  | ||||||
|     def decorator(func): |  | ||||||
|         @wraps(func) |  | ||||||
|         @transaction.atomic |  | ||||||
|         def wrapper(old_instance, *args, **kwargs): |  | ||||||
|             try: |  | ||||||
|                 # 获取新鲜记录 |  | ||||||
|                 fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr)) |  | ||||||
|                 # 调用原函数,但传入新鲜记录 |  | ||||||
|                 return func(fresh_record, *args, **kwargs) |  | ||||||
|             except model_class.DoesNotExist: |  | ||||||
|                 raise ParseError('记录不存在或已被删除') |  | ||||||
|         return wrapper |  | ||||||
|     return decorator |  | ||||||
| 
 |  | ||||||
| def lock_model_record_d_method(model_class, pk_attr='id'): |  | ||||||
|     """ |  | ||||||
|     通用模型锁装饰器(内置事务), 用于装饰类方法 |  | ||||||
|     """ |  | ||||||
|     def decorator(func): |  | ||||||
|         @wraps(func) |  | ||||||
|         @transaction.atomic |  | ||||||
|         def wrapper(self, old_instance, *args, **kwargs): |  | ||||||
|             try: |  | ||||||
|                 # 获取新鲜记录 |  | ||||||
|                 fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr)) |  | ||||||
|                 # 调用原函数,但传入新鲜记录 |  | ||||||
|                 return func(self, fresh_record, *args, **kwargs) |  | ||||||
|             except model_class.DoesNotExist: |  | ||||||
|                 raise ParseError('记录不存在或已被删除') |  | ||||||
|         return wrapper |  | ||||||
|     return decorator |  | ||||||
|  | @ -9,6 +9,7 @@ from django.utils.timezone import now | ||||||
| from user_agents import parse | from user_agents import parse | ||||||
| import logging | import logging | ||||||
| from rest_framework.response import Response | from rest_framework.response import Response | ||||||
|  | from django.db import transaction | ||||||
| from rest_framework.exceptions import ParseError, ValidationError | from rest_framework.exceptions import ParseError, ValidationError | ||||||
| from apps.utils.errors import PKS_ERROR | from apps.utils.errors import PKS_ERROR | ||||||
| from rest_framework.generics import get_object_or_404 | from rest_framework.generics import get_object_or_404 | ||||||
|  | @ -17,8 +18,6 @@ from drf_yasg import openapi | ||||||
| from apps.utils.serializers import PkSerializer | from apps.utils.serializers import PkSerializer | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from apps.utils.serializers import ComplexSerializer | from apps.utils.serializers import ComplexSerializer | ||||||
| from django.db.models import F |  | ||||||
| from django.db import transaction |  | ||||||
| 
 | 
 | ||||||
| # 实例化myLogger | # 实例化myLogger | ||||||
| myLogger = logging.getLogger('log') | myLogger = logging.getLogger('log') | ||||||
|  | @ -82,7 +81,6 @@ class BulkCreateModelMixin(CreateModelMixin): | ||||||
|     def after_bulk_create(self, objs): |     def after_bulk_create(self, objs): | ||||||
|         pass |         pass | ||||||
| 
 | 
 | ||||||
|     @transaction.atomic |  | ||||||
|     def create(self, request, *args, **kwargs): |     def create(self, request, *args, **kwargs): | ||||||
|         """创建(支持批量) |         """创建(支持批量) | ||||||
| 
 | 
 | ||||||
|  | @ -92,9 +90,10 @@ class BulkCreateModelMixin(CreateModelMixin): | ||||||
|         many = False |         many = False | ||||||
|         if isinstance(rdata, list): |         if isinstance(rdata, list): | ||||||
|             many = True |             many = True | ||||||
|         sr = self.get_serializer(data=rdata, many=many) |         with transaction.atomic(): | ||||||
|         sr.is_valid(raise_exception=True) |             sr = self.get_serializer(data=rdata, many=many) | ||||||
|         self.perform_create(sr) |             sr.is_valid(raise_exception=True) | ||||||
|  |             self.perform_create(sr) | ||||||
|         if many: |         if many: | ||||||
|             self.after_bulk_create(sr.data) |             self.after_bulk_create(sr.data) | ||||||
|         return Response(sr.data, status=201) |         return Response(sr.data, status=201) | ||||||
|  | @ -105,7 +104,6 @@ class BulkUpdateModelMixin(UpdateModelMixin): | ||||||
|     def after_bulk_update(self, objs): |     def after_bulk_update(self, objs): | ||||||
|         pass |         pass | ||||||
|      |      | ||||||
|     @transaction.atomic |  | ||||||
|     def partial_update(self, request, *args, **kwargs): |     def partial_update(self, request, *args, **kwargs): | ||||||
|         """部分更新(支持批量) |         """部分更新(支持批量) | ||||||
| 
 | 
 | ||||||
|  | @ -114,7 +112,6 @@ class BulkUpdateModelMixin(UpdateModelMixin): | ||||||
|         kwargs['partial'] = True |         kwargs['partial'] = True | ||||||
|         return self.update(request, *args, **kwargs) |         return self.update(request, *args, **kwargs) | ||||||
|      |      | ||||||
|     @transaction.atomic |  | ||||||
|     def update(self, request, *args, **kwargs): |     def update(self, request, *args, **kwargs): | ||||||
|         """更新(支持批量) |         """更新(支持批量) | ||||||
| 
 | 
 | ||||||
|  | @ -126,15 +123,16 @@ class BulkUpdateModelMixin(UpdateModelMixin): | ||||||
|             queryset = self.filter_queryset(self.get_queryset()) |             queryset = self.filter_queryset(self.get_queryset()) | ||||||
|             objs = [] |             objs = [] | ||||||
|             if isinstance(request.data, list): |             if isinstance(request.data, list): | ||||||
|                 for ind, item in enumerate(request.data): |                 with transaction.atomic(): | ||||||
|                     obj = get_object_or_404(queryset, id=item['id']) |                     for ind, item in enumerate(request.data): | ||||||
|                     sr = self.get_serializer(obj, data=item, partial=partial) |                         obj = get_object_or_404(queryset, id=item['id']) | ||||||
|                     if not sr.is_valid(): |                         sr = self.get_serializer(obj, data=item, partial=partial) | ||||||
|                         err_dict = { f'第{ind+1}': sr.errors} |                         if not sr.is_valid(): | ||||||
|                         raise ValidationError(err_dict) |                             err_dict = { f'第{ind+1}': sr.errors} | ||||||
|                     self.perform_update(sr)  # 用自带的更新,可能需要做其他操作 |                             raise ValidationError(err_dict) | ||||||
|                     objs.append(sr.data) |                         self.perform_update(sr)  # 用自带的更新,可能需要做其他操作 | ||||||
|                 self.after_bulk_update(objs) |                         objs.append(sr.data) | ||||||
|  |                     self.after_bulk_update(objs) | ||||||
|             else: |             else: | ||||||
|                 raise ParseError('提交数据非列表') |                 raise ParseError('提交数据非列表') | ||||||
|             return Response(objs) |             return Response(objs) | ||||||
|  | @ -149,7 +147,6 @@ class BulkUpdateModelMixin(UpdateModelMixin): | ||||||
| class BulkDestroyModelMixin(DestroyModelMixin): | class BulkDestroyModelMixin(DestroyModelMixin): | ||||||
| 
 | 
 | ||||||
|     @swagger_auto_schema(request_body=PkSerializer) |     @swagger_auto_schema(request_body=PkSerializer) | ||||||
|     @transaction.atomic |  | ||||||
|     def destroy(self, request, *args, **kwargs): |     def destroy(self, request, *args, **kwargs): | ||||||
|         """删除(支持批量) |         """删除(支持批量) | ||||||
| 
 | 
 | ||||||
|  | @ -195,8 +192,6 @@ class CustomRetrieveModelMixin(RetrieveModelMixin): | ||||||
| 
 | 
 | ||||||
|         给dict返回数据添加额外信息 |         给dict返回数据添加额外信息 | ||||||
|         """ |         """ | ||||||
|         if hasattr(self, 'add_info_for_list'): |  | ||||||
|             return self.add_info_for_list([data])[0] |  | ||||||
|         return data |         return data | ||||||
| 
 | 
 | ||||||
| class CustomListModelMixin(ListModelMixin): | class CustomListModelMixin(ListModelMixin): | ||||||
|  | @ -245,8 +240,6 @@ class ComplexQueryMixin: | ||||||
|         vdata = sr.validated_data |         vdata = sr.validated_data | ||||||
|         queryset = self.get_queryset() |         queryset = self.get_queryset() | ||||||
|         querys = vdata.get('querys', []) |         querys = vdata.get('querys', []) | ||||||
|         annotate_field_list = vdata.get('annotate_field_list', []) |  | ||||||
| 
 |  | ||||||
|         if not querys: |         if not querys: | ||||||
|             new_qs = queryset |             new_qs = queryset | ||||||
|         else: |         else: | ||||||
|  | @ -268,29 +261,13 @@ class ComplexQueryMixin: | ||||||
|                     new_qs = new_qs | one_qs |                     new_qs = new_qs | one_qs | ||||||
|             except Exception as e: |             except Exception as e: | ||||||
|                 raise ParseError(str(e)) |                 raise ParseError(str(e)) | ||||||
|          |  | ||||||
|         if annotate_field_list: |  | ||||||
|             annotate_dict = getattr(self, "annotate_dict", {}) |  | ||||||
|             if annotate_dict: |  | ||||||
|                 filtered_annotate_dict = { key: annotate_dict[key] for key in annotate_field_list if key in annotate_dict} |  | ||||||
|                 new_qs = new_qs.annotate(**filtered_annotate_dict) |  | ||||||
| 
 |  | ||||||
|         ordering = vdata.get('ordering', None) |         ordering = vdata.get('ordering', None) | ||||||
|         if not ordering: |         if not ordering: | ||||||
|             ordering = getattr(self, 'ordering', None) |             ordering = getattr(self, 'ordering', None) | ||||||
|         if isinstance(ordering, str): |         if isinstance(ordering, str): | ||||||
|             ordering = ordering.replace('\n', '').replace(' ', '') |  | ||||||
|             ordering = ordering.split(',') |             ordering = ordering.split(',') | ||||||
|         order_fields = [] |  | ||||||
|         if ordering: |         if ordering: | ||||||
|             for item in ordering: |             new_qs = new_qs.order_by(*ordering) | ||||||
|                 if item.startswith('-'): |  | ||||||
|                     # JSONField 排序只能用字符串,不要 F |  | ||||||
|                     order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item) |  | ||||||
|                 else: |  | ||||||
|                     order_fields.append(F(item).asc(nulls_last=True) if '__' not in item else item) |  | ||||||
|         new_qs = new_qs.order_by(*order_fields) |  | ||||||
| 
 |  | ||||||
|         page = self.paginate_queryset(new_qs) |         page = self.paginate_queryset(new_qs) | ||||||
|         if page is not None: |         if page is not None: | ||||||
|             serializer = self.get_serializer(page, many=True) |             serializer = self.get_serializer(page, many=True) | ||||||
|  | @ -343,7 +320,6 @@ class MyLoggingMixin(object): | ||||||
|         response = super().finalize_response( |         response = super().finalize_response( | ||||||
|             request, response, *args, **kwargs |             request, response, *args, **kwargs | ||||||
|         ) |         ) | ||||||
|         self.log["response_ms"] = self._get_response_ms() |  | ||||||
|         # Ensure backward compatibility for those using _should_log hook |         # Ensure backward compatibility for those using _should_log hook | ||||||
|         should_log = ( |         should_log = ( | ||||||
|             self._should_log if hasattr(self, "_should_log") else self.should_log |             self._should_log if hasattr(self, "_should_log") else self.should_log | ||||||
|  | @ -372,7 +348,7 @@ class MyLoggingMixin(object): | ||||||
|                     "method": request.method, |                     "method": request.method, | ||||||
|                     "query_params": self._clean_data(request.query_params.dict()), |                     "query_params": self._clean_data(request.query_params.dict()), | ||||||
|                     "user": self._get_user(request), |                     "user": self._get_user(request), | ||||||
|                     # "response_ms": self._get_response_ms(), |                     "response_ms": self._get_response_ms(), | ||||||
|                     "response": self._clean_data(rendered_content), |                     "response": self._clean_data(rendered_content), | ||||||
|                     "status_code": response.status_code, |                     "status_code": response.status_code, | ||||||
|                     "agent": self._get_agent(request), |                     "agent": self._get_agent(request), | ||||||
|  | @ -466,8 +442,7 @@ class MyLoggingMixin(object): | ||||||
|         By default, check if the request method is in logging_methods. |         By default, check if the request method is in logging_methods. | ||||||
|         """ |         """ | ||||||
|         return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \ |         return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \ | ||||||
|             or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])\ |             or (request.method in self.logging_methods and response.status_code not in [401, 403, 404]) | ||||||
|             or (self.log.get("response_ms", 0) > 2000) |  | ||||||
| 
 | 
 | ||||||
|     def _clean_data(self, data): |     def _clean_data(self, data): | ||||||
|         """ |         """ | ||||||
|  |  | ||||||
|  | @ -116,26 +116,11 @@ class BaseModel(models.Model): | ||||||
|     @classmethod |     @classmethod | ||||||
|     def safe_get_or_create(cls, defaults=None, **kwargs): |     def safe_get_or_create(cls, defaults=None, **kwargs): | ||||||
|         defaults = defaults or {} |         defaults = defaults or {} | ||||||
|          |         lock_data = {**kwargs, **defaults} | ||||||
|         for attempt in range(3): |         lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest() | ||||||
|                 try: |         lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}" | ||||||
|                     # 先尝试获取(带锁) |         with cache.lock(lock_key, timeout=10): | ||||||
|                     try: |             return cls.objects.get_or_create(**kwargs, defaults=defaults) | ||||||
|                         obj = cls.objects.select_for_update().get(**kwargs) |  | ||||||
|                         return obj, False |  | ||||||
|                     except cls.DoesNotExist: |  | ||||||
|                         # 不存在则创建 |  | ||||||
|                         obj = cls(**kwargs, **defaults) |  | ||||||
|                         obj.save() |  | ||||||
|                         return obj, True |  | ||||||
|                 except IntegrityError: |  | ||||||
|                     # 发生唯一约束冲突时重试 |  | ||||||
|                     if attempt == 2: |  | ||||||
|                         raise |  | ||||||
|                     time.sleep(0.1 * (attempt + 1)) |  | ||||||
|                 except Exception: |  | ||||||
|                     # 其他异常直接抛出 |  | ||||||
|                     raise |  | ||||||
|          |          | ||||||
|          |          | ||||||
|     def handle_parent(self): |     def handle_parent(self): | ||||||
|  | @ -147,32 +132,32 @@ class BaseModel(models.Model): | ||||||
|         if not self.id: |         if not self.id: | ||||||
|             is_create = True |             is_create = True | ||||||
|             self.id = idWorker.get_id() |             self.id = idWorker.get_id() | ||||||
| 
 |         with transaction.atomic(): | ||||||
|         old_parent = None |             old_parent = None | ||||||
|         need_handle_parent = False |             need_handle_parent = False | ||||||
|         if hasattr(self, "parent"): |             if hasattr(self, "parent"): | ||||||
|             if is_create: |                 if is_create: | ||||||
|                 need_handle_parent = True |  | ||||||
|             else: |  | ||||||
|                 try: |  | ||||||
|                     old_parent = self.__class__.objects.get(id=self.id).parent |  | ||||||
|                 except Exception: |  | ||||||
|                     self.parent = None |  | ||||||
|                     need_handle_parent = True |                     need_handle_parent = True | ||||||
|                 if self.parent != old_parent: |                 else: | ||||||
|                     need_handle_parent = True |                     try: | ||||||
|         try: |                         old_parent = self.__class__.objects.get(id=self.id).parent | ||||||
|             ins = super().save(*args, **kwargs) |                     except Exception: | ||||||
|         except IntegrityError as e: |                         self.parent = None | ||||||
|             if is_create: |                         need_handle_parent = True | ||||||
|                 time.sleep(0.01) |                     if self.parent != old_parent: | ||||||
|                 self.id = idWorker.get_id() |                         need_handle_parent = True | ||||||
|  |             try: | ||||||
|                 ins = super().save(*args, **kwargs) |                 ins = super().save(*args, **kwargs) | ||||||
|             raise e |             except IntegrityError as e: | ||||||
|         # 处理父级 |                 if is_create: | ||||||
|         if need_handle_parent: |                     time.sleep(0.01) | ||||||
|             self.handle_parent() |                     self.id = idWorker.get_id() | ||||||
|         return ins |                     ins = super().save(*args, **kwargs) | ||||||
|  |                 raise e | ||||||
|  |             # 处理父级 | ||||||
|  |             if need_handle_parent: | ||||||
|  |                 self.handle_parent() | ||||||
|  |             return ins | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class SoftModel(BaseModel): | class SoftModel(BaseModel): | ||||||
|  |  | ||||||
|  | @ -75,8 +75,8 @@ class CustomModelSerializer(DynamicFieldsMixin, TreeSerializerMixin, serializers | ||||||
| class QuerySerializer(serializers.Serializer): | class QuerySerializer(serializers.Serializer): | ||||||
|     field = serializers.CharField(label='字段名') |     field = serializers.CharField(label='字段名') | ||||||
|     compare = serializers.ChoiceField( |     compare = serializers.ChoiceField( | ||||||
|         label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains", "isnull"]) |         label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"]) | ||||||
|     value = serializers.JSONField(label='值', allow_null=True) |     value = serializers.CharField(label='值') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class ComplexSerializer(serializers.Serializer): | class ComplexSerializer(serializers.Serializer): | ||||||
|  | @ -85,4 +85,3 @@ class ComplexSerializer(serializers.Serializer): | ||||||
|     ordering = serializers.CharField(required=False) |     ordering = serializers.CharField(required=False) | ||||||
|     querys = serializers.ListField(child=QuerySerializer( |     querys = serializers.ListField(child=QuerySerializer( | ||||||
|         many=True), label="查询列表", required=False) |         many=True), label="查询列表", required=False) | ||||||
|     annotate_field_list = serializers.ListField(child=serializers.CharField(), label="RawSQL字段列表", required=False) |  | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ from rest_framework.exceptions import ParseError | ||||||
| myLogger = logging.getLogger('log') | myLogger = logging.getLogger('log') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @auto_log(name='阿里云短信', raise_exception=True, send_mail=False) | @auto_log(name='阿里云短信', raise_exception=True, send_mail=True) | ||||||
| def send_sms(phone: str, template_code: int, template_param: dict): | def send_sms(phone: str, template_code: int, template_param: dict): | ||||||
|     from aliyunsdkcore.client import AcsClient |     from aliyunsdkcore.client import AcsClient | ||||||
|     from aliyunsdkcore.request import CommonRequest |     from aliyunsdkcore.request import CommonRequest | ||||||
|  |  | ||||||
|  | @ -1,8 +1,6 @@ | ||||||
| from django.db import connection | from django.db import connection | ||||||
| from django.utils import timezone |  | ||||||
| from datetime import datetime |  | ||||||
| 
 | 
 | ||||||
| def execute_raw_sql(sql: str, params=None, timeout=30): | def execute_raw_sql(sql: str, params=None): | ||||||
|     """执行原始sql并返回rows, columns数据 |     """执行原始sql并返回rows, columns数据 | ||||||
| 
 | 
 | ||||||
|     Args: |     Args: | ||||||
|  | @ -10,8 +8,7 @@ def execute_raw_sql(sql: str, params=None, timeout=30): | ||||||
|         params (_type_, optional): 参数列表. Defaults to None. |         params (_type_, optional): 参数列表. Defaults to None. | ||||||
|     """ |     """ | ||||||
|     with connection.cursor() as cursor: |     with connection.cursor() as cursor: | ||||||
|         if timeout: |         cursor.execute("SET statement_timeout TO %s;", [30000]) | ||||||
|             cursor.execute(f"SET statement_timeout TO '{int(timeout*1000)}ms';") |  | ||||||
|         if params: |         if params: | ||||||
|             cursor.execute(sql, params=params) |             cursor.execute(sql, params=params) | ||||||
|         else: |         else: | ||||||
|  | @ -26,7 +23,7 @@ def format_sqldata(columns, rows): | ||||||
|     return [columns] + rows, [dict(zip(columns, row)) for row in rows] |     return [columns] + rows, [dict(zip(columns, row)) for row in rows] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def query_all_dict(sql, params=None, with_time_format=False): | def query_all_dict(sql, params=None): | ||||||
|     ''' |     ''' | ||||||
|     查询所有结果返回字典类型数据 |     查询所有结果返回字典类型数据 | ||||||
|     :param sql: |     :param sql: | ||||||
|  | @ -39,19 +36,9 @@ def query_all_dict(sql, params=None, with_time_format=False): | ||||||
|         else: |         else: | ||||||
|             cursor.execute(sql) |             cursor.execute(sql) | ||||||
|         columns  = [desc[0] for desc in cursor.description] |         columns  = [desc[0] for desc in cursor.description] | ||||||
|         if with_time_format: |  | ||||||
|             results = [] |  | ||||||
|             for row in cursor.fetchall(): |  | ||||||
|                 row_dict = {} |  | ||||||
|                 for col, val in zip(columns, row): |  | ||||||
|                     if isinstance(val, datetime): |  | ||||||
|                         val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S") |  | ||||||
|                     row_dict[col] = val |  | ||||||
|                 results.append(row_dict) |  | ||||||
|             return results |  | ||||||
|         return [dict(zip(columns, row)) for row in cursor.fetchall()] |         return [dict(zip(columns, row)) for row in cursor.fetchall()] | ||||||
| 
 | 
 | ||||||
| def query_one_dict(sql, params=None, with_time_format=False): | def query_one_dict(sql, params=None): | ||||||
|     """ |     """ | ||||||
|     查询一个结果返回字典类型数据 |     查询一个结果返回字典类型数据 | ||||||
|     :param sql: |     :param sql: | ||||||
|  | @ -59,17 +46,13 @@ def query_one_dict(sql, params=None, with_time_format=False): | ||||||
|     :return: |     :return: | ||||||
|     """ |     """ | ||||||
|     with connection.cursor() as cursor: |     with connection.cursor() as cursor: | ||||||
|         cursor.execute(sql, params or ())  # 更简洁的参数处理 |         if params: | ||||||
|  |             cursor.execute(sql, params=params) | ||||||
|  |         else: | ||||||
|  |             cursor.execute(sql) | ||||||
|         columns = [desc[0] for desc in cursor.description] |         columns = [desc[0] for desc in cursor.description] | ||||||
|         row = cursor.fetchone() |         row = cursor.fetchone() | ||||||
|         if with_time_format: |         return dict(zip(columns, row)) | ||||||
|             row_dict = {} |  | ||||||
|             for col, val in zip(columns, row): |  | ||||||
|                 if isinstance(val, datetime): |  | ||||||
|                     val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S") |  | ||||||
|                 row_dict[col] = val |  | ||||||
|             return row_dict |  | ||||||
|         return dict(zip(columns, row)) if row else None  # 安全处理None情况 |  | ||||||
|      |      | ||||||
| import pymysql | import pymysql | ||||||
| import psycopg2 | import psycopg2 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| 
 | 
 | ||||||
| from django.core.cache import cache | from django.core.cache import cache | ||||||
| from django.http import StreamingHttpResponse, Http404 | from django.http import StreamingHttpResponse | ||||||
| from rest_framework.decorators import action | from rest_framework.decorators import action | ||||||
| from rest_framework.exceptions import ParseError | from rest_framework.exceptions import ParseError | ||||||
| from rest_framework.mixins import RetrieveModelMixin | from rest_framework.mixins import RetrieveModelMixin | ||||||
|  | @ -18,9 +18,7 @@ from apps.utils.serializers import ComplexSerializer | ||||||
| from rest_framework.throttling import UserRateThrottle | from rest_framework.throttling import UserRateThrottle | ||||||
| from drf_yasg.utils import swagger_auto_schema | from drf_yasg.utils import swagger_auto_schema | ||||||
| import json | import json | ||||||
| from django.db import connection | 
 | ||||||
| from django.core.exceptions import ObjectDoesNotExist |  | ||||||
| from django.db.utils import NotSupportedError |  | ||||||
| 
 | 
 | ||||||
| class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): | class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): | ||||||
|     """ |     """ | ||||||
|  | @ -91,36 +89,6 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): | ||||||
|             elif hash_v_e: |             elif hash_v_e: | ||||||
|                 return Response(hash_v_e) |                 return Response(hash_v_e) | ||||||
| 
 | 
 | ||||||
|     def get_object(self, force_lock=False): |  | ||||||
|         """ |  | ||||||
|         智能加锁的get_object |  | ||||||
|         - 只读请求:普通查询 |  | ||||||
|         - 非只读请求且在事务中:加锁查询 |  | ||||||
|         - 非只读请求但不在事务中:普通查询(带警告) |  | ||||||
|         """ |  | ||||||
|         # 只读方法列表 |  | ||||||
|         read_only_methods = ['GET', 'HEAD', 'OPTIONS'] |  | ||||||
|          |  | ||||||
|         if self.request.method not in read_only_methods and connection.in_atomic_block: |  | ||||||
|             if force_lock: |  | ||||||
|                 raise ParseError("当前操作需要在事务中进行,请使用事务装饰器") |  | ||||||
|             # 非只读请求且在事务中:加锁查询 |  | ||||||
|             queryset = self.filter_queryset(self.get_queryset()) |  | ||||||
|             lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field |  | ||||||
|             filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} |  | ||||||
|              |  | ||||||
|             try: |  | ||||||
|                 obj = queryset.get(**filter_kwargs) |  | ||||||
|                 l_obj = queryset.model.objects.select_for_update().get(pk=obj.pk) |  | ||||||
|                 self.check_object_permissions(self.request, l_obj) |  | ||||||
|                 return l_obj |  | ||||||
|                          |  | ||||||
|             except queryset.model.DoesNotExist: |  | ||||||
|                 raise Http404 |  | ||||||
|         else: |  | ||||||
|             # 其他情况:普通查询 |  | ||||||
|             return super().get_object() |  | ||||||
|      |  | ||||||
|     def get_serializer_class(self): |     def get_serializer_class(self): | ||||||
|         action_serializer_name = f"{self.action}_serializer_class" |         action_serializer_name = f"{self.action}_serializer_class" | ||||||
|         action_serializer_class = getattr(self, action_serializer_name, None) |         action_serializer_class = getattr(self, action_serializer_name, None) | ||||||
|  | @ -226,3 +194,4 @@ class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListM | ||||||
|     """ |     """ | ||||||
|     增强的ModelViewSet |     增强的ModelViewSet | ||||||
|     """ |     """ | ||||||
|  |     pass | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| # Generated by Django 3.2.12 on 2025-09-19 01:08 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations, models |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('wf', '0002_alter_state_filter_dept'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='workflow', |  | ||||||
|             name='view_path', |  | ||||||
|             field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| # Generated by Django 3.2.12 on 2025-10-16 08:29 |  | ||||||
| 
 |  | ||||||
| from django.db import migrations, models |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| class Migration(migrations.Migration): |  | ||||||
| 
 |  | ||||||
|     dependencies = [ |  | ||||||
|         ('wf', '0003_workflow_view_path'), |  | ||||||
|     ] |  | ||||||
| 
 |  | ||||||
|     operations = [ |  | ||||||
|         migrations.AddField( |  | ||||||
|             model_name='workflow', |  | ||||||
|             name='view_path2', |  | ||||||
|             field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径2'), |  | ||||||
|         ), |  | ||||||
|     ] |  | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue