Compare commits

..

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

98 changed files with 1314 additions and 4241 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -129,7 +129,7 @@ class MIOItemCreateSerializer(CustomModelSerializer):
'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price', 'note', "pack_index"] 'batch', 'count', 'assemb', 'is_testok', 'mioitemw', 'mb', 'wm', 'unit_price', 'note', "pack_index"]
extra_kwargs = { extra_kwargs = {
'mio': {'required': True}, 'warehouse': {'required': False}, 'mio': {'required': True}, 'warehouse': {'required': False},
'material': {'required': False}, 'batch': {'required': False, "allow_null": True, "allow_blank": True}} 'material': {'required': False}, 'batch': {'required': False}}
def create(self, validated_data): def create(self, validated_data):
@ -153,9 +153,7 @@ class MIOItemCreateSerializer(CustomModelSerializer):
validated_data["batch"] = wm.batch validated_data["batch"] = wm.batch
material: Material = validated_data['material'] material: Material = validated_data['material']
batch = validated_data.get("batch", None) batch = validated_data['batch']
if not batch:
batch = ""
if material.is_hidden: if material.is_hidden:
raise ParseError('隐式物料不可出入库') raise ParseError('隐式物料不可出入库')
if mio.type in [MIO.MIO_TYPE_RETURN_IN, MIO.MIO_TYPE_BORROW_OUT]: if mio.type in [MIO.MIO_TYPE_RETURN_IN, MIO.MIO_TYPE_BORROW_OUT]:
@ -171,11 +169,11 @@ class MIOItemCreateSerializer(CustomModelSerializer):
mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN]) mis = MIOItem.objects.filter(batch=batch, material=material, mio__type__in=[MIO.MIO_TYPE_PUR_IN, MIO.MIO_TYPE_DO_IN, MIO.MIO_TYPE_OTHER_IN])
if mis.exists() and (not mis.exclude(test_date=None).exists()): if mis.exists() and (not mis.exclude(test_date=None).exists()):
raise ParseError('该批次的物料未经检验') raise ParseError('该批次的物料未经检验')
with transaction.atomic():
count = validated_data["count"] count = validated_data["count"]
batch = validated_data["batch"] batch = validated_data["batch"]
mioitemw = validated_data.pop('mioitemw', []) mioitemw = validated_data.pop('mioitemw', [])
instance:MIOItem = super().create(validated_data) instance = super().create(validated_data)
assemb_dict = {} assemb_dict = {}
for i in assemb: for i in assemb:
assemb_dict[i['material'].id] = i assemb_dict[i['material'].id] = i
@ -210,13 +208,6 @@ class MIOItemCreateSerializer(CustomModelSerializer):
raise ParseError('不支持自动生成请提供产品明细') raise ParseError('不支持自动生成请提供产品明细')
elif len(mioitemw) >= 1: elif len(mioitemw) >= 1:
mio_type = mio.type mio_type = mio.type
if mio_type != "pur_in" and mio_type != "other_in":
wprIds = [i["wpr"].id for i in mioitemw]
mb_ids = list(Wpr.objects.filter(id__in=wprIds).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == instance.mb.id:
pass
else:
raise ParseError(f'{batch}物料明细中存在{len(mb_ids)}个不同物料批次')
for item in mioitemw: for item in mioitemw:
if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in": if item.get("wpr", None) is None and mio_type != "pur_in" and mio_type != "other_in":
raise ParseError(f'{item["number"]}_请提供产品明细ID') raise ParseError(f'{item["number"]}_请提供产品明细ID')

View File

@ -45,6 +45,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
ftest_sr.update(instance=ftest, validated_data=ftest_data) ftest_sr.update(instance=ftest, validated_data=ftest_data)
return mioitemw return mioitemw
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
wpr: Wpr = validated_data.get("wpr", None) wpr: Wpr = validated_data.get("wpr", None)
if wpr: if wpr:
@ -57,6 +58,7 @@ class MIOItemwCreateUpdateSerializer(CustomModelSerializer):
mioitemw = self.save_ftest(mioitemw, ftest_data) mioitemw = self.save_ftest(mioitemw, ftest_data)
return mioitemw return mioitemw
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop("mioitem") validated_data.pop("mioitem")
ftest_data = validated_data.pop("ftest", None) ftest_data = validated_data.pop("ftest", None)

View File

@ -10,7 +10,7 @@ from apps.wpmw.models import Wpr
from apps.qm.models import Ftest, Defect from apps.qm.models import Ftest, Defect
from django.db.models import Count, Q from django.db.models import Count, Q
def do_out(item: MIOItem, is_reverse: bool = False): def do_out(item: MIOItem):
""" """
生产领料到车间 生产领料到车间
""" """
@ -23,6 +23,8 @@ def do_out(item: MIOItem, is_reverse: bool = False):
mgroup = mio.mgroup mgroup = mio.mgroup
do_user = mio.do_user do_user = mio.do_user
material:Material = item.material material:Material = item.material
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
return
# 获取defect # 获取defect
defect:Defect = None defect:Defect = None
@ -92,11 +94,11 @@ def do_out(item: MIOItem, is_reverse: bool = False):
raise ParseError(f"批次错误!{e}") raise ParseError(f"批次错误!{e}")
mb.count = mb.count - xcount mb.count = mb.count - xcount
if mb.count < 0: if mb.count < 0:
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
else: else:
mb.save() mb.save()
if material.into_wm:
# 领到车间库存(或工段) # 领到车间库存(或工段)
wm, new_create = WMaterial.objects.get_or_create( wm, new_create = WMaterial.objects.get_or_create(
batch=xbatch, material=xmaterial, batch=xbatch, material=xmaterial,
@ -112,16 +114,9 @@ def do_out(item: MIOItem, is_reverse: bool = False):
# 开始变动wpr # 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE: if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item) mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count: if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败") raise ParseError("出入库与明细数量不一致,操作失败")
mb_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("mb__id", flat=True).distinct())
if len(mb_ids) == 1 and mb_ids[0] == mb.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(mb_ids)}个不同物料批次')
for mioitemw in mioitemws: for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb) Wpr.change_or_new(wpr=mioitemw.wpr, wm=wm, old_mb=mb)
@ -141,7 +136,8 @@ def do_in(item: MIOItem):
mgroup = mio.mgroup mgroup = mio.mgroup
do_user = mio.do_user do_user = mio.do_user
material = item.material material = item.material
if material.into_wm is False: # 根据配置不进行入车间库存的处理
return
action_list = [] action_list = []
mias = MIOItemA.objects.filter(mioitem=item) mias = MIOItemA.objects.filter(mioitem=item)
is_zhj = False # 是否组合件入仓库 is_zhj = False # 是否组合件入仓库
@ -176,7 +172,7 @@ 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,
@ -208,7 +204,6 @@ def do_in(item: MIOItem):
production_dept = wm_production_dept production_dept = wm_production_dept
elif wm_production_dept and production_dept != wm_production_dept: elif wm_production_dept and production_dept != wm_production_dept:
raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间') raise ParseError(f'{str(xmaterial)}-{xbatch}车间物料不属于同一车间')
# 增加mb # 增加mb
if not is_zhj: if not is_zhj:
mb, _ = MaterialBatch.objects.get_or_create( mb, _ = MaterialBatch.objects.get_or_create(
@ -231,16 +226,9 @@ def do_in(item: MIOItem):
# 开始变动wpr # 开始变动wpr
if xmaterial.tracking == Material.MA_TRACKING_SINGLE: if xmaterial.tracking == Material.MA_TRACKING_SINGLE:
if material.into_wm is False:
raise ParseError("追踪单个物料不支持不进行车间库存的操作")
mioitemws = MIOItemw.objects.filter(mioitem=item) mioitemws = MIOItemw.objects.filter(mioitem=item)
if mioitemws.count() != item.count: if mioitemws.count() != item.count:
raise ParseError("出入库与明细数量不一致,操作失败") raise ParseError("出入库与明细数量不一致,操作失败")
wm_ids = list(Wpr.objects.filter(wpr_mioitemw__in=mioitemws).values_list("wm__id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm.id:
pass
else:
raise ParseError(f'{xbatch}物料明细中存在{len(wm_ids)}个不同物料批次')
for mioitemw in mioitemws: for mioitemw in mioitemws:
Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm) Wpr.change_or_new(wpr=mioitemw.wpr, mb=mb, old_wm=wm)
@ -410,7 +398,7 @@ class InmService:
if change_count < 0: if change_count < 0:
raise ParseError("存在负数!") raise ParseError("存在负数!")
state = WMaterial.WM_OK state = WMaterial.WM_OK
if defect and defect.okcate in [Defect.DEFECT_NOTOK]: if defect:
state = WMaterial.WM_NOTOK state = WMaterial.WM_NOTOK
mb, _ = MaterialBatch.objects.get_or_create( mb, _ = MaterialBatch.objects.get_or_create(
material=material, material=material,
@ -442,7 +430,7 @@ class InmService:
elif in_or_out == -1: elif in_or_out == -1:
mb.count = mb.count - change_count mb.count = mb.count - change_count
if mb.count < 0: if mb.count < 0:
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败") raise ParseError(f"{mb.batch}-批次库存不足,操作失败")
else: else:
mb.save() mb.save()
if tracking == Material.MA_TRACKING_SINGLE: if tracking == Material.MA_TRACKING_SINGLE:

View File

@ -138,17 +138,13 @@ def daoru_mioitems(path:str, mio:MIO):
mioitems = [] mioitems = []
ind = 2 ind = 2
while sheet[f"a{ind}"].value: while sheet[f"b{ind}"].value:
batch = sheet[f"b{ind}"].value batch = sheet[f"b{ind}"].value
material_number = sheet[f"a{ind}"].value material_number = sheet[f"a{ind}"].value
try: try:
material = Material.objects.get(number=material_number) material = Material.objects.get(number=material_number)
except Exception as e: except Exception as e:
raise ParseError(f"未找到物料:{material_number} {e}") raise ParseError(f"未找到物料:{material_number} {e}")
if batch:
pass
else:
batch = ""
count = sheet[f"c{ind}"].value count = sheet[f"c{ind}"].value
warehouse_name = sheet[f"d{ind}"].value warehouse_name = sheet[f"d{ind}"].value
try: try:

View File

@ -27,7 +27,6 @@ from apps.qm.serializers import FtestProcessSerializer
from apps.mtm.models import Material from apps.mtm.models import Material
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from django.db import connection
# Create your views here. # Create your views here.
@ -152,15 +151,6 @@ class MIOViewSet(CustomModelViewSet):
'item_mio__a_mioitem__batch'] 'item_mio__a_mioitem__batch']
data_filter = True data_filter = True
@classmethod
def lock_and_check_can_update(cls, mio:MIO):
if not connection.in_atomic_block:
raise ParseError("请在事务中调用该方法")
mio:MIO = MIO.objects.select_for_update().get(id=mio.id)
if mio.submit_time is not None:
raise ParseError("该记录已提交无法更改")
return mio
def add_info_for_list(self, data): def add_info_for_list(self, data):
# 获取检验状态 # 获取检验状态
mio_dict = {} mio_dict = {}
@ -205,29 +195,27 @@ class MIOViewSet(CustomModelViewSet):
return super().perform_destroy(instance) return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):
"""提交 """提交
提交 提交
""" """
ins:MIO = self.get_object() ins = self.get_object()
if ins.inout_date is None: if ins.inout_date is None:
raise ParseError('出入库日期未填写') raise ParseError('出入库日期未填写')
if ins.state != MIO.MIO_CREATE: if ins.state != MIO.MIO_CREATE:
raise ParseError('记录状态异常') raise ParseError('记录状态异常')
now = timezone.now() with transaction.atomic():
ins.submit_user = request.user ins.submit_time = timezone.now()
ins.submit_time = now
ins.update_by = request.user
ins.state = MIO.MIO_SUBMITED ins.state = MIO.MIO_SUBMITED
ins.submit_user = request.user
ins.update_by = request.user
ins.save() ins.save()
InmService.update_inm(ins) InmService.update_inm(ins)
InmService.update_material_count(ins) InmService.update_material_count(ins)
return Response(MIOListSerializer(instance=ins).data) return Response(MIOListSerializer(instance=ins).data)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mio.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic
def revert(self, request, *args, **kwargs): def revert(self, request, *args, **kwargs):
"""撤回 """撤回
@ -239,10 +227,10 @@ class MIOViewSet(CustomModelViewSet):
raise ParseError('记录状态异常') raise ParseError('记录状态异常')
if ins.submit_user != user: if ins.submit_user != user:
raise ParseError('非提交人不可撤回') raise ParseError('非提交人不可撤回')
ins.submit_user = None with transaction.atomic():
ins.update_by = user
ins.state = MIO.MIO_CREATE
ins.submit_time = None ins.submit_time = None
ins.state = MIO.MIO_CREATE
ins.update_by = user
ins.save() ins.save()
InmService.update_inm(ins, is_reverse=True) InmService.update_inm(ins, is_reverse=True)
InmService.update_material_count(ins) InmService.update_material_count(ins)
@ -366,12 +354,9 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs) return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.validated_data["mio"] = MIOViewSet.lock_and_check_can_update(serializer.validated_data['mio'])
return super().perform_create(serializer)
def perform_destroy(self, instance): def perform_destroy(self, instance):
MIOViewSet.lock_and_check_can_update(instance.mio) if instance.mio.state != MIO.MIO_CREATE:
raise ParseError('出入库记录非创建中不可删除')
if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user: if has_perm(self.request.user, ['mio.update']) is False and instance.mio.create_by != self.request.user:
raise PermissionDenied('无权限删除') raise PermissionDenied('无权限删除')
return super().perform_destroy(instance) return super().perform_destroy(instance)
@ -503,20 +488,20 @@ class MIOItemwViewSet(CustomModelViewSet):
mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count() mioitem.count_notok = MIOItemw.objects.filter(mioitem=mioitem, ftest__is_ok=False).count()
mioitem.save() mioitem.save()
@transaction.atomic
def perform_create(self, serializer): def perform_create(self, serializer):
MIOViewSet.lock_and_check_can_update(serializer.validated_data['mioitem'].mio)
ins: MIOItemw = serializer.save() ins: MIOItemw = serializer.save()
self.cal_mioitem_count(ins.mioitem) mioitem: MIOItem = ins.mioitem
self.cal_mioitem_count(mioitem)
@transaction.atomic
def perform_update(self, serializer): def perform_update(self, serializer):
ins:MIOItemw = serializer.instance mioitemw = serializer.save()
MIOViewSet.lock_and_check_can_update(ins.mioitem.mio) self.cal_mioitem_count(mioitemw.mioitem)
ins:MIOItemw = serializer.save()
self.cal_mioitem_count(ins.mioitem)
@transaction.atomic
def perform_destroy(self, instance: MIOItemw): def perform_destroy(self, instance: MIOItemw):
mioitem = instance.mioitem mioitem = instance.mioitem
MIOViewSet.lock_and_check_can_update(mioitem.mio)
ftest = instance.ftest ftest = instance.ftest
instance.delete() instance.delete()
if ftest: if ftest:

View File

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

View File

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

View File

@ -4,8 +4,6 @@ from rest_framework.exceptions import ParseError
from apps.utils.models import CommonBDModel from apps.utils.models import CommonBDModel
from collections import defaultdict, deque from collections import defaultdict, deque
from django.db.models import Q from django.db.models import Q
from datetime import datetime, timedelta
from django.utils import timezone
class Process(CommonBModel): class Process(CommonBModel):
""" """
@ -180,32 +178,6 @@ class Mgroup(CommonBModel):
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
def get_shift(self, w_s_time:datetime):
# 如果没有时区信息,使用默认时区(东八区)
if not timezone.is_aware(w_s_time):
w_s_time = timezone.make_aware(w_s_time)
else:
w_s_time = timezone.localtime(w_s_time)
shifts = Shift.objects.filter(rule=self.shift_rule).order_by('sort')
if not shifts:
raise ParseError(f"工段{self.name}未配置班次")
# 处理跨天班次的情况
for shift in shifts:
# 如果开始时间小于结束时间,表示班次在同一天内
if shift.start_time_o < shift.end_time_o:
if shift.start_time_o <= w_s_time.time() < shift.end_time_o:
return w_s_time.date(), shift
else: # 班次跨天(如夜班从当天晚上到次日凌晨)
if w_s_time.time() >= shift.start_time_o or w_s_time.time() < shift.end_time_o:
# 如果当前时间在开始时间之后,属于当天
if w_s_time.time() >= shift.start_time_o:
return w_s_time.date(), shift
# 如果当前时间在结束时间之前,属于前一天
else:
return (w_s_time - timedelta(days=1)).date(), shift
# return w_s_time.date(), None
class TeamMember(BaseModel): class TeamMember(BaseModel):
team = models.ForeignKey(Team, verbose_name='关联班组', team = models.ForeignKey(Team, verbose_name='关联班组',
@ -417,9 +389,8 @@ class Route(CommonADModel):
batch_bind = models.BooleanField('是否绑定批次', default=True) batch_bind = models.BooleanField('是否绑定批次', default=True)
materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials", materials = models.ManyToManyField(Material, verbose_name='关联辅助物料', related_name="route_materials",
through="mtm.routemat", blank=True) through="mtm.routemat", blank=True)
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True, related_name="route_parent") parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
params_json = models.JSONField('工艺参数', default=dict, blank=True) params_json = models.JSONField('工艺参数', default=dict, blank=True)
from_route = models.ForeignKey('self', verbose_name='来源路线', on_delete=models.SET_NULL, null=True, blank=True, related_name="route_f")
def __str__(self): def __str__(self):
x = "" x = ""

