Compare commits

..

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

136 changed files with 2185 additions and 7335 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,9 @@ def check_sql_safe(sql: str):
"""检查sql安全性
"""
sql_upper = sql.upper()
# 将SQL按空格和分号分割成单词
words = [word for word in sql_upper.replace(';', ' ').split() if word]
for kw in forbidden_keywords:
# 检查关键字是否作为独立单词出现
if kw in words:
raise ParseError(f'sql查询有风险-{kw}')
if kw in sql_upper:
raise ParseError('sql查询有风险')
return sql
def format_json_with_placeholders(json_str, **kwargs):

View File

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

View File

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

View File

@ -4,10 +4,9 @@ import json
import time
from django.core.cache import cache
from apps.utils.thread import MyThread
import struct
import uuid
import logging
import threading
import requests
myLogger = logging.getLogger('log')
@ -16,7 +15,7 @@ def get_checksum(body_msg):
def handle_bytes(arr):
if len(arr) < 8:
return f"返回数据长度错误-{arr}"
return "返回数据长度错误"
if arr[0] != 0xEB or arr[1] != 0x90:
return "数据头不正确"
@ -134,9 +133,8 @@ def get_tyy_data_2(*args, retry=1):
sc_all = {}
sc_lock = threading.Lock()
def get_tyy_data_1(*args, retry=1):
def get_tyy_data(*args):
host, port = args[0], int(args[1])
global sc_all
sc = None
@ -150,17 +148,6 @@ def get_tyy_data_1(*args, retry=1):
sc.settimeout(5) # 设置超时
sc.connect((host, port))
sc_all[f"{host}_{port}"] = sc
else:
# 清空接收缓冲区
sc.settimeout(0.1) # 设置短暂超时
for _ in range(5):
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5) # 恢复原超时设置
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
@ -199,7 +186,6 @@ def get_tyy_data_1(*args, retry=1):
sc = None
raise ParseError(f"采集器连接超时-{str(e)}")
with sc_lock:
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
@ -208,45 +194,3 @@ def get_tyy_data_1(*args, retry=1):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data_3(*args, retry=2):
host, port = args[0], int(args[1])
for attempt in range(retry):
try:
# 每次请求都新建连接(确保无共享状态)
with socket.create_connection((host, port), timeout=10) as sc:
sc.sendall(b"R")
# 接收完整响应(避免数据不完整)
# resp = b""
# while True:
# chunk = sc.recv(4096)
# if not chunk:
# break
# resp += chunk
resp = sc.recv(4096)
if not resp:
raise ParseError("设备未启动")
res = handle_bytes(resp)
if isinstance(res, str):
raise ParseError(f"采集器返回数据错误: {res}")
return res
except (socket.timeout, ConnectionError) as e:
if attempt == retry - 1: # 最后一次尝试失败才报错
raise ParseError(f"采集器连接失败: {str(e)}")
time.sleep(0.5) # 失败后等待 1s 再重试
except ParseError:
raise
except Exception as e:
raise ParseError(f"未知错误: {str(e)}")
def get_tyy_data(*args):
host, port = args[0], int(args[1])
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
res = r.json()
if "err_msg" in res:
raise ParseError(res["err_msg"])
return res

View File

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

View File

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

View File

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

View File

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

