diff --git a/apps/em/migrations/0009_alter_equipment_description.py b/apps/em/migrations/0009_alter_equipment_description.py new file mode 100644 index 00000000..90f4b9de --- /dev/null +++ b/apps/em/migrations/0009_alter_equipment_description.py @@ -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='描述'), + ), + ] diff --git a/apps/em/models.py b/apps/em/models.py index 27e3c1f6..df22f117 100644 --- a/apps/em/models.py +++ b/apps/em/models.py @@ -62,7 +62,8 @@ class Equipment(CommonBModel): count = models.PositiveIntegerField('数量', default=1) keeper = models.ForeignKey( 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) diff --git a/apps/inm/filters.py b/apps/inm/filters.py index 8fd8edd9..097b76e5 100644 --- a/apps/inm/filters.py +++ b/apps/inm/filters.py @@ -9,5 +9,7 @@ class MaterialBatchFilter(filters.FilterSet): fields = { "warehouse": ["exact"], "material": ["exact"], + "material__type": ["exact", "in"], + "material__process": ["exact", "in"], "count": ["exact", "gte", "lte"] } diff --git a/apps/inm/migrations/0011_rename_is_bgtest_ok_mioitem_is_testok.py b/apps/inm/migrations/0011_rename_is_bgtest_ok_mioitem_is_testok.py new file mode 100644 index 00000000..4f461d3e --- /dev/null +++ b/apps/inm/migrations/0011_rename_is_bgtest_ok_mioitem_is_testok.py @@ -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', + ), + ] diff --git a/apps/inm/models.py b/apps/inm/models.py index 0c5863f3..c82ad6e0 100644 --- a/apps/inm/models.py +++ b/apps/inm/models.py @@ -121,7 +121,7 @@ class MIOItem(BaseModel): count_n_jsqx = 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): diff --git a/apps/inm/serializers.py b/apps/inm/serializers.py index c2777890..6d35ae64 100644 --- a/apps/inm/serializers.py +++ b/apps/inm/serializers.py @@ -94,7 +94,7 @@ class MIOItemCreateSerializer(CustomModelSerializer): class Meta: model = MIOItem fields = ['mio', 'warehouse', 'material', - 'batch', 'count', 'assemb', 'is_bgtest_ok'] + 'batch', 'count', 'assemb', 'is_testok'] def create(self, validated_data): mio = validated_data['mio'] diff --git a/apps/inm/views.py b/apps/inm/views.py index ff055cf4..d0633b2a 100644 --- a/apps/inm/views.py +++ b/apps/inm/views.py @@ -49,7 +49,8 @@ class MaterialBatchViewSet(ListModelMixin, CustomGenericViewSet): retrieve_serializer_class = MaterialBatchDetailSerializer select_related_fields = ['warehouse', 'material'] filterset_class = MaterialBatchFilter - search_fields = ['material__name'] + search_fields = ['material__name', 'material__number', + 'material__model', 'material__specification', 'batch'] class MioDoViewSet(BulkCreateModelMixin, BulkUpdateModelMixin, CustomGenericViewSet): @@ -123,7 +124,12 @@ class MIOViewSet(CustomModelViewSet): 'submit_user', 'supplier', 'order', 'customer', 'pu_order'] serializer_class = MIOListSerializer 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'] data_filter = True diff --git a/apps/monitor/migrations/0004_auditlog.py b/apps/monitor/migrations/0004_auditlog.py new file mode 100644 index 00000000..41402798 --- /dev/null +++ b/apps/monitor/migrations/0004_auditlog.py @@ -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='操作人')), + ], + ), + ] diff --git a/apps/monitor/models.py b/apps/monitor/models.py index d20c77dc..4bf3b475 100755 --- a/apps/monitor/models.py +++ b/apps/monitor/models.py @@ -4,6 +4,19 @@ from django.db import models 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): """Logs Django rest framework API requests""" @@ -42,7 +55,8 @@ class DrfRequestLog(BaseModel): response = models.TextField(null=True, blank=True) errors = 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: verbose_name = "DRF请求日志" diff --git a/apps/monitor/serializers.py b/apps/monitor/serializers.py index 4a79c5aa..20f3ba53 100644 --- a/apps/monitor/serializers.py +++ b/apps/monitor/serializers.py @@ -1,4 +1,17 @@ from rest_framework import serializers +from apps.utils.serializers import CustomModelSerializer +from apps.monitor.models import AuditLog + class DbbackupDeleteSerializer(serializers.Serializer): - filepaths = serializers.ListField(child=serializers.CharField(), label="文件地址列表") \ No newline at end of file + 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__' diff --git a/apps/monitor/services.py b/apps/monitor/services.py index 3c0a1dad..d1c83ff1 100644 --- a/apps/monitor/services.py +++ b/apps/monitor/services.py @@ -1,4 +1,63 @@ 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: @classmethod @@ -17,7 +76,7 @@ class ServerService: ret['count'] = psutil.cpu_count(logical=False) ret['percent'] = psutil.cpu_percent(interval=1) return ret - + @classmethod def get_disk_dict(cls): ret = {} @@ -29,4 +88,4 @@ class ServerService: @classmethod def get_full(cls): - return {'cpu': cls.get_cpu_dict(), 'memory': cls.get_memory_dict(), 'disk': cls.get_disk_dict()} \ No newline at end of file + return {'cpu': cls.get_cpu_dict(), 'memory': cls.get_memory_dict(), 'disk': cls.get_disk_dict()} diff --git a/apps/monitor/urls.py b/apps/monitor/urls.py index f960a32c..66c88138 100755 --- a/apps/monitor/urls.py +++ b/apps/monitor/urls.py @@ -1,5 +1,5 @@ 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/' HTML_BASE_URL = 'monitor/' @@ -13,5 +13,8 @@ urlpatterns = [ path(API_BASE_URL + 'log//', LogDetailView.as_view()), path(API_BASE_URL + 'dbbackup/', DbBackupView.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') ] diff --git a/apps/monitor/views.py b/apps/monitor/views.py index da476795..1ed2a2fa 100755 --- a/apps/monitor/views.py +++ b/apps/monitor/views.py @@ -7,13 +7,13 @@ from rest_framework.permissions import IsAuthenticated from django.conf import settings import os 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.utils import swagger_auto_schema from rest_framework.exceptions import NotFound from rest_framework.mixins import ListModelMixin 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.services import ServerService @@ -188,3 +188,16 @@ class DrfRequestLogViewSet(ListModelMixin, CustomGenericViewSet): ordering = ['-requested_at'] filterset_class = DrfLogFilterSet 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'] diff --git a/apps/mtm/filters.py b/apps/mtm/filters.py index ef51633a..39b9749a 100644 --- a/apps/mtm/filters.py +++ b/apps/mtm/filters.py @@ -15,6 +15,7 @@ class MaterialFilter(filters.FilterSet): "is_hidden": ["exact"], "is_assemb": ["exact"], "need_route": ["exact"], + "process": ["exact", "in", "isnull"], "orderitem_material__order": ['exact'], "pu_orderitem_material__pu_order": ["exact"], "route_material_out__mgroup": ["exact"] @@ -48,4 +49,7 @@ class RouteFilter(filters.FilterSet): "process": ["exact", "in"], "is_autotask": ["exact"], "mgroup": ["exact", "in", "isnull"], + "mgroup__name": ["exact", "contains"], + "mgroup__belong_dept": ["exact"], + "mgroup__belong_dept__name": ["exact", "contains"] } diff --git a/apps/mtm/serializers.py b/apps/mtm/serializers.py index a783f6a7..7ad7228e 100644 --- a/apps/mtm/serializers.py +++ b/apps/mtm/serializers.py @@ -15,10 +15,12 @@ class ShiftSerializer(CustomModelSerializer): class MaterialSimpleSerializer(CustomModelSerializer): + process_name = serializers.CharField(source='process.name', read_only=True) + class Meta: model = Material fields = ['id', 'name', 'number', 'model', - 'specification', 'type', 'cate', 'brothers'] + 'specification', 'type', 'cate', 'brothers', 'process_name'] class MaterialSerializer(CustomModelSerializer): @@ -124,15 +126,16 @@ class RouteSerializer(CustomModelSerializer): fields = '__all__' read_only_fields = EXCLUDE_FIELDS - # def validate(self, attrs): - # material = attrs['material'] - # if material.type != Material.MA_TYPE_GOOD: - # raise ValidationError('请选择最终产品') - # return super().validate(attrs) + def validate(self, attrs): + if 'mgroup' in attrs and attrs['mgroup']: + attrs['process'] = attrs['mgroup'].process + if attrs.get('process', None) is None: + raise ParseError('未提供操作工序') + return super().validate(attrs) def gen_material_out(self, instance): """ - 废弃不用了 + 自动形成物料 """ name = f'{instance.material.name}-中' 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, 'number': instance.material.number, 'specification': instance.material.specification, + 'model': instance.material.model, 'type': Material.MA_TYPE_HALFGOOD, 'create_by': self.request.user, 'update_by': self.request.user, @@ -147,24 +151,41 @@ class RouteSerializer(CustomModelSerializer): instance.save() def create(self, validated_data): - process = validated_data.get('process', None) - if process and Route.objects.filter(material=validated_data['material'], process=process).exists(): + process = validated_data['process'] + material = validated_data.get('material', None) + if material and process and Route.objects.filter(material=material, process=process).exists(): raise ValidationError('已选择该工序') with transaction.atomic(): instance = super().create(validated_data) - # if 'material_out' in validated_data and validated_data['material_out'] and instance.material: - # pass - # else: - # self.gen_material_out(instance) + material_out = instance.material_out + if material_out: + if material_out.process is None: + 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 def update(self, instance, validated_data): validated_data.pop('material', None) - validated_data.pop('process', None) + process = validated_data.pop('process', None) with transaction.atomic(): instance = super().update(instance, validated_data) - # if 'material_out' in validated_data and validated_data['material_out'] and instance.material: - # pass - # else: - # self.gen_material_out(instance) + material_out = instance.material_out + if material_out: + if material_out.process is None: + 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 diff --git a/apps/mtm/views.py b/apps/mtm/views.py index 62f47fd6..2de4cdf7 100644 --- a/apps/mtm/views.py +++ b/apps/mtm/views.py @@ -51,7 +51,13 @@ class MgroupViewSet(CustomModelViewSet): queryset = Mgroup.objects.all() serializer_class = MgroupSerializer 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'] ordering = ['sort', 'create_time'] diff --git a/apps/pm/filters.py b/apps/pm/filters.py index 373d3ab5..9a0bd81a 100644 --- a/apps/pm/filters.py +++ b/apps/pm/filters.py @@ -38,7 +38,7 @@ class UtaskFilter(filters.FilterSet): class MtaskFilter(filters.FilterSet): - tag = filters.CharFilter(method='filter_tag') + tag = filters.CharFilter(method='filter_tag', label='done, not_done') class Meta: model = Mtask @@ -55,17 +55,22 @@ class MtaskFilter(filters.FilterSet): "material_out__type": ["exact"], "material_out__is_hidden": ["exact"], "mgroup__belong_dept__name": ["exact"], + "mgroup__belong_dept": ["exact"], "utask": ["exact"] } def filter_tag(self, queryset, name, value): - now = timezone.now() - day7_after = now + timedelta(days=7) - if value == 'near_done': - queryset = queryset.filter(count_ok__lt=F('count'), - end_date__lte=day7_after.date(), - end_date__gte=now.date()) - elif value == 'out_done': - queryset = queryset.filter(count_ok__lt=F('count'), - end_date__lt=now.date()) + # now = timezone.now() + # day7_after = now + timedelta(days=7) + # if value == 'near_done': + # queryset = queryset.filter(count_ok__lt=F('count'), + # end_date__lte=day7_after.date(), + # end_date__gte=now.date()) + # elif value == 'out_done': + # queryset = queryset.filter(count_ok__lt=F('count'), + # 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 diff --git a/apps/pm/migrations/0016_auto_20231130_1628.py b/apps/pm/migrations/0016_auto_20231130_1628.py new file mode 100644 index 00000000..fa8412a4 --- /dev/null +++ b/apps/pm/migrations/0016_auto_20231130_1628.py @@ -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='提交人'), + ), + ] diff --git a/apps/pm/migrations/0017_auto_20231205_1724.py b/apps/pm/migrations/0017_auto_20231205_1724.py new file mode 100644 index 00000000..6cd1a9ca --- /dev/null +++ b/apps/pm/migrations/0017_auto_20231205_1724.py @@ -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='任务类型'), + ), + ] diff --git a/apps/pm/models.py b/apps/pm/models.py index 770ffca2..b518284a 100644 --- a/apps/pm/models.py +++ b/apps/pm/models.py @@ -4,6 +4,11 @@ from apps.mtm.models import Material, Mgroup # Create your models here. +TASK_TYPE = ( + ('mass', '量产'), + ('pilot', '中试') +) + class Utask(CommonBDModel): """ @@ -14,15 +19,17 @@ class Utask(CommonBDModel): UTASK_ASSGINED = 20 UTASK_WORKING = 30 UTASK_STOP = 34 - UTASK_DONE = 40 + UTASK_SUBMIT = 40 UTASK_STATES = ( (UTASK_CREATED, '创建中'), (UTASK_DECOMPOSE, '已分解'), (UTASK_ASSGINED, '已下达'), (UTASK_WORKING, '生产中'), (UTASK_STOP, '已停止'), - (UTASK_DONE, '已提交') + (UTASK_SUBMIT, '已提交') ) + type = models.CharField('任务类型', max_length=10, + help_text=str(TASK_TYPE), default='mass') state = models.PositiveIntegerField( '状态', choices=UTASK_STATES, default=UTASK_CREATED, help_text=str(UTASK_STATES)) number = models.CharField('编号', max_length=50, unique=True) @@ -48,13 +55,15 @@ class Mtask(CommonADModel): MTASK_CREATED = 10 MTASK_ASSGINED = 20 MTASK_STOP = 34 - MTASK_DONE = 40 + MTASK_SUBMIT = 40 MTASK_STATES = ( (MTASK_CREATED, '创建中'), (MTASK_ASSGINED, '已下达'), (MTASK_STOP, '已停止'), - (MTASK_DONE, '已提交') + (MTASK_SUBMIT, '已提交') ) + type = models.CharField('任务类型', max_length=10, + help_text=str(TASK_TYPE), default='mass') state = models.PositiveIntegerField( '状态', choices=MTASK_STATES, default=MTASK_CREATED, help_text=str(MTASK_STATES)) 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) 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 def related(self): """ diff --git a/apps/pm/serializers.py b/apps/pm/serializers.py index 3c09d1de..5417f722 100644 --- a/apps/pm/serializers.py +++ b/apps/pm/serializers.py @@ -12,6 +12,7 @@ from apps.wpm.models import Mlog class UtaskSerializer(CustomModelSerializer): material_ = MaterialSimpleSerializer(source='material', read_only=True) + mgroup_name = serializers.CharField(source='mgroup.name', read_only=True) belong_dept = serializers.PrimaryKeyRelatedField( queryset=Dept.objects.all(), required=False) diff --git a/apps/pm/services.py b/apps/pm/services.py index e96e9512..16a6af8e 100644 --- a/apps/pm/services.py +++ b/apps/pm/services.py @@ -71,6 +71,7 @@ class PmService: for i in range(rela_days): task_date = start_date + timedelta(days=i) Mtask.objects.create(**{ + 'type': utask.type, 'number': f'{number}_{i+1}', 'material_out': utask.material, 'material_in': utask.material_in, @@ -124,6 +125,7 @@ class PmService: task_date = start_date + timedelta(days=i) Mtask.objects.create(**{ 'number': f'{number}_r{ind+1}_{i+1}', + 'type': utask.type, 'material_out': halfgood, 'material_in': material_in, 'mgroup': mgroup, @@ -251,19 +253,23 @@ class PmService: change_order_state_when_schedue(orderitemIds) @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.services import mlog_submit, update_mtask now = timezone.now() - for mtask in mtasks: + if mtask.state == Mtask.MTASK_ASSGINED: mlogs = Mlog.objects.filter(mtask=mtask) if mlogs.count() == 0: raise ParseError(f'{mtask.mgroup.name}_未填写日志') for mlog in mlogs: mlog_submit(mlog, user, now) update_mtask(mtask) - mtask.state = Mtask.MTASK_DONE + mtask.state = Mtask.MTASK_SUBMIT + mtask.submit_time = now + mtask.submit_user = user mtask.save() + else: + raise ParseError('该任务状态不可提交') diff --git a/apps/pm/views.py b/apps/pm/views.py index efba844f..0725254f 100644 --- a/apps/pm/views.py +++ b/apps/pm/views.py @@ -158,18 +158,16 @@ class MtaskViewSet(CustomModelViewSet): raise ParseError('该任务非创建中不可删除') 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 def submit(self, request, *args, **kwargs): """提交任务(根据任务ID) 提交任务后不可更新日志 """ - ids = request.data.get('ids', []) + mtask: Mtask = self.get_object() user = request.user - mtasks = Mtask.objects.filter( - id__in=ids, state=Mtask.MTASK_ASSGINED) - PmService.mtasks_submit(mtasks, user) + PmService.mtask_submit(mtask, user) return Response() @action(methods=['post'], detail=True, perms_map={'post': 'mtask.submit'}, serializer_class=Serializer) @@ -181,5 +179,6 @@ class MtaskViewSet(CustomModelViewSet): """ mtask = self.get_object() mtasks = mtask.related - PmService.mtasks_submit(mtasks) + for mtask in mtasks: + PmService.mtask_submit(mtask, self.request.user) return Response() diff --git a/apps/pum/migrations/0003_puorder_materials.py b/apps/pum/migrations/0003_puorder_materials.py new file mode 100644 index 00000000..52ece2f2 --- /dev/null +++ b/apps/pum/migrations/0003_puorder_materials.py @@ -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='多个物料'), + ), + ] diff --git a/apps/pum/migrations/0004_auto_20231201_1350.py b/apps/pum/migrations/0004_auto_20231201_1350.py new file mode 100644 index 00000000..dad3a593 --- /dev/null +++ b/apps/pum/migrations/0004_auto_20231201_1350.py @@ -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='提交人'), + ), + ] diff --git a/apps/pum/models.py b/apps/pum/models.py index 9eae5ccf..7d6886e7 100644 --- a/apps/pum/models.py +++ b/apps/pum/models.py @@ -35,6 +35,8 @@ class PuPlan(CommonBModel): number = models.CharField('编号', max_length=20) name = models.CharField('名称', max_length=50, 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): @@ -58,6 +60,10 @@ class PuOrder(CommonBModel): Supplier, verbose_name='供应商', on_delete=models.CASCADE) delivery_date = models.DateField('截止到货日期', 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): diff --git a/apps/pum/serializers.py b/apps/pum/serializers.py index 57b5fa93..e6e62a3a 100644 --- a/apps/pum/serializers.py +++ b/apps/pum/serializers.py @@ -4,7 +4,7 @@ from apps.utils.constants import EXCLUDE_FIELDS_DEPT, EXCLUDE_FIELDS_BASE, EXCLU from rest_framework.exceptions import ValidationError 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): @@ -83,6 +83,8 @@ class PuOrderSerializer(CustomModelSerializer): source='create_by.name', read_only=True) update_by_name = serializers.CharField( source='update_by.name', read_only=True) + materials_ = MaterialSimpleSerializer( + source='materials', many=True, read_only=True) class Meta: model = PuOrder diff --git a/apps/pum/views.py b/apps/pum/views.py index fb479e69..4eaec077 100644 --- a/apps/pum/views.py +++ b/apps/pum/views.py @@ -46,7 +46,7 @@ class PuPlanViewSet(CustomModelViewSet): raise ParseError('该计划存在明细不可删除') 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): """提交采购计划 @@ -59,6 +59,7 @@ class PuPlanViewSet(CustomModelViewSet): if puplan.state != PuPlan.PUPLAN_CREATE: raise ParseError('采购计划状态异常') puplan.submit_time = timezone.now() + puplan.submit_user = user puplan.state = PuPlan.PUPLAN_SUBMITED puplan.save() return Response() @@ -106,7 +107,7 @@ class PuOrderViewSet(CustomModelViewSet): raise ParseError('采购订单非创建中不可删除') 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 def submit(self, request, *args, **kwargs): """提交采购订单 @@ -122,6 +123,7 @@ class PuOrderViewSet(CustomModelViewSet): if puorder.state != PuOrder.PUORDER_CREATE: raise ParseError('采购计划状态异常') puorder.submit_time = timezone.now() + puorder.submit_user = user puorder.state = PuOrder.PUORDER_SUBMITED puorder.save() PumService.change_puplan_state_when_puorder_sumbit(puorder) diff --git a/apps/sam/views.py b/apps/sam/views.py index e10abdc9..e7cab13e 100644 --- a/apps/sam/views.py +++ b/apps/sam/views.py @@ -60,6 +60,13 @@ class OrderViewSet(CustomModelViewSet): search_fields = ['number'] 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) @transaction.atomic def submit(self, request, *args, **kwargs): diff --git a/apps/system/migrations/0003_alter_permission_parent.py b/apps/system/migrations/0003_alter_permission_parent.py new file mode 100644 index 00000000..ca646f27 --- /dev/null +++ b/apps/system/migrations/0003_alter_permission_parent.py @@ -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='父'), + ), + ] diff --git a/apps/system/models.py b/apps/system/models.py index e7e34e6e..155d2936 100755 --- a/apps/system/models.py +++ b/apps/system/models.py @@ -26,10 +26,11 @@ class Permission(BaseModel): (PERM_TYPE_BUTTON, '按钮') ) 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) 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) def __str__(self): @@ -67,7 +68,8 @@ class Role(CommonADModel): """ name = models.CharField('名称', max_length=32) 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) class Meta: @@ -106,8 +108,10 @@ class PostRole(BaseModel): """ data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices, default=DataFilter.THISLEVEL_AND_BELOW) - post = models.ForeignKey(Post, verbose_name='关联岗位', on_delete=models.CASCADE) - role = models.ForeignKey(Role, verbose_name='关联角色', on_delete=models.CASCADE) + post = models.ForeignKey(Post, verbose_name='关联岗位', + on_delete=models.CASCADE) + role = models.ForeignKey(Role, verbose_name='关联角色', + on_delete=models.CASCADE) class SoftDeletableUserManager(SoftDeletableManagerMixin, UserManager): @@ -127,16 +131,21 @@ class User(AbstractUser, CommonBModel): 'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管') post = models.ForeignKey(Post, verbose_name='主要岗位', on_delete=models.SET_NULL, 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') roles = models.ManyToManyField(Role, verbose_name='关联角色') # 关联账号 secret = models.CharField('密钥', max_length=100, null=True, blank=True) - wx_openid = models.CharField('微信公众号OpenId', max_length=100, null=True, blank=True) - wx_nickname = models.CharField('微信昵称', 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) + wx_openid = models.CharField( + '微信公众号OpenId', max_length=100, null=True, blank=True) + wx_nickname = models.CharField( + '微信昵称', 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() @@ -154,9 +163,12 @@ class UserPost(BaseModel): 用户岗位关系表 """ name = models.CharField('名称', max_length=20, null=True, blank=True) - user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='up_user') - post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='up_post') - dept = models.ForeignKey(Dept, on_delete=models.CASCADE, related_name='up_dept') + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name='up_user') + 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) class Meta: @@ -229,7 +241,8 @@ class File(CommonAModel): (FILE_TYPE_OTHER, '其它') ) 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) class Meta: @@ -250,5 +263,7 @@ class MySchedule(CommonAModel): ) name = models.CharField('名称', max_length=200) type = models.PositiveSmallIntegerField('周期类型', default=10) - interval = models.ForeignKey(IntervalSchedule, on_delete=models.PROTECT, null=True, blank=True) - crontab = models.ForeignKey(CrontabSchedule, on_delete=models.PROTECT, null=True, blank=True) \ No newline at end of file + interval = models.ForeignKey( + IntervalSchedule, on_delete=models.PROTECT, null=True, blank=True) + crontab = models.ForeignKey( + CrontabSchedule, on_delete=models.PROTECT, null=True, blank=True) diff --git a/apps/utils/models.py b/apps/utils/models.py index a23f447a..3e724bab 100755 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -1,6 +1,7 @@ import time import django.utils.timezone as timezone from django.db import models +from django.db.models import Model from django.db.models.query import QuerySet from apps.utils.snowflake import idWorker from django.db import IntegrityError @@ -184,8 +185,17 @@ class CommonBDModel(BaseModel): abstract = True -# class Smslog(BaseModel): -# """ -# 短信发送记录表 -# """ -# phone = models.CharField('号码') +def get_model_info(cls_or_instance): + """ + 返回类似 system.dept 的字符 + """ + 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}' diff --git a/apps/utils/tools.py b/apps/utils/tools.py index 18dc6661..7033a83d 100755 --- a/apps/utils/tools.py +++ b/apps/utils/tools.py @@ -9,10 +9,11 @@ import requests from io import BytesIO from rest_framework.serializers import ValidationError + def tran64(s): missing_padding = len(s) % 4 if missing_padding != 0: - s = s+'='* (4 - missing_padding) + s = s+'=' * (4 - missing_padding) return s @@ -202,4 +203,37 @@ def check_phone_e(phone): re_phone = r'^1\d{10}$' if not re.match(re_phone, phone): raise ValidationError('手机号格式错误') - return phone \ No newline at end of file + 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 diff --git a/apps/wpm/filters.py b/apps/wpm/filters.py index 46952252..b1818d93 100644 --- a/apps/wpm/filters.py +++ b/apps/wpm/filters.py @@ -35,6 +35,8 @@ class WMaterialFilter(filters.FilterSet): model = WMaterial fields = { "material": ["exact", "in"], + "material__type": ["exact", "in"], + "material__process": ["exact", "in"], "belong_dept": ["exact"], "belong_dept__name": ["exact"], "batch": ["exact"], @@ -50,9 +52,10 @@ class MlogFilter(filters.FilterSet): "batch": ["exact"], "handle_date": ["exact"], "handle_user": ["exact"], - "mtask__mgroup__belong_dept__name": ["exact"], - "mgroup__belong_dept__name": ["exact", "in"], - "mgroup__name": ["exact", "in"], + "mgroup": ["exact"], + "mtask__mgroup__belong_dept__name": ["exact", "contains", "in"], + "mgroup__belong_dept__name": ["exact", "in", "contains"], + "mgroup__name": ["exact", "in", "contains"], "submit_time": ["isnull"] } @@ -69,5 +72,7 @@ class HandoverFilter(filters.FilterSet): "recive_dept__name": ["exact"], "send_date": ["exact"], "material__type": ["exact", "in"], - "submit_time": ["isnull"] + "submit_time": ["isnull"], + "mlog": ["isnull"], + "send_mgroup": ["exact"] } diff --git a/apps/wpm/migrations/0036_handover_send_mgroup.py b/apps/wpm/migrations/0036_handover_send_mgroup.py new file mode 100644 index 00000000..6a1d51a3 --- /dev/null +++ b/apps/wpm/migrations/0036_handover_send_mgroup.py @@ -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='送料工段'), + ), + ] diff --git a/apps/wpm/migrations/0037_auto_20231130_1047.py b/apps/wpm/migrations/0037_auto_20231130_1047.py new file mode 100644 index 00000000..9fdad285 --- /dev/null +++ b/apps/wpm/migrations/0037_auto_20231130_1047.py @@ -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, + ), + ] diff --git a/apps/wpm/models.py b/apps/wpm/models.py index 42c14bc6..c535fe2e 100644 --- a/apps/wpm/models.py +++ b/apps/wpm/models.py @@ -129,9 +129,9 @@ class Mlog(CommonADModel): handle_date = models.DateField('操作日期') 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( - 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( User, verbose_name='操作人(多选)', blank=True) handle_leader = models.ForeignKey( @@ -163,12 +163,15 @@ class Handover(CommonADModel): """ send_date = models.DateField('送料日期') 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( Dept, verbose_name='送料部门', on_delete=models.CASCADE, related_name='handover_send_dept') batch = models.CharField('批次号', max_length=50) 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_eweight = models.FloatField('单数重量', default=0) recive_dept = models.ForeignKey( diff --git a/apps/wpm/serializers.py b/apps/wpm/serializers.py index 6e51b876..375739d4 100644 --- a/apps/wpm/serializers.py +++ b/apps/wpm/serializers.py @@ -223,6 +223,7 @@ class MlogSerializer(CustomModelSerializer): validated_data['material_in'] = mtask.material_in material_out = mtask.material_out validated_data['material_out'] = material_out + validated_data['handle_date'] = mtask.start_date # if not WMaterial.objects.filter(batch=batch).exists(): # raise ValidationError('批次号不存在') else: @@ -298,13 +299,19 @@ class DeptBatchSerializer(serializers.Serializer): 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( source='send_user.name', read_only=True) recive_user_name = serializers.CharField( source='recive_user.name', 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: model = Handover fields = '__all__' diff --git a/apps/wpm/services.py b/apps/wpm/services.py index a09eeed8..b5c4f462 100644 --- a/apps/wpm/services.py +++ b/apps/wpm/services.py @@ -144,10 +144,12 @@ def mlog_submit(mlog: Mlog, user: User, now: Union[datetime.datetime, None]): return if now is None: now = timezone.now() + if now.date() < mlog.handle_date: + raise ParseError('不可提交未来的日志') belong_dept = mlog.mgroup.belong_dept material_out = mlog.material_out material_in = mlog.material_in - if material_in and material_in.is_hidden is False: # 需要进行车间库存管理 + if material_in: # 需要进行车间库存管理 # 需要判断领用数是否合理 material_has_qs = WMaterial.objects.filter( 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: material_has.count = material_has.count - mlog.count_use 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(): 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): 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')) 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 @@ -200,8 +202,8 @@ def update_mtask(mtask: Mtask): 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: utask.state = Utask.UTASK_WORKING - if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_DONE).count() == 0: - utask.state = Mtask.MTASK_DONE + if Mtask.objects.filter(utask=utask).exclude(state=Mtask.MTASK_SUBMIT).count() == 0: + utask.state = Utask.UTASK_SUBMIT utask.save() diff --git a/apps/wpm/views.py b/apps/wpm/views.py index 094781ff..4dbe0a50 100644 --- a/apps/wpm/views.py +++ b/apps/wpm/views.py @@ -19,6 +19,7 @@ from .serializers import (SflogExpSerializer, SfLogSerializer, StLogSerializer, MlogSerializer, MlogRelatedSerializer, DeptBatchSerializer, HandoverSerializer, GenHandoverSerializer, GenHandoverWmSerializer, MlogAnaSerializer, AttLogSerializer, OtherLogSerializer) from .services import mlog_submit, update_mtask, handover_submit +from apps.monitor.services import create_auditlog, delete_auditlog # Create your views here. @@ -102,7 +103,7 @@ class WMaterialViewSet(ListModelMixin, CustomGenericViewSet): perms_map = {'get': '*'} queryset = WMaterial.objects.all() serializer_class = WMaterialSerializer - select_related_fields = ['material', 'belong_dept'] + select_related_fields = ['material', 'belong_dept', 'material__process'] search_fields = ['material__name', 'material__number', 'material__specification'] filterset_class = WMaterialFilter @@ -135,10 +136,26 @@ class MlogViewSet(CustomModelViewSet): prefetch_related_fields = ['handle_users', 'material_outs', 'b_mlog'] 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): if instance.submit_time is not None: 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) @transaction.atomic @@ -148,10 +165,13 @@ class MlogViewSet(CustomModelViewSet): 日志提交 """ ins: Mlog = self.get_object() + vdata_old = MlogSerializer(ins).data if ins.submit_time is None: mlog_submit(ins, self.request.user, None) if ins.mtask: update_mtask(ins.mtask) + vdata_new = MlogSerializer(ins).data + create_auditlog('submit', ins, vdata_new, vdata_old) return Response() @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) @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) @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_eweight=mlog.count_real_eweight, mlog=mlog, + send_mgroup=mlog.mgroup, create_by=user ) return Response() diff --git a/server/celery.py b/server/celery.py index 59449c0d..747a08bb 100755 --- a/server/celery.py +++ b/server/celery.py @@ -1,11 +1,11 @@ import os - +from . import conf from celery import Celery # set the default Django settings module for the 'celery' program. 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 # the configuration object to child processes. diff --git a/server/settings.py b/server/settings.py index ecdb0f76..0aed35ec 100755 --- a/server/settings.py +++ b/server/settings.py @@ -70,7 +70,7 @@ DEBUG = conf.DEBUG ALLOWED_HOSTS = ['*'] SYS_NAME = 'XT_EHS' -SYS_VERSION = '2.2.2' +SYS_VERSION = '2.3.0' # Application definition @@ -282,7 +282,7 @@ AUTHENTICATION_BACKENDS = ( CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": "redis://127.0.0.1:6379/2", + "LOCATION": conf.CACHE_LOCATION, "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } @@ -290,7 +290,8 @@ CACHES = { } # 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个任务就会被销毁,可防止内存泄露 CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区 CELERY_ENABLE_UTC = True # 启动时区设置