View File

@ -246,8 +246,8 @@ class RouteSerializer(CustomModelSerializer):
# material = validated_data.get('material', None) # material = validated_data.get('material', None)
# if material and process and Route.objects.filter(material=material, process=process).exists(): # if material and process and Route.objects.filter(material=material, process=process).exists():
# raise ValidationError('已选择该工序!!') # raise ValidationError('已选择该工序!!')
with transaction.atomic():
instance:Route = super().create(validated_data) instance = super().create(validated_data)
material_out = instance.material_out material_out = instance.material_out
if material_out: if material_out:
if material_out.process is None: if material_out.process is None:
@ -263,16 +263,12 @@ class RouteSerializer(CustomModelSerializer):
if instance.material: if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save() instance.save()
rx = Route.objects.filter( rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx: if rx:
instance.from_route = rx msg = ""
instance.save() if rx.routepack:
# msg = "" msg = rx.routepack.name
# if rx.routepack: raise ParseError(f"该工艺步骤已存在-{msg}")
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -281,7 +277,7 @@ class RouteSerializer(CustomModelSerializer):
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH) 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:
@ -298,16 +294,12 @@ class RouteSerializer(CustomModelSerializer):
if instance.material: if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user) instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save() instance.save()
rx = Route.objects.filter( rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
if rx: if rx:
instance.from_route = rx msg = ""
instance.save() if rx.routepack:
# msg = "" msg = rx.routepack.name
# if rx.routepack: raise ParseError(f"该工艺步骤已存在-{msg}")
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
return instance return instance
def to_representation(self, instance): def to_representation(self, instance):
@ -339,12 +331,6 @@ class RouteMatSerializer(CustomModelSerializer):
fields = "__all__" fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE read_only_fields = EXCLUDE_FIELDS_BASE
def validate(self, attrs):
route:Route = attrs["route"]
if route.from_route is not None:
raise ParseError("该工艺步骤引用其他步骤,无法修改")
return attrs
class MaterialExportSerializer(CustomModelSerializer): class MaterialExportSerializer(CustomModelSerializer):
class Meta: class Meta:

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 or sheet[f'd{i}'].value is not None: while sheet[f'b{i}'].value is not None:
type_str = sheet[f'b{i}'].value.replace(' ', '') type_str = sheet[f'b{i}'].value.replace(' ', '')
try: try:
type = type_dict[type_str] type = type_dict[type_str]
@ -155,12 +155,12 @@ def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
raise ParseError('缺少步骤') raise ParseError('缺少步骤')
r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time') r_qs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
first_route = r_qs.first() first_route = r_qs.first()
last_route = r_qs.last()
if first_route.batch_bind: if first_route.batch_bind:
first_route.batch_bind = False first_route.batch_bind = False
first_route.save(update_fields=['batch_bind']) first_route.save(update_fields=['batch_bind'])
# last_route = r_qs.last() if last_route.material_out != routepack.material:
# if last_route.material_out != routepack.material: raise ParseError('最后一步产出与工艺包不一致')
# raise ParseError('最后一步产出与工艺包不一致')
ticket_data = ticket.ticket_data ticket_data = ticket.ticket_data
ticket_data.update({ ticket_data.update({
't_model': 'routepack', 't_model': 'routepack',
@ -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 in [Ticket.TICKET_ACT_STATE_DRAFT, Ticket.TICKET_ACT_STATE_BACK, Ticket.TICKET_ACT_STATE_RETREAT]: if ticket.act_state == Ticket.TICKET_ACT_STATE_DRAFT:
routepack.state = RoutePack.RP_S_CREATE routepack.state = RoutePack.RP_S_CREATE
routepack.save() routepack.save()

View File

@ -39,8 +39,9 @@ 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', 'create_time'] 'type', 'process', 'process__sort', 'sort', 'id', 'number']
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
from apps.inm.models import MaterialBatch from apps.inm.models import MaterialBatch
if MaterialBatch.objects.filter(material=instance).exists(): if MaterialBatch.objects.filter(material=instance).exists():
@ -311,7 +312,7 @@ class RoutePackViewSet(CustomModelViewSet):
return Response({"id": route_new.id}) return Response({"id": route_new.id})
@transaction.atomic @transaction.atomic
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer) @action(methods=['post'], detail=True, permission_classes = [IsAdminUser], serializer_class=Serializer)
def toggle_state(self, request, *args, **kwargs): def toggle_state(self, request, *args, **kwargs):
"""变更工艺路线状态 """变更工艺路线状态
@ -367,22 +368,12 @@ class RouteViewSet(CustomModelViewSet):
select_related_fields = ['material', select_related_fields = ['material',
'process', 'material_in', 'material_out', 'mgroup', 'routepack'] 'process', 'material_in', 'material_out', 'mgroup', 'routepack']
def perform_update(self, serializer): def update(self, request, *args, **kwargs):
ins:Route = serializer.instance obj:Route = self.get_object()
if ins.from_route is not None: routepack = obj.routepack
raise ParseError('该工艺步骤引用其他步骤, 无法编辑')
old_m_in, old_m_out, process = ins.material_in, ins.material_out, ins.process
routepack = ins.routepack
if routepack and routepack.state != RoutePack.RP_S_CREATE: if routepack and routepack.state != RoutePack.RP_S_CREATE:
raise ParseError('该工艺路线非创建中不可编辑') raise ParseError('该状态下不可编辑')
ins_n:Route = serializer.save() return super().update(request, *args, **kwargs)
if Route.objects.filter(from_route__id=ins.id).exists() and (ins_n.material_in != old_m_in or ins_n.material_out != old_m_out or ins_n.process != process):
raise ParseError("该工艺步骤被其他步骤引用, 无法修改关键信息")
def perform_destroy(self, instance:Route):
if Route.objects.filter(from_route=instance).exists():
raise ParseError('该工艺步骤被其他步骤引用,无法删除')
return super().perform_destroy(instance)
class SruleViewSet(CustomModelViewSet): class SruleViewSet(CustomModelViewSet):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,9 @@
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo) from .models import Mroom, MroomBooking, MroomSlot
# Publicity, PatetInfo, 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,30 +17,28 @@ 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 = super().create(validated_data) booking = MroomBooking.objects.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("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists() try:
if ms_exists: MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom)
raise ParseError("时段已预订,请刷新重选") except Exception as e:
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True) raise ParseError(f"时段已预订,请刷新重选-{e}")
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')
@ -52,145 +48,17 @@ 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("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists() try:
if ms_exists: MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom)
raise ParseError("时段已预订,请刷新重选") except Exception as e:
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True) raise ParseError(f"时段已预订,请刷新重选-{e}")
return booking return booking
class MroomSlotSerializer(CustomModelSerializer): class MroomSlotSerializer(CustomModelSerializer):
booking_title = serializers.CharField(source='booking.title', read_only=True) booking_title = serializers.CharField(source='booking.title', read_only=True)
class Meta: class Meta:
model = MroomSlot model = MroomSlot
fields = '__all__' fields = '__all__'
class LendingSealSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = LendingSeal
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class VehicleSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Vehicle
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
class FileRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = FileRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class BorrowRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件")
file_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
file_name = serializers.SerializerMethodField()
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = BorrowRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
def get_file_name(self, obj):
return [file.name for file in obj.borrow_file.all()]
class PublicitySerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Publicity
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class PatentInfoSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = PatentInfo
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
# class PaperSerializer(CustomModelSerializer):
# class Meta:
# model = PaperOfm
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class PlatformSerializer(serializers.ModelSerializer):
# class Meta:
# model = Platform
# fields = ['id', 'name']
# class ProjectSerializer(serializers.ModelSerializer):
# class Meta:
# model = Project
# fields = ['id', 'name']
# class ProjectMemberSerializer(CustomModelSerializer):
# affiliated_platforms = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Platform.objects.all(),
# write_only=True
# )
# affiliated_platforms_detail = PlatformSerializer(
# source='affiliated_platforms', many=True, read_only=True
# )
# affiliated_projects = serializers.PrimaryKeyRelatedField(
# many=True,
# queryset=Project.objects.all(),
# write_only=True
# )
# affiliated_projects_detail = ProjectSerializer(
# source='affiliated_projects', many=True, read_only=True
# )
# class Meta:
# model = PatentRecord
# fields = '__all__'
# class PaperRecordSerializer(CustomModelSerializer):
# class Meta:
# model = PaperRecord
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectApprovalSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectApproval
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS
# class ProjectInfoSerializer(CustomModelSerializer):
# class Meta:
# model = ProjectInfo
# fields = '__all__'
# read_only_fields = EXCLUDE_FIELDS

View File

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

View File

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

View File

@ -1,16 +1,11 @@
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, Publicity, PatentInfo from .models import Mroom, MroomBooking, MroomSlot
# Publicity, , PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo) from .serializers import MroomSerializer, MroomBookingSerializer, MroomSlotSerializer
from .serializers import (MroomSerializer, MroomBookingSerializer, MroomSlotSerializer, LendingSealSerializer,
VehicleSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer, PatentInfoSerializer)
# ,SealSerializer,
# LendingSealSerializer, FileRecordSerializer, BorrowRecordSerializer, PublicitySerializer,
# PatentInfoSerializer, PaperSerializer, PlatformSerializer, ProjectSerializer, ProjectMemberSerializer, PaperRecordSerializer, ProjectApprovalSerializer, ProjectInfoSerializer)
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.utils.mixins import CustomListModelMixin from apps.utils.mixins import CustomListModelMixin
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.ofm.filters import MroomBookingFilterset, SealFilter, BorrowRecordFilter from apps.ofm.filters import MroomBookingFilterset
class MroomViewSet(CustomModelViewSet): class MroomViewSet(CustomModelViewSet):
@ -29,7 +24,7 @@ class MroomBookingViewSet(CustomModelViewSet):
""" """
queryset = MroomBooking.objects.all() queryset = MroomBooking.objects.all()
serializer_class = MroomBookingSerializer serializer_class = MroomBookingSerializer
select_related_fields = ["create_by", "ticket", "belong_dept"] select_related_fields = ["create_by"]
filterset_class = MroomBookingFilterset filterset_class = MroomBookingFilterset
def add_info_for_list(self, data): def add_info_for_list(self, data):
@ -67,7 +62,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["slots"] = info.pop("current_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,178 +77,21 @@ class MroomBookingViewSet(CustomModelViewSet):
def perform_update(self, serializer): def perform_update(self, serializer):
ins:MroomBooking = self.get_object() ins:MroomBooking = self.get_object()
ticket = ins.ticket if ins.create_by != self.request.user:
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 and instance.create_by != self.request.user: if instance.create_by != self.request.user:
raise ParseError("只允许创建者删除") raise ParseError("只允许创建者删除")
ticket = instance.ticket return super().perform_destroy(instance)
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):
"""list: """list: 会议室预订时段
会议室预订时段 会议室预订时段
""" """
queryset = MroomSlot.objects.all() queryset = MroomSlot.objects.all()
serializer_class = MroomSlotSerializer serializer_class = MroomSlotSerializer
filterset_fields = ["mroom", "mdate", "booking", "is_inuse"] filterset_fields = ["mroom", "mdate", "booking"]
class LendingSealViewSet(CustomModelViewSet):
"""list: 印章外出
印章外出
"""
perms_map = {'get': '*', 'post': 'seal.update',
'put': 'seal.update', 'delete': 'seal.delete'}
queryset = LendingSeal.objects.all()
serializer_class = LendingSealSerializer
filterset_class = SealFilter
ordering = ["-create_time"]
data_filter = True
class VehicleViewSet(CustomModelViewSet):
"""list: 车辆
车辆
"""
queryset = Vehicle.objects.all()
serializer_class = VehicleSerializer
ordering = ["-create_time"]
class FilerecordViewSet(CustomModelViewSet):
"""list: 文件
文件
"""
queryset = FileRecord.objects.all()
serializer_class = FileRecordSerializer
filterset_fields = [ "name", "number"]
ordering = ["-create_time", "number", "name"]
class FileborrowViewSet(CustomModelViewSet):
"""list: 文件借阅
文件借阅
"""
queryset = BorrowRecord.objects.all()
serializer_class = BorrowRecordSerializer
filterset_class = BorrowRecordFilter
ordering = ["-create_time"]
class PublicityViewSet(CustomModelViewSet):
"""list: 公告
公告
"""
queryset = Publicity.objects.all()
serializer_class = PublicitySerializer
filterset_fields = ["title","number"]
ordering = ["-create_time", "number"]
class PatentInfoViewSet(CustomModelViewSet):
"""list: 专利
专利
"""
queryset = PatentInfo.objects.all()
serializer_class = PatentInfoSerializer
filterset_fields = ["name", "author", "type"]
ordering = ["-create_time", "name", "author", "type"]
# class PaperViewSet(CustomModelViewSet):
# """list: 论文申密审批
# 论文申密审批
# """
# queryset = PaperOfm.objects.all()
# serializer_class = PaperSerializer
# filterset_fields = ["name", "author"]
# ordering = ["create_time", "name"]
# class PlatformViewSet(CustomModelViewSet):
# """list: 平台
# 平台
# """
# queryset = Platform.objects.all()
# serializer_class = PlatformSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class ProjectViewSet(CustomModelViewSet):
# """list: 项目
# 项目
# """
# queryset = Project.objects.all()
# serializer_class = ProjectSerializer
# filterset_fields = ["name"]
# ordering = ["create_time", "name"]
# class PatentRecordViewSet(CustomModelViewSet):
# """list: 专利台账登记
# 专利台账登记
# """
# queryset = PatentRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["patent", "type"]
# ordering = ["create_time", "patent", "type"]
# class PaperRecordViewSet(CustomModelViewSet):
# """list: 论文台账登记
# 论文台账登记
# """
# queryset = PaperRecord.objects.all()
# serializer_class = ProjectMemberSerializer
# filterset_fields = ["index", "title", "paper_code","paper_type", "authors"]
# ordering = ["create_time", "paper", "type"]
# class ProjectApprovalViewSet(CustomModelViewSet):
# """list: 立项审批表
# 立项审批表
# """
# queryset = ProjectApproval.objects.all()
# serializer_class = ProjectApprovalSerializer
# filterset_fields = ["project_start_date"]
# ordering = ["project_start_date"]
# class ProjectInfoViewSet(CustomModelViewSet):
# """list: 项目信息
# 项目信息
# """
# queryset = ProjectInfo.objects.all()
# serializer_class = ProjectInfoSerializer
# filterset_fields = ["serial_number", "name", "platform", "project_source"]
# ordering = ["serial_number", "name"]