View File

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

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
# 先处理库存
try:
with transaction.atomic():
MIOItem.objects.filter(id=mi.id).update(count_notok=count_notok)
InmService.update_mb_after_test(mi)
except ParseError as e:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ class MaterialSimpleSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ['id', 'name', 'number', 'model',
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking", "bin_number_main"]
'specification', 'type', 'cate', 'brothers', 'process_name', 'full_name', "tracking"]
def get_full_name(self, obj):
return f'{obj.name}|{obj.specification if obj.specification else ""}|{obj.model if obj.model else ""}|{obj.process.name if obj.process else ""}'
@ -246,8 +246,8 @@ class RouteSerializer(CustomModelSerializer):
# material = validated_data.get('material', None)
# if material and process and Route.objects.filter(material=material, process=process).exists():
# raise ValidationError('已选择该工序!!')
instance:Route = super().create(validated_data)
with transaction.atomic():
instance = super().create(validated_data)
material_out = instance.material_out
if material_out:
if material_out.process is None:
@ -263,16 +263,12 @@ class RouteSerializer(CustomModelSerializer):
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
msg = ""
if rx.routepack:
msg = rx.routepack.name
raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def update(self, instance, validated_data):
@ -281,7 +277,7 @@ class RouteSerializer(CustomModelSerializer):
material_out_tracking = validated_data.pop("material_out_tracking", Material.MA_TRACKING_BATCH)
if material_out_tracking is None:
material_out_tracking = Material.MA_TRACKING_BATCH
with transaction.atomic():
instance = super().update(instance, validated_data)
material_out = instance.material_out
if material_out:
@ -298,16 +294,12 @@ class RouteSerializer(CustomModelSerializer):
if instance.material:
instance.material_out = RouteSerializer.gen_material_out(instance, material_out_tracking, user=self.request.user)
instance.save()
rx = Route.objects.filter(
material_in=instance.material_in, material_out=instance.material_out,
process=process).exclude(id=instance.id).order_by("create_time").first()
rx = Route.objects.filter(material_in=instance.material_in, material_out=instance.material_out, process=process).exclude(id=instance.id).first()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
msg = ""
if rx.routepack:
msg = rx.routepack.name
raise ParseError(f"该工艺步骤已存在-{msg}")
return instance
def to_representation(self, instance):
@ -338,15 +330,3 @@ class RouteMatSerializer(CustomModelSerializer):
model = RouteMat
fields = "__all__"
read_only_fields = EXCLUDE_FIELDS_BASE
def validate(self, attrs):
route:Route = attrs["route"]
if route.from_route is not None:
raise ParseError("该工艺步骤引用其他步骤,无法修改")
return attrs
class MaterialExportSerializer(CustomModelSerializer):
class Meta:
model = Material
fields = ["id", "number", "name", "specfication", "unit", "bin_number_main", "cate", "count_safe", "unit_price"]

View File

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

