Merge branch 'develop' of https://e.coding.net/ctcdevteam/hberp/hberp into develop

This commit is contained in:
shilixia 2021-08-27 10:14:24 +08:00
commit d677cb42fb
25 changed files with 1202 additions and 30 deletions

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
class MtmConfig(AppConfig):
name = 'apps.mtm'
verbose_name = '制造技术管理'

View File

@ -0,0 +1,161 @@
# Generated by Django 3.2.6 on 2021-08-24 06:52
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),
('system', '0003_auto_20210812_0909'),
]
operations = [
migrations.CreateModel(
name='Material',
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(max_length=100, unique=True, verbose_name='物料名称')),
('number', models.CharField(max_length=100, unique=True, verbose_name='编号')),
('type', models.CharField(choices=[(1, '成品'), (2, '半成品'), (3, '原材料')], default=1, max_length=20, verbose_name='物料类型')),
('sort_str', models.CharField(blank=True, max_length=100, null=True, verbose_name='排序字符')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='material_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
],
options={
'verbose_name': '物料表',
'verbose_name_plural': '物料表',
},
),
migrations.CreateModel(
name='Process',
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(max_length=100, unique=True, verbose_name='工序名称')),
('number', models.CharField(max_length=100, unique=True, verbose_name='编号')),
('instruction_content', models.TextField(blank=True, null=True, verbose_name='指导书内容')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='process_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('instruction', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.file', verbose_name='指导书')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='process_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '工序',
'verbose_name_plural': '工序',
},
),
migrations.CreateModel(
name='Step',
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(max_length=100, verbose_name='工序步骤名称')),
('number', models.CharField(blank=True, max_length=100, null=True, verbose_name='步骤编号')),
('instruction_content', models.TextField(blank=True, null=True, 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='step_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('process', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.process', verbose_name='所属工序')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='step_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '工序步骤',
'verbose_name_plural': '工序步骤',
},
),
migrations.CreateModel(
name='StepOperationItem',
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='删除标记')),
('field_type', models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('textarea', '文本域')], max_length=50, verbose_name='类型')),
('field_key', models.CharField(help_text='字段类型请尽量特殊,避免与系统中关键字冲突', max_length=50, verbose_name='字段标识')),
('field_name', models.CharField(max_length=50, verbose_name='字段名称')),
('boolean_field_display', models.JSONField(blank=True, default=dict, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号', verbose_name='布尔类型显示名')),
('field_choice', models.JSONField(blank=True, default=dict, help_text='radio,checkbox,select,multiselect类型可供选择的选项格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号', verbose_name='radio、checkbox、select的选项')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stepoperationitem_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.step', verbose_name='关联步骤')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='stepoperationitem_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '操作记录条目',
'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=[
('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.FloatField(default=0, verbose_name='产出量')),
('unit', models.CharField(max_length=20, verbose_name='单位')),
('create_by', 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='创建人')),
('material', 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='outputmaterial_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '输出物料',
'verbose_name_plural': '输出物料',
},
),
migrations.AddField(
model_name='material',
name='process',
field=models.ManyToManyField(related_name='product_process', through='mtm.ProductProcess', to='mtm.Process'),
),
migrations.AddField(
model_name='material',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='material_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.CreateModel(
name='InputMaterial',
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.FloatField(default=0, verbose_name='消耗量')),
('unit', models.CharField(max_length=20, verbose_name='单位')),
('create_by', 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='创建人')),
('material', 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='inputmaterial_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'verbose_name': '输入物料',
'verbose_name_plural': '输入物料',
},
),
]

View File