View File

@ -31,6 +31,7 @@ class UtaskSerializer(CustomModelSerializer):
"priority": {"required": False, "allow_null": True}, "priority": {"required": False, "allow_null": True},
} }
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
if not validated_data.get('number', None): if not validated_data.get('number', None):
validated_data["number"] = Utask.get_a_number() validated_data["number"] = Utask.get_a_number()

View File

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

View File

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

View File

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

View File

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

View File

@ -46,8 +46,8 @@ class FtestWorkFilter(filters.FilterSet):
fields = { fields = {
"material__process__name": ["exact", "contains"], "material__process__name": ["exact", "contains"],
"material": ["exact"], "material": ["exact"],
"wm": ["exact", "isnull"], "wm": ["exact"],
"mb": ["exact", "isnull"], "mb": ["exact"],
"batch": ["exact"], "batch": ["exact"],
"type": ["exact"], "type": ["exact"],
"type2": ["exact"], "type2": ["exact"],

View File

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

View File

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

View File

@ -337,7 +337,7 @@ class Ftest(CommonBDModel):
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user') User, verbose_name='操作人', on_delete=models.CASCADE, related_name='ftest_test_user')
check_user = models.ForeignKey( check_user = models.ForeignKey(
User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True) User, verbose_name='专检人', on_delete=models.CASCADE, related_name='ftest_check_user', null=True, blank=True)
is_ok = models.BooleanField('是否合格', default=True) is_ok = models.BooleanField('是否合格', default=False)
note = models.TextField('备注', default='', blank=True) note = models.TextField('备注', default='', blank=True)
ftest_work = models.ForeignKey( ftest_work = models.ForeignKey(
FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True) FtestWork, verbose_name='关联检验工作', on_delete=models.CASCADE, null=True, blank=True)
@ -353,6 +353,7 @@ 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)
@ -445,7 +446,7 @@ class Ptest(CommonAModel):
val_tg = models.FloatField("Tg", help_text='', null=True, blank=True) val_tg = models.FloatField("Tg", help_text='', null=True, blank=True)
val_tf = models.FloatField("Tf", help_text='', null=True, blank=True) val_tf = models.FloatField("Tf", help_text='', null=True, blank=True)
val_xj = models.CharField( val_xj = models.CharField(
'析晶', max_length=10, null=True, blank=True, choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS)) '析晶', max_length=10, default='S', choices=PTEST_XJ_VALS, help_text=list(PTEST_XJ_VALS))
val_pzxs = models.FloatField( val_pzxs = models.FloatField(
'膨胀系数', help_text='30-300℃', null=True, blank=True) '膨胀系数', help_text='30-300℃', null=True, blank=True)
val_zgwd = models.FloatField('升至最高温度', null=True, blank=True) val_zgwd = models.FloatField('升至最高温度', null=True, blank=True)

View File

@ -215,6 +215,7 @@ 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:
@ -226,7 +227,7 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
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:
@ -291,7 +292,7 @@ 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)
@ -300,7 +301,7 @@ class FtestSerializer(CustomModelSerializer):
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)
@ -363,7 +364,7 @@ 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)
@ -392,7 +393,7 @@ class FtestProcessSerializer(CustomModelSerializer):
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:

View File

@ -6,7 +6,6 @@ from django.utils import timezone
from apps.wf.models import Ticket from apps.wf.models import Ticket
from apps.qm.models import NotOkOption, Defect from apps.qm.models import NotOkOption, Defect
from apps.wpm.services_2 import ana_batch_thread from apps.wpm.services_2 import ana_batch_thread
from apps.inm.models import MaterialBatch
def ftestwork_submit_validate(ins: FtestWork): def ftestwork_submit_validate(ins: FtestWork):
wm:WMaterial = ins.wm wm:WMaterial = ins.wm
@ -24,7 +23,7 @@ def ftestwork_submit_validate(ins: FtestWork):
def ftestwork_submit(ins:FtestWork, user: User): def ftestwork_submit(ins:FtestWork, user: User):
wm:WMaterial = ins.wm wm:WMaterial = ins.wm
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins) fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
if wm and ins.need_update_wm: if ins.need_update_wm:
if ins.qct is None and not fwd_qs.exists(): if ins.qct is None and not fwd_qs.exists():
if wm.state == WMaterial.WM_TEST: if wm.state == WMaterial.WM_TEST:
# 更新对应的车间库存 # 更新对应的车间库存
@ -100,7 +99,7 @@ def ftestwork_submit(ins:FtestWork, user: User):
else: else:
wm:WMaterial = ins.wm wm:WMaterial = ins.wm
# 此时调用了qct表 # 此时调用了qct表
for item in fwd_qs: for item in FtestworkDefect.objects.filter(ftestwork=ins):
item:FtestworkDefect = item item:FtestworkDefect = item
if item.count > 0: if item.count > 0:
wm.count = wm.count - item.count wm.count = wm.count - item.count
@ -125,32 +124,6 @@ def ftestwork_submit(ins:FtestWork, user: User):
if not new_create: if not new_create:
wmx.count = wmx.count + item.count wmx.count = wmx.count + item.count
wmx.save() wmx.save()
if ins.mb:
mb:MaterialBatch = ins.mb
for item in fwd_qs:
item:FtestworkDefect = item
if item.count > 0:
mb.count = mb.count - item.count
if mb.count < 0:
raise ParseError("数量不足,扣减失败")
mb.save()
mbstate = WMaterial.WM_OK
if item.defect.okcate == Defect.DEFECT_NOTOK:
mbstate = WMaterial.WM_NOTOK
mbx, new_create = MaterialBatch.objects.get_or_create(
material=mb.material,
warehouse=mb.warehouse,
batch=mb.batch,
defect=item.defect,
state=mbstate,
defaults={
'count': item.count,
}
)
if not new_create:
mbx.count = mbx.count + item.count
mbx.save()
ins.submit_user = user ins.submit_user = user
ins.submit_time = timezone.now() ins.submit_time = timezone.now()
ins.save() ins.save()

View File

@ -33,6 +33,7 @@ class DefectViewSet(CustomModelViewSet):
filterset_fields = ["cate", "okcate"] filterset_fields = ["cate", "okcate"]
search_fields = ["name", "code"] search_fields = ["name", "code"]
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
QctDefect.objects.filter(defect=instance).delete() QctDefect.objects.filter(defect=instance).delete()
instance.delete() instance.delete()
@ -92,6 +93,7 @@ class QctDefectViewSet(CustomModelViewSet):
filterset_fields = ["qct", "defect"] filterset_fields = ["qct", "defect"]
ordering = ["qct", "sort"] ordering = ["qct", "sort"]
@transaction.atomic
def perform_create(self, serializer): def perform_create(self, serializer):
ins: QctDefect = serializer.save() ins: QctDefect = serializer.save()
if ins.is_default: if ins.is_default:
@ -149,6 +151,7 @@ class TestItemViewSet(CustomModelViewSet):
item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects]) item["affects_name"] = ";".join([affects_dict.get(x, '未知') for x in affects])
return data return data
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
QctTestItem.objects.filter(testitem=instance).delete() QctTestItem.objects.filter(testitem=instance).delete()
instance.delete() instance.delete()
@ -235,16 +238,19 @@ class FtestViewSet(CustomModelViewSet):
ftest_work.count_notok = all_count - ok_count ftest_work.count_notok = all_count - ok_count
ftest_work.save() ftest_work.save()
@transaction.atomic
def perform_create(self, serializer): def perform_create(self, serializer):
ins: Ftest = serializer.save() ins: Ftest = serializer.save()
if ins.ftest_work: if ins.ftest_work:
self.count_sampling(ins.ftest_work) self.count_sampling(ins.ftest_work)
@transaction.atomic
def perform_update(self, serializer): def perform_update(self, serializer):
ins: Ftest = serializer.save() ins: Ftest = serializer.save()
if ins.ftest_work: if ins.ftest_work:
self.count_sampling(ins.ftest_work) self.count_sampling(ins.ftest_work)
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
ftest_work = instance.ftest_work ftest_work = instance.ftest_work
instance.delete() instance.delete()
@ -278,32 +284,27 @@ class FtestWorkViewSet(CustomModelViewSet):
select_related_fields = ['material', 'mb', 'mb__material'] select_related_fields = ['material', 'mb', 'mb__material']
filterset_class = FtestWorkFilter filterset_class = FtestWorkFilter
@transaction.atomic
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
ins:FtestWork = self.get_object() ins:FtestWork = self.get_object()
partial = kwargs.pop('partial', False)
if ins.submit_time is not None: if ins.submit_time is not None:
raise ParseError('已提交无法修改') raise ParseError('已提交无法修改')
if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START: if ins.ticket and ins.ticket.state.type != State.STATE_TYPE_START:
raise ParseError('审批单已进行,无法修改') raise ParseError('审批单已进行,无法修改')
serializer = self.get_serializer(ins, data=request.data, partial=partial) x = super().update(request, *args, **kwargs)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
# 触发批次统计分析 # 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch]) ana_batch_thread(xbatchs=[ins.batch])
return Response(serializer.data) return x
@transaction.atomic
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
ins:FtestWork = self.get_object() ins:FtestWork = self.get_object()
if ins.submit_time is not None: if ins.submit_time is not None:
raise ParseError('已提交无法删除') raise ParseError('已提交无法删除')
if ins.ticket: if ins.ticket:
raise ParseError('存在审批, 无法删除') raise ParseError('存在审批, 无法删除')
self.perform_destroy(ins) x = super().destroy(request, *args, **kwargs)
# 触发批次统计分析 # 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch]) ana_batch_thread(xbatchs=[ins.batch])
return Response(status=204) return x
def perform_create(self, serializer): def perform_create(self, serializer):
ins = serializer.save() ins = serializer.save()
@ -321,8 +322,8 @@ class FtestWorkViewSet(CustomModelViewSet):
ins:FtestWork = self.get_object() ins:FtestWork = self.get_object()
if ins.ticket: if ins.ticket:
raise ParseError('该检验工作存在审批!') raise ParseError('该检验工作存在审批!')
if ins.wm is None and ins.mb is None: if ins.wm is None:
raise ParseError('该检验工作未关联库存') raise ParseError('该检验工作未关联车间库存')
if ins.submit_time is None: if ins.submit_time is None:
ftestwork_submit(ins, request.user) ftestwork_submit(ins, request.user)
else: else:

View File

@ -65,6 +65,7 @@ class OrderViewSet(CustomModelViewSet):
"state": ["exact", "in"], "state": ["exact", "in"],
} }
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
if instance.state != Order.ORDER_CREATE: if instance.state != Order.ORDER_CREATE:
raise ParseError('订单非创建中不可删除') raise ParseError('订单非创建中不可删除')

View File

@ -275,11 +275,13 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
model = Dept model = Dept
exclude = EXCLUDE_FIELDS + ['third_info'] exclude = EXCLUDE_FIELDS + ['third_info']
@transaction.atomic
def create(self, validated_data): def create(self, validated_data):
ins = super().create(validated_data) ins = super().create(validated_data)
sync_dahua_dept(ins) sync_dahua_dept(ins)
return ins return ins
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
ins = super().update(instance, validated_data) ins = super().update(instance, validated_data)
sync_dahua_dept(ins) sync_dahua_dept(ins)

View File