View File

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

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,330 +0,0 @@
from django.db import models, transaction
from apps.utils.models import CommonADModel, BaseModel, CommonBDModel
from apps.system.models import User
from django.core.validators import RegexValidator
from datetime import datetime
from rest_framework.exceptions import ParseError
# Create your models here.
MTASK_CREATED = 10
MTASK_ASSGINED = 20
MTASK_STOP = 34
MTASK_SUBMIT = 40
MTASK_STATES = (
(MTASK_CREATED, '创建中'),
(MTASK_ASSGINED, '已下达'),
(MTASK_STOP, '已停止'),
(MTASK_SUBMIT, '已提交')
)
phone_validator = RegexValidator(r'^1[3456789]\d{9}$', '手机号码格式不正确')
class Mroom(CommonADModel):
"""TN: 会议室基本信息"""
name = models.CharField('会议室名称', max_length=50, unique=True)
location = models.CharField('位置', max_length=100)
capacity = models.PositiveIntegerField('容纳人数')
class MroomBooking(CommonBDModel):
"""TN: 会议室预定信息"""
# belong_dept 是预定部门
title = models.CharField('会议主题', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联会议室',
on_delete=models.SET_NULL, related_name='mrooms_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
participant_count = models.PositiveIntegerField('参会人数', default=0)
key_participants = models.TextField("主要参会领导", null=True, blank=True)
class MroomSlot(BaseModel):
"""TN: 会议室时段"""
mroom = models.ForeignKey(Mroom, on_delete=models.CASCADE, related_name="slot_m")
booking = models.ForeignKey(MroomBooking, on_delete=models.CASCADE, related_name="slot_b")
mdate = models.DateField('会议日期', db_index=True)
slot = models.PositiveIntegerField('时段', help_text='0-47')
is_inuse = models.BooleanField('是否占用', default=True)
# class Seal(BaseModel):
# """TN: 印章类型"""
# name = models.CharField('印章名称', max_length=50, unique=True)
class LendingSeal(CommonBDModel):
"""TN: 印章外出用印信息"""
seal = models.JSONField('印章信息',default=list ,help_text='[公章,法人章,财务章,合同章,业务章,其他章]')
seal_other = models.CharField('其他印章', max_length=50, blank=True, null=True)
filename = models.TextField('文件名称')
file = models.TextField('文件内容')
file_count = models.PositiveIntegerField('用印份数')
is_lending= models.BooleanField('是否借出', default=False)
contacts = models.CharField('联系方式', max_length=50, validators=[phone_validator], blank=True, null=True)
lending_date = models.DateField('借出日期', blank=True, null=True)
return_date = models.DateField('拟归还日期', blank=True, null=True)
actual_return_date = models.DateField('实际归还日期', blank=True, null=True)
reason = models.CharField('借用理由', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='seal_ticket', null=True, blank=True, db_constraint=False)
note = models.TextField('备注', null=True, blank=True)
class Vehicle(CommonBDModel):
"""TN: 用车申请"""
start_time = models.DateField('出车时间', blank=True, null=True)
end_time = models.DateField('还车时间', blank=True, null=True)
location = models.CharField('出发地点', null=True, blank=True, max_length=100)
via = models.CharField('途经地点', null=True, blank=True, max_length=100)
destination = models.CharField('到达地点', null=True, blank=True, max_length=100)
start_km = models.PositiveIntegerField('出发公里数')
end_km = models.PositiveIntegerField('归还公里数', null=True, blank=True)
actual_km = models.PositiveIntegerField('实际行驶公里数', editable=False)
is_city = models.BooleanField('是否市内用车', default=True)
reason = models.CharField('用车事由', max_length=100)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='vehicle_ticket', null=True, blank=True, db_constraint=False)
def save(self, *args, **kwargs):
if self.end_km:
if self.start_km <= self.end_km:
self.actual_km = self.end_km - self.start_km
else:
raise ParseError('归还公里数不能小于出发公里数')
else:
self.actual_km = 0
return super().save(*args, **kwargs)
class FileRecord(CommonBDModel):
"""TN: 档案台账"""
name = models.CharField('资料名称', max_length=100)
number = models.CharField('档案编号', max_length=50, null=True, blank=True)
counts = models.CharField('文件份数', max_length=10, null=True, blank=True)
location = models.CharField('存放位置', max_length=100, null=True, blank=True)
contacts = models.CharField('存档人电话', max_length=50, validators=[phone_validator], blank=True, null=True)
reciver = models.CharField('接收人(综合办)', max_length=50, null=True, blank=True)
remark = models.TextField('备注', max_length=200, null=True, blank=True)
class BorrowRecord(CommonBDModel):
"""TN: 借阅、复印、查阅记录"""
borrow_file = models.ManyToManyField(FileRecord, related_name="borrow_records")
borrow_date = models.DateField('借阅日期', null=True, blank=True)
return_date = models.DateField('归还日期', null=True, blank=True)
contacts = models.CharField('借阅人电话', max_length=50, validators=[phone_validator], null=True, blank=True)
remark = models.JSONField('用途', default=list, help_text=str(['借阅', '复印', '查阅']))
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='borrow_ticket', null=True, blank=True, db_constraint=False)
class Publicity(CommonBDModel):
"""TN: 公示栏"""
number = models.CharField('记录编号', max_length=50, blank=True, null=True)
title = models.CharField('送审稿件标题', max_length=100)
participants = models.CharField('所有撰稿人', max_length=50)
pub_dept = models.CharField('部室/研究院', null=True, blank=True, max_length=50)
pfile = models.CharField('稿件路径', null=True, blank=True, max_length=100)
level = models.JSONField('涉密等级', default=list, help_text=str(['重要', '一般', '非涉密']))
content = models.JSONField('稿件内容涉及', default=list, help_text=str([
"武器装备科研生产综合事项",
"其它"
]))
other_content = models.CharField('其它内容', max_length=100, blank=True, null=True)
report_purpose = models.CharField('宣传报道目的', max_length=100, blank=True, null=True)
channel = models.JSONField('发布渠道', default=list, help_text=str(['互联网', '信息平台', '官微', '公开发行物', '其它']))
other_channel = models.CharField('其它渠道', max_length=50, blank=True, null=True)
report_name = models.CharField('报道名称', max_length=50, blank=True, null=True)
review = models.JSONField('第一撰稿人自审', default=list, help_text=str(['内容不涉及国家秘密和商业秘密,申请公开', '内容涉及国家秘密,申请按涉密渠道发布']), null=True,blank=True)
dept_opinion = models.JSONField('部门负责人意见', default=list, help_text=str(['同意', '不同意']), null=True, blank=True)
secret_period = models.CharField('秘密期限', max_length=50, blank=True, null=True)
dept_opinion_review = models.CharField('部门审查意见', max_length=100, blank=True, null=True)
publicity_opinion = models.CharField('宣传报道意见', max_length=100, blank=True, null=True)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='publicity_ticket', null=True, blank=True, db_constraint=False)
# 记录编号自动生成
def save(self, *args, **kwargs):
if not self.number:
last_number = self.__class__.objects.filter(number__startswith=f"GXKG-{datetime.now().year}-").order_by('-number').first()
if last_number:
try:
last_num = int(last_number.number.split('-')[-1])
except ValueError:
last_num = 0
else:
last_num =0
# 格式化编号,带补零
self.number = f"GXKG-{datetime.now().year}-{last_num+1:02d}"
super().save(*args, **kwargs)
class PatentInfo(CommonBDModel):
"""TN: 专利申密审批表单样式"""
PATENT_TYPE_CHOICES = (
('invention', '发明专利'),
('utility', '实用新型专利'),
('design', '外观设计专利'),
)
APPLY_AREAS = (
('Domestic', '国内申请'),
('Foreign', '国外申请'),
(' PCT', 'PCT申请'),
)
name = models.CharField('拟申请专利名称', max_length=100)
author = models.CharField('发明人(设计人)', max_length=100)
type = models.CharField('专利类型', max_length=50, choices=PATENT_TYPE_CHOICES, default='invention')
is_public = models.BooleanField('是否公开', default=False)
area = models.CharField('拟申请地域', max_length=50, choices=APPLY_AREAS, default='Domestic')
other_area = models.CharField('其它申请地域', max_length=50, blank=True, null=True)
tech_status = models.JSONField('技术状态', default=list, blank=True, help_text='技术状态信息列表每个条目包含name(名称)、status(状态)、file(文件)字段')
tech_file = models.JSONField('技术文件', default=list, help_text='技术文件信息列表每个条目包含name(名称)page(页数)字段')
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='patentInfo_ticket', null=True, blank=True, db_constraint=False)
# class 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,196 +0,0 @@
from .models import (Mroom, MroomBooking, MroomSlot, LendingSeal, Vehicle, FileRecord, BorrowRecord, Publicity, PatentInfo)
# Publicity, PatetInfo, PaperOfm, Platform, Project, PatentRecord, PaperRecord, ProjectApproval, ProjectInfo)
from apps.utils.serializers import CustomModelSerializer
from rest_framework import serializers
from django.db import transaction
from rest_framework.exceptions import ParseError
from apps.utils.constants import EXCLUDE_FIELDS
from apps.wf.serializers import TicketSimpleSerializer
class MroomSerializer(CustomModelSerializer):
class Meta:
model = Mroom
fields = '__all__'
class MroomBookingSerializer(CustomModelSerializer):
mroom = serializers.PrimaryKeyRelatedField(queryset=Mroom.objects.all(), write_only=True, label="会议室")
mdate = serializers.DateField(write_only=True, label="预订日期")
slots = serializers.ListField(child=serializers.IntegerField(), write_only=True, label="时段索引")
create_by_name = serializers.CharField(source='create_by.username', read_only=True)
create_by_phone = serializers.CharField(source='create_by.phone', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = MroomBooking
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
extra_kwargs = {'belong_dept': {'required': True}}
def create(self, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().create(validated_data)
MroomSlot.objects.filter(booking=booking).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
def update(self, instance, validated_data):
mroom = validated_data.pop('mroom')
slots = validated_data.pop('slots')
mdate = validated_data.pop('mdate')
booking = super().update(instance, validated_data)
MroomSlot.objects.filter(booking=instance).delete()
for slot in slots:
if slot < 0 or slot > 47:
raise ParseError("时段索引超出范围")
ms_exists = MroomSlot.objects.filter(mroom=mroom, mdate=mdate, slot=slot, is_inuse=True).exists()
if ms_exists:
raise ParseError("时段已预订,请刷新重选")
MroomSlot.objects.create(booking=booking, slot=slot, mdate=mdate, mroom=mroom, is_inuse=True)
return booking
class MroomSlotSerializer(CustomModelSerializer):
booking_title = serializers.CharField(source='booking.title', read_only=True)
class Meta:
model = MroomSlot
fields = '__all__'
class LendingSealSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = LendingSeal
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class VehicleSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Vehicle
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS + ['actual_km']
class FileRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = FileRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class BorrowRecordSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
borrow_file = serializers.PrimaryKeyRelatedField(queryset=FileRecord.objects.all(), many=True, write_only=True, label="借阅文件")
file_detail = FileRecordSerializer(source='borrow_file', many=True, read_only=True, label="借阅文件详情")
file_name = serializers.SerializerMethodField()
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = BorrowRecord
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
def get_file_name(self, obj):
return [file.name for file in obj.borrow_file.all()]
class PublicitySerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = Publicity
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
class PatentInfoSerializer(CustomModelSerializer):
create_by_name = serializers.CharField(source='create_by.name', read_only=True)
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
class Meta:
model = PatentInfo
fields = '__all__'
read_only_fields = EXCLUDE_FIELDS
# class 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,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,32 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.ofm.views import (MroomViewSet, MroomBookingViewSet, MroomSlotViewSet,LendingSealViewSet, VehicleViewSet, FilerecordViewSet,
FileborrowViewSet, PublicityViewSet, PatentInfoViewSet)
# SealModelViewSet,
# , PublicityViewSet, , PaperViewSet, PlatformViewSet,
# ProjectViewSet, PatentRecordViewSet, PaperRecordViewSet, ProjectApprovalViewSet, ProjectInfoViewSet)
API_BASE_URL = 'api/ofm/'
HTML_BASE_URL = 'dhtml/ofm/'
router = DefaultRouter()
router.register('mroom', MroomViewSet, basename='mroom')
router.register('mroombooking', MroomBookingViewSet, basename='mroombooking')
router.register('mroomslot', MroomSlotViewSet, basename='mroomslot')
# router.register('sealmanage', SealManageViewSet, basename='sealmanage')
router.register('lendingseal', LendingSealViewSet, basename='lendingseal')
router.register('vehicle', VehicleViewSet, basename='vehicle')
router.register('filerecord', FilerecordViewSet, basename='filerecord')
router.register('fileborrow', FileborrowViewSet, basename='fileborrow')
router.register('publicity', PublicityViewSet, basename='publicity')
router.register('patentinfo', PatentInfoViewSet, basename='patentinfo')
# router.register('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 = [
path(API_BASE_URL, include(router.urls)),
]

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,6 @@ class DefectSerializer(CustomModelSerializer):
class QctGetSerializer(serializers.Serializer):
material = serializers.CharField(label="物料ID")
tag = serializers.CharField(label="标签")
type = serializers.CharField(label="类型(in/out)", required=False)
class TestItemSerializer(CustomModelSerializer):
process_name = serializers.CharField(source="process.name", read_only=True)
@ -54,7 +53,6 @@ class QctSerializer(CustomModelSerializer):
class QctTestItemSerializer(CustomModelSerializer):
testitem_name = serializers.CharField(source='testitem.name', read_only=True)
testitem_type = serializers.CharField(source='testitem.type', read_only=True)
testitem_description = serializers.CharField(source='testitem.description', read_only=True)
testitem_field_type = serializers.CharField(source='testitem.field_type', read_only=True)
testitem_choices = serializers.CharField(source='testitem.choices', read_only=True)
@ -63,11 +61,11 @@ class QctTestItemSerializer(CustomModelSerializer):
model = QctTestItem
fields = '__all__'
# def validate(self, attrs):
# testitem:TestItem = attrs.get("testitem")
# if testitem.type != TestItem.T_TEST:
# raise ParseError("只可选择检测项")
# return attrs
def validate(self, attrs):
testitem:TestItem = attrs.get("testitem")
if testitem.type != TestItem.T_TEST:
raise ParseError("只可选择检测项")
return attrs
class QctDefectSerializer(CustomModelSerializer):
defect_name = serializers.CharField(source='defect.name', read_only=True)
@ -215,6 +213,7 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
with transaction.atomic():
ins: FtestWork = super().create(validated_data)
for ftestworkdefect in ftestworkdefect:
if ftestworkdefect['count'] > 0:
@ -226,7 +225,7 @@ class FtestWorkCreateUpdateSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
ftestworkdefect = validated_data.pop("ftestworkdefect", [])
ins:FtestWork = super().update(instance, validated_data)
with transaction.atomic():
fd_ids = []
for item in ftestworkdefect:
if item["count"] > 0:
@ -291,7 +290,7 @@ class FtestSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
with transaction.atomic():
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
@ -300,7 +299,7 @@ class FtestSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
validated_data.pop('ftest_work', None)
ftestitems = validated_data.pop('ftestitems', [])
with transaction.atomic():
instance = super().update(instance, validated_data)
for item in ftestitems:
id = item.get('id', None)
@ -363,7 +362,7 @@ class FtestProcessSerializer(CustomModelSerializer):
def create(self, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
ftestdefects = validated_data.pop('ftestdefects', [])
with transaction.atomic():
instance = super().create(validated_data)
for item in ftestitems:
FtestItem.objects.create(ftest=instance, **item)
@ -392,13 +391,13 @@ class FtestProcessSerializer(CustomModelSerializer):
def update(self, instance, validated_data):
ftestitems = validated_data.pop('ftestitems', [])
ftestdefects = validated_data.pop('ftestdefects', [])
with transaction.atomic():
instance = super().update(instance, validated_data)
for item in ftestitems:
try:
ins = FtestItem.objects.get(testitem = item["testitem"], ftest=instance)
except FtestItem.DoesNotExist:
ins = FtestItem.objects.create(ftest=instance, **item)
raise ParseError("新的检测项!")
for k, v in item.items():
setattr(ins, k, v)
ins.save()
@ -412,7 +411,7 @@ class FtestProcessSerializer(CustomModelSerializer):
myLogger.error(f"缺陷项重复!-ftestid:{instance.id}-defectid:{item2['defect'].id}")
raise ParseError("获取到重复的缺陷项!")
except FtestDefect.DoesNotExist:
ins = FtestDefect.objects.create(ftest=instance, **item2)
raise ParseError("新的缺陷项!")
for k, v in item2.items():
setattr(ins, k, v)
ins.save()

View File

@ -6,7 +6,6 @@ from django.utils import timezone
from apps.wf.models import Ticket
from apps.qm.models import NotOkOption, Defect
from apps.wpm.services_2 import ana_batch_thread
from apps.inm.models import MaterialBatch
def ftestwork_submit_validate(ins: FtestWork):
wm:WMaterial = ins.wm
@ -24,8 +23,8 @@ def ftestwork_submit_validate(ins: FtestWork):
def ftestwork_submit(ins:FtestWork, user: User):
wm:WMaterial = ins.wm
fwd_qs = FtestworkDefect.objects.filter(ftestwork=ins)
if wm and ins.need_update_wm:
if ins.qct is None and not fwd_qs.exists():
if ins.need_update_wm:
if ins.qct is None or not fwd_qs.exists():
if wm.state == WMaterial.WM_TEST:
# 更新对应的车间库存
wm.count = wm.count - ins.count
@ -100,7 +99,7 @@ def ftestwork_submit(ins:FtestWork, user: User):
else:
wm:WMaterial = ins.wm
# 此时调用了qct表
for item in fwd_qs:
for item in FtestworkDefect.objects.filter(ftestwork=ins):
item:FtestworkDefect = item
if item.count > 0:
wm.count = wm.count - item.count
@ -125,37 +124,9 @@ def ftestwork_submit(ins:FtestWork, user: User):
if not new_create:
wmx.count = wmx.count + item.count
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_time = timezone.now()
ins.save()
# 触发批次统计分析
ana_batch_thread(xbatchs=[ins.batch])
def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
ins = FtestWork.objects.get(id=new_ticket_data['t_id'])
@ -177,3 +148,4 @@ def bind_ftestwork(ticket: Ticket, transition, new_ticket_data: dict):
def ftestwork_audit_end(ticket: Ticket):
ins = FtestWork.objects.get(id=ticket.ticket_data['t_id'])
ftestwork_submit(ins, ticket.create_by)
ana_batch_thread(xbatchs=[ins.batch])

View File

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

View File

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

View File

@ -275,11 +275,13 @@ class DeptCreateUpdateSerializer(CustomModelSerializer):
model = Dept
exclude = EXCLUDE_FIELDS + ['third_info']
@transaction.atomic
def create(self, validated_data):
ins = super().create(validated_data)
sync_dahua_dept(ins)
return ins
@transaction.atomic
def update(self, instance, validated_data):
ins = super().update(instance, validated_data)
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 rest_framework.decorators import action
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,
MultiPartParser)
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.filters import DeptFilterSet, UserFilterSet
# 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 apps.utils.permission import ALL_PERMS
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
@ -228,7 +229,7 @@ class PTaskViewSet(CustomModelViewSet):
return Response()
class PTaskResultViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
class PTaskResultViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet):
"""
list:任务执行结果列表
@ -372,7 +373,7 @@ class RoleViewSet(CustomModelViewSet):
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']
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']
def perform_create(self, serializer):
with transaction.atomic():
instance = serializer.save()
user = instance.user
up = UserPost.objects.filter(user=user).order_by(
@ -416,6 +418,7 @@ class UserPostViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListMod
ep.save()
def perform_destroy(self, instance):
with transaction.atomic():
user = instance.user
instance.delete()
up = UserPost.objects.filter(user=user).order_by(
@ -607,7 +610,7 @@ class UserViewSet(CustomModelViewSet):
return Response()
class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin, CustomGenericViewSet):
class FileViewSet(CustomCreateModelMixin, RetrieveModelMixin, ListModelMixin, CustomGenericViewSet):
"""文件上传
list:
@ -648,7 +651,7 @@ class FileViewSet(BulkCreateModelMixin, RetrieveModelMixin, CustomListModelMixin
instance.save()
class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, GenericViewSet):
class ApkViewSet(MyLoggingMixin, ListModelMixin, CreateModelMixin, GenericViewSet):
perms_map = {'get': '*', 'post': 'apk.upload'}
serializer_class = ApkSerializer
@ -689,7 +692,7 @@ class ApkViewSet(MyLoggingMixin, CustomListModelMixin, BulkCreateModelMixin, Gen
return Response()
class MyScheduleViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyModelMixin, CustomGenericViewSet):
class MyScheduleViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet):
perms_map = {'get': '*', 'post': '*',
'delete': 'myschedule.delete'}
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 ''
@transaction.atomic
def perform_create(self, serializer):
vdata = serializer.validated_data
vdata['create_by'] = self.request.user # 不可少

