Compare commits

..

50 Commits

Author SHA1 Message Date
caoqianming db22300fd0 feat: mioitem添加排序字段 2025-05-06 10:54:31 +08:00
caoqianming 962ca5b1be feat: 完善光子批次统计数据 2025-04-30 08:51:00 +08:00
caoqianming 283c566b71 fix: getattr(item, field) is not None 2025-04-29 15:14:20 +08:00
caoqianming d6504fa072 feat: 组合件和入库检验追加统计分析 2025-04-29 14:45:30 +08:00
caoqianming 5857b77aea feat: get_alldata_with_batch添加内容 2025-04-29 14:43:34 +08:00
caoqianming dd0d6751c1 fix: 生产入库时存入生产车间 2025-04-25 13:20:16 +08:00
caoqianming a13ac84ccb fix: do_in保证production_dept赋值 2025-04-24 16:40:48 +08:00
caoqianming 21e9a49069 fix: 完善负数校验2 2025-04-24 16:16:00 +08:00
caoqianming d815d84333 fix: 完善负数校验 2025-04-24 16:00:09 +08:00
caoqianming 0a5cbca4ed feat: 6车间合格率统计decimal invalid 2025-04-24 13:15:14 +08:00
caoqianming ee7908f6bc feat: 通过django settings延迟获取BASE_PROJECT_CODE 2025-04-24 13:14:53 +08:00
caoqianming f4bb1d952f feat: base 将配置文件放到单独的config文件夹中防止误操作 2025-04-24 13:14:43 +08:00
caoqianming f501f2a6ea fix: count_working获取逻辑优化2 2025-04-24 13:14:19 +08:00
caoqianming e47c853578 fix: count_working获取逻辑优化 2025-04-24 13:14:06 +08:00
caoqianming 9362a81bc6 feat: 日志完善负值校验 2025-04-24 13:13:39 +08:00
caoqianming 5498d98e38 feat: base增加PositiveDecimalField 2025-04-24 13:11:54 +08:00
caoqianming a31aa7e337 feat: update_material_count时更新组合件数量 2025-04-24 13:11:28 +08:00
caoqianming a97804e455 fix: 订单检索条件错误 2025-04-24 13:08:25 +08:00
caoqianming 2f9e054558 feat: 工艺步骤增加排序字段 2025-04-24 13:06:54 +08:00
caoqianming 29e15f0f8e feat: 物料筛选low_inm进行优化 2025-04-24 13:06:22 +08:00
caoqianming cafa1c9c87 fix: mio_saleout时正确计算delivered_count 2025-04-24 13:06:12 +08:00
caoqianming bf368bcd26 feat: 变更order状态 2025-04-24 13:05:57 +08:00
caoqianming b9372204a6 feat: update_inm关于销售发货/其他出库的bug 2025-04-24 13:04:48 +08:00
caoqianming 9ad947770a feat: fmlog添加is_fix字段校验 2025-04-24 13:04:35 +08:00
caoqianming 1623d2d684 feat: fmlog添加is_fix字段 2025-04-24 13:03:56 +08:00
caoqianming 4a5f6d9d0e fix: get_alldata_with_batch中小数计算异常捕获 2025-04-24 13:02:54 +08:00
caoqianming b948688ab9 feat: ptest性能检验样品编号字段更改为text 2025-04-24 13:02:42 +08:00
caoqianming 91a499c00b fix: 光子综合查询对小数和None的处理 2025-04-24 13:02:17 +08:00
caoqianming 3b78c4e993 fix: decimal存入json字段时使用myjsondecoder 2025-04-24 13:02:02 +08:00
caoqianming c8ce78f50d feat: 添加MyJSONEncoder以支持decimal 2025-04-24 13:01:49 +08:00
caoqianming b5514afa2b fix: get_alldata_with_batch_and_store时保存json数据采用DjangoJSONEncoder以处理decimal 2025-04-24 13:01:31 +08:00
caoqianming 04daccb733 fix: 组合件入库后未添加count的bug 2025-04-24 13:01:21 +08:00
caoqianming e8cd841ef1 feat: 销售发货编号非必填 2025-04-24 13:01:10 +08:00
caoqianming 2831c9b58b feat: HTML_BASE_URL 前缀统一修改 2025-04-24 13:00:43 +08:00
caoqianming 3175bcf4dc feat: sysbaseview返回系统版本号 2025-04-24 13:00:15 +08:00
caoqianming 5354c064a1 fix: base log delay=True减少冲突 2025-04-24 12:59:59 +08:00
caoqianming 6e9b2c8264 change: base 改变log backupcount为30 2025-04-24 12:59:44 +08:00
caoqianming 6aae02044f perf: base settings里日志记录handler优化 2025-04-24 12:59:30 +08:00
caoqianming 4bccc5a62b feat: 完善版本追踪 2025-04-24 12:59:13 +08:00
caoqianming da5400996f feat: utask编号可不填2 2025-04-24 11:35:10 +08:00
caoqianming 0b20199284 feat: utask编号可不填 2025-04-24 11:34:59 +08:00
caoqianming 88bc901c84 feat: mio 编号可不填 2025-04-24 11:34:11 +08:00
caoqianming 87b935ab04 feat: MIODo 可不填belong_dept 2025-04-24 11:33:14 +08:00
caoqianming c3108641f3 feat: mio number不设置为只读 2025-04-24 11:33:03 +08:00
caoqianming 7b1a6853ab fix: wmaterial search bug 2025-04-24 11:32:46 +08:00
caoqianming 5ae9e235df fix: wmaterial search bug3 2025-04-24 11:32:37 +08:00
caoqianming b327da2342 fix: wmaterial search bug2 2025-04-24 11:32:25 +08:00
caoqianming a581d40ef9 fix: wmaterial search bug 2025-04-24 11:31:56 +08:00
caoqianming 3a263735b0 feat: mio utask number非必填 2025-04-24 11:31:14 +08:00
caoqianming 4cc8236bcb fix: wmaterial 去除 material__process__exclude 字段 2025-04-24 11:30:33 +08:00
213 changed files with 2539 additions and 11327 deletions

View File

@ -7,7 +7,7 @@ from apps.utils.models import CommonADModel, CommonBModel
class Area(CommonBModel):
"""
TN: 地图区域
地图区域
"""
AREA_1 = 10
AREA_2 = 20
@ -47,7 +47,7 @@ class Area(CommonBModel):
class Access(CommonADModel):
"""
TN: 准入禁入权限(动态变化)
准入禁入权限(动态变化)
"""
ACCESS_IN_YES = 10
ACCESS_IN_NO = 20

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

