Compare commits

...

106 Commits

Author SHA1 Message Date
zty 388e225108 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-21 14:10:44 +08:00
zty 25ee92602b add : ofm-create-model-patent 新建专利申请 curd 2025-10-21 14:10:43 +08:00
caoqianming 241df0beca feat: mio 添加查询条件 2025-10-20 16:33:46 +08:00
caoqianming 5ea7980a1b feat: mloguser添加字段 2025-10-20 10:09:38 +08:00
caoqianming a3416cfc0d feat: 修改mroombooking表约束 2025-10-17 14:51:13 +08:00
caoqianming 2ca47b8949 feat: base 获取流转时排序按attribute_type倒序 2025-10-17 12:32:18 +08:00
caoqianming 10792d090c Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 16:30:15 +08:00
caoqianming bfc8454ac7 feat: base workflow添加view_path2字段 2025-10-16 16:30:14 +08:00
zty 02b14ec2c6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 11:18:26 +08:00
zty acb4c802e4 feat: ofm-services 修改绑定反存的接口 2025-10-16 11:18:25 +08:00
caoqianming 260c9893eb feat: base 获取流转时排序按attribute_type2 2025-10-16 10:55:33 +08:00
caoqianming 666a9c169c Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-16 10:05:13 +08:00
caoqianming b869521221 feat: base 获取流转时排序按attribute_type 2025-10-16 10:05:12 +08:00
zty 99d8144bdf feat: ofm-views 修改文件列表排序 2025-10-15 15:09:09 +08:00
zty 8a87ba356e feat: ofm-BorrowRecord - 档案借阅反存ticket-data 2025-10-15 14:10:33 +08:00
zty 830bf18132 feat: ofm-service-vehicle 用车管理反存 实际归还里程树 2025-10-14 16:08:16 +08:00
zty b52e90a11f Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 14:58:11 +08:00
zty ee67e6896a feat: ofm-service 反向存储ticket_data 到 Lendingseal 2025-10-14 14:58:11 +08:00
caoqianming 54f8b82c98 feat: mio增加筛选条件inout_date 2025-10-14 14:18:27 +08:00
caoqianming 6125139fbf Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 13:46:53 +08:00
caoqianming 2169bbea68 feat: do_out和do_in在处理into_wm不应直接忽略 2025-10-14 13:46:53 +08:00
zty 6dfab46b4d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-14 10:21:52 +08:00
zty 477976f86c feat: ofm-views 修改时间倒叙 2025-10-14 10:21:50 +08:00
caoqianming c37ff77eda Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-13 15:45:04 +08:00
caoqianming 1aa7d51769 feat: material添加create_time的排序条件 2025-10-13 15:45:04 +08:00
zty e06cc8c38e fix : ofm-models-publicity 修改记录编号自动生成 2025-10-13 14:26:40 +08:00
zty b93024ca44 fix : ofm-models-publicity 修改记录编号自动生成 2025-10-13 14:18:03 +08:00
zty 7d87c79dd1 feat: ofm-service-pulicity 反存ticket_data 到 obj 2025-10-13 14:04:40 +08:00
zty 9f030ece6d feat: ofm-service-pulicity 反存ticket_data 到 obj 2025-10-13 13:50:04 +08:00
zty 67f9cbb700 feat: ofm--Alter field publicity_opinion on publicity 2025-10-13 09:01:44 +08:00
zty 8fe2b8ca48 feat: ofm-models 修改字段属性 2025-10-11 14:28:09 +08:00
zty 7732ddc88e Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-11 11:32:06 +08:00
zty 6713693c6c feat: ofm-models -pulicity 修改字段属性 2025-10-11 11:32:05 +08:00
caoqianming 20604ef7cb fix: work_start_time 对光子的兼容 2025-10-11 10:28:07 +08:00
caoqianming eb2deb02c2 fix: work_start_time 可不填2 2025-10-11 10:22:19 +08:00
caoqianming f5f6c136d9 fix: work_start_time 可不填 2025-10-11 10:21:22 +08:00
caoqianming 8eee09678a fix: handle_date和shift可传 2025-10-11 10:16:13 +08:00
caoqianming efd40d1d32 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-11 10:11:19 +08:00
caoqianming 3ab9682b07 fix: work_start_time 可不填 2025-10-11 10:11:18 +08:00
zty 1983f7b121 feat: 修改 ofm-moedels - pulicity字段为空值 2025-10-11 09:23:33 +08:00
zty cec6837d00 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-10-10 16:32:21 +08:00
zty e214c6115a feat: ofm-models 增加字段 2025-10-10 16:32:20 +08:00
caoqianming 6fb415a9f0 release: 2.8.2025101011 2025-10-10 11:38:43 +08:00
caoqianming 727d190610 feat: Ptest val_xj可为空 2025-10-10 11:12:59 +08:00
caoqianming a6e0fb4f0d feat: quick调用serializer时传入request 2025-10-09 14:44:26 +08:00
caoqianming 9cf900d2ef doc: 添加一些注释 2025-10-09 11:17:14 +08:00
caoqianming 69e8e7b025 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-30 11:05:25 +08:00
caoqianming 647603f986 fix: p_create_after 自动创建mlogbw时关于exclude语句导致的查询错误 2025-09-30 11:05:24 +08:00
zty d21c1dc55d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-30 09:58:56 +08:00
zty 7f206bd0a7 feat: ofm-修改model 2025-09-30 09:58:53 +08:00
caoqianming bd763be83a feat: Mroombooking添加字段 2025-09-29 15:53:34 +08:00
caoqianming fe499ffac5 feat: base add_info_for_item 可复用list逻辑 2025-09-29 15:52:54 +08:00
caoqianming 0c1e93bf0b feat: 添加wpr查询参数 2025-09-29 14:50:02 +08:00
caoqianming 5d3c4137fe feat: base cquery支持annotate 2025-09-29 14:44:08 +08:00
caoqianming dbaf121685 feat: mroombooking 返回slots 2025-09-29 11:09:09 +08:00
caoqianming fe524e389c release: 2.8.2025092816 2025-09-28 16:51:44 +08:00
caoqianming bf8b886a2d feat: handover revert撤回时做校验2 2025-09-28 16:34:30 +08:00
caoqianming 7b8ec7f9d6 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-28 16:33:40 +08:00
caoqianming 7620122c2d feat: handover revert撤回时做校验 2025-09-28 16:33:39 +08:00
zty de99f85259 feat: ofm-models 修改车辆表的字段信息 2025-09-28 14:16:39 +08:00
zty e7c121de15 feat:iofm-serializer 增加ticket_ 2025-09-28 11:07:08 +08:00
caoqianming bdd686f50b feat: 会议室预定修改 2025-09-28 10:24:29 +08:00
caoqianming 54140ba742 fix: mroombooking 创建时未create_by 2025-09-26 16:26:04 +08:00
caoqianming c186b7d296 feat: p_create_after 优化 2025-09-26 15:14:03 +08:00
caoqianming 3a03cd76ff feat: p_create_after 优化 2025-09-26 14:58:03 +08:00
caoqianming f9c9a592e3 fix: mlogbinserializer 2025-09-26 14:30:51 +08:00
caoqianming 419b52f9be feat: p_create_after 可报当前产品都不可使用 2025-09-26 14:19:11 +08:00
caoqianming fb71f0697a feat: mlog quick增加wprs_in传参 2025-09-26 14:07:14 +08:00
caoqianming 34e217e468 feat: mlog quick跳过创建mlogbw2 2025-09-26 10:46:04 +08:00
caoqianming f34356057d feat: mlog quick跳过创建mlogbw 2025-09-26 10:33:25 +08:00
caoqianming b35015b58d Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-26 09:11:00 +08:00
caoqianming c434c76605 feat: 车间领料时完善提示添加物料名 2025-09-26 09:10:59 +08:00
zty 9909596cdb Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-25 15:42:45 +08:00
zty 6f4a1c4c88 feat: ofm-修改会议室信息 2025-09-25 15:42:24 +08:00
caoqianming 2445ae53f1 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-25 09:11:00 +08:00
caoqianming 0fc57a454f feat: 改版需提供新批次号 2025-09-25 09:11:00 +08:00
zty d26e066769 feat: ofm 修改publicity 的model 2025-09-24 14:08:51 +08:00
zty f347fa0d23 feat: 修改ofm-services.py 2025-09-24 14:04:06 +08:00
zty 362a5ef725 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-24 13:59:57 +08:00
zty ad98280458 add : ofm-新增宣传报道保密模块 2025-09-24 13:59:55 +08:00
caoqianming 3c0fa9f244 feat: base send_sms auto_log send_mail使用False 2025-09-23 16:26:07 +08:00
caoqianming b520b69f95 feat: 优化bind_routepack 2025-09-23 15:08:22 +08:00
caoqianming 899a314f5d feat: base 优化wf通知发送 2025-09-23 14:59:50 +08:00
caoqianming dbb0d6ae75 feat: routepack_ticket_change 变为创建中状态 2025-09-23 13:34:34 +08:00
caoqianming bc4953893b Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-22 11:31:22 +08:00
caoqianming a8722724e1 feat: route增加查询条件以支持获取后续工段的信息 2025-09-22 11:31:21 +08:00
caoqianming 1c447a86ef feat: 导入物料优化一下 2025-09-20 14:14:26 +08:00
caoqianming 820f814766 feat: 优化的mplogxview 2025-09-19 13:02:31 +08:00
caoqianming 1689683aa3 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-19 10:52:23 +08:00
caoqianming cdb201a0ce feat: base sql querydict可传入是否格式化时间参数 2025-09-19 10:52:22 +08:00
zty d8ad57fa7e Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-19 09:22:49 +08:00
zty e32d4b816b add: ofm-增加宣传保密model 2025-09-19 09:22:47 +08:00
caoqianming 47ca272cee feat: base workflow list 返回view_path 2025-09-19 09:20:02 +08:00
caoqianming 4390992a14 feat: base workflow添加view_path 2025-09-19 09:09:44 +08:00
caoqianming 085978a6c4 fix: 优化handover list接口 2025-09-18 15:06:45 +08:00
caoqianming f6668b5d38 feat: 优化handover list接口 2025-09-18 14:48:28 +08:00
caoqianming f6720ad7ce feat: 玻纤拉丝采集问题 2025-09-18 13:20:55 +08:00
caoqianming e5a9f77f3d feat: ana_batch_thread 优化一下 2025-09-18 10:44:23 +08:00
caoqianming c2deb0ee45 feat: base 优化system事务处理 2025-09-17 12:51:20 +08:00
caoqianming 94f87df707 feat: 多个app优化事务处理 2025-09-17 12:50:19 +08:00
caoqianming 694dca27cc feat: base system和wf优化事务处理2 2025-09-17 12:23:08 +08:00
caoqianming 0284809933 feat: base system和wf优化事务处理 2025-09-17 12:15:26 +08:00
caoqianming f60250bb21 Merge branch 'master' of http://gitea.xxhhcty.xyz:8080/zcdsj/factory 2025-09-17 11:15:17 +08:00
caoqianming 0b4524aa85 feat: 优化cd.py 2025-09-17 11:15:15 +08:00
caoqianming f7e27c290f feat: wpr list 返回process_name 2025-09-17 11:14:49 +08:00
caoqianming 2c9c74131c feat: 优化cd.py 2025-09-16 18:51:38 +08:00
62 changed files with 1917 additions and 845 deletions

View File

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

View File

