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

This commit is contained in:
shilixia 2021-10-13 09:36:08 +08:00
commit cf8be1af3f
41 changed files with 1944 additions and 337 deletions

View File

@ -154,6 +154,14 @@ export function createTicket(data) {
data data
}) })
} }
//接单
export function ticketAccpet(id,data) {
return request({
url: `/wf/ticket/${id}/accpet/`,
method: 'post',
data
})
}
//工单详情 //工单详情
export function getTicketDetail(id) { export function getTicketDetail(id) {
return request({ return request({
@ -168,3 +176,11 @@ export function getTicketTransitions(id) {
method: 'get' method: 'get'
}) })
} }
//工单流转记录
export function getTicketFlowlog(id) {
return request({
url: `/wf/ticket/${id}/flowlogs/`,
method: 'get'
})
}

View File

@ -340,6 +340,13 @@ export const asyncRoutes = [
meta: { title: '人员信息详情', icon: 'example', perms: ['workflow_manage'] }, meta: { title: '人员信息详情', icon: 'example', perms: ['workflow_manage'] },
hidden: true hidden: true
}, },
{
path: 'ticketHandle',
name: 'ticketHandle',
component: () => import('@/views/workflow/ticketHandle'),
meta: { title: '工单处理', icon: 'example', perms: ['workflow_manage'] },
hidden: true
},
] ]
}, },
{ {

View File

@ -27,9 +27,20 @@ import TST from "@/views/workflow/transitions"
}; };
}, },
created() { created() {
let id = sessionStorage.getItem('configurationId');
if(this.$route.params.workflow){
this.ID = 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: { methods: {
handleClick(tab, event) { handleClick(tab, event) {

View File

@ -56,7 +56,7 @@
<el-table-column <el-table-column
align="center" align="center"
label="操作" label="操作"
width="220px" width="300px"
> >
<template slot-scope="scope"> <template slot-scope="scope">
<el-link <el-link
@ -76,6 +76,10 @@
type="primary" type="primary"
@click="handleTicket(scope)" @click="handleTicket(scope)"
>查看工单</el-link> >查看工单</el-link>
<el-link
type="primary"
@click="handleWatch(scope)"
>查看流程图</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -87,6 +91,21 @@
@pagination="getList" @pagination="getList"
/> />
</el-card> </el-card>
<div class="svgMark" v-if="limitedWatch" @click="closeMark">
<div class="svgWrapper">
<div class="svgItem">工作流流程图<i class="el-dialog__close el-icon el-icon-close" @click="closeMark"></i></div>
<el-row>
<el-col :span="12">
<p>工作流名称 {{watchedName}}</p>
</el-col>
<el-col :span="12">
<p>创建时间 {{watchedCreateTime}}</p>
</el-col>
</el-row>
<svg height=800 id="mySvg" style="width: max-content!important">
</svg>
</div>
</div>
<el-dialog <el-dialog
:visible.sync="dialogVisible" :visible.sync="dialogVisible"
:title="dialogType === 'edit' ? '编辑工作流' : '新增工作流'"> :title="dialogType === 'edit' ? '编辑工作流' : '新增工作流'">
@ -139,12 +158,14 @@
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script> <script>
import { getWorkflowList, createWorkflow,updateWorkflow,deleteWorkflow,getWfCustomfieldList,getWfStateList } from "@/api/workflow"; import { getWfTransitionList,getWorkflowList, createWorkflow,updateWorkflow,deleteWorkflow,getWfCustomfieldList,getWfStateList } from "@/api/workflow";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
// import vueJsonEditor from 'vue-json-editor' // import vueJsonEditor from 'vue-json-editor'
import Pagination from "@/components/Pagination"; // secondary package based on el-pagination import Pagination from "@/components/Pagination"; // secondary package based on el-pagination
import dagreD3 from 'dagre-d3'
import * as d3 from 'd3'
const defaultworkflow = { const defaultworkflow = {
name: "", name: "",
number: "", number: "",
@ -163,6 +184,7 @@ export default {
title_template:'', title_template:'',
content_template:'', content_template:'',
}, },
limitedWatch:false,
view_permission_check:false, view_permission_check:false,
hasJsonFlag:true, // json是否验证通过 hasJsonFlag:true, // json是否验证通过
hasJsonFlag1:true, // json是否验证通过 hasJsonFlag1:true, // json是否验证通过
@ -179,6 +201,8 @@ export default {
listLoading: true, listLoading: true,
dialogVisible: false, dialogVisible: false,
dialogType: "new", dialogType: "new",
watchedName:'',
watchedCreateTime:'',
rule1: { rule1: {
name: [{ required: true, message: "请输入", trigger: "blur" }], name: [{ required: true, message: "请输入", trigger: "blur" }],
description: [{ required: true, message: "请输入", trigger: "blur" }] description: [{ required: true, message: "请输入", trigger: "blur" }]
@ -289,7 +313,97 @@ export default {
} }
}); });
}, },
handleWatch(scope){
let that = this;
let workFlow = scope.row.id;
that.watchedName = scope.row.name;
that.watchedCreateTime = scope.row.create_time;
that.limitedWatch = true;
that.$nextTick(()=>{
var g = new dagreD3.graphlib.Graph().setGraph({
align: 'DL',
nodesep: 100,
edgesep: 100,
ranksep: 50,
marginx: 0,
marginy: 50,
});
//获取state得到节点
getWfStateList(workFlow).then((response) => {
if (response.data) {
let nodes = response.data;
// 添加节点
nodes.forEach((item) => {
g.setNode(item.id, {
// 节点标签
label: item.name,
// 节点形状
shape: 'rect',
toolText: item.name,
//节点样式
style: "fill:#fff;stroke:#000",
labelStyle: "fill:#000;",
// width: 83,
// height: 40,
rx :5,//矩形节点圆角度
ry :5
});
});
g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
});
//获取流转得到线 链接关系
getWfTransitionList(workFlow).then((res)=>{
if(res.data){
let transitionList = res.data;
transitionList.forEach((transitions)=>{
let transition0 = transitions;
if (transition0.condition_expression.length>3){
debugger;
g.setNode(transition0.source_state_.id+100000, {label: "条件表达式",style: "stroke: #000;fill: #afa", shape: "diamond"});
g.setEdge(transition0.source_state_.id, transition0.source_state_.id+100000, {
// 边标签
label: transition0.name,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
});
let condition_expression = JSON.parse(transition0.condition_expression);
condition_expression.forEach(condition_expression0=>{
g.setEdge(transition0.source_state_.id+100000, condition_expression0.target_state, {
label: condition_expression0.label,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
})
})
}else{
g.setEdge(transition0.source_state_.id, transition0.destination_state_.id, {
// 边标签
label: transition0.name,
// 边样式
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px" // 根据后台数据来改变连线的颜色
});
}
})
g.nodes().length-1
g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
});
// 创建渲染器
let render = new dagreD3.render();
// 选择 svg 并添加一个g元素作为绘图容器.
let svg = d3.select('#mySvg');
let svgGroup = svg.append('g');
// 在绘图容器上运行渲染器生成流程图.
render(d3.select("svg g"), g);
}else{}
});
}
});
})
},
closeMark(){
this.limitedWatch = false;
},
onJsonChange (value) { onJsonChange (value) {
// console.log('更改value:', value); // console.log('更改value:', value);
// 实时保存 // 实时保存
@ -349,3 +463,51 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
.svgMark{
width: 100%;
height: 100%;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: auto;
margin: 0;
z-index: 2000;
background: rgba(0,0,0,.3);
}
.svgWrapper{
background: #fff;
width: 800px;
margin: 10vh auto 0;
text-align: center;
border-radius: 2px;
}
.svgItem{
padding: 20px 40px 0 ;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
font-size: 18px;
display: flex;
justify-content: space-between;
}
svg {
font-size: 14px;
}
.node rect {
stroke: #606266;
fill: #fff;
}
.edgePath path {
stroke: #606266;
fill: #333;
stroke-width: 1.5px;
}
g.conditions > rect {
fill: #00ffd0;
stroke: #000;
}
.el-icon-close{
cursor: pointer;
}
</style>

View File

@ -148,6 +148,28 @@
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="字段状态">
<el-button @click="addWordStatusChange">添加修改</el-button>
<el-row v-for="(item,$index) in statusChange" :key="item+$index" style="margin-top: 10px">
<el-col :span="11">
<el-select style="width: 100%" v-model="item.name" placeholder="请选择字段">
<el-option v-for="item in customfieldList" :key="item.id" :label="item.field_name" :value="item.field_key">
</el-option>
</el-select>
</el-col>
<el-col :span="1" style="height: 1px;"></el-col>
<el-col :span="8">
<el-select style="width: 100%" v-model="item.value" placeholder="请选择状态">
<el-option label="只读" value="1"></el-option>
<el-option label="必填" value="2"></el-option>
<el-option label="可选" value="3"></el-option>
</el-select>
</el-col>
<el-col :span="2" style="text-align: center" v-if="$index!==0">
<i class="el-icon-remove-outline" @click.prevent="removeStatusChange($index)" style="color: red;font-size: 16px;"></i>
</el-col>
</el-row>
</el-form-item>
</el-form> </el-form>
<div style="text-align: right"> <div style="text-align: right">
<el-button type="danger" @click="dialogVisible = false">取消</el-button> <el-button type="danger" @click="dialogVisible = false">取消</el-button>
@ -157,7 +179,7 @@
</div> </div>
</template> </template>
<script> <script>
import { getWfStateList, createWfState,updateWfState,deleteWfState } from "@/api/workflow"; import { getWfStateList, createWfState,updateWfState,deleteWfState,getWfCustomfieldList } from "@/api/workflow";
import { getOrganizationList,getUserList } from "@/api/user"; import { getOrganizationList,getUserList } from "@/api/user";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
const defaultwfstate = { const defaultwfstate = {
@ -177,14 +199,17 @@ export default {
enable_retreat:'', enable_retreat:'',
participant_type:'', participant_type:'',
participant:'', participant:'',
distribute_type:'', distribute_type:'',//分发类型
state_fields:{}//字段状态是否可写
}, },
statusChange:[],
participant:'', participant:'',
participants:[], participants:[],
is_hidden:false, is_hidden:false,
enable_retreat:false, enable_retreat:false,
staffs:[], staffs:[],
departments:[], departments:[],
customfieldList:[],
/*wfstateList: { /*wfstateList: {
count:0 count:0
},*/ },*/
@ -271,6 +296,12 @@ export default {
} }
}); });
getWfCustomfieldList(this.ID).then((response) => {
if (response.data) {
this.customfieldList = response.data;
}
});
}, },
getUser(){ getUser(){
getUserList({}).then(res=>{ getUserList({}).then(res=>{
@ -305,6 +336,17 @@ export default {
this.wfstate = Object.assign({}, scope.row); // copy obj this.wfstate = Object.assign({}, scope.row); // copy obj
this.participants = this.wfstate.participant; this.participants = this.wfstate.participant;
this.participant = this.wfstate.participant; this.participant = this.wfstate.participant;
/////
debugger;
console.log(this.wfstate.state_fields);
let arr = [];
for (let pro in this.wfstate.state_fields) {
let obj = new Object();
obj.name = pro;
obj.value = this.wfstate.state_fields[pro];
arr.push(obj)
}
this.statusChange = arr;
this.wfstate.distribute_type = this.wfstate.distribute_type.toString(); this.wfstate.distribute_type = this.wfstate.distribute_type.toString();
this.dialogType = "edit"; this.dialogType = "edit";
this.dialogVisible = true; this.dialogVisible = true;
@ -312,12 +354,27 @@ export default {
this.$refs["Form"].clearValidate(); this.$refs["Form"].clearValidate();
}); });
}, },
addWordStatusChange(){
this.statusChange.push({name:'',value:''})
},
removeStatusChange(index){
this.statusChange.splice(index, 1)
},
//编辑新建 //编辑新建
async confirm(form) { async confirm(form) {
this.$refs[form].validate((valid) => { this.$refs[form].validate((valid) => {
if (valid) { if (valid) {
const isEdit = this.dialogType === "edit"; const isEdit = this.dialogType === "edit";
// this.wfstate.participant = 1; // this.wfstate.participant = 1;
let state_fields = {};
if(this.statusChange.length>0){
for(let i=0;i<this.statusChange.length;i++){
state_fields[this.statusChange[i].name] = this.statusChange[i].value;
}
}
debugger;
console.log(state_fields);
this.wfstate.state_fields = state_fields;
this.wfstate.participant = this.participant!==''?this.participant:this.participants; this.wfstate.participant = this.participant!==''?this.participant:this.participants;
if (isEdit) { if (isEdit) {
updateWfState(this.wfstate.id, this.wfstate).then((res) => { updateWfState(this.wfstate.id, this.wfstate).then((res) => {

View File

@ -27,8 +27,8 @@
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">新增</el-button> <el-button type="primary" icon="el-icon-plus" @click="handleCreate">新增</el-button>
</div> </div>
</el-card> </el-card>
<el-tabs v-model="activeName" type="border-card" @tab-click="handleClick"> <el-tabs v-model="pageForm.category" type="border-card" @tab-click="handleClick">
<el-tab-pane label="待处理" name="first"> <el-tab-pane label="待处理" name="duty">
<el-table :data="tickets" <el-table :data="tickets"
border fit stripe border fit stripe
style="width: 100%" style="width: 100%"
@ -60,9 +60,9 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作"> <el-table-column align="center" label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-link v-if="stateSteps==scope.row.act_state" type="danger" @click="handlePicture(scope)">查看流程图</el-link> <el-link v-if="scope.row.state_.distribute_type==1&&scope.row.participant_type==2" type="danger" @click="handleGetTicket(scope)">接单</el-link>
<el-link v-else type="danger" @click="handleDetail(scope)">处理</el-link> <el-link v-else-if="scope.row.act_state==1&&scope.row.participant_type!==2" type="primary" @click="handleDetail(scope)">处理</el-link>
<!--<el-link type="danger" @click="handleDetail(scope)">处理</el-link>--> <el-link type="success" @click="handlePicture(scope)">查看流程图</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -73,15 +73,29 @@
@pagination="getList" @pagination="getList"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="已处理" name="second"> <el-tab-pane label="我处理" name="worked">
<el-table :data="tickets" border fit stripe style="width: 100%" > <el-table :data="tickets"
border fit stripe
style="width: 100%"
height="100"
highlight-current-row
v-el-height-adaptive-table="{bottomOffset: 60}">
<el-table-column label="工单标题" min-width="100" prop="title"> <el-table-column label="工单标题" min-width="100" prop="title">
</el-table-column> </el-table-column>
<el-table-column label="当前状态" min-width="100"> <el-table-column label="当前状态" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.act_state===1">已提交</span> <el-tag v-if="scope.row.act_state==0" label="草稿中" value="scope.row.act_state">草稿中</el-tag>
<span v-else-if="scope.row.act_state===4">已完成</span> <el-tag v-else-if="scope.row.act_state==1" label="进行中" value="scope.row.act_state">进行中</el-tag>
<span v-else>审批中</span> <el-tag v-else-if="scope.row.act_state==2" label="被退回" value="scope.row.act_state">被退回</el-tag>
<el-tag v-else-if="scope.row.act_state==3" label="被撤回" value="scope.row.act_state">被撤回</el-tag>
<el-tag v-else-if="scope.row.act_state==4" label="已完成" value="scope.row.act_state">已完成</el-tag>
<el-tag v-else-if="scope.row.act_state==5" label="已关闭" value="scope.row.act_state">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="进行状态" min-width="100">
<template slot-scope="scope">
<span v-if="scope.row.state_.type==0">{{scope.row.state_.name}}</span>
<span v-else>{{scope.row.state_.name}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" min-width="100"> <el-table-column label="类型" min-width="100">
@ -91,8 +105,7 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作"> <el-table-column align="center" label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-link v-if="stateSteps==scope.row.act_state" type="danger" @click="handlePicture(scope)">查看流程图</el-link> <el-link type="primary" v-if="scope.row.act_state==4" @click="handlePicture(scope)">查看流程图</el-link>
<el-link v-else type="danger" @click="handleDetail(scope)">处理</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -103,17 +116,29 @@
@pagination="getList" @pagination="getList"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="已发起" name="third"> <el-tab-pane label="我发起" name="owner">
<!--<el-button type="primary" icon="el-icon-plus" @click="handleusedstepCreate"--> <el-table :data="tickets"
<!--&gt;新增</el-button>--> border fit stripe
<el-table :data="tickets" border fit stripe style="width: 100%" > style="width: 100%"
height="100"
highlight-current-row
v-el-height-adaptive-table="{bottomOffset: 60}">
<el-table-column label="工单标题" min-width="100" prop="title"> <el-table-column label="工单标题" min-width="100" prop="title">
</el-table-column> </el-table-column>
<el-table-column label="当前状态" min-width="100"> <el-table-column label="当前状态" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.act_state===1">已提交</span> <el-tag v-if="scope.row.act_state==0" label="草稿中" value="scope.row.act_state">草稿中</el-tag>
<span v-else-if="scope.row.act_state===4">已完成</span> <el-tag v-else-if="scope.row.act_state==1" label="进行中" value="scope.row.act_state">进行中</el-tag>
<span v-else>审批中</span> <el-tag v-else-if="scope.row.act_state==2" label="被退回" value="scope.row.act_state">被退回</el-tag>
<el-tag v-else-if="scope.row.act_state==3" label="被撤回" value="scope.row.act_state">被撤回</el-tag>
<el-tag v-else-if="scope.row.act_state==4" label="已完成" value="scope.row.act_state">已完成</el-tag>
<el-tag v-else-if="scope.row.act_state==5" label="已关闭" value="scope.row.act_state">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="进行状态" min-width="100">
<template slot-scope="scope">
<span v-if="scope.row.state_.type==0">{{scope.row.state_.name}}</span>
<span v-else>{{scope.row.state_.name}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" min-width="100"> <el-table-column label="类型" min-width="100">
@ -123,7 +148,9 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作"> <el-table-column align="center" label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-link type="danger" @click="handleDetail(scope)">处理</el-link> <el-link v-if="scope.row.state_.distribute_type==1&&scope.row.participant_type==2" type="danger" @click="handleGetTicket(scope)">接单</el-link>
<el-link v-else-if="scope.row.act_state==1&&scope.row.participant_type!==2" type="primary" @click="handleDetail(scope)">处理</el-link>
<el-link type="success" @click="handlePicture(scope)">查看流程图</el-link>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -134,15 +161,29 @@
@pagination="getList" @pagination="getList"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="抄送我" name="fourth"> <el-tab-pane label="抄送我" name="relation">
<el-table :data="tickets" border fit stripe style="width: 100%" > <el-table :data="tickets"
border fit stripe
style="width: 100%"
height="100"
highlight-current-row
v-el-height-adaptive-table="{bottomOffset: 60}">
<el-table-column label="工单标题" min-width="100" prop="title"> <el-table-column label="工单标题" min-width="100" prop="title">
</el-table-column> </el-table-column>
<el-table-column label="当前状态" min-width="100"> <el-table-column label="当前状态" min-width="100">
<template slot-scope="scope"> <template slot-scope="scope">
<span v-if="scope.row.act_state===1">已提交</span> <el-tag v-if="scope.row.act_state==0" label="草稿中" value="scope.row.act_state">草稿中</el-tag>
<span v-else-if="scope.row.act_state===4">已完成</span> <el-tag v-else-if="scope.row.act_state==1" label="进行中" value="scope.row.act_state">进行中</el-tag>
<span v-else>审批中</span> <el-tag v-else-if="scope.row.act_state==2" label="被退回" value="scope.row.act_state">被退回</el-tag>
<el-tag v-else-if="scope.row.act_state==3" label="被撤回" value="scope.row.act_state">被撤回</el-tag>
<el-tag v-else-if="scope.row.act_state==4" label="已完成" value="scope.row.act_state">已完成</el-tag>
<el-tag v-else-if="scope.row.act_state==5" label="已关闭" value="scope.row.act_state">已关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="进行状态" min-width="100">
<template slot-scope="scope">
<span v-if="scope.row.state_.type==0">{{scope.row.state_.name}}</span>
<span v-else>{{scope.row.state_.name}}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="类型" min-width="100"> <el-table-column label="类型" min-width="100">
@ -152,9 +193,8 @@
</el-table-column> </el-table-column>
<el-table-column align="center" label="操作"> <el-table-column align="center" label="操作">
<template slot-scope="scope"> <template slot-scope="scope">
<el-link v-if="stateSteps==scope.row.act_state" type="danger" @click="handlePicture(scope)">查看流程图</el-link> <el-link type="danger" @click="handlePicture(scope)">查看流程图</el-link>
<el-link v-else type="danger" @click="handleDetail(scope)">处理</el-link> <el-link v-if="scope.row.act_state==1" type="danger" @click="handleDetail(scope)">处理</el-link>
<!--<el-link type="danger" @click="handleDetail(scope)">处理</el-link>-->
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@ -169,13 +209,19 @@
<div class="svgMark" v-if="dialogVisible" @click="closeMark"> <div class="svgMark" v-if="dialogVisible" @click="closeMark">
<div class="svgWrapper"> <div class="svgWrapper">
<div class="svgItem">工单流程图<i class="el-dialog__close el-icon el-icon-close" @click="closeMark"></i></div> <div class="svgItem">工单流程图<i class="el-dialog__close el-icon el-icon-close" @click="closeMark"></i></div>
<el-row>
<el-col :span="12">
<p>工单名称 {{watchedName}}</p>
</el-col>
<el-col :span="12">
<p>创建时间 {{watchedCreateTime}}</p>
</el-col>
</el-row>
<el-steps :active="actives" spac="400px" align-center="" style="padding-top: 20px;"> <el-steps :active="actives" spac="400px" align-center="" style="padding-top: 20px;">
<el-step :title="item.name" v-for="item in flowSteps " :key="item.id"> <el-step :title="item.name" v-for="item in flowSteps " :key="item.id">
</el-step> </el-step>
</el-steps> </el-steps>
<svg height=600 id="svg"> <svg height=800 id="mySvg" style="width:100%!important;">
<g id="svgG"/>
<rect/>
</svg> </svg>
</div> </div>
</div> </div>
@ -186,18 +232,113 @@
</el-step> </el-step>
</el-steps> </el-steps>
<el-row> <el-row>
<el-col :span="1" style="height: 1px;"></el-col> <el-form ref="Form" :model="handleForm" label-width="100px" label-position="right" :rules="handleRule">
<el-col :span="11"> <el-col :span="12">
<div class="listItem"><span>流水号</span>{{ticketDetail.sn}}</div> <el-form-item label="标题" style="margin-bottom: 0">
<div class="listItem"><span>创建时间</span>{{ticketDetail.create_time}}</div> <span>{{ticketDetail.title}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作流" style="margin-bottom: 0">
<span v-if="ticketDetail.workflow_">{{ticketDetail.workflow_.name}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="流水号" style="margin-bottom: 0">
<span>{{ticketDetail.sn}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建时间" style="margin-bottom: 0">
<span>{{ticketDetail.create_time}}</span>
</el-form-item>
</el-col>
<el-col v-for="item in ticketDetail.ticket_data_" :key="item.id" :span="12">
<el-form-item :label="item.field_name" v-if="item.field_state==='1'" style="margin-bottom: 0">
<span>{{ticketDetail.ticket_data[item.field_key]}}</span>
</el-form-item>
<el-form-item :label="item.field_name" v-else>
<template v-if="item.field_type=='string'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" :placeholder="item.description"/>
</template>
<template v-if="item.field_type==='int'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" type="number" :placeholder="item.description" oninput="value=value.replace(/[^\d]/g,'')" />
</template>
<template v-if="item.field_type==='float'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" type="number" :placeholder="item.description" />
</template>
<template v-if="item.field_type==='date'">
<el-date-picker
v-model="ticketDetail.ticket_data[item.field_key]"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 100%"
>
</el-date-picker>
</template>
<template v-if="item.field_type==='datetime'">
<el-date-picker
v-model="ticketDetail.ticket_data[item.field_key]"
type="datetime"
placeholder="选择日期"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
>
</el-date-picker>
</template>
<template v-if="item.field_type==='select'">
<el-select style="width: 100%" v-model="ticketDetail.ticket_data[item.field_key]" placeholder="请选择">
<el-option
v-for="item1 in item.field_choice"
:key="item1"
:label="item1"
:value="item1"
>
</el-option>
</el-select>
</template>
<template v-if="item.field_type==='selects'">
<el-select style="width: 100%" multiple v-model="ticketDetail.ticket_data[item.field_key]" placeholder="请选择">
<el-option
v-for="item1 in item.field_choice"
:key="item1"
:label="item1"
:value="item1"
>
</el-option>
</el-select>
</template>
<template v-if="item.field_type==='textarea'">
<el-input type="textarea" :rows="3" v-model="ticketDetail.ticket_data[item.field_key]" placeholder="内容" />
</template>
<template v-if="item.field_type==='file'">
<el-upload
ref="upload"
:action="upUrl"
:on-preview="handlePreview"
:on-success="handleUpSuccess"
:on-remove="handleRemove"
:headers="upHeaders"
:file-list="fileList"
:limit="1"
accept=".doc,.docx,.xls,.xlsx,.ppt,.pptx"
>
<el-button size="small" type="primary">上传文件</el-button>
</el-upload>
</template>
</el-form-item>
</el-col>
<el-col>
<el-form-item label="审批意见">
<el-input v-model="handleForm.suggestion" placeholder="审批意见"/>
</el-form-item>
</el-col> </el-col>
<el-col :span="11">
<div class="listItem"><span>标题</span>{{ticketDetail.title}}</div>
</el-col> </el-form>
</el-row> </el-row>
<div style="text-align: center"> <div style="text-align: center">
<el-button v-for="item in operationBtn" :key="item.id" class="filter-item" type="primary" @click="operationSubmit">{{item.name}}</el-button> <el-button v-for="item in operationBtn" :key="item.id" class="filter-item" type="primary" @click="operationSubmit(item.id)">{{item.name}}</el-button>
</div> </div>
</el-dialog> </el-dialog>
<el-dialog :visible.sync="limitedAdd" title="新增工单"> <el-dialog :visible.sync="limitedAdd" title="新增工单">
@ -219,13 +360,13 @@
<template v-if="item.field_type=='string'"> <template v-if="item.field_type=='string'">
<el-input v-model="item.default_value" :placeholder="item.description" /> <el-input v-model="item.default_value" :placeholder="item.description" />
</template> </template>
<template v-if="item.field_type=='int'"> <template v-if="item.field_type==='int'">
<el-input v-model="item.default_value" type="number" :placeholder="item.description" oninput="value=value.replace(/[^\d]/g,'')" /> <el-input v-model="item.default_value" type="number" :placeholder="item.description" oninput="value=value.replace(/[^\d]/g,'')" />
</template> </template>
<template v-if="item.field_type=='float'"> <template v-if="item.field_type==='float'">
<el-input v-model="item.default_value" type="number" :placeholder="item.description" /> <el-input v-model="item.default_value" type="number" :placeholder="item.description" />
</template> </template>
<template v-if="item.field_type=='date'"> <template v-if="item.field_type==='date'">
<el-date-picker <el-date-picker
v-model="item.default_value" v-model="item.default_value"
type="date" type="date"
@ -235,7 +376,7 @@
> >
</el-date-picker> </el-date-picker>
</template> </template>
<template v-if="item.field_type=='datetime'"> <template v-if="item.field_type==='datetime'">
<el-date-picker <el-date-picker
v-model="item.default_value" v-model="item.default_value"
type="datetime" type="datetime"
@ -245,7 +386,7 @@
> >
</el-date-picker> </el-date-picker>
</template> </template>
<template v-if="item.field_type=='select'"> <template v-if="item.field_type==='select'">
<el-select style="width: 100%" v-model="item.default_value" placeholder="请选择"> <el-select style="width: 100%" v-model="item.default_value" placeholder="请选择">
<el-option <el-option
v-for="item1 in item.field_choice" v-for="item1 in item.field_choice"
@ -256,7 +397,7 @@
</el-option> </el-option>
</el-select> </el-select>
</template> </template>
<template v-if="item.field_type=='selects'"> <template v-if="item.field_type==='selects'">
<el-select style="width: 100%" multiple v-model="item.default_value" placeholder="请选择"> <el-select style="width: 100%" multiple v-model="item.default_value" placeholder="请选择">
<el-option <el-option
v-for="item1 in item.field_choice" v-for="item1 in item.field_choice"
@ -267,10 +408,10 @@
</el-option> </el-option>
</el-select> </el-select>
</template> </template>
<template v-if="item.field_type=='textarea'"> <template v-if="item.field_type==='textarea'">
<el-input type="textarea" :rows="3" v-model="item.default_value" placeholder="指导书内容" /> <el-input type="textarea" :rows="3" v-model="item.default_value" placeholder="内容" />
</template> </template>
<template v-if="item.field_type=='file'"> <template v-if="item.field_type==='file'">
<el-upload <el-upload
ref="upload" ref="upload"
:action="upUrl" :action="upUrl"
@ -297,8 +438,8 @@
<script src="https://d3js.org/d3.v4.min.js"></script> <script src="https://d3js.org/d3.v4.min.js"></script>
<script> <script>
import { upUrl, upHeaders } from "@/api/file"; import { upUrl, upHeaders } from "@/api/file";
import {getWorkflowList,getWfCustomfieldList,createTicket,getWfStateList,getTickets,getWfTransitionList, import {getWorkflowList,getWfCustomfieldList,createTicket,getWfStateList,getTickets,ticketAccpet,getWfTransitionList,
ticketHandle,getWfFlowSteps,getTicketDetail,getTicketTransitions } from "@/api/workflow"; ticketHandle,getWfFlowSteps,getTicketDetail,getTicketTransitions,getTicketFlowlog } from "@/api/workflow";
import Pagination from "@/components/Pagination"; import Pagination from "@/components/Pagination";
import dagreD3 from 'dagre-d3' import dagreD3 from 'dagre-d3'
import * as d3 from 'd3' import * as d3 from 'd3'
@ -309,6 +450,7 @@
data(){ data(){
return{ return{
step:4, step:4,
sort:0,
total:0, total:0,
actives:4, actives:4,
ticketId:0, ticketId:0,
@ -316,6 +458,7 @@
page:1, page:1,
page_size:20, page_size:20,
workflow:'', workflow:'',
category:'duty',
}, },
addForm:{ addForm:{
title:'', title:'',
@ -323,12 +466,19 @@
ticket_data:{}, ticket_data:{},
transition:'' transition:''
}, },
handleForm:{
transition:'',
ticket_data:{},
suggestion:'',
},
handleRule:{},
upUrl: upUrl(), upUrl: upUrl(),
upHeaders: upHeaders(), upHeaders: upHeaders(),
stateSteps:0,
activeName:'first',
keyword:'', keyword:'',
workflow:'', workflow:'',
watchedName:'',
watchedCreateTime:'',
logs:[],
tickets:[], tickets:[],
workflows:[], workflows:[],
ticketDetail:{}, ticketDetail:{},
@ -370,25 +520,6 @@
this.getList(); this.getList();
this.getStates(); this.getStates();
this.getWorkFlow(); this.getWorkFlow();
/* this.workflow = this.$route.params.workflow;
let workflow = localStorage.getItem('workflow');
if(this.workflow){//有传参
this.pageForm.workflow = parseInt(this.workflow);
if(workflow){
localStorage.removeItem('workflow');
localStorage.setItem('workflow',this.pageForm.workflow)
}else{
localStorage.setItem('workflow',this.pageForm.workflow)
}
}else{//无传参
if(workflow){
this.workflow =workflow ;
this.pageForm.workflow =workflow ;
}else{}
}
debugger;
this.getList();
this.getStates();*/
}, },
methods:{ methods:{
getList(){ getList(){
@ -401,29 +532,28 @@
} }
}) })
}, },
getTicketFlowlogs(id){
getTicketFlowlog(id).then(res=>{
if(res.data){
debugger;
console.log(res)
this.logs = res.data;
}
})
},
handleClick(tab, event) { handleClick(tab, event) {
console.log(tab, event); console.log(tab, event);
console.log(this.pageForm.category);
debugger; debugger;
//pagepageSizetotaltickets都要发生变化 this.getList();
let paneName = tab.paneName;
this.activeName = paneName;
if(paneName=='first'){
}else if(paneName=='second'){
}else if(paneName=='third'){
}else if(paneName=='fourth'){
}
}, },
//获取工作流所有状态
getStates(){ getStates(){
if(this.pageForm.workflow!==''){ if(this.pageForm.workflow!==''){
getWfStateList(this.pageForm.workflow).then((response) => { getWfStateList(this.pageForm.workflow).then((response) => {
if (response.data) { if (response.data) {
let nodes = []; let nodes = [];
let res = response.data; let res = response.data;
this.stateSteps = res.length;
for(let i=0;i<res.length;i++){ for(let i=0;i<res.length;i++){
let obj = new Object(); let obj = new Object();
obj.id = res[i].id; obj.id = res[i].id;
@ -437,6 +567,7 @@
}); });
} }
}, },
//获取工作流
getWorkFlow(){ getWorkFlow(){
let listForm = {page:0}; let listForm = {page:0};
getWorkflowList(listForm).then((response) => { getWorkflowList(listForm).then((response) => {
@ -445,6 +576,7 @@
} }
}); });
}, },
//获取过程标记
getEdges(nodes){ getEdges(nodes){
let edge = []; let edge = [];
for(let i=1;i<nodes.length;i++){ for(let i=1;i<nodes.length;i++){
@ -489,9 +621,7 @@
this.addForm.transition = res.data[i].id; this.addForm.transition = res.data[i].id;
} }
} }
}) })
}, },
handlePreview(file) { handlePreview(file) {
if ("url" in file) { if ("url" in file) {
@ -531,118 +661,226 @@
}, },
handlePicture(scope){ handlePicture(scope){
let that = this; let that = this;
getWfFlowSteps( scope.row.id).then((res)=>{ that.dialogVisible = true;
that.watchedName = scope.row.title;
that.watchedCreateTime = scope.row.create_time;
that.pageForm.workflow = scope.row.workflow;
let ticketId = scope.row.id;
// that.getStates();
// that.getTicketFlowlogs(ticketId);
getWfFlowSteps( ticketId).then((res)=>{
if(res.data){ if(res.data){
debugger;
//流程步骤数组
that.flowSteps = res.data; that.flowSteps = res.data;
getTicketDetail( ticketId).then((res)=>{ getTicketDetail( ticketId).then((res)=>{
if(res.data){ if(res.data){
debugger;
that.tooltip = that.createTooltip();
that.ticketDetail = res.data; that.ticketDetail = res.data;
let state = res.data.state; let state = res.data.state;
let dat = that.flowSteps.filter((item)=>{ let dat = that.flowSteps.filter((item)=>{
return item.id==state; return item.id==state;
}) })
this.actives = that.flowSteps.indexOf(dat[0]); that.sort = dat[0].sort;
that.limitedStep = true; that.actives = that.flowSteps.indexOf(dat[0]);
if( that.flowSteps.length-that.actives >1){}else{
that.actives =that.flowSteps.length;
} }
});
}
});
// this.$router.push({name:"test"})
this.dialogVisible = true;
//获取D3
this.$nextTick(()=>{
var g = new dagreD3.graphlib.Graph().setGraph({ var g = new dagreD3.graphlib.Graph().setGraph({
align: 'DL', align: 'DL',
nodesep: 100, nodesep: 100,
edgesep: 100, edgesep: 100,
ranksep: 50, ranksep: 50,
marginx: 50, marginx: 300,
marginy: 50, marginy: 50,
}); });
//获取state得到节点
getWfStateList(that.pageForm.workflow).then((response) => {
if (response.data) {
let nodes = response.data;
// 添加节点 // 添加节点
this.nodes.forEach((item) => { nodes.forEach((item) => {
/*debugger;
console.log(item.sort)
console.log(that.state)*/
if(item.sort==that.sort){
g.setNode(item.id, { g.setNode(item.id, {
// 节点标签 // 节点标签
label: item.label, label: item.name,
// 节点形状 // 节点形状
shape: item.shape, shape: 'rect',
toolText: item.label, toolText: item.name,
//节点样式
style: "fill:#fff;stroke:#000",
//节点样式 //节点样式
style: "fill:#409EFF;stroke:#000",
labelStyle: "fill:#000;", labelStyle: "fill:#000;",
width: 83, rx :5,//矩形节点圆角度
height: 40,
rx :5,
ry :5 ry :5
}); });
}else{
g.setNode(item.id, {
// 节点标签
label: item.name,
// 节点形状
shape: 'rect',
toolText: item.name,
//节点样式
style: "fill:#fff;stroke:#000",
labelStyle: "fill:#000;",
rx :5,//矩形节点圆角度
ry :5
}); });
// 链接关系 }
this.edges.forEach(item => {
g.setEdge(item.source, item.target, {
// 边标签
label: item.label,
// 边样式
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px" // 根据后台数据来改变连线的颜色
});
}); });
g.nodes().forEach(function (v) { g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v))); console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
}); });
g.edges().forEach(function (e) { //获取流转得到线 链接关系
console.log("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(g.edge(e))); getWfTransitionList(that.pageForm.workflow).then((res)=>{
if(res.data){
let transitionList = res.data;
transitionList.forEach((transitions)=>{
let transition0 = transitions;
debugger;
console.log(transition0.condition_expression.length)
if (transition0.condition_expression.length>0){
g.setNode(transition0.source_state_.id+100000, {label: "条件表达式", style: "fill: #a4d088", shape: "diamond"});
g.setEdge(transition0.source_state_.id, transition0.source_state_.id+100000, {
// 边标签
label: transition0.name,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
});
let condition_expression = transition0.condition_expression;
condition_expression.forEach(condition_expression0=>{
g.setEdge(transition0.source_state_.id+100000, condition_expression0.target_state, {
label: condition_expression0.label,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
})
})
}else{
g.setEdge(transition0.source_state_.id, transition0.destination_state_.id, {
// 边标签
label: transition0.name,
// 边样式
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px" // 根据后台数据来改变连线的颜色
});
}
})
g.nodes().length-1
g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
}); });
// 创建渲染器 // 创建渲染器
let render = new dagreD3.render(); let render = new dagreD3.render();
// 选择 svg 并添加一个g元素作为绘图容器. // 选择 svg 并添加一个g元素作为绘图容器.
let svg = d3.select('svg'); let svg = d3.select('#mySvg');
let svgGroup = svg.select('g'); let svgGroup = svg.append('g');
let zoom = d3.zoom()
.on("zoom", function() {
svgGroup.attr("transform", d3.event.transform);
});
svg.call(zoom);
// 在绘图容器上运行渲染器生成流程图. // 在绘图容器上运行渲染器生成流程图.
render(d3.select("svg g"), g); render(d3.select("svg g"), g);
getTicketFlowlog(ticketId).then(res=> {
if (res.data) {
that.logs = res.data;
svgGroup
.selectAll('g.node')
.on('mouseover', (v) => {
// 假如当前toolText为"",则不展示
//这里就是自定义tooltip的内容
let filList = [],strList = [];
filList = nodes.filter((ii) => {
return ii.name === g.node(v).label
})
if (!filList.length) {
return
}
filList.map((k) => {
let filte = that.logs.filter(item=>{
return item.state_.id = k.id;
})
//每个
let str = '处理人:'+filte[0].participant_.name;
strList.push(str)
})
that.tipVisible(strList)
})
.on('mouseout', () => {
this.tipHidden()
})
}
}) })
}else{}
});
}
});
}
});
}
});
}, },
// 创建提示框
createTooltip() {
return d3
.select('body')
.append('div')
.classed('tooltip', true)
.style('opacity', 0)
.style('display', 'none')
},
// tooltip显示
tipVisible(textContent) {
this.tooltip
.transition()
.duration(400)
.style('opacity', 1)
.style('display', 'block')
.style('color', '#ffffff')
.style('z-index', '3999')
.style('padding', '20px 30px')
.style('border-radius', '5px')
.style('position', 'fixed')
.style('background', 'rgba(0,0,0,.7)')
this.tooltip
.html(textContent)
.style('left', `${d3.event.pageX + 20}px`)
.style('top', `${d3.event.pageY-10}px`)
},
// tooltip隐藏
tipHidden() {
this.tooltip
.transition()
.duration(400)
.style('opacity', 0)
.style('display', 'none')
},
//处理工单
handleDetail(scope){ handleDetail(scope){
// this.limitedStep = true; this.$router.push({name:"ticketHandle",params:{ticketId:scope.row.id,workflow:scope.row.workflow}})
let that = this;
that.ticketId = scope.row.id;
let ticketId = scope.row.id;
getWfFlowSteps( scope.row.id).then((res)=>{
if(res.data){
that.flowSteps = res.data;
getTicketDetail( ticketId).then((res)=>{
if(res.data){
that.ticketDetail = res.data;
let state = res.data.state;
debugger;
console.log(state)
console.log(that.flowSteps)
debugger;
let dat = that.flowSteps.filter((item)=>{
return item.id==state;
})
console.log(dat)
console.log(that.flowSteps.indexOf(dat[0]))
debugger;
this.actives = that.flowSteps.indexOf(dat[0]);
that.limitedStep = true;
}
});
}
});
getTicketTransitions(scope.row.id).then(res=>{
this.operationBtn = res.data;
})
}, },
operationSubmit(){ operationSubmit(id){
let transition = {transition:this.operationBtn[0].id,ticket_data:this.ticketDetail.ticket_data}; this.handleForm.transition = id;
ticketHandle(this.ticketId,transition).then(res=>{ this.handleForm.ticket_data = this.ticketDetail.ticket_data;
ticketHandle(this.ticketId,this.handleForm).then(res=>{
if (res.data){ if (res.data){
this.limitedStep = false; this.limitedStep = false;
} }
}) })
}, },
//接单
handleGetTicket(scope){
let ticketId = scope.row.id;
ticketAccpet(ticketId,{}).then(res=>{
if(res.code===200){
this.getList();
}
})
},
stepclick(){}, stepclick(){},
closeMark(){ closeMark(){
this.dialogVisible = false; this.dialogVisible = false;
@ -704,4 +942,37 @@
margin-right: 10px; margin-right: 10px;
display: inline-block; display: inline-block;
} }
.tooltip {
position: absolute;
font-size: 12px;
text-align: left;
border-radius: 3px;
box-shadow: rgb(174, 174, 174) 0 0 10px;
cursor: pointer;
display: inline-block;
padding: 6px;
max-width: 300px;
word-wrap: break-word;
word-break: normal;
}
.tooltip > div {
padding: 10px;
}
.node rect {
stroke: #333;
fill: #999;
}
.node {
cursor: pointer;
}
.edgePath path {
stroke: #333;
fill: #333;
stroke-width: 1.5px;
}
</style> </style>

View File

@ -0,0 +1,462 @@
<template>
<div class="app-container">
<el-card style="margin-bottom: 10px">
<el-steps :active="actives" spac="400px" align-center="" style="padding-top: 20px;">
<el-step :title="item.name" v-for="item in flowSteps " :key="item.id"></el-step>
</el-steps>
</el-card>
<el-row>
<el-col :span="8">
<el-card>
<svg height=800 id="mySvg" style="width:100%!important;"></svg>
</el-card>
</el-col>
<el-col :span="16">
<el-card style="margin-left: 10px">
<el-form ref="Form" :model="handleForm" label-width="100px" label-position="right" :rules="handleRule">
<el-col :span="12">
<el-form-item label="标题" style="margin-bottom: 0">
<span>{{ticketDetail.title}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工作流" style="margin-bottom: 0">
<span v-if="ticketDetail.workflow_">{{ticketDetail.workflow_.name}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="流水号" style="margin-bottom: 0">
<span>{{ticketDetail.sn}}</span>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建时间" style="margin-bottom: 0">
<span>{{ticketDetail.create_time}}</span>
</el-form-item>
</el-col>
<el-row style="display: flex;flex-wrap: wrap;width:100%;">
<el-col v-for="item in ticketDetail.ticket_data_" :key="item.id" :span="12">
<el-form-item :label="item.field_name" v-if="item.field_state==='1'">
<span>{{ticketDetail.ticket_data[item.field_key]}}</span>
</el-form-item>
<el-form-item :label="item.field_name" v-else>
<template v-if="item.field_type=='string'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" :placeholder="item.description"/>
</template>
<template v-if="item.field_type==='int'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" type="number" :placeholder="item.description" oninput="value=value.replace(/[^\d]/g,'')" />
</template>
<template v-if="item.field_type==='float'">
<el-input v-model="ticketDetail.ticket_data[item.field_key]" type="number" :placeholder="item.description" />
</template>
<template v-if="item.field_type==='date'">
<el-date-picker
v-model="ticketDetail.ticket_data[item.field_key]"
type="date"
placeholder="选择日期"
value-format="yyyy-MM-dd"
style="width: 100%"
>
</el-date-picker>
</template>
<template v-if="item.field_type==='datetime'">
<el-date-picker
v-model="ticketDetail.ticket_data[item.field_key]"
type="datetime"
placeholder="选择日期"
value-format="yyyy-MM-dd HH:mm:ss"
style="width: 100%"
>
</el-date-picker>
</template>
<template v-if="item.field_type==='select'">
<el-select style="width: 100%" v-model="ticketDetail.ticket_data[item.field_key]" placeholder="请选择">
<el-option
v-for="item1 in item.field_choice"
:key="item1"
:label="item1"
:value="item1"
>
</el-option>
</el-select>
</template>
<template v-if="item.field_type==='selects'">
<el-select style="width: 100%" multiple v-model="ticketDetail.ticket_data[item.field_key]" placeholder="请选择">
<el-option
v-for="item1 in item.field_choice"
:key="item1"
:label="item1"
:value="item1"
>
</el-option>
</el-select>
</template>
<template v-if="item.field_type==='textarea'">
<el-input type="textarea" :rows="3" v-model="ticketDetail.ticket_data[item.field_key]" placeholder="内容" />
</template>
<template v-if="item.field_type==='file'">
<el-upload
ref="upload"
:action="upUrl"
:on-preview="handlePreview"
:on-success="handleUpSuccess"
:headers="upHeaders"
:file-list="fileList"
:limit="1"
accept=".doc,.docx,.xls,.xlsx,.ppt,.pptx"
>
<el-button size="small" type="primary">上传文件</el-button>
</el-upload>
</template>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="审批意见">
<el-input type="textarea" :rows="3" v-model="handleForm.suggestion" placeholder="审批意见"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div style="text-align: center">
<el-button v-for="item in operationBtn" :key="item.id" class="filter-item" type="primary" @click="operationSubmit(item.id)">{{item.name}}</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
import { upUrl, upHeaders } from "@/api/file";
import {getWorkflowList,getWfCustomfieldList,getWfStateList,getWfTransitionList,
ticketHandle,getWfFlowSteps,getTicketDetail,getTicketTransitions,getTicketFlowlog } from "@/api/workflow";
import Pagination from "@/components/Pagination";
import dagreD3 from 'dagre-d3'
import * as d3 from 'd3'
export default {
name: "ticketHandle",
components: { Pagination },
inject:['reload'],
data(){
return{
step:4,
actives:4,
ticketId:0,
stateSteps:0,
handleForm:{
transition:'',
ticket_data:{},
suggestion:'',
},
tooltip:null,
handleRule:{},
upUrl: upUrl(),
upHeaders: upHeaders(),
workflow:'',
watchedName:'',
watchedCreateTime:'',
logs:[],
edges: [],
nodes: [],
tooltip: [],
fileList:[],
workflows:[],
flowSteps:[],
ticketDetail:{},
operationBtn:[],
customfields:[],
transitions:[]
}
},
mounted(){
let that = this;
let ticketId = that.ticketId = this.$route.params.ticketId;
let workflow = that.workflow = this.$route.params.workflow;
let handleTicketId = sessionStorage.getItem('handleTicketId');
let handleTicketWorkflow = sessionStorage.getItem('handleTicketWorkflow');
if(ticketId&&workflow){
if(handleTicketId&&handleTicketWorkflow){
sessionStorage.removeItem('handleTicketId');
sessionStorage.removeItem('handleTicketWorkflow');
sessionStorage.setItem('handleTicketId',ticketId);
sessionStorage.setItem('handleTicketWorkflow',workflow);
}else{
sessionStorage.setItem('handleTicketId',ticketId);
sessionStorage.setItem('handleTicketWorkflow',workflow);
}
}else{
ticketId = that.ticketId = handleTicketId;
workflow = that.workflow = handleTicketWorkflow;
}
getTicketTransitions(ticketId).then(res=>{
this.operationBtn = res.data;
})
getWfFlowSteps( ticketId).then((res)=>{
if(res.data){
//流程步骤数组
that.flowSteps = res.data;
getTicketDetail( ticketId).then((res)=>{
if(res.data){
that.tooltip = that.createTooltip();
that.ticketDetail = res.data;
let state = res.data.state;
let dat = that.flowSteps.filter((item)=>{
return item.id==state;
})
that.sort = dat[0].sort;
that.actives = that.flowSteps.indexOf(dat[0]);
if( that.flowSteps.length-that.actives >1){}else{
that.actives =that.flowSteps.length;
}
var g = new dagreD3.graphlib.Graph().setGraph({
align: 'DL',
nodesep: 100,
edgesep: 100,
ranksep: 50,
marginx: 50,
marginy: 50,
});
//获取state得到节点
getWfStateList(workflow).then((response) => {
if (response.data) {
let nodes = response.data;
// 添加节点
nodes.forEach((item) => {
if(item.sort==that.sort){
g.setNode(item.id, {
// 节点标签
label: item.name,
// 节点形状
shape: 'rect',
toolText: item.name,
//节点样式
style: "fill:#409EFF;stroke:#000",
labelStyle: "fill:#000;",
rx :5,//矩形节点圆角度
ry :5
});
}else{
g.setNode(item.id, {
// 节点标签
label: item.name,
// 节点形状
shape: 'rect',
toolText: item.name,
//节点样式
style: "fill:#fff;stroke:#000",
labelStyle: "fill:#000;",
rx :5,//矩形节点圆角度
ry :5
});
}
});
g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
});
//获取流转得到线 链接关系
getWfTransitionList(workflow).then((res)=>{
if(res.data){
let transitionList = res.data;
transitionList.forEach((transitions)=>{
let transition0 = transitions;
if (transition0.condition_expression.length>0){
g.setNode(transition0.source_state_.id+100000, {label: "条件表达式", style: "fill: #a4d088", shape: "diamond"});
g.setEdge(transition0.source_state_.id, transition0.source_state_.id+100000, {
// 边标签
label: transition0.name,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
});
let condition_expression = transition0.condition_expression;
condition_expression.forEach(condition_expression0=>{
g.setEdge(transition0.source_state_.id+100000, condition_expression0.target_state, {
label: condition_expression0.label,
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px"
})
})
}else{
g.setEdge(transition0.source_state_.id, transition0.destination_state_.id, {
// 边标签
label: transition0.name,
// 边样式
style: "fill:#ffffff;stroke:#c0c1c3;stroke-width:1.5px" // 根据后台数据来改变连线的颜色
});
}
})
g.nodes().length-1
g.nodes().forEach(function (v) {
console.log("Node " + v + ": " + JSON.stringify(g.node(v)));
});
// 创建渲染器
let render = new dagreD3.render();
// 选择 svg 并添加一个g元素作为绘图容器.
let svg = d3.select('#mySvg');
let svgGroup = svg.append('g');
let zoom = d3.zoom()
.on("zoom", function() {
svgGroup.attr("transform", d3.event.transform);
});
svg.call(zoom);
// 在绘图容器上运行渲染器生成流程图.
render(d3.select("svg g"), g);
// this.getTicketFlowlogs(ticketId);
getTicketFlowlog(ticketId).then(res=>{
if(res.data){
that.logs = res.data;
svgGroup
.selectAll('g.node')
.on('mouseover', (v) => {
// 假如当前toolText为"",则不展示
//这里就是自定义tooltip的内容
let filList = [], strList = [];
filList = nodes.filter((ii) => {
return ii.name === g.node(v).label
})
if (!filList.length) {
return
}
filList.map((k) => {
let filte = that.logs.filter(item=>{
return item.state_.id = k.id;
})
//每个
let str = '处理人:'+filte[0].participant_.name;
strList.push(str)
})
that.tipVisible(strList)
})
.on('mouseout', () => {
this.tipHidden()
})
}
})
}else{}
});
}
});
}
});
}
});
},
activated(){
},
methods:{
//工单流转记录
getTicketFlowlogs(id){
getTicketFlowlog(id).then(res=>{
if(res.data){
this.logs = res.data;
}
})
},
handlePreview(file) {
if ("url" in file) {
window.open(file.url);
} else {
window.open(file.response.data.path);
}
},
handleUpSuccess(res, file, filelist) {
this.process.instruction = res.data.id;
},
handleRemove(file, filelist){
this.process.instruction = null;
},
// 创建提示框
createTooltip() {
return d3
.select('body')
.append('div')
.classed('tooltip', true)
.style('opacity', 0)
.style('display', 'none')
},
// tooltip显示
tipVisible(textContent) {
this.tooltip
.transition()
.duration(400)
.style('opacity', 1)
.style('display', 'block')
.style('color', '#ffffff')
.style('z-index', '3999')
.style('padding', '20px 30px')
.style('border-radius', '5px')
.style('position', 'fixed')
.style('background', 'rgba(0,0,0,.7)')
this.tooltip
.html(textContent)
.style('left', `${d3.event.pageX + 15}px`)
.style('top', `${d3.event.pageY-10}px`)
},
// tooltip隐藏
tipHidden() {
this.tooltip
.transition()
.duration(400)
.style('opacity', 0)
.style('display', 'none')
},
operationSubmit(id){
this.handleForm.transition = id;
this.handleForm.ticket_data = this.ticketDetail.ticket_data;
let obj = new Object();
obj.transition = id;
obj.ticket_data = this.ticketDetail.ticket_data;
obj.suggestion = this.handleForm.suggestion;
console.log(this.handleForm);
ticketHandle(this.ticketId,obj).then(res=>{
if (res.data){
this.$router.replace({name:"ticket"})
}
})
},
}
}
</script>
<style scoped>
.svgWrapper{
background: #fff;
width: 800px;
margin: 10vh auto 0;
text-align: center;
border-radius: 2px;
}
.svgItem{
padding: 20px 40px 0 ;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
font-size: 18px;
display: flex;
justify-content: space-between;
}
svg {
font-size: 14px;
}
.node rect {
stroke: #606266;
fill: #fff;
}
.edgePath path {
stroke: #606266;
fill: #333;
stroke-width: 1.5px;
}
.el-icon-close{
cursor: pointer;
}
.listItem{
margin-top: 15px;
font-size: 16px;
}
.listItem>span{
width: 100px;
text-align: right;
margin-right: 10px;
display: inline-block;
}
</style>

View File

@ -97,10 +97,14 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="条件表达式" prop="condition_expression"> <el-form-item label="条件表达式" prop="condition_expression">
<el-input v-model="wftransition.condition_expression"/> <vue-json-editor
v-model="wftransition.condition_expression"
:showBtns="false"
:mode="'code'"
lang="zh"
/>
</el-form-item> </el-form-item>
<el-form-item label="属性类型" prop="attribute_type"> <el-form-item label="属性类型" prop="attribute_type">
<el-select style="width: 100%" v-model="wftransition.attribute_type" placeholder="请选择"> <el-select style="width: 100%" v-model="wftransition.attribute_type" placeholder="请选择">
<el-option <el-option
v-for="item in options" v-for="item in options"
@ -126,13 +130,13 @@
<script> <script>
import {getWfStateList, getWfTransitionList, createWfTransition,updateWfTransition,deleteWfTransition } from "@/api/workflow"; import {getWfStateList, getWfTransitionList, createWfTransition,updateWfTransition,deleteWfTransition } from "@/api/workflow";
import checkPermission from "@/utils/permission"; import checkPermission from "@/utils/permission";
import vueJsonEditor from 'vue-json-editor'
import { genTree } from "@/utils" import { genTree } from "@/utils"
const defaultwftransition = { const defaultwftransition = {
name: "", name: "",
}; };
export default { export default {
components: { }, components: { vueJsonEditor },
name: "TST", name: "TST",
props: ["ID"], props: ["ID"],
data() { data() {
@ -211,13 +215,11 @@ export default {
this.wftransition = Object.assign({}, scope.row); // copy obj this.wftransition = Object.assign({}, scope.row); // copy obj
this.dialogType = "edit"; this.dialogType = "edit";
this.dialogVisible = true; this.dialogVisible = true;
this.$nextTick(() => { // this.wftransition.condition_expression = JSON.stringify(scope.row.condition_expression)
this.$refs["Form"].clearValidate(); // this.$nextTick(() => {
}); // this.$refs["Form"].clearValidate();
// });
}, },
async confirm(form) { async confirm(form) {
this.$refs[form].validate((valid) => { this.$refs[form].validate((valid) => {
if (valid) { if (valid) {
@ -245,8 +247,6 @@ export default {
} }
}); });
}, },
handleDelete(scope) { handleDelete(scope) {
this.$confirm("确认删除?", "警告", { this.$confirm("确认删除?", "警告", {
confirmButtonText: "确认", confirmButtonText: "确认",
@ -262,9 +262,6 @@ export default {
console.error(err); console.error(err);
}); });
}, },
}, },
}; };
</script> </script>

View File

@ -1,5 +1,6 @@
from django.shortcuts import render 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.models import WareHouse,Inventory
from apps.inm.serializers import WareHouseSerializer, WareHouseCreateUpdateSerializer,InventorySerializer,InventoryCreateUpdateSerializer from apps.inm.serializers import WareHouseSerializer, WareHouseCreateUpdateSerializer,InventorySerializer,InventoryCreateUpdateSerializer
@ -23,7 +24,8 @@ class WarehouseViewSet(CreateUpdateModelAMixin, ModelViewSet):
if self.action in ['create', 'update']: if self.action in ['create', 'update']:
return WareHouseCreateUpdateSerializer return WareHouseCreateUpdateSerializer
return WareHouseSerializer return WareHouseSerializer
class InventoryViewSet(CreateUpdateModelAMixin, ModelViewSet):
class InventoryViewSet(ListModelMixin, GenericViewSet):
""" """
物料基本信息-增删改查 物料基本信息-增删改查
""" """
@ -33,8 +35,3 @@ class InventoryViewSet(CreateUpdateModelAMixin, ModelViewSet):
filterset_fields = [] filterset_fields = []
ordering_fields = ['create_time'] ordering_fields = ['create_time']
ordering = ['-create_time'] ordering = ['-create_time']
def get_serializer_class(self):
if self.action in ['create', 'update']:
return InventoryCreateUpdateSerializer
return InventorySerializer

View File

@ -94,24 +94,6 @@ class Migration(migrations.Migration):
'verbose_name_plural': '操作记录条目', '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( migrations.CreateModel(
name='OutputMaterial', name='OutputMaterial',
fields=[ fields=[

View File

@ -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,
),
]

View File

@ -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='最后编辑人'),
),
]

View File

@ -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='最后编辑人'),
),
]

View File

@ -29,7 +29,6 @@ class Material(CommonAModel):
specification = models.CharField('型号', max_length=100, null=True, blank=True) specification = models.CharField('型号', max_length=100, null=True, blank=True)
type = models.CharField('物料类型', choices= type_choices, max_length=20, default=1) type = models.CharField('物料类型', choices= type_choices, max_length=20, default=1)
sort_str = models.CharField('排序字符', max_length=100, null=True, blank=True) 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) unit = models.CharField('基准计量单位', choices=unit_choices, default='', max_length=10)
count = models.IntegerField('物料总数', default=0) count = models.IntegerField('物料总数', default=0)
class Meta: 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) name = models.CharField('工序步骤名称', max_length=100)
number = models.CharField('步骤编号', max_length=100, null=True, blank=True) number = models.CharField('步骤编号', max_length=100, null=True, blank=True)
instruction_content = models.TextField('相应操作指导', null=True, blank=True) instruction_content = models.TextField('相应操作指导', null=True, blank=True)
@ -127,26 +126,28 @@ class RecordFormField(CommonAModel):
def __str__(self): def __str__(self):
return self.field_key + '-' + self.field_name 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) 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) sort = models.IntegerField('排序号', default=1)
class Meta: class Meta:
verbose_name = '产品生产工序' verbose_name = '产品生产工序'
verbose_name_plural = verbose_name verbose_name_plural = verbose_name
class InputMaterial(CommonAModel): class InputMaterial(CommonAModel):
""" """
输入物料 输入物料
""" """
material = models.ForeignKey(Material, verbose_name='输入物料', on_delete=models.CASCADE, related_name='inputmaterial') material = models.ForeignKey(Material, verbose_name='输入物料', on_delete=models.CASCADE, related_name='inputmaterial')
count = models.FloatField('消耗量', default=1) count = models.FloatField('消耗量', default=1)
product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE, related_name='inputmaterial_product') subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE)
sort = models.IntegerField('排序号', default=1) sort = models.IntegerField('排序号', default=1)
class Meta: class Meta:
@ -161,8 +162,7 @@ class OutputMaterial(CommonAModel):
""" """
material = models.ForeignKey(Material, verbose_name='输出物料', on_delete=models.CASCADE, related_name='outputmaterial') material = models.ForeignKey(Material, verbose_name='输出物料', on_delete=models.CASCADE, related_name='outputmaterial')
count = models.FloatField('产出量', default=1) count = models.FloatField('产出量', default=1)
product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE, related_name='outputmaterial_product') subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE)
sort = models.IntegerField('排序号', default=1) sort = models.IntegerField('排序号', default=1)
class Meta: class Meta:
@ -171,11 +171,11 @@ class OutputMaterial(CommonAModel):
class UsedStep(CommonAModel): class UsedStep(CommonAModel):
""" """
产品生产子工序 涉及的生产子工序
""" """
step = models.ForeignKey(Step, verbose_name='子工序', on_delete=models.CASCADE, related_name='usedsteps') step = models.ForeignKey(Step, verbose_name='子工序', on_delete=models.CASCADE, related_name='usedsteps')
product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE) remark = models.TextField('生产备注', null=True, blank=True)
process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE) subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
class Meta: class Meta:
verbose_name = '产品生产子工序' verbose_name = '产品生产子工序'
@ -188,8 +188,7 @@ class TechDoc(CommonAModel):
""" """
name = models.CharField('名称', max_length=50) name = models.CharField('名称', max_length=50)
file = models.ForeignKey(File, verbose_name='技术文件', on_delete=models.CASCADE) file = models.ForeignKey(File, verbose_name='技术文件', on_delete=models.CASCADE)
product = models.ForeignKey(Material, verbose_name='关联产品', on_delete=models.CASCADE) subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='关联工序', on_delete=models.CASCADE)
content = models.TextField('内容', null=True, blank=True) content = models.TextField('内容', null=True, blank=True)
class Meta: class Meta:

View File

@ -1,12 +1,11 @@
from apps.em.serializers import EquipmentSimpleSerializer from apps.em.serializers import EquipmentSimpleSerializer
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ParseError, ValidationError 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 from apps.system.serializers import FileSimpleSerializer, OrganizationSimpleSerializer
class MaterialSerializer(serializers.ModelSerializer): class MaterialSerializer(serializers.ModelSerializer):
processes = serializers.ListField(child=serializers.IntegerField(min_value=1))
class Meta: class Meta:
model = Material model = Material
fields = '__all__' fields = '__all__'
@ -18,7 +17,8 @@ class MaterialDetailSerializer(serializers.ModelSerializer):
fields = '__all__' fields = '__all__'
def get_processes_(self, obj): 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 return ProcessSimpleSerializer(instance=objs, many=True).data
@ -60,18 +60,11 @@ class StepDetailSerializer(serializers.ModelSerializer):
queryset = queryset.prefetch_related('equipments') queryset = queryset.prefetch_related('equipments')
return queryset return queryset
class ProductProcessListSerializer(serializers.ModelSerializer): class SubProductionSerializer(serializers.ModelSerializer):
process_ = ProcessSimpleSerializer(source='process', read_only=True)
product_ = MaterialSimpleSerializer(source='product', read_only=True)
class Meta: class Meta:
model = ProductProcess model = SubProduction
fields = '__all__' fields = '__all__'
class ProductProcessUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = ProductProcess
fields = ['sort']
class InputMaterialListSerializer(serializers.ModelSerializer): class InputMaterialListSerializer(serializers.ModelSerializer):
material_ = MaterialSimpleSerializer(source='material', read_only=True) material_ = MaterialSimpleSerializer(source='material', read_only=True)
class Meta: class Meta:
@ -88,10 +81,10 @@ class OutputMaterialListSerializer(serializers.ModelSerializer):
class InputMaterialSerializer(serializers.ModelSerializer): class InputMaterialSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = InputMaterial model = InputMaterial
fields = ['count', 'sort', 'material', 'product', 'process'] fields = ['count', 'sort', 'material', 'subproduction']
def create(self, validated_data): 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('该物料已存在') raise ValidationError('该物料已存在')
return super().create(validated_data) return super().create(validated_data)
@ -103,10 +96,10 @@ class InputMaterialUpdateSerializer(serializers.ModelSerializer):
class OutputMaterialSerializer(serializers.ModelSerializer): class OutputMaterialSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = OutputMaterial model = OutputMaterial
fields = ['count', 'sort', 'material', 'product', 'process'] fields = ['count', 'sort', 'material', 'subproduction']
def create(self, validated_data): 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('该物料已存在') raise ValidationError('该物料已存在')
return super().create(validated_data) return super().create(validated_data)
@ -121,7 +114,15 @@ class UsedStepCreateSerializer(serializers.ModelSerializer):
""" """
class Meta: class Meta:
model = UsedStep model = UsedStep
fields = ['step', 'product', 'process'] fields = ['step', 'subproduction', 'remark']
class UsedStepUpdateSerializer(serializers.ModelSerializer):
"""
产品生产子工序编辑
"""
class Meta:
model = UsedStep
fields = ['remark']
class UsedStepListSerializer(serializers.ModelSerializer): class UsedStepListSerializer(serializers.ModelSerializer):
""" """
@ -198,7 +199,7 @@ class TechDocListSerializer(serializers.ModelSerializer):
class TechDocCreateSerializer(serializers.ModelSerializer): class TechDocCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = TechDoc model = TechDoc
fields = ['file', 'product', 'process', 'name', 'content'] fields = ['file', 'subproduction', 'name', 'content']
class TechDocUpdateSerializer(serializers.ModelSerializer): class TechDocUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -1,14 +1,14 @@
from django.db.models import base from django.db.models import base
from rest_framework import urlpatterns 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 django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
router = DefaultRouter() router = DefaultRouter()
router.register('material', MaterialViewSet, basename='material') router.register('material', MaterialViewSet, basename='material')
router.register('process', ProcessViewSet, basename='process') router.register('process', ProcessViewSet, basename='process')
# router.register('productprocess', ProductProcessViewSet, basename='productprocess')
router.register('step', StepViewSet, basename='step') router.register('step', StepViewSet, basename='step')
router.register('subproducation', SubProductionViewSet, basename='subproducation')
router.register('inputmaterial', InputMaterialViewSet, basename='inputmaterial') router.register('inputmaterial', InputMaterialViewSet, basename='inputmaterial')
router.register('outputmaterial', OutputMaterialViewSet, basename='outputmaterial') router.register('outputmaterial', OutputMaterialViewSet, basename='outputmaterial')
router.register('usedstep', UsedStepViewSet, basename='usedstep') router.register('usedstep', UsedStepViewSet, basename='usedstep')