@ -8,7 +8,8 @@ from django_celery_beat.models import (CrontabSchedule, IntervalSchedule,
from django_celery_results.models import TaskResult from django_celery_results.models import TaskResult
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied from rest_framework.exceptions import ParseError, ValidationError, PermissionDenied
from rest_framework.mixins import RetrieveModelMixin from rest_framework.mixins import (CreateModelMixin, DestroyModelMixin,
ListModelMixin, RetrieveModelMixin)
from rest_framework.parsers import (JSONParser, from rest_framework.parsers import (JSONParser,
MultiPartParser) MultiPartParser)
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
@ -19,7 +20,7 @@ from apps.hrm.models import Employee
from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG from apps.system.errors import OLD_PASSWORD_WRONG, PASSWORD_NOT_SAME, SCHEDULE_WRONG
from apps.system.filters import DeptFilterSet, UserFilterSet from apps.system.filters import DeptFilterSet, UserFilterSet
# from django_q.models import Task as QTask, Schedule as QSchedule # from django_q.models import Task as QTask, Schedule as QSchedule
from apps.utils.mixins import (MyLoggingMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin) from apps.utils.mixins import (CustomCreateModelMixin, MyLoggingMixin)
from django.conf import settings from django.conf import settings
from apps.utils.permission import ALL_PERMS from apps.utils.permission import ALL_PERMS
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
@ -228,7 +229,7 @@ class PTaskViewSet(CustomModelViewSet):
return Response() return Response()
class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet): class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
""" """
list:任务执行结果列表 list:任务执行结果列表
@ -372,7 +373,7 @@ class RoleViewSet(CustomModelViewSet):
ordering = ['create_time'] ordering = ['create_time']
class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet): class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
"""岗位/角色关系 """岗位/角色关系
岗位/角色关系 岗位/角色关系
@ -384,7 +385,7 @@ class PostRoleViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
filterset_fields = ['post', 'role'] filterset_fields = ['post', 'role']
class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet): class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet):
"""用户/岗位关系 """用户/岗位关系
用户/岗位关系 用户/岗位关系
@ -397,6 +398,7 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
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(
@ -416,6 +418,7 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
ep.save() ep.save()
def perform_destroy(self, instance): def perform_destroy(self, instance):
with transaction.atomic():
user = instance.user user = instance.user
instance.delete() instance.delete()
up = UserPost.objects.filter(user=user).order_by( up = UserPost.objects.filter(user=user).order_by(
@ -607,7 +610,7 @@ class UserViewSet(CustomModelViewSet):
return Response() return Response()
class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet): class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
"""文件上传 """文件上传
list: list:
@ -648,7 +651,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin
instance.save() instance.save()
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet): class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
perms_map = {'get': '*', 'post': 'apk.upload'} perms_map = {'get': '*', 'post': 'apk.upload'}
serializer_class = ApkSerializer serializer_class = ApkSerializer
@ -689,7 +692,7 @@ class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, Gen
return Response() return Response()
class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet): class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
perms_map = {'get': '*', 'post': '*', perms_map = {'get': '*', 'post': '*',
'delete': 'myschedule.delete'} 'delete': 'myschedule.delete'}
serializer_class = MyScheduleSerializer serializer_class = MyScheduleSerializer
@ -714,6 +717,7 @@ class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyM
return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}") return get_description(f"{data['minute']} {data['hour']} {data['day_of_month']} {data['month_of_year']} {data['day_of_week']}")
return '' return ''
@transaction.atomic
def perform_create(self, serializer): def perform_create(self, serializer):
vdata = serializer.validated_data vdata = serializer.validated_data
vdata['create_by'] = self.request.user # 不可少 vdata['create_by'] = self.request.user # 不可少

View File

@ -9,6 +9,7 @@ from django.utils.timezone import now
from user_agents import parse from user_agents import parse
import logging import logging
from rest_framework.response import Response from rest_framework.response import Response
from django.db import transaction
from rest_framework.exceptions import ParseError, ValidationError from rest_framework.exceptions import ParseError, ValidationError
from apps.utils.errors import PKS_ERROR from apps.utils.errors import PKS_ERROR
from rest_framework.generics import get_object_or_404 from rest_framework.generics import get_object_or_404
@ -18,7 +19,6 @@ from apps.utils.serializers import PkSerializer
from rest_framework.decorators import action from rest_framework.decorators import action
from apps.utils.serializers import ComplexSerializer from apps.utils.serializers import ComplexSerializer
from django.db.models import F from django.db.models import F
from django.db import transaction
# 实例化myLogger # 实例化myLogger
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
@ -82,7 +82,6 @@ class BulkCreateModelMixin(CreateModelMixin):
def after_bulk_create(self, objs): def after_bulk_create(self, objs):
pass pass
@transaction.atomic
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
"""创建(支持批量) """创建(支持批量)
@ -92,6 +91,7 @@ class BulkCreateModelMixin(CreateModelMixin):
many = False many = False
if isinstance(rdata, list): if isinstance(rdata, list):
many = True many = True
with transaction.atomic():
sr = self.get_serializer(data=rdata, many=many) sr = self.get_serializer(data=rdata, many=many)
sr.is_valid(raise_exception=True) sr.is_valid(raise_exception=True)
self.perform_create(sr) self.perform_create(sr)
@ -105,7 +105,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
def after_bulk_update(self, objs): def after_bulk_update(self, objs):
pass pass
@transaction.atomic
def partial_update(self, request, *args, **kwargs): def partial_update(self, request, *args, **kwargs):
"""部分更新(支持批量) """部分更新(支持批量)
@ -114,7 +113,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
kwargs['partial'] = True kwargs['partial'] = True
return self.update(request, *args, **kwargs) return self.update(request, *args, **kwargs)
@transaction.atomic
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
"""更新(支持批量) """更新(支持批量)
@ -126,6 +124,7 @@ class BulkUpdateModelMixin(UpdateModelMixin):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset())
objs = [] objs = []
if isinstance(request.data, list): if isinstance(request.data, list):
with transaction.atomic():
for ind, item in enumerate(request.data): for ind, item in enumerate(request.data):
obj = get_object_or_404(queryset, id=item['id']) obj = get_object_or_404(queryset, id=item['id'])
sr = self.get_serializer(obj, data=item, partial=partial) sr = self.get_serializer(obj, data=item, partial=partial)
@ -149,7 +148,6 @@ class BulkUpdateModelMixin(UpdateModelMixin):
class BulkDestroyModelMixin(DestroyModelMixin): class BulkDestroyModelMixin(DestroyModelMixin):
@swagger_auto_schema(request_body=PkSerializer) @swagger_auto_schema(request_body=PkSerializer)
@transaction.atomic
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
"""删除(支持批量) """删除(支持批量)
@ -195,8 +193,6 @@ class CustomRetrieveModelMixin(RetrieveModelMixin):
给dict返回数据添加额外信息 给dict返回数据添加额外信息
""" """
if hasattr(self, 'add_info_for_list'):
return self.add_info_for_list([data])[0]
return data return data
class CustomListModelMixin(ListModelMixin): class CustomListModelMixin(ListModelMixin):
@ -245,8 +241,6 @@ class ComplexQueryMixin:
vdata = sr.validated_data vdata = sr.validated_data
queryset = self.get_queryset() queryset = self.get_queryset()
querys = vdata.get('querys', []) querys = vdata.get('querys', [])
annotate_field_list = vdata.get('annotate_field_list', [])
if not querys: if not querys:
new_qs = queryset new_qs = queryset
else: else:
@ -268,29 +262,17 @@ 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('-'):
# JSONField 排序只能用字符串,不要 F new_qs = new_qs.order_by(F(item[1:]).desc(nulls_last=True))
order_fields.append(F(item[1:]).desc(nulls_last=True) if '__' not in item else item)
else: else:
order_fields.append(F(item).asc(nulls_last=True) if '__' not in item else item) new_qs = new_qs.order_by(item)
new_qs = new_qs.order_by(*order_fields)
page = self.paginate_queryset(new_qs) page = self.paginate_queryset(new_qs)
if page is not None: if page is not None:
serializer = self.get_serializer(page, many=True) serializer = self.get_serializer(page, many=True)
@ -343,7 +325,6 @@ class MyLoggingMixin(object):
response = super().finalize_response( response = super().finalize_response(
request, response, *args, **kwargs request, response, *args, **kwargs
) )
self.log["response_ms"] = self._get_response_ms()
# Ensure backward compatibility for those using _should_log hook # Ensure backward compatibility for those using _should_log hook
should_log = ( should_log = (
self._should_log if hasattr(self, "_should_log") else self.should_log self._should_log if hasattr(self, "_should_log") else self.should_log
@ -372,7 +353,7 @@ class MyLoggingMixin(object):
"method": request.method, "method": request.method,
"query_params": self._clean_data(request.query_params.dict()), "query_params": self._clean_data(request.query_params.dict()),
"user": self._get_user(request), "user": self._get_user(request),
# "response_ms": self._get_response_ms(), "response_ms": self._get_response_ms(),
"response": self._clean_data(rendered_content), "response": self._clean_data(rendered_content),
"status_code": response.status_code, "status_code": response.status_code,
"agent": self._get_agent(request), "agent": self._get_agent(request),
@ -466,8 +447,7 @@ class MyLoggingMixin(object):
By default, check if the request method is in logging_methods. By default, check if the request method is in logging_methods.
""" """
return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \ return self.logging_methods == "__all__" or response.status_code > 404 or response.status_code == 400 \
or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])\ or (request.method in self.logging_methods and response.status_code not in [401, 403, 404])
or (self.log.get("response_ms", 0) > 2000)
def _clean_data(self, data): def _clean_data(self, data):
""" """

View File

@ -116,26 +116,11 @@ class BaseModel(models.Model):
@classmethod @classmethod
def safe_get_or_create(cls, defaults=None, **kwargs): def safe_get_or_create(cls, defaults=None, **kwargs):
defaults = defaults or {} defaults = defaults or {}
lock_data = {**kwargs, **defaults}
for attempt in range(3): lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest()
try: lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
# 先尝试获取(带锁) with cache.lock(lock_key, timeout=10):
try: return cls.objects.get_or_create(**kwargs, defaults=defaults)
obj = cls.objects.select_for_update().get(**kwargs)
return obj, False
except cls.DoesNotExist:
# 不存在则创建
obj = cls(**kwargs, **defaults)
obj.save()
return obj, True
except IntegrityError:
# 发生唯一约束冲突时重试
if attempt == 2:
raise
time.sleep(0.1 * (attempt + 1))
except Exception:
# 其他异常直接抛出
raise
def handle_parent(self): def handle_parent(self):
@ -147,7 +132,7 @@ class BaseModel(models.Model):
if not self.id: if not self.id:
is_create = True is_create = True
self.id = idWorker.get_id() self.id = idWorker.get_id()
with transaction.atomic():
old_parent = None old_parent = None
need_handle_parent = False need_handle_parent = False
if hasattr(self, "parent"): if hasattr(self, "parent"):

View File

@ -85,4 +85,3 @@ class ComplexSerializer(serializers.Serializer):
ordering = serializers.CharField(required=False) ordering = serializers.CharField(required=False)
querys = serializers.ListField(child=QuerySerializer( querys = serializers.ListField(child=QuerySerializer(
many=True), label="查询列表", required=False) many=True), label="查询列表", required=False)
annotate_field_list = serializers.ListField(child=serializers.CharField(), label="RawSQL字段列表", required=False)

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=False) @auto_log(name='阿里云短信', raise_exception=True, send_mail=True)
def send_sms(phone: str, template_code: int, template_param: dict): def send_sms(phone: str, template_code: int, template_param: dict):
from aliyunsdkcore.client import AcsClient from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest from aliyunsdkcore.request import CommonRequest

View File

@ -1,6 +1,4 @@
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数据
@ -25,7 +23,7 @@ def format_sqldata(columns, rows):
return [columns] + rows, [dict(zip(columns, row)) for row in rows] return [columns] + rows, [dict(zip(columns, row)) for row in rows]
def query_all_dict(sql, params=None, with_time_format=False): def query_all_dict(sql, params=None):
''' '''
查询所有结果返回字典类型数据 查询所有结果返回字典类型数据
:param sql: :param sql:
@ -38,19 +36,9 @@ def query_all_dict(sql, params=None, with_time_format=False):
else: else:
cursor.execute(sql) cursor.execute(sql)
columns = [desc[0] for desc in cursor.description] columns = [desc[0] for desc in cursor.description]
if with_time_format:
results = []
for row in cursor.fetchall():
row_dict = {}
for col, val in zip(columns, row):
if isinstance(val, datetime):
val = timezone.make_naive(val).strftime("%Y-%m-%d %H:%M:%S")
row_dict[col] = val
results.append(row_dict)
return results
return [dict(zip(columns, row)) for row in cursor.fetchall()] return [dict(zip(columns, row)) for row in cursor.fetchall()]
def query_one_dict(sql, params=None, with_time_format=False): def query_one_dict(sql, params=None):
""" """
查询一个结果返回字典类型数据 查询一个结果返回字典类型数据
:param sql: :param sql:
@ -61,13 +49,6 @@ def query_one_dict(sql, params=None, with_time_format=False):
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

@ -1,6 +1,6 @@
from django.core.cache import cache from django.core.cache import cache
from django.http import StreamingHttpResponse, Http404 from django.http import StreamingHttpResponse
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.mixins import RetrieveModelMixin from rest_framework.mixins import RetrieveModelMixin
@ -18,9 +18,7 @@ from apps.utils.serializers import ComplexSerializer
from rest_framework.throttling import UserRateThrottle from rest_framework.throttling import UserRateThrottle
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
import json import json
from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import NotSupportedError
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
""" """
@ -91,36 +89,6 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
elif hash_v_e: elif hash_v_e:
return Response(hash_v_e) return Response(hash_v_e)
def get_object(self, force_lock=False):
"""
智能加锁的get_object
- 只读请求普通查询
- 非只读请求且在事务中加锁查询
- 非只读请求但不在事务中普通查询带警告
"""
# 只读方法列表
read_only_methods = ['GET', 'HEAD', 'OPTIONS']
if self.request.method not in read_only_methods and connection.in_atomic_block:
if force_lock:
raise ParseError("当前操作需要在事务中进行,请使用事务装饰器")
# 非只读请求且在事务中:加锁查询
queryset = self.filter_queryset(self.get_queryset())
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
try:
obj = queryset.get(**filter_kwargs)
l_obj = queryset.model.objects.select_for_update().get(pk=obj.pk)
self.check_object_permissions(self.request, l_obj)
return l_obj
except queryset.model.DoesNotExist:
raise Http404
else:
# 其他情况:普通查询
return super().get_object()
def get_serializer_class(self): def get_serializer_class(self):
action_serializer_name = f"{self.action}_serializer_class" action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None) action_serializer_class = getattr(self, action_serializer_name, None)
@ -226,3 +194,4 @@ class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListM
""" """
增强的ModelViewSet 增强的ModelViewSet
""" """
pass

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-19 01:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_state_filter_dept'),
]
operations = [
migrations.AddField(
model_name='workflow',
name='view_path',
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-10-16 08:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0003_workflow_view_path'),
]
operations = [
migrations.AddField(
model_name='workflow',
name='view_path2',
field=models.TextField(blank=True, null=True, verbose_name='前端自定义页面路径2'),
),
]

View File

@ -21,8 +21,6 @@ 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', 'view_path'] fields = ['id', 'name', 'key']
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().order_by("-attribute_type", "-id") return Transition.objects.filter(is_deleted=False, source_state=state).all()
@classmethod @classmethod
def get_ticket_steps(cls, ticket: Ticket): def get_ticket_steps(cls, ticket: Ticket):
@ -442,18 +442,12 @@ 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):
# 如果状态变化或是转交加签的情况再发送通知 # 如果状态变化或是转交加签的情况再发送通知
cls.send_ticket_notice(ticketflow=last_log) Thread(target=send_ticket_notice_t, args=(ticket,), daemon=True).start()
# 如果目标状态是脚本则异步执行 # 如果目标状态是脚本则异步执行
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):
# 定时任务触发的工单关闭 # 定时任务触发的工单关闭
@ -485,13 +479,10 @@ 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)
cls.task_ticket(ticket=ticket) def send_ticket_notice_t(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,7 +239,6 @@ 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):
""" """
新建工单 新建工单
@ -264,7 +263,7 @@ 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(),
@ -298,7 +297,6 @@ 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):
""" """
处理工单 处理工单
@ -309,13 +307,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):
""" """
转交工单 转交工单
@ -327,15 +325,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': '*'})
@ -383,12 +381,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
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='', 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('无需接单')
@ -423,12 +420,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save() ticket.save()
# 更新流转记录 # 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签说明 suggestion = request.data.get('suggestion', '') # 加签说明
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=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)
@ -448,12 +444,11 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R
ticket.save() ticket.save()
# 更新流转记录 # 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签意见 suggestion = request.data.get('suggestion', '') # 加签意见
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=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