@ -10,7 +10,7 @@ from apps.auth1.errors import USERNAME_OR_PASSWORD_WRONG
from rest_framework_simplejwt.tokens import RefreshToken
from django.core.cache import cache
from apps.auth1.services import check_phone_code
from apps.utils.sms import send_sms
from apps.utils.tools import rannum
from apps.utils.wxmp import wxmpClient
from apps.utils.wx import wxClient
@ -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
@ -183,7 +182,6 @@ class SendCode(CreateAPIView):
短信验证码发送
"""
from apps.utils.sms import send_sms
phone = request.data['phone']
code = rannum(6)
is_ok, _ = send_sms(phone, 505, {'code': code})
@ -235,29 +233,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

@ -5,19 +5,16 @@ from apps.bi.models import Dataset
import concurrent
from apps.utils.sql import execute_raw_sql, format_sqldata
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE", "INSERT", "CREATE", "ALTER", "GRANT", "REVOKE", "EXEC", "EXECUTE"]
forbidden_keywords = ["UPDATE", "DELETE", "DROP", "TRUNCATE"]
def check_sql_safe(sql: str):
"""检查sql安全性
"""
sql_upper = sql.upper()
# 将SQL按空格和分号分割成单词
words = [word for word in sql_upper.replace(';', ' ').split() if word]
for kw in forbidden_keywords:
# 检查关键字是否作为独立单词出现
if kw in words:
raise ParseError(f'sql查询有风险-{kw}')
if kw in sql_upper:
raise ParseError('sql查询有风险')
return sql
def format_json_with_placeholders(json_str, **kwargs):

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

@ -1,11 +0,0 @@
from django_filters import rest_framework as filters
from django.utils import timezone
from .models import LabelTemplate
from apps.utils.filters import MyJsonListFilter
class LabelTemplateFilter(filters.FilterSet):
process = MyJsonListFilter(label='按工序查询', field_name="process_json")
class Meta:
model = LabelTemplate
fields = ['process', "name"]

View File

@ -1,28 +0,0 @@
# Generated by Django 3.2.12 on 2025-04-30 05:17
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('cm', '0003_alter_lablemat_state'),
]
operations = [
migrations.CreateModel(
name='LabelTemplate',
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.TextField(verbose_name='名称')),
('commands', models.JSONField(blank=True, default=list, verbose_name='指令模板')),
],
options={
'abstract': False,
},
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-05-06 02:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cm', '0004_labeltemplate'),
]
operations = [
migrations.AddField(
model_name='labeltemplate',
name='process_json',
field=models.JSONField(blank=True, default=list, verbose_name='工序'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-05-23 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cm', '0005_labeltemplate_process_json'),
]
operations = [
migrations.AlterField(
model_name='lablemat',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
]

View File

@ -3,40 +3,13 @@ from apps.utils.models import BaseModel
from apps.mtm.models import Material
from apps.pum.models import Supplier
from apps.wpm.models import WmStateOption
from rest_framework.exceptions import NotFound, ParseError
# Create your models here.
class LableMat(BaseModel):
"""TN: 标签物料"""
state = models.PositiveSmallIntegerField('状态', default=10, choices=WmStateOption.choices)
material = models.ForeignKey(Material, on_delete=models.CASCADE)
batch = models.TextField('批次号', db_index=True)
batch = models.CharField('批次号', max_length=100)
supplier = models.ForeignKey(Supplier, verbose_name='外协供应商', on_delete=models.SET_NULL, null=True, blank=True)
notok_sign = models.CharField('不合格标记', max_length=10, null=True, blank=True)
defect = models.ForeignKey("qm.defect", verbose_name='缺陷', on_delete=models.SET_NULL, null=True, blank=True)
material_origin = models.ForeignKey(Material, verbose_name='原始物料', on_delete=models.SET_NULL, null=True, blank=True, related_name='lm_mo')
class LabelTemplate(BaseModel):
"""TN: 标签模板"""
name = models.TextField("名称")
commands = models.JSONField("指令模板", default=list, blank=True)
process_json = models.JSONField("工序", default=list, blank=True)
@classmethod
def gen_commands(cls, label_template, label_template_name, tdata):
if label_template:
lt = LabelTemplate.objects.get(id=label_template)
else:
lt = LabelTemplate.objects.filter(name=label_template_name).first()
if not lt:
raise NotFound("标签模板不存在")
commands:list = lt.commands
try:
n_commands = []
for item in commands:
item = item.format(**tdata)
n_commands.append(item)
except Exception as e:
raise ParseError(f"标签解析错误-{str(e)}")
return n_commands

View File

@ -1,20 +1,11 @@
from rest_framework import serializers
from .models import LableMat, LabelTemplate
from .models import LableMat
from apps.qm.models import NotOkOption
from apps.wpm.models import WmStateOption
from apps.utils.serializers import CustomModelSerializer
class TidSerializer(serializers.Serializer):
tid = serializers.CharField(label='表ID')
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
extra_data = serializers.JSONField(label='额外数据', allow_null=True, required=False)
class Tid2Serializer(serializers.Serializer):
label_template = serializers.CharField(label='标签模板ID', allow_null=True, required=False)
label_template_name = serializers.CharField(label='标签模板名称', allow_null=True, required=False)
data = serializers.JSONField(label='数据', allow_null=True, required=False)
class LabelMatSerializer(serializers.ModelSerializer):
@ -33,9 +24,3 @@ class LabelMatSerializer(serializers.ModelSerializer):
def get_state_name(self, obj):
return getattr(WmStateOption, str(obj.state), WmStateOption.OK).label if obj.state else None
class LabelTemplateSerializer(CustomModelSerializer):
class Meta:
model = LabelTemplate
fields = '__all__'

View File

@ -1,13 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.cm.views import LableMatViewSet, LabelTemplateViewSet
from apps.cm.views import LableMatViewSet
API_BASE_URL = 'api/cm/'
HTML_BASE_URL = 'dhtml/cm/'
router = DefaultRouter()
router.register('labelmat', LableMatViewSet, basename='labelmat')
router.register('labeltemplate', LabelTemplateViewSet, basename='labeltemplate')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -1,12 +1,11 @@
from apps.cm.models import LableMat, LabelTemplate
from apps.cm.models import LableMat
from rest_framework.decorators import action
from apps.cm.serializers import TidSerializer, LabelMatSerializer, LabelTemplateSerializer, Tid2Serializer
from apps.cm.serializers import TidSerializer, LabelMatSerializer
from apps.inm.models import MaterialBatch, MIOItem
from apps.wpm.models import WMaterial
from rest_framework.exceptions import ParseError, NotFound
from rest_framework.response import Response
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin, CustomModelViewSet
from apps.cm.filters import LabelTemplateFilter
from apps.utils.viewsets import CustomGenericViewSet, RetrieveModelMixin, CustomListModelMixin
# Create your views here.
SPLIT_FIELD = "#"
@ -25,9 +24,6 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从仓库明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
mb = MaterialBatch.objects.get(id=tid)
except MaterialBatch.DoesNotExist:
@ -35,10 +31,6 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
obj, _ = LableMat.objects.get_or_create(state=mb.state, material=mb.material, batch=mb.batch, defect=mb.defect, defaults={"supplier": mb.supplier})
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
@ -49,9 +41,6 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从车间库存明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
wm = WMaterial.objects.get(id=tid)
except WMaterial.DoesNotExist:
@ -61,10 +50,6 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
material_origin=wm.material_origin)
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
@action(methods=["post"], detail=False, serializer_class=TidSerializer)
@ -75,9 +60,6 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
从出入库明细获取物料标签
"""
tid = request.data.get("tid")
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
extra_data = request.data.get("extra_data", {})
try:
mioitem: MIOItem = MIOItem.objects.get(id=tid)
except MIOItem.DoesNotExist:
@ -85,32 +67,4 @@ class LableMatViewSet(CustomListModelMixin, RetrieveModelMixin, CustomGenericVie
obj, _ = LableMat.objects.get_or_create(state=10, material=mioitem.material, batch=mioitem.batch, defaults={"supplier": mioitem.mio.supplier})
rdata = LabelMatSerializer(obj).data
rdata["code_label"] = f"mat{SPLIT_FIELD}{obj.id}"
if label_template or label_template_name:
tdata = {**rdata, **extra_data}
commands = LabelTemplate.gen_commands(label_template, label_template_name, tdata)
rdata["commands"] = commands
return Response(rdata)
class LabelTemplateViewSet(CustomModelViewSet):
"""
list: 标签模板
标签模板
"""
queryset = LabelTemplate.objects.all()
serializer_class = LabelTemplateSerializer
filterset_class = LabelTemplateFilter
@action(methods=["post"], detail=False, serializer_class=Tid2Serializer, perms_map={"post": "*"})
def commands(self, request, *args, **kwargs):
"""
获取标签指令
获取标签指令
"""
label_template = request.data.get("label_template", None)
label_template_name = request.data.get("label_template_name", None)
data = request.data.get("data", {})
return Response({"commands": LabelTemplate.gen_commands(label_template, label_template_name, data)})

View File

@ -3,7 +3,6 @@ from apps.utils.models import CommonADModel
# Create your models here.
class Article(CommonADModel):
"""TN: 文章"""
title = models.CharField(max_length=200, verbose_name="标题")
poster = models.TextField(verbose_name="封面地址", null=True, blank=True)
video = models.TextField(verbose_name="视频地址", null=True, blank=True)

View File

@ -5,7 +5,6 @@ from apps.utils.models import BaseModel
class Project(BaseModel):
"""TN: 项目"""
name = models.CharField('项目名称', max_length=50)
code = models.CharField('标识', max_length=20, unique=True)
config_json = models.JSONField('配置信息', default=dict)

View File

@ -36,8 +36,3 @@ class SpeakerSerializer(serializers.Serializer):
class AreaManSerializer(serializers.Serializer):
area = serializers.CharField()
class ServerTimeSerializer(serializers.Serializer):
server_time = serializers.DateTimeField()
timezone = serializers.CharField(read_only=True)

View File

@ -2,8 +2,6 @@ from __future__ import absolute_import, unicode_literals
from celery import shared_task
import subprocess
from server.settings import DATABASES, BACKUP_PATH, SH_PATH, SD_PWD
import logging
myLogger = logging.getLogger('log')
@shared_task
@ -25,7 +23,6 @@ def backup_database():
@shared_task
def reload_server_git():
command = "bash {}/git_server.sh".format(SH_PATH)
myLogger.info(f"reload_server_git: {command}")
completed = subprocess.run(command, shell=True, capture_output=True, text=True)
if completed.returncode != 0:
return completed.stderr

View File

@ -1,6 +1,5 @@
from django.urls import path, include
from apps.develop.views import (BackupDatabase, BackupMedia, ReloadClientGit,
ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet, testScanHtml, ServerTime)
from apps.develop.views import BackupDatabase, BackupMedia, ReloadClientGit, ReloadServerGit, ReloadServerOnly, TestViewSet, CorrectViewSet
from rest_framework.routers import DefaultRouter
API_BASE_URL = 'api/develop/'
@ -15,7 +14,5 @@ urlpatterns = [
path(API_BASE_URL + 'reload_server_only/', ReloadServerOnly.as_view()),
path(API_BASE_URL + 'backup_database/', BackupDatabase.as_view()),
path(API_BASE_URL + 'backup_media/', BackupMedia.as_view()),
path(API_BASE_URL + 'server_time/', ServerTime.as_view()),
path(API_BASE_URL, include(router.urls)),
path(HTML_BASE_URL + "testscan/", testScanHtml)
]

View File

@ -2,14 +2,14 @@
from rest_framework.views import APIView
from rest_framework.exceptions import ParseError
from rest_framework.permissions import IsAdminUser, AllowAny
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.decorators import action
from apps.am.models import Area
from apps.am.tasks import cache_areas_info
from apps.develop.serializers import AreaManSerializer, CleanDataSerializer, GenerateVoiceSerializer, SendSmsSerializer, SpeakerSerializer, \
TestTaskSerializer, TestAlgoSerializer, ServerTimeSerializer
TestTaskSerializer, TestAlgoSerializer
from apps.develop.tasks import backup_database, backup_media, reload_web_git, reload_server_git, reload_server_only
from rest_framework.exceptions import APIException
from apps.ecm.service import check_not_in_place, create_remind, handle_xx_event, loc_change, notify_event, rail_in, snap_and_analyse
@ -19,6 +19,7 @@ from apps.opm.models import Opl
from apps.third.dahua import dhClient
from apps.third.speaker import spClient
from apps.third.models import TDevice
from apps.utils.permission import RbacPermission
from apps.utils.sms import send_sms
from apps.utils.speech import generate_voice
from apps.utils.tools import get_info_from_id
@ -28,50 +29,14 @@ from rest_framework.authentication import BasicAuthentication, SessionAuthentica
from apps.utils.viewsets import CustomGenericViewSet
from apps.utils.wx import wxClient
from apps.wf.models import Transition
from apps.wf.models import State, Transition, Workflow
from django.db import transaction
from apps.utils.snowflake import idWorker
from django.core.cache import cache
from apps.utils.decorators import auto_log
from django.http import HttpResponse
from server.settings import SD_PWD, TIME_ZONE
import subprocess
from drf_yasg.utils import swagger_auto_schema
from datetime import datetime
import json
from apps.utils.decorators import auto_log, idempotent
# Create your views here.
class ServerTime(APIView):
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAdminUser()]
@swagger_auto_schema(responses={200: ServerTimeSerializer})
def get(self, request):
"""
获取服务器时间
获取服务器时间
"""
return Response({"server_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "timezone": TIME_ZONE})
@swagger_auto_schema(request_body=ServerTimeSerializer)
def post(self, request):
"""
修改服务器时间
修改服务器时间
"""
command = f'date -s "{request.data["server_time"]}"'
completed = subprocess.run(
["sudo", "-S", "sh", "-c", command], # 添加 -S 参数
input=SD_PWD + "\n", # 注意要在密码后加换行符
capture_output=True,
text=True
)
if completed.returncode != 0:
raise ParseError(completed.stderr)
return Response()
class ReloadServerGit(APIView):
permission_classes = [IsAdminUser]
@ -650,14 +615,14 @@ class TestViewSet(CustomGenericViewSet):
'apps.rpm.tasks.close_rpj_by_leave_time']).delete()
if 'mes' in datas:
from apps.inm.models import MaterialBatch, MIO
from apps.mtm.models import Material, RoutePack
from apps.mtm.models import Material
from apps.wpmw.models import Wpr
from apps.wpm.models import WMaterial, Mlog, Handover, BatchSt
from apps.wpm.models import WMaterial, Mlog
from apps.pum.models import PuOrder
from apps.sam.models import Order
from apps.pm.models import Utask, Mtask
from apps.wpm.models import Handover
from apps.qm.models import Ftest, FtestWork
from apps.wf.models import Ticket
MaterialBatch.objects.all().delete()
MIO.objects.all().delete()
Wpr.objects.all().delete()
@ -670,9 +635,7 @@ class TestViewSet(CustomGenericViewSet):
Handover.objects.all().delete()
Ftest.objects.all().delete()
FtestWork.objects.all().delete()
Material.objects.get_queryset(all=True).update(count=0, count_mb=0, count_wm=0)
BatchSt.objects.all().delete()
Ticket.objects.get_queryset(all=True).delete()
Material.objects.all().update(count=0, count_mb=0, count_wm=0)
return Response()
@action(methods=['post'], detail=False, serializer_class=Serializer, permission_classes=[])
@ -680,35 +643,3 @@ class TestViewSet(CustomGenericViewSet):
from apps.wpm.tasks import cal_exp_duration_sec
cal_exp_duration_sec('3397169058570170368')
return Response()
html_str = """
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebView 扫码</title>
</head>
<body>
<button onclick="openScanner()">扫码</button>
<p>扫描结果: <span id="result"></span></p>
<script>
function openScanner() {
if (window.Android) {
window.Android.openScanner();
} else {
alert("当前环境不支持扫码");
}
}
function onScanResult(data) {
document.getElementById("result").innerText = data;
}
</script>
</body>
</html>
"""
def testScanHtml(request):
return HttpResponse(html_str)

View File

