This commit is contained in:
caoqianming 2023-12-16 09:56:48 +08:00
commit e5d2d5ef84
42 changed files with 614 additions and 104 deletions

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2023-11-28 07:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('em', '0008_equipment_accuracy_level'),
]
operations = [
migrations.AlterField(
model_name='equipment',
name='description',
field=models.CharField(blank=True, default='', max_length=200, verbose_name='描述'),
),
]

View File

@ -62,7 +62,8 @@ class Equipment(CommonBModel):
count = models.PositiveIntegerField('数量', default=1) count = models.PositiveIntegerField('数量', default=1)
keeper = models.ForeignKey( keeper = models.ForeignKey(
User, verbose_name='责任人', on_delete=models.CASCADE, null=True, blank=True) User, verbose_name='责任人', on_delete=models.CASCADE, null=True, blank=True)
description = models.CharField('描述', max_length=200, default='', null=True) description = models.CharField(
'描述', max_length=200, default='', blank=True)
# 以下是计量检测设备单独字段 # 以下是计量检测设备单独字段
# mgmtype = models.IntegerField('管理类别', choices=mgmtype_choices, default=1) # mgmtype = models.IntegerField('管理类别', choices=mgmtype_choices, default=1)

View File

@ -9,5 +9,7 @@ class MaterialBatchFilter(filters.FilterSet):
fields = { fields = {
"warehouse": ["exact"], "warehouse": ["exact"],
"material": ["exact"], "material": ["exact"],
"material__type": ["exact", "in"],
"material__process": ["exact", "in"],
"count": ["exact", "gte", "lte"] "count": ["exact", "gte", "lte"]
} }

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2023-12-15 11:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('inm', '0010_auto_20231116_1904'),
]
operations = [
migrations.RenameField(
model_name='mioitem',
old_name='is_bgtest_ok',
new_name='is_testok',
),
]

View File

@ -121,7 +121,7 @@ class MIOItem(BaseModel):
count_n_jsqx = models.PositiveIntegerField('结石气线', default=0) count_n_jsqx = models.PositiveIntegerField('结石气线', default=0)
count_n_qt = models.PositiveIntegerField('其他', default=0) count_n_qt = models.PositiveIntegerField('其他', default=0)
is_bgtest_ok = models.BooleanField('配套件是否合格', default=True) is_testok = models.BooleanField('配套件是否合格', default=True)
class MIOItemA(BaseModel): class MIOItemA(BaseModel):

View File

@ -94,7 +94,7 @@ class MIOItemCreateSerializer(CustomModelSerializer):
class Meta: class Meta:
model = MIOItem model = MIOItem
fields = ['mio', 'warehouse', 'material', fields = ['mio', 'warehouse', 'material',
'batch', 'count', 'assemb', 'is_bgtest_ok'] 'batch', 'count', 'assemb', 'is_testok']
def create(self, validated_data): def create(self, validated_data):
mio = validated_data['mio'] mio = validated_data['mio']

View File

@ -49,7 +49,8 @@ class MaterialBatchViewSet(ListModelMixin, CustomGenericViewSet):
retrieve_serializer_class = MaterialBatchDetailSerializer retrieve_serializer_class = MaterialBatchDetailSerializer
select_related_fields = ['warehouse', 'material'] select_related_fields = ['warehouse', 'material']
filterset_class = MaterialBatchFilter filterset_class = MaterialBatchFilter
search_fields = ['material__name'] search_fields = ['material__name', 'material__number',
'material__model', 'material__specification', 'batch']
class MioDoViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomGenericViewSet): class MioDoViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomGenericViewSet):
@ -123,7 +124,12 @@ class MIOViewSet(CustomModelViewSet):
'submit_user', 'supplier', 'order', 'customer', 'pu_order'] 'submit_user', 'supplier', 'order', 'customer', 'pu_order']
serializer_class = MIOListSerializer serializer_class = MIOListSerializer
retrieve_serializer_class = MIODetailSerializer retrieve_serializer_class = MIODetailSerializer
filterset_fields = ['state', 'type', 'pu_order', 'order'] filterset_fields = {
'state': ["exact", "in"],
"type": ["exact", "in"],
"pu_order": ["exact"],
"order": ["exact"]
}
search_fields = ['number'] search_fields = ['number']
data_filter = True data_filter = True

View File

@ -0,0 +1,31 @@
# Generated by Django 3.2.12 on 2023-12-07 01:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('monitor', '0003_alter_drfrequestlog_view_method'),
]
operations = [
migrations.CreateModel(
name='AuditLog',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('action', models.CharField(max_length=20, verbose_name='动作')),
('model_name', models.CharField(max_length=20, verbose_name='模型名')),
('instance_id', models.CharField(editable=False, max_length=20, verbose_name='记录ID')),
('change_reason', models.CharField(default='', max_length=50, verbose_name='变更原因')),
('change_time', models.DateTimeField(verbose_name='变更时间')),
('val_new', models.JSONField(default=dict, verbose_name='变更后完整数据')),
('difference', models.JSONField(default=list, verbose_name='变更情况')),
('change_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='操作人')),
],
),
]

View File