@ -9,6 +9,7 @@ def correct_tuihuo_log():
mlogs = Mlog.objects.filter(mgroup__name='管料退火', count_use=0) mlogs = Mlog.objects.filter(mgroup__name='管料退火', count_use=0)
for mlog in mlogs: for mlog in mlogs:
try: try:
with transaction.atomic():
count_use = mlog.count_ok + mlog.count_notok count_use = mlog.count_ok + mlog.count_notok
material_has = WMaterial.objects.get( material_has = WMaterial.objects.get(
batch=mlog.batch, material=mlog.material_in, belong_dept=mlog.mgroup.belong_dept) batch=mlog.batch, material=mlog.material_in, belong_dept=mlog.mgroup.belong_dept)

View File

@ -61,7 +61,8 @@ class WMaterialFilter(filters.FilterSet):
qs = queryset.filter(material__id__in=matoutIds).exclude(state=WMaterial.WM_REPAIR)|queryset.filter(state=WMaterial.WM_REPAIRED, mgroup__id=mgroupId) qs = queryset.filter(material__id__in=matoutIds).exclude(state=WMaterial.WM_REPAIR)|queryset.filter(state=WMaterial.WM_REPAIRED, mgroup__id=mgroupId)
elif value == "canfix": elif value == "canfix":
matoutIds = process.get_canout_mat_ids() matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(state=WMaterial.WM_REPAIR, mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK) qs = queryset.filter(state=WMaterial.WM_REPAIR, mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK).exclude(
state=WMaterial.WM_REPAIR).exclude(state=WMaterial.WM_REPAIRED)
return qs return qs
else: else:
raise ParseError("请提供工段查询条件") raise ParseError("请提供工段查询条件")
@ -185,7 +186,6 @@ class MlogbFilter(filters.FilterSet):
"wm_in__state": ["exact"], "wm_in__state": ["exact"],
"material_in": ["exact", "isnull"], "material_in": ["exact", "isnull"],
"material_out": ["exact", "isnull"], "material_out": ["exact", "isnull"],
"parent": ["exact", "isnull"],
} }
def filter_type(self, queryset, name, value): def filter_type(self, queryset, name, value):

View File