@ -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__lte=600).order_by("start_time").last() st = st_qs.filter(start_time__gte=dt_start, start_time__lte=dt_end, duration_sec__gte=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__lte=600).order_by("end_time").first() st = st_qs.filter(end_time__gte=dt_start, end_time__lte=dt_end, duration_sec__gte=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 == 10: if mpointfrom and mpoint.cal_related_mgroup_running == 20:
stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n) stlog, xtype = get_first_stlog_time_from_duration(mpoint.mgroup, dt, dt_hour_n)

View File

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

View File

@ -12,6 +12,7 @@ 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
@ -21,7 +22,13 @@ 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:测点
@ -166,6 +173,97 @@ class XscriptViewSet(CustomModelViewSet):
# select_related_fields = ['mpoint'] # select_related_fields = ['mpoint']
# filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept'] # filterset_fields = ['mpoint', 'mpoint__mgroup', 'mpoint__mgroup__belong_dept']
class MpLogxAPIView(APIView):
"""
list:测点采集数据
测点采集数据
"""
perms_map = {"get": "*"}
@swagger_auto_schema(manual_parameters=[
openapi.Parameter('mpoint', openapi.IN_QUERY, description='测点ID', type=openapi.TYPE_STRING),
openapi.Parameter('timex__gte', openapi.IN_QUERY, description='开始时间', type=openapi.TYPE_STRING),
openapi.Parameter('timex__lte', openapi.IN_QUERY, description='结束时间', type=openapi.TYPE_STRING),
openapi.Parameter('page', openapi.IN_QUERY, description='页码', type=openapi.TYPE_INTEGER),
openapi.Parameter('page_size', openapi.IN_QUERY, description='每页数量', type=openapi.TYPE_INTEGER),
openapi.Parameter('ordering', openapi.IN_QUERY, description='排序字段,如 -timex', type=openapi.TYPE_STRING),
openapi.Parameter('fields', openapi.IN_QUERY, description='返回字段,如 timex,val_float,val_int', type=openapi.TYPE_STRING),
])
def get(self, request, *args, **kwargs):
mpoint = request.query_params.get("mpoint", None)
timex__gte_str = request.query_params.get("timex__gte", None)
timex__lte_str = request.query_params.get("timex__lte", None)
page = int(request.query_params.get("page", 1))
page_size = int(request.query_params.get("page_size", 20))
fields = request.query_params.get("fields", None)
if page < 0 and page_size < 0:
raise ParseError("page, page_size must be positive")
ordering = request.query_params.get("ordering", "-timex") # 默认倒序
if mpoint is None or timex__gte_str is None:
raise ParseError("mpoint, timex__gte are required")
# 处理时间
timex__gte = timezone.make_aware(datetime.strptime(timex__gte_str, "%Y-%m-%d %H:%M:%S"))
timex__lte = timezone.make_aware(datetime.strptime(timex__lte_str, "%Y-%m-%d %H:%M:%S")) if timex__lte_str else timezone.now()
# 统计总数
count_sql = """SELECT COUNT(*) AS total_count FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s"""
count_data = query_one_dict(count_sql, [mpoint, timex__gte, timex__lte], with_time_format=True)
# 排序白名单
allowed_fields = {"timex", "val_mrs", "val_int", "val_float"} # 根据表字段修改
order_fields = []
for field in ordering.split(","):
field = field.strip()
if not field:
continue
desc = field.startswith("-")
field_name = field[1:] if desc else field
if field_name in allowed_fields:
order_fields.append(f"{field_name} {'DESC' if desc else 'ASC'}")
# 如果没有合法字段,使用默认排序
if not order_fields:
order_fields = ["timex DESC"]
order_clause = "ORDER BY " + ", ".join(order_fields)
# 构造 SQL
if page == 0:
if fields:
# 过滤白名单,避免非法列
fields = [f for f in fields.split(",") if f in allowed_fields]
if not fields:
fields = ["timex", "val_float", "val_int"] # 默认列
select_clause = ", ".join(fields)
else:
select_clause = "timex, val_float, val_int" # 默认列
page_sql = f"""SELECT {select_clause} FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause}"""
page_params = [mpoint, timex__gte, timex__lte]
else:
page_sql = f"""SELECT * FROM enm_mplogx
WHERE mpoint_id=%s AND timex >= %s AND timex <= %s
{order_clause} LIMIT %s OFFSET %s"""
page_params = [mpoint, timex__gte, timex__lte, page_size, (page-1)*page_size]
page_data = query_all_dict(page_sql, page_params, with_time_format=True)
if page == 0:
return Response(page_data)
return Response({
"count": count_data["total_count"],
"page": page,
"page_size": page_size,
"results": page_data
})
class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet): class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
""" """

View File

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

View File

@ -45,7 +45,6 @@ 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:
@ -58,7 +57,6 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
mioitemw = self.save_ftest(mioitemw, ftest_data) mioitemw = self.save_ftest(mioitemw, ftest_data)
return mioitemw return mioitemw
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop("mioitem") validated_data.pop("mioitem")
ftest_data = validated_data.pop("ftest", None) ftest_data = validated_data.pop("ftest", None)

View File

@ -10,7 +10,7 @@ from apps.wpmw.models import Wpr
from apps.qm.models import Ftest, Defect from apps.qm.models import Ftest, Defect
from django.db.models import Count, Q from django.db.models import Count, Q
def do_out(item: MIOItem): def do_out(item: MIOItem, is_reverse: bool = False):
""" """
生产领料到车间 生产领料到车间
""" """
@ -23,8 +23,6 @@ def do_out(item: MIOItem):
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
@ -94,26 +92,28 @@ def do_out(item: MIOItem):
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}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
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("出入库与明细数量不一致,操作失败")
@ -141,8 +141,7 @@ 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 # 是否组合件入仓库
@ -177,38 +176,39 @@ def do_in(item: MIOItem):
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
xbatchs.append(xbatch) xbatchs.append(xbatch)
if material.into_wm:
wm_qs = WMaterial.objects.filter( wm_qs = WMaterial.objects.filter(
batch=xbatch, batch=xbatch,
material=xmaterial, material=xmaterial,
belong_dept=belong_dept, belong_dept=belong_dept,
mgroup=mgroup, mgroup=mgroup,
defect=defect, defect=defect,
state=WMaterial.WM_OK) state=WMaterial.WM_OK)
count_x = wm_qs.count() count_x = wm_qs.count()
if count_x == 1: if count_x == 1:
wm = wm_qs.first() wm = wm_qs.first()
elif count_x == 0: elif count_x == 0:
raise ParseError( raise ParseError(
f'{str(xmaterial)}-{xbatch}-批次库存不存在!') f'{str(xmaterial)}-{xbatch}-批次库存不存在!')
else: else:
raise ParseError( raise ParseError(
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!') f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
wm.update_by = do_user
wm.save()
else:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不足')
wm_production_dept = wm.mgroup.belong_dept if wm.mgroup else wm.belong_dept
if production_dept is None:
production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 增加mb # 增加mb
if not is_zhj: if not is_zhj:
mb, _ = MaterialBatch.objects.get_or_create( mb, _ = MaterialBatch.objects.get_or_create(
@ -231,6 +231,8 @@ 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("出入库与明细数量不一致,操作失败")
@ -440,7 +442,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}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
else: else:
mb.save() mb.save()
if tracking == Material.MA_TRACKING_SINGLE: if tracking == Material.MA_TRACKING_SINGLE:

View File

@ -1,6 +1,7 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from apps.mtm.models import Goal, Material, Route from apps.mtm.models import Goal, Material, Route, RoutePack
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):
@ -45,6 +46,8 @@ 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,3 +64,15 @@ class RouteFilter(filters.FilterSet):
"mgroup__belong_dept__name": ["exact", "contains"], "mgroup__belong_dept__name": ["exact", "contains"],
"from_route": ["exact", "isnull"], "from_route": ["exact", "isnull"],
} }
def filter_nprocess_name(self, queryset, name, value):
return queryset
def filter_material_in_has(self, queryset, name, value):
nprocess_name = self.data.get('nprocess_name', None)
if nprocess_name:
routepack_qs = queryset.filter(material_in__id=value, routepack__isnull=False, routepack__state=RoutePack.RP_S_CONFIRM).values_list('routepack', flat=True)
qs = queryset.filter(routepack__in=routepack_qs, process__name=nprocess_name)
return qs
raise ParseError("nprocess_name is required")

View File

@ -246,34 +246,34 @@ 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:Route = 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()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx:
instance.from_route = rx
instance.save() instance.save()
# msg = "" rx = Route.objects.filter(
# if rx.routepack: material_in=instance.material_in, material_out=instance.material_out,
# msg = rx.routepack.name process=process).exclude(id=instance.id).order_by("create_time").first()
# raise ParseError(f"该工艺步骤已存在-{msg}") if rx:
return instance instance.from_route = rx
instance.save()
# msg = ""
# 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 +281,34 @@ 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()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx:
instance.from_route = rx
instance.save() instance.save()
# msg = "" rx = Route.objects.filter(
# if rx.routepack: material_in=instance.material_in, material_out=instance.material_out,
# msg = rx.routepack.name process=process).exclude(id=instance.id).order_by("create_time").first()
# raise ParseError(f"该工艺步骤已存在-{msg}") if rx:
return instance instance.from_route = rx
instance.save()
# msg = ""
# 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)

View File

