diff --git a/hb_client/src/api/workflow.js b/hb_client/src/api/workflow.js index 5fd2b05..9e69564 100644 --- a/hb_client/src/api/workflow.js +++ b/hb_client/src/api/workflow.js @@ -154,6 +154,14 @@ export function createTicket(data) { data }) } +//接单 +export function ticketAccpet(id,data) { + return request({ + url: `/wf/ticket/${id}/accpet/`, + method: 'post', + data + }) +} //工单详情 export function getTicketDetail(id) { return request({ @@ -168,3 +176,11 @@ export function getTicketTransitions(id) { method: 'get' }) } + +//工单流转记录 +export function getTicketFlowlog(id) { + return request({ + url: `/wf/ticket/${id}/flowlogs/`, + method: 'get' + }) +} diff --git a/hb_client/src/router/index.js b/hb_client/src/router/index.js index 0b386cb..4b8ad23 100644 --- a/hb_client/src/router/index.js +++ b/hb_client/src/router/index.js @@ -340,6 +340,13 @@ export const asyncRoutes = [ meta: { title: '人员信息详情', icon: 'example', perms: ['workflow_manage'] }, hidden: true }, + { + path: 'ticketHandle', + name: 'ticketHandle', + component: () => import('@/views/workflow/ticketHandle'), + meta: { title: '工单处理', icon: 'example', perms: ['workflow_manage'] }, + hidden: true + }, ] }, { diff --git a/hb_client/src/views/workflow/configuration.vue b/hb_client/src/views/workflow/configuration.vue index 1e55695..aaee8bc 100644 --- a/hb_client/src/views/workflow/configuration.vue +++ b/hb_client/src/views/workflow/configuration.vue @@ -27,9 +27,20 @@ import TST from "@/views/workflow/transitions" }; }, created() { - - this.ID = this.$route.params.workflow; - + let id = sessionStorage.getItem('configurationId'); + if(this.$route.params.workflow){ + this.ID = this.$route.params.workflow; + if(id){ + sessionStorage.removeItem('configurationId'); + sessionStorage.setItem('configurationId',this.$route.params.workflow); + }else{ + sessionStorage.setItem('configurationId',this.$route.params.workflow); + } + }else{ + if(id){ + this.ID = id; + } + } }, methods: { handleClick(tab, event) { @@ -38,4 +49,4 @@ import TST from "@/views/workflow/transitions" } }; - \ No newline at end of file + diff --git a/hb_client/src/views/workflow/index.vue b/hb_client/src/views/workflow/index.vue index c92c762..40d6889 100644 --- a/hb_client/src/views/workflow/index.vue +++ b/hb_client/src/views/workflow/index.vue @@ -50,13 +50,13 @@ {{ !!(scope.row.view_permission_check)?'是':'否' }} - + @@ -87,6 +91,21 @@ @pagination="getList" /> +
+
+
工作流流程图
+ + +

工作流名称 :{{watchedName}}

+
+ +

创建时间 :{{watchedCreateTime}}