@ -0,0 +1,132 @@
from django.db import models
from django.db.models.base import Model
import django.utils.timezone as timezone
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
class Material(CommonAModel):
"""
物料
"""
type_choices=(
(1, '成品'),
(2, '半成品'),
(3, '原材料')
)
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')
class Meta:
verbose_name = '物料表'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Process(CommonAModel):
"""
工序
"""
name = models.CharField('工序名称', max_length=100, unique=True)
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)
class Meta:
verbose_name = '工序'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Step(CommonAModel):
"""
工序步骤
"""
process = models.ForeignKey(Process, on_delete=models.CASCADE, verbose_name='所属工序')
name = models.CharField('工序步骤名称', max_length=100)
number = models.CharField('步骤编号', max_length=100, null=True, blank=True)
instruction_content = models.TextField('相应操作指导', null=True, blank=True)
sort = models.IntegerField('排序号', default=1)
class Meta:
verbose_name = '工序步骤'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class StepOperationItem(CommonAModel):
"""
操作记录条目
"""
field_type_choices = (
('string', '字符串'),
('int', '整型'),
('float', '浮点'),
('boolean', '布尔'),
('date', '日期'),
('datetime', '日期时间'),
('radio', '单选'),
('checkbox', '多选'),
('select', '单选下拉'),
('selects', '多选下拉'),
('textarea', '文本域'),
)
step = models.ForeignKey(Step, on_delete=models.CASCADE, verbose_name='关联步骤')
field_type = models.CharField('类型', max_length=50, choices=field_type_choices)
field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突')
field_name = models.CharField('字段名称', max_length=50)
boolean_field_display = models.JSONField('布尔类型显示名', default=dict, blank=True,
help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号')
field_choice = models.JSONField('radio、checkbox、select的选项', default=dict, blank=True,
help_text='radio,checkbox,select,multiselect类型可供选择的选项格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号')
sort = models.IntegerField('排序号', default=1)
class Meta:
verbose_name = '操作记录条目'
verbose_name_plural = verbose_name
def __str__(self):
return self.field_key + '-' + self.field_name
class ProductProcess(CommonAModel):
"""
产品生产工艺
"""
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)
number = models.FloatField('消耗量', default=0)
unit = models.CharField('单位', max_length=20)
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)
class Meta:
verbose_name = '输出物料'
verbose_name_plural = verbose_name

View File

@ -0,0 +1,43 @@
from rest_framework.serializers import ModelSerializer
from .models import Material, Process, ProductProcess, Step
from apps.system.serializers import FileSimpleSerializer
class MaterialSerializer(ModelSerializer):
class Meta:
model = Material
fields = '__all__'
class MaterialSimpleSerializer(ModelSerializer):
class Meta:
model = Material
fields = ['id', 'name', 'number']
class ProcessSerializer(ModelSerializer):
instruction_ = FileSimpleSerializer(source='instruction', read_only=True)
class Meta:
model = Process
fields = '__all__'
class ProcessSimpleSerializer(ModelSerializer):
class Meta:
model = Process
fields = ['id', 'name', 'number']
class StepSerializer(ModelSerializer):
class Meta:
model = Step
fields = '__all__'
class ProductProcessListSerializer(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 Meta:
model = ProductProcess
fields = ['sort']

View File

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

View File

@ -0,0 +1,15 @@
from django.db.models import base
from rest_framework import urlpatterns
from apps.mtm.views import MaterialViewSet, ProcessViewSet, ProductProcessViewSet, StepViewSet
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')
urlpatterns = [
path('', include(router.urls)),
]

View File

@ -0,0 +1,83 @@
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.system.mixins import CreateUpdateModelAMixin, OptimizationMixin
from rest_framework.decorators import action
from rest_framework.response import Response
from utils.pagination import PageOrNot
# Create your views here.
class MaterialViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
"""
物料表-增删改查
"""
perms_map = {'get': '*', 'post': 'material_create',
'put': 'material_update', 'delete': 'material_delete'}
queryset = Material.objects.all()
serializer_class = MaterialSerializer
search_fields = ['name', 'number']
filterset_fields = ['type']
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)
class ProcessViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
"""
工序表-增删改查
"""
perms_map = {'get': '*', 'post': 'process_create',
'put': 'process_update', 'delete': 'process_delete'}
queryset = Process.objects.select_related('instruction').all()
serializer_class = ProcessSerializer
search_fields = ['name', 'number']
filterset_fields = ['number']
ordering_fields = ['number']
ordering = ['number']
@action(methods=['get'], detail=True, perms_map={'get':'process_update'}, 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)
class StepViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
"""
"""
perms_map = {'*':'process_update'}
queryset = Step.objects.all()
serializer_class = StepSerializer
search_fields = ['name', 'number']
filterset_fields = ['process']
ordering = ['sort']
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()

View File

@ -49,6 +49,11 @@ class PTaskSerializer(serializers.ModelSerializer):
return 'crontab' return 'crontab'
return 'interval' return 'interval'
class FileSimpleSerializer(serializers.ModelSerializer):
class Meta:
model =File
fields = ['id', 'name', 'file', 'path']
class FileSerializer(serializers.ModelSerializer): class FileSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = File model = File

View File