@ -58,7 +58,7 @@ def daoru_material(path: str):
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: while sheet[f'b{i}'].value is not None or sheet[f'd{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]
@ -171,8 +171,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 +182,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 == Ticket.TICKET_ACT_STATE_DRAFT: if ticket.act_state in [Ticket.TICKET_ACT_STATE_DRAFT, Ticket.TICKET_ACT_STATE_BACK, Ticket.TICKET_ACT_STATE_RETREAT]:
routepack.state = RoutePack.RP_S_CREATE routepack.state = RoutePack.RP_S_CREATE
routepack.save() routepack.save()

View File

@ -39,9 +39,8 @@ class MaterialViewSet(CustomModelViewSet):
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'] 'type', 'process', 'process__sort', 'sort', 'id', 'number', 'create_time']
@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():
@ -368,9 +367,8 @@ 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']
@transaction.atomic
def perform_update(self, serializer): def perform_update(self, serializer):
ins:Route = self.get_object() ins:Route = serializer.instance
if ins.from_route is not None: if ins.from_route is not None:
raise ParseError('该工艺步骤引用其他步骤, 无法编辑') raise ParseError('该工艺步骤引用其他步骤, 无法编辑')
old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
from django.db import models from django.db import models, transaction
from apps.utils.models import CommonADModel, BaseModel, CommonBDModel from apps.utils.models import CommonADModel, BaseModel, CommonBDModel
from apps.system.models import User from apps.system.models import User
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from datetime import datetime
from rest_framework.exceptions import ParseError
# Create your models here. # Create your models here.
@ -24,9 +26,15 @@ class Mroom(CommonADModel):
location = models.CharField('位置', max_length=100) location = models.CharField('位置', max_length=100)
capacity = models.PositiveIntegerField('容纳人数') capacity = models.PositiveIntegerField('容纳人数')
class MroomBooking(CommonADModel): class MroomBooking(CommonBDModel):
"""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):
@ -35,9 +43,7 @@ class MroomSlot(BaseModel):
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b") booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
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:
unique_together = ('mroom', 'mdate', 'slot')
# class Seal(BaseModel): # class Seal(BaseModel):
@ -49,6 +55,7 @@ class LendingSeal(CommonBDModel):
"""TN: 印章外出用印信息""" """TN: 印章外出用印信息"""
seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]') seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]')
seal_other = models.CharField('其他印章', max_length=50, blank=True, null=True)
filename = models.TextField('文件名称') filename = models.TextField('文件名称')
file = models.TextField('文件内容') file = models.TextField('文件内容')
file_count = models.PositiveIntegerField('用印份数') file_count = models.PositiveIntegerField('用印份数')
@ -71,15 +78,20 @@ class Vehicle(CommonBDModel):
via = 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) destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
start_km = models.PositiveIntegerField('出发公里数') start_km = models.PositiveIntegerField('出发公里数')
end_km = models.PositiveIntegerField('归还公里数') end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True)
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False) actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
is_city = models.BooleanField('是否市内用车', default=True) is_city = models.BooleanField('是否市内用车', default=True)
reason = models.CharField('用车事由', max_length=100) reason = models.CharField('用车事由', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单', ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False) on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.end_km and self.start_km: if self.end_km:
self.actual_km = self.end_km - self.start_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) return super().save(*args, **kwargs)
@ -105,64 +117,75 @@ class BorrowRecord(CommonBDModel):
on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False) on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False)
# class Publicity(CommonBDModel): class Publicity(CommonBDModel):
# """TN: 公示栏""" """TN: 公示栏"""
# CHANNEL_CHOICES = [ number = models.CharField('记录编号', max_length=50, blank=True, null=True)
# ('internet', '互联网'), title = models.CharField('送审稿件标题', max_length=100)
# ('platform', '信息平台'), participants = models.CharField('所有撰稿人', max_length=50)
# ('official', '官微'), pub_dept = models.CharField('部室/研究院', null=True, blank=True, max_length=50)
# ('publication', '公开发行物'), pfile = models.CharField('稿件路径', null=True, blank=True, max_length=100)
# ('other', '其它'), level = models.JSONField('涉密等级', default=list, help_text=['重要', '一般', '非涉密'])
# ] content = models.JSONField('稿件内容涉及', default=list, help_text=[
# SECRET_LEVELS = ( "武器装备科研生产综合事项",
# (0, '重要'), "其它"
# (1, '一般'), ])
# (2, '非涉密'), other_content = models.CharField('其它内容', max_length=100, blank=True, null=True)
# ) report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True)
# CONTENT_CHOICES = [ channel = models.JSONField('发布渠道', default=list, help_text=['互联网', '信息平台', '官微', '公开发行物', '其它'])
# ('device', '武器装备科研生产综合事项'), other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True)
# ('other', '其他'), report_name = models.CharField('报道名称', max_length=50, blank=True, null=True)
# ] review = models.JSONField('第一撰稿人自审', default=list, help_text=['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布'], null=True,blank=True)
# number = models.CharField('记录编号', max_length=50) dept_opinion = models.JSONField('部门负责人意见', default=list, help_text=['同意', '不同意'], null=True, blank=True)
# title = models.CharField('送审稿件标题', max_length=100) secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
# participants = models.CharField('所有撰稿人', max_length=50) dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
# level = models.PositiveBigIntegerField('涉密等级', choices=SECRET_LEVELS, default=2, help_text=str(SECRET_LEVELS)) publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
# content = models.CharField('稿件内容涉及', max_length=50, choices=CONTENT_CHOICES, default='device', blank=True) ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
# report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True) on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
# channel = models.CharField('发布渠道', max_length=50, choices=CHANNEL_CHOICES, default='platform')
# other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True) # 记录编号自动生成
# review = models.CharField('第一撰稿人自审', max_length=100, blank=True, null=True) def save(self, *args, **kwargs):
# dept_opinion = models.CharField('部门负责人意见', max_length=100, blank=True, null=True) if not self.number:
# manager_opinion = models.CharField('总经理', max_length=100, blank=True, null=True) 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): class PatentInfo(CommonBDModel):
# """TN: 专利申密审批表单样式""" """TN: 专利申密审批表单样式"""
# PATENT_TYPE_CHOICES = ( PATENT_TYPE_CHOICES = (
# ('invention', '发明专利'), ('invention', '发明专利'),
# ('utility', '实用新型专利'), ('utility', '实用新型专利'),
# ('design', '外观设计专利'), ('design', '外观设计专利'),
# ) )
# APPLY_AREAS = ( APPLY_AREAS = (
# ('Domestic', '国内申请'), ('Domestic', '国内申请'),
# ('Foreign', '国外申请'), ('Foreign', '国外申请'),
# (' PCT', 'PCT申请'), (' PCT', 'PCT申请'),
# ) )
# name = models.CharField('拟申请专利名称', max_length=100) name = models.CharField('拟申请专利名称', max_length=100)
# author = models.CharField('发明人(设计人)', max_length=100) author = models.CharField('发明人(设计人)', max_length=100)
# type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention') type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention')
# is_public = models.BooleanField('是否公开', default=False) is_public = models.BooleanField('是否公开', default=False)
# area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic') area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic')
# identified = models.BooleanField('是否进行过科技成果鉴定', default=False) identified = models.BooleanField('是否进行过科技成果鉴定', default=False)
# published_article = models.BooleanField('是否发表过文章', default=False) published_article = models.BooleanField('是否发表过文章', default=False)
# exhibited = models.BooleanField('是否参与过展会展出', default=False) exhibited = models.BooleanField('是否参与过展会展出', default=False)
# applied_to_production = models.BooleanField('是否参与应用于生产/销售', default=False) applied_to_production = models.BooleanField('是否参与应用于生产/销售', default=False)
# participated_in_exchange = models.BooleanField('是否参与过技术交流', default=False) participated_in_exchange = models.BooleanField('是否参与过技术交流', default=False)
# tech_background_pages = models.PositiveIntegerField('技术背景材料页数', null=True, blank=True) tech_background_pages = models.PositiveIntegerField('技术背景材料页数', null=True, blank=True)
# tech_disclosure_pages = models.PositiveIntegerField('技术交底材料页数', null=True, blank=True) tech_disclosure_pages = models.PositiveIntegerField('技术交底材料页数', null=True, blank=True)
# novelty_report_pages = models.PositiveIntegerField('查新检索报告页数', null=True, blank=True) novelty_report_pages = models.PositiveIntegerField('查新检索报告页数', null=True, blank=True)
# diagrams_or_photos_pages = models.PositiveIntegerField('图/照片页数或张数', null=True, blank=True) diagrams_or_photos_pages = models.PositiveIntegerField('图/照片页数或张数', null=True, blank=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='patentInfo_ticket', null=True, blank=True, db_constraint=False)
# class PaperOfm(CommonADModel): # class PaperOfm(CommonADModel):
# """TN: 论文申密审批表单""" # """TN: 论文申密审批表单"""

View File

@ -1,11 +1,11 @@
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord ) from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo)
# , Publicity, # Publicity, PatetInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
# Publicity, PatentInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from apps.utils.serializers import CustomModelSerializer from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers from rest_framework import serializers
from django.db import transaction from django.db import transaction
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.utils.constants import EXCLUDE_FIELDS from apps.utils.constants import EXCLUDE_FIELDS
from apps.wf.serializers import TicketSimpleSerializer
class MroomSerializer(CustomModelSerializer): class MroomSerializer(CustomModelSerializer):
@ -19,28 +19,30 @@ class MroomBookingSerializer(CustomModelSerializer):
mdate = serializers.DateField(write_only=True, label="预订日期") mdate = serializers.DateField(write_only=True, label="预订日期")
slots = serializers.ListField(child=serializers.IntegerField(), 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_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: class Meta:
model = MroomBooking model = MroomBooking
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS read_only_fields = EXCLUDE_FIELDS
extra_kwargs = {'belong_dept': {'required': True}}
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
mroom = validated_data.pop('mroom') mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots') slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate') mdate = validated_data.pop('mdate')
booking = MroomBooking.objects.create(**validated_data) booking = super().create(validated_data)
MroomSlot.objects.filter(booking=booking).delete() MroomSlot.objects.filter(booking=booking).delete()
for slot in slots: for slot in slots:
if slot < 0 or slot > 47: if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围") raise ParseError("时段索引超出范围")
try: ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom) if ms_exists:
except Exception as e: raise ParseError("时段已预订,请刷新重选")
raise ParseError(f"时段已预订,请刷新重选-{e}") MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking return booking
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
mroom = validated_data.pop('mroom') mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots') slots = validated_data.pop('slots')
@ -50,10 +52,10 @@ class MroomBookingSerializer(CustomModelSerializer):
for slot in slots: for slot in slots:
if slot < 0 or slot > 47: if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围") raise ParseError("时段索引超出范围")
try: ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom) if ms_exists:
except Exception as e: raise ParseError("时段已预订,请刷新重选")
raise ParseError(f"时段已预订,请刷新重选-{e}") MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking return booking
@ -67,6 +69,7 @@ class MroomSlotSerializer(CustomModelSerializer):
class LendingSealSerializer(CustomModelSerializer): class LendingSealSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True) create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.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: class Meta:
model = LendingSeal model = LendingSeal
fields = '__all__' fields = '__all__'
@ -76,6 +79,7 @@ class LendingSealSerializer(CustomModelSerializer):
class VehicleSerializer(CustomModelSerializer): class VehicleSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True) create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.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: class Meta:
model = Vehicle model = Vehicle
fields = '__all__' fields = '__all__'
@ -85,6 +89,7 @@ class VehicleSerializer(CustomModelSerializer):
class FileRecordSerializer(CustomModelSerializer): class FileRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True) create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.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: class Meta:
model = FileRecord model = FileRecord
fields = '__all__' fields = '__all__'
@ -97,6 +102,7 @@ class BorrowRecordSerializer(CustomModelSerializer):
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件") 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_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
file_name = serializers.SerializerMethodField() file_name = serializers.SerializerMethodField()
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta: class Meta:
model = BorrowRecord model = BorrowRecord
fields = '__all__' fields = '__all__'
@ -106,18 +112,21 @@ class BorrowRecordSerializer(CustomModelSerializer):
return [file.name for file in obj.borrow_file.all()] return [file.name for file in obj.borrow_file.all()]
# class PublicitySerializer(CustomModelSerializer): class PublicitySerializer(CustomModelSerializer):
# class Meta: create_by_name = serializers.CharField(source='create_by.name', read_only=True)
# model = Publicity belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
# fields = '__all__' ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
# read_only_fields = EXCLUDE_FIELDS class Meta:
model = Publicity
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
# class PatentInfoSerializer(CustomModelSerializer): class PatentInfoSerializer(CustomModelSerializer):
# class Meta: class Meta:
# model = PatentInfo model = PatentInfo
# fields = '__all__' fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS read_only_fields = EXCLUDE_FIELDS
# class PaperSerializer(CustomModelSerializer): # class PaperSerializer(CustomModelSerializer):

View File

@ -1,7 +1,7 @@
from apps.wf.models import Ticket from apps.wf.models import Ticket
# TicketFlow, Transition, Workflow, CustomField, State, # TicketFlow, Transition, Workflow, CustomField, State,
from apps.ofm.models import LendingSeal, Vehicle, BorrowRecord from apps.ofm.models import LendingSeal, Vehicle, BorrowRecord, Publicity, MroomBooking, MroomSlot, PatentInfo
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
@ -10,9 +10,28 @@ def seal_submit_validate(ins: LendingSeal):
raise ParseError('该日志已提交!') raise ParseError('该日志已提交!')
if ins.mtask and ins.mtask.state == LendingSeal.MTASK_STOP: if ins.mtask and ins.mtask.state == LendingSeal.MTASK_STOP:
raise ParseError('该任务已停止!') 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): def bind_lendingseal(ticket: Ticket, transition, new_ticket_data: dict):
ins = LendingSeal.objects.get(id=new_ticket_data['t_id']) ins = LendingSeal.objects.get(id=new_ticket_data['t_id'])
ins.actual_return_date = None
ticket_data = ticket.ticket_data ticket_data = ticket.ticket_data
ticket_data.update({ ticket_data.update({
't_model': 'LendingSeal', 't_model': 'LendingSeal',
@ -24,6 +43,19 @@ def bind_lendingseal(ticket: Ticket, transition, new_ticket_data: dict):
if ins.ticket is None: if ins.ticket is None:
ins.ticket = ticket ins.ticket = ticket
ins.save() 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): def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
ins = Vehicle.objects.get(id=new_ticket_data['t_id']) ins = Vehicle.objects.get(id=new_ticket_data['t_id'])
@ -32,6 +64,8 @@ def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
't_model': 'Vehicle', 't_model': 'Vehicle',
't_id': ins.id, 't_id': ins.id,
}) })
ins.actual_km = None
ins.end_time = None
ticket.ticket_data = ticket_data ticket.ticket_data = ticket_data
ticket.create_by = ins.create_by ticket.create_by = ins.create_by
ticket.save() ticket.save()
@ -39,6 +73,16 @@ def bind_vehicle(ticket: Ticket, transition, new_ticket_data: dict):
ins.ticket = ticket ins.ticket = ticket
ins.save() 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): def bind_file(ticket: Ticket, transition, new_ticket_data: dict):
ins = BorrowRecord.objects.get(id=new_ticket_data['t_id']) ins = BorrowRecord.objects.get(id=new_ticket_data['t_id'])
@ -47,6 +91,62 @@ def bind_file(ticket: Ticket, transition, new_ticket_data: dict):
't_model': 'BorrowRecord', 't_model': 'BorrowRecord',
't_id': ins.id, '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.ticket_data = ticket_data
ticket.create_by = ins.create_by ticket.create_by = ins.create_by
ticket.save() ticket.save()

View File

@ -1,8 +1,9 @@
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.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet, FileborrowViewSet) from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet,
FileborrowViewSet, PublicityViewSet, PatentInfoViewSet)
# SealModelViewSet, # SealModelViewSet,
# , PublicityViewSet, PatentInfoViewSet, PaperViewSet, PlatformViewSet, # , PublicityViewSet, , PaperViewSet, PlatformViewSet,
# ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet) # ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet)
API_BASE_URL = 'api/ofm/' API_BASE_URL = 'api/ofm/'
@ -17,9 +18,8 @@ router.register('lendingseal', LendingSealViewSet, basename='lendingseal')
router.register('vehicle', VehicleViewSet, basename='vehicle') router.register('vehicle', VehicleViewSet, basename='vehicle')
router.register('filerecord', FilerecordViewSet, basename='filerecord') router.register('filerecord', FilerecordViewSet, basename='filerecord')
router.register('fileborrow', FileborrowViewSet, basename='fileborrow') router.register('fileborrow', FileborrowViewSet, basename='fileborrow')
# router.register('publicity', PublicityViewSet, basename='publicity') router.register('publicity', PublicityViewSet, basename='publicity')
# router.register('patentinfo', PatentInfoViewSet, basename='patentinfo') router.register('patentinfo', PatentInfoViewSet, basename='patentinfo')
# router.register('paper', PaperViewSet, basename='paper') # router.register('paper', PaperViewSet, basename='paper')
# router.register('platform', PlatformViewSet, basename='platform') # router.register('platform', PlatformViewSet, basename='platform')
# router.register('project', ProjectViewSet, basename='project') # router.register('project', ProjectViewSet, basename='project')

View File

@ -1,9 +1,9 @@
from django.shortcuts import render from django.shortcuts import render
from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet
from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord from .models import Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo
# Seal # Publicity, , PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
# Publicity, PatentInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo) from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer,
from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer, VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer) VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, PatentInfoSerializer)
# ,SealSerializer, # ,SealSerializer,
# LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, # LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer,
# PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer) # PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer)
@ -29,7 +29,7 @@ class MroomBookingViewSet(CustomModelViewSet):
""" """
queryset = MroomBooking.objects.all() queryset = MroomBooking.objects.all()
serializer_class = MroomBookingSerializer serializer_class = MroomBookingSerializer
select_related_fields = ["create_by"] select_related_fields = ["create_by", "ticket", "belong_dept"]
filterset_class = MroomBookingFilterset filterset_class = MroomBookingFilterset
def add_info_for_list(self, data): def add_info_for_list(self, data):
@ -67,7 +67,7 @@ class MroomBookingViewSet(CustomModelViewSet):
start_time = self._slot_to_time(info["current_slots"][0]) start_time = self._slot_to_time(info["current_slots"][0])
end_time = self._slot_to_time(info["current_slots"][-1] + 1) end_time = self._slot_to_time(info["current_slots"][-1] + 1)
info["time_ranges"].append(f"{start_time}-{end_time}") info["time_ranges"].append(f"{start_time}-{end_time}")
info.pop("current_slots") # 清理临时数据 info["slots"] = info.pop("current_slots") # 清理临时数据
for item in data: for item in data:
item.update(booking_info.get(item["id"], {})) item.update(booking_info.get(item["id"], {}))
@ -82,14 +82,26 @@ class MroomBookingViewSet(CustomModelViewSet):
def perform_update(self, serializer): def perform_update(self, serializer):
ins:MroomBooking = self.get_object() ins:MroomBooking = self.get_object()
if ins.create_by != self.request.user: 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("只允许创建者修改") raise ParseError("只允许创建者修改")
return super().perform_update(serializer) return super().perform_update(serializer)
def perform_destroy(self, instance): def perform_destroy(self, instance):
if instance.create_by != self.request.user: if instance.create_by and instance.create_by != self.request.user:
raise ParseError("只允许创建者删除") raise ParseError("只允许创建者删除")
return super().perform_destroy(instance) 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): class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet):
@ -99,7 +111,7 @@ class MroomSlotViewSet(CustomListModelMixin, CustomGenericViewSet):
""" """
queryset = MroomSlot.objects.all() queryset = MroomSlot.objects.all()
serializer_class = MroomSlotSerializer serializer_class = MroomSlotSerializer
filterset_fields = ["mroom", "mdate", "booking"] filterset_fields = ["mroom", "mdate", "booking", "is_inuse"]
class LendingSealViewSet(CustomModelViewSet): class LendingSealViewSet(CustomModelViewSet):
@ -112,7 +124,7 @@ class LendingSealViewSet(CustomModelViewSet):
queryset = LendingSeal.objects.all() queryset = LendingSeal.objects.all()
serializer_class = LendingSealSerializer serializer_class = LendingSealSerializer
filterset_class = SealFilter filterset_class = SealFilter
ordering = ["create_time"] ordering = ["-create_time"]
data_filter = True data_filter = True
@ -123,7 +135,7 @@ class VehicleViewSet(CustomModelViewSet):
""" """
queryset = Vehicle.objects.all() queryset = Vehicle.objects.all()
serializer_class = VehicleSerializer serializer_class = VehicleSerializer
ordering = ["create_time"] ordering = ["-create_time"]
class FilerecordViewSet(CustomModelViewSet): class FilerecordViewSet(CustomModelViewSet):
@ -134,7 +146,7 @@ class FilerecordViewSet(CustomModelViewSet):
queryset = FileRecord.objects.all() queryset = FileRecord.objects.all()
serializer_class = FileRecordSerializer serializer_class = FileRecordSerializer
filterset_fields = [ "name", "number"] filterset_fields = [ "name", "number"]
ordering = ["create_time", "number", "name"] ordering = ["-create_time", "number", "name"]
class FileborrowViewSet(CustomModelViewSet): class FileborrowViewSet(CustomModelViewSet):
@ -145,29 +157,29 @@ class FileborrowViewSet(CustomModelViewSet):
queryset = BorrowRecord.objects.all() queryset = BorrowRecord.objects.all()
serializer_class = BorrowRecordSerializer serializer_class = BorrowRecordSerializer
filterset_class = BorrowRecordFilter filterset_class = BorrowRecordFilter
ordering = ["create_time"] ordering = ["-create_time"]
# class PublicityViewSet(CustomModelViewSet): class PublicityViewSet(CustomModelViewSet):
# """list: 公告 """list: 公告
# 公告 公告
# """ """
# queryset = Publicity.objects.all() queryset = Publicity.objects.all()
# serializer_class = PublicitySerializer serializer_class = PublicitySerializer
# filterset_fields = ["title","number"] filterset_fields = ["title","number"]
# ordering = ["create_time", "number"] ordering = ["-create_time", "number"]
# class PatentInfoViewSet(CustomModelViewSet): class PatentInfoViewSet(CustomModelViewSet):
# """list: 专利 """list: 专利
# 专利 专利
# """ """
# queryset = PatentInfo.objects.all() queryset = PatentInfo.objects.all()
# serializer_class = PatentInfoSerializer serializer_class = PatentInfoSerializer
# filterset_fields = ["name", "author", "type"] filterset_fields = ["name", "author", "type"]
# ordering = ["create_time", "name", "author", "type"] ordering = ["-create_time", "name", "author", "type"]
# class PaperViewSet(CustomModelViewSet): # class PaperViewSet(CustomModelViewSet):

View File

@ -31,7 +31,6 @@ 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()

View File

@ -43,7 +43,6 @@ 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:
@ -65,7 +64,6 @@ 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
@ -114,7 +112,6 @@ 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']
@ -126,7 +123,6 @@ class PuOrderItemSerializer(CustomModelSerializer):
PumService.cal_pu_order_total_price(pu_order) PumService.cal_pu_order_total_price(pu_order)
return ins return ins
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop('material') validated_data.pop('material')
validated_data.pop('pu_order') validated_data.pop('pu_order')

View File

@ -80,7 +80,6 @@ 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
@ -104,7 +103,6 @@ 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('采购订单非创建中不可删除')
@ -145,7 +143,6 @@ class PuOrderItemViewSet(CustomModelViewSet):
filterset_fields = ['material', 'pu_order'] filterset_fields = ['material', 'pu_order']
ordering = ['create_time'] ordering = ['create_time']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
pu_order = instance.pu_order pu_order = instance.pu_order
if pu_order.state != PuOrder.PUORDER_CREATE: if pu_order.state != PuOrder.PUORDER_CREATE:

View File

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

View File

@ -353,13 +353,12 @@ 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):
with transaction.atomic(): ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date)
ftest = Ftest.objects.create(qct=qct, test_user=test_user, test_date=test_date) for testitem in qct.testitems.all():
for testitem in qct.testitems.all(): FtestItem.objects.create(ftest=ftest, testitem=testitem)
FtestItem.objects.create(ftest=ftest, testitem=testitem) for defect in qct.defects.all():
for defect in qct.defects.all(): FtestDefect.objects.create(ftest=ftest, defect=defect)
FtestDefect.objects.create(ftest=ftest, defect=defect) return ftest
return ftest
# @classmethod # @classmethod
# def cal_count_notok(cls, ftestwork: FtestWork): # def cal_count_notok(cls, ftestwork: FtestWork):
@ -446,7 +445,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, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS)) '析晶', max_length=10, null=True, blank=True, 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)

View File

@ -215,34 +215,33 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
def create(self, validated_data): def create(self, validated_data):
ftestworkdefect = validated_data.pop("ftestworkdefect", []) ftestworkdefect = validated_data.pop("ftestworkdefect", [])
with transaction.atomic(): ins: FtestWork = super().create(validated_data)
ins: FtestWork = super().create(validated_data) for ftestworkdefect in ftestworkdefect:
for ftestworkdefect in ftestworkdefect: if ftestworkdefect['count'] > 0:
if ftestworkdefect['count'] > 0: FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect)
FtestworkDefect.objects.create(ftestwork=ins, **ftestworkdefect) if ins.qct or ftestworkdefect:
if ins.qct or ftestworkdefect: ins.cal_count()
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):
@ -292,24 +291,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
@ -364,75 +363,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) ins = FtestItem.objects.create(ftest=instance, **item)
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) ins = FtestDefect.objects.create(ftest=instance, **item2)
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
if ins.has and ins.defect.okcate in [Defect.DEFECT_NOTOK]: else:
is_ok = False ins.is_main = False
if not has_is_main: ins.save()
ins.is_main = True if not is_ok:
has_is_main = True instance.defect_main = defect_main
defect_main = ins.defect else:
else: instance.defect_main = None
ins.is_main = False instance.is_ok = is_ok
ins.save() instance.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

View File

@ -33,7 +33,6 @@ 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()
@ -93,7 +92,6 @@ 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:
@ -151,7 +149,6 @@ 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()
@ -238,19 +235,16 @@ 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()
@ -284,27 +278,32 @@ 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('审批单已进行,无法修改')
x = super().update(request, *args, **kwargs) serializer = self.get_serializer(ins, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
# 触发批次统计分析 # 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch]) ana_batch_thread(xbatchs=[ins.batch])
return x return Response(serializer.data)
@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('存在审批, 无法删除')
x = super().destroy(request, *args, **kwargs) self.perform_destroy(ins)
# 触发批次统计分析 # 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch]) ana_batch_thread(xbatchs=[ins.batch])
return x return Response(status=204)
def perform_create(self, serializer): def perform_create(self, serializer):
ins = serializer.save() ins = serializer.save()

View File

@ -65,7 +65,6 @@ 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('订单非创建中不可删除')

View File

@ -275,13 +275,11 @@ 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)

View File

@ -8,8 +8,7 @@ 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 (CreateModelMixin, DestroyModelMixin, from rest_framework.mixins import RetrieveModelMixin
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
@ -20,7 +19,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 (CustomCreateModelMixin, MyLoggingMixin) from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin)
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
@ -229,7 +228,7 @@ class PTaskViewSet(CustomModelViewSet):
return Response() return Response()
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet): class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
""" """
list:任务执行结果列表 list:任务执行结果列表
@ -373,7 +372,7 @@ class RoleViewSet(CustomModelViewSet):
ordering = ['create_time'] ordering = ['create_time']
class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""岗位/角色关系 """岗位/角色关系
岗位/角色关系 岗位/角色关系
@ -385,7 +384,7 @@ class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
filterset_fields = ['post', 'role'] filterset_fields = ['post', 'role']
class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""用户/岗位关系 """用户/岗位关系
用户/岗位关系 用户/岗位关系
@ -398,37 +397,13 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
ordering = ['sort', 'create_time'] ordering = ['sort', 'create_time']
def perform_create(self, serializer): def perform_create(self, serializer):
with transaction.atomic(): instance = serializer.save()
instance = serializer.save() user = instance.user
user = instance.user up = UserPost.objects.filter(user=user).order_by(
up = UserPost.objects.filter(user=user).order_by( 'sort', 'create_time').first()
'sort', 'create_time').first() if up:
if up: user.belong_dept = up.dept
user.belong_dept = up.dept user.post = up.post
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()
# 更新人员表 # 更新人员表
@ -440,6 +415,28 @@ class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, Custo
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)
@ -610,7 +607,7 @@ class UserViewSet(CustomModelViewSet):
return Response() return Response()
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet): class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""文件上传 """文件上传
list: list:
@ -651,7 +648,7 @@ class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, Cu
instance.save() instance.save()
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet): class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
perms_map = {'get': '*', 'post': 'apk.upload'} perms_map = {'get': '*', 'post': 'apk.upload'}
serializer_class = ApkSerializer serializer_class = ApkSerializer
@ -692,7 +689,7 @@ class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSe
return Response() return Response()
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet): class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
perms_map = {'get': '*', 'post': '*', perms_map = {'get': '*', 'post': '*',
'delete': 'myschedule.delete'} 'delete': 'myschedule.delete'}
serializer_class = MyScheduleSerializer serializer_class = MyScheduleSerializer
@ -717,7 +714,6 @@ class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, Cus
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 # 不可少