View File

@ -1,51 +0,0 @@
from contextlib import contextmanager
from rest_framework.exceptions import ParseError
from functools import wraps
from django.db import transaction
@contextmanager
def lock_model_record(model_class, pk):
"""
Locks a model instance and returns it.
"""
try:
instance = model_class.objects.select_for_update().get(pk=pk)
yield instance
except model_class.DoesNotExist:
raise ParseError("该记录不存在或已被删除")
def lock_model_record_d_func(model_class, pk_attr='id'):
"""
通用模型锁装饰器内置事务用于装饰函数
"""
def decorator(func):
@wraps(func)
@transaction.atomic
def wrapper(old_instance, *args, **kwargs):
try:
# 获取新鲜记录
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
# 调用原函数,但传入新鲜记录
return func(fresh_record, *args, **kwargs)
except model_class.DoesNotExist:
raise ParseError('记录不存在或已被删除')
return wrapper
return decorator
def lock_model_record_d_method(model_class, pk_attr='id'):
"""
通用模型锁装饰器内置事务, 用于装饰类方法
"""
def decorator(func):
@wraps(func)
@transaction.atomic
def wrapper(self, old_instance, *args, **kwargs):
try:
# 获取新鲜记录
fresh_record = model_class.objects.select_for_update().get(pk=getattr(old_instance, pk_attr))
# 调用原函数,但传入新鲜记录
return func(self, fresh_record, *args, **kwargs)
except model_class.DoesNotExist:
raise ParseError('记录不存在或已被删除')
return wrapper
return decorator