@ -1,24 +0,0 @@
# Generated by Django 3.2.12 on 2025-08-27 07:22
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('wpm', '0121_auto_20250723_0912'),
]
operations = [
migrations.AlterField(
model_name='mlog',
name='create_time',
field=models.DateTimeField(db_index=True, default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间'),
),
migrations.AlterField(
model_name='mlog',
name='handle_date',
field=models.DateField(blank=True, db_index=True, null=True, verbose_name='操作日期'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-09-04 08:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wpm', '0122_auto_20250827_1522'),
]
operations = [
migrations.AddField(
model_name='mlogbdefect',
name='count_has',
field=models.DecimalField(decimal_places=1, default=0, max_digits=11, verbose_name='含有该缺陷的数量'),
),
]

View File

@ -1,25 +0,0 @@
# 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

@ -15,8 +15,6 @@ from django.db import transaction
from django.db.models import Max from django.db.models import Max
import re import re
from django.db.models import Q from django.db.models import Q
import django.utils.timezone as timezone
from apps.utils.sql import query_all_dict
# Create your models here. # Create your models here.
class SfLog(CommonADModel): class SfLog(CommonADModel):
@ -200,7 +198,7 @@ class Mlog(CommonADModel):
mtask = models.ForeignKey( mtask = models.ForeignKey(
Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask') Mtask, verbose_name='关联任务', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_mtask')
mgroup = models.ForeignKey( mgroup = models.ForeignKey(
Mgroup, verbose_name='工段', on_delete=models.CASCADE, null=True, blank=True, db_index=True) Mgroup, verbose_name='工段', on_delete=models.CASCADE, null=True, blank=True)
wm_in = models.ForeignKey(WMaterial, verbose_name='投入物料所在库存', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_wm_in') wm_in = models.ForeignKey(WMaterial, verbose_name='投入物料所在库存', on_delete=models.SET_NULL, null=True, blank=True, related_name='mlog_wm_in')
material_in = models.ForeignKey( material_in = models.ForeignKey(
Material, verbose_name='消耗物', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_material_in') Material, verbose_name='消耗物', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_material_in')
@ -269,10 +267,10 @@ class Mlog(CommonADModel):
count_n_b = models.DecimalField('', default=0, max_digits=11, decimal_places=1) # 光芯七车间 count_n_b = models.DecimalField('', default=0, max_digits=11, decimal_places=1) # 光芯七车间
count_n_qt = models.DecimalField('其他', default=0, max_digits=11, decimal_places=1) count_n_qt = models.DecimalField('其他', default=0, max_digits=11, decimal_places=1)
handle_date = models.DateField('操作日期', null=True, blank=True, db_index=True) handle_date = models.DateField('操作日期', null=True, blank=True)
team = models.ForeignKey(Team, verbose_name='班组', on_delete=models.SET_NULL, null=True, blank=True) team = models.ForeignKey(Team, verbose_name='班组', on_delete=models.SET_NULL, null=True, blank=True)
handle_user = models.ForeignKey( handle_user = models.ForeignKey(
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True, db_index=True) # 成型人 User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True) # 成型人
handle_user_2 = models.ForeignKey( handle_user_2 = models.ForeignKey(
User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True) # 切料人 User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True) # 切料人
handle_users = models.ManyToManyField( handle_users = models.ManyToManyField(
@ -294,8 +292,6 @@ class Mlog(CommonADModel):
test_user = models.ForeignKey( test_user = models.ForeignKey(
User, verbose_name='检验人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_test_user') User, verbose_name='检验人', on_delete=models.CASCADE, null=True, blank=True, related_name='mlog_test_user')
test_time = models.DateTimeField('检验时间', null=True, blank=True) test_time = models.DateTimeField('检验时间', null=True, blank=True)
create_time = models.DateTimeField(
default=timezone.now, verbose_name='创建时间', help_text='创建时间', db_index=True)
@property @property
def mlogb(self): def mlogb(self):
@ -368,9 +364,6 @@ 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('操作日期')
@ -480,7 +473,6 @@ class MlogbDefect(BaseModel):
mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE) mlogb = models.ForeignKey(Mlogb, verbose_name='生产记录', on_delete=models.CASCADE)
defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True) defect = models.ForeignKey("qm.Defect", verbose_name='缺陷', on_delete=models.CASCADE, null=True, blank=True)
count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1) count = models.DecimalField('数量', default=0, max_digits=11, decimal_places=1)
count_has = models.DecimalField('含有该缺陷的数量', default=0, max_digits=11, decimal_places=1)
@classmethod @classmethod
def get_defect_qs(cls, ftype="all"): def get_defect_qs(cls, ftype="all"):
@ -516,7 +508,7 @@ class Mlogbw(BaseModel):
@classmethod @classmethod
def cal_count_notok(cls, mlogb: Mlogb): def cal_count_notok(cls, mlogb: Mlogb):
from apps.qm.models import Defect, FtestDefect from apps.qm.models import Defect
# 锁定mlogb以防止并发修改 # 锁定mlogb以防止并发修改
# mlogb:Mlogb = Mlogb.objects.select_for_update().get(pk=mlogb.pk) # mlogb:Mlogb = Mlogb.objects.select_for_update().get(pk=mlogb.pk)
count = Mlogbw.objects.filter(mlogb=mlogb).count() count = Mlogbw.objects.filter(mlogb=mlogb).count()
@ -527,15 +519,10 @@ 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类不分开这里会导致count_ok_full与count_ok一样了暂时不做处理 tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__is_ok=False)
tqs = Mlogbw.objects.filter(mlogb=mlogb, ftest__defect_main__isnull=False) tqs_a = Mlogbw.objects.filter(mlogb=mlogb, ftest__is_ok=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')) defects = {defect.id: defect for defect in Defect.objects.filter(id__in=tqs.values_list("ftest__defect_main", flat=True))}
defect_ids = tqs.values_list("ftest__defect_main", flat=True)
tqs_has_a = FtestDefect.objects.filter(ftest__mlogbw_ftest__mlogb=mlogb, has=True).values("defect").annotate(xcount=Count('id'))
defect_ids = defect_ids.union(tqs_has_a.values_list("defect", flat=True))
defects = {defect.id: defect for defect in Defect.objects.filter(id__in=defect_ids)}
md_ids = [] md_ids = []
MlogbDefect.objects.filter(mlogb=mlogb).delete()
for t in tqs_a: for t in tqs_a:
md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t["ftest__defect_main"]]) md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t["ftest__defect_main"]])
md.count = t["xcount"] md.count = t["xcount"]
@ -544,13 +531,7 @@ class Mlogbw(BaseModel):
count_notok += t["xcount"] count_notok += t["xcount"]
if defects[t["ftest__defect_main"]].okcate != 10: if defects[t["ftest__defect_main"]].okcate != 10:
count_notok_full += t["xcount"] count_notok_full += t["xcount"]
MlogbDefect.objects.filter(mlogb=mlogb).exclude(id__in=md_ids).delete()
for t2 in tqs_has_a:
md, _ = MlogbDefect.objects.get_or_create(mlogb=mlogb, defect=defects[t2["defect"]], defaults={"count": 0})
md.count_has = t2["xcount"]
md.save()
md_ids.append(md.id)
mlogb.count_notok = count_notok mlogb.count_notok = count_notok
mlogb.count_ok = count - mlogb.count_notok mlogb.count_ok = count - mlogb.count_notok
mlogb.count_ok_full = count - count_notok_full mlogb.count_ok_full = count - count_notok_full
@ -741,6 +722,7 @@ class BatchSt(BaseModel):
# return ins, True # return ins, True
@classmethod @classmethod
@transaction.atomic
def init_dag(cls, batch:str): def init_dag(cls, batch:str):
""" """
更新批次数据关系链(初步) 更新批次数据关系链(初步)
@ -818,30 +800,7 @@ class BatchLog(BaseModel):
@classmethod @classmethod
def batches_to(cls, batch:str): def batches_to(cls, batch:str):
batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct()
# query = """
# SELECT batch FROM wpm_batchst
# WHERE batch ~ %s
# """
query = """
SELECT batch
FROM wpm_batchst
WHERE batch ~ %s
ORDER BY
-- 先按前缀部分排序例如 'A'
SUBSTRING(batch FROM '^(.*)-') DESC,
-- 再按后缀的数值部分排序 '2', '11' 转为整数
CAST(SUBSTRING(batch FROM '-([0-9]+)$') AS INTEGER) DESC
""" # 排序可在sql层处理
query_ = """SELECT batch FROM wpm_batchst WHERE batch ~ %s"""
pattern = f'^{batch}-[0-9]+$'
"""可以用如下方法直接查询
"""
# batches = BatchLog.objects.filter(source__batch=batch, relation_type="split").values_list("target__batch", flat=True).distinct()
# batches = sorted(list(batches), key=custom_key)
batches_r = query_all_dict(query_, params=(pattern,))
batches = [b["batch"] for b in batches_r]
batches = sorted(list(batches), key=custom_key) batches = sorted(list(batches), key=custom_key)
last_batch_num = None last_batch_num = None
if batches: if batches:

View File

@ -1,6 +1,6 @@
from apps.wpm.models import BatchSt from apps.wpm.models import BatchSt
import logging import logging
from apps.wpm.models import Mlogb, Mlogbw, MlogbDefect from apps.wpm.models import Mlogb, MlogbDefect
from apps.mtm.models import Mgroup from apps.mtm.models import Mgroup
import decimal import decimal
from django.db.models import Sum from django.db.models import Sum
@ -41,14 +41,10 @@ def main(batch: str, mgroup_obj:Mgroup=None):
for item in mlogb1_qs: for item in mlogb1_qs:
# 找到对应的输入 # 找到对应的输入
mlogb_from:Mlogb = item.mlogb_from mlogb_from:Mlogb = item.mlogb_from
mlogbw_from:Mlogbw = item.mlogbw_from
if mlogb_from: if mlogb_from:
mlogb_q_ids.append(mlogb_from.id) mlogb_q_ids.append(mlogb_from.id)
data[f"{mgroup_name}_count_use"] += mlogb_from.count_use data[f"{mgroup_name}_count_use"] += mlogb_from.count_use
data[f"{mgroup_name}_count_pn_jgqbl"] += mlogb_from.count_pn_jgqbl data[f"{mgroup_name}_count_pn_jgqbl"] += mlogb_from.count_pn_jgqbl
if mlogbw_from:
data[f"{mgroup_name}_count_use"] += 1
data[f"{mgroup_name}_count_pn_jgqbl"] += 0
if item.mlog.handle_user: if item.mlog.handle_user:
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user) data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
if item.mlog.handle_date: if item.mlog.handle_date:
@ -69,7 +65,6 @@ def main(batch: str, mgroup_obj:Mgroup=None):
data[f"{mgroup_name}_合格率"] = 0 data[f"{mgroup_name}_合格率"] = 0
mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count")) mlogbd1_qs = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count__gt=0).values("defect__name").annotate(total=Sum("count"))
mlogbd1_qs_x = MlogbDefect.objects.filter(mlogb__in=mlogb1_qs, count_has__gt=0).values("defect__name").annotate(total=Sum("count_has"))
mlogbd1_q_qs = MlogbDefect.objects.filter(mlogb__id__in=mlogb_q_ids, count__gt=0).values("defect__name").annotate(total=Sum("count")) mlogbd1_q_qs = MlogbDefect.objects.filter(mlogb__id__in=mlogb_q_ids, count__gt=0).values("defect__name").annotate(total=Sum("count"))
for item in mlogbd1_q_qs: for item in mlogbd1_q_qs:
@ -80,10 +75,6 @@ def main(batch: str, mgroup_obj:Mgroup=None):
data[f"{mgroup_name}_缺陷_{item['defect__name']}"] = item["total"] data[f"{mgroup_name}_缺陷_{item['defect__name']}"] = item["total"]
data[f"{mgroup_name}_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2) data[f"{mgroup_name}_缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
for item in mlogbd1_qs_x:
data[f"{mgroup_name}_含缺陷_{item['defect__name']}"] = item["total"]
data[f"{mgroup_name}_含缺陷_{item['defect__name']}_比例"] = round((item["total"] / data[f"{mgroup_name}_count_real"])*100, 2)
data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"])) data[f"{mgroup_name}_日期"] = list(set(data[f"{mgroup_name}_日期"]))
data[f"{mgroup_name}_小日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d") data[f"{mgroup_name}_小日期"] = max(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")
data[f"{mgroup_name}_大日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d") data[f"{mgroup_name}_大日期"] = min(data[f"{mgroup_name}_日期"]).strftime("%Y-%m-%d")

View File

@ -29,7 +29,6 @@ def main(batch: str, mgroup_obj):
if mlogb1_qs.exists(): if mlogb1_qs.exists():
data[f"{mgroup_name}_日期"] = [] data[f"{mgroup_name}_日期"] = []
data[f"{mgroup_name}_操作人"] = [] data[f"{mgroup_name}_操作人"] = []
data[f"{mgroup_name}_班次"] = []
data[f"{mgroup_name}_count_use"] = 0 data[f"{mgroup_name}_count_use"] = 0
data[f"{mgroup_name}_count_real"] = 0 data[f"{mgroup_name}_count_real"] = 0
data[f"{mgroup_name}_count_ok"] = 0 data[f"{mgroup_name}_count_ok"] = 0
@ -48,8 +47,6 @@ def main(batch: str, mgroup_obj):
data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user) data[f"{mgroup_name}_操作人"].append(item.mlog.handle_user)
if item.mlog.handle_date: if item.mlog.handle_date:
data[f"{mgroup_name}_日期"].append(item.mlog.handle_date) data[f"{mgroup_name}_日期"].append(item.mlog.handle_date)
if item.mlog.shift:
data[f"{mgroup_name}_班次"].append(item.mlog.shift.name)
data[f"{mgroup_name}_count_real"] += item.count_real data[f"{mgroup_name}_count_real"] += item.count_real
data[f"{mgroup_name}_count_ok"] += item.count_ok data[f"{mgroup_name}_count_ok"] += item.count_ok
data[f"{mgroup_name}_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0 data[f"{mgroup_name}_count_ok_full"] += item.count_ok_full if item.count_ok_full else 0
@ -83,8 +80,6 @@ def main(batch: str, mgroup_obj):
data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]]) data[f"{mgroup_name}_日期"] = ";".join([item.strftime("%Y-%m-%d") for item in data[f"{mgroup_name}_日期"]])
data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"])) data[f"{mgroup_name}_操作人"] = list(set(data[f"{mgroup_name}_操作人"]))
data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]]) data[f"{mgroup_name}_操作人"] = ";".join([item.name for item in data[f"{mgroup_name}_操作人"]])
data[f"{mgroup_name}_班次"] = list(set(data[f"{mgroup_name}_班次"]))
data[f"{mgroup_name}_班次"] = ";".join([item for item in data[f"{mgroup_name}_班次"]])
mlogb2_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False, mlogb2_qs = Mlogb.objects.filter(mlog__submit_time__isnull=False,

View File

@ -26,8 +26,6 @@ import logging
from apps.qm.models import Defect from apps.qm.models import Defect
from apps.utils.snowflake import idWorker from apps.utils.snowflake import idWorker
from decimal import Decimal from decimal import Decimal
from apps.em.models import Equipment
from django.db.models import Q
mylogger = logging.getLogger("log") mylogger = logging.getLogger("log")
class OtherLogSerializer(CustomModelSerializer): class OtherLogSerializer(CustomModelSerializer):
@ -52,6 +50,7 @@ class StLogSerializer(CustomModelSerializer):
def create(self, validated_data): def create(self, validated_data):
current_sflog_id = validated_data.pop('current_sflog') current_sflog_id = validated_data.pop('current_sflog')
current_note = validated_data.pop('current_note', '') current_note = validated_data.pop('current_note', '')
with transaction.atomic():
sflog = get_sflog( sflog = get_sflog(
validated_data['mgroup'], validated_data['start_time']) validated_data['mgroup'], validated_data['start_time'])
if current_sflog_id != sflog.id: if current_sflog_id != sflog.id:
@ -66,6 +65,9 @@ class StLogSerializer(CustomModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
if instance.is_shutdown: # 停机不可编辑end_time if instance.is_shutdown: # 停机不可编辑end_time
validated_data.pop('end_time', None) validated_data.pop('end_time', None)
# if instance.end_time:
# raise ParseError('该异常已结束无需编辑')
with transaction.atomic():
validated_data.pop('mgroup', None) validated_data.pop('mgroup', None)
validated_data.pop('start_time', None) validated_data.pop('start_time', None)
end_time = validated_data.pop('end_time', None) end_time = validated_data.pop('end_time', None)
@ -212,17 +214,12 @@ class MlogbDefectSerializer(CustomModelSerializer):
defect_okcate = serializers.CharField(source="defect.okcate", read_only=True) defect_okcate = serializers.CharField(source="defect.okcate", read_only=True)
class Meta: class Meta:
model = MlogbDefect model = MlogbDefect
fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate", "count_has"] fields = ["id", "defect_name", "count", "mlogb", "defect", "defect_okcate"]
read_only_fields = EXCLUDE_FIELDS_BASE + ["mlogb"] read_only_fields = EXCLUDE_FIELDS_BASE + ["mlogb"]
extra_kwargs = {
'count_has': {'required': False},
}
def validate(self, attrs): def validate(self, attrs):
if attrs["count"] < 0: if attrs["count"] < 0:
raise serializers.ValidationError("存在负数!") raise serializers.ValidationError("存在负数!")
if "count_has" not in attrs or attrs["count_has"] < attrs["count"]:
attrs["count_has"] = attrs["count"]
return attrs return attrs
class MlogbSerializer(CustomModelSerializer): class MlogbSerializer(CustomModelSerializer):
@ -257,7 +254,6 @@ class MlogListSerializer(CustomModelSerializer):
source='create_by.name', read_only=True) source='create_by.name', read_only=True)
update_by_name = serializers.CharField( update_by_name = serializers.CharField(
source='update_by.name', read_only=True) source='update_by.name', read_only=True)
# + 0.3s
material_in_ = MaterialSimpleSerializer( material_in_ = MaterialSimpleSerializer(
source='material_in', read_only=True) source='material_in', read_only=True)
material_out_ = MaterialSimpleSerializer( material_out_ = MaterialSimpleSerializer(
@ -266,7 +262,6 @@ class MlogListSerializer(CustomModelSerializer):
source='material_out', read_only=True) source='material_out', read_only=True)
material_in_name = serializers.StringRelatedField( material_in_name = serializers.StringRelatedField(
source='material_in', read_only=True) source='material_in', read_only=True)
#
handle_user_name = serializers.CharField( handle_user_name = serializers.CharField(
source='handle_user.name', read_only=True) source='handle_user.name', read_only=True)
handle_user_2_name = serializers.CharField( handle_user_2_name = serializers.CharField(
@ -278,17 +273,15 @@ class MlogListSerializer(CustomModelSerializer):
equipment_2_name = serializers.StringRelatedField( equipment_2_name = serializers.StringRelatedField(
source='equipment_2', read_only=True) source='equipment_2', read_only=True)
shift_name = serializers.CharField(source='shift.name', read_only=True) shift_name = serializers.CharField(source='shift.name', read_only=True)
test_user_name = serializers.CharField(source='test_user.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True) ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
submit_user_name = serializers.CharField(source='submit_user.name', read_only=True) test_user_name = serializers.CharField(source='test_user.name', read_only=True)
handle_users_ = UserSimpleSerializer( handle_users_ = UserSimpleSerializer(
source='handle_users', many=True, read_only=True) source='handle_users', many=True, read_only=True)
submit_user_name = serializers.CharField(source='submit_user.name', read_only=True)
class Meta: class Meta:
model = Mlog model = Mlog
fields = '__all__' fields = '__all__'
# exclude = ["equipments","handle_users", "material_outs"]
class MlogbDetailSerializer(CustomModelSerializer): class MlogbDetailSerializer(CustomModelSerializer):
@ -358,9 +351,8 @@ class MlogSerializer(CustomModelSerializer):
['submit_time', 'submit_user', 'material_outs'] ['submit_time', 'submit_user', 'material_outs']
extra_kwargs = { extra_kwargs = {
"batch": {"required": True}, "batch": {"required": True},
"shift": {"required": False}, "shift": {"required": True},
"material_out": {"required": True}, "material_out": {"required": True}
"work_start_time": {"required": False}
} }
def create(self, validated_data): def create(self, validated_data):
@ -370,7 +362,7 @@ class MlogSerializer(CustomModelSerializer):
mlogbindefect = validated_data.pop('mlogindefect', None) mlogbindefect = validated_data.pop('mlogindefect', None)
if mtask and mtask.state != Mtask.MTASK_ASSGINED: if mtask and mtask.state != Mtask.MTASK_ASSGINED:
raise ParseError('该任务非下达中不可选择') raise ParseError('该任务非下达中不可选择')
with transaction.atomic():
mlogb = validated_data.pop('mlogb', []) mlogb = validated_data.pop('mlogb', [])
instance: Mlog = super().create(validated_data) instance: Mlog = super().create(validated_data)
## 返工没有加工前不良 ## 返工没有加工前不良
@ -474,7 +466,7 @@ class MlogSerializer(CustomModelSerializer):
if instance.mtask: if instance.mtask:
validated_data.pop('handle_date', None) validated_data.pop('handle_date', None)
# validated_data.pop('handle_user', None) # validated_data.pop('handle_user', None)
with transaction.atomic():
mlogb = validated_data.pop('mlogb', []) mlogb = validated_data.pop('mlogb', [])
instance: Mlog = super().update(instance, validated_data) instance: Mlog = super().update(instance, validated_data)
## 返工没有加工前不良 ## 返工没有加工前不良
@ -613,7 +605,12 @@ class MlogSerializer(CustomModelSerializer):
raise ParseError('合格数量不能小于0') raise ParseError('合格数量不能小于0')
if attrs['count_real'] != attrs['count_ok'] + attrs['count_notok']: if attrs['count_real'] != attrs['count_ok'] + attrs['count_notok']:
raise ParseError('生产数量需等于合格数量+不合格数量') raise ParseError('生产数量需等于合格数量+不合格数量')
if mtask:
if mtask.start_date == mtask.end_date:
attrs['handle_date'] = mtask.start_date
else:
if attrs.get('work_end_time', None):
attrs['handle_date'] = localdate(attrs['work_end_time'])
mtaskb: Mtaskb = attrs.get('mtaskb', None) mtaskb: Mtaskb = attrs.get('mtaskb', None)
if mtaskb: if mtaskb:
mtask = mtaskb.mtask mtask = mtaskb.mtask
@ -626,29 +623,13 @@ class MlogSerializer(CustomModelSerializer):
if wm_in and wm_in.material != mtask.material_in: if wm_in and wm_in.material != mtask.material_in:
raise ParseError('消耗物料与任务不一致') raise ParseError('消耗物料与任务不一致')
attrs['material_out'] = material_out attrs['material_out'] = material_out
if mtask.start_date == mtask.end_date:
attrs['handle_date'] = mtask.end_date
else: else:
mgroup = attrs['mgroup'] mgroup = attrs['mgroup']
material_out = attrs['material_out'] material_out = attrs['material_out']
if not (mgroup and material_out): if not (mgroup and material_out):
raise ParseError('缺少工段或产物!') raise ParseError('缺少工段或产物!')
# 时间
mgroup:Mgroup = attrs['mgroup']
work_start_time:datetime = attrs.get('work_start_time', None)
if work_start_time:
attrs['handle_date'], attrs["shift"] = mgroup.get_shift(work_start_time)
else:
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"):
handle_user = self.request.user handle_user = self.request.user
@ -693,24 +674,22 @@ class MlogInitSerializer(CustomModelSerializer):
supplier = attrs.get('supplier', None) supplier = attrs.get('supplier', None)
if not supplier: if not supplier:
raise ParseError('外协必须选择外协单位') raise ParseError('外协必须选择外协单位')
if attrs.get('work_start_time', None) and 'handle_date' not in attrs:
attrs['handle_date'] = localdate(attrs['work_end_time'])
# 如果已经确定产出则自动获取qct # 如果已经确定产出则自动获取qct
if attrs.get("material_out", None): if attrs.get("material_out", None):
attrs["qct"] = Qct.get(attrs["material_out"], "process", "out") attrs["qct"] = Qct.get(attrs["material_out"], "process", "out")
attrs["handle_date"], attrs["shift"] = mgroup.get_shift(attrs['work_start_time'])
return attrs return attrs
class MlogChangeSerializer(CustomModelSerializer): class MlogChangeSerializer(CustomModelSerializer):
class Meta: class Meta:
model = Mlog model = Mlog
fields = ['id', 'work_start_time', 'work_end_time', 'handle_user', 'note', 'oinfo_json', 'test_file', 'test_user', 'test_time', 'equipment', "team"] fields = ['id', 'work_end_time', 'handle_user', 'note', 'oinfo_json', 'test_file', 'test_user', 'test_time', 'equipment', "team"]
def update(self, instance, validated_data): # def validate(self, attrs):
work_start_time = validated_data.get('work_start_time', None) # if attrs.get('work_end_time', None):
if work_start_time: # attrs['handle_date'] = localdate(attrs['work_end_time'])
mgroup:Mgroup = instance.mgroup # return attrs
validated_data["handle_date"], validated_data["shift"] = mgroup.get_shift(work_start_time)
return super().update(instance, validated_data)
class CountJsonSerializer(serializers.Serializer): class CountJsonSerializer(serializers.Serializer):
@ -727,12 +706,11 @@ 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", "wprs_in"] 'count_break', 'note', "parent", "mlogbdefect", "count_json_from"]
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}}
@ -784,14 +762,20 @@ 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('生产日志已提交不可编辑')
with transaction.atomic():
ins:Mlogb = super().create(validated_data) ins:Mlogb = super().create(validated_data)
validated_data["wprs_in"] = wprs_in # if mlog.is_fix:
# if mlog.material_in is None:
# mlog.material_in = ins.material_in
# mlog.material_out = ins.material_in
# mlog.save(update_fields=["material_in", "material_out"])
# elif mlog.material_in != ins.material_in:
# raise ParseError('该记录必须使用同一物料')
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()})
@ -828,9 +812,10 @@ class MlogbInUpdateSerializer(CustomModelSerializer):
MlogbDefect.objects.bulk_create(mlogb_defect_objects) MlogbDefect.objects.bulk_create(mlogb_defect_objects)
ins.cal_count_pn_jgqbl(cal_mlog=False) ins.cal_count_pn_jgqbl(cal_mlog=False)
# 只有普通工序的才可联动 # 只有普通工序的才可联动
route:Route = ins.route if ins.route else ins.mlog.route material_out:Material = ins.mlog.material_out
route:Route = mlog.route
if material_out.tracking == Material.MA_TRACKING_BATCH:
if route and route.process and route.process.mtype == Process.PRO_NORMAL: if route and route.process and route.process.mtype == Process.PRO_NORMAL:
if route.material_out.tracking == Material.MA_TRACKING_BATCH:
mlogbout_qs = Mlogb.objects.filter(mlog=ins.mlog, mlogb_from=ins) mlogbout_qs = Mlogb.objects.filter(mlog=ins.mlog, mlogb_from=ins)
if mlogbout_qs.count() == 1: if mlogbout_qs.count() == 1:
mlogbout = mlogbout_qs.first() mlogbout = mlogbout_qs.first()
@ -882,20 +867,20 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
ftest_sr.update(instance=ftest, validated_data=ftest_data) ftest_sr.update(instance=ftest, validated_data=ftest_data)
return mlogbw return mlogbw
@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:
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(f'{wpr.number}-该产品已选入') raise ParseError('该产品已选入')
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:
mlogbw = self.save_ftest(mlogbw, ftest_data) mlogbw = self.save_ftest(mlogbw, ftest_data)
return mlogbw return mlogbw
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop("mlogb") validated_data.pop("mlogb")
ftest_data = validated_data.pop("ftest", None) ftest_data = validated_data.pop("ftest", None)
@ -910,142 +895,6 @@ class MlogbwCreateUpdateSerializer(CustomModelSerializer):
ftest.delete() ftest.delete()
return mlogbw return mlogbw
class MlogbwStartTestSerializer(serializers.Serializer):
mlogbw_ids = serializers.ListField(child=serializers.CharField(), label="mlogbwId列表")
test_equip = serializers.CharField(label="测试设备", allow_null=True, required=False, allow_blank=True)
test_user = serializers.CharField(label="测试人员")
defects = serializers.ListField(child=serializers.CharField(), required=False, allow_null=True, label="缺陷项列表")
testitems = serializers.ListField(child=serializers.CharField(), required=False, allow_null=True, label="测试项列表")
test_date = serializers.DateField(label="测试日期")
qct = serializers.CharField(label="检测表id")
def save(self, **kwargs):
from apps.qm.models import Ftest, FtestDefect, FtestItem
validated_data = self.validated_data
mlogbw_ids = validated_data["mlogbw_ids"]
test_equip_id = validated_data.get("test_equip")
test_user_id = validated_data["test_user"]
defect_ids = validated_data.get("defects", [])
testitem_ids = validated_data.get("testitems", [])
test_date = validated_data["test_date"]
qct_id = validated_data["qct"]
# 预加载相关对象
test_equip = Equipment.objects.get(id=test_equip_id) if test_equip_id else None
test_user = User.objects.get(id=test_user_id)
qct = Qct.objects.get(id=qct_id)
# 批量获取所有mlogbw对象
mlogbws = Mlogbw.objects.filter(id__in=mlogbw_ids).select_related('ftest')
# 预加载缺陷和测试项
defects = Defect.objects.filter(id__in=defect_ids) if defect_ids else []
testitems = FtestItem.objects.filter(id__in=testitem_ids) if testitem_ids else []
existing_ftests = {}
new_ftests = []
mlogbws_to_update = []
# 分离已存在和需要新建的ftest
for mlogbw in mlogbws:
if mlogbw.ftest:
existing_ftests[mlogbw.ftest_id] = mlogbw.ftest
else:
ftest = Ftest(test_date=test_date, qct=qct, test_user=test_user, type="process", id=idWorker.get_id(), is_ok=True)
new_ftests.append(ftest)
mlogbws_to_update.append(mlogbw)
# 批量创建新的ftest
if new_ftests:
Ftest.objects.bulk_create(new_ftests)
# 更新mlogbw的ftest关系
for mlogbw, ftest in zip(mlogbws_to_update, new_ftests):
mlogbw.ftest = ftest
Mlogbw.objects.bulk_update(mlogbws_to_update, ['ftest'])
# 将新创建的ftest添加到existing_ftests中
for ftest in new_ftests:
existing_ftests[ftest.id] = ftest
# 批量处理缺陷项需要更新test_user
if defects and existing_ftests:
# 获取所有现有的FtestDefect记录
existing_defect_ids = FtestDefect.objects.filter(
ftest_id__in=existing_ftests.keys(),
defect_id__in=[d.id for d in defects]
).values_list('defect_id', 'ftest_id')
existing_defect_map = {(defect_id, ftest_id) for defect_id, ftest_id in existing_defect_ids}
defects_to_create = []
defects_to_update = []
for ftest in existing_ftests.values():
for defect in defects:
if (defect.id, ftest.id) in existing_defect_map:
# 已有记录,需要更新
defects_to_update.append((ftest.id, defect.id))
else:
# 新记录,需要创建
defects_to_create.append(
FtestDefect(ftest=ftest, defect=defect, test_user=test_user, id=idWorker.get_id())
)
# 批量创建新记录
if defects_to_create:
FtestDefect.objects.bulk_create(defects_to_create)
# 批量更新已有记录的test_user
if defects_to_update:
FtestDefect.objects.filter(
ftest_id__in=[item[0] for item in defects_to_update],
defect_id__in=[item[1] for item in defects_to_update]
).update(test_user=test_user)
# 批量处理测试项需要更新test_user和test_equip
if testitems and existing_ftests:
# 获取所有现有的FtestItem记录
existing_testitem_ids = FtestItem.objects.filter(
ftest_id__in=existing_ftests.keys(),
testitem_id__in=[t.id for t in testitems]
).values_list('testitem_id', 'ftest_id')
existing_testitem_map = {(testitem_id, ftest_id) for testitem_id, ftest_id in existing_testitem_ids}
testitems_to_create = []
testitems_to_update_condition = Q()
for ftest in existing_ftests.values():
for testitem in testitems:
if (testitem.id, ftest.id) in existing_testitem_map:
# 已有记录,添加到更新条件
testitems_to_update_condition |= Q(ftest=ftest, testitem=testitem)
else:
# 新记录,需要创建
testitems_to_create.append(
FtestItem(
ftest=ftest,
testitem=testitem,
test_user=test_user,
test_equip=test_equip,
id=idWorker.get_id()
)
)
# 批量创建新记录
if testitems_to_create:
FtestItem.objects.bulk_create(testitems_to_create)
# 批量更新已有记录的test_user和test_equip
if testitems_to_update_condition:
FtestItem.objects.filter(testitems_to_update_condition).update(
test_user=test_user,
test_equip=test_equip
)
class MlogbOutUpdateSerializer(CustomModelSerializer): class MlogbOutUpdateSerializer(CustomModelSerializer):
mlogbdefect = MlogbDefectSerializer(many=True, required=False) mlogbdefect = MlogbDefectSerializer(many=True, required=False)
count_json = CountJsonSerializer(required=False, many=True) count_json = CountJsonSerializer(required=False, many=True)
@ -1056,12 +905,29 @@ class MlogbOutUpdateSerializer(CustomModelSerializer):
read_only_fields = EXCLUDE_FIELDS_BASE + ['mlog', 'mtask', 'wm_in', 'material_in', 'material_out', read_only_fields = EXCLUDE_FIELDS_BASE + ['mlog', 'mtask', 'wm_in', 'material_in', 'material_out',
'count_use', 'count_break', 'count_pn_jgqbl', 'mlogbdefect', "qct", "count_json"] 'count_use', 'count_break', 'count_pn_jgqbl', 'mlogbdefect', "qct", "count_json"]
# def create(self, validated_data):
# material_out:Material = validated_data["material_out"]
# mlogbdefect = validated_data.pop("mlogbdefect", [])
# with transaction.atomic():
# ins = super().create(validated_data)
# if mlogbdefect and material_out.tracking == Material.MA_TRACKING_BATCH:
# count_notok = 0
# mlogbdefect_new = [item for item in mlogbdefect if item["count"] > 0]
# for item in mlogbdefect_new:
# defect:Defect = item["defect"]
# MlogbDefect.objects.create(mlogb=ins, **item)
# if defect.cate == Defect.DEFECT_NOTOK:
# count_notok +=1
# ins.count_notok = count_notok
# ins.count_ok = ins.count_real - ins.count_notok
# ins.save()
# else:
# raise ParseError("mlogbdefect仅支持批次件")
# return ins
@transaction.atomic
def update(self, instance, validated_data): def update(self, instance, validated_data):
mlog:Mlog = instance.mlog
if mlog.submit_time is not None:
raise ParseError('生产日志已提交不可编辑')
mlogbdefect = validated_data.pop("mlogbdefect", None) mlogbdefect = validated_data.pop("mlogbdefect", None)
with transaction.atomic():
ins:Mlogb = super().update(instance, validated_data) ins:Mlogb = super().update(instance, validated_data)
if ins.need_inout is False: if ins.need_inout is False:
if ins.mlogb_from: if ins.mlogb_from:
@ -1160,12 +1026,6 @@ 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())
@ -1259,15 +1119,7 @@ class HandoverSerializer(CustomModelSerializer):
if tracking == Material.MA_TRACKING_SINGLE: if tracking == Material.MA_TRACKING_SINGLE:
handoverbw = item.get("handoverbw", []) handoverbw = item.get("handoverbw", [])
if handoverbw: if handoverbw:
item["count"] = len(handoverbw)
t_count += len(handoverbw) t_count += len(handoverbw)
wprIds = [i["wpr"].id for i in handoverbw]
wm_ids = list(Wpr.objects.filter(id__in=wprIds).values_list("wm_id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm.id:
pass
else:
raise ParseError(f'{wm.batch}物料明细中存在{len(wm_ids)}个不同物料批次')
elif wm.count == item["count"]: elif wm.count == item["count"]:
t_count += item["count"] t_count += item["count"]
else: else:
@ -1311,6 +1163,7 @@ class HandoverSerializer(CustomModelSerializer):
def create(self, validated_data): def create(self, validated_data):
handoverb = validated_data.pop('handoverb', []) handoverb = validated_data.pop('handoverb', [])
with transaction.atomic():
ins = super().create(validated_data) ins = super().create(validated_data)
mtype = validated_data["mtype"] mtype = validated_data["mtype"]
for ind, item in enumerate(handoverb): for ind, item in enumerate(handoverb):
@ -1341,7 +1194,7 @@ class HandoverSerializer(CustomModelSerializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
handoverb = validated_data.pop('handoverb', []) handoverb = validated_data.pop('handoverb', [])
with transaction.atomic():
insx = super().update(instance, validated_data) insx = super().update(instance, validated_data)
Handoverb.objects.filter(handover=instance).delete() Handoverb.objects.filter(handover=instance).delete()
for ind, item in enumerate(handoverb): for ind, item in enumerate(handoverb):
@ -1380,8 +1233,6 @@ 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):
@ -1472,9 +1323,6 @@ class MlogTCreateSerializer(CustomModelSerializer):
class BatchStSerializer(CustomModelSerializer): class BatchStSerializer(CustomModelSerializer):
material_start_name = serializers.CharField(source='material_start.name', read_only=True)
material_start_model = serializers.CharField(source='material_start.model', read_only=True)
material_start_specification = serializers.CharField(source='material_start.specification', read_only=True)
class Meta: class Meta:
model = BatchSt model = BatchSt
fields = "__all__" fields = "__all__"
@ -1492,17 +1340,10 @@ class MlogUserSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MlogUser model = MlogUser
fields = "__all__" fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE + ["shift", "handle_date"] read_only_fields = EXCLUDE_FIELDS_BASE
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"]
@ -1533,12 +1374,11 @@ class BatchMgroupSerializer(serializers.Serializer):
class MlogQuickSerializer(serializers.Serializer): class MlogQuickSerializer(serializers.Serializer):
work_start_time = serializers.DateTimeField(label="开始时间") work_start_time = serializers.DateTimeField(label="开始时间")
# handle_date = serializers.DateField(label="操作日期") handle_date = serializers.DateField(label="操作日期")
work_end_time = serializers.DateTimeField(label="结束时间", required=False) work_end_time = serializers.DateTimeField(label="结束时间", required=False)
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, MlogUser from .models import SfLog, WMaterial, Mlog, Mlogb, Mlogbw, Handover, Handoverb, Handoverbw, MlogbDefect, BatchLog, BatchSt
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
@ -147,19 +147,17 @@ def get_pcoal_heat(year_s: int, month_s: int, day_s: int):
myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True) myLogger.error(f'获取煤粉热值失败,{e}, {year_s}, {month_s}, {day_s}', exc_info=True)
return 25000 return 25000
# @lock_model_record_d_func(Mlog) @lock_model_record_d_func(Mlog)
def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
""" """
生产日志提交后需要执行的操作 生产日志提交后需要执行的操作
""" """
if mlog.work_start_time and mlog.work_start_time > timezone.now():
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('操作结束时间不能早于操作开始时间')
if mlog.count_real == 0: if mlog.count_real == 0:
raise ParseError('产出数量不能为0') raise ParseError('产出数量不能为0')
# if mlog.submit_time is not None: if mlog.submit_time is not None:
# return return
if now is None: if now is None:
now = timezone.now() now = timezone.now()
if mlog.handle_date is None: if mlog.handle_date is None:
@ -205,7 +203,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
for item in m_ins: for item in m_ins:
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item) mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.defect and itemx.count > 0: if itemx.defect:
m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item)) m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
else: else:
m_ins_list = [(material_in, mlog.batch, mlog.count_use, None, mlog)] m_ins_list = [(material_in, mlog.batch, mlog.count_use, None, mlog)]
@ -284,7 +282,6 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE: # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# Mlogbw.cal_count_notok(item) # Mlogbw.cal_count_notok(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.count > 0:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item)) m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# # 获取所有主要的不合格项/先暂时保留 # # 获取所有主要的不合格项/先暂时保留
# bw_qs = Mlogbw.objects.filter(mlogb=item) # bw_qs = Mlogbw.objects.filter(mlogb=item)
@ -385,8 +382,6 @@ 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 and mlog.work_start_time is not None:
mlog.work_end_time = now
mlog.save() mlog.save()
# 更新任务进度 # 更新任务进度
@ -397,19 +392,19 @@ 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(xbatchs=xbatches, mgroup=mlog.mgroup) ana_batch_thread(xbatches)
# 触发单个统计 # 触发单个统计
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))
if wprIds: if wprIds:
ana_wpr_thread(wprIds, mlog.mgroup) ana_wpr_thread(wprIds, mlog.mgroup)
# @lock_model_record_d_func(Mlog) @lock_model_record_d_func(Mlog)
def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
"""日志撤回 """日志撤回
""" """
# if mlog.submit_time is None: if mlog.submit_time is None:
# return return
if now is None: if now is None:
now = timezone.now() now = timezone.now()
@ -440,7 +435,6 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE: # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# Mlogbw.cal_count_notok(item) # Mlogbw.cal_count_notok(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.count > 0:
m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item)) m_outs_list.append((item.material_out, item.batch, itemx.count, 0, itemx.defect, item))
# if item.material_out.tracking == Material.MA_TRACKING_SINGLE: # if item.material_out.tracking == Material.MA_TRACKING_SINGLE:
# # 获取所有主要的不合格项 # # 获取所有主要的不合格项
@ -536,7 +530,7 @@ def mlog_revert(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
for item in m_ins: for item in m_ins:
mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item) mbd_qs = MlogbDefect.get_defect_qs_from_mlogb(item)
for itemx in mbd_qs: for itemx in mbd_qs:
if itemx.defect and itemx.count > 0: if itemx.defect:
m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item)) m_ins_bl_list.append((item.material_in, item.batch, itemx.count, itemx.defect, item))
else: else:
m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in, mlog)] m_ins_list = [(material_in, mlog.batch, mlog.count_use, mlog.wm_in, mlog)]
@ -619,7 +613,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, mgroup=mlog.mgroup) ana_batch_thread(xbatches)
# 触发单个统计 # 触发单个统计
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))
@ -713,8 +707,8 @@ def update_mtask(mtask: Mtask, fill_way: int = 10):
utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0 utask.count_real = res2['sum_count_real'] if res2['sum_count_real'] else 0
utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0 utask.count_ok = res2['sum_count_ok'] if res2['sum_count_ok'] else 0
utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0 utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
# if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0: if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
# utask.state = Utask.UTASK_SUBMIT utask.state = Utask.UTASK_SUBMIT
utask.save() utask.save()
@lock_model_record_d_func(Handover) @lock_model_record_d_func(Handover)
@ -739,8 +733,6 @@ 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("只有合并时才能提供新批次号")
@ -760,7 +752,7 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
raise ParseError('拆批请选择车间库存') raise ParseError('拆批请选择车间库存')
batches_to_limit = BatchLog.batches_to(batch=handover.wm.batch)["batches"] batches_to_limit = BatchLog.batches_to(batch=handover.wm.batch)["batches"]
source_b, _ = BatchSt.g_create(batch=handover.wm.batch) source_b, _ = BatchSt.g_create(batch=handover.wm.batch)
for indx, item in enumerate(handoverb_list): for item in handoverb_list:
wmId, xcount, handover_or_b = item wmId, xcount, handover_or_b = item
if xcount <= 0: if xcount <= 0:
raise ParseError("存在非正数!") raise ParseError("存在非正数!")
@ -930,16 +922,9 @@ def handover_submit(handover:Handover, user: User, now: Union[datetime.datetime,
handoverbws = Handoverbw.objects.filter(handoverb=handover_or_b) handoverbws = Handoverbw.objects.filter(handoverb=handover_or_b)
if handoverbws.count() != xcount: if handoverbws.count() != xcount:
raise ParseError("交接与明细数量不一致,操作失败") raise ParseError("交接与明细数量不一致,操作失败")
wm_ids = list(Wpr.objects.filter(wpr_handoverbw__in=handoverbws).values_list("wm_id", flat=True).distinct())
if len(wm_ids) == 1 and wm_ids[0] == wm_from.id:
pass
else:
raise ParseError(f'{batch}物料明细中存在{len(wm_ids)}个不同物料批次')
for item in handoverbws: for item in handoverbws:
wpr:Wpr = item.wpr wpr:Wpr = item.wpr
Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb) Wpr.change_or_new(wpr=wpr, wm=wm_to, old_wm=wpr.wm, old_mb=wpr.mb)
if wm_to.count != Wpr.objects.filter(wm=wm_to).count():
raise ParseError("交接与明细数量不一致2,操作失败")
handover.submit_user = user handover.submit_user = user
handover.submit_time = now handover.submit_time = now
@ -1026,10 +1011,6 @@ 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

@ -1,16 +1,14 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from apps.wpm.models import (Handoverbw, Mlogbw, WMaterial) from apps.wpm.models import (Handoverbw, Mlogbw)
from apps.inm.models import MIOItemw from apps.inm.models import MIOItemw
from apps.wpmw.models import Wpr from apps.wpmw.models import Wpr
from apps.mtm.models import Route, Material, Mgroup from apps.mtm.models import Route, Material
from django.db.models import Q from django.db.models import Q
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
class WprFilter(filters.FilterSet): class WprFilter(filters.FilterSet):
can_use = filters.CharFilter(method='filter_can_use') can_use = filters.CharFilter(method='filter_can_use')
mgroupx = filters.CharFilter(label='MgroupId', method='filter_mgroupx')
tag = filters.CharFilter(label="todo/done", method="filter_tag")
class Meta: class Meta:
model = Wpr model = Wpr
fields = { fields = {
@ -22,30 +20,6 @@ class WprFilter(filters.FilterSet):
"number": ["exact"] "number": ["exact"]
} }
def filter_mgroupx(self, queryset, name, value):
return queryset.filter(wm__in=WMaterial.ava_qs(mgroup=Mgroup.objects.get(id=value)))
def filter_tag(self, queryset, name, value):
mgroupId = self.data.get("mgroupx", None)
if mgroupId:
process = Mgroup.objects.get(id=mgroupId).process
queryset = queryset.filter(material__type__in=[Material.MA_TYPE_MAINSO, Material.MA_TYPE_HALFGOOD, Material.MA_TYPE_GOOD])
if value == "todo":
matIds = process.get_canin_mat_ids()
qs = queryset.filter(material__id__in=matIds).exclude(state=WMaterial.WM_REPAIRED)|queryset.filter(state=WMaterial.WM_REPAIR, wm__mgroup__id=mgroupId)
return qs
elif value == "done":
matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(material__id__in=matoutIds).exclude(state=WMaterial.WM_REPAIR)|queryset.filter(state=WMaterial.WM_REPAIRED, wm__mgroup__id=mgroupId)
return qs
elif value == "canfix":
matoutIds = process.get_canout_mat_ids()
qs = queryset.filter(state=WMaterial.WM_REPAIR, wm__mgroup__id=mgroupId)| queryset.filter(material__id__in=matoutIds, state=WMaterial.WM_NOTOK)
return qs
else:
raise ParseError("请提供工段查询条件")
def filter_can_use(self, queryset, name, value): def filter_can_use(self, queryset, name, value):
if value == 'yes': if value == 'yes':
# 交接记录 # 交接记录

View File

@ -13,25 +13,14 @@ class WprDefectSerializer(CustomModelSerializer):
class WprSerializer(CustomModelSerializer): class WprSerializer(CustomModelSerializer):
wm_batch = serializers.CharField(source="wm.batch", read_only=True)
mb_batch = serializers.CharField(source="mb.batch", read_only=True)
material_name = serializers.StringRelatedField( material_name = serializers.StringRelatedField(
source='material', read_only=True) source='material', read_only=True)
# material_start_name = serializers.StringRelatedField(source='material_start', read_only=True)
wm_material_ofrom_name = serializers.StringRelatedField(source="wm.material_ofrom", read_only=True)
wprdefect = WprDefectSerializer(many=True, read_only=True) wprdefect = WprDefectSerializer(many=True, read_only=True)
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)
wm_ = WMaterialSerializer(source='wm', read_only=True) wm_ = WMaterialSerializer(source='wm', read_only=True)