View File

@ -195,6 +195,8 @@ 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):
@ -243,6 +245,8 @@ 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:
@ -264,17 +268,29 @@ 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: for item in ordering:
if item.startswith('-'): if item.startswith('-'):
new_qs = new_qs.order_by(F(item[1:]).desc(nulls_last=True)) # JSONField 排序只能用字符串,不要 F
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
else: else:
new_qs = new_qs.order_by(item) 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)

View File

@ -85,3 +85,4 @@ 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)

View File

@ -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=True) @auto_log(name='阿里云短信', raise_exception=True, send_mail=False)
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

View File

@ -1,4 +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): def execute_raw_sql(sql: str, params=None):
"""执行原始sql并返回rows, columns数据 """执行原始sql并返回rows, columns数据
@ -23,7 +25,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): def query_all_dict(sql, params=None, with_time_format=False):
''' '''
查询所有结果返回字典类型数据 查询所有结果返回字典类型数据
:param sql: :param sql:
@ -36,9 +38,19 @@ def query_all_dict(sql, params=None):
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): def query_one_dict(sql, params=None, with_time_format=False):
""" """
查询一个结果返回字典类型数据 查询一个结果返回字典类型数据
:param sql: :param sql:
@ -49,6 +61,13 @@ def query_one_dict(sql, params=None):
cursor.execute(sql, params or ()) # 更简洁的参数处理 cursor.execute(sql, params or ()) # 更简洁的参数处理
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:
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情况 return dict(zip(columns, row)) if row else None # 安全处理None情况
import pymysql import pymysql