View File

@ -2,8 +2,8 @@ from django.shortcuts import render
from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.mixins import CreateModelMixin, ListModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin 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.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, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, RecordFormCreateSerializer, RecordFormFieldCreateSerializer, RecordFormFieldSerializer, RecordFormFieldUpdateSerializer, RecordFormSerializer, RecordFormUpdateSerializer, StepDetailSerializer, StepSerializer, TechDocCreateSerializer, TechDocListSerializer, TechDocUpdateSerializer, UsedStepCreateSerializer, UsedStepListSerializer 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 apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
@ -30,15 +30,6 @@ class MaterialViewSet(PageOrNot, CreateUpdateModelAMixin, ModelViewSet):
return MaterialDetailSerializer return MaterialDetailSerializer
return MaterialSerializer 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): 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) serializer = self.serializer_class(instance=Step.objects.prefetch_related('equipments').filter(process=process, is_deleted=False), many=True)
return Response(serializer.data) 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() queryset = Step.objects.all()
serializer_class = StepSerializer serializer_class = StepSerializer
search_fields = ['name', 'number'] search_fields = ['name', 'number']
@ -78,20 +69,16 @@ class StepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin,
return StepDetailSerializer return StepDetailSerializer
return StepSerializer return StepSerializer
# class ProductProcessViewSet(PageOrNot, CreateModelMixin, UpdateModelMixin, ListModelMixin, DestroyModelMixin, GenericViewSet): class SubProductionViewSet(CreateUpdateModelAMixin, ModelViewSet):
# """ """
# 产品生产工艺流程增删改查 产品生产分解增删改查
# """ """
# perms_map={'*':'*'} perms_map={'*':'*'}
# queryset = ProductProcess.objects.select_related('process', 'product').all() queryset = SubProduction.objects.all()
# filterset_fields = ['process', 'product'] filterset_fields = ['product']
# serializer_class = ProductProcessListSerializer search_fields = ['name']
# ordering = ['sort'] serializer_class = SubProductionSerializer
ordering = ['sort']
# def get_serializer_class(self):
# if self.action == 'update':
# return ProductProcessUpdateSerializer
# return super().get_serializer_class()
class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet):
""" """
@ -100,7 +87,7 @@ class InputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet):
perms_map = {'*':'*'} perms_map = {'*':'*'}
queryset = InputMaterial.objects.select_related('material').all() queryset = InputMaterial.objects.select_related('material').all()
serializer_class = InputMaterialSerializer serializer_class = InputMaterialSerializer
filterset_fields = ['process', 'product'] filterset_fields = ['subproduction']
ordering = ['sort', '-create_time'] ordering = ['sort', '-create_time']
def get_serializer_class(self): def get_serializer_class(self):
@ -117,7 +104,7 @@ class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet):
perms_map = {'*':'*'} perms_map = {'*':'*'}
queryset = OutputMaterial.objects.select_related('material').all() queryset = OutputMaterial.objects.select_related('material').all()
serializer_class = OutputMaterialSerializer serializer_class = OutputMaterialSerializer
filterset_fields = ['process', 'product'] filterset_fields = ['subproduction']
ordering = ['sort', '-create_time'] ordering = ['sort', '-create_time']
def get_serializer_class(self): def get_serializer_class(self):
@ -127,18 +114,20 @@ class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet):
return OutputMaterialUpdateSerializer return OutputMaterialUpdateSerializer
return OutputMaterialSerializer return OutputMaterialSerializer
class UsedStepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet): class UsedStepViewSet(OptimizationMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, UpdateModelMixin, GenericViewSet):
""" """
产品生产子工序表 产品生产子工序表
""" """
perms_map = {'*':'*'} perms_map = {'*':'*'}
queryset = UsedStep.objects.all() queryset = UsedStep.objects.all()
filterset_fields = ['process', 'product', 'step'] filterset_fields = ['subproduction', 'step']
ordering = ['step__sort', '-step__create_time'] ordering = ['step__sort', '-step__create_time']
def get_serializer_class(self): def get_serializer_class(self):
if self.action =='create': if self.action =='create':
return UsedStepCreateSerializer return UsedStepCreateSerializer
elif self.action == 'update':
return UsedStepUpdateSerializer
return UsedStepListSerializer return UsedStepListSerializer
class RecordFormViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): class RecordFormViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet):
@ -189,7 +178,7 @@ class TechDocViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet):
""" """
perms_map = {'*':'*'} perms_map = {'*':'*'}
queryset = TechDoc.objects.select_related('file').all() queryset = TechDoc.objects.select_related('file').all()
filterset_fields = ['process', 'product'] filterset_fields = ['subproduction']
search_fields = ['name'] search_fields = ['name']
ordering = ['-id'] ordering = ['-id']

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 SamConfig(AppConfig):
name = 'apps.pm'
verbose_name = '生产计划管理'