View File

@ -10,7 +10,6 @@ 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):
@ -18,9 +17,8 @@ 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"]
prefetch_related_fields = ["defects"] prefetch_related_fields = ["defects"]
queryset = Wpr.objects.all() queryset = Wpr.objects.all()
serializer_class = WprSerializer serializer_class = WprSerializer
@ -29,10 +27,6 @@ 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)
@ -42,7 +36,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
@ -53,17 +47,16 @@ 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("请传入前缀参数")
@ -74,7 +67,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:
@ -94,7 +87,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:
@ -103,7 +96,8 @@ class WprViewSet(CustomListModelMixin, RetrieveModelMixin, ComplexQueryMixin, Cu
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):
"""分配出库对外编号 """分配出库对外编号
@ -114,7 +108,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)}")

View File

@ -1,151 +1,3 @@
## 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
- feat: 新增功能
- mioitem处理事务处理 [caoqianming]
- base 移除基础model层事务 [caoqianming]
- inm和wpm关于事务处理的修改 [caoqianming]
- 批次的DAG数据只获取直接的上下级 [caoqianming]
- wpm优化事务和悲观锁 [caoqianming]
- cdview优化校验 [caoqianming]
- base 优化get_object [caoqianming]
- mlogbout serializer update校验mlog [caoqianming]
- 生产日志子表全部加mlog锁 [caoqianming]
- ofm-serializer fix [zty]
- base 优化get_object事务 [caoqianming]
- ofm-修改字段 [zty]
- ofm - 档案管理修改 serializer [zty]
- ofm-档案管理-serializer 更新 [zty]
- wpm修改为自动事务 [caoqianming]
- 修改wpm事务 [caoqianming]
- wpm 取消 transaction [caoqianming]
- base CustomGenericViewSet 添加自动事务 [caoqianming]
- ofm-修改 -档案借 filterset_class [zty]
- ofm 增加档案台账模糊查询 [zty]
- 默认提交时间为结束时间 [caoqianming]
- 快速报工的bug [caoqianming]
- base 日志默认记录耗时大于2s的 [caoqianming]
- ofm 增加档案台账 [zty]
- mlog submit/revert 改用乐观锁 [caoqianming]
- ofm 修改车辆model [zty]
- ofm 修改 车辆model [zty]
- wpr list 返回wm_material_ofrom_name [caoqianming]
- wpr list 返回wm_material_name [caoqianming]
- wpr list 返回material_start_name [caoqianming]
- vehicle 增加serializer 返回字段 [zty]
- wpr list返回batch [caoqianming]
- 增加wpr tag查询条件以支持todo等 [caoqianming]
- 修改车辆的字段 [zty]
- ofm 车辆管理迁移文件 [zty]
- ofm 车辆审批 [zty]
- 出入库记录和交接记录提交处理时都进行单个的强校验 [caoqianming]
- 添加物料明细与批次不匹配的校验2 [caoqianming]
- 添加物料明细与批次不匹配的校验 [caoqianming]
- gen_number_with_rule 优化一下 [caoqianming]
- handover_submit增强校验 [caoqianming]
- mlog list 支持返回mlogbw number [caoqianming]
- gen_number_with_rule按生产日志中的工段过滤 [caoqianming]
- gen_number_with_rule 完善 [caoqianming]
- 工段未配置班次提醒 [caoqianming]
- 板段号生成逻辑修改 [caoqianming]
- ofm-印章绑定审批修改 [zty]
- fix: 问题修复
- wpm取消事务时出现的缩进bug [caoqianming]
- 次批作为输入不会影响输出批次号 [caoqianming]
- lock_and_check_can_update [caoqianming]
- lock_and_check_can_update [caoqianming]
- base 在create update destroy添加自动事务 [caoqianming]
- base 修改_should_use_transaction [caoqianming]
- 产生编号时存在bug [caoqianming]
- 交接记录已提交不可变动 [caoqianming]
- mlog change 导致的bug问题 [caoqianming]
- wpr list filter bug [caoqianming]
- 出入库记录和交接记录提交处理时都进行单个的强校验 [caoqianming]
## 2.8.2025090815
- feat: 新增功能
- batch bxerp含缺陷统计 [caoqianming]
- mlog list 添加update_time筛选 [caoqianming]
- 工艺步骤中辅料使用的校验 [caoqianming]
- mlogb添加parent isnull查询 [caoqianming]
- 次批触发输出产生 [caoqianming]
- 修改印章的model 和 serializer [zty]
- 修改 ofm sevice 印章模块 [zty]
- base 优化safe_get_or_create [caoqianming]
- 修改ofm seal 过滤查询功能 [zty]
- 行政管理 -印章管理 [zty]
- ftest默认为合格 [caoqianming]
- mlogbdefect添加count_has 字段及处理 [caoqianming]
- update_mb_item defect处理更严谨 [caoqianming]
- 取消最后一步产出与工艺包不一致 的校验 [caoqianming]
- 出入库记录添加乐观锁 [caoqianming]
- batch_bxerp考虑mlogbw_from [caoqianming]
- mlog_submit mlogbdefect增加筛选条件 [caoqianming]
- 导入物料明细时可默认批次号为无 [caoqianming]
- 光芯批次统计增加班次返回 [caoqianming]
- mlog_submit 进行操作时间校验 [caoqianming]
- route update 时from_route存在则不可修改关键信息 [caoqianming]
- mlogchange支持work_start_time [caoqianming]
- route采用引用方式允许重复创建 [caoqianming]
- MlogSerializer 处理 work_start_time 的 bug [caoqianming]
- 修改能管采集的点位 [zty]
- mlog work_start_time必填 [caoqianming]
- ftestwork submit支持mb [zty]
- toggle_state 使用 routepack.update权限 [caoqianming]
- ftestwork支持对materialbatch检查 [caoqianming]
- batchst返回material_start相关信息 [caoqianming]
- fix: 问题修复
- ftestwork submit 校验wm和mb bug [caoqianming]
- MlogbInUpdateSerializer 联动count_use 和 count_real [caoqianming]
- batchlog batches_to 优化 [caoqianming]
## 2.7.2025082816
- feat: 新增功能
- 添加定时任务以标记mtask为已完成 [caoqianming]
- update_mtask 不再触发状态改变 [caoqianming]
- 子任务提交会触发大任务提交 [caoqianming]
- 优化mlog list查询 [caoqianming]
- 增加直接userid获取token的接口 [caoqianming]
- mlog 添加索引 [caoqianming]
- 增加mlogbw start_test接口以优化批量操作 [caoqianming]
- 优化mlogbw bulk update [caoqianming]
## 2.7.2025082616 ## 2.7.2025082616
- feat: 新增功能 - feat: 新增功能
- 添加mlog和handover的锁 [caoqianming] - 添加mlog和handover的锁 [caoqianming]