View File

@ -0,0 +1,18 @@
# 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='前端自定义页面路径'),
),
]

View File

@ -0,0 +1,18 @@
# 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'),
),
]

View File

@ -21,6 +21,8 @@ class Workflow(CommonAModel):
'标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}') '标题模板', max_length=50, default='{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}')
content_template = models.CharField( content_template = models.CharField(
'内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}') '内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}')
view_path = models.TextField('前端自定义页面路径', null=True, blank=True)
view_path2 = models.TextField('前端自定义页面路径2', null=True, blank=True)
class Meta: class Meta:
verbose_name = '工作流' verbose_name = '工作流'

View File

@ -27,7 +27,7 @@ class StateSerializer(CustomModelSerializer):
class WorkflowSimpleSerializer(CustomModelSerializer): class WorkflowSimpleSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Workflow model = Workflow
fields = ['id', 'name', 'key'] fields = ['id', 'name', 'key', 'view_path']
class StateSimpleSerializer(CustomModelSerializer): class StateSimpleSerializer(CustomModelSerializer):

View File

@ -77,7 +77,7 @@ class WfService(object):
""" """
获取状态可执行的操作 获取状态可执行的操作
""" """
return Transition.objects.filter(is_deleted=False, source_state=state).all() return Transition.objects.filter(is_deleted=False, source_state=state).all().order_by("-attribute_type", "-id")
@classmethod @classmethod
def get_ticket_steps(cls, ticket: Ticket): def get_ticket_steps(cls, ticket: Ticket):
@ -442,11 +442,17 @@ class WfService(object):
last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or
ticket.in_add_node): ticket.in_add_node):
# 如果状态变化或是转交加签的情况再发送通知 # 如果状态变化或是转交加签的情况再发送通知
Thread(target=send_ticket_notice_t, args=(ticket,), daemon=True).start() cls.send_ticket_notice(ticketflow=last_log)
# 如果目标状态是脚本则异步执行 # 如果目标状态是脚本则异步执行
if state.participant_type == State.PARTICIPANT_TYPE_ROBOT: if state.participant_type == State.PARTICIPANT_TYPE_ROBOT:
run_task.delay(ticket_id=ticket.id) run_task.delay(ticket_id=ticket.id)
@classmethod
def send_ticket_notice(cls, ticketflow:TicketFlow):
# 根据ticketflow发送通知
Thread(target=send_ticket_notice_t, args=(ticketflow,), daemon=True).start()
@classmethod @classmethod
def close_by_task(cls, ticket: Ticket, suggestion: str): def close_by_task(cls, ticket: Ticket, suggestion: str):
@ -479,10 +485,13 @@ class WfService(object):
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT,
participant=handler, transition=None) participant=handler, transition=None)
def send_ticket_notice_t(ticket: Ticket): cls.task_ticket(ticket=ticket)
def send_ticket_notice_t(ticketflow: TicketFlow):
""" """
发送通知 发送通知
""" """
ticket = ticketflow.ticket
params = {'workflow': ticket.workflow.name, 'state': ticket.state.name} params = {'workflow': ticket.workflow.name, 'state': ticket.state.name}
if ticket.participant_type == 1: if ticket.participant_type == 1:
# 发送短信通知 # 发送短信通知

View File