View File

@ -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': '生产计划',
},
),
]

View File

@ -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='生产数量'),
),
]

View File

View File

@ -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

View File

@ -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__'

View File

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

13
hb_server/apps/pm/urls.py Normal file
View File

@ -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)),
]

View File

@ -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

View File

@ -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='已排数量'),
),
]

View File

@ -5,6 +5,7 @@ from django.db.models.base import Model
import django.utils.timezone as timezone import django.utils.timezone as timezone
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from utils.model import SoftModel, BaseModel from utils.model import SoftModel, BaseModel
from apps.mtm.models import Material 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) 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) product = models.ForeignKey(Material, verbose_name='所需产品', on_delete=models.CASCADE)
count = models.IntegerField('所需数量', default=0) count = models.IntegerField('所需数量', default=0)
planed_count = models.IntegerField('已排数量', default=0)
delivery_date = models.DateField('交货日期') delivery_date = models.DateField('交货日期')
class Meta: class Meta:
verbose_name = '订单信息' verbose_name = '订单信息'

View File

@ -3,7 +3,9 @@ from apps.sam.models import Contract, Customer, Order
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
from apps.system.mixins import CreateUpdateCustomMixin from apps.system.mixins import CreateUpdateCustomMixin
from django.shortcuts import render 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. # Create your views here.
class CustomerViewSet(CreateUpdateCustomMixin, ModelViewSet): class CustomerViewSet(CreateUpdateCustomMixin, ModelViewSet):
""" """
@ -56,3 +58,13 @@ class OrderViewSet(CreateUpdateCustomMixin, ModelViewSet):
if self.action in ['create', 'update']: if self.action in ['create', 'update']:
return OrderCreateUpdateSerializer return OrderCreateUpdateSerializer
return OrderSerializer 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)

