diff --git a/hb_client/.env.production b/hb_client/.env.production
index 484e806..d214255 100644
--- a/hb_client/.env.production
+++ b/hb_client/.env.production
@@ -3,4 +3,5 @@ ENV = 'production'
# base api
VUE_APP_BASE_API = 'http://47.95.0.242:2222/api'
+#VUE_APP_BASE_API = 'http://127.0.0.1:8000/api'
diff --git a/hb_client/src/App.vue b/hb_client/src/App.vue
index b06679a..e50cc4e 100644
--- a/hb_client/src/App.vue
+++ b/hb_client/src/App.vue
@@ -14,9 +14,19 @@ export default {
},
data(){
return{
- isRouterAlive:true
+ isRouterAlive:true,
+ timer:null
}
},
+ mounted(){
+ // this.$store.dispatch("user/getCount", {})
+ this.timer = window.setInterval(() => {
+ setTimeout(() => {
+ this.$store.dispatch("user/getCount", {})
+ },0)
+ },30000)
+
+ },
methods:{
reload(){
this.isRouterAlive=false;
@@ -24,6 +34,9 @@ export default {
this.isRouterAlive=true;
})
},
+ },
+ destroyed() {
+ clearInterval(this.timer)
}
}
@@ -36,4 +49,9 @@ export default {
.el-step__title.is-process{
color: #409EFF;
}
+ .navbarBadge .el-badge__content.is-fixed{
+ top: 15px;
+ right: 18px;
+ }
+
diff --git a/hb_client/src/api/testModel.js b/hb_client/src/api/testModel.js
index fc6204e..3cbdb33 100644
--- a/hb_client/src/api/testModel.js
+++ b/hb_client/src/api/testModel.js
@@ -1,7 +1,7 @@
import request from '@/utils/request'
export function faceLogin(data) {
return request({
- url: '/system/facelogin/',
+ url: '/hrm/facelogin/',
method: 'post',
data
})
diff --git a/hb_client/src/api/workflow.js b/hb_client/src/api/workflow.js
index 137b132..001df7e 100644
--- a/hb_client/src/api/workflow.js
+++ b/hb_client/src/api/workflow.js
@@ -163,13 +163,45 @@ export function ticketAccpet(id,data) {
})
}
//撤回工单,允许创建人在指定状态撤回工单至初始状态
-export function getTicketRetreat(id,data) {
+export function ticketRetreat(id,data) {
return request({
url: `/wf/ticket/${id}/retreat/`,
method: 'post',
data
})
}
+//关闭工单,仅允许创建人在初始状态关闭工单
+export function ticketAddNode(id,data) {
+ return request({
+ url: `/wf/ticket/${id}/add_node/`,
+ method: 'post',
+ data
+ })
+}
+//加签
+export function ticketClose(id,data) {
+ return request({
+ url: `/wf/ticket/${id}/close/`,
+ method: 'post',
+ data
+ })
+}
+//加签
+export function ticketAddNodeEnd(id,data) {
+ return request({
+ url: `/wf/ticket/${id}/add_node_end/`,
+ method: 'post',
+ data
+ })
+}
+//工单删除
+export function ticketDestory(data) {
+ return request({
+ url: `/wf/ticket/destory/`,
+ method: 'post',
+ data
+ })
+}
//工单详情
export function getTicketDetail(id) {
return request({
@@ -193,3 +225,18 @@ export function getTicketFlowlog(id) {
method: 'get'
})
}
+//工单代办数量
+export function getCount(data) {
+ return request({
+ url: `/wf/ticket/duty_agg/`,
+ method: 'get',
+ params:data
+ })
+}
+//工单代办数量
+export function getCodes() {
+ return request({
+ url: `/wf/participant_from_code`,
+ method: 'get'
+ })
+}
diff --git a/hb_client/src/layout/components/Navbar.vue b/hb_client/src/layout/components/Navbar.vue
index 7cb30fd..1ba9bc9 100644
--- a/hb_client/src/layout/components/Navbar.vue
+++ b/hb_client/src/layout/components/Navbar.vue
@@ -6,6 +6,9 @@
+
+
+
+
+
+
+
{{item.workflow__name}}:{{item.count}}
+
+
@@ -15,6 +24,7 @@
@@ -92,4 +114,35 @@ export default {
.mobile .fixed-header {
width: 100%;
}
+ .floatDiv{
+ position: fixed;
+ z-index: 3000;
+ bottom: 10vh;
+ right: 5vh;
+ width: 50px;
+ height: 50px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 50px;
+ border-radius: 26px;
+ border: 2px solid #409EFF;
+ }
+ .typeWrap{
+ display: none;
+ background: #ffffff;
+ padding: 10px;
+ box-shadow: 0 0 7px 2px #d3dce6;
+ position: absolute;
+ right: 60px;
+ bottom: 0;
+ }
+ .floatDiv:hover>.typeWrap{
+ display: block;
+ }
+ .detailItem{
+ height: 30px;
+ color: #888888;
+ line-height: 30px;
+ width: max-content;
+ }
diff --git a/hb_client/src/router/index.js b/hb_client/src/router/index.js
index 0cd776d..e7da4f0 100644
--- a/hb_client/src/router/index.js
+++ b/hb_client/src/router/index.js
@@ -86,13 +86,13 @@ export const asyncRoutes = [
component: Layout,
redirect: '/mtm/material/',
name: 'mtm',
- meta: { title: '制造管理', icon: 'example', perms: ['procurement_set'] },
+ meta: { title: '制造管理', icon: 'example', perms: ['mtm_manage'] },
children: [
{
path: 'material',
name: 'material',
component: () => import('@/views/mtm/material'),
- meta: { title: '物料清单', icon: 'example', perms: ['vendor_manage'] }
+ meta: { title: '物料清单', icon: 'example', perms: ['mtm_material'] }
}
,
{
@@ -106,7 +106,7 @@ export const asyncRoutes = [
path: 'process',
name: 'process',
component: () => import('@/views/mtm/process'),
- meta: { title: '工序管理', icon: 'example', perms: ['vendor_manage'] }
+ meta: { title: '工序管理', icon: 'example', perms: ['mtm_process'] }
},
{
path: 'step/:id',
@@ -127,7 +127,7 @@ export const asyncRoutes = [
path: '/mtm/productprocess/',
name: 'productprocess',
component: () => import('@/views/mtm/productprocess'),
- meta: { title: '产品管理', icon: 'example', perms: ['vendor_manage'] }
+ meta: { title: '产品管理', icon: 'example', perms: ['mtm_productprocess'] }
},
]
}
@@ -137,7 +137,7 @@ export const asyncRoutes = [
component: Layout,
redirect: '/pm/plan',
name: 'pm',
- meta: { title: '生产管理', icon: 'example', perms: ['equipment_set'] },
+ meta: { title: '生产管理', icon: 'example', perms: ['pm_manage'] },
children: [
{
path: 'plan',
@@ -157,13 +157,13 @@ export const asyncRoutes = [
path: 'resources',
name: 'resources',
component: () => import('@/views/pm/resources'),
- meta: { title: '生产资源配置', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '生产资源配置', icon: 'example', perms: ['pm_resources'] }
},
{
path: 'testitem',
name: 'testitem',
component: () => import('@/views/pm/plan'),
- meta: { title: '生产作业管理', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '生产作业管理', icon: 'example', perms: ['pm_testitem'] }
}
]
}
@@ -194,31 +194,31 @@ export const asyncRoutes = [
component: Layout,
redirect: '/em/equipment',
name: 'em',
- meta: { title: '设备管理', icon: 'example', perms: ['equipment_set'] },
+ meta: { title: '设备管理', icon: 'example', perms: ['em_manage'] },
children: [
{
path: 'equipment',
name: 'equipment',
component: () => import('@/views/em/equipment'),
- meta: { title: '生产设备', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '生产设备', icon: 'example', perms: ['em_equipment'] }
},
{
path: 'detection ',
name: 'detection ',
component: () => import('@/views/em/detection'),
- meta: { title: '监视和测量设备', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '监视和测量设备', icon: 'example', perms: ['em_detection'] }
},
{
path: 'record',
name: 'record',
component: () => import('@/views/em/record'),
- meta: { title: '校准检定记录', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '校准检定记录', icon: 'example', perms: ['em_record'] }
},
{
path: 'detection ',
name: 'detection ',
component: () => import('@/views/em/detection'),
- meta: { title: '运维记录', icon: 'example', perms: ['index_manage'] }
+ meta: { title: '运维记录', icon: 'example', perms: ['em_detection'] }
}
]
},
@@ -349,32 +349,39 @@ export const asyncRoutes = [
component: Layout,
redirect: '/workflow/index',
name: 'workflow',
- meta: { title: '工作流', icon: 'example', perms: ['workflow_set'] },
+ meta: { title: '工作流', icon: 'example', perms: ['workflow_manage'] },
children: [
{
path: 'index',
name: 'index',
component: () => import('@/views/workflow/index'),
- meta: { title: '工作流配置', icon: 'example', perms: ['workflow_manage'] }
+ meta: { title: '工作流配置', icon: 'example', perms: ['workflow_index'] }
},
{
path: 'ticket',
name: 'ticket',
component: () => import('@/views/workflow/ticket'),
- meta: { title: '工单管理', icon: 'example', perms: ['workflow_manage'] },
+ meta: { title: '工单管理', icon: 'example' ,noCache: true, perms: ['workflow_ticket'] },
+ },
+ {
+ path: 'workFlowTickets',
+ name: 'workFlowTickets',
+ component: () => import('@/views/workflow/workFlowTickets'),
+ meta: { title: '工单管理', icon: 'example' ,noCache: true,},
+ hidden: true
},
{
path: 'configuration',
name: 'configuration',
component: () => import('@/views/workflow/configuration'),
- meta: { title: '人员信息详情', icon: 'example', perms: ['workflow_manage'] },
+ meta: { title: '人员信息详情', icon: 'example' },
hidden: true
},
{
path: 'ticketHandle',
name: 'ticketHandle',
component: () => import('@/views/workflow/ticketHandle'),
- meta: { title: '工单处理', icon: 'example', perms: ['workflow_manage'] },
+ meta: { title: '工单处理', icon: 'example',noCache: true,},
hidden: true
},
]
diff --git a/hb_client/src/store/getters.js b/hb_client/src/store/getters.js
index 566e457..3ff47b0 100644
--- a/hb_client/src/store/getters.js
+++ b/hb_client/src/store/getters.js
@@ -5,6 +5,7 @@ const getters = {
avatar: state => state.user.avatar,
name: state => state.user.name,
perms: state => state.user.perms,
+ count: state => state.user.count,
size: state => state.app.size,
permission_routes: state => state.permission.routes,
visitedViews: state => state.tagsView.visitedViews,
diff --git a/hb_client/src/store/modules/user.js b/hb_client/src/store/modules/user.js
index 0f1d024..c8c3190 100644
--- a/hb_client/src/store/modules/user.js
+++ b/hb_client/src/store/modules/user.js
@@ -1,4 +1,5 @@
import { login, logout, getInfo } from '@/api/user'
+import { getCount } from '@/api/workflow'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { resetRouter } from '@/router'
@@ -7,6 +8,7 @@ const getDefaultState = () => {
token: getToken(),
name: '',
avatar: '',
+ count: {},
perms: []
}
}
@@ -28,6 +30,9 @@ const mutations = {
},
SET_PERMS: (state, perms) => {
state.perms = perms
+ },
+ SET_COUNT: (state, count) => {
+ state.count = count
}
}
@@ -90,15 +95,27 @@ const actions = {
},
// remove token
- resetToken({ commit }) {
+ resetToken({ commit },data) {
return new Promise(resolve => {
- removeToken() // must remove token first
- commit('RESET_STATE')
+ removeToken(); // must remove token first
+ commit('RESET_STATE');
+ commit('SET_TOKEN', data.access);
+ setToken(data.access);
resolve()
})
},
setSize({ commit }, size) {
commit('SET_SIZE', size)
+ },
+ getCount({ commit }) {
+ return new Promise((resolve, reject) => {
+ getCount({}).then((res) => {
+ commit('SET_COUNT', res.data);
+ resolve()
+ }).catch(error => {
+ reject(error)
+ })
+ })
}
}
diff --git a/hb_client/src/styles/index.scss b/hb_client/src/styles/index.scss
index 059de63..d80b15e 100644
--- a/hb_client/src/styles/index.scss
+++ b/hb_client/src/styles/index.scss
@@ -86,21 +86,21 @@ div:focus {
.el-dialog__header {
padding: 10px 10px 6px;
}
-// .el-dialog{
-// display: flex;
-// flex-direction: column;
-// margin:0 !important;
-// position:absolute;
-// top:50%;
-// left:50%;
-// transform:translate(-50%,-50%);
-// /*height:600px;*/
-// max-height:calc(100% - 30px);
-// max-width:calc(100% - 30px);
-// }
+ .el-dialog{
+ display: flex;
+ flex-direction: column;
+ margin:0 !important;
+ position:absolute;
+ top:50%;
+ left:50%;
+ transform:translate(-50%,-50%);
+ /*height:600px;*/
+ max-height:calc(100% - 30px);
+ max-width:calc(100% - 30px);
+ }
.el-dialog .el-dialog__body{
- // flex:1;
- // overflow: auto;
+ flex:1;
+ overflow: auto;
padding: 8px 12px;
}
diff --git a/hb_client/src/views/dashboard/index.vue b/hb_client/src/views/dashboard/index.vue
index bfd075e..5dc8de5 100644
--- a/hb_client/src/views/dashboard/index.vue
+++ b/hb_client/src/views/dashboard/index.vue
@@ -1,5 +1,9 @@
+
+
+
+
name: {{ name }}
perms: {{ perm }}
@@ -13,8 +17,19 @@ export default {
computed: {
...mapGetters([
'name',
- 'perms'
+ 'perms',
+ 'count'
])
+ },
+ methods:{
+ gotoTicketPage(){
+ let path = this.$route.path;
+ if(path==='/workflow/ticket'){
+ this.$message.success("已在当前页面");
+ }else{
+ this.$router.push({name:'ticket',params:{}})
+ }
+ },
}
}
diff --git a/hb_client/src/views/login/index.vue b/hb_client/src/views/login/index.vue
index f9ad27a..2c6092f 100644
--- a/hb_client/src/views/login/index.vue
+++ b/hb_client/src/views/login/index.vue
@@ -37,6 +37,7 @@
name="password"
tabindex="2"
auto-complete="on"
+ id="passwordInput"
@keyup.enter.native="handleLogin"
> {
+ this.$store.dispatch("user/getCount", {})
this.$router.push({ path: this.redirect || "/" });
this.loading = false;
localStorage.setItem("rem_username", this.loginForm.username);
@@ -147,6 +149,7 @@
.catch(() => {
this.loading = false;
});
+
} else {
console.log("error submit!!");
return false;
@@ -200,7 +203,9 @@
margin-left: 2px;
}
}
-
+ #passwordInput{
+ padding-right: 35px;
+ }
.show-pwd {
height: 39px;
margin-right: 2px;
diff --git a/hb_client/src/views/testModel/faceLogin.vue b/hb_client/src/views/testModel/faceLogin.vue
index 335298a..360a18d 100644
--- a/hb_client/src/views/testModel/faceLogin.vue
+++ b/hb_client/src/views/testModel/faceLogin.vue
@@ -13,13 +13,15 @@
diff --git a/hb_server/apps/hrm/migrations/0003_employee_face_data.py b/hb_server/apps/hrm/migrations/0003_employee_face_data.py
new file mode 100644
index 0000000..1823ad2
--- /dev/null
+++ b/hb_server/apps/hrm/migrations/0003_employee_face_data.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.6 on 2021-10-18 05:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('hrm', '0002_auto_20210924_1127'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='employee',
+ name='face_data',
+ field=models.JSONField(blank=True, null=True, verbose_name='人脸识别数据'),
+ ),
+ ]
diff --git a/hb_server/apps/hrm/models.py b/hb_server/apps/hrm/models.py
index c448e64..1176416 100644
--- a/hb_server/apps/hrm/models.py
+++ b/hb_server/apps/hrm/models.py
@@ -28,9 +28,12 @@ class Employee(CommonAModel):
academic = models.CharField('学历', max_length=50, null=True, blank=True)
jobstate = models.IntegerField('在职状态', choices=jobstate_choices, default=1)
job = models.ForeignKey(Position, null=True, blank=True, on_delete=models.SET_NULL, verbose_name='岗位')
+ face_data = models.JSONField('人脸识别数据', null=True, blank=True)
class Meta:
verbose_name = '员工补充信息'
verbose_name_plural = verbose_name
def __str__(self):
- return self.name
\ No newline at end of file
+ return self.name
+
+
diff --git a/hb_server/apps/hrm/serializers.py b/hb_server/apps/hrm/serializers.py
index 96ab4df..7a14400 100644
--- a/hb_server/apps/hrm/serializers.py
+++ b/hb_server/apps/hrm/serializers.py
@@ -1,6 +1,6 @@
from apps.system.models import User
from rest_framework.serializers import ModelSerializer
-
+from rest_framework import serializers
from .models import Employee
from apps.system.serializers import UserListSerializer, UserSimpleSerializer
from django.db.models.query import Prefetch
@@ -20,3 +20,6 @@ class EmployeeSerializer(ModelSerializer):
# queryset=User.objects.filter(employee_user__isnull=True))
# )
# return queryset
+
+class FaceLoginSerializer(serializers.Serializer):
+ base64 = serializers.CharField()
diff --git a/hb_server/apps/hrm/urls.py b/hb_server/apps/hrm/urls.py
index bd532d0..293124c 100644
--- a/hb_server/apps/hrm/urls.py
+++ b/hb_server/apps/hrm/urls.py
@@ -1,12 +1,13 @@
from django.db.models import base
from rest_framework import urlpatterns
-from apps.hrm.views import EmployeeViewSet
+from apps.hrm.views import EmployeeViewSet, FaceLogin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('employee', EmployeeViewSet, basename='employee')
urlpatterns = [
+ path('facelogin/', FaceLogin.as_view()),
path('', include(router.urls)),
]
diff --git a/hb_server/apps/hrm/views.py b/hb_server/apps/hrm/views.py
index 843fa96..151fa06 100644
--- a/hb_server/apps/hrm/views.py
+++ b/hb_server/apps/hrm/views.py
@@ -1,9 +1,34 @@
from django.shortcuts import render
+from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet, GenericViewSet
from rest_framework.mixins import UpdateModelMixin, RetrieveModelMixin
from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin
from apps.hrm.models import Employee
-from apps.hrm.serializers import EmployeeSerializer
+from apps.hrm.serializers import EmployeeSerializer, FaceLoginSerializer
+import face_recognition
+from django.conf import settings
+from django.core.cache import cache
+import logging
+from rest_framework.generics import CreateAPIView
+from rest_framework import status
+from rest_framework_simplejwt.tokens import RefreshToken
+
+from apps.system.models import User
+logger = logging.getLogger('log')
+
+
+def load_face_data(username:int, path:str):
+ """
+ 将某用户face_encoding加载进缓存
+ """
+ face_datas = cache.get_or_set('face_datas', {}, timeout=None)
+ photo_path = settings.BASE_DIR + path
+ picture_of_me = face_recognition.load_image_file(photo_path)
+ my_face_encoding = face_recognition.face_encodings(picture_of_me)[0]
+ face_datas[username] = my_face_encoding
+ cache.set('face_datas', face_datas, timeout=None)
+ return my_face_encoding
+
# Create your views here.
class EmployeeViewSet(CreateUpdateModelAMixin, OptimizationMixin, UpdateModelMixin, RetrieveModelMixin, GenericViewSet):
"""
@@ -12,4 +37,73 @@ class EmployeeViewSet(CreateUpdateModelAMixin, OptimizationMixin, UpdateModelMix
perms_map = {'get': '*', 'put': 'employee_update'}
queryset = Employee.objects.all()
serializer_class = EmployeeSerializer
- ordering = ['-pk']
\ No newline at end of file
+ ordering = ['-pk']
+
+ def perform_update(self, serializer):
+ instance = serializer.save(update_by = self.request.user)
+ try:
+ photo_path = settings.BASE_DIR + instance.photo
+ picture_of_me = face_recognition.load_image_file(photo_path)
+ my_face_encoding = face_recognition.face_encodings(picture_of_me)[0]
+ instance.face_data = my_face_encoding.tolist()
+ instance.save()
+ except:
+ logger.error('人脸识别出错')
+
+import uuid
+import base64
+import os
+
+def tran64(s):
+ missing_padding = len(s) % 4
+ if missing_padding != 0:
+ s = s+'='* (4 - missing_padding)
+ return s
+
+class FaceLogin(CreateAPIView):
+ authentication_classes = []
+ permission_classes = []
+ serializer_class = FaceLoginSerializer
+
+
+ def create(self, request, *args, **kwargs):
+ """
+ 人脸识别登录
+ """
+ # serializer = FaceLoginSerializer(data=request.data)
+ # serializer.is_valid(raise_exception=True)
+ filename = str(uuid.uuid4())
+ filepath = settings.BASE_DIR +'/temp/' + filename +'.png'
+ with open(filepath, 'wb') as f:
+ data = tran64(request.data.get('base64').replace(' ', '+'))
+ f.write(base64.urlsafe_b64decode(data))
+ # picture_of_me = face_recognition.load_image_file(settings.BASE_DIR +'/temp/me.png')
+ # my_face_encoding = face_recognition.face_encodings(picture_of_me)[0]
+ #results = face_recognition.compare_faces([my_face_encoding], unknown_face_encoding, tolerance=0.2)
+ try:
+ unknown_picture = face_recognition.load_image_file(filepath)
+ unknown_face_encoding = face_recognition.face_encodings(unknown_picture)[0]
+ os.remove(filepath)
+ except:
+ return Response('头像解码失败', status=status.HTTP_400_BAD_REQUEST)
+
+ # 匹配人脸库
+ user_faces = Employee.objects.filter(face_data__isnull=False, user__is_active=True).values('user', 'face_data')
+ user_l = []
+ face_l = []
+ for i in user_faces:
+ user_l.append(i['user'])
+ face_l.append(i['face_data'])
+
+ results = face_recognition.compare_faces(face_l, unknown_face_encoding, tolerance=0.5)
+ for index, value in enumerate(results):
+ if value:
+ # 识别成功
+ user = User.objects.get(id=user_l[index])
+ refresh = RefreshToken.for_user(user)
+ return Response({
+ 'refresh': str(refresh),
+ 'access': str(refresh.access_token),
+ 'username':user.username
+ })
+ return Response('未找到对应用户', status=status.HTTP_400_BAD_REQUEST)
\ No newline at end of file
diff --git a/hb_server/apps/inm/models.py b/hb_server/apps/inm/models.py
index 52a7ddc..20decf8 100644
--- a/hb_server/apps/inm/models.py
+++ b/hb_server/apps/inm/models.py
@@ -6,7 +6,7 @@ from apps.system.models import CommonAModel, CommonBModel, Organization, User, D
from utils.model import SoftModel, BaseModel
from simple_history.models import HistoricalRecords
from apps.mtm.models import Material
-
+from apps.pm.models import SubProductionPlan
class WareHouse(CommonAModel):
@@ -57,4 +57,13 @@ class FIFO(CommonAModel):
(3, '采购入库'),
(4, '生产入库')
)
- type = models.IntegerField('出入库类型', default=1)
\ No newline at end of file
+ type = models.IntegerField('出入库类型', default=1)
+ operator = models.ForeignKey(User, verbose_name='操作人', on_delete=models.CASCADE)
+ subproduction_plan = models.ForeignKey(SubProductionPlan, verbose_name='关联子生产计划', on_delete=models.DO_NOTHING, null=True, blank=True)
+
+# class FIFODetail(CommonAModel):
+# """
+# 领料详细记录
+# """
+
+
diff --git a/hb_server/apps/mtm/migrations/0023_auto_20211018_1057.py b/hb_server/apps/mtm/migrations/0023_auto_20211018_1057.py
new file mode 100644
index 0000000..9cf0179
--- /dev/null
+++ b/hb_server/apps/mtm/migrations/0023_auto_20211018_1057.py
@@ -0,0 +1,49 @@
+# Generated by Django 3.2.6 on 2021-10-18 02:57
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mtm', '0022_auto_20211014_0944'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='recordformfield',
+ name='high_limit',
+ field=models.FloatField(blank=True, null=True, verbose_name='上限值'),
+ ),
+ migrations.AddField(
+ model_name='recordformfield',
+ name='high_rule',
+ field=models.IntegerField(blank=True, choices=[(1, '小于'), (2, '小于等于')], null=True, verbose_name='上限规则'),
+ ),
+ migrations.AddField(
+ model_name='recordformfield',
+ name='low_limit',
+ field=models.FloatField(blank=True, null=True, verbose_name='下限值'),
+ ),
+ migrations.AddField(
+ model_name='recordformfield',
+ name='low_rule',
+ field=models.IntegerField(blank=True, choices=[(1, '大于'), (2, '大于等于')], null=True, verbose_name='下限规则'),
+ ),
+ migrations.AddField(
+ model_name='recordformfield',
+ name='need_judge',
+ field=models.BooleanField(default=False, verbose_name='需要判定'),
+ ),
+ migrations.AddField(
+ model_name='recordformfield',
+ name='rule_expression',
+ field=models.JSONField(default=list, help_text='判定表达式, 格式为[{"expression":"{value} > 3 and {value}<10"}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,符合条件返回true,表达式只支持简单的运算或datetime/time运算.以首次匹配成功的条件为准,所以多个条件不要有冲突', verbose_name='判定表达式'),
+ ),
+ migrations.AlterField(
+ model_name='usedstep',
+ name='step',
+ field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='usedstep', to='mtm.step', verbose_name='子工序'),
+ ),
+ ]
diff --git a/hb_server/apps/mtm/migrations/0024_alter_recordformfield_rule_expression.py b/hb_server/apps/mtm/migrations/0024_alter_recordformfield_rule_expression.py
new file mode 100644
index 0000000..f31fbc4
--- /dev/null
+++ b/hb_server/apps/mtm/migrations/0024_alter_recordformfield_rule_expression.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.6 on 2021-10-19 01:44
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mtm', '0023_auto_20211018_1057'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='recordformfield',
+ name='rule_expression',
+ field=models.JSONField(default=list, help_text='判定表达式, 格式为[{"expression":"{value} > 3 and {value}<10"}] 其中{}用于填充的字段key,运算时会换算成实际的值,符合条件返回true,表达式只支持简单的运算或datetime/time运算.以首次匹配成功的条件为准,所以多个条件不要有冲突', verbose_name='判定表达式'),
+ ),
+ ]
diff --git a/hb_server/apps/mtm/migrations/0025_outputmaterial_is_main.py b/hb_server/apps/mtm/migrations/0025_outputmaterial_is_main.py
new file mode 100644
index 0000000..51f4988
--- /dev/null
+++ b/hb_server/apps/mtm/migrations/0025_outputmaterial_is_main.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.6 on 2021-10-21 08:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mtm', '0024_alter_recordformfield_rule_expression'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='outputmaterial',
+ name='is_main',
+ field=models.BooleanField(default=True, verbose_name='是否主产出'),
+ ),
+ ]
diff --git a/hb_server/apps/mtm/models.py b/hb_server/apps/mtm/models.py
index 62f983e..2602113 100644
--- a/hb_server/apps/mtm/models.py
+++ b/hb_server/apps/mtm/models.py
@@ -110,6 +110,14 @@ class RecordFormField(CommonAModel):
('selects', '多选下拉'),
('textarea', '文本域'),
)
+ high_rule_choices = (
+ (1, '小于'),
+ (2, '小于等于'),
+ )
+ low_rule_choices = (
+ (1, '大于'),
+ (2, '大于等于'),
+ )
form = models.ForeignKey(RecordForm, on_delete=models.CASCADE, verbose_name='关联表格')
field_type = models.CharField('类型', max_length=50, choices=field_type_choices)
field_key = models.CharField('字段标识', max_length=50, help_text='字段类型请尽量特殊,避免与系统中关键字冲突')
@@ -119,6 +127,13 @@ class RecordFormField(CommonAModel):
field_choice = models.JSONField('radio、checkbox、select的选项', default=dict, blank=True, null=True,
help_text='radio,checkbox,select,multiselect类型可供选择的选项,格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号')
sort = models.IntegerField('排序号', default=1)
+ need_judge = models.BooleanField('需要判定', default=False)
+ high_limit = models.FloatField('上限值', null=True, blank=True)
+ high_rule = models.IntegerField('上限规则', choices=high_rule_choices, null=True, blank=True)
+ low_limit = models.FloatField('下限值', null=True, blank=True)
+ low_rule = models.IntegerField('下限规则', choices=low_rule_choices, null=True, blank=True)
+ rule_expression = models.JSONField('判定表达式', default=list, help_text='判定表达式, 格式为[{"expression":"{value} > 3 and {value}<10"}] 其中{}用于填充的字段key,运算时会换算成实际的值,符合条件返回true,表达式只支持简单的运算或datetime/time运算.以首次匹配成功的条件为准,所以多个条件不要有冲突' )
+
class Meta:
verbose_name = '记录表格字段'
verbose_name_plural = verbose_name
@@ -162,6 +177,7 @@ class OutputMaterial(CommonAModel):
输出物料
"""
material = models.ForeignKey(Material, verbose_name='输出物料', on_delete=models.CASCADE, related_name='outputmaterial')
+ is_main = models.BooleanField('是否主产出', default=True) # 以该产品完成度计算进度
count = models.FloatField('产出量', default=1)
subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
sort = models.IntegerField('排序号', default=1)
@@ -174,7 +190,7 @@ 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='usedstep')
remark = models.TextField('生产备注', null=True, blank=True)
subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
diff --git a/hb_server/apps/mtm/serializers.py b/hb_server/apps/mtm/serializers.py
index 5da68a4..b23fcef 100644
--- a/hb_server/apps/mtm/serializers.py
+++ b/hb_server/apps/mtm/serializers.py
@@ -98,9 +98,11 @@ class InputMaterialUpdateSerializer(serializers.ModelSerializer):
class OutputMaterialSerializer(serializers.ModelSerializer):
class Meta:
model = OutputMaterial
- fields = ['count', 'sort', 'material', 'subproduction']
+ fields = ['count', 'sort', 'material', 'subproduction', 'is_main']
def create(self, validated_data):
+ if OutputMaterial.objects.filter(subproduction=validated_data['subproduction'], is_deleted=False, is_main=True).exists():
+ raise ValidationError('主产出只能有1个')
if OutputMaterial.objects.filter(material=validated_data['material'], subproduction=validated_data['subproduction'], is_deleted=False).exists():
raise ValidationError('该物料已存在')
return super().create(validated_data)
@@ -108,7 +110,7 @@ class OutputMaterialSerializer(serializers.ModelSerializer):
class OutputMaterialUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = OutputMaterial
- fields = ['count', 'sort']
+ fields = ['count', 'sort', 'is_main']
class UsedStepCreateSerializer(serializers.ModelSerializer):
"""
@@ -175,7 +177,7 @@ class RecordFormFieldSerializer(serializers.ModelSerializer):
class RecordFormFieldCreateSerializer(serializers.ModelSerializer):
class Meta:
model = RecordFormField
- fields = ['form', 'field_type', 'field_key', 'field_name', 'boolean_field_display', 'field_choice', 'sort']
+ fields = ['form', 'field_type', 'field_key', 'field_name', 'boolean_field_display', 'field_choice', 'sort', 'need_judge', 'high_limit', 'high_rule', 'low_limit', 'low_rule', 'rule_expression']
def validate(self, data):
if RecordFormField.objects.filter(field_key=data['field_key'], form=data['form'], is_deleted=False).exists():
@@ -185,7 +187,7 @@ class RecordFormFieldCreateSerializer(serializers.ModelSerializer):
class RecordFormFieldUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = RecordFormField
- fields = ['field_type', 'field_name', 'boolean_field_display', 'field_choice', 'sort']
+ fields = ['field_type', 'field_name', 'boolean_field_display', 'field_choice', 'sort', 'need_judge', 'high_limit', 'high_rule', 'low_limit', 'low_rule', 'rule_expression']
class RecordFormFieldSimpleSerializer(serializers.ModelSerializer):
class Meta:
diff --git a/hb_server/apps/pm/migrations/0004_subproductionplan_steps.py b/hb_server/apps/pm/migrations/0004_subproductionplan_steps.py
new file mode 100644
index 0000000..56b78dd
--- /dev/null
+++ b/hb_server/apps/pm/migrations/0004_subproductionplan_steps.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.6 on 2021-10-15 02:14
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pm', '0003_auto_20211014_1503'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subproductionplan',
+ name='steps',
+ field=models.JSONField(default=list, verbose_name='工艺步骤'),
+ ),
+ ]
diff --git a/hb_server/apps/pm/migrations/0005_auto_20211019_0944.py b/hb_server/apps/pm/migrations/0005_auto_20211019_0944.py
new file mode 100644
index 0000000..46824e4
--- /dev/null
+++ b/hb_server/apps/pm/migrations/0005_auto_20211019_0944.py
@@ -0,0 +1,38 @@
+# Generated by Django 3.2.6 on 2021-10-19 01:44
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('mtm', '0024_alter_recordformfield_rule_expression'),
+ ('pm', '0004_subproductionplan_steps'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='subproductionplan',
+ name='state',
+ field=models.IntegerField(default=0, verbose_name='状态'),
+ ),
+ migrations.CreateModel(
+ name='SubProductionProgress',
+ 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='删除标记')),
+ ('type', models.IntegerField(default=1, verbose_name='物料应用类型')),
+ ('count', models.IntegerField(verbose_name='应出入数')),
+ ('count_real', models.IntegerField(verbose_name='实际出入数')),
+ ('material', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mtm.material', verbose_name='关联物料')),
+ ('subproduction_plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='progress_subplan', to='pm.subproductionplan', verbose_name='关联子生产计划')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/hb_server/apps/pm/migrations/0006_alter_subproductionprogress_count_real.py b/hb_server/apps/pm/migrations/0006_alter_subproductionprogress_count_real.py
new file mode 100644
index 0000000..a4924dd
--- /dev/null
+++ b/hb_server/apps/pm/migrations/0006_alter_subproductionprogress_count_real.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.6 on 2021-10-19 01:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('pm', '0005_auto_20211019_0944'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='subproductionprogress',
+ name='count_real',
+ field=models.IntegerField(default=0, verbose_name='实际出入数'),
+ ),
+ ]
diff --git a/hb_server/apps/pm/models.py b/hb_server/apps/pm/models.py
index f7db0c1..8e9299d 100644
--- a/hb_server/apps/pm/models.py
+++ b/hb_server/apps/pm/models.py
@@ -31,20 +31,35 @@ class SubProductionPlan(CommonAModel):
"""
子生产计划
"""
+ state_choices=(
+ (0, '制定中'),
+ (1, '已下达'),
+ (2, '已接收'),
+ (3, '生产中'),
+ (4, '已完成')
+ )
production_plan = models.ForeignKey(ProductionPlan, verbose_name='关联主生产计划', on_delete=models.CASCADE)
subproduction = models.ForeignKey(SubProduction, verbose_name='关联生产分解', on_delete=models.CASCADE)
start_date = models.DateField('计划开工日期')
end_date = models.DateField('计划完工日期')
workshop = models.ForeignKey(Organization, verbose_name='生产车间', on_delete=models.CASCADE)
process = models.ForeignKey(Process, verbose_name='关联大工序', on_delete=models.CASCADE)
- # steps = models.JSONField('工艺步骤', default=list)
+ steps = models.JSONField('工艺步骤', default=list)
+ state = models.IntegerField('状态', default=0)
class Meta:
verbose_name = '子生产计划'
verbose_name_plural = verbose_name
-# class ProductionProgress(BaseModel):
-# """
-# 子计划生产进度
-# """
-# subproduction_plan = models.ForeignKey(SubProductionPlan, verbose_name='关联子生产计划', on_delete=models.CASCADE)
-# material = models.
\ No newline at end of file
+class SubProductionProgress(BaseModel):
+ """
+ 子计划生产进度统计表
+ """
+ type_choices=(
+ (1, '输入物料'),
+ (2, '输出物料')
+ )
+ subproduction_plan = models.ForeignKey(SubProductionPlan, verbose_name='关联子生产计划', on_delete=models.CASCADE, related_name='progress_subplan')
+ material = models.ForeignKey(Material, verbose_name='关联物料', on_delete=models.CASCADE)
+ type = models.IntegerField('物料应用类型', default=1)
+ count = models.IntegerField('应出入数')
+ count_real = models.IntegerField('实际出入数', default=0)
diff --git a/hb_server/apps/pm/serializers.py b/hb_server/apps/pm/serializers.py
index bc8f6b4..e117b8b 100644
--- a/hb_server/apps/pm/serializers.py
+++ b/hb_server/apps/pm/serializers.py
@@ -1,4 +1,4 @@
-from apps.pm.models import ProductionPlan, SubProductionPlan
+from apps.pm.models import ProductionPlan, SubProductionPlan, SubProductionProgress
from rest_framework import serializers
from apps.sam.serializers import OrderSerializer
from apps.mtm.serializers import MaterialSimpleSerializer, ProcessSimpleSerializer
@@ -35,3 +35,12 @@ class SubProductionPlanUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = SubProductionPlan
fields = ['start_date', 'end_date']
+
+class GenSubPlanSerializer(serializers.Serializer):
+ pass
+
+class SubProductionProgressSerializer(serializers.ModelSerializer):
+ material_ = MaterialSimpleSerializer(source='material', read_only=True)
+ class Meta:
+ model = SubProductionProgress
+ fields = '__all__'
\ No newline at end of file
diff --git a/hb_server/apps/pm/views.py b/hb_server/apps/pm/views.py
index ca26e45..62c2473 100644
--- a/hb_server/apps/pm/views.py
+++ b/hb_server/apps/pm/views.py
@@ -2,17 +2,18 @@ from rest_framework import serializers
from rest_framework.views import APIView
from apps.em.models import Equipment
from apps.em.serializers import EquipmentSerializer
-from apps.mtm.models import InputMaterial, Step, SubProduction, UsedStep
+from apps.mtm.models import InputMaterial, OutputMaterial, Step, SubProduction, UsedStep
from apps.system.mixins import CreateUpdateModelAMixin
-from apps.pm.serializers import ProductionPlanCreateFromOrderSerializer, ProductionPlanSerializer, ResourceCalListSerializer, ResourceCalSerializer, SubProductionPlanListSerializer, SubProductionPlanUpdateSerializer
+from apps.pm.serializers import GenSubPlanSerializer, ProductionPlanCreateFromOrderSerializer, ProductionPlanSerializer, ResourceCalListSerializer, ResourceCalSerializer, SubProductionPlanListSerializer, SubProductionPlanUpdateSerializer, SubProductionProgressSerializer
from rest_framework.mixins import CreateModelMixin, ListModelMixin, UpdateModelMixin
-from apps.pm.models import ProductionPlan, SubProductionPlan
+from apps.pm.models import ProductionPlan, SubProductionProgress, SubProductionPlan
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
from rest_framework.decorators import action
+from django.db.models import F
# Create your views here.
def updateOrderPlanedCount(order):
@@ -59,7 +60,7 @@ class ProductionPlanViewSet(CreateUpdateModelAMixin, ListModelMixin, CreateModel
updateOrderPlanedCount(instance.order)
return Response()
- @action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=serializers.Serializer)
+ @action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=GenSubPlanSerializer)
def gen_subplan(self, request, pk=None):
"""
生成子计划
@@ -69,9 +70,16 @@ class ProductionPlanViewSet(CreateUpdateModelAMixin, ListModelMixin, CreateModel
raise APIException('已生成子计划')
subps = SubProduction.objects.filter(product=production_plan.product).order_by('process__number')
for i in subps:
- SubProductionPlan.objects.create(production_plan=production_plan, subproduction=i,
+ steps = Step.objects.filter(usedstep__subproduction=i, usedstep__subproduction__is_deleted=False,
+ usedstep__is_deleted=False, is_deleted=False).values('id', 'number', 'name', 'usedstep__remark')
+ instance = SubProductionPlan.objects.create(production_plan=production_plan, subproduction=i,
start_date=production_plan.start_date, end_date=production_plan.end_date,
- workshop=i.process.workshop, process=i.process, create_by=request.user)
+ workshop=i.process.workshop, process=i.process, create_by=request.user,
+ steps = list(steps))
+ for m in InputMaterial.objects.filter(subproduction=i, is_deleted=False).order_by('sort'):
+ SubProductionProgress.objects.create(material=m.material, type=1, count=m.count, subproduction_plan=instance)
+ for m in OutputMaterial.objects.filter(subproduction=i, is_deleted=False).order_by('sort'):
+ SubProductionProgress.objects.create(material=m.material, type=2, count=m.count, subproduction_plan=instance)
production_plan.is_planed=True
production_plan.save()
return Response()
@@ -92,7 +100,28 @@ class SubProductionPlanViewSet(CreateUpdateModelAMixin, ListModelMixin, UpdateMo
return SubProductionPlanListSerializer
elif self.action == 'update':
return SubProductionPlanUpdateSerializer
+ return SubProductionPlanListSerializer
+ @action(methods=['get'], detail=True, perms_map={'get':'*'}, serializer_class=SubProductionProgressSerializer)
+ def progress(self, request, pk=None):
+ """
+ 生产进度详情
+ """
+ obj = self.get_object()
+ serializer = SubProductionProgressSerializer(instance=obj.progress_subplan, many=True)
+ return Response(serializer.data)
+
+ @action(methods=['post'], detail=True, perms_map={'post':'*'}, serializer_class=serializers.Serializer)
+ def issue(self, request, pk=None):
+ """
+ 下达任务
+ """
+ obj = self.get_object()
+ if obj.state == 0:
+ obj.state = 1
+ obj.save()
+ return Response()
+ raise APIException('计划状态有误')
class ResourceViewSet(GenericViewSet):
@@ -135,8 +164,8 @@ class ResourceViewSet(GenericViewSet):
for i in rdata:
rdata_l.append(i['id'])
subproductions = SubProduction.objects.filter(product__id__in=rdata_l, is_deleted=False)
- steps = Step.objects.filter(usedsteps__is_deleted=False, usedsteps__subproduction__in=subproductions)
- equips = Equipment.objects.filter(step_equips__in=steps, is_deleted=False)
+ steps = Step.objects.filter(usedstep__is_deleted=False, usedstep__subproduction__in=subproductions)
+ equips = Equipment.objects.filter(step_equips__in=steps, is_deleted=False).distinct()
serializer = EquipmentSerializer(instance=equips, many=True)
return Response(serializer.data)
diff --git a/hb_server/apps/system/serializers.py b/hb_server/apps/system/serializers.py
index a39e88b..10636ea 100644
--- a/hb_server/apps/system/serializers.py
+++ b/hb_server/apps/system/serializers.py
@@ -199,5 +199,3 @@ class UserCreateSerializer(serializers.ModelSerializer):
return phone
-class FaceLoginSerializer(serializers.Serializer):
- base64 = serializers.CharField()
\ No newline at end of file
diff --git a/hb_server/apps/system/urls.py b/hb_server/apps/system/urls.py
index 55e1420..02a4ecb 100644
--- a/hb_server/apps/system/urls.py
+++ b/hb_server/apps/system/urls.py
@@ -1,5 +1,5 @@
from django.urls import path, include
-from .views import FaceLogin, TaskList, UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet, PTaskViewSet
+from .views import TaskList, UserViewSet, OrganizationViewSet, PermissionViewSet, RoleViewSet, PositionViewSet, TestView, DictTypeViewSet, DictViewSet, PTaskViewSet
from rest_framework import routers
@@ -15,6 +15,5 @@ router.register('ptask', PTaskViewSet, basename="ptask")
urlpatterns = [
path('', include(router.urls)),
path('task/', TaskList.as_view()),
- path('test/', TestView.as_view()),
- path('facelogin/', FaceLogin.as_view())
+ path('test/', TestView.as_view())
]
diff --git a/hb_server/apps/system/views.py b/hb_server/apps/system/views.py
index 2ee96a6..65a1320 100644
--- a/hb_server/apps/system/views.py
+++ b/hb_server/apps/system/views.py
@@ -21,6 +21,7 @@ from rest_framework.views import APIView
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.exceptions import ValidationError, ParseError
+from apps.hrm.models import Employee
from utils.queryset import get_child_queryset2
from .filters import UserFilter
@@ -29,7 +30,7 @@ from .models import (Dict, DictType, File, Organization, Permission, Position,
Role, User)
from .permission import RbacPermission, get_permission_list
from .permission_data import RbacFilterSet
-from .serializers import (DictSerializer, DictTypeSerializer, FaceLoginSerializer, FileSerializer,
+from .serializers import (DictSerializer, DictTypeSerializer, FileSerializer,
OrganizationSerializer, PermissionSerializer,
PositionSerializer, RoleSerializer, PTaskSerializer,PTaskCreateUpdateSerializer,
UserCreateSerializer, UserListSerializer,
@@ -352,43 +353,3 @@ class FileViewSet(CreateModelMixin, DestroyModelMixin, RetrieveModelMixin, ListM
instance.save()
-
-#import face_recognition
-import uuid
-import base64
-import os
-
-def tran64(s):
- missing_padding = len(s) % 4
- if missing_padding != 0:
- s = s+'='* (4 - missing_padding)
- return s
-
-class FaceLogin(CreateAPIView):
- authentication_classes = []
- permission_classes = []
- serializer_class = FaceLoginSerializer
-
-
- def create(self, request, *args, **kwargs):
- """
- 人脸识别登录
- """
- # serializer = FaceLoginSerializer(data=request.data)
- # serializer.is_valid(raise_exception=True)
- filename = str(uuid.uuid4())
- filepath = settings.BASE_DIR +'/temp/' + filename +'.png'
- with open(filepath, 'wb') as f:
- data = tran64(request.data.get('base64').replace(' ', '+'))
- f.write(base64.urlsafe_b64decode(data))
- # picture_of_me = face_recognition.load_image_file(settings.BASE_DIR +'/temp/me.png')
- # my_face_encoding = face_recognition.face_encodings(picture_of_me)[0]
- # unknown_picture = face_recognition.load_image_file(filepath)
- # unknown_face_encoding = face_recognition.face_encodings(unknown_picture)[0]
- #results = face_recognition.compare_faces([my_face_encoding], unknown_face_encoding, tolerance=0.2)
- os.remove(filepath)
- # if results[0] == True:
- # return Response('这是曹前明')
- # else:
- # return Response('这不是曹前明')
-
diff --git a/hb_server/apps/wf/filters.py b/hb_server/apps/wf/filters.py
index 7e2990c..bffa820 100644
--- a/hb_server/apps/wf/filters.py
+++ b/hb_server/apps/wf/filters.py
@@ -16,7 +16,9 @@ class TicketFilterSet(filters.FilterSet):
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)
+ queryset = queryset.filter(ticketflow_ticket__participant=user).exclude(create_by=user).order_by('-update_time').distinct()
+ elif value == 'cc':
+ queryset = queryset.filter(ticketflow_ticket__participant_cc__contains=user.id).exclude(create_by=user).order_by('-update_time').distinct()
elif value == 'all':
pass
else:
diff --git a/hb_server/apps/wf/migrations/0014_wfscript.py b/hb_server/apps/wf/migrations/0014_wfscript.py
new file mode 100644
index 0000000..2b88441
--- /dev/null
+++ b/hb_server/apps/wf/migrations/0014_wfscript.py
@@ -0,0 +1,36 @@
+# Generated by Django 3.2.6 on 2021-10-19 01:58
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('wf', '0013_alter_ticketflow_transition'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='WfScript',
+ 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='删除标记')),
+ ('usage', models.IntegerField(choices=[(1, '获取处理人'), (2, '执行操作')], default=1, verbose_name='脚本用途')),
+ ('wait', models.BooleanField(default=True, verbose_name='是否等待执行完成')),
+ ('name', models.CharField(max_length=100, verbose_name='脚本名称')),
+ ('content', models.TextField(verbose_name='脚本内容')),
+ ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wfscript_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
+ ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='wfscript_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
+ ('workflow', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wf.workflow', verbose_name='关联工作流')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
diff --git a/hb_server/apps/wf/migrations/0015_auto_20211021_1049.py b/hb_server/apps/wf/migrations/0015_auto_20211021_1049.py
new file mode 100644
index 0000000..115ffc4
--- /dev/null
+++ b/hb_server/apps/wf/migrations/0015_auto_20211021_1049.py
@@ -0,0 +1,40 @@
+# Generated by Django 3.2.6 on 2021-10-21 02:49
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('system', '0003_auto_20210812_0909'),
+ ('wf', '0014_wfscript'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='state',
+ name='filter_policy',
+ field=models.IntegerField(choices=[(0, '无'), (1, '和工单同属一及上级部门'), (2, '和创建人同属一及上级部门'), (3, '和上步处理人同属一及上级部门')], default=0, verbose_name='参与人过滤策略'),
+ ),
+ migrations.AddField(
+ model_name='ticket',
+ name='belong_dept',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_belong_dept', to='system.organization', verbose_name='所属部门'),
+ ),
+ migrations.AlterField(
+ model_name='state',
+ name='participant_type',
+ field=models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段'), (9, '代码获取')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型'),
+ ),
+ migrations.AlterField(
+ model_name='ticket',
+ name='participant_type',
+ field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型'),
+ ),
+ migrations.AlterField(
+ model_name='ticketflow',
+ name='participant_type',
+ field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (5, '变量'), (6, '脚本'), (7, '工单的字段'), (8, '父工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='处理人类型'),
+ ),
+ ]
diff --git a/hb_server/apps/wf/migrations/0016_auto_20211024_2349.py b/hb_server/apps/wf/migrations/0016_auto_20211024_2349.py
new file mode 100644
index 0000000..c0fc04f
--- /dev/null
+++ b/hb_server/apps/wf/migrations/0016_auto_20211024_2349.py
@@ -0,0 +1,53 @@
+# Generated by Django 3.2.6 on 2021-10-24 15:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('wf', '0015_auto_20211021_1049'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='state',
+ name='participant_cc',
+ field=models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给'),
+ ),
+ migrations.AddField(
+ model_name='ticketflow',
+ name='participant_cc',
+ field=models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给'),
+ ),
+ migrations.AddField(
+ model_name='wfscript',
+ name='func_str',
+ field=models.CharField(blank=True, max_length=50, null=True, verbose_name='函数名'),
+ ),
+ migrations.AlterField(
+ model_name='state',
+ name='participant_type',
+ field=models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型'),
+ ),
+ migrations.AlterField(
+ model_name='ticket',
+ name='participant_type',
+ field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型'),
+ ),
+ migrations.AlterField(
+ model_name='ticketflow',
+ name='intervene_type',
+ field=models.IntegerField(choices=[(0, '正常处理'), (1, '转交'), (2, '加签'), (3, '加签处理完成'), (4, '接单'), (5, '评论'), (6, '删除'), (7, '强制关闭'), (8, '强制修改状态'), (9, 'hook操作'), (10, '撤回'), (11, '抄送')], default=0, help_text='流转类型', verbose_name='干预类型'),
+ ),
+ migrations.AlterField(
+ model_name='ticketflow',
+ name='participant_type',
+ field=models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='处理人类型'),
+ ),
+ migrations.AlterField(
+ model_name='wfscript',
+ name='usage',
+ field=models.IntegerField(choices=[(1, '获取处理人'), (2, '执行操作')], default=2, verbose_name='脚本用途'),
+ ),
+ ]
diff --git a/hb_server/apps/wf/models.py b/hb_server/apps/wf/models.py
index 4540eb0..4bc7d75 100644
--- a/hb_server/apps/wf/models.py
+++ b/hb_server/apps/wf/models.py
@@ -1,3 +1,4 @@
+from random import choice
from django.db import models
from django.db.models.base import Model
import django.utils.timezone as timezone
@@ -39,16 +40,18 @@ class State(CommonAModel):
PARTICIPANT_TYPE_ROBOT = 6
PARTICIPANT_TYPE_FIELD = 7
PARTICIPANT_TYPE_PARENT_FIELD = 8
+ PARTICIPANT_TYPE_FORMCODE = 9
state_participanttype_choices = (
(0, '无处理人'),
(PARTICIPANT_TYPE_PERSONAL, '个人'),
(PARTICIPANT_TYPE_MULTI, '多人'),
- (PARTICIPANT_TYPE_DEPT, '部门'),
+ # (PARTICIPANT_TYPE_DEPT, '部门'),
(PARTICIPANT_TYPE_ROLE, '角色'),
- (PARTICIPANT_TYPE_VARIABLE, '变量'),
+ # (PARTICIPANT_TYPE_VARIABLE, '变量'),
(PARTICIPANT_TYPE_ROBOT, '脚本'),
(PARTICIPANT_TYPE_FIELD, '工单的字段'),
- (PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段')
+ # (PARTICIPANT_TYPE_PARENT_FIELD, '父工单的字段'),
+ (PARTICIPANT_TYPE_FORMCODE, '代码获取')
)
STATE_DISTRIBUTE_TYPE_ACTIVE = 1 # 主动接单
STATE_DISTRIBUTE_TYPE_DIRECT = 2 # 直接处理(当前为多人的情况,都可以处理,而不需要先接单)
@@ -64,6 +67,13 @@ class State(CommonAModel):
STATE_FIELD_READONLY= 1 # 字段只读
STATE_FIELD_REQUIRED = 2 # 字段必填
STATE_FIELD_OPTIONAL = 3 # 字段可选
+
+ state_filter_choices=(
+ (0, '无'),
+ (1, '和工单同属一及上级部门'),
+ (2, '和创建人同属一及上级部门'),
+ (3, '和上步处理人同属一及上级部门'),
+ )
name = models.CharField('名称', max_length=50)
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流')
is_hidden = models.BooleanField('是否隐藏', default=False, help_text='设置为True时,获取工单步骤api中不显示此状态(当前处于此状态时除外)')
@@ -74,6 +84,8 @@ class State(CommonAModel):
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:可选. 示例:{"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.全部处理(要求所有参与人都要处理一遍,才能进入下一步)')
+ filter_policy = models.IntegerField('参与人过滤策略', default=0, choices=state_filter_choices)
+ participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
class Transition(CommonAModel):
"""
@@ -97,6 +109,7 @@ class Transition(CommonAModel):
TRANSITION_INTERVENE_TYPE_ALTER_STATE = 8 # 强制修改状态操作
TRANSITION_INTERVENE_TYPE_HOOK = 9 # hook操作
TRANSITION_INTERVENE_TYPE_RETREAT = 10 # 撤回
+ TRANSITION_INTERVENE_TYPE_CC = 11 # 抄送
intervene_type_choices = (
(0, '正常处理'),
@@ -109,7 +122,8 @@ class Transition(CommonAModel):
(TRANSITION_INTERVENE_TYPE_CLOSE, '强制关闭'),
(TRANSITION_INTERVENE_TYPE_ALTER_STATE, '强制修改状态'),
(TRANSITION_INTERVENE_TYPE_HOOK, 'hook操作'),
- (TRANSITION_INTERVENE_TYPE_RETREAT, '撤回')
+ (TRANSITION_INTERVENE_TYPE_RETREAT, '撤回'),
+ (TRANSITION_INTERVENE_TYPE_CC, '抄送')
)
name = models.CharField('操作', max_length=50)
@@ -155,7 +169,7 @@ class CustomField(CommonAModel):
help_text='radio,checkbox,select,multiselect类型可供选择的选项,格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号')
label = models.JSONField('标签', blank=True, default=dict, help_text='自定义标签,json格式,调用方可根据标签自行处理特殊场景逻辑,loonflow只保存文本内容')
-class Ticket(CommonAModel):
+class Ticket(CommonBModel):
"""
工单
"""
@@ -177,9 +191,9 @@ class Ticket(CommonAModel):
category_choices =(
('all', '全部'),
('owner', '我创建的'),
- ('duty', '代办'),
+ ('duty', '待办'),
('worked', '我处理的'),
- ('relation', '抄送我的')
+ ('cc', '抄送我的')
)
title = models.CharField('标题', max_length=500, blank=True, default='', help_text="工单标题")
workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='关联工作流')
@@ -207,4 +221,21 @@ class TicketFlow(BaseModel):
participant = models.ForeignKey(User, verbose_name='处理人', on_delete=models.SET_NULL, null=True, blank=True, related_name='ticketflow_participant')
state = models.ForeignKey(State, verbose_name='当前状态', default=0, blank=True, on_delete=models.CASCADE)
ticket_data = models.JSONField('工单数据', default=dict, blank=True, help_text='可以用于记录当前表单数据,json格式')
- intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices)
\ No newline at end of file
+ intervene_type = models.IntegerField('干预类型', default=0, help_text='流转类型', choices=Transition.intervene_type_choices)
+ participant_cc = models.JSONField('抄送给', default=list, blank=True, help_text='抄送给(userid列表)')
+
+class WfScript(CommonAModel):
+ """
+ 执行脚本
+ """
+ usage_choices =(
+ (1, '获取处理人'),
+ (2, '执行操作'),
+ )
+ usage = models.IntegerField('脚本用途', default=2, choices=usage_choices)
+ wait = models.BooleanField('是否等待执行完成', default=True)
+ name = models.CharField('脚本名称', max_length=100)
+ workflow = models.ForeignKey(Workflow, verbose_name='关联工作流', null=True, blank=True, on_delete=models.SET_NULL)
+ func_str = models.CharField('函数名', max_length=50, null=True, blank=True)
+ content = models.TextField('脚本内容')
+
diff --git a/hb_server/apps/wf/scripts.py b/hb_server/apps/wf/scripts.py
new file mode 100644
index 0000000..5e818f7
--- /dev/null
+++ b/hb_server/apps/wf/scripts.py
@@ -0,0 +1,14 @@
+class GetParticipants:
+
+ all_funcs = [
+ {'func':'get_create_by', 'name':'获取工单创建人'}
+ ]
+
+ # def all_funcs(self):
+ # # return list(filter(lambda x: x.startswith('get_') and callable(getattr(self, x)), dir(self)))
+ # return [(func, getattr(self, func).__doc__) for func in dir(self) if callable(getattr(self, func)) and func.startswith('get_')]
+
+ def get_create_by(self, state:dict={}, ticket:dict={}, ticket_data:dict={}, request={}):
+ """工单创建人"""
+ participant = ticket.create_by.id
+ return participant
\ No newline at end of file
diff --git a/hb_server/apps/wf/serializers.py b/hb_server/apps/wf/serializers.py
index b9256bd..45369a5 100644
--- a/hb_server/apps/wf/serializers.py
+++ b/hb_server/apps/wf/serializers.py
@@ -136,7 +136,10 @@ class TicketCloseSerializer(serializers.Serializer):
class TicketAddNodeSerializer(serializers.Serializer):
suggestion = serializers.CharField(label="加签说明", required = False)
- add_node_man = serializers.IntegerField(label='加签人')
+ toadd_user = serializers.IntegerField(label='发送给谁去加签')
class TicketAddNodeEndSerializer(serializers.Serializer):
- suggestion = serializers.CharField(label="加签意见", required = False)
\ No newline at end of file
+ suggestion = serializers.CharField(label="加签意见", required = False)
+
+class TicketDestorySerializer(serializers.Serializer):
+ ids = serializers.ListField(child=serializers.IntegerField(), label='工单ID列表')
\ No newline at end of file
diff --git a/hb_server/apps/wf/services.py b/hb_server/apps/wf/services.py
index a6ec576..f45c1b5 100644
--- a/hb_server/apps/wf/services.py
+++ b/hb_server/apps/wf/services.py
@@ -7,6 +7,9 @@ from rest_framework.exceptions import APIException
from django.utils import timezone
from datetime import timedelta
import random
+from .scripts import GetParticipants
+from utils.queryset import get_parent_queryset
+
class WfService(object):
@staticmethod
def get_worlflow_states(workflow:Workflow):
@@ -103,7 +106,7 @@ class WfService(object):
@classmethod
- def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, ticket_data:dict={})->object:
+ def get_next_state_by_transition_and_ticket_info(cls, ticket:Ticket, transition: Transition, ticket_data:dict={}, request:dict={})->object:
"""
获取下个节点状态
"""
@@ -111,16 +114,19 @@ class WfService(object):
destination_state = transition.destination_state
ticket_all_value = cls.get_ticket_all_field_value(ticket)
ticket_all_value.update(**ticket_data)
+ for key, value in ticket_all_value.items():
+ if isinstance(ticket_all_value[key], str):
+ ticket_all_value[key] = "'" + ticket_all_value[key] + "'"
if transition.condition_expression:
for i in transition.condition_expression:
expression = i['expression'].format(**ticket_all_value)
import datetime, time # 用于支持条件表达式中对时间的操作
- if eval(expression):
+ if eval(expression, {"__builtins__":None}, {'datetime':datetime, 'time':time}):
destination_state = State.objects.get(pk=i['target_state'])
return destination_state
@classmethod
- def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, ticket_data:dict={}):
+ def get_ticket_state_participant_info(cls, state:State, ticket:Ticket, ticket_data:dict={}, request={}):
"""
获取工单目标状态实际的处理人, 处理人类型
"""
@@ -141,17 +147,34 @@ class WfService(object):
multi_all_person_dict = {}
destination_participant_type, destination_participant = state.participant_type, state.participant
if destination_participant_type == State.PARTICIPANT_TYPE_FIELD:
- destination_participant = ticket_data.get(destination_participant, None) if destination_participant in ticket_data else Ticket.ticket_data.get(destination_participant, None)
+ destination_participant = ticket_data.get(destination_participant, 0) if destination_participant in ticket_data \
+ else Ticket.ticket_data.get(destination_participant, 0)
- elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#单部门
- destination_participant = list(User.objects.filter(dept=destination_participant).values_list('id', flat=True))
+ elif destination_participant_type == State.PARTICIPANT_TYPE_FORMCODE:#代码获取
+ destination_participant = getattr(GetParticipants, destination_participant)(state=state, ticket=ticket, ticket_data=ticket_data, request=request)
- elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#单角色
- destination_participant = list(User.objects.filter(roles=destination_participant).values_list('id', flat=True))
-
+ elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT:#部门
+ destination_participant = list(User.objects.filter(dept__in=destination_participant).values_list('id', flat=True))
+
+ elif destination_participant_type == State.PARTICIPANT_TYPE_ROLE:#角色
+ user_queryset = User.objects.filter(roles__in=destination_participant)
+ # 如果选择了角色, 需要走过滤策略
+ if ticket.filter_policy == 1:
+ depts = get_parent_queryset(ticket.belong_dept)
+ user_queryset = user_queryset.filter(dept__in=depts)
+ elif ticket.filter_policy == 2:
+ depts = get_parent_queryset(ticket.create_by.dept)
+ user_queryset = user_queryset.filter(dept__in=depts)
+ elif ticket.filter_policy == 3:
+ depts = get_parent_queryset(request.user.dept)
+ user_queryset = user_queryset.filter(dept__in=depts)
+ destination_participant = list(user_queryset.values_list('id', flat=True))
if type(destination_participant) == list:
destination_participant_type = State.PARTICIPANT_TYPE_MULTI
destination_participant = list(set(destination_participant))
+ if len(destination_participant) == 1: # 如果只有一个人
+ destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
+ destination_participant = destination_participant[0]
else:
destination_participant_type = State.PARTICIPANT_TYPE_PERSONAL
if destination_participant_type == State.PARTICIPANT_TYPE_MULTI:
diff --git a/hb_server/apps/wf/urls.py b/hb_server/apps/wf/urls.py
index b03a965..5c9ae0d 100644
--- a/hb_server/apps/wf/urls.py
+++ b/hb_server/apps/wf/urls.py
@@ -1,6 +1,6 @@
from django.db.models import base
from rest_framework import urlpatterns
-from apps.wf.views import CustomFieldViewSet, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
+from apps.wf.views import CustomFieldViewSet, FromCodeListView, StateViewSet, TicketFlowViewSet, TicketViewSet, TransitionViewSet, WorkflowViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
@@ -12,6 +12,7 @@ router.register('customfield', CustomFieldViewSet, basename='wf_customfield')
router.register('ticket', TicketViewSet, basename='wf_ticket')
router.register('ticketflow', TicketFlowViewSet, basename='wf_ticketflow')
urlpatterns = [
+ path('participant_from_code', FromCodeListView.as_view()),
path('', include(router.urls)),
]
diff --git a/hb_server/apps/wf/views.py b/hb_server/apps/wf/views.py
index ddc9bc2..e97cbb4 100644
--- a/hb_server/apps/wf/views.py
+++ b/hb_server/apps/wf/views.py
@@ -1,10 +1,12 @@
+from django.db.models import query
+from rest_framework.views import APIView
from apps.system.models import User
from apps.wf.filters import TicketFilterSet
from django.core.exceptions import AppRegistryNotReady
from rest_framework.response import Response
from rest_framework import serializers
from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
-from apps.wf.serializers import CustomFieldSerializer, StateSerializer, TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, TicketCreateSerializer, TicketFlowSerializer, TicketFlowSimpleSerializer, TicketHandleSerializer, TicketRetreatSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer, TicketListSerializer, TicketDetailSerializer
+from apps.wf.serializers import CustomFieldSerializer, StateSerializer, TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, TicketCreateSerializer, TicketDestorySerializer, TicketFlowSerializer, TicketFlowSimpleSerializer, TicketHandleSerializer, TicketRetreatSerializer, TicketSerializer, TransitionSerializer, WorkflowSerializer, TicketListSerializer, TicketDetailSerializer
from django.shortcuts import get_object_or_404, render
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from rest_framework.decorators import action, api_view
@@ -13,8 +15,18 @@ from apps.system.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin,
from apps.wf.services import WfService
from rest_framework.exceptions import APIException, PermissionDenied
from rest_framework import status
+from django.db.models import Count
+from .scripts import GetParticipants
+
# Create your views here.
+class FromCodeListView(APIView):
+ def get(self, request, format=None):
+ """
+ 获取处理人代码列表
+ """
+ return Response(GetParticipants.all_funcs)
+
class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet):
perms_map = {'get': '*', 'post': 'workflow_create',
'put': 'workflow_update', 'delete': 'workflow_delete'}
@@ -132,11 +144,12 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
if value == State.STATE_FIELD_REQUIRED:
if key not in ticket_data or not ticket_data[key]:
raise APIException('字段{}必填'.format(key))
- ticket = serializer.save(state=start_state, create_by=request.user, act_state=Ticket.TICKET_ACT_STATE_DRAFT) # 先创建出来
+ ticket = serializer.save(state=start_state, create_by=request.user, act_state=Ticket.TICKET_ACT_STATE_DRAFT, belong_dept=request.user.dept) # 先创建出来
+
next_state = WfService.get_next_state_by_transition_and_ticket_info(ticket=ticket, transition=transition)
participant_info = WfService.get_ticket_state_participant_info(state=next_state, ticket=ticket, ticket_data=ticket.ticket_data)
destination_participant_type = participant_info.get('destination_participant_type', 0)
- destination_participant = participant_info.get('destination_participant', None)
+ destination_participant = participant_info.get('destination_participant', 0)
multi_all_person = participant_info.get('multi_all_person', {}) # 多人需要全部处理情况
sn = WfService.get_ticket_sn(ticket.workflow) # 流水号
if next_state.type == State.STATE_TYPE_END:
@@ -163,8 +176,29 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
TicketFlow.objects.create(ticket=ticket, state=start_state, ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=rdata.get('suggestion',''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
participant=ticket.create_by, transition=transition)
+ # 开始状态需要抄送
+ if start_state.participant_cc:
+ TicketFlow.objects.create(ticket=ticket, state=ticket.start_state,
+ participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
+ participant=None, participant_cc=start_state.participant_cc)
+ # 目标状态需要抄送
+ if next_state.participant_cc:
+ TicketFlow.objects.create(ticket=ticket, state=ticket.next_state,
+ participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
+ participant=None, participant_cc=next_state.participant_cc)
return Response(TicketSerializer(instance=ticket).data)
+ @action(methods=['get'], detail=False, perms_map={'get':'*'})
+ def duty_agg(self, request, pk=None):
+ """
+ 工单待办聚合
+ """
+ ret = {}
+ queryset = Ticket.objects.filter(participant__contains=request.user.id, is_deleted=False)\
+ .exclude(act_state__in=[Ticket.TICKET_ACT_STATE_FINISH, Ticket.TICKET_ACT_STATE_CLOSED])
+ ret['total_count'] = queryset.count()
+ ret['details'] = list(queryset.values('workflow', 'workflow__name').annotate(count = Count('workflow')))
+ return Response(ret)
@action(methods=['post'], detail=True, perms_map={'post':'*'})
def handle(self, request, pk=None):
@@ -189,7 +223,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
if value == State.STATE_FIELD_REQUIRED:
if key not in ticket_data or not ticket_data[key]:
raise APIException('字段{}必填'.format(key))
- destination_state = WfService.get_next_state_by_transition_and_ticket_info(ticket, transition, ticket_data)
+ destination_state = WfService.get_next_state_by_transition_and_ticket_info(ticket, transition, ticket_data, request)
multi_all_person = ticket.multi_all_person
if multi_all_person:
multi_all_person[request.user.id] =dict(transition=transition.id)
@@ -197,7 +231,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
if WfService.check_dict_has_all_same_value(multi_all_person):
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, data['ticket_data'])
destination_participant_type = participant_info.get('destination_participant_type', 0)
- destination_participant = participant_info.get('destination_participant', None)
+ destination_participant = participant_info.get('destination_participant', 0)
multi_all_person = {}
else:
# 处理人没有没有全部处理完成或者处理动作不一致
@@ -206,12 +240,12 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
destination_participant = []
for key, value in multi_all_person.items():
if not value:
- destination_participant.push(key)
+ destination_participant.append(key)
else:
# 当前处理人类型非全部处理
participant_info = WfService.get_ticket_state_participant_info(destination_state, ticket, data['ticket_data'])
destination_participant_type = participant_info.get('destination_participant_type', 0)
- destination_participant = participant_info.get('destination_participant', None)
+ destination_participant = participant_info.get('destination_participant', 0)
multi_all_person = participant_info.get('multi_all_person', {})
# 更新工单信息:基础字段及自定义字段, add_relation字段 需要下个处理人是部门、角色等的情况
@@ -240,6 +274,11 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
TicketFlow.objects.create(ticket=ticket, state=source_state, ticket_data=WfService.get_ticket_all_field_value(ticket),
suggestion=data.get('suggestion',''), participant_type=State.PARTICIPANT_TYPE_PERSONAL,
participant=request.user, transition=transition)
+ # 目标状态需要抄送
+ if destination_state.participant_cc:
+ TicketFlow.objects.create(ticket=ticket, state=ticket.destination_state,
+ participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC,
+ participant=None, participant_cc=destination_state.participant_cc)
return Response(TicketSerializer(instance=ticket).data)
@@ -321,7 +360,7 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
"""
ticket = self.get_object()
data = request.data
- add_user = User.objects.get(pk=data['add_node_man'])
+ add_user = User.objects.get(pk=data['toadd_user'])
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.participant = add_user.id
ticket.in_add_node = True
@@ -342,8 +381,8 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
ticket = self.get_object()
ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL
ticket.in_add_node = False
- ticket.add_node_man = None
ticket.participant = ticket.add_node_man.id
+ ticket.add_node_man = None
ticket.save()
# 更新流转记录
suggestion = request.data.get('suggestion', '') # 加签意见
@@ -375,6 +414,15 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin
else:
return Response('工单不可关闭', status=status.HTTP_400_BAD_REQUEST)
+ @action(methods=['post'], detail=False, perms_map={'post':'*'}, serializer_class=TicketDestorySerializer)
+ def destory(self, request, pk=None):
+ """
+ 批量物理删除
+ """
+ Ticket.objects.filter(id__in=request.data.get('ids', [])).delete(soft=False)
+ return Response()
+
+
class TicketFlowViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
"""
diff --git a/hb_server/apps/wpm/__init__.py b/hb_server/apps/wpm/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hb_server/apps/wpm/admin.py b/hb_server/apps/wpm/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/hb_server/apps/wpm/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/hb_server/apps/wpm/apps.py b/hb_server/apps/wpm/apps.py
new file mode 100644
index 0000000..7c2337e
--- /dev/null
+++ b/hb_server/apps/wpm/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+
+class WpmConfig(AppConfig):
+ name = 'apps.wpm'
+ verbose_name = '车间生产'
+
+
diff --git a/hb_server/apps/wpm/models.py b/hb_server/apps/wpm/models.py
new file mode 100644
index 0000000..3e34f34
--- /dev/null
+++ b/hb_server/apps/wpm/models.py
@@ -0,0 +1,55 @@
+from django.db import models
+from django.db.models.base import Model
+import django.utils.timezone as timezone
+from django.db.models.query import QuerySet
+from apps.system.models import CommonAModel, CommonBModel, Organization, User, Dict, File
+from utils.model import SoftModel, BaseModel
+from simple_history.models import HistoricalRecords
+from apps.mtm.models import Material, Step, RecordForm
+
+class Product(CommonAModel):
+ """
+ 产品(所有生产过程中出现过的)
+ """
+ act_state_choices=(
+ (0, '待执行'),
+ (1, '进行中'),
+ (2, '已完成')
+ )
+ number = models.CharField('物品编号', primary_key=True, null=True, blank=True, max_length=50)
+ m_state = models.ForeignKey(Material, verbose_name='所属物料状态', on_delete=models.CASCADE)
+ p_state = models.ForeignKey(Step, verbose_name='所在步骤', on_delete=models.CASCADE, null=True, blank=True)
+ act_state = models.IntegerField('进行状态', default=0)
+ remark = models.CharField('备注', max_length=200, null=True, blank=True)
+
+class ProductForm(CommonAModel):
+ """
+ 记录表格
+ """
+ record_form = models.ForeignKey(RecordForm, verbose_name='所用表格', on_delete=models.CASCADE)
+ data = models.JSONField('记录的数据', default=dict, blank=True)
+
+
+class ProductFlow(BaseModel):
+ """
+ 产品流转日志
+ """
+ product = models.ForeignKey(Product, verbose_name='产品', on_delete=models.CASCADE)
+
+
+class Vendor(CommonAModel):
+ """
+ 供应商信息
+ """
+ name = models.CharField('供应商名称', max_length=50, unique=True)
+ contact = models.CharField('联系人', max_length=20)
+ contact_phone = models.CharField('联系电话', max_length=11, unique=True)
+ address = models.CharField('地址', max_length=200, null=True, blank=True)
+ description = models.CharField('描述', max_length=200, blank=True, null=True)
+ material = models.CharField('供应的物料', max_length=200, blank=True, null=True)
+ class Meta:
+ verbose_name = '供应商信息'
+ verbose_name_plural = verbose_name
+
+ def __str__(self):
+ return self.name
\ No newline at end of file
diff --git a/hb_server/apps/wpm/serializers.py b/hb_server/apps/wpm/serializers.py
new file mode 100644
index 0000000..037f7ef
--- /dev/null
+++ b/hb_server/apps/wpm/serializers.py
@@ -0,0 +1,9 @@
+from rest_framework.serializers import ModelSerializer
+
+from .models import Vendor
+
+
+class VendorSerializer(ModelSerializer):
+ class Meta:
+ model = Vendor
+ fields = '__all__'
diff --git a/hb_server/apps/wpm/tests.py b/hb_server/apps/wpm/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/hb_server/apps/wpm/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/hb_server/apps/wpm/urls.py b/hb_server/apps/wpm/urls.py
new file mode 100644
index 0000000..a81ad24
--- /dev/null
+++ b/hb_server/apps/wpm/urls.py
@@ -0,0 +1,12 @@
+from django.db.models import base
+from rest_framework import urlpatterns
+from apps.pum.views import VendorViewSet
+from django.urls import path, include
+from rest_framework.routers import DefaultRouter
+
+router = DefaultRouter()
+router.register('vendor', VendorViewSet, basename='vendor')
+urlpatterns = [
+ path('', include(router.urls)),
+]
+
diff --git a/hb_server/apps/wpm/views.py b/hb_server/apps/wpm/views.py
new file mode 100644
index 0000000..bb46fb1
--- /dev/null
+++ b/hb_server/apps/wpm/views.py
@@ -0,0 +1,23 @@
+from django.shortcuts import render
+from rest_framework.viewsets import ModelViewSet
+
+from apps.pum.models import Vendor
+from apps.pum.serializers import VendorSerializer
+from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin
+
+
+
+
+# Create your views here.
+class VendorViewSet(CreateUpdateModelAMixin, ModelViewSet):
+ """
+ 供应商-增删改查
+ """
+ perms_map = {'get': '*', 'post': 'vendor_create',
+ 'put': 'vendor_update', 'delete': 'vendor_delete'}
+ queryset = Vendor.objects.all()
+ serializer_class = VendorSerializer
+ search_fields = ['name', 'contact']
+ filterset_fields = []
+ ordering_fields = ['create_time']
+ ordering = ['-create_time']
diff --git a/hb_server/requirements.txt b/hb_server/requirements.txt
index 595fea2..a223cbc 100644
--- a/hb_server/requirements.txt
+++ b/hb_server/requirements.txt
@@ -10,3 +10,4 @@ drf-yasg==1.20.0
psutil==5.8.0
pillow==8.3.1
opencv-python==4.5.3.56
+django-celery-results==2.2.0
diff --git a/hb_server/server/settings.py b/hb_server/server/settings.py
index b2e7755..7d1174e 100644
--- a/hb_server/server/settings.py
+++ b/hb_server/server/settings.py
@@ -41,6 +41,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'corsheaders',
'django_celery_beat',
+ 'django_celery_results',
'drf_yasg',
'rest_framework',
"django_filters",
@@ -55,7 +56,8 @@ INSTALLED_APPS = [
'apps.inm',
'apps.sam',
'apps.qm',
- 'apps.pm'
+ 'apps.pm',
+ # 'apps.wpm'
]
MIDDLEWARE = [
@@ -202,6 +204,7 @@ CELERY_BROKER_URL = "redis://redis:6379/0" # 任务存储
CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行300个任务就会被销毁,可防止内存泄露
CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区
CELERY_ENABLE_UTC = True # 启动时区设置
+CELERY_RESULT_BACKEND = 'django-db'
# swagger配置
SWAGGER_SETTINGS = {
diff --git a/hb_server/utils/queryset.py b/hb_server/utils/queryset.py
index 80c1d3d..73a6bb1 100644
--- a/hb_server/utils/queryset.py
+++ b/hb_server/utils/queryset.py
@@ -57,4 +57,14 @@ def get_child_queryset2(obj, hasParent=True):
while child_queryset:
queryset = queryset | child_queryset
child_queryset = cls.objects.filter(parent__in=child_queryset)
- return queryset
\ No newline at end of file
+ return queryset
+
+def get_parent_queryset(obj, hasSelf=True):
+ cls = type(obj)
+ ids = []
+ if hasSelf:
+ ids.append(obj.id)
+ while obj.parent:
+ obj = obj.parent
+ ids.append(obj.id)
+ return cls.objects.filter(id__in=ids)
\ No newline at end of file