@ -239,6 +239,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
raise ParseError('请指定查询分类') raise ParseError('请指定查询分类')
return super().filter_queryset(queryset) return super().filter_queryset(queryset)
@transaction.atomic
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
""" """
新建工单 新建工单
@ -263,25 +264,25 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
save_ticket_data[key] = ticket_data[key] save_ticket_data[key] = ticket_data[key]
else: else:
save_ticket_data = ticket_data save_ticket_data = ticket_data
with transaction.atomic():
ticket = serializer.save(state=start_state, ticket = serializer.save(state=start_state,
create_by=request.user, create_by=request.user,
create_time=timezone.now(), create_time=timezone.now(),
act_state=Ticket.TICKET_ACT_STATE_DRAFT, act_state=Ticket.TICKET_ACT_STATE_DRAFT,
belong_dept=request.user.belong_dept, belong_dept=request.user.belong_dept,
ticket_data=save_ticket_data) # 先创建出来 ticket_data=save_ticket_data) # 先创建出来
# 更新title和sn # 更新title和sn
title = vdata.get('title', '') title = vdata.get('title', '')
title_template = ticket.workflow.title_template title_template = ticket.workflow.title_template
if title_template: if title_template:
all_ticket_data = {**rdata, **ticket_data} all_ticket_data = {**rdata, **ticket_data}
title = title_template.format(**all_ticket_data) title = title_template.format(**all_ticket_data)
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
ticket.sn = sn ticket.sn = sn
ticket.title = title ticket.title = title
ticket.save() ticket.save()
ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data, ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data,
handler=request.user, created=True) handler=request.user, created=True)
return Response(TicketSerializer(instance=ticket).data) return Response(TicketSerializer(instance=ticket).data)
@action(methods=['get'], detail=False, perms_map={'get': '*'}) @action(methods=['get'], detail=False, perms_map={'get': '*'})
@ -297,6 +298,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
return Response(ret) return Response(ret)
@action(methods=['post'], detail=True, perms_map={'post': '*'}) @action(methods=['post'], detail=True, perms_map={'post': '*'})
@transaction.atomic
def handle(self, request, pk=None): def handle(self, request, pk=None):
""" """
处理工单 处理工单
@ -307,13 +309,13 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
vdata = serializer.validated_data vdata = serializer.validated_data
new_ticket_data = ticket.ticket_data new_ticket_data = ticket.ticket_data
new_ticket_data.update(**vdata['ticket_data']) new_ticket_data.update(**vdata['ticket_data'])
with transaction.atomic(): ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'],
ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'], new_ticket_data=new_ticket_data, handler=request.user,
new_ticket_data=new_ticket_data, handler=request.user, suggestion=vdata.get('suggestion', ''))
suggestion=vdata.get('suggestion', ''))
return Response(TicketSerializer(instance=ticket).data) return Response(TicketSerializer(instance=ticket).data)
@action(methods=['post'], detail=True, perms_map={'post': '*'}) @action(methods=['post'], detail=True, perms_map={'post': '*'})
@transaction.atomic
def deliver(self, request, pk=None): def deliver(self, request, pk=None):
""" """
转交工单 转交工单
@ -325,15 +327,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
vdata = serializer.validated_data # 校验之后的数据 vdata = serializer.validated_data # 校验之后的数据
if not ticket.state.enable_deliver: if not ticket.state.enable_deliver:
raise ParseError('不允许转交') raise ParseError('不允许转交')
with transaction.atomic(): ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL ticket.participant = vdata['target_user']
ticket.participant = vdata['target_user'] ticket.save()
ticket.save() tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket),
ticket_data=WfService.get_ticket_all_field_value(ticket), suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
suggestion=vdata.get('suggestion', ''), participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER, participant=request.user, transition=None)
participant=request.user, transition=None) WfService.send_ticket_notice(ticketflow=tf)
return Response() return Response()
@action(methods=['get'], detail=True, perms_map={'get': '*'}) @action(methods=['get'], detail=True, perms_map={'get': '*'})
@ -381,11 +383,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save() ticket.save()
# 接单日志 # 接单日志
# 更新工单流转记录 # 更新工单流转记录
TicketFlow.objects.create(ticket=ticket, state=ticket.state, tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket), ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL, suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT, intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT,
participant=request.user, transition=None) participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response() return Response()
else: else:
raise ParseError('无需接单') raise ParseError('无需接单')
@ -420,11 +423,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save() ticket.save()
# 更新流转记录 # 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签说明 suggestion = request.data.get('suggestion', '') # 加签说明
TicketFlow.objects.create(ticket=ticket, state=ticket.state, tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket), ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE,
participant=request.user, transition=None) participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response() return Response()
@action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer) @action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer)
@ -444,11 +448,12 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save() ticket.save()
# 更新流转记录 # 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签意见 suggestion = request.data.get('suggestion', '') # 加签意见
TicketFlow.objects.create(ticket=ticket, state=ticket.state, tf = TicketFlow.objects.create(ticket=ticket, state=ticket.state,
ticket_data=WfService.get_ticket_all_field_value(ticket), ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL,
intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE_END,
participant=request.user, transition=None) participant=request.user, transition=None)
WfService.send_ticket_notice(ticketflow=tf)
return Response() return Response()
@action(methods=['post'], detail=True, perms_map={'post': '*'}, @action(methods=['post'], detail=True, perms_map={'post': '*'},

View File

@ -0,0 +1,25 @@
# Generated by Django 3.2.12 on 2025-10-20 02:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('em', '0022_equipment_cd_req_addr'),
('wpm', '0123_mlogbdefect_count_has'),
]
operations = [
migrations.AddField(
model_name='mloguser',
name='equipment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mloguser_equipment', to='em.equipment', verbose_name='生产设备'),
),
migrations.AddField(
model_name='mloguser',
name='work_start_time',
field=models.DateTimeField(blank=True, null=True, verbose_name='生产开始时间'),
),
]

View File

@ -368,6 +368,9 @@ class MlogUser(BaseModel):
mlog = models.ForeignKey(Mlog, verbose_name='关联日志', on_delete=models.CASCADE) mlog = models.ForeignKey(Mlog, verbose_name='关联日志', on_delete=models.CASCADE)
handle_user = models.ForeignKey(User, verbose_name='操作人', on_delete=models.CASCADE) handle_user = models.ForeignKey(User, verbose_name='操作人', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='子工序', on_delete=models.CASCADE) process = models.ForeignKey(Process, verbose_name='子工序', on_delete=models.CASCADE)
work_start_time = models.DateTimeField('生产开始时间', null=True, blank=True)
equipment = models.ForeignKey(
Equipment, verbose_name='生产设备', on_delete=models.CASCADE, null=True, blank=True, related_name='mloguser_equipment')
shift = models.ForeignKey(Shift, verbose_name='关联班次', on_delete=models.CASCADE) shift = models.ForeignKey(Shift, verbose_name='关联班次', on_delete=models.CASCADE)
handle_date = models.DateField('操作日期') handle_date = models.DateField('操作日期')
@ -524,7 +527,7 @@ class Mlogbw(BaseModel):
mlogb.count_real = count mlogb.count_real = count
count_notok = 0 count_notok = 0
count_notok_full = 0 count_notok_full = 0
# 个追踪的合格b类不分 # 个追踪的合格b类不分这里会导致count_ok_full与count_ok一样了暂时不做处理
tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False) tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False)
tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False).values("ftest__defect_main").annotate(xcount=Count('id')) tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False).values("ftest__defect_main").annotate(xcount=Count('id'))
defect_ids = tqs.values_list("ftest__defect_main", flat=True) defect_ids = tqs.values_list("ftest__defect_main", flat=True)

View File