@ -4,6 +4,19 @@ from django.db import models
from apps.utils.models import BaseModel from apps.utils.models import BaseModel
class AuditLog(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
action = models.CharField('动作', max_length=20)
model_name = models.CharField('模型名', max_length=20)
instance_id = models.CharField('记录ID', max_length=20, editable=False)
change_reason = models.CharField('变更原因', default='', max_length=50)
change_user = models.ForeignKey(
'system.user', on_delete=models.SET_NULL, verbose_name='操作人', null=True, blank=True)
change_time = models.DateTimeField('变更时间')
val_new = models.JSONField('变更后完整数据', default=dict)
difference = models.JSONField('变更情况', default=list)
class DrfRequestLog(BaseModel): class DrfRequestLog(BaseModel):
"""Logs Django rest framework API requests""" """Logs Django rest framework API requests"""
@ -42,7 +55,8 @@ class DrfRequestLog(BaseModel):
response = models.TextField(null=True, blank=True) response = models.TextField(null=True, blank=True)
errors = models.TextField(null=True, blank=True) errors = models.TextField(null=True, blank=True)
agent = models.TextField(null=True, blank=True) agent = models.TextField(null=True, blank=True)
status_code = models.PositiveIntegerField(null=True, blank=True, db_index=True) status_code = models.PositiveIntegerField(
null=True, blank=True, db_index=True)
class Meta: class Meta:
verbose_name = "DRF请求日志" verbose_name = "DRF请求日志"

View File

@ -1,4 +1,17 @@
from rest_framework import serializers from rest_framework import serializers
from apps.utils.serializers import CustomModelSerializer
from apps.monitor.models import AuditLog
class DbbackupDeleteSerializer(serializers.Serializer): class DbbackupDeleteSerializer(serializers.Serializer):
filepaths = serializers.ListField(child=serializers.CharField(), label="文件地址列表") filepaths = serializers.ListField(
child=serializers.CharField(), label="文件地址列表")
class AuditLogSerializer(CustomModelSerializer):
change_user_name = serializers.CharField(
source='change_user.name', read_only=True)
class Meta:
model = AuditLog
fields = '__all__'

View File

@ -1,4 +1,63 @@
import psutil import psutil
from apps.monitor.models import AuditLog
from apps.system.models import User
from datetime import datetime
from apps.utils.tools import compare_values
from apps.utils.models import get_model_info
def delete_auditlog(model, instance_id):
"""
删除其对应的审计记录
"""
model_name = get_model_info(model)
AuditLog.objects.filter(model_name=model_name,
instance_id=instance_id).delete()
def create_auditlog(action: str, instance, val_new: dict, val_old: dict = None, change_reason: str = '', delete_time: datetime = None, delete_user: User = None):
"""
生成审计日志
action: create/update/delete/其他action
"""
app_label_model_name = get_model_info(instance)
if val_old is None:
val_old = {}
difference = []
has_changed = False
if action == 'create':
has_changed = True
change_user = instance.create_by
change_time = instance.create_time
elif action == 'delete':
has_changed = True
change_user = delete_user if delete_user else instance.update_by
change_time = delete_time if delete_time else instance.update_time
else:
change_user = instance.update_by
change_time = instance.update_time
for k, v in val_new.items():
if k not in ['create_by', 'update_by', 'create_time', 'update_time', 'id']:
if k not in val_old:
difference.append(
{'field': k, 'action': 'create', 'val_old': None, 'val_new': v})
elif not compare_values(val_new.get(k), val_old.get(k), ignore_order=True):
difference.append(
{'field': k, 'action': 'update', 'val_old': val_old[k], 'val_new': v})
if difference:
has_changed = True
if has_changed:
AuditLog.objects.create(
action=action,
model_name=app_label_model_name,
instance_id=instance.id,
val_new=val_new,
difference=difference,
change_reason=change_reason,
change_user=change_user,
change_time=change_time
)
class ServerService: class ServerService:
@classmethod @classmethod
@ -17,7 +76,7 @@ class ServerService:
ret['count'] = psutil.cpu_count(logical=False) ret['count'] = psutil.cpu_count(logical=False)
ret['percent'] = psutil.cpu_percent(interval=1) ret['percent'] = psutil.cpu_percent(interval=1)
return ret return ret
@classmethod @classmethod
def get_disk_dict(cls): def get_disk_dict(cls):
ret = {} ret = {}
@ -29,4 +88,4 @@ class ServerService:
@classmethod @classmethod
def get_full(cls): def get_full(cls):
return {'cpu': cls.get_cpu_dict(), 'memory': cls.get_memory_dict(), 'disk': cls.get_disk_dict()} return {'cpu': cls.get_cpu_dict(), 'memory': cls.get_memory_dict(), 'disk': cls.get_disk_dict()}

View File

@ -1,5 +1,5 @@
from django.urls import path from django.urls import path
from .views import DrfRequestLogViewSet, ServerInfoView, LogView, LogDetailView, index, room, video, DbBackupView from .views import DrfRequestLogViewSet, ServerInfoView, LogView, LogDetailView, index, room, video, DbBackupView, AuditlogViewSet
API_BASE_URL = 'api/monitor/' API_BASE_URL = 'api/monitor/'
HTML_BASE_URL = 'monitor/' HTML_BASE_URL = 'monitor/'
@ -13,5 +13,8 @@ urlpatterns = [
path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()), path(API_BASE_URL + 'log/<str:name>/', LogDetailView.as_view()),
path(API_BASE_URL + 'dbbackup/', DbBackupView.as_view()), path(API_BASE_URL + 'dbbackup/', DbBackupView.as_view()),
path(API_BASE_URL + 'server/', ServerInfoView.as_view()), path(API_BASE_URL + 'server/', ServerInfoView.as_view()),
path(API_BASE_URL + 'request_log/', DrfRequestLogViewSet.as_view({'get': 'list'}), name='requestlog_view') path(API_BASE_URL + 'request_log/',
DrfRequestLogViewSet.as_view({'get': 'list'}), name='requestlog_view'),
path(API_BASE_URL + 'auditlog/',
AuditlogViewSet.as_view({'get': 'list'}), name='auditlog_view')
] ]

View File

@ -7,13 +7,13 @@ from rest_framework.permissions import IsAuthenticated
from django.conf import settings from django.conf import settings
import os import os
from rest_framework import serializers from rest_framework import serializers
from apps.monitor.serializers import DbbackupDeleteSerializer from apps.monitor.serializers import DbbackupDeleteSerializer, AuditLogSerializer
from drf_yasg import openapi from drf_yasg import openapi
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from rest_framework.exceptions import NotFound from rest_framework.exceptions import NotFound
from rest_framework.mixins import ListModelMixin from rest_framework.mixins import ListModelMixin
from apps.monitor.filters import DrfLogFilterSet from apps.monitor.filters import DrfLogFilterSet
from apps.monitor.models import DrfRequestLog from apps.monitor.models import DrfRequestLog, AuditLog
from apps.monitor.errors import LOG_NOT_FONED from apps.monitor.errors import LOG_NOT_FONED
from apps.monitor.services import ServerService from apps.monitor.services import ServerService
@ -188,3 +188,16 @@ class DrfRequestLogViewSet(ListModelMixin, CustomGenericViewSet):
ordering = ['-requested_at'] ordering = ['-requested_at']
filterset_class = DrfLogFilterSet filterset_class = DrfLogFilterSet
search_fields = ['path', 'view'] search_fields = ['path', 'view']
class AuditlogViewSet(ListModelMixin, CustomGenericViewSet):
"""审计日志
审计日志
"""
perms_map = {'get': '*'}
queryset = AuditLog.objects.all()
list_serializer_class = AuditLogSerializer
ordering = ['-change_time']
filterset_fields = ['change_user', 'model_name', 'action', 'instance_id']
search_fields = ['model_name', 'action']

View File

@ -15,6 +15,7 @@ class MaterialFilter(filters.FilterSet):
"is_hidden": ["exact"], "is_hidden": ["exact"],
"is_assemb": ["exact"], "is_assemb": ["exact"],
"need_route": ["exact"], "need_route": ["exact"],
"process": ["exact", "in", "isnull"],
"orderitem_material__order": ['exact'], "orderitem_material__order": ['exact'],
"pu_orderitem_material__pu_order": ["exact"], "pu_orderitem_material__pu_order": ["exact"],
"route_material_out__mgroup": ["exact"] "route_material_out__mgroup": ["exact"]
@ -48,4 +49,7 @@ class RouteFilter(filters.FilterSet):
"process": ["exact", "in"], "process": ["exact", "in"],
"is_autotask": ["exact"], "is_autotask": ["exact"],
"mgroup": ["exact", "in", "isnull"], "mgroup": ["exact", "in", "isnull"],
"mgroup__name": ["exact", "contains"],
"mgroup__belong_dept": ["exact"],
"mgroup__belong_dept__name": ["exact", "contains"]
} }

View File