View File

@ -80,7 +80,9 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
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:
@ -108,8 +110,10 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
sc.close() sc.close()
except Exception: except Exception:
pass 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()
@ -118,16 +122,6 @@ class JSONRequestHandler(BaseHTTPRequestHandler):
res = handle_bytes(resp) res = handle_bytes(resp)
if isinstance(res, str): if isinstance(res, str):
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) self.error(res)
else: else:
self.ok(res) self.ok(res)

View File

@ -1,143 +0,0 @@
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.2025101011' SYS_VERSION = '2.7.2025082616'
X_FRAME_OPTIONS = 'SAMEORIGIN' X_FRAME_OPTIONS = 'SAMEORIGIN'
# Application definition # Application definition

View File

@ -21,11 +21,10 @@ 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=f'{settings.SYS_NAME}--{get_sysconfig("base.base_name", "demo")}', title=settings.SYS_NAME,
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"),

View File

@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
# 设置默认版本号 (格式: 2.7.YYYYMMDDHH) # 设置默认版本号 (格式: 2.7.YYYYMMDDHH)
DEFAULT_VERSION="2.8.$(date '+%Y%m%d%H')" DEFAULT_VERSION="2.7.$(date '+%Y%m%d%H')"
# 获取参数 (起始tag) # 获取参数 (起始tag)
TARGET_TAG="$1" TARGET_TAG="$1"