@ -5,7 +5,7 @@ from django_celery_beat.models import PeriodicTask
# Create your models here.
class RiskPoint(CommonAModel):
"""
TN: 风险点表
风险点表
"""
R_LEVEL = (
(10, '低风险'),
@ -34,7 +34,7 @@ class RiskPoint(CommonAModel):
class Risk(CommonAModel):
"""
TN: 风险表
风险表
"""
name = models.TextField('项目/步骤')
level = models.PositiveSmallIntegerField('风险等级', default=10, choices=RiskPoint.R_LEVEL)
@ -56,7 +56,7 @@ class Risk(CommonAModel):
class CheckTaskSet(CommonADModel):
"""
TN:检查任务派发设置
检查任务派发设置
"""
riskpoint = models.ForeignKey(RiskPoint, verbose_name='关联风险点', related_name='ctask_riskpoint', on_delete=models.SET_NULL, null=True, blank=True)
note = models.TextField('派发备注', null=True, blank=True)
@ -68,7 +68,7 @@ class CheckTaskSet(CommonADModel):
class CheckWork(CommonAModel):
"""
TN: 检查工作
检查工作
"""
CW_TYPE = (
(10, '手动'),
@ -90,7 +90,7 @@ class CheckWork(CommonAModel):
class Hazard(CommonADModel):
"""
TN: 事故隐患表
事故隐患表
"""
H_HARM = (
(10, '无伤害'),
@ -142,7 +142,7 @@ class Hazard(CommonADModel):
class CheckItem(BaseModel):
"""
TN:检查工作-隐患关联表
检查工作-隐患关联表
"""
CITEM_RESULT = (
(10, '未检查'),

View File

@ -16,7 +16,7 @@ LEVEL_CHOICES = (
class EventCate(CommonAModel):
"""
TN: 事件种类
事件种类
"""
EVENT_TRIGGER_CHOICES = (
(10, '监控'),
@ -47,7 +47,7 @@ class EventCate(CommonAModel):
class AlgoChannel(BaseModel):
"""TN: 算法通道"""
algo = models.ForeignKey(EventCate, verbose_name='关联算法', on_delete=models.CASCADE, related_name='ac_algo')
vchannel = models.ForeignKey(TDevice, verbose_name='视频通道', on_delete=models.CASCADE, related_name='ac_vchannel')
always_on = models.BooleanField("实时开启", default=False)
@ -58,7 +58,7 @@ class AlgoChannel(BaseModel):
class NotifySetting(CommonADModel):
"""
TN: 提醒配置
提醒配置
"""
PUSH_FILTER1_CHOICES = (
(10, '当事人部门'),
@ -84,7 +84,7 @@ class NotifySetting(CommonADModel):
class Event(CommonBDModel):
"""
TN: 事件
事件
"""
EVENT_MARK_CHOICES = (
(10, '正常'),
@ -118,7 +118,6 @@ class Event(CommonBDModel):
class Eventdo(BaseModel):
"""TN: 事件处理记录"""
cate = models.ForeignKey(EventCate, verbose_name='关联事件种类',
on_delete=models.CASCADE, related_name='do_cate')
event = models.ForeignKey(Event, verbose_name='关联事件',
@ -131,7 +130,7 @@ class Eventdo(BaseModel):
class Remind(BaseModel):
"""
TN: 事件提醒表
事件提醒表
"""
event = models.ForeignKey(Event, verbose_name='关联事件',
on_delete=models.CASCADE)

View File

@ -6,7 +6,6 @@ from apps.system.models import User, Dept, File
class Train(CommonADModel):
"""TN: 线下培训"""
T_L_POST = 10
T_L_TEAM = 20
T_L_DEPT = 30
@ -28,7 +27,6 @@ class Train(CommonADModel):
class Questioncat(CommonAModel):
"""TN: 题库分类"""
name = models.CharField(max_length=200, verbose_name="名称")
parent = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, verbose_name="父级", related_name="cate_parent")
sort = models.PositiveIntegerField(default=0, verbose_name="排序")
@ -42,7 +40,6 @@ class Questioncat(CommonAModel):
class Question(CommonAModel):
"""TN: 题目"""
Q_DAN = 10
Q_DUO = 20
Q_PAN = 30
@ -68,7 +65,6 @@ class Question(CommonAModel):
class Paper(CommonAModel):
"""TN: 押题卷"""
PAPER_FIX = 10 # 固定试卷
PAPER_RANDOM = 20 # 自动抽题
@ -98,7 +94,6 @@ class Paper(CommonAModel):
class PaperQuestion(BaseModel):
"""TN: 试卷-试题关系表"""
paper = models.ForeignKey(Paper, on_delete=models.CASCADE, verbose_name="试卷")
question = models.ForeignKey(Question, on_delete=models.CASCADE, verbose_name="试题")
total_score = models.FloatField(default=0, verbose_name="单题满分")
@ -106,7 +101,6 @@ class PaperQuestion(BaseModel):
class Exam(CommonADModel):
"""TN: 在线考试"""
name = models.CharField("名称", max_length=100)
open_time = models.DateTimeField("开启时间")
close_time = models.DateTimeField("关闭时间", null=True, blank=True)
@ -126,7 +120,7 @@ class Exam(CommonADModel):
class ExamRecord(CommonADModel):
"""
TN: 考试记录表
考试记录表
"""
exam = models.ForeignKey(Exam, on_delete=models.CASCADE, verbose_name="关联考试", related_name="record_exam")
@ -145,7 +139,6 @@ class ExamRecord(CommonADModel):
class AnswerDetail(BaseModel):
"""TN: 答题记录详情表"""
examrecord = models.ForeignKey(ExamRecord, on_delete=models.CASCADE, related_name="detail_er")
total_score = models.FloatField(default=0, verbose_name="该题满分")
question = models.ForeignKey(Question, on_delete=models.CASCADE)

View File

@ -1,252 +1,26 @@
import socket
from rest_framework.exceptions import ParseError
import json
import time
from django.core.cache import cache
from apps.utils.thread import MyThread
import uuid
import logging
import threading
import requests
myLogger = logging.getLogger('log')
def get_checksum(body_msg):
return sum(body_msg) & 0xFF
def handle_bytes(arr):
if len(arr) < 8:
return f"返回数据长度错误-{arr}"
if arr[0] != 0xEB or arr[1] != 0x90:
return "数据头不正确"
# 读取长度信息
length_arr = arr[2:4][::-1] # 反转字节
length = int.from_bytes(length_arr, byteorder='little', signed=True) # 小端格式
# 提取内容
body = arr[4:4 + length - 3]
# 校验和检查
check_sum = get_checksum(body)
if check_sum != arr[length + 1]:
return "校验错误"
# 尾部标识检查
if arr[length + 2] != 0xFF or arr[length + 3] != 0xFE:
return "尾错误"
content = body.decode('utf-8')
res = json.loads(content)
return res[0]
def get_tyy_data_t(host, port, tid):
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
sc = None
def connect_and_send(retry=1):
nonlocal sc
try:
if sc is None:
sc = socket.socket()
sc.connect((host, int(port)))
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc = None
except OSError as e:
sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
except ConnectionResetError:
sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器重置了连接"})
except socket.timeout:
sc = None
cache.set(cd_thread_key_val, {"err_msg": "采集器连接超时"})
except Exception as e:
sc = None
cache.set(cd_thread_key_val, {"err_msg": f"采集器连接失败-{str(e)}"})
while cache.get(cd_thread_key_id) == tid:
if cache.get(cd_thread_key_val) == "get":
cache.set(cd_thread_key_val, "working")
connect_and_send()
if sc is None:
continue
resp = sc.recv(1024)
res = handle_bytes(resp)
if isinstance(res, str):
cache.set(cd_thread_key_val, {"err_msg": f'采集器返回数据错误-{res}'})
elif not res:
cache.set(cd_thread_key_val, {"err_msg": f"采集器返回数据为空-{str(res)}"})
else:
myLogger.info(f"采集器返回数据-{res}")
cache.set(cd_thread_key_val, res)
time.sleep(0.3)
if sc:
try:
sc.close()
except Exception:
pass
def get_tyy_data_2(*args, retry=1):
host, port = args[0], int(args[1])
cd_thread_key_id = f"cd_thread_{host}_{port}_id"
cd_thread_key_val = f"cd_thread_{host}_{port}_val"
cd_thread_val_id = cache.get(cd_thread_key_id, default=None)
if cd_thread_val_id is None:
tid = uuid.uuid4()
cache.set(cd_thread_key_id, tid, timeout=10800)
cd_thread = MyThread(target=get_tyy_data_t, args=(host, port, tid), daemon=True)
cd_thread.start()
cache.set(cd_thread_key_val, "get")
num = 0
get_val = False
while True:
num += 1
if num > 8:
break
val = cache.get(cd_thread_key_val)
if isinstance(val, dict):
get_val = True
if "err_msg" in val:
raise ParseError(val["err_msg"])
return val
time.sleep(0.3)
if not get_val and retry > 0:
cache.set(cd_thread_key_id, None)
get_tyy_data_2(*args, retry=retry-1)
sc_all = {}
sc_lock = threading.Lock()
def get_tyy_data_1(*args, retry=1):
host, port = args[0], int(args[1])
global sc_all
sc = None
def connect_and_send(retry=1):
nonlocal sc
sc = sc_all.get(f"{host}_{port}", None)
try:
if sc is None:
sc = socket.socket()
sc.settimeout(5) # 设置超时
sc.connect((host, port))
sc_all[f"{host}_{port}"] = sc
else:
# 清空接收缓冲区
sc.settimeout(0.1) # 设置短暂超时
for _ in range(5):
try:
data = sc.recv(65536)
if not data:
break
except (socket.timeout, BlockingIOError):
break
sc.settimeout(5) # 恢复原超时设置
sc.sendall(b"R")
except BrokenPipeError:
if retry > 0:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
return connect_and_send(retry-1)
else:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError("采集器连接失败-管道重置")
except OSError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接失败-{str(e)}")
except TimeoutError as e:
if sc:
try:
sc.close()
except Exception:
pass
sc_all.pop(f"{host}_{port}", None)
sc = None
raise ParseError(f"采集器连接超时-{str(e)}")
with sc_lock:
connect_and_send()
resp = sc.recv(1024)
res = handle_bytes(resp)
# myLogger.error(res)
if isinstance(res, str):
raise ParseError(f'采集器返回数据错误-{res}')
else:
return res
def get_tyy_data_3(*args, retry=2):
host, port = args[0], int(args[1])
for attempt in range(retry):
try:
# 每次请求都新建连接(确保无共享状态)
with socket.create_connection((host, port), timeout=10) as sc:
sc.sendall(b"R")
# 接收完整响应(避免数据不完整)
# resp = b""
# while True:
# chunk = sc.recv(4096)
# if not chunk:
# break
# resp += chunk
resp = sc.recv(4096)
if not resp:
raise ParseError("设备未启动")
res = handle_bytes(resp)
if isinstance(res, str):
raise ParseError(f"采集器返回数据错误: {res}")
return res
except (socket.timeout, ConnectionError) as e:
if attempt == retry - 1: # 最后一次尝试失败才报错
raise ParseError(f"采集器连接失败: {str(e)}")
time.sleep(0.5) # 失败后等待 1s 再重试
except ParseError:
raise
except Exception as e:
raise ParseError(f"未知错误: {str(e)}")
def get_tyy_data(*args):
host, port = args[0], int(args[1])
r = requests.get(f"http://127.0.0.1:2300?host={host}&port={port}")
res = r.json()
if "err_msg" in res:
raise ParseError(res["err_msg"])
sc = socket.socket()
try:
sc.connect((args[0], int(args[1])))
except Exception:
raise ParseError("无法连接到采集器")
sc.send(b"R")
resp = sc.recv(1024)
if len(resp) < 8:
raise ParseError("设备未启动")
try:
json_data = resp[5:-4]
json_str = json_data.decode('utf-8')
res = json.loads(json_str)
except Exception:
raise
finally:
sc.close()
return res
if __name__ == '__main__':
print(get_tyy_data())

View File

@ -9,7 +9,6 @@ etype_choices = ((10, "生产设备"), (20, "计量设备"), (30, "治理设备"
class Ecate(CommonADModel):
"""TN:设备类别"""
name = models.CharField("名称", max_length=50, unique=True)
code = models.CharField("编码", max_length=50, unique=True, null=True, blank=True)
type = models.PositiveSmallIntegerField("类型", choices=etype_choices, help_text=str(etype_choices))
@ -23,7 +22,8 @@ class Ecate(CommonADModel):
class Equipment(CommonBModel):
"""
TN:设备台账信息,其中belong_dept是责任部门
设备台账信息
其中belong_dept是责任部门
"""
RUNING = 10
STANDBY = 20
@ -126,7 +126,7 @@ class Equipment(CommonBModel):
class EcheckRecord(CommonADModel):
"""
TN:校准检定记录
校准检定记录
"""
CHECK_CHOICES = ((10, "正常"), (20, "异常"))
@ -139,7 +139,7 @@ class EcheckRecord(CommonADModel):
class EInspect(CommonADModel):
"""
TN:巡检记录
巡检记录
"""
INSPECT_RESULTS = (

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

@ -5,7 +5,6 @@ from apps.mtm.models import Material, Mgroup, Team
from django_celery_beat.models import PeriodicTask
class Xscript(BaseModel):
"""TN:脚本"""
name = models.CharField("脚本名称", max_length=50)
code = models.TextField("脚本内容", null=True, blank=True)
base_data = models.JSONField("基础数据", default=dict, null=True, blank=True)
@ -14,7 +13,7 @@ class Xscript(BaseModel):
periodictask = models.ForeignKey(PeriodicTask, verbose_name='关联定时任务', on_delete=models.CASCADE, related_name='xscript_periodictask', null=True, blank=True)
class Mpoint(CommonBModel):
"""TN:测点"""
"""测点"""
MT_AUTO = 10
MT_COMPUTE = 20
MT_MANUAL = 30
@ -62,7 +61,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):
@ -70,7 +69,7 @@ class Mpoint(CommonBModel):
class MpLogx(models.Model):
"""
TN:测点记录超表
测点记录超表
"""
timex = models.DateTimeField("采集时间", primary_key=True)
@ -88,7 +87,7 @@ class MpLogx(models.Model):
class MpLog(BaseModel):
"""TN:旧表(已废弃)"""
"""旧表(已废弃)"""
mpoint = models.ForeignKey(Mpoint, verbose_name="关联测点", on_delete=models.SET_NULL, null=True, blank=True)
tag_id = models.BigIntegerField("记录ID", db_index=True)
@ -98,7 +97,7 @@ class MpLog(BaseModel):
class MpointStat(CommonADModel):
"""TN:测点统计表"""
"""测点统计表"""
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year/month/day/year_s/month_s/month_st/day_s/sflog/hour_s/hour")
year = models.PositiveSmallIntegerField("", null=True, blank=True)
@ -124,7 +123,7 @@ class MpointStat(CommonADModel):
class EnStat(BaseModel):
"""
TN:能源数据统计表
能源数据统计表
"""
type = models.CharField("统计维度", max_length=50, default="hour", help_text="year_s/month_s/month_st/day_s/sflog/hour_s")
@ -175,7 +174,7 @@ class EnStat(BaseModel):
class EnStat2(BaseModel):
"""
TN:能源数据统计表2
能源数据统计表2
"""
type = models.CharField("统计维度", max_length=50, default="month_s", help_text="month_s/day_s")

View File

@ -64,7 +64,7 @@ def get_can_save_from_save_expr(expr_str: str, self_val) -> bool:
if current_val is None:
return True
else:
expr_str = expr_str.replace(f"{{{match}}}", str(current_val))
expr_str = expr_str.replace(f"{{{match}}}", current_val)
try:
rval = eval(expr_str)
except Exception as e:
@ -207,8 +207,6 @@ class MpointCache:
set_eq_rs(ep_belong_id, timex, Equipment.OFFLINE)
def set(self, last_timex: datetime, last_val):
# last_timex保存到秒
last_timex = last_timex.replace(microsecond=0)
current_cache_val = self.data
cache_key = self.cache_key
last_data = current_cache_val["last_data"]

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):
"""
@ -277,20 +151,11 @@ class MpLogxViewSet(CustomListModelMixin, CustomGenericViewSet):
serializer_class = MpLogxSerializer
filterset_fields = {
"timex": ["exact", "gte", "lte", "year", "month", "day"],
"mpoint": ["exact", "in"],
"mpoint__ep_monitored": ["exact"]
"mpoint": ["exact"],
}
ordering_fields = ["timex"]
ordering = ["-timex"]
@action(methods=["get"], detail=False, perms_map={"get": "*"})
def to_wide(self, request, *args, **kwargs):
"""转换为宽表
转换为宽表
"""
queryset = self.filter_queryset(self.get_queryset())
class MpointStatViewSet(BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, CustomGenericViewSet):
"""

View File

@ -6,7 +6,6 @@ from apps.em.models import Equipment
class Drain(CommonBModel):
"""TN:排口表"""
DR_TYPE_CHOICES = (
(10, "排放口"),
(20, "污染源"),
@ -38,7 +37,7 @@ class Drain(CommonBModel):
class DrainEquip(BaseModel):
"""
TN:排口/设备关系表
排口/设备关系表
"""
drain = models.ForeignKey(Drain, verbose_name="排口", related_name="drainequip_drain", on_delete=models.CASCADE)
@ -51,7 +50,7 @@ class DrainEquip(BaseModel):
class EnvData(models.Model):
"""
TN:环保监测数据
环保监测数据
"""
enp_fields = [
@ -106,7 +105,6 @@ class EnvData(models.Model):
class VehicleAccess(BaseModel):
"""TN:车辆出入厂记录"""
type = models.PositiveSmallIntegerField("出入类型", default=1, help_text="1: 进厂, 2: 出厂")
vehicle_number = models.CharField("车牌号", max_length=10)
access_time = models.DateTimeField("出入时间", null=True, blank=True)
@ -116,7 +114,6 @@ class VehicleAccess(BaseModel):
class CarWash(BaseModel):
"""TN:洗车记录"""
station = models.ForeignKey(Equipment, verbose_name="洗车台", on_delete=models.CASCADE)
vehicle_number = models.CharField("车牌号", max_length=10, default="")
start_time = models.DateTimeField("洗车时间", null=True, blank=True)

View File

@ -6,7 +6,6 @@ from apps.wpm.models import SfLog
# Create your models here.
class Fee(BaseModel):
"""TN:费用"""
name = models.CharField('名称', max_length=20)
cate = models.CharField('父名称', max_length=20)
element = models.CharField('要素', max_length=20, help_text='直接材料/直接人工/制造费用')
@ -14,7 +13,6 @@ class Fee(BaseModel):
class FeeSet(CommonADModel):
"""TN:费用集合"""
year = models.PositiveSmallIntegerField('年份')
month = models.PositiveSmallIntegerField('月份')
mgroup = models.ForeignKey(Mgroup, verbose_name='关联工段', on_delete=models.CASCADE)
@ -23,7 +21,6 @@ class FeeSet(CommonADModel):
class PriceSet(CommonADModel):
"""TN:价格集合"""
year = models.PositiveSmallIntegerField('年份')
month = models.PositiveSmallIntegerField('月份')
material = models.ForeignKey('mtm.material', on_delete=models.CASCADE, verbose_name='关联物料')

View File

@ -8,7 +8,7 @@ from datetime import timedelta
class Employee(CommonBModel):
"""
TN:员工信息
员工信息
"""
JOB_ON = 10
JOB_OFF = 20
@ -77,7 +77,7 @@ class Employee(CommonBModel):
class NotWorkRemark(CommonADModel):
"""
TN:离岗说明
离岗说明
"""
not_work_date = models.DateField('未打卡日期')
user = models.ForeignKey(User, verbose_name='用户', on_delete=models.CASCADE)
@ -86,7 +86,7 @@ class NotWorkRemark(CommonADModel):
class Attendance(CommonADModel):
"""
TN:到岗记录
到岗记录
"""
ATT_STATE_CHOICES = [
('pending', '待定'),
@ -119,7 +119,7 @@ class Attendance(CommonADModel):
class ClockRecord(BaseModel):
"""
TN:打卡记录
打卡记录
"""
ClOCK_ON = 10
CLOCK_OFF = 20
@ -157,7 +157,7 @@ class ClockRecord(BaseModel):
class Certificate(CommonAModel):
"""
TN:证书
证书
"""
CERT_OK = 10
CERT_EXPIRING = 20

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 ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.ichat'

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

@ -1,17 +0,0 @@
from django.db import models
from apps.system.models import CommonADModel, BaseModel
# Create your models here.
class Conversation(CommonADModel):
"""
TN: 对话
"""
title = models.CharField(max_length=200, default='新对话',verbose_name='对话标题')
class Message(BaseModel):
"""
TN: 消息
"""
conversation = models.ForeignKey(Conversation, on_delete=models.CASCADE, related_name='messages', verbose_name='对话')
content = models.TextField(verbose_name='消息内容')
role = models.CharField("角色", max_length=10, default='user', help_text="system/user")

View File

@ -1,14 +0,0 @@
# 角色
你是一位数据分析专家和前端程序员,具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述, 并形成报告。
# 技能
1. 仔细分析用户提供的JSON格式数据分析用户需求。
2. 依据得到的需求, 分别获取JSON数据中的关键信息。
3. 根据2中的关键信息最优化选择表格/饼图/柱状图/折线图等格式绘制报告。
# 回答要求
1. 仅生成完整的HTML代码所有功能都需要实现支持响应式不要输出任何解释或说明。
2. 代码中如需要Echarts等js库请直接使用中国大陆的CDN链接例如bootcdn的链接。
3. 标题为 数据分析报告。
3. 在开始部分请以表格形式简略展示获取的JSON数据。
4. 之后选择最合适的图表方式生成相应的图。
5. 在最后提供可下载该报告的完整PDF的按钮和功能。
6. 在最后提供可下载含有JSON数据的EXCEL文件的按钮和功能。

View File

@ -1,53 +0,0 @@
# 角色
你是一位资深的Postgresql数据库SQL专家具备深厚的专业知识和丰富的实践经验。你能够精准理解用户的文本描述并生成准确可执行的SQL语句。
# 技能
1. 仔细分析用户提供的文本描述,明确用户需求。
2. 根据对用户需求的理解生成符合Postgresql数据库语法的准确可执行的SQL语句。
# 回答要求
1. 如果用户的询问未以 查询 开头,请直接回复 "请以 查询 开头,重新描述你的需求"。
2. 生成的SQL语句必须符合Postgresql数据库的语法规范。
3. 不要使用 Markerdown 和 SQL 语法格式输出,禁止添加语法标准、备注、说明等信息。
4. 直接输出符合Postgresql标准的SQL语句用txt纯文本格式展示即可。
5. 如果无法生成符合要求的SQL语句请直接回复 "无法生成"。
# 示例
1. 问:查询 外协白片抛 工段在2025年6月1日到2025年6月15日之间的生产合格数以及合格率等
select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '外协白片抛'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-15'
2. 问:查询 黑化 工段在2025年6月的生产合格数以及合格率等
答: select
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mgroup.name = '黑化'
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
3. 问:查询 各工段 在2025年6月的生产合格数以及合格率等
答: select
mgroup.name as 工段,
sum(mlog.count_use) as 领用数,
sum(mlog.count_real) as 生产数,
sum(mlog.count_ok) as 合格数,
sum(mlog.count_notok) as 不合格数,
CAST ( SUM ( mlog.count_ok ) AS FLOAT ) / NULLIF ( SUM ( mlog.count_real ), 0 ) * 100 AS 合格率
from wpm_mlog mlog
left join mtm_mgroup mgroup on mgroup.id = mlog.mgroup_id
where mlog.submit_time is not null
and mlog.handle_date >= '2025-06-01'
and mlog.handle_date <= '2025-06-30'
group by mgroup.id
order by mgroup.sort

View File

@ -1,22 +0,0 @@
import json
from .models import Message
from django.http import StreamingHttpResponse
def stream_generator(stream_response: bytes, conversation_id: str):
full_content = ''
for chunk in stream_response.iter_content(chunk_size=1024):
if chunk:
full_content += chunk.decode('utf-8')
try:
data = json.loads(full_content)
content = data.get("choices", [{}])[0].get("delta", {}).get("content", "")
Message.objects.create(
conversation_id=conversation_id,
content=content
)
yield f" data:{content}\n\n"
full_content = ''
except json.JSONDecodeError:
continue
return StreamingHttpResponse(stream_generator(stream_response, conversation_id), content_type='text/event-stream')

View File

@ -1,18 +0,0 @@
from rest_framework import serializers
from .models import Conversation, Message
from apps.utils.constants import EXCLUDE_FIELDS
class MessageSerializer(serializers.ModelSerializer):
class Meta:
model = Message
fields = ['id', 'conversation', 'content', 'role']
read_only_fields = EXCLUDE_FIELDS
class ConversationSerializer(serializers.ModelSerializer):
messages = MessageSerializer(many=True, read_only=True)
class Meta:
model = Conversation
fields = ['id', 'title', 'messages']
read_only_fields = EXCLUDE_FIELDS

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,16 +0,0 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from apps.ichat.views import QueryLLMviewSet, ConversationViewSet
from apps.ichat.views2 import WorkChain
API_BASE_URL = 'api/ichat/'
router = DefaultRouter()
router.register('conversation', ConversationViewSet, basename='conversation')
router.register('message', QueryLLMviewSet, basename='message')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
path(API_BASE_URL + 'workchain/ask/', WorkChain.as_view(), name='workchain')
]

View File

@ -1,88 +0,0 @@
import re
import psycopg2
import threading
from django.db import transaction
from .models import Message
# 数据库连接
def connect_db():
from server.conf import DATABASES
db_conf = DATABASES['default']
conn = psycopg2.connect(
host=db_conf['HOST'],
port=db_conf['PORT'],
user=db_conf['USER'],
password=db_conf['PASSWORD'],
database=db_conf['NAME']
)
return conn
def extract_sql_code(text):
# 优先尝试 ```sql 包裹的语句
match = re.search(r"```sql\s*(.+?)```", text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
# fallback: 寻找首个 select 语句
match = re.search(r"(SELECT\s.+?;)", text, re.IGNORECASE | re.DOTALL)
if match:
return match.group(1).strip()
return None
def get_schema_text(conn, table_names:list):
cur = conn.cursor()
query = """
SELECT
table_name, column_name, data_type
FROM
information_schema.columns
WHERE
table_schema = 'public'
and table_name in %s;
"""
cur.execute(query, (tuple(table_names), ))
schema = {}
for table_name, column_name, data_type in cur.fetchall():
if table_name not in schema:
schema[table_name] = []
schema[table_name].append(f"{column_name} ({data_type})")
cur.close()
schema_text = ""
for table_name, columns in schema.items():
schema_text += f"{table_name} 包含列:{', '.join(columns)}\n"
return schema_text
def is_safe_sql(sql:str) -> bool:
sql = sql.strip().lower()
return sql.startswith("select") or sql.startswith("show") and not re.search(r"delete|update|insert|drop|create|alter", sql)
def execute_sql(conn, sql_query):
cur = conn.cursor()
cur.execute(sql_query)
try:
rows = cur.fetchall()
columns = [desc[0] for desc in cur.description]
result = [dict(zip(columns, row)) for row in rows]
except psycopg2.ProgrammingError:
result = cur.statusmessage
cur.close()
return result
def strip_sql_markdown(content: str) -> str:
# 去掉包裹在 ```sql 或 ``` 中的内容
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
else:
return None
# ORM 写入包装函数
def save_message_thread_safe(**kwargs):
def _save():
with transaction.atomic():
Message.objects.create(**kwargs)
threading.Thread(target=_save).start()

View File

@ -1,87 +0,0 @@
import requests
from langchain_core.language_models import LLM
from langchain_core.outputs import LLMResult, Generation
from langchain_experimental.sql import SQLDatabaseChain
from langchain_community.utilities import SQLDatabase
from server.conf import DATABASES
from apps.ichat.serializers import CustomLLMrequestSerializer
from rest_framework.views import APIView
from urllib.parse import quote_plus
from rest_framework.response import Response
db_conf = DATABASES['default']
# 密码需要 URL 编码(因为有特殊字符如 @
password_encodeed = quote_plus(db_conf['PASSWORD'])
db = SQLDatabase.from_uri(f"postgresql+psycopg2://{db_conf['USER']}:{password_encodeed}@{db_conf['HOST']}/{db_conf['NAME']}", include_tables=["enm_mpoint", "enm_mpointstat"])
# model_url = "http://14.22.88.72:11025/v1/chat/completions"
model_url = "http://139.159.180.64:11434/v1/chat/completions"
class CustomLLM(LLM):
model_url: str
mode: str = 'chat'
def _call(self, prompt: str, stop: list = None) -> str:
data = {
"model":"glm4",
"messages": self.build_message(prompt),
"stream": False,
}
response = requests.post(self.model_url, json=data, timeout=600)
response.raise_for_status()
content = response.json()["choices"][0]["message"]["content"]
print('content---', content)
clean_sql = self.strip_sql_markdown(content) if self.mode == 'sql' else content.strip()
return clean_sql
def _generate(self, prompts: list, stop: list = None) -> LLMResult:
generations = []
for prompt in prompts:
text = self._call(prompt, stop)
generations.append([Generation(text=text)])
return LLMResult(generations=generations)
def strip_sql_markdown(self, content: str) -> str:
import re
# 去掉包裹在 ```sql 或 ``` 中的内容
match = re.search(r"```sql\s*(.*?)```", content, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
else:
return content.strip()
def build_message(self, prompt: str) -> list:
if self.mode == 'sql':
system_prompt = (
"你是一个 SQL 助手,严格遵循以下规则:\n"
"1. 只返回 PostgreSQL 语法 SQL 语句。\n"
"2. 严格禁止添加任何解释、注释、Markdown 代码块标记(包括 ```sql 和 ```)。\n"
"3. 输出必须是纯 SQL且可直接执行无需任何额外处理。\n"
"4. 在 SQL 中如有多个表,请始终使用表名前缀引用字段,避免字段歧义。"
)
else:
system_prompt = "你是一个聊天助手,请根据用户的问题,提供简洁明了的答案。"
return [
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
]
@property
def _llm_type(self) -> str:
return "custom_llm"
class QueryLLMview(APIView):
def post(self, request):
serializer = CustomLLMrequestSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
prompt = serializer.validated_data['prompt']
mode = serializer.validated_data.get('mode', 'chat')
llm = CustomLLM(model_url=model_url, mode=mode)
print('prompt---', prompt, mode)
if mode == 'sql':
chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)
result = chain.invoke(prompt)
else:
result = llm._call(prompt)
return Response({"result": result})

View File

@ -1,155 +0,0 @@
import requests
import json
from rest_framework.views import APIView
from apps.ichat.serializers import MessageSerializer, ConversationSerializer
from rest_framework.response import Response
from apps.ichat.models import Conversation, Message
from apps.ichat.utils import connect_db, extract_sql_code, execute_sql, get_schema_text, is_safe_sql, save_message_thread_safe
from django.http import StreamingHttpResponse, JsonResponse
from rest_framework.decorators import action
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet
# API_KEY = "sk-5644e2d6077b46b9a04a8a2b12d6b693"
# API_BASE = "https://dashscope.aliyuncs.com/compatible-mode/v1"
# MODEL = "qwen-plus"
# #本地部署的模式
API_KEY = "JJVAide0hw3eaugGmxecyYYFw45FX2LfhnYJtC+W2rw"
API_BASE = "http://106.0.4.200:9000/v1"
MODEL = "qwen14b"
# google gemini
# API_KEY = "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"
# API_BASE = "https://openrouter.ai/api/v1"
# MODEL="google/gemini-2.0-flash-exp:free"
# deepseek v3
# API_KEY = "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"
# API_BASE = "https://openrouter.ai/api/v1"
# MODEL="deepseek/deepseek-chat-v3-0324:free"
TABLES = ["enm_mpoint", "enm_mpointstat", "enm_mplogx"] # 如果整个数据库全都给模型,准确率下降,所以只给模型部分表
class QueryLLMviewSet(CustomModelViewSet):
queryset = Message.objects.all()
serializer_class = MessageSerializer
ordering = ['create_time']
perms_map = {'get':'*', 'post':'*', 'put':'*'}
@action(methods=['post'], detail=False, perms_map={'post':'*'} ,serializer_class=MessageSerializer)
def completion(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
prompt = serializer.validated_data['content']
conversation = serializer.validated_data['conversation']
if not prompt or not conversation:
return JsonResponse({"error": "缺少 prompt 或 conversation"}, status=400)
save_message_thread_safe(content=prompt, conversation=conversation, role="user")
url = f"{API_BASE}/chat/completions"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}"
}
user_prompt = f"""
我提问的问题是:{prompt}请判断我的问题是否与数据库查询或操作相关如果是回答"database"如果不是回答"general"
注意
只需回答"database""general"即可不要有其他内容
"""
_payload = {
"model": MODEL,
"messages": [{"role": "user", "content": user_prompt}, {"role":"system" , "content": "只返回一个结果'database''general'"}],
"temperature": 0,
"max_tokens": 10
}
try:
class_response = requests.post(url, headers=headers, json=_payload)
class_response.raise_for_status()
class_result = class_response.json()
question_type = class_result.get('choices', [{}])[0].get('message', {}).get('content', '').strip().lower()
print("question_type", question_type)
if question_type == "database":
conn = connect_db()
schema_text = get_schema_text(conn, TABLES)
print("schema_text----------------------", schema_text)
user_prompt = f"""你是一个专业的数据库工程师,根据以下数据库结构:
{schema_text}
请根据我的需求生成一条标准的PostgreSQL SQL语句直接返回SQL不要额外解释
需求是{prompt}
"""
else:
user_prompt = f"""
回答以下问题不需要涉及数据库查询
问题: {prompt}
请直接回答问题不要提及数据库或SQL
"""
# TODO 是否应该拿到conservastion的id然后根据id去数据库查询所以的messages, 然后赋值给messages
history = Message.objects.filter(conversation=conversation).order_by('create_time')
# chat_history = [{"role": msg.role, "content": msg.content} for msg in history]
# chat_history.append({"role": "user", "content": prompt})
chat_history = [{"role":"user", "content":prompt}]
print("chat_history", chat_history)
payload = {
"model": MODEL,
"messages": chat_history,
"temperature": 0,
"stream": True
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()
except requests.exceptions.RequestException as e:
return JsonResponse({"error":f"LLM API调用失败: {e}"}, status=500)
def stream_generator():
accumulated_content = ""
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith('data:'):
if decoded_line.strip() == "data: [DONE]":
break # OpenAI-style标志结束
try:
data = json.loads(decoded_line[6:])
content = data.get('choices', [{}])[0].get('delta', {}).get('content', '')
if content:
accumulated_content += content
yield f"data: {content}\n\n"
except Exception as e:
yield f"data: [解析失败]: {str(e)}\n\n"
print("accumulated_content", accumulated_content)
save_message_thread_safe(content=accumulated_content, conversation=conversation, role="system")
if question_type == "database":
sql = extract_sql_code(accumulated_content)
if sql:
try:
conn = connect_db()
if is_safe_sql(sql):
result = execute_sql(conn, sql)
save_message_thread_safe(content=f"SQL结果: {result}", conversation=conversation, role="system")
yield f"data: SQL执行结果: {result}\n\n"
else:
yield f"data: 拒绝执行非查询类 SQL{sql}\n\n"
except Exception as e:
yield f"data: SQL执行失败: {str(e)}\n\n"
finally:
if conn:
conn.close()
else:
yield "data: \\n[文本结束]\n\n"
return StreamingHttpResponse(stream_generator(), content_type='text/event-stream')
# 先新建对话 生成对话session_id
class ConversationViewSet(CustomModelViewSet):
queryset = Conversation.objects.all()
serializer_class = ConversationSerializer
ordering = ['create_time']
perms_map = {'get':'*', 'post':'*', 'put':'*'}

View File

@ -1,129 +0,0 @@
import requests
import os
from apps.utils.sql import execute_raw_sql
import json
from apps.utils.tools import MyJSONEncoder
from .utils import is_safe_sql
from rest_framework.views import APIView
from drf_yasg.utils import swagger_auto_schema
from rest_framework import serializers
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from django.conf import settings
from apps.utils.mixins import MyLoggingMixin
from django.core.cache import cache
import uuid
from apps.utils.thread import MyThread
LLM_URL = getattr(settings, "LLM_URL", "")
API_KEY = getattr(settings, "LLM_API_KEY", "")
MODEL = "qwen14b"
HEADERS = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
CUR_DIR = os.path.dirname(os.path.abspath(__file__))
def load_promot(name):
with open(os.path.join(CUR_DIR, f'promot/{name}.md'), 'r') as f:
return f.read()
def ask(input:str, p_name:str, stream=False):
his = [{"role":"system", "content": load_promot(p_name)}]
his.append({"role":"user", "content": input})
payload = {
"model": MODEL,
"messages": his,
"temperature": 0,
"stream": stream
}
response = requests.post(LLM_URL, headers=HEADERS, json=payload, stream=stream)
if not stream:
return response.json()["choices"][0]["message"]["content"]
else:
# 处理流式响应
full_content = ""
for chunk in response.iter_lines():
if chunk:
# 通常流式响应是SSE格式data: {...}
decoded_chunk = chunk.decode('utf-8')
if decoded_chunk.startswith("data:"):
json_str = decoded_chunk[5:].strip()
if json_str == "[DONE]":
break
try:
chunk_data = json.loads(json_str)
if "choices" in chunk_data and chunk_data["choices"]:
delta = chunk_data["choices"][0].get("delta", {})
if "content" in delta:
print(delta["content"])
full_content += delta["content"]
except json.JSONDecodeError:
continue
return full_content
def work_chain(input:str, t_key:str):
pdict = {"state": "progress", "steps": [{"state":"ok", "msg":"正在生成查询语句"}]}
cache.set(t_key, pdict)
res_text = ask(input, 'w_sql')
if res_text == '请以 查询 开头,重新描述你的需求':
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":res_text})
cache.set(t_key, pdict)
return
else:
pdict["steps"].append({"state":"ok", "msg":"查询语句生成成功", "content":res_text})
cache.set(t_key, pdict)
if not is_safe_sql(res_text):
pdict["state"] = "error"
pdict["steps"].append({"state":"error", "msg":"当前查询存在风险,请重新描述你的需求"})
cache.set(t_key, pdict)
return
pdict["steps"].append({"state":"ok", "msg":"正在执行查询语句"})
cache.set(t_key, pdict)
res = execute_raw_sql(res_text)
pdict["steps"].append({"state":"ok", "msg":"查询语句执行成功", "content":res})
cache.set(t_key, pdict)
pdict["steps"].append({"state":"ok", "msg":"正在生成报告"})
cache.set(t_key, pdict)
res2 = ask(json.dumps(res, cls=MyJSONEncoder, ensure_ascii=False), 'w_ana')
content = res2.lstrip('```html ').rstrip('```')
pdict["state"] = "done"
pdict["content"] = content
pdict["steps"].append({"state":"ok", "msg":"报告生成成功", "content": content})
cache.set(t_key, pdict)
return
class InputSerializer(serializers.Serializer):
input = serializers.CharField(label="查询需求")
class WorkChain(MyLoggingMixin, APIView):
@swagger_auto_schema(
operation_summary="提交查询需求",
request_body=InputSerializer)
def post(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
input = request.data.get('input')
t_key = f'ichat_{uuid.uuid4()}'
MyThread(target=work_chain, args=(input, t_key)).start()
return Response({'ichat_tid': t_key})
@swagger_auto_schema(
operation_summary="获取查询进度")
def get(self, request):
llm_enabled = getattr(settings, "LLM_ENABLED", False)
if not llm_enabled:
raise ParseError('LLM功能未启用')
ichat_tid = request.GET.get('ichat_tid')
if ichat_tid:
return Response(cache.get(ichat_tid))
if __name__ == "__main__":
print(work_chain("查询 一次超洗 工段在2025年6月的生产合格数等并形成报告"))
from apps.ichat.views2 import work_chain
print(work_chain('查询外观检验工段在2025年6月的生产合格数等并形成报告'))

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

@ -13,8 +13,7 @@ class MaterialBatchFilter(filters.FilterSet):
"material__process": ["exact", "in"],
"count": ["exact", "gte", "lte"],
"state": ["exact", "in"],
"defect": ["exact"],
"batch": ["exact"]
"defect": ["exact"]
}
@ -35,11 +34,7 @@ class MioFilter(filters.FilterSet):
"order": ["exact"],
"item_mio__test_date": ["isnull"],
"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"]
}
def filter_materials__type(self, queryset, name, value):

View File

@ -1,33 +0,0 @@
# Generated by Django 3.2.12 on 2025-05-23 01:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inm', '0029_alter_mioitem_batch'),
]
operations = [
migrations.AlterField(
model_name='materialbatch',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='materialbatcha',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='mioitem',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
migrations.AlterField(
model_name='mioitema',
name='batch',
field=models.TextField(db_index=True, verbose_name='批次号'),
),
]

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,13 +5,13 @@ 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.
class WareHouse(CommonBModel):
"""
TN:仓库信息
仓库信息
"""
number = models.CharField('仓库编号', max_length=20)
name = models.CharField('仓库名称', max_length=20)
@ -20,9 +20,9 @@ class WareHouse(CommonBModel):
class MaterialBatch(BaseModel):
"""
TN:物料批次
物料批次
"""
batch = models.TextField('批次号', db_index=True)
batch = models.TextField('批次号')
state = models.PositiveSmallIntegerField('状态', default=10, choices=((10, '合格'), (20, '不合格'), (30, '返修'), (40, '检验'), (50, '报废')))
material = models.ForeignKey(
Material, on_delete=models.CASCADE, verbose_name='物料')
@ -39,15 +39,11 @@ 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:组合件物料批次
组合件物料批次
"""
batch = models.TextField('批次号', db_index=True)
batch = models.CharField('批次号', max_length=100)
material = models.ForeignKey(
Material, on_delete=models.CASCADE, verbose_name='物料')
rate = models.PositiveIntegerField('比例', default=1)
@ -56,39 +52,30 @@ 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):
"""
TN:出入库记录
出入库记录
"""
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,16 +124,9 @@ 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:出入库明细
出入库明细
"""
mio = models.ForeignKey(MIO, verbose_name='关联出入库',
on_delete=models.CASCADE, related_name='item_mio')
@ -158,8 +138,7 @@ class MIOItem(BaseModel):
WareHouse, on_delete=models.CASCADE, verbose_name='仓库')
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)
batch = models.TextField('批次号')
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):
@ -208,11 +182,11 @@ class MIOItem(BaseModel):
class MIOItemA(BaseModel):
"""
TN:组合件出入库明细
组合件出入库明细
"""
material = models.ForeignKey(
Material, verbose_name='物料', on_delete=models.CASCADE)
batch = models.TextField('批次号', db_index=True)
batch = models.CharField('批次号', max_length=50)
rate = models.PositiveIntegerField('比例', default=1)
mioitem = models.ForeignKey(
MIOItem, verbose_name='关联出入库明细', on_delete=models.CASCADE, related_name='a_mioitem')
@ -224,10 +198,9 @@ class MIOItemA(BaseModel):
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,19 +109,17 @@ 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', [])
if mio.type == MIO.MIO_TYPE_DO_IN and not assemb:
if mio.type == MIO.MIO_TYPE_DO_IN:
if not wm:
raise ParseError('生产入库必须指定车间库存')
elif mio.type == MIO.MIO_TYPE_DO_OUT:
@ -153,14 +134,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 +147,12 @@ 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"]
assemb = validated_data.pop('assemb', [])
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":
if item.get("wpr", None) is None and mio_type != "pur_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

@ -1,38 +1,31 @@
from apps.inm.models import (MIO, MIOItem,
MaterialBatch, MaterialBatchA,
MIOItemA, MIOItemw)
MIOItemA, WareHouse, MIOItemw)
from rest_framework.exceptions import ParseError
from apps.mtm.models import Material
from apps.mtm.models import Material, Process
from apps.utils.tools import ranstr
from apps.utils.thread import MyThread
from apps.mtm.services_2 import cal_material_count
from apps.wpm.models import WMaterial, BatchSt, BatchLog
from apps.wpm.services_2 import ana_batch_thread
from apps.wpm.models import WMaterial
from apps.wpm.services_2 import get_alldata_with_batch_and_store
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):
"""
生产领料到车间
"""
if item.mb and item.mb.defect is not None:
raise ParseError("生产领料不支持不合格品")
from apps.inm.models import MaterialBatch
mio:MIO = item.mio
belong_dept = mio.belong_dept
mgroup = mio.mgroup
do_user = mio.do_user
material:Material = item.material
# 获取defect
defect:Defect = None
if item.wm and item.mb:
raise ParseError("车间和仓库库存不能同时存在")
if item.wm:
defect = item.wm.defect
elif item.mb:
defect = item.mb.defect
if material.into_wm is False: # 用于混料的原料不与车间库存交互, 这个是配置项目
return
action_list = []
mias = MIOItemA.objects.filter(mioitem=item)
is_zhj = False # 是否组合件领料
@ -42,18 +35,16 @@ def do_out(item: MIOItem, is_reverse: bool = False):
for i in range(len(mias_list)):
material, batch, rate = mias_list[i]
new_count = rate * item.count # 假设 item.count 存在
action_list.append([material, batch, new_count, None])
action_list.append([material, batch, new_count])
else:
action_list = [[item.material, item.batch, item.count, defect]]
action_list = [[item.material, item.batch, item.count]]
if is_zhj:
try:
mb = MaterialBatch.objects.get(
material=item.material,
warehouse=item.warehouse,
batch=item.batch,
state=WMaterial.WM_OK,
defect=None
batch=item.batch
)
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
raise ParseError(f"组合件批次错误!{e}")
@ -73,11 +64,7 @@ def do_out(item: MIOItem, is_reverse: bool = False):
xmaterial:Material = al[0]
xbatch:str = al[1]
xcount:str = al[2]
defect:Defect = al[3]
xbatches.append(xbatch)
if xcount <= 0:
raise ParseError("存在非正数!")
mb = None
if not is_zhj:
try:
@ -85,23 +72,22 @@ def do_out(item: MIOItem, is_reverse: bool = False):
material=xmaterial,
warehouse=item.warehouse,
batch=xbatch,
state=WMaterial.WM_OK,
defect=defect
state=10,
defect=None
)
except (MaterialBatch.DoesNotExist, MaterialBatch.MultipleObjectsReturned) as e:
raise ParseError(f"批次错误!{e}")
mb.count = mb.count - xcount
if mb.count < 0:
raise ParseError(f"{mb.batch}-{str(mb.material)}-批次库存不足,操作失败")
raise ParseError("批次库存不足,操作失败")
else:
mb.save()
if material.into_wm:
# 领到车间库存(或工段)
wm, new_create = WMaterial.objects.get_or_create(
batch=xbatch, material=xmaterial,
wm, new_create = WMaterial.objects.get_or_create(batch=xbatch, material=xmaterial,
belong_dept=belong_dept, mgroup=mgroup,
state=WMaterial.WM_OK, defect=defect)
state=WMaterial.WM_OK)
if new_create:
wm.create_by = do_user
wm.batch_ofrom = mb.batch if mb else None
@ -112,21 +98,17 @@ 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)
# 触发批次统计分析
ana_batch_thread(xbatches)
xbatches = list(set(xbatches))
if xbatches:
for xbatch in xbatches:
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
def do_in(item: MIOItem):
@ -134,36 +116,26 @@ def do_in(item: MIOItem):
生产入库后更新车间物料
"""
mio = item.mio
wmin:WMaterial = item.wm
if wmin and wmin.state != WMaterial.WM_OK:
raise ParseError("非合格物料无法入库")
if item.wm and item.wm.defect is not None:
raise ParseError("不合格物料无法入库")
belong_dept = mio.belong_dept
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 # 是否组合件入仓库
# 获取defect
defect:Defect = None
if item.wm and item.mb:
raise ParseError("车间和仓库库存不能同时存在")
if item.wm:
defect = item.wm.defect
elif item.mb:
defect = item.mb.defect
if mias.exists():
is_zhj = True
mias_list = mias.values_list('material', 'batch', 'rate')
for i in mias_list:
material, batch, rate = i
new_count = rate * item.count # 假设 item.count 存在
action_list.append([material, batch, new_count, None])
action_list.append([material, batch, new_count])
else:
action_list = [[item.material, item.batch, item.count, defect]]
action_list = [[item.material, item.batch, item.count]]
production_dept = None
@ -171,18 +143,14 @@ def do_in(item: MIOItem):
if is_zhj:
xbatchs = [item.batch]
for al in action_list:
xmaterial, xbatch, xcount, defect = al
if xcount <= 0:
raise ParseError("存在非正数!")
xmaterial, xbatch, xcount = al
xbatchs.append(xbatch)
if material.into_wm:
# 扣减车间库存
wm_qs = WMaterial.objects.filter(
batch=xbatch,
material=xmaterial,
belong_dept=belong_dept,
mgroup=mgroup,
defect=defect,
state=WMaterial.WM_OK)
count_x = wm_qs.count()
if count_x == 1:
@ -193,8 +161,6 @@ def do_in(item: MIOItem):
else:
raise ParseError(
f'{str(xmaterial)}-{xbatch}-存在多个相同批次!')
# 扣减车间库存
new_count = wm.count - xcount
if new_count >= 0:
wm.count = new_count
@ -208,15 +174,14 @@ 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(
material=xmaterial,
warehouse=item.warehouse,
batch=xbatch,
state=WMaterial.WM_OK,
defect=defect,
state=10,
defect=None,
defaults={
"count": 0,
"batch_ofrom": wm.batch_ofrom,
@ -231,16 +196,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)
@ -250,8 +208,6 @@ def do_in(item: MIOItem):
material=item.material,
warehouse=item.warehouse,
batch=item.batch,
defect=None,
state=WMaterial.WM_OK,
defaults={"count": 0, "production_dept": production_dept}
)
if not is_created:
@ -264,7 +220,9 @@ def do_in(item: MIOItem):
MaterialBatchA.objects.create(mb=mb, material=mia.material, batch=mia.batch, rate=mia.rate)
# 批次统计分析
ana_batch_thread(xbatchs)
xbatchs = list(set(xbatchs))
for xbatch in xbatchs:
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
class InmService:
@ -283,42 +241,18 @@ class InmService:
"""
更新库存, 支持反向操作
"""
if not MIOItem.objects.filter(mio=instance).exists():
raise ParseError("出入库记录缺失明细,无法操作")
in_or_out = 1
if is_reverse:
in_or_out = -1
if instance.type == MIO.MIO_TYPE_PUR_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)
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)
cls.update_mb(instance, in_or_out)
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)
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]:
cls.update_mb(instance, in_or_out)
elif instance.type == MIO.MIO_TYPE_DO_IN:
mioitems = MIOItem.objects.filter(mio=instance)
if is_reverse:
for item in mioitems:
@ -326,14 +260,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 +272,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 +305,6 @@ class InmService:
out = -1
默认使用count字段做加减
"""
mio_type = i.mio.type
material: Material = i.material
warehouse = i.warehouse
tracking = material.tracking
@ -407,10 +340,10 @@ class InmService:
xbatchs = []
for material, warehouse, batch, change_count, defect, mioitem in m_list:
xbatchs.append(batch)
if change_count < 0:
raise ParseError("存在负数!")
if change_count <= 0:
continue
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 +375,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("批次库存不足,操作失败")
else:
mb.save()
if tracking == Material.MA_TRACKING_SINGLE:
@ -453,34 +386,69 @@ 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("不支持的操作")
# 批次统计分析
ana_batch_thread(xbatchs)
xbatchs = list(set(xbatchs))
for xbatch in xbatchs:
MyThread(target=get_alldata_with_batch_and_store, args=(xbatch,)).start()
def daoru_mb(path: str):
"""
导入物料批次(如没有物料自动创建)
"""
# 注释的是初次导入时做的数据矫正
# objs1 = Material.objects.filter(specification__contains=' ')
# for i in objs1:
# i.specification = i.specification.replace(' ', '')
# i.save()
# objs2 = Material.objects.filter(specification__contains='×')
# for i in objs2:
# i.specification = i.specification.replace('×', '*')
# i.save()
# objs3 = (Material.objects.filter(
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
# for i in objs3:
# i.specification = i.specification.replace(
# '优级', '|优级').replace('一级', '|一级')
# i.save()
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
@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("不支持该出入库单明细撤销")
wb = load_workbook(path)
process_l = Process.objects.all()
process_d = {p.name: p for p in process_l}
warehouse_l = WareHouse.objects.all()
warehouse_d = {w.name: w for w in warehouse_l}
for sheet in wb.worksheets:
i = 3
while sheet[f"a{i}"].value:
try:
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
name = sheet[f"b{i}"].value.replace(" ", "")
specification = sheet[f"c{i}"].value.replace(" ", "")
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
model = sheet[f"e{i}"].value.replace(" ", "")
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
batch = sheet[f"g{i}"].value.replace(" ", "")
count = int(sheet[f"h{i}"].value)
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
except KeyError as e:
raise ParseError(f"{i}行数据有误:{str(e)}")
material, _ = Material.objects.get_or_create(
type=type,
name=name,
specification=specification,
model=model,
process=process,
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
)
MaterialBatch.objects.get_or_create(
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
)
cal_material_count([material.id])
i = i + 1

View File

@ -1,161 +0,0 @@
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.utils.tools import ranstr
from apps.mtm.services_2 import cal_material_count
def daoru_mb(path: str):
"""
导入物料批次(如没有物料自动创建)
"""
# 注释的是初次导入时做的数据矫正
# objs1 = Material.objects.filter(specification__contains=' ')
# for i in objs1:
# i.specification = i.specification.replace(' ', '')
# i.save()
# objs2 = Material.objects.filter(specification__contains='×')
# for i in objs2:
# i.specification = i.specification.replace('×', '*')
# i.save()
# objs3 = (Material.objects.filter(
# specification__contains='优级') | Material.objects.filter(specification__contains='一级')).exclude(specification__contains='|')
# for i in objs3:
# i.specification = i.specification.replace(
# '优级', '|优级').replace('一级', '|一级')
# i.save()
type_dict = {"主要原料": 30, "半成品": 20, "成品": 10, "辅助材料": 40, "加工工具": 50, "辅助工装": 60, "办公用品": 70}
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
wb = load_workbook(path)
process_l = Process.objects.all()
process_d = {p.name: p for p in process_l}
warehouse_l = WareHouse.objects.all()
warehouse_d = {w.name: w for w in warehouse_l}
for sheet in wb.worksheets:
i = 3
while sheet[f"a{i}"].value:
try:
type = type_dict[sheet[f"a{i}"].value.replace(" ", "")]
name = sheet[f"b{i}"].value.replace(" ", "")
specification = sheet[f"c{i}"].value.replace(" ", "")
if sheet[f"d{i}"].value and sheet[f"d{i}"].value.replace(" ", ""):
specification = specification + "|" + sheet[f"d{i}"].value.replace(" ", "")
model = sheet[f"e{i}"].value.replace(" ", "")
process = process_d[sheet[f"f{i}"].value.replace(" ", "")]
batch = sheet[f"g{i}"].value.replace(" ", "")
count = int(sheet[f"h{i}"].value)
warehouse = warehouse_d[sheet[f"i{i}"].value.replace(" ", "")]
except KeyError as e:
raise ParseError(f"{i}行数据有误:{str(e)}")
material, _ = Material.objects.get_or_create(
type=type,
name=name,
specification=specification,
model=model,
process=process,
defaults={"type": type, "name": name, "specification": specification, "model": model, "process": process, "number": ranstr(6), "id": idWorker.get_id()},
)
MaterialBatch.objects.get_or_create(
material=material, batch=batch, warehouse=warehouse, defaults={"material": material, "batch": batch, "warehouse": warehouse, "count": count, "id": idWorker.get_id()}
)
cal_material_count([material.id])
i = i + 1
def daoru_mioitem_test(path:str, mioitem:MIOItem):
from apps.utils.snowflake import idWorker
from openpyxl import load_workbook
from apps.qm.models import TestItem, Ftest, Qct, FtestItem, FtestDefect
qct = Qct.get(mioitem.material, tag="inm", type="in")
if qct is None:
raise ParseError("未找到检验表")
t_name_list = ["配套序号", "棒编号", "棒最大外径/mm", "锥度/mm", "管编号", "管最大内径/mm", "配合间隙"]
t_list = []
for name in t_name_list:
try:
t_list.append(TestItem.objects.get(name=name))
except TestItem.DoesNotExist:
raise ParseError(f"未找到检验项:{name}")
except TestItem.MultipleObjectsReturned:
raise ParseError(f"检验项重复:{name}")
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")
mioitemws = MIOItemw.objects.filter(mioitem=mioitem).order_by("number")
for ind, item in enumerate(mioitemws):
ftest:Ftest = item.ftest
if ftest is None:
ftest = Ftest.objects.create(
type="purin",
test_numer=item.number,
qct=qct,
test_user=test_user,
is_ok=True,
test_date=test_date)
item.ftest = ftest
item.save()
else:
FtestItem.objects.filter(ftest=ftest).delete()
FtestDefect.objects.filter(ftest=ftest).delete()
ftest.is_ok = True
ftest.defect_main = None
ftest.save()
i = ind + 4
if sheet[f"c{i}"].value:
ftestitems = []
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[0], test_val_json=sheet[f"b{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[1], test_val_json=sheet[f"c{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[2], test_val_json=sheet[f"e{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[3], test_val_json=sheet[f"f{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[4], test_val_json=sheet[f"g{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[5], test_val_json=sheet[f"j{i}"].value, test_user=test_user, id=idWorker.get_id()))
ftestitems.append(FtestItem(ftest=ftest, testitem=t_list[6], test_val_json=sheet[f"k{i}"].value, test_user=test_user, id=idWorker.get_id()))
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,21 @@ 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 import InmService, daoru_mb
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 +144,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 +167,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 +179,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 +191,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 +223,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 +242,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 +251,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):
@ -472,22 +351,12 @@ class MIOItemViewSet(CustomListModelMixin, BulkCreateModelMixin, BulkDestroyMode
res[i] = 0
return Response(res)
@action(methods=['post'], detail=True, perms_map={'post': 'mio.update'}, serializer_class=serializers.Serializer)
@transaction.atomic
def test_daoru_bg(self, request, *args, **kwargs):
"""导入棒管检验
导入棒管检验
"""
daoru_mioitem_test(path=settings.BASE_DIR + request.data.get('path', ''), mioitem=self.get_object())
return Response()
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 +372,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

@ -5,7 +5,6 @@ from apps.utils.models import BaseModel
class AuditLog(models.Model):
"""TN:审计表"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
action = models.CharField('动作', max_length=20)
model_name = models.CharField('模型名', max_length=20)
@ -19,7 +18,7 @@ class AuditLog(models.Model):
class DrfRequestLog(BaseModel):
"""TN:Logs Django rest framework API requests"""
"""Logs Django rest framework API requests"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
user = models.ForeignKey(

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,19 +0,0 @@
# Generated by Django 3.2.12 on 2025-03-21 08:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mtm', '0053_mgroup_mtype'),
]
operations = [
migrations.AddField(
model_name='process',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='父工序'),
),
]

View File

@ -1,24 +0,0 @@
# Generated by Django 3.2.12 on 2025-03-27 04:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mtm', '0054_process_parent'),
]
operations = [
migrations.AddField(
model_name='routepack',
name='gjson',
field=models.JSONField(blank=True, default=dict, verbose_name='子图结构'),
),
migrations.AlterField(
model_name='routepack',
name='material',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='产品'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-04-21 02:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0055_auto_20250327_1239'),
]
operations = [
migrations.AddField(
model_name='mgroup',
name='batch_append_code',
field=models.BooleanField(default=False, verbose_name='批号追加工段标识'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-04-28 06:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0056_mgroup_batch_append_code'),
]
operations = [
migrations.AddField(
model_name='process',
name='number_to_batch',
field=models.BooleanField(default=False, verbose_name='个号转批号'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.12 on 2025-05-16 07:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0057_process_number_to_batch'),
]
operations = [
migrations.AddField(
model_name='process',
name='wpr_number_rule',
field=models.TextField(blank=True, null=True, verbose_name='单个编号规则'),
),
]

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

@ -2,14 +2,10 @@ from django.db import models
from apps.system.models import CommonAModel, Dictionary, CommonBModel, CommonADModel, File, BaseModel
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):
"""
TN:工序
工序
"""
PRO_PROD = 10
PRO_TEST = 20
@ -18,7 +14,6 @@ class Process(CommonBModel):
PRO_NORMAL = 10
PRO_DIV = 20
PRO_MERGE = 30
name = models.CharField('工序名称', max_length=100)
type = models.PositiveSmallIntegerField("工序类型", default=PRO_PROD, choices=((PRO_PROD, '生产工序'), (PRO_TEST, '检验工序')))
mtype = models.PositiveSmallIntegerField("工序生产类型", default=PRO_NORMAL, choices=((PRO_NORMAL, '常规'), (PRO_DIV, '切分'), (PRO_MERGE, '合并')))
@ -32,28 +27,14 @@ class Process(CommonBModel):
batch_append_equip = models.BooleanField('批号追加设备', default=False)
mlog_need_ticket = models.BooleanField('日志提交是否需要审批', default=False)
mstate_json = models.JSONField('中间状态', default=list, blank=True)
parent = models.ForeignKey('self', verbose_name='父工序', on_delete=models.CASCADE, null=True, blank=True)
number_to_batch = models.BooleanField('个号转批号', default=False)
wpr_number_rule = models.TextField("单个编号规则", null=True, blank=True)
class Meta:
verbose_name = '工序'
ordering = ['sort', 'create_time']
def get_canout_mat_ids(self):
"""获取可产出的materialIds
"""
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:物料"""
MA_TYPE_BASE = 0
MA_TYPE_GOOD = 10
MA_TYPE_HALFGOOD = 20
@ -109,8 +90,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 = '物料表'
@ -121,7 +100,7 @@ class Material(CommonAModel):
class Shift(CommonBModel):
"""TN:班次
"""班次
"""
name = models.CharField('名称', max_length=50)
rule = models.CharField('所属规则', max_length=10, default='默认')
@ -134,13 +113,13 @@ class Shift(CommonBModel):
class Srule(CommonBDModel):
"""
TN:班组规则
班组规则
"""
rule = models.JSONField('排班规则', default=list, blank=True)
class Team(CommonBModel):
"""TN:班组, belong_dept为所属车间
"""班组, belong_dept为所属车间
"""
name = models.CharField('名称', max_length=50)
leader = models.ForeignKey(
@ -148,7 +127,7 @@ class Team(CommonBModel):
class Mgroup(CommonBModel):
"""TN:测点集
"""测点集
"""
M_SELF = 10
M_OTHER = 20
@ -168,7 +147,6 @@ class Mgroup(CommonBModel):
'直接材料', default=list, blank=True, help_text='material的ID列表')
test_materials = models.JSONField(
'检测材料', default=list, blank=True, help_text='material的ID列表')
batch_append_code = models.BooleanField('批号追加工段标识', default=False)
sort = models.PositiveSmallIntegerField('排序', default=1)
need_enm = models.BooleanField('是否进行能源监测', default=True)
is_running = models.BooleanField('是否正常运行', default=False)
@ -180,32 +158,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='关联班组',
@ -219,7 +171,7 @@ class TeamMember(BaseModel):
class Goal(CommonADModel):
"""TN:目标
"""目标
"""
mgroup = models.ForeignKey(
Mgroup, verbose_name='关联工段', on_delete=models.CASCADE, null=True, blank=True)
@ -246,7 +198,7 @@ class Goal(CommonADModel):
class RoutePack(CommonADModel):
"""
TN:加工工艺
加工工艺
"""
RP_S_CREATE = 10
RP_S_AUDIT = 20
@ -259,143 +211,14 @@ class RoutePack(CommonADModel):
state = models.PositiveSmallIntegerField('状态', default=10, choices=RP_STATE)
name = models.CharField('名称', max_length=100)
material = models.ForeignKey(
Material, verbose_name='产品', on_delete=models.CASCADE, null=True, blank=True)
Material, verbose_name='产品', on_delete=models.CASCADE)
ticket = models.ForeignKey('wf.ticket', verbose_name='关联工单',
on_delete=models.SET_NULL, related_name='routepack_ticket', null=True, blank=True, db_constraint=False)
document = models.ForeignKey("system.file", verbose_name='工艺文件', on_delete=models.SET_NULL, null=True, blank=True, db_constraint=False)
gjson = models.JSONField('子图结构', default=dict, blank=True)
def validate_and_return_gjson(self, raise_exception=True):
"""
校验所有子图返回结构化数据
{
final_material_out_id1: {
"routes": [1, 2, ...], # 子图关联的Route ID
"meta": {
"is_valid": True,
"errors": [],
"levels": {material_id: 层级深度}
}
},
final_material_out_id2: { ... }
}
"""
all_routes = Route.objects.filter(routepack=self).order_by('sort', 'process__sort', 'create_time')
result = {}
# 1. 构建正向和反向图material_id作为节点
forward_graph = defaultdict(list) # out_id -> [in_ids]
reverse_graph = defaultdict(list) # in_id -> [out_ids]
route_edges = defaultdict(list) # (in_id, out_id) -> [Route]
for route in all_routes:
if not (route.material_in and route.material_out):
raise ParseError(f"Route(ID:{route.id}) 缺失material_in或material_out")
in_id, out_id = route.material_in.id, route.material_out.id
forward_graph[out_id].append(in_id)
reverse_graph[in_id].append(out_id)
route_edges[(in_id, out_id)].append(route.id)
# 2. 识别所有最终节点未被任何in_id引用的out_id
all_out_ids = {r.material_out.id for r in all_routes}
all_in_ids = {r.material_in.id for r in all_routes}
final_ids = all_out_ids - all_in_ids
if not final_ids:
raise ParseError("未找到最终产品节点")
# 3. 为每个最终产品构建子图
for final_id in final_ids:
# BFS逆向遍历所有上游节点
visited = set()
queue = deque([final_id])
while queue:
current_out_id = queue.popleft()
if current_out_id in visited:
continue
visited.add(current_out_id)
# 将当前节点的所有上游in_id加入队列
for in_id in forward_graph.get(current_out_id, []):
if in_id not in visited:
queue.append(in_id)
# 收集所有关联Route只要out_id在子图内
subgraph_route_ids = []
for route in all_routes:
if route.material_out.id in visited:
subgraph_route_ids.append(route.id)
result[final_id] = {"routes": subgraph_route_ids}
return result
def get_gjson(self, need_update=False, final_material_id=None):
if not self.gjson or need_update:
self.gjson = self.validate_and_return_gjson()
self.save()
return self.gjson.get(final_material_id, {}) if final_material_id else self.gjson
def get_dags(self):
gjson = self.get_gjson()
materialIdList = []
routeIdList = []
for final_material_id, data in gjson.items():
materialIdList.append(final_material_id)
routeIdList.extend(data['routes'])
# 获取所有相关路由并确保关联了material_in和material_out
route_qs = Route.objects.filter(id__in=routeIdList).select_related("material_in", "material_out", "process")
# 收集所有相关物料ID并过滤掉None值
matids1 = [mid for mid in route_qs.values_list("material_in__id", flat=True) if mid is not None]
matids2 = [mid for mid in route_qs.values_list("material_out__id", flat=True) if mid is not None]
materialIdList.extend(matids1)
materialIdList.extend(matids2)
# 构建路由字典,添加空值检查
route_dict = {}
for r in route_qs:
if r.material_in and r.material_out: # 只有两个物料都存在时才添加
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
}
# 获取所有物料信息
mat_qs = Material.objects.filter(id__in=materialIdList).order_by("process__sort", "create_time")
mat_dict = {mat.id: {"id": mat.id, "label": str(mat), "shape": "rect"} for mat in mat_qs}
res = {}
for final_material_id, data in gjson.items():
# 确保最终物料存在于mat_dict中
if final_material_id not in mat_dict:
continue
item = {"name": mat_dict[final_material_id]["label"]}
edges = []
nodes_set = set()
for route_id in data['routes']:
# 只处理存在于route_dict中的路由
if route_id in route_dict:
edges.append(route_dict[route_id])
nodes_set.update([route_dict[route_id]['source'], route_dict[route_id]['target']])
item['edges'] = edges
# 只添加存在于mat_dict中的节点
item['nodes'] = [mat_dict[node_id] for node_id in nodes_set if node_id in mat_dict]
res[final_material_id] = item
return res
def get_final_material_ids(self):
return list(self.get_gjson().keys())
class Route(CommonADModel):
"""
TN:加工路线
加工路线
"""
routepack = models.ForeignKey(RoutePack, verbose_name='关联路线包', on_delete=models.CASCADE, null=True, blank=True)
material = models.ForeignKey(
@ -417,152 +240,25 @@ 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")
def __str__(self):
x = ""
if self.material_in:
x = x + str(self.material_in) + "->"
if self.material_out:
x = x + str(self.material_out)
return x
parent = models.ForeignKey('self', verbose_name='上级路线', on_delete=models.CASCADE, null=True, blank=True)
@staticmethod
def get_routes(material: Material=None, routepack:RoutePack=None, routeIds=None):
def get_routes(material: Material):
"""
TN:返回工艺路线带车间(不关联工艺包)
返回工艺路线带车间(不关联工艺包)
"""
if material:
kwargs = {'material': material, 'routepack__isnull': True}
rqs = Route.objects.filter(
# 校验工艺路线是否正常
rq = Route.objects.filter(
**kwargs).order_by('sort', 'process__sort', 'create_time')
# if not rq.exists():
# raise ParseError('未配置工艺路线')
# if rq.first().material_in is None or rq.last().material_out is None or rq.last().material_out != rq.last().material:
# raise ParseError('首步缺少输入/最后一步缺少输出')
# if not rq.filter(is_count_utask=True).exists():
# raise ParseError('未指定统计步骤')
elif routepack:
rqs = Route.objects.filter(routepack=routepack).order_by('sort', 'process__sort', 'create_time')
elif routeIds:
rqs = Route.objects.filter(id__in=routeIds).order_by('sort', 'process__sort', 'create_time')
return rqs
if not rq.exists():
raise ParseError('未配置工艺路线')
if rq.first().material_in is None or rq.last().material_out is None or rq.last().material_out != rq.last().material:
raise ParseError('首步缺少输入/最后一步缺少输出')
if not rq.filter(is_count_utask=True).exists():
raise ParseError('未指定统计步骤')
return rq
@classmethod
def validate_dag(cls, final_material_out:Material, rqs):
"""
TN:校验工艺路线是否正确
- 所有 Route 必须有 material_in material_out
- 所有路径最终指向给定的 final_material_out
- 无循环依赖
- 所有物料必须能到达 final_material_out
"""
# 1. 检查字段完整性
invalid_routes = rqs.filter(
models.Q(material_in__isnull=True) |
models.Q(material_out__isnull=True)
)
if invalid_routes.exists():
raise ParseError("存在 Route 记录缺失 material_in 或 material_out")
# 2. 构建依赖图使用material_id作为键
graph = defaultdict(list) # {material_out_id: [material_in_id]}
reverse_graph = defaultdict(list) # {material_in_id: [material_out_id]}
all_material_ids = set()
for route in rqs.all():
out_id = route.material_out.id
in_id = route.material_in.id
graph[out_id].append(in_id)
reverse_graph[in_id].append(out_id)
all_material_ids.update({in_id, out_id})
# 3. 检查final_material_out是否是终点
final_id = final_material_out.id
if final_id in reverse_graph:
# raise ParseError(
# f"最终物料 {final_material_out.name}(ID:{final_id}) 不能作为任何Route的输入"
# )
raise ParseError("最终物料不可作为输入")
# 4. BFS检查路径可达性使用material_id操作
visited = set()
queue = deque([final_id])
while queue:
current_id = queue.popleft()
if current_id in visited:
continue
visited.add(current_id)
for in_id in graph.get(current_id, []):
queue.append(in_id)
# 5. 检查未到达的物料
unreachable_ids = all_material_ids - visited
if unreachable_ids:
# unreachable_materials = Material.objects.filter(id__in=unreachable_ids).values_list('name', flat=True)
# raise ParseError(
# f"以下物料无法到达最终物料: {list(unreachable_materials)}"
# )
raise ParseError("存在无法到达的节点")
# 6. DFS检查循环依赖
visited_cycle = set()
path_cycle = set()
def has_cycle(material_id):
if material_id in path_cycle:
return True
if material_id in visited_cycle:
return False
visited_cycle.add(material_id)
path_cycle.add(material_id)
for out_id in reverse_graph.get(material_id, []):
if has_cycle(out_id):
return True
path_cycle.remove(material_id)
return False
for material_id in all_material_ids:
if has_cycle(material_id):
# cycle_material = Material.objects.get(id=material_id)
# raise ParseError(f"循环依赖涉及物料: {cycle_material.name}(ID:{material_id})")
raise ParseError('存在循环依赖')
return True
@classmethod
def get_dag(cls, rqs):
# 收集所有相关批次和边
nodes_set = set()
edges = []
for rq in rqs:
if rq.material_in and rq.material_out:
source = rq.material_in.id
target = rq.material_out.id
nodes_set.update([source, target])
edges.append({
'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")
# 构建节点数据,默认使用'rect'形状
nodes = [{
'id': item.id,
'label': str(item),
'shape': 'rect' # 可根据业务需求调整形状
} for item in nodes_qs]
return {'nodes': nodes, 'edges': edges}
class RouteMat(BaseModel):
"""TN:工艺路线辅助物料"""
route = models.ForeignKey(Route, verbose_name='关联路线', on_delete=models.CASCADE, related_name="routemat_route")
material = models.ForeignKey(Material, verbose_name='辅助物料', on_delete=models.CASCADE)

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 ""}'
@ -149,14 +149,8 @@ class RoutePackSerializer(CustomModelSerializer):
# raise ParseError('不可变更产品')
# return super().update(instance, validated_data)
class RoutePackCopySerializer(serializers.Serializer):
routepack = serializers.CharField(label='工艺包ID')
new_name = serializers.CharField(label='新名称')
material_in = serializers.CharField(label='原料ID')
material_out = serializers.CharField(label='产品ID')
class RouteSerializer(CustomModelSerializer):
name = serializers.CharField(source='__str__', read_only=True)
material_ = MaterialSerializer(source='material', read_only=True)
routepack_name = serializers.StringRelatedField(source='routepack.name', read_only=True)
process_name = serializers.CharField(source='process.name', read_only=True)
@ -169,7 +163,7 @@ class RouteSerializer(CustomModelSerializer):
source='material_out.type', read_only=True)
material_out_is_hidden = serializers.BooleanField(
source='material_out.is_hidden', read_only=True)
material_out_tracking = serializers.IntegerField(write_only=True, required=False, allow_null=True)
material_out_tracking = serializers.IntegerField(source='material_out.tracking', required=False)
class Meta:
model = Route
@ -184,14 +178,11 @@ class RouteSerializer(CustomModelSerializer):
process: Process = attrs.get('process', None)
if process is None:
raise ParseError('未提供操作工序')
if process.parent is not None:
raise ParseError('操作工序不可为子工序')
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) < 1:
raise ParseError('切分数量必须大于等于1')
if process.mtype == Process.PRO_DIV and attrs.get('div_number', 1) <= 1:
raise ParseError('切分数量必须大于1')
return super().validate(attrs)
@classmethod
def gen_material_out(cls, instance: Route, material_out_tracking:int, user=None):
def gen_material_out(self, instance: Route, material_out_tracking:int):
"""
自动形成物料
"""
@ -207,17 +198,20 @@ class RouteSerializer(CustomModelSerializer):
material_out.cate = material.cate
material_out.tracking = material_out_tracking
material_out.save()
return material_out
instance.material_out = material_out
instance.save()
return
material_out = Material.objects.get_queryset(all=True).filter(name=material.name, model=material.model, process=process, specification=material.specification).first()
if material_out:
material_out.is_deleted = False
if material_out.parent is None:
if material_out.id != material.id:
material_out.parent = material
material_out.cate = material.cate
material_out.tracking = material_out_tracking
material_out.save()
return material_out
instance.material_out = material_out
instance.save()
return
material_out = Material.objects.create(**{'parent': instance.material, 'process': instance.process,
'is_hidden': True, 'name': material.name,
'number': material.number,
@ -226,17 +220,16 @@ class RouteSerializer(CustomModelSerializer):
'type': Material.MA_TYPE_HALFGOOD,
'cate': material.cate,
'tracking': material_out_tracking,
'create_by': user,
'update_by': user,
'create_by': self.request.user,
'update_by': self.request.user,
})
return material_out
instance.material_out = material_out
instance.save()
return
def create(self, validated_data):
process = validated_data['process']
routepack = validated_data.get('routepack', None)
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
if routepack:
pass
# if Route.objects.filter(routepack=routepack, process=process).exists():
@ -246,77 +239,41 @@ 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:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material:
if instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
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()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
self.gen_material_out(instance, validated_data.get("material_out_tracking", Material.MA_TRACKING_BATCH))
return instance
def update(self, instance, validated_data):
validated_data.pop('material', None)
process = validated_data.pop('process', None)
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:
if material_out.process is None:
material_out.process = process
if material_out_tracking != material_out.tracking:
raise ParseError("物料跟踪类型不一致!请前往物料处修改")
if material_out.parent is None and instance.material:
if instance.material:
material_out.parent = instance.material
material_out.save()
# elif material_out.process != process:
# raise ParseError('物料工序错误!请重新选择')
else:
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()
if rx:
instance.from_route = rx
instance.save()
# msg = ""
# if rx.routepack:
# msg = rx.routepack.name
# raise ParseError(f"该工艺步骤已存在-{msg}")
self.gen_material_out(instance, validated_data.get("material_out_tracking", Material.MA_TRACKING_BATCH))
return instance
def to_representation(self, instance):
res = super().to_representation(instance)
if instance.material_out:
res['material_out_tracking'] = instance.material_out.tracking
else:
res['material_out_tracking'] = None
return res
class SruleSerializer(CustomModelSerializer):
belong_dept_name = serializers.CharField(source='belong_dept.name', read_only=True)
@ -338,15 +295,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:
@ -148,19 +146,18 @@ def mgroup_run_change(mgroup: Mgroup, new_run: bool, last_timex: datetime, note:
def bind_routepack(ticket: Ticket, transition, new_ticket_data: dict):
routepack = RoutePack.objects.get(id=new_ticket_data['t_id'])
routepack.get_gjson(need_update=True)
if routepack.ticket and routepack.ticket.id!=ticket.id:
raise ParseError('重复创建工单')
if not Route.objects.filter(routepack=routepack).exists():
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 +179,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)
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
@ -20,9 +20,6 @@ from django.db import transaction
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 +31,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():
@ -60,18 +58,6 @@ class MaterialViewSet(CustomModelViewSet):
daoru_material(settings.BASE_DIR + request.data.get('path', ''))
return Response()
@action(methods=['post'], detail=True, serializer_class=Serializer, perms_map={'post': 'material.create'})
@transaction.atomic
def cal_count(self, request, *args, **kwargs):
"""统计数量
统计数量
"""
ins = self.get_object()
from apps.mtm.services_2 import cal_material_count
cal_material_count([ins.id])
return Response()
@action(methods=['put'], detail=True, serializer_class=Serializer, perms_map={'put': '*'})
@transaction.atomic
def set_week_esitimate_consume(self, request, *args, **kwargs):
@ -90,23 +76,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:班次
@ -221,10 +190,7 @@ class ProcessViewSet(CustomModelViewSet):
serializer_class = ProcessSerializer
select_related_fields = ['belong_dept']
search_fields = ['name', 'cate']
filterset_fields = {
"cate": ['exact'],
"parent": ['isnull', "exact"]
}
filterset_fields = ['cate']
ordering = ['sort', 'create_time']
def perform_destroy(self, instance):
@ -262,43 +228,26 @@ class RoutePackViewSet(CustomModelViewSet):
return Response(status=204)
@transaction.atomic
@action(methods=['post'], detail=False, perms_map={'post': 'routepack.create'}, serializer_class=RoutePackCopySerializer)
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.create'}, serializer_class=Serializer)
def copy(self, request, *args, **kwargs):
"""复制工艺路线
复制工艺路线
"""
data = request.data
sr = RoutePackCopySerializer(data=data)
sr.is_valid(raise_exception=True)
vdata = sr.validated_data
obj = self.get_object()
user = request.user
now = timezone.now()
new_name = vdata["new_name"]
rp = RoutePack.objects.get(id=vdata["routepack"])
matin = Material.objects.get(id=vdata["material_in"])
matout = Material.objects.get(id=vdata["material_out"])
obj_c = RoutePack()
obj_c.name = new_name
obj_c.material = matout
obj_c.name = f'{obj.name}_copy'
obj_c.material = obj.material
obj_c.create_by = user
obj_c.create_time = now
obj_c.save()
genM = {}
for ind, route in enumerate(Route.get_routes(routepack=rp)):
for route in Route.objects.filter(routepack=obj):
route_new = Route()
process = route.process
for f in Route._meta.fields:
if f.name in ['process', 'sort', 'is_autotask', 'is_count_utask', 'out_rate', 'div_number', 'hour_work', 'batch_bind']:
if f.name not in ['id', 'create_by', 'update_by', 'create_time', 'update_time']:
setattr(route_new, f.name, getattr(route, f.name, None))
route_new.material = matout
# material_out = RouteSerializer.gen_material_out(instance=route_new, material_out_tracking=route.material_out.tracking)
# route_new.material_out = material_out
# if ind == 0:
# route_new.material_in = matin
# elif route.material_in.process and route.material_in.process.id in genM:
# route_new.material_in = genM[route.material_in.process.id]
# genM[process.id] = material_out
route_new.routepack = obj_c
route_new.create_by = user
route_new.create_time = now
@ -308,56 +257,9 @@ class RoutePackViewSet(CustomModelViewSet):
rm_new.route = route_new
rm_new.material = rm.material
rm_new.save()
return Response({"id": route_new.id})
@transaction.atomic
@action(methods=['post'], detail=True, perms_map={'post': 'routepack.update'}, serializer_class=Serializer)
def toggle_state(self, request, *args, **kwargs):
"""变更工艺路线状态
变更工艺路线状态
"""
ins:RoutePack = self.get_object()
if ins.state == RoutePack.RP_S_CONFIRM:
ins.state = RoutePack.RP_S_CREATE
elif ins.state == RoutePack.RP_S_CREATE:
if ins.ticket is not None:
ins.get_gjson(need_update=True)
ins.state = RoutePack.RP_S_CONFIRM
else:
raise ParseError("该路线未提交审核")
else:
raise ParseError("该状态下不可变更")
ins.save()
return Response()
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def dag(self, request, *args, **kwargs):
"""获取总图
获取总图
"""
return Response(Route.get_dag(rqs=Route.objects.filter(routepack=self.get_object())))
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def dags(self, request, *args, **kwargs):
"""获取所有子图
获取所有子图
"""
ins = self.get_object()
return Response(ins.get_dags())
@action(methods=['get'], detail=True, perms_map={'get': '*'})
def final_materials(self, request, *args, **kwargs):
"""获取最终产品
获取最终产品"""
ins:RoutePack = self.get_object()
matIds = ins.get_final_material_ids()
qs = Material.objects.filter(id__in=matIds)
res = [{"id": x.id, "name": str(x)} for x in qs]
return Response(res)
class RouteViewSet(CustomModelViewSet):
queryset = Route.objects.all()
serializer_class = RouteSerializer
@ -367,22 +269,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='关联会议室'),
),
]

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