@ -15,10 +15,12 @@ class ShiftSerializer(CustomModelSerializer):
class MaterialSimpleSerializer(CustomModelSerializer): class MaterialSimpleSerializer(CustomModelSerializer):
process_name = serializers.CharField(source='process.name', read_only=True)
class Meta: class Meta:
model = Material model = Material
fields = ['id', 'name', 'number', 'model', fields = ['id', 'name', 'number', 'model',
'specification', 'type', 'cate', 'brothers'] 'specification', 'type', 'cate', 'brothers', 'process_name']
class MaterialSerializer(CustomModelSerializer): class MaterialSerializer(CustomModelSerializer):
@ -124,15 +126,16 @@ class RouteSerializer(CustomModelSerializer):
fields = '__all__' fields = '__all__'
read_only_fields = EXCLUDE_FIELDS read_only_fields = EXCLUDE_FIELDS
# def validate(self, attrs): def validate(self, attrs):
# material = attrs['material'] if 'mgroup' in attrs and attrs['mgroup']:
# if material.type != Material.MA_TYPE_GOOD: attrs['process'] = attrs['mgroup'].process
# raise ValidationError('请选择最终产品') if attrs.get('process', None) is None:
# return super().validate(attrs) raise ParseError('未提供操作工序')
return super().validate(attrs)
def gen_material_out(self, instance): def gen_material_out(self, instance):
""" """
废弃不用了 自动形成物料
""" """
name = f'{instance.material.name}-中' name = f'{instance.material.name}-中'
instance.material_out, _ = Material.objects.get_or_create(type=Material.MA_TYPE_HALFGOOD, parent=instance.material, process=instance.process, instance.material_out, _ = Material.objects.get_or_create(type=Material.MA_TYPE_HALFGOOD, parent=instance.material, process=instance.process,
@ -140,6 +143,7 @@ class RouteSerializer(CustomModelSerializer):
'is_hidden': True, 'name': name, 'is_hidden': True, 'name': name,
'number': instance.material.number, 'number': instance.material.number,
'specification': instance.material.specification, 'specification': instance.material.specification,
'model': instance.material.model,
'type': Material.MA_TYPE_HALFGOOD, 'type': Material.MA_TYPE_HALFGOOD,
'create_by': self.request.user, 'create_by': self.request.user,
'update_by': self.request.user, 'update_by': self.request.user,
@ -147,24 +151,41 @@ class RouteSerializer(CustomModelSerializer):
instance.save() instance.save()
def create(self, validated_data): def create(self, validated_data):
process = validated_data.get('process', None) process = validated_data['process']
if process and Route.objects.filter(material=validated_data['material'], process=process).exists(): material = validated_data.get('material', None)
if material and process and Route.objects.filter(material=material, process=process).exists():
raise ValidationError('已选择该工序') raise ValidationError('已选择该工序')
with transaction.atomic(): with transaction.atomic():
instance = super().create(validated_data) instance = super().create(validated_data)
# if 'material_out' in validated_data and validated_data['material_out'] and instance.material: material_out = instance.material_out
# pass if material_out:
# else: if material_out.process is None:
# self.gen_material_out(instance) material_out.process = process
if instance.material:
material_out.parent = instance.material
material_out.save()
elif material_out.process != process:
raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
self.gen_material_out()
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
validated_data.pop('material', None) validated_data.pop('material', None)
validated_data.pop('process', None) process = validated_data.pop('process', None)
with transaction.atomic(): with transaction.atomic():
instance = super().update(instance, validated_data) instance = super().update(instance, validated_data)
# if 'material_out' in validated_data and validated_data['material_out'] and instance.material: material_out = instance.material_out
# pass if material_out:
# else: if material_out.process is None:
# self.gen_material_out(instance) material_out.process = process
if instance.material:
material_out.parent = instance.material
material_out.save()
elif material_out.process != process:
raise ParseError('物料工序错误!请重新选择')
else:
if instance.material:
self.gen_material_out()
return instance return instance

View File

@ -51,7 +51,13 @@ class MgroupViewSet(CustomModelViewSet):
queryset = Mgroup.objects.all() queryset = Mgroup.objects.all()
serializer_class = MgroupSerializer serializer_class = MgroupSerializer
select_related_fields = ['create_by', 'belong_dept', 'process'] select_related_fields = ['create_by', 'belong_dept', 'process']
filterset_fields = ['belong_dept', 'process', 'cate', 'belong_dept__name'] filterset_fields = {
"belong_dept": ["exact"],
"process": ["exact"],
"cate": ["exact"],
"belong_dept__name": ["exact", "contains"],
"name": ["exact", "contains"]
}
search_fields = ['number'] search_fields = ['number']
ordering = ['sort', 'create_time'] ordering = ['sort', 'create_time']

View File

@ -38,7 +38,7 @@ class UtaskFilter(filters.FilterSet):
class MtaskFilter(filters.FilterSet): class MtaskFilter(filters.FilterSet):
tag = filters.CharFilter(method='filter_tag') tag = filters.CharFilter(method='filter_tag', label='done, not_done')
class Meta: class Meta:
model = Mtask model = Mtask
@ -55,17 +55,22 @@ class MtaskFilter(filters.FilterSet):
"material_out__type": ["exact"], "material_out__type": ["exact"],
"material_out__is_hidden": ["exact"], "material_out__is_hidden": ["exact"],
"mgroup__belong_dept__name": ["exact"], "mgroup__belong_dept__name": ["exact"],
"mgroup__belong_dept": ["exact"],
"utask": ["exact"] "utask": ["exact"]
} }
def filter_tag(self, queryset, name, value): def filter_tag(self, queryset, name, value):
now = timezone.now() # now = timezone.now()
day7_after = now + timedelta(days=7) # day7_after = now + timedelta(days=7)
if value == 'near_done': # if value == 'near_done':
queryset = queryset.filter(count_ok__lt=F('count'), # queryset = queryset.filter(count_ok__lt=F('count'),
end_date__lte=day7_after.date(), # end_date__lte=day7_after.date(),
end_date__gte=now.date()) # end_date__gte=now.date())
elif value == 'out_done': # elif value == 'out_done':
queryset = queryset.filter(count_ok__lt=F('count'), # queryset = queryset.filter(count_ok__lt=F('count'),
end_date__lt=now.date()) # end_date__lt=now.date())
if value == 'done':
queryset = queryset.filter(count_ok__gte=F('count'))
elif value == 'not_done':
queryset = queryset.filter(count_ok__lt=F('count'))
return queryset return queryset

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.12 on 2023-11-30 08:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pm', '0015_utask_count_day'),
]
operations = [
migrations.AddField(
model_name='mtask',
name='submit_time',
field=models.DateTimeField(blank=True, null=True, verbose_name='提交时间'),
),
migrations.AddField(
model_name='mtask',
name='submit_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='mtask_submit_user', to=settings.AUTH_USER_MODEL, verbose_name='提交人'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2023-12-05 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pm', '0016_auto_20231130_1628'),
]
operations = [
migrations.AddField(
model_name='mtask',
name='type',
field=models.CharField(default='mass', help_text="(('mass', '量产'), ('pilot', '中试'))", max_length=10, verbose_name='任务类型'),
),
migrations.AddField(
model_name='utask',
name='type',
field=models.CharField(default='mass', help_text="(('mass', '量产'), ('pilot', '中试'))", max_length=10, verbose_name='任务类型'),
),
]

View File