View File

@ -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

View File

@ -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='关联工单'),
),
]

View File

@ -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='干预类型'),
),
]

View File

@ -39,7 +39,7 @@ class State(CommonAModel):
PARTICIPANT_TYPE_ROBOT = 6 PARTICIPANT_TYPE_ROBOT = 6
PARTICIPANT_TYPE_FIELD = 7 PARTICIPANT_TYPE_FIELD = 7
PARTICIPANT_TYPE_PARENT_FIELD = 8 PARTICIPANT_TYPE_PARENT_FIELD = 8
type2_choices = ( state_participanttype_choices = (
(0, '无处理人'), (0, '无处理人'),
(PARTICIPANT_TYPE_PERSONAL, '个人'), (PARTICIPANT_TYPE_PERSONAL, '个人'),
(PARTICIPANT_TYPE_MULTI, '多人'), (PARTICIPANT_TYPE_MULTI, '多人'),
@ -70,9 +70,9 @@ class State(CommonAModel):
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_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') 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.全部处理(要求所有参与人都要处理一遍,才能进入下一步)') distribute_type = models.IntegerField('分配方式', default=1, choices=state_distribute_choices, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
class Transition(CommonAModel): class Transition(CommonAModel):
@ -87,6 +87,31 @@ class Transition(CommonAModel):
(2, '拒绝'), (2, '拒绝'),
(3, '其他') (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) 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='所属工作流')
timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效') timer = models.IntegerField('定时器(单位秒)', default=0, help_text='单位秒。处于源状态X秒后如果状态都没有过变化则自动流转到目标状态。设置时间有效')
@ -149,6 +174,13 @@ class Ticket(CommonAModel):
(TICKET_ACT_STATE_FINISH, '已完成'), (TICKET_ACT_STATE_FINISH, '已完成'),
(TICKET_ACT_STATE_CLOSED, '已关闭') (TICKET_ACT_STATE_CLOSED, '已关闭')
) )
category_choices =(
('all', '全部'),
('owner', '我创建的'),
('duty', '代办'),
('worked', '我处理的'),
('relation', '抄送我的')
)
title = models.CharField('标题', max_length=500, blank=True, default='', help_text="工单标题") title = models.CharField('标题', max_length=500, blank=True, default='', help_text="工单标题")
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流') workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流')
sn = models.CharField('流水号', max_length=25, help_text="工单的流水号") sn = models.CharField('流水号', max_length=25, help_text="工单的流水号")
@ -159,7 +191,7 @@ class Ticket(CommonAModel):
in_add_node = models.BooleanField('加签状态中', default=False, help_text='是否处于加签状态下') 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='加签操作的人,工单当前处理人处理完成后会回到该处理人,当处于加签状态下才有效') 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列表') participant = models.JSONField('当前处理人', default=list, blank=True, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表')
act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices) act_state = models.IntegerField('进行状态', default=1, help_text='当前工单的进行状态', choices=act_state_choices)
multi_all_person = models.JSONField('全部处理的结果', default=dict, blank=True, help_text='需要当前状态处理人全部处理时实际的处理结果json格式') 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) 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) 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_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) 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) 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格式') ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据json格式')
intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices)

