diff --git a/hb_client/.env.production b/hb_client/.env.production index 8994f69..484e806 100644 --- a/hb_client/.env.production +++ b/hb_client/.env.production @@ -2,5 +2,5 @@ ENV = 'production' # base api -VUE_APP_BASE_API = '' +VUE_APP_BASE_API = 'http://47.95.0.242:2222/api' diff --git a/hb_client/package.json b/hb_client/package.json index 7ff3a3c..1c2f90c 100644 --- a/hb_client/package.json +++ b/hb_client/package.json @@ -17,46 +17,45 @@ }, "dependencies": { "@riophae/vue-treeselect": "^0.4.0", - "axios": "0.18.1", + "axios": "^0.21.1", "element-ui": "^2.15.5", "file-saver": "^2.0.2", - "js-cookie": "2.2.0", - "normalize.css": "7.0.0", + "js-cookie": "^3.0.0", + "normalize.css": "^8.0.1", "nprogress": "0.2.0", - "path-to-regexp": "2.4.0", - "vue": "2.6.10", - "vue-json-editor": "^1.4.3", - "vue-router": "3.0.6", - "vuex": "3.1.0", - "xlsx": "^0.15.5" + "path-to-regexp": "^6.2.0", + "vue": "^2.6.14", + "vue-router": "^3.5.2", + "vuex": "^3.6.2", + "xlsx": "^0.17.1" }, "devDependencies": { - "@babel/core": "7.0.0", - "@babel/register": "7.0.0", + "@babel/core": "^7.15.0", + "@babel/register": "^7.15.3", "@vue/cli-plugin-babel": "3.6.0", "@vue/cli-plugin-eslint": "^3.9.1", "@vue/cli-plugin-unit-jest": "3.6.3", "@vue/cli-service": "3.6.0", - "@vue/test-utils": "1.0.0-beta.29", + "@vue/test-utils": "^1.2.2", "autoprefixer": "^9.5.1", "babel-core": "7.0.0-bridge.0", - "babel-eslint": "10.0.1", + "babel-eslint": "^10.1.0", "babel-jest": "23.6.0", "chalk": "2.4.2", - "connect": "3.6.6", + "connect": "^3.7.0", "eslint": "5.15.3", "eslint-plugin-vue": "5.2.2", "html-webpack-plugin": "3.2.0", - "mockjs": "1.0.1-beta3", + "mockjs": "^1.1.0", "node-sass": "^4.13.1", "runjs": "^4.3.2", "sass-loader": "^7.1.0", - "script-ext-html-webpack-plugin": "2.1.3", + "script-ext-html-webpack-plugin": "^2.1.5", "script-loader": "0.7.2", "serve-static": "^1.13.2", "svg-sprite-loader": "4.1.3", "svgo": "1.2.2", - "vue-template-compiler": "2.6.10" + "vue-template-compiler": "^2.6.14" }, "engines": { "node": ">=8.9", diff --git a/hb_server/apps/em/migrations/0003_equipment_type.py b/hb_server/apps/em/migrations/0003_equipment_type.py new file mode 100644 index 0000000..5615a9b --- /dev/null +++ b/hb_server/apps/em/migrations/0003_equipment_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-08-31 06:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('em', '0002_alter_equipment_model'), + ] + + operations = [ + migrations.AddField( + model_name='equipment', + name='type', + field=models.IntegerField(choices=[(1, '生产设备'), (2, '检验工具')], default=1, verbose_name='类型'), + ), + ] diff --git a/hb_server/apps/em/models.py b/hb_server/apps/em/models.py index 8083e89..b55b489 100644 --- a/hb_server/apps/em/models.py +++ b/hb_server/apps/em/models.py @@ -18,6 +18,11 @@ class Equipment(CommonBModel): (0, '停用'), (2, '报废') ) + type_choices = ( + (1, '生产设备'), + (2, '检验工具') + ) + type = models.IntegerField('类型', choices=type_choices, default=1) name = models.CharField('设备名称', max_length=50) number = models.CharField('设备编号', max_length=50, unique=True) model = models.CharField('规格型号', max_length=60, null=True, blank=True) diff --git a/hb_server/apps/em/serializers.py b/hb_server/apps/em/serializers.py index d78ebb8..dcf4262 100644 --- a/hb_server/apps/em/serializers.py +++ b/hb_server/apps/em/serializers.py @@ -16,3 +16,8 @@ class EquipmentSerializer(ModelSerializer): """ Perform necessary eager loading of data. """ queryset = queryset.select_related('belong_dept','keeper') return queryset + +class EquipmentSimpleSerializer(ModelSerializer): + class Meta: + model = Equipment + fields = ['id', 'number', 'name'] \ No newline at end of file diff --git a/hb_server/apps/mtm/migrations/0002_stepoperationitem_sort.py b/hb_server/apps/mtm/migrations/0002_stepoperationitem_sort.py new file mode 100644 index 0000000..73e98f2 --- /dev/null +++ b/hb_server/apps/mtm/migrations/0002_stepoperationitem_sort.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-08-27 02:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='stepoperationitem', + name='sort', + field=models.IntegerField(default=1, verbose_name='排序号'), + ), + ] diff --git a/hb_server/apps/mtm/migrations/0003_auto_20210827_1604.py b/hb_server/apps/mtm/migrations/0003_auto_20210827_1604.py new file mode 100644 index 0000000..470e16b --- /dev/null +++ b/hb_server/apps/mtm/migrations/0003_auto_20210827_1604.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.6 on 2021-08-27 08:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0002_stepoperationitem_sort'), + ] + + operations = [ + migrations.RemoveField( + model_name='material', + name='process', + ), + migrations.AddField( + model_name='material', + name='processes', + field=models.JSONField(blank=True, default=list, verbose_name='工艺流程'), + ), + ] diff --git a/hb_server/apps/mtm/migrations/0004_auto_20210830_1129.py b/hb_server/apps/mtm/migrations/0004_auto_20210830_1129.py new file mode 100644 index 0000000..ccb2d7c --- /dev/null +++ b/hb_server/apps/mtm/migrations/0004_auto_20210830_1129.py @@ -0,0 +1,68 @@ +# Generated by Django 3.2.6 on 2021-08-30 03:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0003_auto_20210827_1604'), + ] + + operations = [ + migrations.AddField( + model_name='inputmaterial', + name='process', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='关联工序'), + preserve_default=False, + ), + migrations.AddField( + model_name='inputmaterial', + name='product', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='inputmaterial_product', to='mtm.material', verbose_name='关联产品'), + preserve_default=False, + ), + migrations.AddField( + model_name='inputmaterial', + name='sort', + field=models.IntegerField(default=1, verbose_name='排序号'), + ), + migrations.AddField( + model_name='outputmaterial', + name='process', + field=models.ForeignKey(default=2, on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='关联工序'), + preserve_default=False, + ), + migrations.AddField( + model_name='outputmaterial', + name='product', + field=models.ForeignKey(default=3, on_delete=django.db.models.deletion.CASCADE, related_name='outputmaterial_product', to='mtm.material', verbose_name='关联产品'), + preserve_default=False, + ), + migrations.AddField( + model_name='outputmaterial', + name='sort', + field=models.IntegerField(default=1, verbose_name='排序号'), + ), + migrations.AlterField( + model_name='inputmaterial', + name='material', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inputmaterial', to='mtm.material', verbose_name='输入物料'), + ), + migrations.AlterField( + model_name='inputmaterial', + name='number', + field=models.FloatField(default=1, verbose_name='消耗量'), + ), + migrations.AlterField( + model_name='outputmaterial', + name='material', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='outputmaterial', to='mtm.material', verbose_name='输出物料'), + ), + migrations.AlterField( + model_name='outputmaterial', + name='number', + field=models.FloatField(default=1, verbose_name='产出量'), + ), + ] diff --git a/hb_server/apps/mtm/migrations/0005_auto_20210830_1313.py b/hb_server/apps/mtm/migrations/0005_auto_20210830_1313.py new file mode 100644 index 0000000..68a53a4 --- /dev/null +++ b/hb_server/apps/mtm/migrations/0005_auto_20210830_1313.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.6 on 2021-08-30 05:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0004_auto_20210830_1129'), + ] + + operations = [ + migrations.RemoveField( + model_name='inputmaterial', + name='unit', + ), + migrations.RemoveField( + model_name='outputmaterial', + name='unit', + ), + migrations.AddField( + model_name='material', + name='unit', + field=models.CharField(choices=[('块', '块'), ('套', '套')], default='块', max_length=10, verbose_name='基准计量单位'), + ), + ] diff --git a/hb_server/apps/mtm/migrations/0006_auto_20210830_1331.py b/hb_server/apps/mtm/migrations/0006_auto_20210830_1331.py new file mode 100644 index 0000000..1c9e75a --- /dev/null +++ b/hb_server/apps/mtm/migrations/0006_auto_20210830_1331.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.6 on 2021-08-30 05:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0005_auto_20210830_1313'), + ] + + operations = [ + migrations.RenameField( + model_name='inputmaterial', + old_name='number', + new_name='count', + ), + migrations.RenameField( + model_name='outputmaterial', + old_name='number', + new_name='count', + ), + ] diff --git a/hb_server/apps/mtm/migrations/0007_auto_20210831_1441.py b/hb_server/apps/mtm/migrations/0007_auto_20210831_1441.py new file mode 100644 index 0000000..31b351d --- /dev/null +++ b/hb_server/apps/mtm/migrations/0007_auto_20210831_1441.py @@ -0,0 +1,47 @@ +# Generated by Django 3.2.6 on 2021-08-31 06:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('system', '0003_auto_20210812_0909'), + ('em', '0003_equipment_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mtm', '0006_auto_20210830_1331'), + ] + + operations = [ + migrations.AddField( + model_name='process', + name='workshop', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='system.organization', verbose_name='生产车间'), + ), + migrations.AddField( + model_name='step', + name='equipments', + field=models.ManyToManyField(to='em.Equipment', verbose_name='使用设备'), + ), + migrations.CreateModel( + name='UsedStep', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), + ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), + ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='usedstep_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='关联工序')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='关联产品')), + ('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='usedsteps', to='mtm.step', verbose_name='子工序')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='usedstep_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'verbose_name': '产品生产子工序', + 'verbose_name_plural': '产品生产子工序', + }, + ), + ] diff --git a/hb_server/apps/mtm/models.py b/hb_server/apps/mtm/models.py index df7a7ec..9223d29 100644 --- a/hb_server/apps/mtm/models.py +++ b/hb_server/apps/mtm/models.py @@ -5,6 +5,8 @@ from django.db.models.query import QuerySet from apps.system.models import CommonAModel, CommonBModel, Organization, User, Dict, File from utils.model import SoftModel, BaseModel from simple_history.models import HistoricalRecords +from apps.system.models import Organization +from apps.em.models import Equipment class Material(CommonAModel): """ @@ -15,11 +17,16 @@ class Material(CommonAModel): (2, '半成品'), (3, '原材料') ) + unit_choices =( + ('块', '块'), + ('套', '套') + ) name = models.CharField('物料名称', max_length=100, unique=True) number = models.CharField('编号', max_length=100, unique=True) type = models.CharField('物料类型', choices= type_choices, max_length=20, default=1) sort_str = models.CharField('排序字符', max_length=100, null=True, blank=True) - process = models.ManyToManyField('mtm.process', through='mtm.ProductProcess', related_name='product_process') + processes = models.JSONField('工艺流程', default=list, blank=True) + unit = models.CharField('基准计量单位', choices=unit_choices, default='块', max_length=10) class Meta: verbose_name = '物料表' @@ -36,6 +43,7 @@ class Process(CommonAModel): number = models.CharField('编号', max_length=100, unique=True) instruction = models.ForeignKey(File, verbose_name='指导书', on_delete=models.SET_NULL, null=True, blank=True) instruction_content = models.TextField('指导书内容', null=True, blank=True) + workshop = models.ForeignKey(Organization, verbose_name='生产车间', on_delete=models.CASCADE, null=True, blank=True) class Meta: verbose_name = '工序' @@ -53,6 +61,7 @@ class Step(CommonAModel): number = models.CharField('步骤编号', max_length=100, null=True, blank=True) instruction_content = models.TextField('相应操作指导', null=True, blank=True) sort = models.IntegerField('排序号', default=1) + equipments = models.ManyToManyField(Equipment, verbose_name='使用设备') class Meta: verbose_name = '工序步骤' @@ -61,6 +70,12 @@ class Step(CommonAModel): def __str__(self): return self.name +# class StepTable(CommonAModel): +# """ +# 过程记录表格 +# """ + + class StepOperationItem(CommonAModel): """ 操作记录条目 @@ -110,23 +125,40 @@ class InputMaterial(CommonAModel): """ 输入物料 """ - material = models.ForeignKey(Material, verbose_name='输入物料', on_delete=models.CASCADE) - number = models.FloatField('消耗量', default=0) - unit = models.CharField('单位', max_length=20) + material = models.ForeignKey(Material, verbose_name='输入物料', on_delete=models.CASCADE, related_name='inputmaterial') + count = models.FloatField('消耗量', default=1) + product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE, related_name='inputmaterial_product') + process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE) + sort = models.IntegerField('排序号', default=1) class Meta: verbose_name = '输入物料' verbose_name_plural = verbose_name + class OutputMaterial(CommonAModel): """ 输出物料 """ - material = models.ForeignKey(Material, verbose_name='输出物料', on_delete=models.CASCADE) - number = models.FloatField('产出量', default=0) - unit = models.CharField('单位', max_length=20) + material = models.ForeignKey(Material, verbose_name='输出物料', on_delete=models.CASCADE, related_name='outputmaterial') + count = models.FloatField('产出量', default=1) + product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE, related_name='outputmaterial_product') + process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE) + sort = models.IntegerField('排序号', default=1) class Meta: verbose_name = '输出物料' - verbose_name_plural = verbose_name \ No newline at end of file + verbose_name_plural = verbose_name + +class UsedStep(CommonAModel): + """ + 产品生产子工序 + """ + step = models.ForeignKey(Step, verbose_name='子工序', on_delete=models.CASCADE, related_name='usedsteps') + product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE) + process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE) + + class Meta: + verbose_name = '产品生产子工序' + verbose_name_plural = verbose_name diff --git a/hb_server/apps/mtm/serializers.py b/hb_server/apps/mtm/serializers.py index 60fe1c8..3bf650f 100644 --- a/hb_server/apps/mtm/serializers.py +++ b/hb_server/apps/mtm/serializers.py @@ -1,43 +1,139 @@ -from rest_framework.serializers import ModelSerializer - -from .models import Material, Process, ProductProcess, Step -from apps.system.serializers import FileSimpleSerializer +from apps.em.serializers import EquipmentSerializer, EquipmentSimpleSerializer +from rest_framework import serializers +from rest_framework.exceptions import ValidationError +from .models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, Step, UsedStep +from apps.system.serializers import FileSimpleSerializer, OrganizationSimpleSerializer -class MaterialSerializer(ModelSerializer): +class MaterialSerializer(serializers.ModelSerializer): + processes = serializers.ListField(child=serializers.IntegerField(min_value=1)) class Meta: model = Material fields = '__all__' -class MaterialSimpleSerializer(ModelSerializer): +class MaterialDetailSerializer(serializers.ModelSerializer): + processes_ = serializers.SerializerMethodField() class Meta: model = Material - fields = ['id', 'name', 'number'] + fields = '__all__' + + def get_processes_(self, obj): + objs = Process.objects.filter(id__in=obj.processes) + return ProcessSimpleSerializer(instance=objs, many=True).data -class ProcessSerializer(ModelSerializer): + +class MaterialSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Material + fields = ['id', 'name', 'number', 'unit'] + +class ProcessSerializer(serializers.ModelSerializer): instruction_ = FileSimpleSerializer(source='instruction', read_only=True) + workshop_ = OrganizationSimpleSerializer(source='workshop', read_only=True) class Meta: model = Process fields = '__all__' -class ProcessSimpleSerializer(ModelSerializer): +class ProcessSimpleSerializer(serializers.ModelSerializer): class Meta: model = Process fields = ['id', 'name', 'number'] -class StepSerializer(ModelSerializer): +class StepSerializer(serializers.ModelSerializer): class Meta: model = Step fields = '__all__' -class ProductProcessListSerializer(ModelSerializer): +class StepSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = Step + fields = ['id', 'name', 'sort'] + +class StepDetailSerializer(serializers.ModelSerializer): + equipments_ = EquipmentSimpleSerializer(source='equipments', many=True) + class Meta: + model = Step + fields = '__all__' + + @staticmethod + def setup_eager_loading(queryset): + queryset = queryset.prefetch_related('equipments') + return queryset + +class ProductProcessListSerializer(serializers.ModelSerializer): process_ = ProcessSimpleSerializer(source='process', read_only=True) product_ = MaterialSimpleSerializer(source='product', read_only=True) class Meta: model = ProductProcess fields = '__all__' -class ProductProcessUpdateSerializer(ModelSerializer): +class ProductProcessUpdateSerializer(serializers.ModelSerializer): class Meta: model = ProductProcess - fields = ['sort'] \ No newline at end of file + fields = ['sort'] + +class InputMaterialListSerializer(serializers.ModelSerializer): + material_ = MaterialSimpleSerializer(source='material', read_only=True) + class Meta: + model = InputMaterial + fields = '__all__' + +class OutputMaterialListSerializer(serializers.ModelSerializer): + material_ = MaterialSimpleSerializer(source='material', read_only=True) + class Meta: + model = OutputMaterial + fields = '__all__' + + +class InputMaterialSerializer(serializers.ModelSerializer): + class Meta: + model = InputMaterial + fields = ['count', 'sort', 'material', 'product', 'process'] + + def create(self, validated_data): + if InputMaterial.objects.filter(material=validated_data['material'], product=validated_data['product'], process=validated_data['process'], is_deleted=False).exists(): + raise ValidationError('该物料已存在') + return super().create(validated_data) + +class InputMaterialUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = InputMaterial + fields = ['count', 'sort'] + +class OutputMaterialSerializer(serializers.ModelSerializer): + class Meta: + model = OutputMaterial + fields = ['count', 'sort', 'material', 'product', 'process'] + + def create(self, validated_data): + if OutputMaterial.objects.filter(material=validated_data['material'], product=validated_data['product'], process=validated_data['process'], is_deleted=False).exists(): + raise ValidationError('该物料已存在') + return super().create(validated_data) + +class OutputMaterialUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = OutputMaterial + fields = ['count', 'sort'] + +class UsedStepCreateSerializer(serializers.ModelSerializer): + """ + 产品生产子工序创建 + """ + class Meta: + model = UsedStep + fields = ['step', 'product', 'process'] + +class UsedStepListSerializer(serializers.ModelSerializer): + """ + 产品生产子工序序列化 + """ + step_ = StepSimpleSerializer(source='step', read_only=True) + class Meta: + model = UsedStep + fields = '__all__' + + @staticmethod + def setup_eager_loading(queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.select_related('step') + return queryset diff --git a/hb_server/apps/mtm/urls.py b/hb_server/apps/mtm/urls.py index 9d4d325..8ac7cf2 100644 --- a/hb_server/apps/mtm/urls.py +++ b/hb_server/apps/mtm/urls.py @@ -1,14 +1,17 @@ from django.db.models import base from rest_framework import urlpatterns -from apps.mtm.views import MaterialViewSet, ProcessViewSet, ProductProcessViewSet, StepViewSet +from apps.mtm.views import InputMaterialViewSet, MaterialViewSet, OutputMaterialViewSet, ProcessViewSet, StepViewSet, UsedStepViewSet from django.urls import path, include from rest_framework.routers import DefaultRouter router = DefaultRouter() router.register('material', MaterialViewSet, basename='material') router.register('process', ProcessViewSet, basename='process') -router.register('productprocess', ProductProcessViewSet, basename='productprocess') +# router.register('productprocess', ProductProcessViewSet, basename='productprocess') router.register('step', StepViewSet, basename='step') +router.register('inputmaterial', InputMaterialViewSet, basename='inputmaterial') +router.register('outputmaterial', OutputMaterialViewSet, basename='outputmaterial') +router.register('usedstep', UsedStepViewSet, basename='usedstep') urlpatterns = [ path('', include(router.urls)), ] diff --git a/hb_server/apps/mtm/views.py b/hb_server/apps/mtm/views.py index f1510cb..0c61d43 100644 --- a/hb_server/apps/mtm/views.py +++ b/hb_server/apps/mtm/views.py @@ -2,12 +2,13 @@ from django.shortcuts import render from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework.mixins import CreateModelMixin, ListModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin -from apps.mtm.models import Material, Process, ProductProcess, Step -from apps.mtm.serializers import MaterialSerializer, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, StepSerializer +from apps.mtm.models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, Step, UsedStep +from apps.mtm.serializers import InputMaterialListSerializer, InputMaterialSerializer, InputMaterialUpdateSerializer, MaterialDetailSerializer, MaterialSerializer, MaterialSimpleSerializer, OutputMaterialListSerializer, OutputMaterialSerializer, OutputMaterialUpdateSerializer, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, StepDetailSerializer, StepSerializer, UsedStepCreateSerializer, UsedStepListSerializer from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin from rest_framework.decorators import action from rest_framework.response import Response from utils.pagination import PageOrNot +from rest_framework.exceptions import APIException # Create your views here. @@ -24,14 +25,19 @@ class MaterialViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): ordering_fields = ['number', 'sort_str'] ordering = ['number'] - @action(methods=['get'], detail=True, perms_map={'get':'*'}, pagination_class=None, serializer_class=StepSerializer) - def steps(self, request, pk=None): - """ - 工序下的子工序 - """ - process = self.get_object() - serializer = self.serializer_class(instance=Step.objects.filter(process=process, is_deleted=False), many=True) - return Response(serializer.data) + def get_serializer_class(self, *args, **kwargs): + if self.action == 'retrieve': + return MaterialDetailSerializer + return MaterialSerializer + + # @action(methods=['get'], detail=True, perms_map={'get':'*'}, pagination_class=None, serializer_class=MaterialSimpleSerializer) + # def processes(self, request, pk=None): + # """ + # 产品生产工艺流程 + # """ + # material = self.get_object() + # serializer = self.serializer_class(instance=Process.objects.filter(id__in=material.processes), many=True) + # return Response(serializer.data) class ProcessViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): @@ -40,10 +46,10 @@ class ProcessViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): """ perms_map = {'get': '*', 'post': 'process_create', 'put': 'process_update', 'delete': 'process_delete'} - queryset = Process.objects.select_related('instruction').all() + queryset = Process.objects.select_related('instruction', 'workshop').all() serializer_class = ProcessSerializer search_fields = ['name', 'number'] - filterset_fields = ['number'] + filterset_fields = ['number', 'workshop'] ordering_fields = ['number'] ordering = ['number'] @@ -56,8 +62,9 @@ class ProcessViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): serializer = self.serializer_class(instance=Step.objects.filter(process=process, is_deleted=False), many=True) return Response(serializer.data) -class StepViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet): +class StepViewSet(CreateUpdateModelAMixin, CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet): """ + 子工序-增删改查 """ perms_map = {'*':'process_update'} queryset = Step.objects.all() @@ -66,18 +73,70 @@ class StepViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, Destro filterset_fields = ['process'] ordering = ['sort'] + def get_serializer_class(self): + if self.action == 'retrieve': + return StepDetailSerializer + return StepSerializer -class ProductProcessViewSet(PageOrNot, CreateModelMixin, UpdateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): +# class ProductProcessViewSet(PageOrNot, CreateModelMixin, UpdateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): +# """ +# 产品生产工艺流程增删改查 +# """ +# perms_map={'*':'*'} +# queryset = ProductProcess.objects.select_related('process', 'product').all() +# filterset_fields = ['process', 'product'] +# serializer_class = ProductProcessListSerializer +# ordering = ['sort'] + +# def get_serializer_class(self): +# if self.action == 'update': +# return ProductProcessUpdateSerializer +# return super().get_serializer_class() + +class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): """ - 产品生产工艺流程增删改查 + 输入物料-增删改查 """ - perms_map={'*':'*'} - queryset = ProductProcess.objects.select_related('process', 'product').all() - filterset_fields = ['process', 'product'] - serializer_class = ProductProcessListSerializer - ordering = ['sort'] + perms_map = {'*':'*'} + queryset = InputMaterial.objects.select_related('material').all() + serializer_class = InputMaterialSerializer + filterset_fields = ['process', 'material'] + ordering = ['sort', '-create_time'] def get_serializer_class(self): - if self.action == 'update': - return ProductProcessUpdateSerializer - return super().get_serializer_class() \ No newline at end of file + if self.action == 'list': + return InputMaterialListSerializer + elif self.action == 'update': + return InputMaterialUpdateSerializer + return InputMaterialSerializer + +class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): + """ + 输出物料-增删改查 + """ + perms_map = {'*':'*'} + queryset = OutputMaterial.objects.select_related('material').all() + serializer_class = OutputMaterialSerializer + filterset_fields = ['process', 'material'] + ordering = ['sort', '-create_time'] + + def get_serializer_class(self): + if self.action == 'list': + return OutputMaterialListSerializer + elif self.action == 'update': + return OutputMaterialUpdateSerializer + return OutputMaterialSerializer + +class UsedStepViewSet(OptimizationMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet): + """ + 产品生产子工序表 + """ + perms_map = {'*':'*'} + queryset = UsedStep.objects.all() + filterset_fields = ['process', 'product', 'step'] + ordering = ['step__sort', '-step__create_time'] + + def get_serializer_class(self): + if self.action =='create': + return UsedStepCreateSerializer + return UsedStepListSerializer \ No newline at end of file diff --git a/hb_server/apps/wf/migrations/0007_auto_20210827_1103.py b/hb_server/apps/wf/migrations/0007_auto_20210827_1103.py new file mode 100644 index 0000000..a10b7ae --- /dev/null +++ b/hb_server/apps/wf/migrations/0007_auto_20210827_1103.py @@ -0,0 +1,76 @@ +# Generated by Django 3.2.6 on 2021-08-27 03:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wf', '0006_auto_20210825_1542'), + ] + + operations = [ + migrations.RemoveField( + model_name='ticket', + name='add_node_man', + ), + migrations.RemoveField( + model_name='ticketflow', + name='participant', + ), + migrations.AddField( + model_name='state', + name='distribute_type', + field=models.IntegerField(choices=[(1, '主动接单'), (2, '直接处理'), (3, '随机分配'), (4, '全部处理')], default=1, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)', verbose_name='分配方式'), + ), + migrations.AddField( + model_name='state', + name='participant', + field=models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\\部门id\\角色id\\变量(create_by,create_by_tl)\\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot', verbose_name='参与者'), + ), + migrations.AddField( + model_name='ticket', + name='act_state', + field=models.IntegerField(choices=[(0, '草稿中'), (1, '进行中'), (2, '被退回'), (3, '被撤回'), (4, '已完成'), (5, '已关闭')], default=1, help_text='当前工单的进行状态', verbose_name='进行状态'), + ), + migrations.AddField( + model_name='ticket', + name='multi_all_person', + field=models.JSONField(blank=True, default=dict, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式', verbose_name='全部处理的结果'), + ), + migrations.AddField( + model_name='ticket', + name='participant', + field=models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表', verbose_name='当前处理人'), + ), + migrations.AddField( + model_name='ticket', + name='participant_type', + field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型'), + ), + migrations.AddField( + model_name='workflow', + name='sn_prefix', + field=models.CharField(default='hb', max_length=50, verbose_name='流水号前缀'), + ), + migrations.AlterField( + model_name='state', + name='participant_type', + field=models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型'), + ), + migrations.AlterField( + model_name='state', + name='type', + field=models.IntegerField(choices=[(0, '普通'), (1, '开始'), (2, '结束')], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)', verbose_name='状态类型'), + ), + migrations.AlterField( + model_name='workflow', + name='display_form_str', + field=models.JSONField(blank=True, default=list, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='展现表单字段'), + ), + migrations.AlterField( + model_name='workflow', + name='limit_expression', + field=models.JSONField(blank=True, default=dict, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)', verbose_name='限制表达式'), + ), + ] diff --git a/hb_server/apps/wf/migrations/0008_auto_20210827_1104.py b/hb_server/apps/wf/migrations/0008_auto_20210827_1104.py new file mode 100644 index 0000000..fe1e449 --- /dev/null +++ b/hb_server/apps/wf/migrations/0008_auto_20210827_1104.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.6 on 2021-08-27 03:04 + +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), + ('wf', '0007_auto_20210827_1103'), + ] + + operations = [ + migrations.AddField( + model_name='ticket', + name='add_node_man', + field=models.ForeignKey(blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='加签人'), + ), + migrations.AddField( + model_name='ticketflow', + name='participant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='处理人'), + ), + ] diff --git a/hb_server/apps/wf/migrations/0009_ticketflow_participant_type.py b/hb_server/apps/wf/migrations/0009_ticketflow_participant_type.py new file mode 100644 index 0000000..62b04be --- /dev/null +++ b/hb_server/apps/wf/migrations/0009_ticketflow_participant_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-08-27 09:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wf', '0008_auto_20210827_1104'), + ] + + operations = [ + migrations.AddField( + model_name='ticketflow', + name='participant_type', + field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='处理人类型'), + ), + ] diff --git a/hb_server/apps/wf/models.py b/hb_server/apps/wf/models.py index bf60399..2591a75 100644 --- a/hb_server/apps/wf/models.py +++ b/hb_server/apps/wf/models.py @@ -12,11 +12,11 @@ class Workflow(CommonAModel): 工作流 """ name = models.CharField('名称', max_length=50) - sn_prefix = models.CharField('流水号前缀', max_length=50) + sn_prefix = models.CharField('流水号前缀', max_length=50, default='hb') description = models.CharField('描述', max_length=200) view_permission_check = models.BooleanField('查看权限校验', default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单') - limit_expression = models.JSONField('限制表达式', max_length=1000, default=dict, blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)') - display_form_str = models.JSONField('展现表单字段', max_length=10000, default=list, blank=True, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') + limit_expression = models.JSONField('限制表达式', default=dict, blank=True, help_text='限制周期({"period":24} 24小时), 限制次数({"count":1}在限制周期内只允许提交1次), 限制级别({"level":1} 针对(1单个用户 2全局)限制周期限制次数,默认特定用户);允许特定人员提交({"allow_persons":"zhangsan,lisi"}只允许张三提交工单,{"allow_depts":"1,2"}只允许部门id为1和2的用户提交工单,{"allow_roles":"1,2"}只允许角色id为1和2的用户提交工单)') + display_form_str = models.JSONField('展现表单字段', default=list, blank=True, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') title_template = models.CharField('标题模板', max_length=50, default='你有一个待办工单:{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}') content_template = models.CharField('内容模板', max_length=1000, default='标题:{title}, 创建时间:{create_time}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}') @@ -60,6 +60,10 @@ class State(CommonAModel): (STATE_DISTRIBUTE_TYPE_RANDOM, '随机分配'), (STATE_DISTRIBUTE_TYPE_ALL, '全部处理'), ) + + STATE_FIELD_READONLY= 1 # 字段只读 + STATE_FIELD_REQUIRED = 2 # 字段必填 + STATE_FIELD_OPTIONAL = 3 # 字段可选 name = models.CharField('名称', max_length=50) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') is_hidden = models.BooleanField('是否隐藏', default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)') @@ -69,7 +73,7 @@ class State(CommonAModel): participant_type = models.IntegerField('参与者类型', choices=type2_choices, default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by') participant = models.JSONField('参与者', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\部门id\角色id\变量(create_by,create_by_tl)\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot') state_fields = models.JSONField('表单字段', default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选. 示例:{"created_at":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称') # json格式存储,包括读写属性1:只读,2:必填,3:可选,4:不显示, 字典的字典 - distribute_type = models.IntegerField('分配方式', default=1, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)') + distribute_type = models.IntegerField('分配方式', default=1, choices=state_distribute_choices, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)') class Transition(CommonAModel): """ @@ -150,7 +154,7 @@ class Ticket(CommonAModel): parent_state = models.ForeignKey(State, null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单状态', related_name='ticket_parent_state') ticket_data = models.JSONField('工单数据', default=dict, help_text='工单所有字段内容') in_add_node = models.BooleanField('加签状态中', default=False, help_text='是否处于加签状态下') - add_node_man = models.CharField('加签人', max_length=50, default='', blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效') + add_node_man = models.ForeignKey(User, verbose_name='加签人', on_delete=models.SET_NULL, null=True, blank=True, help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效') participant_type = models.IntegerField('当前处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.type2_choices) participant = models.JSONField('当前处理人', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表') @@ -164,6 +168,7 @@ class TicketFlow(BaseModel): ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='关联工单') transition = models.ForeignKey(Transition, verbose_name='流转id', help_text='与worklow.Transition关联, 为0时表示认为干预的操作', on_delete=models.CASCADE) suggestion = models.CharField('处理意见', max_length=10000, default='', blank=True) - participant = models.CharField('处理人', max_length=50, default='', blank=True) + participant_type = models.IntegerField('处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.type2_choices) + participant = models.ForeignKey(User, verbose_name='处理人', on_delete=models.SET_NULL, null=True, blank=True) state = models.ForeignKey(State, verbose_name='当前状态', default=0, blank=True, on_delete=models.CASCADE) ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据,json格式') \ No newline at end of file diff --git a/hb_server/apps/wf/serializers.py b/hb_server/apps/wf/serializers.py index dc7ed0a..d4ca696 100644 --- a/hb_server/apps/wf/serializers.py +++ b/hb_server/apps/wf/serializers.py @@ -48,6 +48,10 @@ class TicketCreateSerializer(serializers.ModelSerializer): class Meta: model=Ticket fields=['title','workflow','ticket_data', 'transition'] + + def create(self, validated_data): + validated_data.pop('transition') + return super().create(validated_data) class TicketSerializer(serializers.ModelSerializer): workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True) diff --git a/hb_server/apps/wf/services.py b/hb_server/apps/wf/services.py index 8e4cc0f..2337b95 100644 --- a/hb_server/apps/wf/services.py +++ b/hb_server/apps/wf/services.py @@ -1,3 +1,4 @@ +from typing import Tuple from apps.system.models import User from apps.wf.models import CustomField, State, Ticket, Transition, Workflow from rest_framework.exceptions import APIException @@ -114,41 +115,65 @@ class WfService(object): 回到初始状态 """ return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL, - destination_participant=ticket.create_by, - multi_all_person=dict()) + destination_participant=ticket.create_by.id, + multi_all_person={}) elif state.type == State.STATE_TYPE_END: """ 到达结束状态 """ return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL, - destination_participant='', - multi_all_person=dict()) + destination_participant=0, + multi_all_person={}) multi_all_person_dict = {} - destination_participant_type, destination_participant = State.participant_type, State.participant - + destination_participant_type, destination_participant = state.participant_type, state.participant if destination_participant_type == State.PARTICIPANT_TYPE_FIELD: - destination_participant = ticket_data.get(destination_participant, '') if destination_participant in ticket_data else Ticket.ticket_data.get(destination_participant, '') + destination_participant = ticket_data.get(destination_participant, None) if destination_participant in ticket_data else Ticket.ticket_data.get(destination_participant, None) - if destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#单部门 - destination_participant = User.objects.filter(dept=destination_participant).values_list('id') + elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#单部门 + destination_participant = list(User.objects.filter(dept=destination_participant).values_list('id', flat=True)) - if destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#单角色 - destination_participant = User.objects.filter(roles=destination_participant).values_list('id') + elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#单角色 + destination_participant = list(User.objects.filter(roles=destination_participant).values_list('id', flat=True)) if type(destination_participant) == list: destination_participant_type = State.PARTICIPANT_TYPE_MULTI destination_participant = list(set(destination_participant)) else: destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL - if destination_participant_type == State.PARTICIPANT_TYPE_MULTI: if state.distribute_type == State.STATE_DISTRIBUTE_TYPE_RANDOM: destination_participant = random.choice(destination_participant) elif state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ALL: for i in destination_participant: multi_all_person_dict[i]={} + return dict(destination_participant_type=destination_participant_type, destination_participant=destination_participant, multi_all_person=multi_all_person_dict) + + @classmethod + def ticket_handle_permission_check(cls, ticket:Ticket, user:User)-> dict: + transitions = cls.get_state_transitions(ticket.state) + if not transitions: + return dict(permission=True, msg="工单当前状态无需操作") + current_participant_count = 1 + participant_type = ticket.participant_type + participant = ticket.participant + state = ticket.state + if participant_type == State.PARTICIPANT_TYPE_PERSONAL: + if user.id != participant: + return dict(permission=False, msg="非当前处理人") + elif participant_type in [State.PARTICIPANT_TYPE_MULTI, State.PARTICIPANT_TYPE_DEPT, State.PARTICIPANT_TYPE_ROLE]: + if user.id not in participant: + return dict(permission=False, msg="非当前处理人") + current_participant_count = len(participant.split(',')) + if current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE: + return dict(permission=False, msg="需要先接单再处理") + if ticket.in_add_node: + return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作") + return dict(permission=True, msg="") + + + diff --git a/hb_server/apps/wf/views.py b/hb_server/apps/wf/views.py index 1702059..5bf968a 100644 --- a/hb_server/apps/wf/views.py +++ b/hb_server/apps/wf/views.py @@ -1,11 +1,12 @@ +from django.core.exceptions import AppRegistryNotReady from rest_framework.response import Response from rest_framework import serializers from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin from apps.wf.serializers import CustomFieldSerializer, StateSerializer, TicketCreateSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer -from django.shortcuts import render +from django.shortcuts import get_object_or_404, render from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.decorators import action, api_view -from apps.wf.models import CustomField, Ticket, Workflow, State, Transition +from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow from apps.system.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin from apps.wf.services import WfService from rest_framework.exceptions import APIException @@ -104,28 +105,63 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin """ 新建工单 """ - serializer = self.get_serializer(data=request.data) + rdata = request.data + serializer = self.get_serializer(data=rdata) serializer.is_valid(raise_exception=True) - start_state = WfService.get_workflow_start_state(serializer.data['workflow']) - transition = Transition.objects.get(pk=serializer.data['transition']) - ticket_data = serializer.data['ticket_data'] + start_state = WfService.get_workflow_start_state(rdata['workflow']) + transition = Transition.objects.get(pk=rdata['transition']) + ticket_data = rdata['ticket_data'] if transition.field_require_check: for key, value in start_state.state_fields.items(): #校验必填项 - if value == 2: + if value == State.STATE_FIELD_REQUIRED: if key not in ticket_data or not ticket_data[key]: raise APIException('字段{}必填'.format(key)) - ticket = serializer.save() + ticket = serializer.save(state=start_state) # 先创建出来 next_state = WfService.get_next_state_id_by_transition_and_ticket_info(ticket=ticket, transition=transition) participant_info = WfService.get_ticket_state_participant_info(state=next_state, ticket=ticket, ticket_data=ticket.ticket_data) destination_participant_type = participant_info.get('destination_participant_type', 0) destination_participant = participant_info.get('destination_participant', '') - multi_all_person = participant_info.get('multi_all_person', '{}') # 多人需要全部处理情况 - sn = WfService.get_ticket_sn(ticket.workflow) + multi_all_person = participant_info.get('multi_all_person', {}) # 多人需要全部处理情况 + sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 if next_state.type == State.STATE_TYPE_END: act_state = Ticket.TICKET_ACT_STATE_FINISH elif next_state.type == State.STATE_TYPE_START: - pass - + act_state = Ticket.TICKET_ACT_STATE_DRAFT + else: + act_state = Ticket.TICKET_ACT_STATE_ONGOING + title = rdata.get('title', '') + title_template = ticket.workflow.title_template + if title_template: + all_ticket_data = {**rdata, **rdata['ticket_data']} + title = title_template.format(**all_ticket_data) + # 更新工单 + ticket.sn=sn + ticket.title=title + ticket.state=next_state + ticket.participant=destination_participant + ticket.participant_type=destination_participant_type + ticket.act_state=act_state + ticket.multi_all_person=multi_all_person + ticket.save() + # 新增流转记录 + TicketFlow.objects.create(ticket=ticket, state=start_state, ticket_data=ticket_data, + suggestion=rdata.get('suggestion',''), participant_type=State.PARTICIPANT_TYPE_PERSONAL, + participant=ticket.create_by, transition=transition) + return Response(TicketSerializer(instance=ticket).data) + + + @action(methods=['post'], detail=True, perms_map={'get':'*'}) + def handle(self, request, pk=None): + """ + 处理工单 + """ + try: + ticket = Ticket.objects.get(pk=pk) + except: + raise APIException('工单不存在') + data = request.data + result = WfService.ticket_handle_permission_check() + @action(methods=['get'], detail=True, perms_map={'get':'*'}) def flowsteps(self, request, pk=None): """ diff --git a/hb_server/server/urls.py b/hb_server/server/urls.py index e9bf6e7..1e6b935 100644 --- a/hb_server/server/urls.py +++ b/hb_server/server/urls.py @@ -18,6 +18,7 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from django.conf.urls import url from drf_yasg import openapi from drf_yasg.views import get_schema_view from rest_framework import routers @@ -47,6 +48,7 @@ urlpatterns = [ # api文档 path('api/docs/', include_docs_urls(title="接口文档", authentication_classes=[], permission_classes=[])), + url(r'^api/swagger(?P\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'), path('api/swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), diff --git a/hb_server/utils/pagination.py b/hb_server/utils/pagination.py index 76f8c74..f33f120 100644 --- a/hb_server/utils/pagination.py +++ b/hb_server/utils/pagination.py @@ -5,12 +5,21 @@ class MyPagination(PageNumberPagination): page_size = 10 page_size_query_param = 'page_size' + def paginate_queryset(self, queryset, request, view): + if request.query_params.get('pageoff', None) or request.query_params.get('page', None)=='0': + if queryset.count()<500: + return None + elif queryset.count()>=500: + raise ParseError('单次请求数据量大,请分页获取') + return super().paginate_queryset(queryset, request, view=view) + class PageOrNot: def paginate_queryset(self, queryset): if (self.paginator is None): return None - elif self.request.query_params.get('pageoff', None) and queryset.count()<500: - return None - elif self.request.query_params.get('pageoff', None) and queryset.count()>=500: - raise ParseError('单次请求数据量大,请求中止') + elif self.request.query_params.get('pageoff', None) or self.request.query_params.get('page', None) == 0: + if queryset.count()<500: + return None + elif queryset.count()>=500: + raise ParseError('单次请求数据量大,请求中止') return self.paginator.paginate_queryset(queryset, self.request, view=self) diff --git a/hb_server/utils/response.py b/hb_server/utils/response.py index 2923188..4657b5e 100644 --- a/hb_server/utils/response.py +++ b/hb_server/utils/response.py @@ -50,11 +50,13 @@ class FitJSONRenderer(JSONRenderer): response_body.code = response.status_code if response_body.code >= 400: # 响应异常 response_body.data = data # data里是详细异常信息 + prefix = "" if isinstance(data, dict): - data = data[list(data.keys())[0]] - elif isinstance(data, list): + prefix = list(data.keys())[0] + data = data[prefix] + if isinstance(data, list): data = data[0] - response_body.msg = data # 取一部分放入msg,方便前端alert + response_body.msg = prefix + ":" + data # 取一部分放入msg,方便前端alert else: response_body.data = data renderer_context.get("response").status_code = 200 # 统一成200响应,用code区分