@ -4,6 +4,11 @@ from apps.mtm.models import Material, Mgroup
# Create your models here. # Create your models here.
TASK_TYPE = (
('mass', '量产'),
('pilot', '中试')
)
class Utask(CommonBDModel): class Utask(CommonBDModel):
""" """
@ -14,15 +19,17 @@ class Utask(CommonBDModel):
UTASK_ASSGINED = 20 UTASK_ASSGINED = 20
UTASK_WORKING = 30 UTASK_WORKING = 30
UTASK_STOP = 34 UTASK_STOP = 34
UTASK_DONE = 40 UTASK_SUBMIT = 40
UTASK_STATES = ( UTASK_STATES = (
(UTASK_CREATED, '创建中'), (UTASK_CREATED, '创建中'),
(UTASK_DECOMPOSE, '已分解'), (UTASK_DECOMPOSE, '已分解'),
(UTASK_ASSGINED, '已下达'), (UTASK_ASSGINED, '已下达'),
(UTASK_WORKING, '生产中'), (UTASK_WORKING, '生产中'),
(UTASK_STOP, '已停止'), (UTASK_STOP, '已停止'),
(UTASK_DONE, '已提交') (UTASK_SUBMIT, '已提交')
) )
type = models.CharField('任务类型', max_length=10,
help_text=str(TASK_TYPE), default='mass')
state = models.PositiveIntegerField( state = models.PositiveIntegerField(
'状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES)) '状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES))
number = models.CharField('编号', max_length=50, unique=True) number = models.CharField('编号', max_length=50, unique=True)
@ -48,13 +55,15 @@ class Mtask(CommonADModel):
MTASK_CREATED = 10 MTASK_CREATED = 10
MTASK_ASSGINED = 20 MTASK_ASSGINED = 20
MTASK_STOP = 34 MTASK_STOP = 34
MTASK_DONE = 40 MTASK_SUBMIT = 40
MTASK_STATES = ( MTASK_STATES = (
(MTASK_CREATED, '创建中'), (MTASK_CREATED, '创建中'),
(MTASK_ASSGINED, '已下达'), (MTASK_ASSGINED, '已下达'),
(MTASK_STOP, '已停止'), (MTASK_STOP, '已停止'),
(MTASK_DONE, '已提交') (MTASK_SUBMIT, '已提交')
) )
type = models.CharField('任务类型', max_length=10,
help_text=str(TASK_TYPE), default='mass')
state = models.PositiveIntegerField( state = models.PositiveIntegerField(
'状态', choices=MTASK_STATES, default=MTASK_CREATED, help_text=str(MTASK_STATES)) '状态', choices=MTASK_STATES, default=MTASK_CREATED, help_text=str(MTASK_STATES))
number = models.CharField('编号', max_length=50, unique=True) number = models.CharField('编号', max_length=50, unique=True)
@ -75,6 +84,10 @@ class Mtask(CommonADModel):
Utask, verbose_name='关联大任务', on_delete=models.CASCADE, related_name='mtask_utask', null=True, blank=True) Utask, verbose_name='关联大任务', on_delete=models.CASCADE, related_name='mtask_utask', null=True, blank=True)
peifen_kg = models.FloatField('配粉料数', default=0) peifen_kg = models.FloatField('配粉料数', default=0)
submit_time = models.DateTimeField('提交时间', null=True, blank=True)
submit_user = models.ForeignKey(
'system.user', verbose_name='提交人', on_delete=models.CASCADE, null=True, blank=True, related_name='mtask_submit_user')
@property @property
def related(self): def related(self):
""" """

View File

@ -12,6 +12,7 @@ from apps.wpm.models import Mlog
class UtaskSerializer(CustomModelSerializer): class UtaskSerializer(CustomModelSerializer):
material_ = MaterialSimpleSerializer(source='material', read_only=True) material_ = MaterialSimpleSerializer(source='material', read_only=True)
mgroup_name = serializers.CharField(source='mgroup.name', read_only=True)
belong_dept = serializers.PrimaryKeyRelatedField( belong_dept = serializers.PrimaryKeyRelatedField(
queryset=Dept.objects.all(), required=False) queryset=Dept.objects.all(), required=False)

View File