View File

@ -23,7 +23,7 @@ class WorkflowSimpleSerializer(serializers.ModelSerializer):
class StateSimpleSerializer(serializers.ModelSerializer): class StateSimpleSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = State model = State
fields = ['id', 'name', 'type'] fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat']
class TransitionSerializer(serializers.ModelSerializer): class TransitionSerializer(serializers.ModelSerializer):
source_state_ = StateSimpleSerializer(source='source_state', read_only=True) source_state_ = StateSimpleSerializer(source='source_state', read_only=True)
@ -71,6 +71,43 @@ class TicketSerializer(serializers.ModelSerializer):
queryset = queryset.select_related('workflow','state') queryset = queryset.select_related('workflow','state')
return queryset 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): class TicketFlowSerializer(serializers.ModelSerializer):
participant_ = UserSimpleSerializer(source='participant', read_only=True) participant_ = UserSimpleSerializer(source='participant', read_only=True)
state_ = StateSimpleSerializer(source='state', read_only=True) state_ = StateSimpleSerializer(source='state', read_only=True)
@ -78,8 +115,18 @@ class TicketFlowSerializer(serializers.ModelSerializer):
model = TicketFlow model = TicketFlow
fields = '__all__' 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): class TicketHandleSerializer(serializers.Serializer):
transition = serializers.IntegerField(label="流转id") transition = serializers.IntegerField(label="流转id")
ticket_data = serializers.JSONField(label="表单数据json") ticket_data = serializers.JSONField(label="表单数据json")
suggestion = serializers.CharField(label="处理意见", required = False) suggestion = serializers.CharField(label="处理意见", required = False)
class TicketRetreatSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="撤回原因", required = False)