View File

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

View File

@ -116,26 +116,11 @@ class BaseModel(models.Model):
@classmethod
def safe_get_or_create(cls, defaults=None, **kwargs):
defaults = defaults or {}
for attempt in range(3):
try:
# 先尝试获取(带锁)
try:
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
lock_data = {**kwargs, **defaults}
lock_hash = hashlib.md5(str(lock_data).encode()).hexdigest()
lock_key = f"safe_get_or_create:{cls.__name__}:{lock_hash}"
with cache.lock(lock_key, timeout=10):
return cls.objects.get_or_create(**kwargs, defaults=defaults)
def handle_parent(self):
@ -147,7 +132,7 @@ class BaseModel(models.Model):
if not self.id:
is_create = True
self.id = idWorker.get_id()
with transaction.atomic():
old_parent = None
need_handle_parent = False
if hasattr(self, "parent"):

View File

@ -75,8 +75,8 @@ class CustomModelSerializer(DynamicFieldsMixin, TreeSerializerMixin, serializers
class QuerySerializer(serializers.Serializer):
field = serializers.CharField(label='字段名')
compare = serializers.ChoiceField(
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains", "isnull"])
value = serializers.JSONField(label='', allow_null=True)
label='比较式', choices=["", "!", "gte", "gt", "lte", "lt", "in", "contains"])
value = serializers.CharField(label='')
class ComplexSerializer(serializers.Serializer):
@ -85,4 +85,3 @@ class ComplexSerializer(serializers.Serializer):
ordering = serializers.CharField(required=False)
querys = serializers.ListField(child=QuerySerializer(
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')
@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):
from aliyunsdkcore.client import AcsClient
from aliyunsdkcore.request import CommonRequest

View File

@ -1,6 +1,4 @@
from django.db import connection
from django.utils import timezone
from datetime import datetime
def execute_raw_sql(sql: str, params=None):
"""执行原始sql并返回rows, columns数据
@ -25,7 +23,7 @@ def format_sqldata(columns, 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:
@ -38,19 +36,9 @@ def query_all_dict(sql, params=None, with_time_format=False):
else:
cursor.execute(sql)
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()]
def query_one_dict(sql, params=None, with_time_format=False):
def query_one_dict(sql, params=None):
"""
查询一个结果返回字典类型数据
:param sql:
@ -58,17 +46,13 @@ def query_one_dict(sql, params=None, with_time_format=False):
:return:
"""
with connection.cursor() as cursor:
cursor.execute(sql, params or ()) # 更简洁的参数处理
if params:
cursor.execute(sql, params=params)
else:
cursor.execute(sql)
columns = [desc[0] for desc in cursor.description]
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))
import pymysql
import psycopg2

View File

@ -1,6 +1,6 @@
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.exceptions import ParseError
from rest_framework.mixins import RetrieveModelMixin
@ -18,9 +18,7 @@ from apps.utils.serializers import ComplexSerializer
from rest_framework.throttling import UserRateThrottle
from drf_yasg.utils import swagger_auto_schema
import json
from django.db import connection
from django.core.exceptions import ObjectDoesNotExist
from django.db.utils import NotSupportedError
class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
"""
@ -91,36 +89,6 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet):
elif 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):
action_serializer_name = f"{self.action}_serializer_class"
action_serializer_class = getattr(self, action_serializer_name, None)
@ -226,3 +194,4 @@ class CustomModelViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomListM
"""
增强的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'),
),
]

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