+
+
+ + +
+
@@ -139,12 +158,14 @@ + + diff --git a/hb_client/src/views/workflow/state.vue b/hb_client/src/views/workflow/state.vue index 7895dca..6c12777 100644 --- a/hb_client/src/views/workflow/state.vue +++ b/hb_client/src/views/workflow/state.vue @@ -148,6 +148,28 @@ + + 添加修改 + + + + + + + + + + + + + + + + + + + +
取消 @@ -157,7 +179,7 @@
+ + diff --git a/hb_client/src/views/workflow/transitions.vue b/hb_client/src/views/workflow/transitions.vue index a11521a..341fa2b 100644 --- a/hb_client/src/views/workflow/transitions.vue +++ b/hb_client/src/views/workflow/transitions.vue @@ -97,10 +97,14 @@ - + - import {getWfStateList, getWfTransitionList, createWfTransition,updateWfTransition,deleteWfTransition } from "@/api/workflow"; import checkPermission from "@/utils/permission"; - +import vueJsonEditor from 'vue-json-editor' import { genTree } from "@/utils" const defaultwftransition = { name: "", }; export default { - components: { }, + components: { vueJsonEditor }, name: "TST", props: ["ID"], data() { @@ -211,13 +215,11 @@ export default { this.wftransition = Object.assign({}, scope.row); // copy obj this.dialogType = "edit"; this.dialogVisible = true; - this.$nextTick(() => { - this.$refs["Form"].clearValidate(); - }); + // this.wftransition.condition_expression = JSON.stringify(scope.row.condition_expression) + // this.$nextTick(() => { + // this.$refs["Form"].clearValidate(); + // }); }, - - - async confirm(form) { this.$refs[form].validate((valid) => { if (valid) { @@ -245,8 +247,6 @@ export default { } }); }, - - handleDelete(scope) { this.$confirm("确认删除?", "警告", { confirmButtonText: "确认", @@ -262,9 +262,6 @@ export default { console.error(err); }); }, - - - }, }; diff --git a/hb_server/apps/inm/views.py b/hb_server/apps/inm/views.py index 4c3b8c4..e8e8754 100644 --- a/hb_server/apps/inm/views.py +++ b/hb_server/apps/inm/views.py @@ -1,5 +1,6 @@ from django.shortcuts import render -from rest_framework.viewsets import ModelViewSet +from rest_framework.mixins import ListModelMixin +from rest_framework.viewsets import GenericViewSet, ModelViewSet from apps.inm.models import WareHouse,Inventory from apps.inm.serializers import WareHouseSerializer, WareHouseCreateUpdateSerializer,InventorySerializer,InventoryCreateUpdateSerializer @@ -23,7 +24,8 @@ class WarehouseViewSet(CreateUpdateModelAMixin, ModelViewSet): if self.action in ['create', 'update']: return WareHouseCreateUpdateSerializer return WareHouseSerializer -class InventoryViewSet(CreateUpdateModelAMixin, ModelViewSet): + +class InventoryViewSet(ListModelMixin, GenericViewSet): """ 物料基本信息-增删改查 """ @@ -33,8 +35,3 @@ class InventoryViewSet(CreateUpdateModelAMixin, ModelViewSet): filterset_fields = [] ordering_fields = ['create_time'] ordering = ['-create_time'] - - def get_serializer_class(self): - if self.action in ['create', 'update']: - return InventoryCreateUpdateSerializer - return InventorySerializer diff --git a/hb_server/apps/mtm/migrations/0001_initial.py b/hb_server/apps/mtm/migrations/0001_initial.py index d44f801..654ed11 100644 --- a/hb_server/apps/mtm/migrations/0001_initial.py +++ b/hb_server/apps/mtm/migrations/0001_initial.py @@ -94,24 +94,6 @@ class Migration(migrations.Migration): 'verbose_name_plural': '操作记录条目', }, ), - migrations.CreateModel( - name='ProductProcess', - 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='删除标记')), - ('sort', models.IntegerField(default=1, verbose_name='排序号')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='productprocess_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='产品')), - ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='productprocess_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ], - options={ - 'verbose_name': '产品生产工序', - 'verbose_name_plural': '产品生产工序', - }, - ), migrations.CreateModel( name='OutputMaterial', fields=[ diff --git a/hb_server/apps/mtm/migrations/0019_auto_20211012_0901.py b/hb_server/apps/mtm/migrations/0019_auto_20211012_0901.py new file mode 100644 index 0000000..9cc24ed --- /dev/null +++ b/hb_server/apps/mtm/migrations/0019_auto_20211012_0901.py @@ -0,0 +1,137 @@ +# Generated by Django 3.2.6 on 2021-10-12 01:01 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('mtm', '0018_material_count'), + ] + + operations = [ + migrations.CreateModel( + name='SubProduction', + 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='删除标记')), + ('name', models.CharField(blank=True, max_length=50, null=True, verbose_name='命名')), + ('sort', models.IntegerField(default=1, verbose_name='排序号')), + ], + options={ + 'verbose_name': '产品生产工序', + 'verbose_name_plural': '产品生产工序', + }, + ), + migrations.RemoveField( + model_name='inputmaterial', + name='create_by', + ), + migrations.RemoveField( + model_name='inputmaterial', + name='process', + ), + migrations.RemoveField( + model_name='inputmaterial', + name='product', + ), + migrations.RemoveField( + model_name='inputmaterial', + name='update_by', + ), + migrations.RemoveField( + model_name='material', + name='processes', + ), + migrations.RemoveField( + model_name='outputmaterial', + name='create_by', + ), + migrations.RemoveField( + model_name='outputmaterial', + name='process', + ), + migrations.RemoveField( + model_name='outputmaterial', + name='product', + ), + migrations.RemoveField( + model_name='outputmaterial', + name='update_by', + ), + migrations.RemoveField( + model_name='techdoc', + name='create_by', + ), + migrations.RemoveField( + model_name='techdoc', + name='process', + ), + migrations.RemoveField( + model_name='techdoc', + name='product', + ), + migrations.RemoveField( + model_name='techdoc', + name='update_by', + ), + migrations.RemoveField( + model_name='usedstep', + name='create_by', + ), + migrations.RemoveField( + model_name='usedstep', + name='process', + ), + migrations.RemoveField( + model_name='usedstep', + name='product', + ), + migrations.RemoveField( + model_name='usedstep', + name='update_by', + ), + migrations.AddField( + model_name='usedstep', + name='remark', + field=models.TextField(blank=True, null=True, verbose_name='生产备注'), + ), + migrations.AlterField( + model_name='step', + name='process', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='step_process', to='mtm.process', verbose_name='所属工序'), + ), + migrations.AddField( + model_name='subproduction', + name='product', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='产品'), + ), + migrations.AddField( + model_name='inputmaterial', + name='subproduction', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.subproduction', verbose_name='关联生产分解'), + preserve_default=False, + ), + migrations.AddField( + model_name='outputmaterial', + name='subproduction', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.subproduction', verbose_name='关联生产分解'), + preserve_default=False, + ), + migrations.AddField( + model_name='techdoc', + name='subproduction', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.subproduction', verbose_name='关联生产分解'), + preserve_default=False, + ), + migrations.AddField( + model_name='usedstep', + name='subproduction', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='mtm.subproduction', verbose_name='关联生产分解'), + preserve_default=False, + ), + ] diff --git a/hb_server/apps/mtm/migrations/0020_auto_20211012_1657.py b/hb_server/apps/mtm/migrations/0020_auto_20211012_1657.py new file mode 100644 index 0000000..2d80438 --- /dev/null +++ b/hb_server/apps/mtm/migrations/0020_auto_20211012_1657.py @@ -0,0 +1,26 @@ +# Generated by Django 3.2.6 on 2021-10-12 08:57 + +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', '0019_auto_20211012_0901'), + ] + + operations = [ + migrations.AddField( + model_name='subproduction', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subproduction_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddField( + model_name='subproduction', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subproduction_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + ] diff --git a/hb_server/apps/mtm/migrations/0021_auto_20211013_0856.py b/hb_server/apps/mtm/migrations/0021_auto_20211013_0856.py new file mode 100644 index 0000000..21d8787 --- /dev/null +++ b/hb_server/apps/mtm/migrations/0021_auto_20211013_0856.py @@ -0,0 +1,56 @@ +# Generated by Django 3.2.6 on 2021-10-13 00:56 + +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', '0020_auto_20211012_1657'), + ] + + operations = [ + migrations.AddField( + model_name='inputmaterial', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inputmaterial_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddField( + model_name='inputmaterial', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inputmaterial_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AddField( + model_name='outputmaterial', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='outputmaterial_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddField( + model_name='outputmaterial', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='outputmaterial_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AddField( + model_name='techdoc', + name='create_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='techdoc_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'), + ), + migrations.AddField( + model_name='techdoc', + name='update_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='techdoc_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + ), + migrations.AddField( + model_name='usedstep', + name='create_by', + field=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='创建人'), + ), + migrations.AddField( + model_name='usedstep', + name='update_by', + field=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='最后编辑人'), + ), + ] diff --git a/hb_server/apps/mtm/models.py b/hb_server/apps/mtm/models.py index 6fca06f..82b39c7 100644 --- a/hb_server/apps/mtm/models.py +++ b/hb_server/apps/mtm/models.py @@ -16,9 +16,9 @@ class Material(CommonAModel): (1, '成品'), (2, '半成品'), (3, '主要原料'), - (4,'辅助原料') , + (4, '辅助原料') , (5, '加工工具'), - (6,'辅助工具') + (6, '辅助工具') ) unit_choices =( ('块', '块'), @@ -29,7 +29,6 @@ class Material(CommonAModel): specification = models.CharField('型号', max_length=100, null=True, blank=True) type = models.CharField('物料类型', choices= type_choices, max_length=20, default=1) sort_str = models.CharField('排序字符', max_length=100, null=True, blank=True) - processes = models.JSONField('工艺流程', default=list, blank=True, null=True) unit = models.CharField('基准计量单位', choices=unit_choices, default='块', max_length=10) count = models.IntegerField('物料总数', default=0) class Meta: @@ -60,7 +59,7 @@ class Step(CommonAModel): """ 工序步骤 """ - process = models.ForeignKey(Process, on_delete=models.CASCADE, verbose_name='所属工序') + process = models.ForeignKey(Process, on_delete=models.CASCADE, verbose_name='所属工序', related_name='step_process') name = models.CharField('工序步骤名称', max_length=100) number = models.CharField('步骤编号', max_length=100, null=True, blank=True) instruction_content = models.TextField('相应操作指导', null=True, blank=True) @@ -127,26 +126,28 @@ class RecordFormField(CommonAModel): def __str__(self): return self.field_key + '-' + self.field_name -class ProductProcess(CommonAModel): + + +class SubProduction(CommonAModel): """ - 产品生产工艺 + 产品生产分解 """ + name = models.CharField('命名', max_length=50, null=True, blank=True) product = models.ForeignKey(Material, verbose_name='产品', on_delete=models.CASCADE) - 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 InputMaterial(CommonAModel): """ 输入物料 """ 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) + subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE) sort = models.IntegerField('排序号', default=1) class Meta: @@ -161,8 +162,7 @@ class OutputMaterial(CommonAModel): """ 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) + subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE) sort = models.IntegerField('排序号', default=1) class Meta: @@ -171,11 +171,11 @@ class OutputMaterial(CommonAModel): 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) + remark = models.TextField('生产备注', null=True, blank=True) + subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE) class Meta: verbose_name = '产品生产子工序' @@ -188,8 +188,7 @@ class TechDoc(CommonAModel): """ name = models.CharField('名称', max_length=50) file = models.ForeignKey(File, verbose_name='技术文件', on_delete=models.CASCADE) - product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE) - process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE) + subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE) content = models.TextField('内容', null=True, blank=True) class Meta: diff --git a/hb_server/apps/mtm/serializers.py b/hb_server/apps/mtm/serializers.py index 6a1dd98..7cd8fe5 100644 --- a/hb_server/apps/mtm/serializers.py +++ b/hb_server/apps/mtm/serializers.py @@ -1,12 +1,11 @@ from apps.em.serializers import EquipmentSimpleSerializer from rest_framework import serializers from rest_framework.exceptions import ParseError, ValidationError -from .models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, RecordForm, RecordFormField, Step, TechDoc, UsedStep +from .models import InputMaterial, Material, OutputMaterial, Process, RecordForm, RecordFormField, Step, TechDoc, UsedStep, SubProduction from apps.system.serializers import FileSimpleSerializer, OrganizationSimpleSerializer class MaterialSerializer(serializers.ModelSerializer): - processes = serializers.ListField(child=serializers.IntegerField(min_value=1)) class Meta: model = Material fields = '__all__' @@ -18,7 +17,8 @@ class MaterialDetailSerializer(serializers.ModelSerializer): fields = '__all__' def get_processes_(self, obj): - objs = Process.objects.filter(id__in=obj.processes).order_by('number') + steps = UsedStep.objects.filter(subproduction__product=obj).values_list('step', flat=True) + objs = Process.objects.filter(step_process__id__in=steps).distinct().order_by('number') return ProcessSimpleSerializer(instance=objs, many=True).data @@ -60,18 +60,11 @@ class StepDetailSerializer(serializers.ModelSerializer): 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 SubProductionSerializer(serializers.ModelSerializer): class Meta: - model = ProductProcess + model = SubProduction fields = '__all__' -class ProductProcessUpdateSerializer(serializers.ModelSerializer): - class Meta: - model = ProductProcess - fields = ['sort'] - class InputMaterialListSerializer(serializers.ModelSerializer): material_ = MaterialSimpleSerializer(source='material', read_only=True) class Meta: @@ -88,10 +81,10 @@ class OutputMaterialListSerializer(serializers.ModelSerializer): class InputMaterialSerializer(serializers.ModelSerializer): class Meta: model = InputMaterial - fields = ['count', 'sort', 'material', 'product', 'process'] + fields = ['count', 'sort', 'material', 'subproduction'] 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(): + if InputMaterial.objects.filter(material=validated_data['material'], subproduction=validated_data['subproduction'], is_deleted=False).exists(): raise ValidationError('该物料已存在') return super().create(validated_data) @@ -103,10 +96,10 @@ class InputMaterialUpdateSerializer(serializers.ModelSerializer): class OutputMaterialSerializer(serializers.ModelSerializer): class Meta: model = OutputMaterial - fields = ['count', 'sort', 'material', 'product', 'process'] + fields = ['count', 'sort', 'material', 'subproduction'] 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(): + if OutputMaterial.objects.filter(material=validated_data['material'], subproduction=validated_data['subproduction'], is_deleted=False).exists(): raise ValidationError('该物料已存在') return super().create(validated_data) @@ -121,7 +114,15 @@ class UsedStepCreateSerializer(serializers.ModelSerializer): """ class Meta: model = UsedStep - fields = ['step', 'product', 'process'] + fields = ['step', 'subproduction', 'remark'] + +class UsedStepUpdateSerializer(serializers.ModelSerializer): + """ + 产品生产子工序编辑 + """ + class Meta: + model = UsedStep + fields = ['remark'] class UsedStepListSerializer(serializers.ModelSerializer): """ @@ -198,7 +199,7 @@ class TechDocListSerializer(serializers.ModelSerializer): class TechDocCreateSerializer(serializers.ModelSerializer): class Meta: model = TechDoc - fields = ['file', 'product', 'process', 'name', 'content'] + fields = ['file', 'subproduction', 'name', 'content'] class TechDocUpdateSerializer(serializers.ModelSerializer): class Meta: diff --git a/hb_server/apps/mtm/urls.py b/hb_server/apps/mtm/urls.py index 7048bc5..5c18f0c 100644 --- a/hb_server/apps/mtm/urls.py +++ b/hb_server/apps/mtm/urls.py @@ -1,14 +1,14 @@ from django.db.models import base from rest_framework import urlpatterns -from apps.mtm.views import InputMaterialViewSet, MaterialViewSet, OutputMaterialViewSet, ProcessViewSet, RecordFormFieldViewSet, RecordFormViewSet, StepViewSet, TechDocViewSet, UsedStepViewSet +from apps.mtm.views import InputMaterialViewSet, MaterialViewSet, OutputMaterialViewSet, ProcessViewSet, RecordFormFieldViewSet, RecordFormViewSet, StepViewSet, SubProductionViewSet, TechDocViewSet, 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('step', StepViewSet, basename='step') +router.register('subproducation', SubProductionViewSet, basename='subproducation') router.register('inputmaterial', InputMaterialViewSet, basename='inputmaterial') router.register('outputmaterial', OutputMaterialViewSet, basename='outputmaterial') router.register('usedstep', UsedStepViewSet, basename='usedstep') diff --git a/hb_server/apps/mtm/views.py b/hb_server/apps/mtm/views.py index 974bdba..0e1c44f 100644 --- a/hb_server/apps/mtm/views.py +++ b/hb_server/apps/mtm/views.py @@ -2,8 +2,8 @@ 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 InputMaterial, Material, OutputMaterial, Process, ProductProcess, RecordForm, RecordFormField, Step, TechDoc, UsedStep -from apps.mtm.serializers import InputMaterialListSerializer, InputMaterialSerializer, InputMaterialUpdateSerializer, MaterialDetailSerializer, MaterialSerializer, MaterialSimpleSerializer, OutputMaterialListSerializer, OutputMaterialSerializer, OutputMaterialUpdateSerializer, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, RecordFormCreateSerializer, RecordFormFieldCreateSerializer, RecordFormFieldSerializer, RecordFormFieldUpdateSerializer, RecordFormSerializer, RecordFormUpdateSerializer, StepDetailSerializer, StepSerializer, TechDocCreateSerializer, TechDocListSerializer, TechDocUpdateSerializer, UsedStepCreateSerializer, UsedStepListSerializer +from apps.mtm.models import InputMaterial, Material, OutputMaterial, Process, RecordForm, RecordFormField, Step, TechDoc, UsedStep, SubProduction +from apps.mtm.serializers import InputMaterialListSerializer, InputMaterialSerializer, InputMaterialUpdateSerializer, MaterialDetailSerializer, MaterialSerializer, MaterialSimpleSerializer, OutputMaterialListSerializer, OutputMaterialSerializer, OutputMaterialUpdateSerializer, ProcessSerializer, RecordFormCreateSerializer, RecordFormFieldCreateSerializer, RecordFormFieldSerializer, RecordFormFieldUpdateSerializer, RecordFormSerializer, RecordFormUpdateSerializer, StepDetailSerializer, StepSerializer, SubProductionSerializer, TechDocCreateSerializer, TechDocListSerializer, TechDocUpdateSerializer, UsedStepCreateSerializer, UsedStepListSerializer, UsedStepUpdateSerializer from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin from rest_framework.decorators import action from rest_framework.response import Response @@ -30,15 +30,6 @@ class MaterialViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): 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): """ @@ -62,11 +53,11 @@ class ProcessViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet): serializer = self.serializer_class(instance=Step.objects.prefetch_related('equipments').filter(process=process, is_deleted=False), many=True) return Response(serializer.data) -class StepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet): +class StepViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): """ 子工序-增删改查 """ - perms_map = {'*':'process_update'} + perms_map = {'*':'*'} queryset = Step.objects.all() serializer_class = StepSerializer search_fields = ['name', 'number'] @@ -78,20 +69,16 @@ class StepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin, return StepDetailSerializer return StepSerializer -# 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 SubProductionViewSet(CreateUpdateModelAMixin, ModelViewSet): + """ + 产品生产分解增删改查 + """ + perms_map={'*':'*'} + queryset = SubProduction.objects.all() + filterset_fields = ['product'] + search_fields = ['name'] + serializer_class = SubProductionSerializer + ordering = ['sort'] class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): """ @@ -100,7 +87,7 @@ class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): perms_map = {'*':'*'} queryset = InputMaterial.objects.select_related('material').all() serializer_class = InputMaterialSerializer - filterset_fields = ['process', 'product'] + filterset_fields = ['subproduction'] ordering = ['sort', '-create_time'] def get_serializer_class(self): @@ -117,7 +104,7 @@ class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): perms_map = {'*':'*'} queryset = OutputMaterial.objects.select_related('material').all() serializer_class = OutputMaterialSerializer - filterset_fields = ['process', 'product'] + filterset_fields = ['subproduction'] ordering = ['sort', '-create_time'] def get_serializer_class(self): @@ -127,18 +114,20 @@ class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): return OutputMaterialUpdateSerializer return OutputMaterialSerializer -class UsedStepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet): +class UsedStepViewSet(OptimizationMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet): """ 产品生产子工序表 """ perms_map = {'*':'*'} queryset = UsedStep.objects.all() - filterset_fields = ['process', 'product', 'step'] + filterset_fields = ['subproduction', 'step'] ordering = ['step__sort', '-step__create_time'] def get_serializer_class(self): if self.action =='create': return UsedStepCreateSerializer + elif self.action == 'update': + return UsedStepUpdateSerializer return UsedStepListSerializer class RecordFormViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): @@ -189,7 +178,7 @@ class TechDocViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): """ perms_map = {'*':'*'} queryset = TechDoc.objects.select_related('file').all() - filterset_fields = ['process', 'product'] + filterset_fields = ['subproduction'] search_fields = ['name'] ordering = ['-id'] diff --git a/hb_server/apps/pm/__init__.py b/hb_server/apps/pm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hb_server/apps/pm/admin.py b/hb_server/apps/pm/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/hb_server/apps/pm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/hb_server/apps/pm/apps.py b/hb_server/apps/pm/apps.py new file mode 100644 index 0000000..c0c4b31 --- /dev/null +++ b/hb_server/apps/pm/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + +class SamConfig(AppConfig): + name = 'apps.pm' + verbose_name = '生产计划管理' + + diff --git a/hb_server/apps/pm/migrations/0001_initial.py b/hb_server/apps/pm/migrations/0001_initial.py new file mode 100644 index 0000000..7f16d5a --- /dev/null +++ b/hb_server/apps/pm/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 3.2.6 on 2021-10-08 08:02 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mtm', '0018_material_count'), + ('sam', '0004_order_planed_count'), + ] + + operations = [ + migrations.CreateModel( + name='ProductionPlan', + 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='删除标记')), + ('number', models.CharField(max_length=50, unique=True, verbose_name='编号')), + ('count', models.IntegerField(default=0, verbose_name='生产数量')), + ('start_date', models.DateField(verbose_name='计划开工日期')), + ('end_date', models.DateField(verbose_name='计划完工日期')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='productionplan_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('order', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='sam.order', verbose_name='关联订单')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='生产产品')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='productionplan_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), + ], + options={ + 'verbose_name': '生产计划', + 'verbose_name_plural': '生产计划', + }, + ), + ] diff --git a/hb_server/apps/pm/migrations/0002_alter_productionplan_count.py b/hb_server/apps/pm/migrations/0002_alter_productionplan_count.py new file mode 100644 index 0000000..6a41103 --- /dev/null +++ b/hb_server/apps/pm/migrations/0002_alter_productionplan_count.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-10-08 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pm', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='productionplan', + name='count', + field=models.IntegerField(default=1, verbose_name='生产数量'), + ), + ] diff --git a/hb_server/apps/pm/migrations/__init__.py b/hb_server/apps/pm/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hb_server/apps/pm/models.py b/hb_server/apps/pm/models.py new file mode 100644 index 0000000..062e0f1 --- /dev/null +++ b/hb_server/apps/pm/models.py @@ -0,0 +1,30 @@ +from apps.system.models import CommonAModel +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.db.models.base import Model +import django.utils.timezone as timezone +from django.db.models.query import QuerySet + +from utils.model import SoftModel, BaseModel +from apps.mtm.models import Material +from apps.sam.models import Order + + +class ProductionPlan(CommonAModel): + """ + 生产计划 + """ + number = models.CharField('编号', max_length=50, unique=True) + order = models.ForeignKey(Order, verbose_name='关联订单', null=True, blank=True, on_delete=models.SET_NULL) + product = models.ForeignKey(Material, verbose_name='生产产品', on_delete=models.CASCADE) + count = models.IntegerField('生产数量', default=1) + start_date = models.DateField('计划开工日期') + end_date = models.DateField('计划完工日期') + + class Meta: + verbose_name = '生产计划' + verbose_name_plural = verbose_name + + def __str__(self): + return self.number + diff --git a/hb_server/apps/pm/serializers.py b/hb_server/apps/pm/serializers.py new file mode 100644 index 0000000..e3975c1 --- /dev/null +++ b/hb_server/apps/pm/serializers.py @@ -0,0 +1,17 @@ +from apps.pm.models import ProductionPlan +from rest_framework import serializers +from apps.sam.serializers import OrderSerializer +from apps.mtm.serializers import MaterialSimpleSerializer + + +class ProductionPlanCreateFromOrderSerializer(serializers.ModelSerializer): + class Meta: + model = ProductionPlan + fields = ['order', 'number', 'count', 'start_date', 'end_date'] + +class ProductionPlanSerializer(serializers.ModelSerializer): + order_ = OrderSerializer(source='order', read_only=True) + product_ = MaterialSimpleSerializer(source='product', read_only=True) + class Meta: + model = ProductionPlan + fields ='__all__' diff --git a/hb_server/apps/pm/tests.py b/hb_server/apps/pm/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/hb_server/apps/pm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/hb_server/apps/pm/urls.py b/hb_server/apps/pm/urls.py new file mode 100644 index 0000000..69e52f0 --- /dev/null +++ b/hb_server/apps/pm/urls.py @@ -0,0 +1,13 @@ +from apps.pm.views import ProductionPlanViewSet +from django.db.models import base +from rest_framework import urlpatterns +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() +router.register('productionplan', ProductionPlanViewSet, basename='productionplan') + +urlpatterns = [ + path('', include(router.urls)), +] + diff --git a/hb_server/apps/pm/views.py b/hb_server/apps/pm/views.py new file mode 100644 index 0000000..a0d28fa --- /dev/null +++ b/hb_server/apps/pm/views.py @@ -0,0 +1,56 @@ +from rest_framework.views import APIView +from apps.system.mixins import CreateUpdateModelAMixin +from apps.pm.serializers import ProductionPlanCreateFromOrderSerializer, ProductionPlanSerializer +from rest_framework.mixins import CreateModelMixin, ListModelMixin +from apps.pm.models import ProductionPlan +from rest_framework.viewsets import GenericViewSet, ModelViewSet +from django.shortcuts import render +from apps.sam.models import Order +from rest_framework.exceptions import APIException +from rest_framework.response import Response +# Create your views here. + +def updateOrderPlanedCount(order): + """ + 更新订单已排数量 + """ + planed_count = 0 + plans = ProductionPlan.objects.filter(order=order) + for i in plans: + planed_count = planed_count + i.count + order.planed_count = planed_count + order.save() + +class ProductionPlanViewSet(CreateUpdateModelAMixin, ListModelMixin, CreateModelMixin, GenericViewSet): + """ + 生产计划 + """ + perms_map = {'*': '*'} + queryset = ProductionPlan.objects.select_related('order', 'order__contract', 'product') + serializer_class = ProductionPlanSerializer + search_fields = ['number'] + filterset_fields = [] + ordering_fields = ['id'] + ordering = ['-id'] + + def get_serializer_class(self): + if self.action in ['create']: + return ProductionPlanCreateFromOrderSerializer + return ProductionPlanSerializer + + def create(self, request, *args, **kwargs): + data = request.data + serializer = self.get_serializer(data=data) + serializer.is_valid(raise_exception=True) + if data.get('order', None): + order = Order.objects.get(pk=data['order']) + if order.planed_count >= data['count'] or data['count'] > 0: + pass + else: + raise APIException('排产数量错误') + instance = serializer.save(create_by=request.user, product=order.product) + updateOrderPlanedCount(instance.order) + return Response() + +class ResourceCalculate(APIView): + pass \ No newline at end of file diff --git a/hb_server/apps/sam/migrations/0004_order_planed_count.py b/hb_server/apps/sam/migrations/0004_order_planed_count.py new file mode 100644 index 0000000..df4b48a --- /dev/null +++ b/hb_server/apps/sam/migrations/0004_order_planed_count.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-10-08 07:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sam', '0003_contract_invoice'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='planed_count', + field=models.IntegerField(default=0, verbose_name='已排数量'), + ), + ] diff --git a/hb_server/apps/sam/models.py b/hb_server/apps/sam/models.py index 4683844..583fe87 100644 --- a/hb_server/apps/sam/models.py +++ b/hb_server/apps/sam/models.py @@ -5,6 +5,7 @@ from django.db.models.base import Model import django.utils.timezone as timezone from django.db.models.query import QuerySet + from utils.model import SoftModel, BaseModel from apps.mtm.models import Material @@ -62,6 +63,7 @@ class Order(CommonAModel): contract = models.ForeignKey(Contract, verbose_name='所属合同', null=True, blank=True, on_delete=models.SET_NULL) product = models.ForeignKey(Material, verbose_name='所需产品', on_delete=models.CASCADE) count = models.IntegerField('所需数量', default=0) + planed_count = models.IntegerField('已排数量', default=0) delivery_date = models.DateField('交货日期') class Meta: verbose_name = '订单信息' diff --git a/hb_server/apps/sam/views.py b/hb_server/apps/sam/views.py index 8f66873..f1f4be7 100644 --- a/hb_server/apps/sam/views.py +++ b/hb_server/apps/sam/views.py @@ -3,7 +3,9 @@ from apps.sam.models import Contract, Customer, Order from rest_framework.viewsets import ModelViewSet from apps.system.mixins import CreateUpdateCustomMixin from django.shortcuts import render - +from rest_framework.decorators import action +from django.db.models import F +from rest_framework.response import Response # Create your views here. class CustomerViewSet(CreateUpdateCustomMixin, ModelViewSet): """ @@ -55,4 +57,14 @@ class OrderViewSet(CreateUpdateCustomMixin, ModelViewSet): def get_serializer_class(self): if self.action in ['create', 'update']: return OrderCreateUpdateSerializer - return OrderSerializer \ No newline at end of file + return OrderSerializer + + @action(methods=['get'], detail=False, perms_map={'get':'*'}) + def toplan(self, request, pk=None): + queryset = Order.objects.filter(count__gt=F('planed_count')).order_by('-id') + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) \ No newline at end of file diff --git a/hb_server/apps/wf/filters.py b/hb_server/apps/wf/filters.py new file mode 100644 index 0000000..7e2990c --- /dev/null +++ b/hb_server/apps/wf/filters.py @@ -0,0 +1,24 @@ +from django_filters import rest_framework as filters +from .models import Ticket +class TicketFilterSet(filters.FilterSet): + start_create = filters.DateFilter(field_name="create_time", lookup_expr='gte') + end_create = filters.DateFilter(field_name="create_time", lookup_expr='lte') + category = filters.ChoiceFilter(choices = Ticket.category_choices, method='filter_category') + + class Meta: + model = Ticket + fields = ['workflow', 'state', 'act_state', 'start_create', 'end_create', 'category'] + + def filter_category(self, queryset, name, value): + user=self.request.user + if value == 'owner': + queryset = queryset.filter(create_by=user) + elif value == 'duty': + queryset = queryset.filter(participant__contains=user.id).exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED]) + elif value == 'worked': + queryset = queryset.filter(ticketflow_ticket__participant=user).exclude(create_by=user) + elif value == 'all': + pass + else: + queryset = queryset.none() + return queryset \ No newline at end of file diff --git a/hb_server/apps/wf/migrations/0011_auto_20210930_0954.py b/hb_server/apps/wf/migrations/0011_auto_20210930_0954.py new file mode 100644 index 0000000..fedb874 --- /dev/null +++ b/hb_server/apps/wf/migrations/0011_auto_20210930_0954.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.6 on 2021-09-30 01:54 + +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', '0010_alter_ticketflow_transition'), + ] + + operations = [ + migrations.AlterField( + model_name='state', + name='state_fields', + field=models.JSONField(default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选. 示例:{"create_time":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='表单字段'), + ), + migrations.AlterField( + model_name='ticketflow', + name='participant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticketflow_participant', to=settings.AUTH_USER_MODEL, verbose_name='处理人'), + ), + migrations.AlterField( + model_name='ticketflow', + name='ticket', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticketflow_ticket', to='wf.ticket', verbose_name='关联工单'), + ), + ] diff --git a/hb_server/apps/wf/migrations/0012_ticketflow_intervene_type.py b/hb_server/apps/wf/migrations/0012_ticketflow_intervene_type.py new file mode 100644 index 0000000..c6c9534 --- /dev/null +++ b/hb_server/apps/wf/migrations/0012_ticketflow_intervene_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.6 on 2021-10-12 08:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wf', '0011_auto_20210930_0954'), + ] + + operations = [ + migrations.AddField( + model_name='ticketflow', + name='intervene_type', + field=models.IntegerField(choices=[(0, '正常处理'), (1, '转交'), (2, '加签'), (3, '加签处理完成'), (4, '接单'), (5, '评论'), (6, '删除'), (7, '强制关闭'), (8, '强制修改状态'), (9, 'hook操作'), (10, '撤回')], default=0, help_text='流转类型', verbose_name='干预类型'), + ), + ] diff --git a/hb_server/apps/wf/models.py b/hb_server/apps/wf/models.py index c4cfc29..4878086 100644 --- a/hb_server/apps/wf/models.py +++ b/hb_server/apps/wf/models.py @@ -39,7 +39,7 @@ class State(CommonAModel): PARTICIPANT_TYPE_ROBOT = 6 PARTICIPANT_TYPE_FIELD = 7 PARTICIPANT_TYPE_PARENT_FIELD = 8 - type2_choices = ( + state_participanttype_choices = ( (0, '无处理人'), (PARTICIPANT_TYPE_PERSONAL, '个人'), (PARTICIPANT_TYPE_MULTI, '多人'), @@ -70,9 +70,9 @@ class State(CommonAModel): sort = models.IntegerField('状态顺序', default=0, help_text='用于工单步骤接口时,step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前') type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)') enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态') - 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_type = models.IntegerField('参与者类型', choices=state_participanttype_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:不显示, 字典的字典 + state_fields = models.JSONField('表单字段', default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选. 示例:{"create_time":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, choices=state_distribute_choices, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)') class Transition(CommonAModel): @@ -87,6 +87,31 @@ class Transition(CommonAModel): (2, '拒绝'), (3, '其他') ) + TRANSITION_INTERVENE_TYPE_DELIVER = 1 # 转交操作 + TRANSITION_INTERVENE_TYPE_ADD_NODE = 2 # 加签操作 + TRANSITION_INTERVENE_TYPE_ADD_NODE_END = 3 # 加签处理完成 + TRANSITION_INTERVENE_TYPE_ACCEPT = 4 # 接单操作 + TRANSITION_INTERVENE_TYPE_COMMENT = 5 # 评论操作 + TRANSITION_INTERVENE_TYPE_DELETE = 6 # 删除操作 + TRANSITION_INTERVENE_TYPE_CLOSE = 7 # 强制关闭操作 + TRANSITION_INTERVENE_TYPE_ALTER_STATE = 8 # 强制修改状态操作 + TRANSITION_INTERVENE_TYPE_HOOK = 9 # hook操作 + TRANSITION_INTERVENE_TYPE_RETREAT = 10 # 撤回 + + intervene_type_choices = ( + (0, '正常处理'), + (TRANSITION_INTERVENE_TYPE_DELIVER, '转交'), + (TRANSITION_INTERVENE_TYPE_ADD_NODE, '加签'), + (TRANSITION_INTERVENE_TYPE_ADD_NODE_END, '加签处理完成'), + (TRANSITION_INTERVENE_TYPE_ACCEPT, '接单'), + (TRANSITION_INTERVENE_TYPE_COMMENT, '评论'), + (TRANSITION_INTERVENE_TYPE_DELETE, '删除'), + (TRANSITION_INTERVENE_TYPE_CLOSE, '强制关闭'), + (TRANSITION_INTERVENE_TYPE_ALTER_STATE, '强制修改状态'), + (TRANSITION_INTERVENE_TYPE_HOOK, 'hook操作'), + (TRANSITION_INTERVENE_TYPE_RETREAT, '撤回') + ) + name = models.CharField('操作', max_length=50) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效') @@ -149,6 +174,13 @@ class Ticket(CommonAModel): (TICKET_ACT_STATE_FINISH, '已完成'), (TICKET_ACT_STATE_CLOSED, '已关闭') ) + category_choices =( + ('all', '全部'), + ('owner', '我创建的'), + ('duty', '代办'), + ('worked', '我处理的'), + ('relation', '抄送我的') + ) title = models.CharField('标题', max_length=500, blank=True, default='', help_text="工单标题") workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流') sn = models.CharField('流水号', max_length=25, help_text="工单的流水号") @@ -159,7 +191,7 @@ class Ticket(CommonAModel): in_add_node = models.BooleanField('加签状态中', default=False, 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_type = models.IntegerField('当前处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.state_participanttype_choices) participant = models.JSONField('当前处理人', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表') act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices) multi_all_person = models.JSONField('全部处理的结果', default=dict, blank=True, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式') @@ -168,10 +200,11 @@ class TicketFlow(BaseModel): """ 工单流转日志 """ - ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='关联工单') + ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, verbose_name='关联工单', related_name='ticketflow_ticket') transition = models.ForeignKey(Transition, verbose_name='流转id', help_text='与worklow.Transition关联, 为0时表示认为干预的操作', on_delete=models.CASCADE, null=True, blank=True) suggestion = models.CharField('处理意见', max_length=10000, 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) + participant_type = models.IntegerField('处理人类型', default=0, help_text='0.无处理人,1.个人,2.多人', choices=State.state_participanttype_choices) + participant = models.ForeignKey(User, verbose_name='处理人', on_delete=models.SET_NULL, null=True, blank=True, related_name='ticketflow_participant') 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 + ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据,json格式') + intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices) \ No newline at end of file diff --git a/hb_server/apps/wf/serializers.py b/hb_server/apps/wf/serializers.py index e674c31..9075341 100644 --- a/hb_server/apps/wf/serializers.py +++ b/hb_server/apps/wf/serializers.py @@ -23,7 +23,7 @@ class WorkflowSimpleSerializer(serializers.ModelSerializer): class StateSimpleSerializer(serializers.ModelSerializer): class Meta: model = State - fields = ['id', 'name', 'type'] + fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat'] class TransitionSerializer(serializers.ModelSerializer): source_state_ = StateSimpleSerializer(source='source_state', read_only=True) @@ -71,6 +71,43 @@ class TicketSerializer(serializers.ModelSerializer): queryset = queryset.select_related('workflow','state') return queryset +class TicketListSerializer(serializers.ModelSerializer): + workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True) + state_ = StateSimpleSerializer(source='state', read_only=True) + + class Meta: + model = Ticket + fields = ['id', 'title', 'sn', 'workflow', 'workflow_', 'state', 'state_', 'act_state', 'create_time', 'update_time', 'participant_type', 'create_by'] + + @staticmethod + def setup_eager_loading(queryset): + queryset = queryset.select_related('workflow','state') + return queryset + +class TicketDetailSerializer(serializers.ModelSerializer): + workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True) + state_ = StateSimpleSerializer(source='state', read_only=True) + ticket_data_ = serializers.SerializerMethodField() + class Meta: + model = Ticket + fields = '__all__' + + @staticmethod + def setup_eager_loading(queryset): + queryset = queryset.select_related('workflow','state') + return queryset + + def get_ticket_data_(self, obj): + ticket_data = obj.ticket_data + state_fields = obj.state.state_fields + all_fields = CustomField.objects.filter(workflow=obj.workflow).order_by('sort') + all_fields_l = CustomFieldSerializer(instance=all_fields, many=True).data + for i in all_fields_l: + key = i['field_key'] + i['field_state'] = state_fields.get(key, 1) + i['field_value'] = ticket_data.get(key, None) + return all_fields_l + class TicketFlowSerializer(serializers.ModelSerializer): participant_ = UserSimpleSerializer(source='participant', read_only=True) state_ = StateSimpleSerializer(source='state', read_only=True) @@ -78,8 +115,18 @@ class TicketFlowSerializer(serializers.ModelSerializer): model = TicketFlow fields = '__all__' +class TicketFlowSimpleSerializer(serializers.ModelSerializer): + participant_ = UserSimpleSerializer(source='participant', read_only=True) + state_ = StateSimpleSerializer(source='state', read_only=True) + class Meta: + model = TicketFlow + exclude = ['ticket_data'] + class TicketHandleSerializer(serializers.Serializer): transition = serializers.IntegerField(label="流转id") ticket_data = serializers.JSONField(label="表单数据json") - suggestion = serializers.CharField(label="处理意见", required = False) \ No newline at end of file + suggestion = serializers.CharField(label="处理意见", required = False) + +class TicketRetreatSerializer(serializers.Serializer): + suggestion = serializers.CharField(label="撤回原因", required = False) \ No newline at end of file diff --git a/hb_server/apps/wf/services.py b/hb_server/apps/wf/services.py index bee8799..c86fcb7 100644 --- a/hb_server/apps/wf/services.py +++ b/hb_server/apps/wf/services.py @@ -1,3 +1,4 @@ +from apps.wf.serializers import CustomFieldSerializer from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer from typing import Tuple from apps.system.models import User @@ -56,10 +57,11 @@ class WfService(object): @classmethod def get_ticket_steps(cls, ticket:Ticket): steps = cls.get_worlflow_states(ticket.workflow) + nsteps_list = [] for i in steps: - if ticket.state.is_hidden and ticket.state != i: - steps.remove(i) - return steps + if ticket.state == i or (not i.is_hidden): + nsteps_list.append(i) + return nsteps_list @classmethod def get_ticket_transitions(cls, ticket:Ticket): @@ -90,20 +92,20 @@ class WfService(object): @classmethod - def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition)->object: + def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, ticket_data:dict={})->object: """ 获取下个节点状态 """ - # if ticket: # 如果是新建工单 - # source_state = ticket.state - # else: - # source_state = cls.get_workflow_start_state(workflow) - # if transition.source_state != source_state: - # raise APIException('流转错误') source_state = ticket.state destination_state = transition.destination_state + ticket_all_value = cls.get_ticket_all_field_value(ticket) + ticket_all_value.update(**ticket_data) if transition.condition_expression: - pass + for i in transition.condition_expression: + expression = i['expression'].format(**ticket_all_value) + import datetime, time # 用于支持条件表达式中对时间的操作 + if eval(expression): + destination_state = State.objects.get(pk=i['target_state']) return destination_state @classmethod @@ -163,21 +165,23 @@ class WfService(object): state = ticket.state if participant_type == State.PARTICIPANT_TYPE_PERSONAL: if user.id != participant: - return dict(permission=False, msg="非当前处理人") + return dict(permission=False, msg="非当前处理人", need_accept=False) 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="非当前处理人") + return dict(permission=False, msg="非当前处理人", need_accept=False) current_participant_count = len(participant) if current_participant_count == 1: - if [user.id] != participant: - return dict(permission=False, msg="非当前处理人") + if [user.id] == participant or user.id == participant: + pass + else: + return dict(permission=False, msg="非当前处理人", need_accept=False) elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE: if user.id not in participant: - return dict(permission=False, msg="非当前处理人") + return dict(permission=False, msg="非当前处理人", need_accept=False) return dict(permission=False, msg="需要先接单再处理", need_accept=True) if ticket.in_add_node: - return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作") - return dict(permission=True, msg="") + return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作", need_accept=False) + return dict(permission=True, msg="", need_accept=False) @classmethod def check_dict_has_all_same_value(cls, dict_obj: object)->tuple: @@ -212,3 +216,4 @@ class WfService(object): return field_info_dict + diff --git a/hb_server/apps/wf/urls.py b/hb_server/apps/wf/urls.py index 7d169f9..b03a965 100644 --- a/hb_server/apps/wf/urls.py +++ b/hb_server/apps/wf/urls.py @@ -1,6 +1,6 @@ from django.db.models import base from rest_framework import urlpatterns -from apps.wf.views import CustomFieldViewSet, StateViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet +from apps.wf.views import CustomFieldViewSet, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet from django.urls import path, include from rest_framework.routers import DefaultRouter @@ -10,6 +10,7 @@ router.register('state', StateViewSet, basename='wf_state') router.register('transition', TransitionViewSet, basename='wf_transitions') router.register('customfield', CustomFieldViewSet, basename='wf_customfield') router.register('ticket', TicketViewSet, basename='wf_ticket') +router.register('ticketflow', TicketFlowViewSet, basename='wf_ticketflow') urlpatterns = [ path('', include(router.urls)), ] diff --git a/hb_server/apps/wf/views.py b/hb_server/apps/wf/views.py index 9df0518..a1aab22 100644 --- a/hb_server/apps/wf/views.py +++ b/hb_server/apps/wf/views.py @@ -1,8 +1,9 @@ +from apps.wf.filters import TicketFilterSet 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, TicketFlowSerializer, TicketHandleSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer +from apps.wf.serializers import CustomFieldSerializer, StateSerializer, TicketCreateSerializer, TicketFlowSerializer, TicketFlowSimpleSerializer, TicketHandleSerializer, TicketRetreatSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer, TicketListSerializer, TicketDetailSerializer from django.shortcuts import get_object_or_404, render from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.decorators import action, api_view @@ -93,7 +94,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin queryset = Ticket.objects.all() serializer_class = TicketSerializer search_fields = ['title'] - filterset_fields = ['workflow', 'state'] + filterset_class = TicketFilterSet ordering = ['-create_time'] def get_serializer_class(self): @@ -101,7 +102,18 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin return TicketCreateSerializer elif self.action == 'handle': return TicketHandleSerializer + elif self.action == 'retreat': + return TicketRetreatSerializer + elif self.action == 'list': + return TicketListSerializer + elif self.action == 'retrieve': + return TicketDetailSerializer return super().get_serializer_class() + + def get_queryset(self): + if self.action=='list' and (not self.request.query_params.get('category', None)): + raise APIException('请指定查询分类') + return super().get_queryset() def create(self, request, *args, **kwargs): """ @@ -118,7 +130,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin if value == State.STATE_FIELD_REQUIRED: if key not in ticket_data or not ticket_data[key]: raise APIException('字段{}必填'.format(key)) - ticket = serializer.save(state=start_state, create_by=request.user) # 先创建出来 + ticket = serializer.save(state=start_state, create_by=request.user, act_state=Ticket.TICKET_ACT_STATE_DRAFT) # 先创建出来 next_state = WfService.get_next_state_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) @@ -152,7 +164,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin return Response(TicketSerializer(instance=ticket).data) - @action(methods=['post'], detail=True, perms_map={'get':'*'}) + @action(methods=['post'], detail=True, perms_map={'post':'*'}) def handle(self, request, pk=None): """ 处理工单 @@ -175,7 +187,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin if value == State.STATE_FIELD_REQUIRED: if key not in ticket_data or not ticket_data[key]: raise APIException('字段{}必填'.format(key)) - destination_state = WfService.get_next_state_by_transition_and_ticket_info(ticket, transition) + destination_state = WfService.get_next_state_by_transition_and_ticket_info(ticket, transition, ticket_data) multi_all_person = ticket.multi_all_person if multi_all_person: multi_all_person[request.user.id] =dict(transition=transition.id) @@ -237,6 +249,16 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin ticket = self.get_object() steps = WfService.get_ticket_steps(ticket) return Response(StateSerializer(instance=steps, many=True).data) + + @action(methods=['get'], detail=True, perms_map={'get':'*'}) + def flowlogs(self, request, pk=None): + """ + 工单流转记录 + """ + ticket = self.get_object() + flowlogs = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time') + serializer = TicketFlowSerializer(instance=flowlogs, many=True) + return Response(serializer.data) @action(methods=['get'], detail=True, perms_map={'get':'*'}) def transitions(self, request, pk=None): @@ -261,11 +283,46 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin # 接单日志 # 更新工单流转记录 TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket), - suggestion='接单处理', participant_type=State.PARTICIPANT_TYPE_PERSONAL, + suggestion='', participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_ATTRIBUTE_TYPE_ACCEPT, participant=request.user, transition=None) return Response() else: raise APIException('无需接单') + + @action(methods=['post'], detail=True, perms_map={'post':'*'}) + def retreat(self, request, pk=None): + """ + 撤回工单,允许创建人在指定状态撤回工单至初始状态,状态设置中开启允许撤回 + """ + ticket = self.get_object() + if ticket.create_by != request.user: + raise APIException('非创建人不可撤回') + if not ticket.state.enable_retreat: + raise APIException('该状态不可撤回') + start_state = WfService.get_workflow_start_state(ticket.workflow) + ticket.state = start_state + ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL + ticket.participant = request.user.id + ticket.act_state = Ticket.TICKET_ACT_STATE_RETREAT + ticket.save() + # 更新流转记录 + suggestion = request.data.get('suggestion', '') # 撤回原因 + TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket), + suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_RETREAT, + participant=request.user, transition=None) + return Response() + + @action(methods=['post'], detail=True, perms_map={'post':'*'}) + def add_node(self, request, pk=None): + """ + 加签 + """ + + def close(self, request, pk=None): + """ + 关闭工单(超级管理员或者创建人在初始状态) + """ + class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ @@ -275,5 +332,5 @@ class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): queryset = TicketFlow.objects.all() serializer_class = TicketFlowSerializer search_fields = ['suggestion'] - filterset_fields = ['paticipant', 'state', 'ticket'] + filterset_fields = ['ticket'] ordering = ['-create_time'] \ No newline at end of file diff --git a/hb_server/server/settings.py b/hb_server/server/settings.py index 652d71e..b2e7755 100644 --- a/hb_server/server/settings.py +++ b/hb_server/server/settings.py @@ -54,7 +54,8 @@ INSTALLED_APPS = [ 'apps.mtm', 'apps.inm', 'apps.sam', - 'apps.qm' + 'apps.qm', + 'apps.pm' ] MIDDLEWARE = [ diff --git a/hb_server/server/urls.py b/hb_server/server/urls.py index e27e660..af315af 100644 --- a/hb_server/server/urls.py +++ b/hb_server/server/urls.py @@ -67,7 +67,7 @@ urlpatterns = [ path('api/inm/', include('apps.inm.urls')), path('api/sam/', include('apps.sam.urls')), path('api/qm/', include('apps.qm.urls')), - + path('api/pm/', include('apps.pm.urls')), # 工具 path('api/utils/signature/', GenSignature.as_view()),