@ -71,6 +71,7 @@ class PmService:
for i in range(rela_days): for i in range(rela_days):
task_date = start_date + timedelta(days=i) task_date = start_date + timedelta(days=i)
Mtask.objects.create(**{ Mtask.objects.create(**{
'type': utask.type,
'number': f'{number}_{i+1}', 'number': f'{number}_{i+1}',
'material_out': utask.material, 'material_out': utask.material,
'material_in': utask.material_in, 'material_in': utask.material_in,
@ -124,6 +125,7 @@ class PmService:
task_date = start_date + timedelta(days=i) task_date = start_date + timedelta(days=i)
Mtask.objects.create(**{ Mtask.objects.create(**{
'number': f'{number}_r{ind+1}_{i+1}', 'number': f'{number}_r{ind+1}_{i+1}',
'type': utask.type,
'material_out': halfgood, 'material_out': halfgood,
'material_in': material_in, 'material_in': material_in,
'mgroup': mgroup, 'mgroup': mgroup,
@ -251,19 +253,23 @@ class PmService:
change_order_state_when_schedue(orderitemIds) change_order_state_when_schedue(orderitemIds)
@classmethod @classmethod
def mtasks_submit(cls, mtasks: QuerySet[Mtask], user: User): def mtask_submit(cls, mtask: Mtask, user: User):
""" """
锁定生产任务 锁定生产任务
""" """
from apps.wpm.models import Mlog from apps.wpm.models import Mlog
from apps.wpm.services import mlog_submit, update_mtask from apps.wpm.services import mlog_submit, update_mtask
now = timezone.now() now = timezone.now()
for mtask in mtasks: if mtask.state == Mtask.MTASK_ASSGINED:
mlogs = Mlog.objects.filter(mtask=mtask) mlogs = Mlog.objects.filter(mtask=mtask)
if mlogs.count() == 0: if mlogs.count() == 0:
raise ParseError(f'{mtask.mgroup.name}_未填写日志') raise ParseError(f'{mtask.mgroup.name}_未填写日志')
for mlog in mlogs: for mlog in mlogs:
mlog_submit(mlog, user, now) mlog_submit(mlog, user, now)
update_mtask(mtask) update_mtask(mtask)
mtask.state = Mtask.MTASK_DONE mtask.state = Mtask.MTASK_SUBMIT
mtask.submit_time = now
mtask.submit_user = user
mtask.save() mtask.save()
else:
raise ParseError('该任务状态不可提交')

View File

@ -158,18 +158,16 @@ class MtaskViewSet(CustomModelViewSet):
raise ParseError('该任务非创建中不可删除') raise ParseError('该任务非创建中不可删除')
return super().perform_destroy(instance) return super().perform_destroy(instance)
@action(methods=['post'], detail=False, perms_map={'post': 'mtask.submit'}, serializer_class=PkSerializer) @action(methods=['post'], detail=True, perms_map={'post': 'mtask.submit'}, serializer_class=PkSerializer)
@transaction.atomic @transaction.atomic
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):
"""提交任务(根据任务ID) """提交任务(根据任务ID)
提交任务后不可更新日志 提交任务后不可更新日志
""" """
ids = request.data.get('ids', []) mtask: Mtask = self.get_object()
user = request.user user = request.user
mtasks = Mtask.objects.filter( PmService.mtask_submit(mtask, user)
id__in=ids, state=Mtask.MTASK_ASSGINED)
PmService.mtasks_submit(mtasks, user)
return Response() return Response()
@action(methods=['post'], detail=True, perms_map={'post': 'mtask.submit'}, serializer_class=Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mtask.submit'}, serializer_class=Serializer)
@ -181,5 +179,6 @@ class MtaskViewSet(CustomModelViewSet):
""" """
mtask = self.get_object() mtask = self.get_object()
mtasks = mtask.related mtasks = mtask.related
PmService.mtasks_submit(mtasks) for mtask in mtasks:
PmService.mtask_submit(mtask, self.request.user)
return Response() return Response()

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2023-12-01 05:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mtm', '0025_auto_20231120_1139'),
('pum', '0002_alter_puorderitem_material'),
]
operations = [
migrations.AddField(
model_name='puorder',
name='materials',
field=models.ManyToManyField(blank=True, related_name='pu_order_materials', through='pum.PuOrderItem', to='mtm.Material', verbose_name='多个物料'),
),
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.2.12 on 2023-12-01 05:50
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('pum', '0003_puorder_materials'),
]
operations = [
migrations.AddField(
model_name='puorder',
name='submit_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='submit_user_puorder', to=settings.AUTH_USER_MODEL, verbose_name='提交人'),
),
migrations.AddField(
model_name='puplan',
name='submit_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='submit_user_puplan', to=settings.AUTH_USER_MODEL, verbose_name='提交人'),
),
]

View File

@ -35,6 +35,8 @@ class PuPlan(CommonBModel):
number = models.CharField('编号', max_length=20) number = models.CharField('编号', max_length=20)
name = models.CharField('名称', max_length=50, null=True, blank=True) name = models.CharField('名称', max_length=50, null=True, blank=True)
submit_time = models.DateTimeField('提交时间', null=True, blank=True) submit_time = models.DateTimeField('提交时间', null=True, blank=True)
submit_user = models.ForeignKey(
'system.user', verbose_name='提交人', related_name='submit_user_puplan', on_delete=models.CASCADE, null=True, blank=True)
class PuOrder(CommonBModel): class PuOrder(CommonBModel):
@ -58,6 +60,10 @@ class PuOrder(CommonBModel):
Supplier, verbose_name='供应商', on_delete=models.CASCADE) Supplier, verbose_name='供应商', on_delete=models.CASCADE)
delivery_date = models.DateField('截止到货日期', null=True, blank=True) delivery_date = models.DateField('截止到货日期', null=True, blank=True)
submit_time = models.DateTimeField('提交时间', null=True, blank=True) submit_time = models.DateTimeField('提交时间', null=True, blank=True)
submit_user = models.ForeignKey(
'system.user', verbose_name='提交人', related_name='submit_user_puorder', on_delete=models.CASCADE, null=True, blank=True)
materials = models.ManyToManyField(
Material, verbose_name='多个物料', blank=True, through='pum.puorderitem', related_name='pu_order_materials')
class PuOrderItem(BaseModel): class PuOrderItem(BaseModel):

View File

@ -4,7 +4,7 @@ from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLU
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem from apps.pum.models import Supplier, PuPlan, PuPlanItem, PuOrder, PuOrderItem
from apps.mtm.serializers import MaterialSerializer from apps.mtm.serializers import MaterialSerializer, MaterialSimpleSerializer
class SupplierSerializer(CustomModelSerializer): class SupplierSerializer(CustomModelSerializer):
@ -83,6 +83,8 @@ class PuOrderSerializer(CustomModelSerializer):
source='create_by.name', read_only=True) source='create_by.name', read_only=True)
update_by_name = serializers.CharField( update_by_name = serializers.CharField(
source='update_by.name', read_only=True) source='update_by.name', read_only=True)
materials_ = MaterialSimpleSerializer(
source='materials', many=True, read_only=True)
class Meta: class Meta:
model = PuOrder model = PuOrder

View File

@ -46,7 +46,7 @@ class PuPlanViewSet(CustomModelViewSet):
raise ParseError('该计划存在明细不可删除') raise ParseError('该计划存在明细不可删除')
return super().perform_destroy(instance) return super().perform_destroy(instance)
@action(methods=['post'], detail=True, perms_map={'post': 'pu_plan.update'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'pu_plan.submit'}, serializer_class=serializers.Serializer)
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):
"""提交采购计划 """提交采购计划
@ -59,6 +59,7 @@ class PuPlanViewSet(CustomModelViewSet):
if puplan.state != PuPlan.PUPLAN_CREATE: if puplan.state != PuPlan.PUPLAN_CREATE:
raise ParseError('采购计划状态异常') raise ParseError('采购计划状态异常')
puplan.submit_time = timezone.now() puplan.submit_time = timezone.now()
puplan.submit_user = user
puplan.state = PuPlan.PUPLAN_SUBMITED puplan.state = PuPlan.PUPLAN_SUBMITED
puplan.save() puplan.save()
return Response() return Response()
@ -106,7 +107,7 @@ class PuOrderViewSet(CustomModelViewSet):
raise ParseError('采购订单非创建中不可删除') raise ParseError('采购订单非创建中不可删除')
instance.delete(soft=False) instance.delete(soft=False)
@action(methods=['post'], detail=True, perms_map={'post': 'pu_order.update'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'pu_order.submit'}, serializer_class=serializers.Serializer)
@transaction.atomic @transaction.atomic
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):
"""提交采购订单 """提交采购订单
@ -122,6 +123,7 @@ class PuOrderViewSet(CustomModelViewSet):
if puorder.state != PuOrder.PUORDER_CREATE: if puorder.state != PuOrder.PUORDER_CREATE:
raise ParseError('采购计划状态异常') raise ParseError('采购计划状态异常')
puorder.submit_time = timezone.now() puorder.submit_time = timezone.now()
puorder.submit_user = user
puorder.state = PuOrder.PUORDER_SUBMITED puorder.state = PuOrder.PUORDER_SUBMITED
puorder.save() puorder.save()
PumService.change_puplan_state_when_puorder_sumbit(puorder) PumService.change_puplan_state_when_puorder_sumbit(puorder)

View File

@ -60,6 +60,13 @@ class OrderViewSet(CustomModelViewSet):
search_fields = ['number'] search_fields = ['number']
filter_fields = ['contract', 'customer'] filter_fields = ['contract', 'customer']
@transaction.atomic
def perform_destroy(self, instance):
order = instance.order
if order.state != Order.ORDER_CREATE:
raise ParseError('订单非创建中不可删除')
instance.delete()
@action(methods=['post'], detail=True, perms_map={'post': 'order.update'}, serializer_class=serializers.Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'order.update'}, serializer_class=serializers.Serializer)
@transaction.atomic @transaction.atomic
def submit(self, request, *args, **kwargs): def submit(self, request, *args, **kwargs):

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.12 on 2023-12-04 00:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('system', '0002_myschedule'),
]
operations = [
migrations.AlterField(
model_name='permission',
name='parent',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.permission', verbose_name=''),
),
]

View File