View File

@ -1,3 +1,4 @@
from apps.wf.serializers import CustomFieldSerializer
from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer from apps.wf.serializers import TicketSerializer, TicketSimpleSerializer
from typing import Tuple from typing import Tuple
from apps.system.models import User from apps.system.models import User
@ -56,10 +57,11 @@ class WfService(object):
@classmethod @classmethod
def get_ticket_steps(cls, ticket:Ticket): def get_ticket_steps(cls, ticket:Ticket):
steps = cls.get_worlflow_states(ticket.workflow) steps = cls.get_worlflow_states(ticket.workflow)
nsteps_list = []
for i in steps: for i in steps:
if ticket.state.is_hidden and ticket.state != i: if ticket.state == i or (not i.is_hidden):
steps.remove(i) nsteps_list.append(i)
return steps return nsteps_list
@classmethod @classmethod
def get_ticket_transitions(cls, ticket:Ticket): def get_ticket_transitions(cls, ticket:Ticket):
@ -90,20 +92,20 @@ class WfService(object):
@classmethod @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 source_state = ticket.state
destination_state = transition.destination_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: 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 return destination_state
@classmethod @classmethod
@ -163,21 +165,23 @@ class WfService(object):
state = ticket.state state = ticket.state
if participant_type == State.PARTICIPANT_TYPE_PERSONAL: if participant_type == State.PARTICIPANT_TYPE_PERSONAL:
if user.id != participant: 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]: elif participant_type in [State.PARTICIPANT_TYPE_MULTI, State.PARTICIPANT_TYPE_DEPT, State.PARTICIPANT_TYPE_ROLE]:
if user.id not in participant: if user.id not in participant:
return dict(permission=False, msg="非当前处理人") return dict(permission=False, msg="非当前处理人", need_accept=False)
current_participant_count = len(participant) current_participant_count = len(participant)
if current_participant_count == 1: if current_participant_count == 1:
if [user.id] != participant: if [user.id] == participant or user.id == participant:
return dict(permission=False, msg="非当前处理人") pass
else:
return dict(permission=False, msg="非当前处理人", need_accept=False)
elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE: elif current_participant_count >1 and state.distribute_type == State.STATE_DISTRIBUTE_TYPE_ACTIVE:
if user.id not in participant: 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) return dict(permission=False, msg="需要先接单再处理", need_accept=True)
if ticket.in_add_node: if ticket.in_add_node:
return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作") return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作", need_accept=False)
return dict(permission=True, msg="") return dict(permission=True, msg="", need_accept=False)
@classmethod @classmethod
def check_dict_has_all_same_value(cls, dict_obj: object)->tuple: def check_dict_has_all_same_value(cls, dict_obj: object)->tuple:
@ -212,3 +216,4 @@ class WfService(object):
return field_info_dict return field_info_dict