@ -0,0 +1,106 @@
# Generated by Django 3.2.6 on 2021-08-17 05:51
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='State',
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(max_length=50, verbose_name='名称')),
('is_hidden', models.BooleanField(default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)', verbose_name='是否隐藏')),
('sort', models.IntegerField(default=0, help_text='用于工单步骤接口时step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前', verbose_name='状态顺序')),
('type', models.IntegerField(choices=[(0, '普通类型'), (1, '初始状态'), (2, '结束状态')], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理即没有对应的transition)', verbose_name='状态类型')),
('enable_retreat', models.BooleanField(default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态', verbose_name='允许撤回')),
('participant_type', models.IntegerField(blank=True, default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5参与人填creator', verbose_name='参与者类型')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Workflow',
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(max_length=50, verbose_name='名称')),
('description', models.CharField(max_length=200, verbose_name='描述')),
('view_permission_check', models.BooleanField(default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单', verbose_name='查看权限校验')),
('limit_expression', 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的用户提交工单)', max_length=1000, verbose_name='限制表达式')),
('display_form_str', models.JSONField(blank=True, default=list, help_text='默认"[]",用于用户只有对应工单查看权限时显示哪些字段,field_key的list的json,如["days","sn"],内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称)state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', max_length=10000, verbose_name='展现表单字段')),
('title_template', models.CharField(blank=True, default='你有一个待办工单:{title}', help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}', max_length=50, null=True, verbose_name='标题模板')),
('content_template', models.CharField(blank=True, default='标题:{title}, 创建时间:{create_time}', help_text='工单字段的值可以作为参数写到模板中,格式如:标题:{title}, 创建时间:{create_time}', max_length=1000, null=True, verbose_name='内容模板')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='workflow_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Transition',
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(max_length=50, verbose_name='操作')),
('timer', models.IntegerField(default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效', verbose_name='定时器(单位秒)')),
('condition_expression', models.JSONField(default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准所以多个条件不要有冲突', max_length=1000, verbose_name='条件表达式')),
('attribute_type', models.IntegerField(choices=[(1, '同意'), (2, '拒绝'), (3, '其他')], default=1, help_text='属性类型1.同意2.拒绝3.其他', verbose_name='属性类型')),
('field_require_check', models.BooleanField(default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容', verbose_name='是否校验必填项')),
('destination_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dstate_transition', to='wf.state', verbose_name='目的状态')),
('source_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sstate_transition', to='wf.state', verbose_name='源状态')),
('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='state',
name='workflow',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流'),
),
migrations.CreateModel(
name='CustomField',
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='删除标记')),
('field_type', models.IntegerField(help_text='5.字符串10.整形15.浮点型20.布尔25.日期30.日期时间35.单选框40.多选框45.下拉列表50.多选下拉列表55.文本域60.用户名, 70.多选的用户名, 80.附件(只保存路径,多个使用逗号隔开)', verbose_name='类型')),
('field_key', models.CharField(help_text='字段类型请尽量特殊,避免与系统中关键字冲突', max_length=50, verbose_name='字段标识')),
('field_name', models.CharField(max_length=50, verbose_name='字段名称')),
('sort', models.IntegerField(default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列', verbose_name='排序')),
('default_value', models.CharField(blank=True, help_text='前端展示时,可以将此内容作为表单中的该字段的默认值', max_length=100, null=True, verbose_name='默认值')),
('description', models.CharField(blank=True, default='', help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述', max_length=100, verbose_name='描述')),
('placeholder', models.CharField(blank=True, default='', help_text='用户工单详情表单中作为字段的占位符显示', max_length=100, verbose_name='占位符')),
('field_template', models.TextField(blank=True, default='', help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder', verbose_name='文本域模板')),
('boolean_field_display', models.JSONField(blank=True, default=dict, help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号', verbose_name='布尔类型显示名')),
('field_choice', models.JSONField(blank=True, default=dict, help_text='radio,checkbox,select,multiselect类型可供选择的选项格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号', verbose_name='radio、checkbox、select的选项')),
('label', models.JSONField(blank=True, default=dict, help_text='自定义标签json格式调用方可根据标签自行处理特殊场景逻辑loonflow只保存文本内容', verbose_name='标签')),
('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-08-19 01:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='field_type',
field=models.IntegerField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('textarea', '文本域'), ('selectuser', '单选用户'), ('selectusers', '多选用户'), ('file', '附件')], help_text='5.字符串10.整形15.浮点型20.布尔25.日期30.日期时间35.单选框40.多选框45.下拉列表50.多选下拉列表55.文本域60.用户名, 70.多选的用户名, 80.附件(只保存路径,多个使用逗号隔开)', verbose_name='类型'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2021-08-19 03:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0002_alter_customfield_field_type'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='field_type',
field=models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('textarea', '文本域'), ('selectuser', '单选用户'), ('selectusers', '多选用户'), ('file', '附件')], help_text='5.字符串10.整形15.浮点型20.布尔25.日期30.日期时间35.单选框40.多选框45.下拉列表50.多选下拉列表55.文本域60.用户名, 70.多选的用户名, 80.附件(只保存路径,多个使用逗号隔开)', max_length=50, verbose_name='类型'),
),
]

View File

@ -0,0 +1,111 @@
# Generated by Django 3.2.6 on 2021-08-23 07:46
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0003_alter_customfield_field_type'),
]
operations = [
migrations.CreateModel(
name='Ticket',
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='删除标记')),
('title', models.CharField(blank=True, default='', help_text='工单标题', max_length=500, verbose_name='标题')),
('sn', models.CharField(help_text='工单的流水号', max_length=25, verbose_name='流水号')),
('ticket_data', models.JSONField(default=dict, help_text='工单所有字段内容', verbose_name='工单数据')),
('in_add_node', models.BooleanField(default=False, help_text='是否处于加签状态下', verbose_name='加签状态中')),
('add_node_man', models.CharField(blank=True, default='', help_text='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效', max_length=50, verbose_name='加签人')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='wf.ticket', verbose_name='父工单')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='customfield',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AddField(
model_name='customfield',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AddField(
model_name='state',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AddField(
model_name='state',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AddField(
model_name='transition',
name='create_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人'),
),
migrations.AddField(
model_name='transition',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_update_by', to=settings.AUTH_USER_MODEL, 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参与人填creator', verbose_name='参与者类型'),
),
migrations.CreateModel(
name='TicketFlow',
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='删除标记')),
('suggestion', models.CharField(blank=True, default='', max_length=10000, verbose_name='处理意见')),
('participant', models.CharField(blank=True, default='', max_length=50, verbose_name='处理人')),
('ticket_data', models.JSONField(blank=True, default=dict, help_text='可以用于记录当前表单数据json格式', verbose_name='工单数据')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticketflow_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('state', models.ForeignKey(blank=True, default=0, on_delete=django.db.models.deletion.CASCADE, to='wf.state', verbose_name='当前状态')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.ticket', verbose_name='关联工单')),
('transition', models.ForeignKey(help_text='与worklow.Transition关联 为0时表示认为干预的操作', on_delete=django.db.models.deletion.CASCADE, to='wf.transition', verbose_name='流转id')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticketflow_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='ticket',
name='parent_state',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='ticket_parent_state', to='wf.state', verbose_name='父工单状态'),
),
migrations.AddField(
model_name='ticket',
name='state',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_state', to='wf.state', verbose_name='当前状态'),
),
migrations.AddField(
model_name='ticket',
name='update_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'),
),
migrations.AddField(
model_name='ticket',
name='workflow',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='关联工作流'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 3.2.6 on 2021-08-23 07:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wf', '0004_auto_20210823_1546'),
]
operations = [
migrations.RemoveField(
model_name='ticketflow',
name='create_by',
),
migrations.RemoveField(
model_name='ticketflow',
name='update_by',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.6 on 2021-08-25 07:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wf', '0005_auto_20210823_1548'),
]
operations = [
migrations.AddField(
model_name='state',
name='state_fields',
field=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:工作流名称', verbose_name='表单字段'),
),
migrations.AlterField(
model_name='state',
name='type',
field=models.IntegerField(choices=[(0, '普通类型'), (1, 1), (2, 2)], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理即没有对应的transition)', verbose_name='状态类型'),
),
]

View File

View File

@ -12,18 +12,53 @@ class Workflow(CommonAModel):
工作流 工作流
""" """
name = models.CharField('名称', max_length=50) name = models.CharField('名称', max_length=50)
sn_prefix = models.CharField('流水号前缀', max_length=50)
description = models.CharField('描述', max_length=200) description = models.CharField('描述', max_length=200)
view_permission_check = models.BooleanField('查看权限校验', default=True, help_text='开启后,只允许工单的关联人(创建人、曾经的处理人)有权限查看工单') 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:工作流名称')
title_template = models.CharField('标题模板', max_length=50, default='你有一个待办工单:{title}', null=True, blank=True, help_text='工单字段的值可以作为参数写到模板中,格式如:你有一个待办工单:{title}') 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}')
class State(BaseModel): class State(CommonAModel):
""" """
状态记录 状态记录
""" """
STATE_TYPE_START = 1
STATE_TYPE_END = 2
type_choices = ( type_choices = (
(0, '普通类型'), (0, '普通'),
(1, '初始状态'), (STATE_TYPE_START, '开始'),
(2, '结束状态') (STATE_TYPE_END, '结束')
)
PARTICIPANT_TYPE_PERSONAL = 1
PARTICIPANT_TYPE_MULTI = 2
PARTICIPANT_TYPE_DEPT = 3
PARTICIPANT_TYPE_ROLE = 4
PARTICIPANT_TYPE_VARIABLE = 5
PARTICIPANT_TYPE_ROBOT = 6
PARTICIPANT_TYPE_FIELD = 7
PARTICIPANT_TYPE_PARENT_FIELD = 8
type2_choices = (
(0, '无处理人'),
(PARTICIPANT_TYPE_PERSONAL, '个人'),
(PARTICIPANT_TYPE_MULTI, '多人'),
(PARTICIPANT_TYPE_DEPT, '部门'),
(PARTICIPANT_TYPE_ROLE, '角色'),
(PARTICIPANT_TYPE_VARIABLE, '变量'),
(PARTICIPANT_TYPE_ROBOT, '脚本'),
(PARTICIPANT_TYPE_FIELD, '工单的字段'),
(PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段')
)
STATE_DISTRIBUTE_TYPE_ACTIVE = 1 # 主动接单
STATE_DISTRIBUTE_TYPE_DIRECT = 2 # 直接处理(当前为多人的情况,都可以处理,而不需要先接单)
STATE_DISTRIBUTE_TYPE_RANDOM = 3 # 随机分配
STATE_DISTRIBUTE_TYPE_ALL = 4 # 全部处理
state_distribute_choices=(
(STATE_DISTRIBUTE_TYPE_ACTIVE, '主动接单'),
(STATE_DISTRIBUTE_TYPE_DIRECT, '直接处理'),
(STATE_DISTRIBUTE_TYPE_RANDOM, '随机分配'),
(STATE_DISTRIBUTE_TYPE_ALL, '全部处理'),
) )
name = models.CharField('名称', max_length=50) name = models.CharField('名称', max_length=50)
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
@ -31,11 +66,12 @@ class State(BaseModel):
sort = models.IntegerField('状态顺序', default=0, help_text='用于工单步骤接口时step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前') sort = models.IntegerField('状态顺序', default=0, help_text='用于工单步骤接口时step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前')
type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理即没有对应的transition)') type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理即没有对应的transition)')
enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态') 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 = 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.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
participant_type = models.IntegerField('参与者类型', default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5参与人填creator') class Transition(CommonAModel):
class Transition(BaseModel):
""" """
工作流流转定时器条件(允许跳过) 条件流转与定时器不可同时存在 工作流流转定时器条件(允许跳过) 条件流转与定时器不可同时存在
""" """
@ -49,31 +85,31 @@ class Transition(BaseModel):
timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效') timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效')
source_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='源状态', related_name='sstate_transition') source_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='源状态', related_name='sstate_transition')
destination_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='目的状态', related_name='dstate_transition') destination_state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='目的状态', related_name='dstate_transition')
condition_expression = models.JSONField('条件表达式', max_length=1000, default='[]', help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准所以多个条件不要有冲突' ) condition_expression = models.JSONField('条件表达式', max_length=1000, default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准所以多个条件不要有冲突' )
attribute_type = models.IntegerField('属性类型', default=1, choices=attribute_type_choices, help_text='属性类型1.同意2.拒绝3.其他') attribute_type = models.IntegerField('属性类型', default=1, choices=attribute_type_choices, help_text='属性类型1.同意2.拒绝3.其他')
field_require_check = models.BooleanField('是否校验必填项', default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容') field_require_check = models.BooleanField('是否校验必填项', default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容')
class CustomField(BaseModel): class CustomField(CommonAModel):
"""自定义字段, 设定某个工作流有哪些自定义字段""" """自定义字段, 设定某个工作流有哪些自定义字段"""
field_type_choices = ( field_type_choices = (
('string', '字符串'), ('string', '字符串'),
('int', '整型'), ('int', '整型'),
('float', '浮点'), ('float', '浮点'),
('bol', '布尔'), ('boolean', '布尔'),
('date', '日期'), ('date', '日期'),
('datetime', '日期时间'), ('datetime', '日期时间'),
('radio', '单选'), ('radio', '单选'),
('checkbox', '多选'), ('checkbox', '多选'),
('select', '单选下拉'), ('select', '单选下拉'),
('mutiselect', '多选下拉'), ('selects', '多选下拉'),
('textarea', '文本域'), ('textarea', '文本域'),
('selectuser', '单选用户'), ('selectuser', '单选用户'),
('selectusers', '多选用户'), ('selectusers', '多选用户'),
('file', '附件') ('file', '附件')
) )
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
field_type = models.IntegerField('类型', help_text='5.字符串10.整形15.浮点型20.布尔25.日期30.日期时间35.单选框40.多选框45.下拉列表50.多选下拉列表55.文本域60.用户名, 70.多选的用户名, 80.附件(只保存路径,多个使用逗号隔开)') field_type = models.CharField('类型', max_length=50, choices=field_type_choices, help_text='5.字符串10.整形15.浮点型20.布尔25.日期30.日期时间35.单选框40.多选框45.下拉列表50.多选下拉列表55.文本域60.用户名, 70.多选的用户名, 80.附件(只保存路径,多个使用逗号隔开)')
field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突') field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突')
field_name = models.CharField('字段名称', max_length=50) field_name = models.CharField('字段名称', max_length=50)
sort = models.IntegerField('排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列') sort = models.IntegerField('排序', default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列')
@ -81,8 +117,53 @@ class CustomField(BaseModel):
description = models.CharField('描述', max_length=100, blank=True, default='', help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述') description = models.CharField('描述', max_length=100, blank=True, default='', help_text='字段的描述信息,可用于显示在字段的下方对该字段的详细描述')
placeholder = models.CharField('占位符', max_length=100, blank=True, default='', help_text='用户工单详情表单中作为字段的占位符显示') placeholder = models.CharField('占位符', max_length=100, blank=True, default='', help_text='用户工单详情表单中作为字段的占位符显示')
field_template = models.TextField('文本域模板', default='', blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder') field_template = models.TextField('文本域模板', default='', blank=True, help_text='文本域类型字段前端显示时可以将此内容作为字段的placeholder')
boolean_field_display = models.JSONField('布尔类型显示名', default='{}', blank=True, boolean_field_display = models.JSONField('布尔类型显示名', default=dict, blank=True,
help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号') help_text='当为布尔类型时候,可以支持自定义显示形式。{"1":"","0":""}或{"1":"需要","0":"不需要"},注意数字也需要引号')
field_choice = models.JSONField('radio、checkbox、select的选项', default='{}', blank=True, field_choice = models.JSONField('radio、checkbox、select的选项', default=dict, blank=True,
help_text='radio,checkbox,select,multiselect类型可供选择的选项格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号') help_text='radio,checkbox,select,multiselect类型可供选择的选项格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号')
label = models.JSONField('标签', blank=True, default='{}', help_text='自定义标签json格式调用方可根据标签自行处理特殊场景逻辑loonflow只保存文本内容') label = models.JSONField('标签', blank=True, default=dict, help_text='自定义标签json格式调用方可根据标签自行处理特殊场景逻辑loonflow只保存文本内容')
class Ticket(CommonAModel):
"""
工单
"""
TICKET_ACT_STATE_DRAFT = 0 # 草稿中
TICKET_ACT_STATE_ONGOING = 1 # 进行中
TICKET_ACT_STATE_BACK = 2 # 被退回
TICKET_ACT_STATE_RETREAT = 3 # 被撤回
TICKET_ACT_STATE_FINISH = 4 # 已完成
TICKET_ACT_STATE_CLOSED = 5 # 已关闭
act_state_choices =(
(TICKET_ACT_STATE_DRAFT, '草稿中'),
(TICKET_ACT_STATE_ONGOING, '进行中'),
(TICKET_ACT_STATE_BACK, '被退回'),
(TICKET_ACT_STATE_RETREAT, '被撤回'),
(TICKET_ACT_STATE_FINISH, '已完成'),
(TICKET_ACT_STATE_CLOSED, '已关闭')
)
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="工单的流水号")
state = models.ForeignKey(State, on_delete=models.CASCADE, verbose_name='当前状态', related_name='ticket_state')
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父工单')
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='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效')
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列表')
act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices)
multi_all_person = models.JSONField('全部处理的结果', default=dict, blank=True, help_text='需要当前状态处理人全部处理时实际的处理结果json格式')
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)
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格式')

View File

@ -1,9 +1,63 @@
from rest_framework.serializers import ModelSerializer import rest_framework
from rest_framework import serializers
from .models import Workflow from .models import State, Ticket, Workflow, Transition, CustomField
class WorkflowSerializer(ModelSerializer): class WorkflowSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Workflow model = Workflow
fields = '__all__' fields = '__all__'
class StateSerializer(serializers.ModelSerializer):
class Meta:
model = State
fields = '__all__'
class WorkflowSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = Workflow
fields = ['id', 'name']
class StateSimpleSerializer(serializers.ModelSerializer):
class Meta:
model = State
fields = ['id', 'name']
class TransitionSerializer(serializers.ModelSerializer):
source_state_ = StateSimpleSerializer(source='source_state', read_only=True)
destination_state_ = StateSimpleSerializer(source='destination_state', read_only=True)
class Meta:
model = Transition
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.select_related('source_state','destination_state')
return queryset
class CustomFieldSerializer(serializers.ModelSerializer):
class Meta:
model = CustomField
fields = '__all__'
class TicketCreateSerializer(serializers.ModelSerializer):
transition = serializers.IntegerField(label='流转ID')
class Meta:
model=Ticket
fields=['title','workflow','ticket_data', 'transition']
class TicketSerializer(serializers.ModelSerializer):
workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True)
class Meta:
model = Ticket
fields = '__all__'
@staticmethod
def setup_eager_loading(queryset):
queryset = queryset.select_related('workflow','state')
return queryset

View File

@ -0,0 +1,154 @@
from apps.system.models import User
from apps.wf.models import CustomField, State, Ticket, Transition, Workflow
from rest_framework.exceptions import APIException
from django.utils import timezone
from datetime import timedelta
import random
class WfService(object):
@staticmethod
def get_worlflow_states(workflow:Workflow):
"""
获取工作流状态列表
"""
return State.objects.filter(workflow=workflow, is_deleted=False).order_by('sort')
@staticmethod
def get_workflow_transitions(workflow:Workflow):
"""
获取工作流流转列表
"""
return Transition.objects.filter(workflow=workflow, is_deleted=False)
@staticmethod
def get_workflow_start_state(workflow:Workflow):
"""
获取工作流初始状态
"""
try:
wf_state_obj = State.objects.get(workflow=workflow, type=State.STATE_TYPE_START, is_deleted=False)
return wf_state_obj
except:
raise Exception('工作流状态配置错误')
@staticmethod
def get_workflow_custom_fields(workflow:Workflow):
"""
获取工单字段
"""
return CustomField.objects.filter(is_deleted=False, workflow=workflow).order_by('sort')
@classmethod
def get_ticket_transitions(cls, ticket:Ticket):
"""
获取工单当前状态下可用的流转条件
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_state_transitions(cls, state:State):
"""
获取状态可执行的操作
"""
return Transition.objects.filter(is_deleted=False, source_state=state).all()
@classmethod
def get_ticket_steps(cls, ticket:Ticket):
steps = cls.get_worlflow_states(ticket.workflow)
for i in steps:
if ticket.state.is_hidden and ticket.state != i:
steps.remove(i)
return steps
@classmethod
def get_ticket_transitions(cls, ticket:Ticket):
"""
获取工单可执行的操作
"""
return cls.get_state_transitions(ticket.state)
@classmethod
def get_transition_by_args(cls, kwargs:dict):
"""
查询并获取流转
"""
kwargs['is_deleted'] = False
return Transition.objects.filter(**kwargs).all()
@classmethod
def get_ticket_sn(cls, workflow:Workflow):
"""
生成工单流水号
"""
now = timezone.now()
today = str(now)[:10]+' 00:00:00'
next_day = str(now+timedelta(days=1))[:10]+' 00:00:00'
ticket_day_count_new = Ticket.objects.filter(create_time__gte=today, create_time__lte=next_day, workflow=workflow).count()+1
return '%s_%04d%02d%02d%04d' % (workflow.sn_prefix, now.year, now.month, now.day, ticket_day_count_new)
@classmethod
def get_next_state_id_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition)->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
if transition.condition_expression:
pass
return destination_state
@classmethod
def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, ticket_data:dict={}):
"""
获取工单目标状态实际的处理人, 处理人类型
"""
if state.type == State.STATE_TYPE_START:
"""
回到初始状态
"""
return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL,
destination_participant=ticket.create_by,
multi_all_person=dict())
elif state.type == State.STATE_TYPE_END:
"""
到达结束状态
"""
return dict(destination_participant_type=State.PARTICIPANT_TYPE_PERSONAL,
destination_participant='',
multi_all_person=dict())
multi_all_person_dict = {}
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, '')
if destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#单部门
destination_participant = User.objects.filter(dept=destination_participant).values_list('id')
if destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#单角色
destination_participant = User.objects.filter(roles=destination_participant).values_list('id')
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)

View File

@ -1,11 +1,15 @@
from django.db.models import base from django.db.models import base
from rest_framework import urlpatterns from rest_framework import urlpatterns
from apps.pum.views import VendorViewSet from apps.wf.views import CustomFieldViewSet, StateViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
router = DefaultRouter() router = DefaultRouter()
router.register('vendor', VendorViewSet, basename='vendor') router.register('workflow', WorkflowViewSet, basename='wf')
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')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
] ]

View File

@ -1,10 +1,14 @@
from apps.wf.serializers import WorkflowSerializer 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 render
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action, api_view
from apps.wf.models import Workflow, State, Transition from apps.wf.models import CustomField, Ticket, Workflow, State, Transition
from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin from apps.system.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin
from apps.wf.services import WfService
from rest_framework.exceptions import APIException
# Create your views here. # Create your views here.
class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
@ -15,4 +19,127 @@ class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
search_fields = ['name', 'description'] search_fields = ['name', 'description']
filterset_fields = [] filterset_fields = []
ordering_fields = ['create_time'] ordering_fields = ['create_time']
ordering = ['-create_time'] ordering = ['-create_time']
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=StateSerializer)
def states(self, request, pk=None):
"""
工作流下的状态节点
"""
wf = self.get_object()
serializer = self.serializer_class(instance=WfService.get_worlflow_states(wf), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=TransitionSerializer)
def transitions(self, request, pk=None):
"""
工作流下的流转规则
"""
wf = self.get_object()
serializer = self.serializer_class(instance=WfService.get_workflow_transitions(wf), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_update'}, pagination_class=None, serializer_class=CustomFieldSerializer)
def customfields(self, request, pk=None):
"""
工作流下的自定义字段
"""
wf = self.get_object()
serializer = self.serializer_class(instance=CustomField.objects.filter(workflow=wf), many=True)
return Response(serializer.data)
@action(methods=['get'], detail=True, perms_map={'get':'workflow_init'})
def init(self, request, pk=None):
"""
新建工单初始化
"""
ret={}
wf = self.get_object()
start_state = WfService.get_workflow_start_state(wf)
transitions = WfService.get_state_transitions(start_state)
ret['workflow'] = pk
ret['transitions'] = TransitionSerializer(instance=transitions, many=True).data
field_list = CustomFieldSerializer(instance=WfService.get_workflow_custom_fields(wf), many=True).data
ret['field_list'] = field_list
return Response(ret)
class StateViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'*':'*'}
queryset = State.objects.all()
serializer_class = StateSerializer
search_fields = ['name']
filterset_fields = ['workflow']
ordering = ['sort']
class TransitionViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'*':'*'}
queryset = Transition.objects.all()
serializer_class = TransitionSerializer
search_fields = ['name']
filterset_fields = ['workflow']
ordering = ['id']
class CustomFieldViewSet(CreateModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin, GenericViewSet):
perms_map = {'*':'*'}
queryset = CustomField.objects.all()
serializer_class = CustomFieldSerializer
search_fields = ['field_name']
filterset_fields = ['workflow', 'field_type']
ordering = ['sort']
class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet):
perms_map = {'*':'*'}
queryset = Ticket.objects.all()
serializer_class = TicketSerializer
search_fields = ['title']
filterset_fields = ['workflow', 'state']
ordering = ['-create_time']
def get_serializer_class(self):
if self.action == 'create':
return TicketCreateSerializer
return super().get_serializer_class()
def create(self, request, *args, **kwargs):
"""
新建工单
"""
serializer = self.get_serializer(data=request.data)
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']
if transition.field_require_check:
for key, value in start_state.state_fields.items(): #校验必填项
if value == 2:
if key not in ticket_data or not ticket_data[key]:
raise APIException('字段{}必填'.format(key))
ticket = serializer.save()
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)
if next_state.type == State.STATE_TYPE_END:
act_state = Ticket.TICKET_ACT_STATE_FINISH
elif next_state.type == State.STATE_TYPE_START:
pass
@action(methods=['get'], detail=True, perms_map={'get':'*'})
def flowsteps(self, request, pk=None):
"""
工单流转step, 用于显示当前状态的step图(线性结构)
"""
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 transitions(self, request, pk=None):
"""
获取工单可执行的操作
"""
ticket = self.get_object()
transitions = WfService.get_ticket_transitions(ticket)
return Response(TransitionSerializer(instance=transitions, many=True).data)

View File

@ -49,7 +49,9 @@ INSTALLED_APPS = [
'apps.monitor', 'apps.monitor',
'apps.pum', 'apps.pum',
'apps.em', 'apps.em',
'apps.hrm' 'apps.hrm',
'apps.wf',
'apps.mtm'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -60,7 +60,8 @@ urlpatterns = [
path('api/pum/', include('apps.pum.urls')), path('api/pum/', include('apps.pum.urls')),
path('api/em/', include('apps.em.urls')), path('api/em/', include('apps.em.urls')),
path('api/hrm/', include('apps.hrm.urls')), path('api/hrm/', include('apps.hrm.urls')),
path('api/wf/', include('apps.wf.urls')),
path('api/mtm/', include('apps.mtm.urls')),
# 工具 # 工具
path('api/utils/signature/', GenSignature.as_view()), path('api/utils/signature/', GenSignature.as_view()),