@ -26,10 +26,11 @@ class Permission(BaseModel):
(PERM_TYPE_BUTTON, '按钮') (PERM_TYPE_BUTTON, '按钮')
) )
name = models.CharField('名称', max_length=30) name = models.CharField('名称', max_length=30)
type = models.PositiveSmallIntegerField('类型', choices=menu_type_choices, default=30) type = models.PositiveSmallIntegerField(
'类型', choices=menu_type_choices, default=30)
sort = models.PositiveSmallIntegerField('排序标记', default=1) sort = models.PositiveSmallIntegerField('排序标记', default=1)
parent = models.ForeignKey('self', null=True, blank=True, parent = models.ForeignKey('self', null=True, blank=True,
on_delete=models.SET_NULL, verbose_name='') on_delete=models.SET_NULL, verbose_name='', db_constraint=False)
codes = models.JSONField('权限标识', default=list, null=True, blank=True) codes = models.JSONField('权限标识', default=list, null=True, blank=True)
def __str__(self): def __str__(self):
@ -67,7 +68,8 @@ class Role(CommonADModel):
""" """
name = models.CharField('名称', max_length=32) name = models.CharField('名称', max_length=32)
code = models.CharField('角色标识', max_length=32, null=True, blank=True) code = models.CharField('角色标识', max_length=32, null=True, blank=True)
perms = models.ManyToManyField(Permission, blank=True, verbose_name='功能权限', related_name='role_perms') perms = models.ManyToManyField(
Permission, blank=True, verbose_name='功能权限', related_name='role_perms')
description = models.CharField('描述', max_length=50, blank=True, null=True) description = models.CharField('描述', max_length=50, blank=True, null=True)
class Meta: class Meta:
@ -106,8 +108,10 @@ class PostRole(BaseModel):
""" """
data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices, data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices,
default=DataFilter.THISLEVEL_AND_BELOW) default=DataFilter.THISLEVEL_AND_BELOW)
post = models.ForeignKey(Post, verbose_name='关联岗位', on_delete=models.CASCADE) post = models.ForeignKey(Post, verbose_name='关联岗位',
role = models.ForeignKey(Role, verbose_name='关联角色', on_delete=models.CASCADE) on_delete=models.CASCADE)
role = models.ForeignKey(Role, verbose_name='关联角色',
on_delete=models.CASCADE)
class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager): class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager):
@ -127,16 +131,21 @@ class User(AbstractUser, CommonBModel):
'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管') 'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管')
post = models.ForeignKey(Post, verbose_name='主要岗位', on_delete=models.SET_NULL, post = models.ForeignKey(Post, verbose_name='主要岗位', on_delete=models.SET_NULL,
null=True, blank=True) null=True, blank=True)
posts = models.ManyToManyField(Post, through='system.userpost', related_name='user_posts') posts = models.ManyToManyField(
Post, through='system.userpost', related_name='user_posts')
depts = models.ManyToManyField(Dept, through='system.userpost') depts = models.ManyToManyField(Dept, through='system.userpost')
roles = models.ManyToManyField(Role, verbose_name='关联角色') roles = models.ManyToManyField(Role, verbose_name='关联角色')
# 关联账号 # 关联账号
secret = models.CharField('密钥', max_length=100, null=True, blank=True) secret = models.CharField('密钥', max_length=100, null=True, blank=True)
wx_openid = models.CharField('微信公众号OpenId', max_length=100, null=True, blank=True) wx_openid = models.CharField(
wx_nickname = models.CharField('微信昵称', max_length=100, null=True, blank=True) '微信公众号OpenId', max_length=100, null=True, blank=True)
wx_headimg = models.CharField('微信头像', max_length=100, null=True, blank=True) wx_nickname = models.CharField(
wxmp_openid = models.CharField('微信小程序OpenId', max_length=100, null=True, blank=True) '微信昵称', max_length=100, null=True, blank=True)
wx_headimg = models.CharField(
'微信头像', max_length=100, null=True, blank=True)
wxmp_openid = models.CharField(
'微信小程序OpenId', max_length=100, null=True, blank=True)
objects = SoftDeletableUserManager() objects = SoftDeletableUserManager()
@ -154,9 +163,12 @@ class UserPost(BaseModel):
用户岗位关系表 用户岗位关系表
""" """
name = models.CharField('名称', max_length=20, null=True, blank=True) name = models.CharField('名称', max_length=20, null=True, blank=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='up_user') user = models.ForeignKey(
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='up_post') User, on_delete=models.CASCADE, related_name='up_user')
dept = models.ForeignKey(Dept, on_delete=models.CASCADE, related_name='up_dept') post = models.ForeignKey(
Post, on_delete=models.CASCADE, related_name='up_post')
dept = models.ForeignKey(
Dept, on_delete=models.CASCADE, related_name='up_dept')
sort = models.PositiveSmallIntegerField('排序', default=1) sort = models.PositiveSmallIntegerField('排序', default=1)
class Meta: class Meta:
@ -229,7 +241,8 @@ class File(CommonAModel):
(FILE_TYPE_OTHER, '其它') (FILE_TYPE_OTHER, '其它')
) )
mime = models.CharField('文件格式', max_length=120, null=True, blank=True) mime = models.CharField('文件格式', max_length=120, null=True, blank=True)
type = models.CharField('文件类型', max_length=50, choices=type_choices, default='文档') type = models.CharField('文件类型', max_length=50,
choices=type_choices, default='文档')
path = models.CharField('地址', max_length=200, null=True, blank=True) path = models.CharField('地址', max_length=200, null=True, blank=True)
class Meta: class Meta:
@ -250,5 +263,7 @@ class MySchedule(CommonAModel):
) )
name = models.CharField('名称', max_length=200) name = models.CharField('名称', max_length=200)
type = models.PositiveSmallIntegerField('周期类型', default=10) type = models.PositiveSmallIntegerField('周期类型', default=10)
interval = models.ForeignKey(IntervalSchedule, on_delete=models.PROTECT, null=True, blank=True) interval = models.ForeignKey(
crontab = models.ForeignKey(CrontabSchedule, on_delete=models.PROTECT, null=True, blank=True) IntervalSchedule, on_delete=models.PROTECT, null=True, blank=True)
crontab = models.ForeignKey(
CrontabSchedule, on_delete=models.PROTECT, null=True, blank=True)

View File

@ -1,6 +1,7 @@
import time import time
import django.utils.timezone as timezone import django.utils.timezone as timezone
from django.db import models from django.db import models
from django.db.models import Model
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from apps.utils.snowflake import idWorker from apps.utils.snowflake import idWorker
from django.db import IntegrityError from django.db import IntegrityError
@ -184,8 +185,17 @@ class CommonBDModel(BaseModel):
abstract = True abstract = True
# class Smslog(BaseModel): def get_model_info(cls_or_instance):
# """ """
# 短信发送记录表 返回类似 system.dept 的字符
# """ """
# phone = models.CharField('号码') if isinstance(cls_or_instance, Model):
# 是一个模型实例
app_label = cls_or_instance._meta.app_label
model_name = cls_or_instance._meta.model_name
else:
# 假定是一个模型类
app_label = cls_or_instance._meta.app_label
model_name = cls_or_instance._meta.model_name
return f'{app_label}.{model_name}'

View File

@ -9,10 +9,11 @@ import requests
from io import BytesIO from io import BytesIO
from rest_framework.serializers import ValidationError from rest_framework.serializers import ValidationError
def tran64(s): def tran64(s):
missing_padding = len(s) % 4 missing_padding = len(s) % 4
if missing_padding != 0: if missing_padding != 0:
s = s+'='* (4 - missing_padding) s = s+'=' * (4 - missing_padding)
return s return s
@ -202,4 +203,37 @@ def check_phone_e(phone):
re_phone = r'^1\d{10}$' re_phone = r'^1\d{10}$'
if not re.match(re_phone, phone): if not re.match(re_phone, phone):
raise ValidationError('手机号格式错误') raise ValidationError('手机号格式错误')
return phone return phone
def compare_dicts(dict1, dict2, ignore_order=False):
if ignore_order:
for key in sorted(dict1.keys()):
if key not in dict2 or not compare_values(dict1[key], dict2[key], ignore_order):
return False
return True
else:
return dict1 == dict2
def compare_lists_of_dicts(list1, list2, ignore_order=False):
"""比较两个列表,这里的列表包含字典(对象)"""
if ignore_order:
# 转换列表中的字典为元组列表,然后排序进行比较
sorted_list1 = sorted((tuple(sorted(d.items())) for d in list1))
sorted_list2 = sorted((tuple(sorted(d.items())) for d in list2))
return sorted_list1 == sorted_list2
else:
# 按顺序比较列表中的字典
return all(compare_dicts(obj1, obj2) for obj1, obj2 in zip(list1, list2))
def compare_values(val1, val2, ignore_order=False):
"""通用比较函数,也可以处理字典和列表。"""
if isinstance(val1, list) and isinstance(val2, list):
# 假设这里我们关心列表中对象的顺序
return compare_lists_of_dicts(val1, val2, ignore_order)
elif isinstance(val1, dict) and isinstance(val2, dict):
return compare_dicts(val1, val2, ignore_order)
else:
return val1 == val2

View File