View File

@ -1,6 +1,6 @@
from django.db.models import base from django.db.models import base
from rest_framework import urlpatterns 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 django.urls import path, include
from rest_framework.routers import DefaultRouter 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('transition', TransitionViewSet, basename='wf_transitions')
router.register('customfield', CustomFieldViewSet, basename='wf_customfield') router.register('customfield', CustomFieldViewSet, basename='wf_customfield')
router.register('ticket', TicketViewSet, basename='wf_ticket') router.register('ticket', TicketViewSet, basename='wf_ticket')
router.register('ticketflow', TicketFlowViewSet, basename='wf_ticketflow')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),
] ]

View File

@ -1,8 +1,9 @@
from apps.wf.filters import TicketFilterSet
from django.core.exceptions import AppRegistryNotReady from django.core.exceptions import AppRegistryNotReady
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import serializers from rest_framework import serializers
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin 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 django.shortcuts import get_object_or_404, render
from rest_framework.viewsets import GenericViewSet, ModelViewSet from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action, api_view from rest_framework.decorators import action, api_view
@ -93,7 +94,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
queryset = Ticket.objects.all() queryset = Ticket.objects.all()
serializer_class = TicketSerializer serializer_class = TicketSerializer
search_fields = ['title'] search_fields = ['title']
filterset_fields = ['workflow', 'state'] filterset_class = TicketFilterSet
ordering = ['-create_time'] ordering = ['-create_time']
def get_serializer_class(self): def get_serializer_class(self):
@ -101,8 +102,19 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
return TicketCreateSerializer return TicketCreateSerializer
elif self.action == 'handle': elif self.action == 'handle':
return TicketHandleSerializer 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() 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): def create(self, request, *args, **kwargs):
""" """
新建工单 新建工单
@ -118,7 +130,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
if value == State.STATE_FIELD_REQUIRED: if value == State.STATE_FIELD_REQUIRED:
if key not in ticket_data or not ticket_data[key]: if key not in ticket_data or not ticket_data[key]:
raise APIException('字段{}必填'.format(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) 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) 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_type = participant_info.get('destination_participant_type', 0)
@ -152,7 +164,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
return Response(TicketSerializer(instance=ticket).data) 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): def handle(self, request, pk=None):
""" """
处理工单 处理工单
@ -175,7 +187,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
if value == State.STATE_FIELD_REQUIRED: if value == State.STATE_FIELD_REQUIRED:
if key not in ticket_data or not ticket_data[key]: if key not in ticket_data or not ticket_data[key]:
raise APIException('字段{}必填'.format(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 multi_all_person = ticket.multi_all_person
if multi_all_person: if multi_all_person:
multi_all_person[request.user.id] =dict(transition=transition.id) multi_all_person[request.user.id] =dict(transition=transition.id)
@ -238,6 +250,16 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
steps = WfService.get_ticket_steps(ticket) steps = WfService.get_ticket_steps(ticket)
return Response(StateSerializer(instance=steps, many=True).data) 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':'*'}) @action(methods=['get'], detail=True, perms_map={'get':'*'})
def transitions(self, request, pk=None): def transitions(self, request, pk=None):
""" """
@ -261,12 +283,47 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
# 接单日志 # 接单日志
# 更新工单流转记录 # 更新工单流转记录
TicketFlow.objects.create(ticket=ticket, state=ticket.state, ticket_data=WfService.get_ticket_all_field_value(ticket), 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) participant=request.user, transition=None)
return Response() return Response()
else: else:
raise APIException('无需接单') 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): class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
""" """
工单日志 工单日志
@ -275,5 +332,5 @@ class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
queryset = TicketFlow.objects.all() queryset = TicketFlow.objects.all()
serializer_class = TicketFlowSerializer serializer_class = TicketFlowSerializer
search_fields = ['suggestion'] search_fields = ['suggestion']
filterset_fields = ['paticipant', 'state', 'ticket'] filterset_fields = ['ticket']
ordering = ['-create_time'] ordering = ['-create_time']

View File

@ -54,7 +54,8 @@ INSTALLED_APPS = [
'apps.mtm', 'apps.mtm',
'apps.inm', 'apps.inm',
'apps.sam', 'apps.sam',
'apps.qm' 'apps.qm',
'apps.pm'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -67,7 +67,7 @@ urlpatterns = [
path('api/inm/', include('apps.inm.urls')), path('api/inm/', include('apps.inm.urls')),
path('api/sam/', include('apps.sam.urls')), path('api/sam/', include('apps.sam.urls')),
path('api/qm/', include('apps.qm.urls')), path('api/qm/', include('apps.qm.urls')),
path('api/pm/', include('apps.pm.urls')),
# 工具 # 工具
path('api/utils/signature/', GenSignature.as_view()), path('api/utils/signature/', GenSignature.as_view()),