@ -355,12 +355,12 @@ class MlogSerializer(CustomModelSerializer):
model = Mlog model = Mlog
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + \ read_only_fields = EXCLUDE_FIELDS + \
['submit_time', 'submit_user', 'material_outs', "handle_date", "shift"] ['submit_time', 'submit_user', 'material_outs']
extra_kwargs = { extra_kwargs = {
"batch": {"required": True}, "batch": {"required": True},
"shift": {"required": False}, "shift": {"required": False},
"material_out": {"required": True}, "material_out": {"required": True},
"work_start_time": {"required": True} "work_start_time": {"required": False}
} }
def create(self, validated_data): def create(self, validated_data):
@ -634,14 +634,20 @@ class MlogSerializer(CustomModelSerializer):
# 时间 # 时间
mgroup:Mgroup = attrs['mgroup'] mgroup:Mgroup = attrs['mgroup']
work_start_time:datetime = attrs['work_start_time'] work_start_time:datetime = attrs.get('work_start_time', None)
handle_date, attrs["shift"] = mgroup.get_shift(work_start_time) if work_start_time:
if mtask and mtask.start_date == mtask.end_date: attrs['handle_date'], attrs["shift"] = mgroup.get_shift(work_start_time)
attrs['handle_date'] = mtask.end_date
if attrs['handle_date'] != handle_date:
raise ParseError('任务日期与生产日期不一致')
else: else:
attrs['handle_date'] = handle_date if "handle_date" in attrs and attrs["handle_date"]:
pass
else:
raise ParseError('缺少生产日期')
if mtask and mtask.start_date == mtask.end_date:
if attrs['handle_date'] != mtask.end_date:
if work_start_time:
raise ParseError('任务日期与生产日期不一致')
else:
attrs['handle_date'] = mtask.end_date
handle_user = attrs.get('handle_user', None) handle_user = attrs.get('handle_user', None)
if handle_user is None and hasattr(self, "request"): if handle_user is None and hasattr(self, "request"):
@ -721,11 +727,12 @@ class CountJsonFromSerializer(serializers.Serializer):
class MlogbInSerializer(CustomModelSerializer): class MlogbInSerializer(CustomModelSerializer):
mlogbdefect = MlogbDefectSerializer(many=True, required=False) mlogbdefect = MlogbDefectSerializer(many=True, required=False)
count_json_from = CountJsonFromSerializer(required=False, many=True) count_json_from = CountJsonFromSerializer(required=False, many=True)
wprs_in = serializers.ListField(child=serializers.CharField(), label="单个ID的列表", required=False)
class Meta: class Meta:
model = Mlogb model = Mlogb
fields = ['id', 'mlog', 'mtask', 'route', 'wm_in', 'count_use', 'count_pn_jgqbl', fields = ['id', 'mlog', 'mtask', 'route', 'wm_in', 'count_use', 'count_pn_jgqbl',
'count_break', 'note', "parent", "mlogbdefect", "count_json_from"] 'count_break', 'note', "parent", "mlogbdefect", "count_json_from", "wprs_in"]
extra_kwargs = {'count_use': {'required': True}, 'mtask': {'required': False}, extra_kwargs = {'count_use': {'required': True}, 'mtask': {'required': False},
'wm_in': {'required': True, "allow_empty": False}} 'wm_in': {'required': True, "allow_empty": False}}
@ -777,12 +784,14 @@ class MlogbInSerializer(CustomModelSerializer):
mlog: Mlog = validated_data['mlog'] mlog: Mlog = validated_data['mlog']
mtask: Mtask = validated_data.get("mtask", None) mtask: Mtask = validated_data.get("mtask", None)
mlogbdefect = validated_data.pop("mlogbdefect", None) mlogbdefect = validated_data.pop("mlogbdefect", None)
wprs_in = validated_data.pop("wprs_in", [])
if Mlogb.objects.filter(mlog=mlog, mtask=mtask, wm_in=validated_data['wm_in'], parent=None).exists(): if Mlogb.objects.filter(mlog=mlog, mtask=mtask, wm_in=validated_data['wm_in'], parent=None).exists():
raise ParseError('该记录已存在') raise ParseError('该记录已存在')
if mlog.submit_time is not None: if mlog.submit_time is not None:
raise ParseError('生产日志已提交不可编辑') raise ParseError('生产日志已提交不可编辑')
ins:Mlogb = super().create(validated_data) ins:Mlogb = super().create(validated_data)
validated_data["wprs_in"] = wprs_in
if mlogbdefect is not None and ins.material_in.tracking == Material.MA_TRACKING_BATCH: if mlogbdefect is not None and ins.material_in.tracking == Material.MA_TRACKING_BATCH:
mlogb_defect_objects = [ mlogb_defect_objects = [
MlogbDefect(**{**item, "mlogb": ins, "id": idWorker.get_id()}) MlogbDefect(**{**item, "mlogb": ins, "id": idWorker.get_id()})
@ -878,7 +887,9 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
if wpr: if wpr:
mlogb: Mlogb = validated_data["mlogb"] mlogb: Mlogb = validated_data["mlogb"]
if Mlogbw.objects.filter(mlogb=mlogb, wpr=wpr).exists(): if Mlogbw.objects.filter(mlogb=mlogb, wpr=wpr).exists():
raise ParseError('该产品已选入') raise ParseError(f'{wpr.number}-该产品已选入')
if Mlogbw.objects.filter(mlogb__mlog__submit_time__isnull=True, wpr=wpr).exists():
raise ParseError(f'{wpr.number}-该产品已在其他日志中选入')
ftest_data = validated_data.pop("ftest", None) ftest_data = validated_data.pop("ftest", None)
mlogbw = super().create(validated_data) mlogbw = super().create(validated_data)
if ftest_data: if ftest_data:
@ -1149,6 +1160,12 @@ class HandoverbSerializer(CustomModelSerializer):
read_only_fields = EXCLUDE_FIELDS_BASE + ['handover'] read_only_fields = EXCLUDE_FIELDS_BASE + ['handover']
extra_kwargs = {'wm': {'required': True}} extra_kwargs = {'wm': {'required': True}}
class HandoverbListSerializer(CustomModelSerializer):
defect_name = serializers.CharField(source="wm.defect.name", read_only=True)
class Meta:
model = Handoverb
fields = "__all__"
class HandoverSerializer(CustomModelSerializer): class HandoverSerializer(CustomModelSerializer):
# wm = serializers.PrimaryKeyRelatedField( # wm = serializers.PrimaryKeyRelatedField(
# label='车间库存ID', queryset=WMaterial.objects.all()) # label='车间库存ID', queryset=WMaterial.objects.all())
@ -1363,6 +1380,8 @@ class HandoverUpdateSerializer(CustomModelSerializer):
model = Handover model = Handover
fields = ['id', 'send_date', 'send_user', 'count', 'count_eweight', 'recive_user', 'note'] fields = ['id', 'send_date', 'send_user', 'count', 'count_eweight', 'recive_user', 'note']
class HandoverListSerializer(HandoverSerializer):
handoverb = HandoverbListSerializer(many=True, required=False)
class GenHandoverSerializer(serializers.Serializer): class GenHandoverSerializer(serializers.Serializer):
@ -1473,10 +1492,17 @@ class MlogUserSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MlogUser model = MlogUser
fields = "__all__" fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE read_only_fields = EXCLUDE_FIELDS_BASE + ["shift", "handle_date"]
extra_kwargs = {
"work_start_time": {"required": True}
}
def create(self, validated_data): def create(self, validated_data):
mlog:Mlog = validated_data["mlog"] mlog:Mlog = validated_data["mlog"]
work_start_time:datetime = validated_data["work_start_time"]
if mlog.work_start_time < mlog.work_start_time:
raise ParseError("操作时间不能早于日志开始时间")
validated_data["handle_date"], validated_data["shift"] = mlog.mgroup.get_shift(work_start_time)
if mlog.submit_time is not None: if mlog.submit_time is not None:
raise ParseError("该日志已提交") raise ParseError("该日志已提交")
process:Process = validated_data["process"] process:Process = validated_data["process"]
@ -1512,6 +1538,7 @@ class MlogQuickSerializer(serializers.Serializer):
team = serializers.CharField(label="班组ID", required=False) team = serializers.CharField(label="班组ID", required=False)
equipment = serializers.CharField(label="设备ID", required=False) equipment = serializers.CharField(label="设备ID", required=False)
wm_in = serializers.CharField(label="输入车间库存ID") wm_in = serializers.CharField(label="输入车间库存ID")
wprs_in = serializers.ListField(child=serializers.CharField(), label="单个ID的列表", required=False)
count_use = serializers.IntegerField(label="领用数量") count_use = serializers.IntegerField(label="领用数量")
is_fix = serializers.BooleanField(label="是否返修") is_fix = serializers.BooleanField(label="是否返修")
mgroup = serializers.CharField(label="工段ID") mgroup = serializers.CharField(label="工段ID")

View File

@ -11,7 +11,7 @@ from apps.system.models import User
from apps.pm.models import Mtask from apps.pm.models import Mtask
from apps.mtm.models import Mgroup, Shift, Material, Route, RoutePack, Team, Srule from apps.mtm.models import Mgroup, Shift, Material, Route, RoutePack, Team, Srule
from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt, MlogUser
from apps.mtm.services_2 import cal_material_count from apps.mtm.services_2 import cal_material_count
from apps.wf.models import Ticket from apps.wf.models import Ticket
from apps.wf.services import WfService from apps.wf.services import WfService
@ -152,7 +152,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
""" """
生产日志提交后需要执行的操作 生产日志提交后需要执行的操作
""" """
if mlog.work_start_time > timezone.now(): if mlog.work_start_time and mlog.work_start_time > timezone.now():
raise ParseError('操作开始时间不能晚于当前时间') raise ParseError('操作开始时间不能晚于当前时间')
if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time: if mlog.work_start_time and mlog.work_end_time and mlog.work_end_time < mlog.work_start_time:
raise ParseError('操作结束时间不能早于操作开始时间') raise ParseError('操作结束时间不能早于操作开始时间')
@ -385,7 +385,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
mlog.submit_user = user mlog.submit_user = user
mlog.stored_notok = stored_notok mlog.stored_notok = stored_notok
mlog.stored_mgroup = stored_mgroup mlog.stored_mgroup = stored_mgroup
if mlog.work_end_time is None: if mlog.work_end_time is None and mlog.work_start_time is not None:
mlog.work_end_time = now mlog.work_end_time = now
mlog.save() mlog.save()
@ -397,7 +397,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 触发批次统计分析 # 触发批次统计分析
xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True)) xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
ana_batch_thread(xbatches) ana_batch_thread(xbatchs=xbatches, mgroup=mlog.mgroup)
# 触发单个统计 # 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
@ -619,7 +619,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# 触发批次统计分析 # 触发批次统计分析
xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True)) xbatches = list(Mlogb.objects.filter(mlog=mlog).values_list('batch', flat=True))
ana_batch_thread(xbatches) ana_batch_thread(xbatches, mgroup=mlog.mgroup)
# 触发单个统计 # 触发单个统计
wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True)) wprIds = list(Mlogbw.objects.filter(mlogb__mlog=mlog, ftest__isnull=False, wpr__isnull=False).values_list('wpr__id', flat=True))
@ -739,6 +739,8 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
recive_mgroup = handover.recive_mgroup recive_mgroup = handover.recive_mgroup
recive_dept = handover.recive_dept recive_dept = handover.recive_dept
if handover.type == Handover.H_CHANGE:
handover.mtype = Handover.H_MERGE
new_batch = handover.new_batch new_batch = handover.new_batch
if new_batch and mtype != Handover.H_MERGE: if new_batch and mtype != Handover.H_MERGE:
raise ParseError("只有合并时才能提供新批次号") raise ParseError("只有合并时才能提供新批次号")
@ -1024,6 +1026,10 @@ def mlog_submit_validate(ins: Mlog):
raise ParseError('该日志未指定消耗!') raise ParseError('该日志未指定消耗!')
if Mlogb.objects.filter(material_out__isnull=False, count_real=0, mlog=ins).exists(): if Mlogb.objects.filter(material_out__isnull=False, count_real=0, mlog=ins).exists():
raise ParseError('产出数量不能为0!') raise ParseError('产出数量不能为0!')
if ins.is_fix is False and ins.route:
process = ins.route.process
if Process.objects.filter(parent=process).exists() and not MlogUser.objects.filter(mlog=ins).exists():
raise ParseError('该日志子工序信息未完善!')
def bind_mlog(ticket: Ticket, transition, new_ticket_data: dict): def bind_mlog(ticket: Ticket, transition, new_ticket_data: dict):
ins = Mlog.objects.get(id=new_ticket_data['t_id']) ins = Mlog.objects.get(id=new_ticket_data['t_id'])

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,13 @@ class WprSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Wpr model = Wpr
fields = '__all__' fields = '__all__'
def to_representation(self, instance):
ret = super().to_representation(instance)
material_name = ret.get("material_name", "")
if material_name:
ret["process_name"] = material_name.split("|")[-1]
return ret
class WprDetailSerializer(WprSerializer): class WprDetailSerializer(WprSerializer):
mb_ = MaterialBatchSerializer(source='mb', read_only=True) mb_ = MaterialBatchSerializer(source='mb', read_only=True)

View File