@ -35,6 +35,8 @@ class WMaterialFilter(filters.FilterSet):
model = WMaterial model = WMaterial
fields = { fields = {
"material": ["exact", "in"], "material": ["exact", "in"],
"material__type": ["exact", "in"],
"material__process": ["exact", "in"],
"belong_dept": ["exact"], "belong_dept": ["exact"],
"belong_dept__name": ["exact"], "belong_dept__name": ["exact"],
"batch": ["exact"], "batch": ["exact"],
@ -50,9 +52,10 @@ class MlogFilter(filters.FilterSet):
"batch": ["exact"], "batch": ["exact"],
"handle_date": ["exact"], "handle_date": ["exact"],
"handle_user": ["exact"], "handle_user": ["exact"],
"mtask__mgroup__belong_dept__name": ["exact"], "mgroup": ["exact"],
"mgroup__belong_dept__name": ["exact", "in"], "mtask__mgroup__belong_dept__name": ["exact", "contains", "in"],
"mgroup__name": ["exact", "in"], "mgroup__belong_dept__name": ["exact", "in", "contains"],
"mgroup__name": ["exact", "in", "contains"],
"submit_time": ["isnull"] "submit_time": ["isnull"]
} }
@ -69,5 +72,7 @@ class HandoverFilter(filters.FilterSet):
"recive_dept__name": ["exact"], "recive_dept__name": ["exact"],
"send_date": ["exact"], "send_date": ["exact"],
"material__type": ["exact", "in"], "material__type": ["exact", "in"],
"submit_time": ["isnull"] "submit_time": ["isnull"],
"mlog": ["isnull"],
"send_mgroup": ["exact"]
} }

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.12 on 2023-11-29 08:51
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('mtm', '0025_auto_20231120_1139'),
('wpm', '0035_otherlog'),
]
operations = [
migrations.AddField(
model_name='handover',
name='send_mgroup',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='mtm.mgroup', verbose_name='送料工段'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 3.2.12 on 2023-11-30 02:47
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('mtm', '0025_auto_20231120_1139'),
('wpm', '0036_handover_send_mgroup'),
]
operations = [
migrations.AlterField(
model_name='handover',
name='material',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='物料'),
preserve_default=False,
),
migrations.AlterField(
model_name='handover',
name='send_user',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='handover_send_user', to='system.user', verbose_name='交送人'),
preserve_default=False,
),
]

View File

@ -129,9 +129,9 @@ class Mlog(CommonADModel):
handle_date = models.DateField('操作日期') handle_date = models.DateField('操作日期')
handle_user = models.ForeignKey( handle_user = models.ForeignKey(
User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True) User, verbose_name='操作人', on_delete=models.CASCADE, related_name='mlog_handle_user', null=True, blank=True) # 成型人
handle_user_2 = models.ForeignKey( handle_user_2 = models.ForeignKey(
User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True) User, verbose_name='操作人2', on_delete=models.CASCADE, related_name='mlog_handle_user_2', null=True, blank=True) # 切料人
handle_users = models.ManyToManyField( handle_users = models.ManyToManyField(
User, verbose_name='操作人(多选)', blank=True) User, verbose_name='操作人(多选)', blank=True)
handle_leader = models.ForeignKey( handle_leader = models.ForeignKey(
@ -163,12 +163,15 @@ class Handover(CommonADModel):
""" """
send_date = models.DateField('送料日期') send_date = models.DateField('送料日期')
send_user = models.ForeignKey( send_user = models.ForeignKey(
User, verbose_name='交送人', on_delete=models.CASCADE, related_name='handover_send_user', null=True, blank=True) User, verbose_name='交送人', on_delete=models.CASCADE, related_name='handover_send_user')
send_mgroup = models.ForeignKey(
Mgroup, verbose_name='送料工段', on_delete=models.CASCADE, null=True, blank=True
)
send_dept = models.ForeignKey( send_dept = models.ForeignKey(
Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept') Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept')
batch = models.CharField('批次号', max_length=50) batch = models.CharField('批次号', max_length=50)
material = models.ForeignKey( material = models.ForeignKey(
Material, verbose_name='物料', on_delete=models.CASCADE, null=True, blank=True) Material, verbose_name='物料', on_delete=models.CASCADE)
count = models.PositiveIntegerField('送料数', default=0) count = models.PositiveIntegerField('送料数', default=0)
count_eweight = models.FloatField('单数重量', default=0) count_eweight = models.FloatField('单数重量', default=0)
recive_dept = models.ForeignKey( recive_dept = models.ForeignKey(

View File

@ -223,6 +223,7 @@ class MlogSerializer(CustomModelSerializer):
validated_data['material_in'] = mtask.material_in validated_data['material_in'] = mtask.material_in
material_out = mtask.material_out material_out = mtask.material_out
validated_data['material_out'] = material_out validated_data['material_out'] = material_out
validated_data['handle_date'] = mtask.start_date
# if not WMaterial.objects.filter(batch=batch).exists(): # if not WMaterial.objects.filter(batch=batch).exists():
# raise ValidationError('批次号不存在') # raise ValidationError('批次号不存在')
else: else:
@ -298,13 +299,19 @@ class DeptBatchSerializer(serializers.Serializer):
class HandoverSerializer(CustomModelSerializer): class HandoverSerializer(CustomModelSerializer):
material = serializers.PrimaryKeyRelatedField(required=True, label='物料ID', queryset=Material.objects.all()) material = serializers.PrimaryKeyRelatedField(
required=True, label='物料ID', queryset=Material.objects.all())
send_user_name = serializers.CharField( send_user_name = serializers.CharField(
source='send_user.name', read_only=True) source='send_user.name', read_only=True)
recive_user_name = serializers.CharField( recive_user_name = serializers.CharField(
source='recive_user.name', read_only=True) source='recive_user.name', read_only=True)
material_ = MaterialSimpleSerializer(source='material', read_only=True) material_ = MaterialSimpleSerializer(source='material', read_only=True)
def validate(self, attrs):
if attrs.get('mlog', None):
attrs['send_mgroup'] = attrs['mlog'].mgroup
return super().validate(attrs)
class Meta: class Meta:
model = Handover model = Handover
fields = '__all__' fields = '__all__'

View File

@ -144,10 +144,12 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
return return
if now is None: if now is None:
now = timezone.now() now = timezone.now()
if now.date() < mlog.handle_date:
raise ParseError('不可提交未来的日志')
belong_dept = mlog.mgroup.belong_dept belong_dept = mlog.mgroup.belong_dept
material_out = mlog.material_out material_out = mlog.material_out
material_in = mlog.material_in material_in = mlog.material_in
if material_in and material_in.is_hidden is False: # 需要进行车间库存管理 if material_in: # 需要进行车间库存管理
# 需要判断领用数是否合理 # 需要判断领用数是否合理
material_has_qs = WMaterial.objects.filter( material_has_qs = WMaterial.objects.filter(
batch=mlog.batch, material=material_in, belong_dept=belong_dept) batch=mlog.batch, material=material_in, belong_dept=belong_dept)
@ -163,7 +165,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
else: else:
material_has.count = material_has.count - mlog.count_use material_has.count = material_has.count - mlog.count_use
material_has.save() material_has.save()
if material_out and material_out.is_hidden is False: # 需要入车间库存 if material_out: # 需要入车间库存
# 有多个产物的情况 # 有多个产物的情况
if material_out.brothers and Mlogb.objects.filter(mlog=mlog).exists(): if material_out.brothers and Mlogb.objects.filter(mlog=mlog).exists():
for item in Mlogb.objects.filter(mlog=mlog): for item in Mlogb.objects.filter(mlog=mlog):
@ -185,7 +187,7 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]):
def update_mtask(mtask: Mtask): def update_mtask(mtask: Mtask):
from apps.pm.models import Utask from apps.pm.models import Utask
res = Mlog.objects.filter(mtask=mtask).aggregate(sum_count_real=Sum( res = Mlog.objects.filter(mtask=mtask).exclude(submit_time=None).aggregate(sum_count_real=Sum(
'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok')) 'count_real'), sum_count_ok=Sum('count_ok'), sum_count_notok=Sum('count_notok'))
mtask.count_real = res['sum_count_real'] if res['sum_count_real'] else 0 mtask.count_real = res['sum_count_real'] if res['sum_count_real'] else 0
mtask.count_ok = res['sum_count_ok'] if res['sum_count_ok'] else 0 mtask.count_ok = res['sum_count_ok'] if res['sum_count_ok'] else 0
@ -200,8 +202,8 @@ def update_mtask(mtask: Mtask):
utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0 utask.count_notok = res2['sum_count_notok'] if res2['sum_count_notok'] else 0
if utask.count_ok > 0 and utask.state == Utask.UTASK_ASSGINED: if utask.count_ok > 0 and utask.state == Utask.UTASK_ASSGINED:
utask.state = Utask.UTASK_WORKING utask.state = Utask.UTASK_WORKING
if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_DONE).count() == 0: if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0:
utask.state = Mtask.MTASK_DONE utask.state = Utask.UTASK_SUBMIT
utask.save() utask.save()

View File

@ -19,6 +19,7 @@ from .serializers import (SflogExpSerializer, SfLogSerializer, StLogSerializer,
MlogSerializer, MlogRelatedSerializer, DeptBatchSerializer, HandoverSerializer, MlogSerializer, MlogRelatedSerializer, DeptBatchSerializer, HandoverSerializer,
GenHandoverSerializer, GenHandoverWmSerializer, MlogAnaSerializer, AttLogSerializer, OtherLogSerializer) GenHandoverSerializer, GenHandoverWmSerializer, MlogAnaSerializer, AttLogSerializer, OtherLogSerializer)
from .services import mlog_submit, update_mtask, handover_submit from .services import mlog_submit, update_mtask, handover_submit
from apps.monitor.services import create_auditlog, delete_auditlog
# Create your views here. # Create your views here.
@ -102,7 +103,7 @@ class WMaterialViewSet(ListModelMixin, CustomGenericViewSet):
perms_map = {'get': '*'} perms_map = {'get': '*'}
queryset = WMaterial.objects.all() queryset = WMaterial.objects.all()
serializer_class = WMaterialSerializer serializer_class = WMaterialSerializer
select_related_fields = ['material', 'belong_dept'] select_related_fields = ['material', 'belong_dept', 'material__process']
search_fields = ['material__name', search_fields = ['material__name',
'material__number', 'material__specification'] 'material__number', 'material__specification']
filterset_class = WMaterialFilter filterset_class = WMaterialFilter
@ -135,10 +136,26 @@ class MlogViewSet(CustomModelViewSet):
prefetch_related_fields = ['handle_users', 'material_outs', 'b_mlog'] prefetch_related_fields = ['handle_users', 'material_outs', 'b_mlog']
filterset_class = MlogFilter filterset_class = MlogFilter
@transaction.atomic
def perform_create(self, serializer):
ins = serializer.save()
data = MlogSerializer(ins).data
create_auditlog('create', ins, data)
@transaction.atomic
def perform_destroy(self, instance): def perform_destroy(self, instance):
if instance.submit_time is not None: if instance.submit_time is not None:
raise ParseError('日志已提交不可变动') raise ParseError('日志已提交不可变动')
return super().perform_destroy(instance) delete_auditlog(instance, instance.id)
instance.delete()
@transaction.atomic
def perform_update(self, serializer):
ins = serializer.instance
val_old = MlogSerializer(instance=ins).data
serializer.save()
val_new = MlogSerializer(instance=ins).data
create_auditlog('update', ins, val_new, val_old)
@action(methods=['post'], detail=True, perms_map={'post': 'mlog.submit'}, serializer_class=Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'mlog.submit'}, serializer_class=Serializer)
@transaction.atomic @transaction.atomic
@ -148,10 +165,13 @@ class MlogViewSet(CustomModelViewSet):
日志提交 日志提交
""" """
ins: Mlog = self.get_object() ins: Mlog = self.get_object()
vdata_old = MlogSerializer(ins).data
if ins.submit_time is None: if ins.submit_time is None:
mlog_submit(ins, self.request.user, None) mlog_submit(ins, self.request.user, None)
if ins.mtask: if ins.mtask:
update_mtask(ins.mtask) update_mtask(ins.mtask)
vdata_new = MlogSerializer(ins).data
create_auditlog('submit', ins, vdata_new, vdata_old)
return Response() return Response()
@action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MlogRelatedSerializer) @action(methods=['post'], detail=False, perms_map={'post': '*'}, serializer_class=MlogRelatedSerializer)
@ -241,7 +261,7 @@ class HandoverViewSet(CustomModelViewSet):
@action(methods=['post'], detail=True, perms_map={'post': 'handover.submit'}, serializer_class=Serializer) @action(methods=['post'], detail=True, perms_map={'post': 'handover.submit'}, serializer_class=Serializer)
@transaction.atomic @transaction.atomic
def submit(self, request): def submit(self, request, *args, **kwargs):
"""交接记录提交(变动车间库存) """交接记录提交(变动车间库存)
交接记录提交 交接记录提交
@ -283,7 +303,7 @@ class HandoverViewSet(CustomModelViewSet):
@action(methods=['post'], detail=False, perms_map={'post': 'handover.create'}, serializer_class=GenHandoverSerializer) @action(methods=['post'], detail=False, perms_map={'post': 'handover.create'}, serializer_class=GenHandoverSerializer)
@transaction.atomic @transaction.atomic
def gen_by_mlogs(self, request): def gen_by_mlog(self, request):
"""从生产日志生成交接记录 """从生产日志生成交接记录
从生产日志生成交接记录 从生产日志生成交接记录
@ -306,6 +326,7 @@ class HandoverViewSet(CustomModelViewSet):
count=mlog.count_real, count=mlog.count_real,
count_eweight=mlog.count_real_eweight, count_eweight=mlog.count_real_eweight,
mlog=mlog, mlog=mlog,
send_mgroup=mlog.mgroup,
create_by=user create_by=user
) )
return Response() return Response()

View File

@ -1,11 +1,11 @@
import os import os
from . import conf
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
app = Celery('ehs') app = Celery(conf.BASE_PROJECT_CODE)
# Using a string here means the worker doesn't have to serialize # Using a string here means the worker doesn't have to serialize
# the configuration object to child processes. # the configuration object to child processes.

View File

@ -70,7 +70,7 @@ DEBUG = conf.DEBUG
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
SYS_NAME = 'XT_EHS' SYS_NAME = 'XT_EHS'
SYS_VERSION = '2.2.2' SYS_VERSION = '2.3.0'
# Application definition # Application definition
@ -282,7 +282,7 @@ AUTHENTICATION_BACKENDS = (
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2", "LOCATION": conf.CACHE_LOCATION,
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "CLIENT_CLASS": "django_redis.client.DefaultClient",
} }
@ -290,7 +290,8 @@ CACHES = {
} }
# celery配置,celery正常运行必须安装redis # celery配置,celery正常运行必须安装redis
CELERY_BROKER_URL = "redis://127.0.0.1:6379/3" # 任务存储 CELERY_BROKER_URL = conf.CELERY_BROKER_URL # 任务存储
CELERY_TASK_DEFAULT_QUEUE = conf.CELERY_TASK_DEFAULT_QUEUE # 任务队列
CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁可防止内存泄露 CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁可防止内存泄露
CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区 CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区
CELERY_ENABLE_UTC = True # 启动时区设置 CELERY_ENABLE_UTC = True # 启动时区设置