@ -10,6 +10,7 @@ from rest_framework.exceptions import ParseError
from django.db import transaction from django.db import transaction
from apps.wpmw.filters import WprFilter from apps.wpmw.filters import WprFilter
from apps.utils.sql import query_one_dict from apps.utils.sql import query_one_dict
from django.db.models.expressions import RawSQL
class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet): class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, CustomGenericViewSet):
@ -17,6 +18,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
动态产品 动态产品
""" """
perms_map = {"get": "*"} perms_map = {"get": "*"}
select_related_fields = ["wm", "mb", "material", "wm__material_ofrom"] select_related_fields = ["wm", "mb", "material", "wm__material_ofrom"]
prefetch_related_fields = ["defects"] prefetch_related_fields = ["defects"]
@ -27,6 +29,10 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
ordering = ["number", "create_time"] ordering = ["number", "create_time"]
ordering_fields = ["number", "create_time", "update_time"] ordering_fields = ["number", "create_time", "update_time"]
search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"] search_fields = ["number", "material__name", "material__model", "material__specification", "number_out"]
annotate_dict = {
"number_prefix": RawSQL("regexp_replace(number, '(\\d+)$', '')", []),
"number_suffix": RawSQL("COALESCE(NULLIF(regexp_replace(number, '.*?(\\d+)$', '\\1'), ''), '0')::bigint", []),
}
def filter_queryset(self, queryset): def filter_queryset(self, queryset):
qs = super().filter_queryset(queryset) qs = super().filter_queryset(queryset)
@ -36,7 +42,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
qs.exclude(mb=None, wm=None) qs.exclude(mb=None, wm=None)
return qs return qs
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=WprNewSerializer) @action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WprNewSerializer)
def new_number(self, request, *args, **kwargs): def new_number(self, request, *args, **kwargs):
"""获取新的编号""" """获取新的编号"""
data = request.data data = request.data
@ -47,16 +53,17 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
wpr_last = wps_qs.order_by("number").last() wpr_last = wps_qs.order_by("number").last()
count = wps_qs.count() count = wps_qs.count()
last_number = wpr_last.number if count > 0 else None last_number = wpr_last.number if count > 0 else None
last_number_count = int(last_number.split("-")[-1].lstrip('0')) last_number_count = int(last_number.split("-")[-1].lstrip("0"))
mat = Material.objects.get(id=material_start) mat = Material.objects.get(id=material_start)
return Response({"count": count, "last_number": last_number, "material_model":mat.model, "last_number_count": last_number_count}) return Response({"count": count, "last_number": last_number, "material_model": mat.model, "last_number_count": last_number_count})
@action(methods=['get'], detail=False, perms_map={'get': '*'}) @action(methods=["get"], detail=False, perms_map={"get": "*"})
def number_out_last(self, request, *args, **kwargs): def number_out_last(self, request, *args, **kwargs):
"""获取最新的出库对外编号 """获取最新的出库对外编号
获取最新的出库对外编号(get请求传入prefix参数和with_unsubmit参数with_unsubmit默认为yes表示是否包含未出库的记录prefix为前缀'WPR-2023-0)""" 获取最新的出库对外编号(get请求传入prefix参数和with_unsubmit参数with_unsubmit默认为yes表示是否包含未出库的记录prefix为前缀'WPR-2023-0)"""
from apps.inm.models import MIOItemw from apps.inm.models import MIOItemw
prefix = request.query_params.get("prefix", None) prefix = request.query_params.get("prefix", None)
if not prefix: if not prefix:
raise ParseError("请传入前缀参数") raise ParseError("请传入前缀参数")
@ -67,7 +74,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
SELECT id, number_out FROM wpmw_wpr SELECT id, number_out FROM wpmw_wpr
WHERE number_out ~ %s order by number_out desc limit 1 WHERE number_out ~ %s order by number_out desc limit 1
""" """
pattern = f'^{prefix}[0-9]+$' pattern = f"^{prefix}[0-9]+$"
number_outs = [] number_outs = []
wpr_qs_last = query_one_dict(query, [pattern]) wpr_qs_last = query_one_dict(query, [pattern])
if wpr_qs_last: if wpr_qs_last:
@ -87,7 +94,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
if number_outs: if number_outs:
number_outs.sort() number_outs.sort()
number_out_last = number_outs[-1] number_out_last = number_outs[-1]
number_int_last = number_out_last.lstrip(prefix).lstrip('0') number_int_last = number_out_last.lstrip(prefix).lstrip("0")
try: try:
number_int_last = int(number_int_last) number_int_last = int(number_int_last)
except ValueError: except ValueError:
@ -95,9 +102,8 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
return Response({"number_out_last": number_out_last, "number_out_int_last": int(number_int_last)}) return Response({"number_out_last": number_out_last, "number_out_int_last": int(number_int_last)})
else: else:
return Response({"number_out_last": None}) return Response({"number_out_last": None})
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=WproutListSerializer) @action(methods=["post"], detail=False, perms_map={"post": "*"}, serializer_class=WproutListSerializer)
@transaction.atomic @transaction.atomic
def assgin_number_out(self, request, *args, **kwargs): def assgin_number_out(self, request, *args, **kwargs):
"""分配出库对外编号 """分配出库对外编号
@ -108,7 +114,7 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
vdata = sr.validated_data vdata = sr.validated_data
items = vdata["items"] items = vdata["items"]
number_outs = [i["number_out"] for i in items] number_outs = [i["number_out"] for i in items]
existing_numbers = Wpr.objects.filter(number_out__in=number_outs, number_out__isnull=False).values_list('number_out', flat=True) existing_numbers = Wpr.objects.filter(number_out__in=number_outs, number_out__isnull=False).values_list("number_out", flat=True)
if existing_numbers.exists(): if existing_numbers.exists():
used_numbers = list(existing_numbers) used_numbers = list(existing_numbers)
raise ParseError(f"以下对外编号已被使用: {used_numbers[0]}{len(used_numbers)}") raise ParseError(f"以下对外编号已被使用: {used_numbers[0]}{len(used_numbers)}")
@ -116,4 +122,4 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
wpr = Wpr.objects.get(id=i["id"]) wpr = Wpr.objects.get(id=i["id"])
wpr.number_out = i["number_out"] wpr.number_out = i["number_out"]
wpr.save() wpr.save()
return Response() return Response()

View File

@ -1,3 +1,44 @@
## 2.8.2025101011
- feat: 新增功能
- Ptest val_xj可为空 [caoqianming]
- quick调用serializer时传入request [caoqianming]
- ofm-修改model [zty]
- Mroombooking添加字段 [caoqianming]
- base add_info_for_item 可复用list逻辑 [caoqianming]
- 添加wpr查询参数 [caoqianming]
- base cquery支持annotate [caoqianming]
- mroombooking 返回slots [caoqianming]
- fix: 问题修复
- p_create_after 自动创建mlogbw时关于exclude语句导致的查询错误 [caoqianming]
## 2.8.2025092816
- feat: 新增功能
- handover revert撤回时做校验 [caoqianming]
- ofm-models 修改车辆表的字段信息 [zty]
- iofm-serializer 增加ticket_ [zty]
- 会议室预定修改 [caoqianming]
- p_create_after 优化 [caoqianming]
- mlog quick增加wprs_in传参 [caoqianming]
- 车间领料时完善提示添加物料名 [caoqianming]
- ofm-修改会议室信息 [zty]
- 改版需提供新批次号 [caoqianming]
- ofm 修改publicity 的model [zty]
- 修改ofm-services.py [zty]
- base send_sms auto_log send_mail使用False [caoqianming]
- 优化bind_routepack [caoqianming]
- base 优化wf通知发送 [caoqianming]
- routepack_ticket_change 变为创建中状态 [caoqianming]
- route增加查询条件以支持获取后续工段的信息 [caoqianming]
- 导入物料优化一下 [caoqianming]
- 优化的mplogxview [caoqianming]
- base sql querydict可传入是否格式化时间参数 [caoqianming]
- base workflow list 添加并返回view_path [caoqianming]
- 优化handover list接口 [caoqianming]
- 玻纤拉丝采集问题 [caoqianming]
- base system和wf优化事务处理 [caoqianming]
- 优化cd.py [caoqianming]
- wpr list 返回process_name [caoqianming]
- fix: 问题修复
- mroombooking 创建时未create_by [caoqianming]
## 2.8.2025091616 ## 2.8.2025091616
- feat: 新增功能 - feat: 新增功能
- mioitem处理事务处理 [caoqianming] - mioitem处理事务处理 [caoqianming]

View File

@ -75,46 +75,42 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
with sc_lock: # 加锁,防止竞争 with sc_lock: # 加锁,防止竞争
sc = sc_all[addr] sc = sc_all[addr]
try: try:
if sc is None: if sc is None:
sc = socket.socket() sc = socket.socket()
sc.settimeout(5) sc.settimeout(5)
sc.connect((host, int(port))) sc.connect((host, int(port)))
with sc_lock: # 再次加锁,更新 sc_all
sc_all[addr] = sc sc_all[addr] = sc
sc.settimeout(0.5)
sc.settimeout(0.5) while True:
while True: try:
try: data = sc.recv(65536)
data = sc.recv(65536) if not data:
if not data: break
except (socket.timeout, BlockingIOError):
break break
except (socket.timeout, BlockingIOError):
break sc.settimeout(5)
sc.sendall(b"R")
sc.settimeout(5)
sc.sendall(b"R") data = bytearray()
while len(data) < 8:
data = bytearray() chunk = sc.recv(1024)
while len(data) < 8: if not chunk:
chunk = sc.recv(1024) raise ConnectionError("连接中断")
if not chunk: data.extend(chunk)
raise ConnectionError("连接中断")
data.extend(chunk) return sc, bytes(data)
return sc, bytes(data) except Exception as e:
if sc is not None:
except Exception as e: try:
if sc is not None: sc.close()
try: except Exception:
sc.close() pass
except Exception:
pass
with sc_lock: # 加锁,清除无效连接
sc_all[addr] = None sc_all[addr] = None
self.error(f'采集器通信失败: {e}') self.error(f'采集器通信失败: {e}')
print(traceback.format_exc()) return None, None
return None, None
sc, resp = connect_and_send() sc, resp = connect_and_send()
if sc is None or resp is None: if sc is None or resp is None:
@ -122,7 +118,17 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
res = handle_bytes(resp) res = handle_bytes(resp)
if isinstance(res, str): if isinstance(res, str):
self.error(res) if res == "数据头不正确":
sc, resp = connect_and_send()
if sc is None or resp is None:
return
res = handle_bytes(resp)
if isinstance(res, str):
self.error(res)
else:
self.ok(res)
else:
self.error(res)
else: else:
self.ok(res) self.ok(res)

143
out_service/insert_kvt.py Normal file
View File

@ -0,0 +1,143 @@
import requests
import os
import sys
import django
import json
import logging
import time
import threading
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(CUR_DIR)
sys.path.insert(0, BASE_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
django.setup()
from apps.enm.services import insert_mplogx_item
from django.utils import timezone
from apps.utils.tasks import send_mail_task
from datetime import datetime, timedelta
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(CUR_DIR)
sys.path.insert(0, BASE_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
django.setup()
SERVER = '192.168.1.37'
PORT = '8089'
PATH = f'http://{SERVER}:{PORT}/GetTagList'
SERVER2 = '192.168.1.43'
PORT2 = '8081'
PATH2 = f'http://{SERVER2}:{PORT2}/GetTagList'
#MIN_LIST = set(range(0,61,5))
MIN_LIST = [0]
myLogger = logging.getLogger("log")
class MailController:
def __init__(self):
self.last_sent_time = None
self.interval = timedelta(days=1)
self.lock = threading.Lock()
self._is_sending = False
def should_send_mail(self):
now = datetime.now()
# thread_name = threading.current_thread().name
with self.lock:
if self._is_sending:
# myLogger.info(f"线程 {thread_name}: 邮件正在发送中,跳过")
return False
if self.last_sent_time is None:
# myLogger.info(f"线程 {thread_name}: 首次发送邮件")
self._is_sending = True
return True
time_since_last = now - self.last_sent_time
if time_since_last > self.interval:
# myLogger.info(f"线程 {thread_name}: 距离上次发送已超过间隔,允许发送")
self._is_sending = True
return True
else:
# myLogger.info(f"线程 {thread_name}: 距离上次发送不足间隔,跳过")
return False
def mark_as_sent(self):
with self.lock:
self.last_sent_time = datetime.now()
self._is_sending = False
myLogger.info("邮件发送状态已重置")
mail_controller = MailController()
def send_error_notification(error_message):
"""
发送错误通知
"""
if mail_controller.should_send_mail():
try:
send_mail_task.delay(subject='insert_kvt_error', message=str(error_message))
myLogger.error(f"请求组态王失败:{str(error_message)}")
except Exception as e:
myLogger.exception(f"发送错误邮件失败: {e}")
finally:
mail_controller.mark_as_sent()
def fetch_data(timex, enp_mpoint_dict, path):
"""
从数据库转存到超表
"""
response = None
try:
response = requests.get(path, timeout=5)
response.raise_for_status() # 如果响应码不是 200将触发异常
except requests.RequestException as e:
send_error_notification(e)
if response is None:
return
try:
lines = response.text
json_line = [line.strip() for line in lines.split('\n') if line.strip() ]
current_object = []
# 将碎片分组为完整的 JSON 对象
for line in json_line:
current_object.append(line.strip()) # 将当前行加入对象
if line.strip() == '}': # 遇到结束大括号时
try:
obj_str = ' '.join(current_object).replace(',}', '}')
obj_dict = json.loads(obj_str)
insert_mplogx_item(obj_dict.get('strVarName'), obj_dict.get('VarValue'), timex, enp_mpoint_dict)
except json.JSONDecodeError as e:
send_error_notification(e)
current_object = [] # 重置,准备处理下一个对象
except Exception as e:
send_error_notification(e)
def get_data():
last_triggered = None
while True:
now = timezone.now()
now = now.replace(microsecond=0)
if now.second in MIN_LIST:
if last_triggered != now:
last_triggered = now
enp_mpoint_dict= {}
threads = [
threading.Thread(target=fetch_data, args=(now, enp_mpoint_dict, PATH), daemon=True),
threading.Thread(target=fetch_data, args=(now, enp_mpoint_dict, PATH2), daemon=True)
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=10) # 设置超时防止线程挂起
time.sleep(0.5)
if __name__ == '__main__':
get_data()

View File

@ -35,7 +35,7 @@ sys.path.insert(0, os.path.join(BASE_DIR, 'apps'))
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
SYS_NAME = '星途工厂综合管理系统' SYS_NAME = '星途工厂综合管理系统'
SYS_VERSION = '2.8.2025091616' SYS_VERSION = '2.8.2025101011'
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Application definition # Application definition

View File

@ -21,10 +21,11 @@ from drf_yasg import openapi
from drf_yasg.views import get_schema_view from drf_yasg.views import get_schema_view
from rest_framework.documentation import include_docs_urls from rest_framework.documentation import include_docs_urls
from django.views.generic import TemplateView from django.views.generic import TemplateView
from server.settings import get_sysconfig
schema_view = get_schema_view( schema_view = get_schema_view(
openapi.Info( openapi.Info(
title=settings.SYS_NAME, title=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}',
default_version=settings.SYS_VERSION, default_version=settings.SYS_VERSION,
contact=openapi.Contact(email="caoqianming@foxmail.com"), contact=openapi.Contact(email="caoqianming@foxmail.com"),
license=openapi.License(name="MIT License"), license=openapi.License(name="MIT License"),