From 32459d8710977eceee057f2510187b518ebc5e1b Mon Sep 17 00:00:00 2001 From: caoqianming Date: Wed, 1 Sep 2021 14:47:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=AE=B0=E5=BD=95=E8=A1=A8?= =?UTF-8?q?=E6=A0=BC=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hb_server/apps/mtm/models.py | 32 +++++++++---- hb_server/apps/mtm/serializers.py | 52 ++++++++++++++++++++- hb_server/apps/mtm/urls.py | 4 +- hb_server/apps/mtm/views.py | 41 ++++++++++++++-- hb_server/apps/wf/services.py | 35 ++++++++++++-- hb_server/apps/wf/views.py | 77 ++++++++++++++++++++++++++++--- 6 files changed, 216 insertions(+), 25 deletions(-) diff --git a/hb_server/apps/mtm/models.py b/hb_server/apps/mtm/models.py index 9223d29..7896ac0 100644 --- a/hb_server/apps/mtm/models.py +++ b/hb_server/apps/mtm/models.py @@ -70,15 +70,27 @@ class Step(CommonAModel): def __str__(self): return self.name -# class StepTable(CommonAModel): -# """ -# 过程记录表格 -# """ - - -class StepOperationItem(CommonAModel): +class RecordForm(CommonAModel): """ - 操作记录条目 + 记录表格 + """ + type_choices=( + (1, '生产记录'), + ) + name = models.CharField('表格名称', max_length=100, unique=True) + type = models.IntegerField('表格类型', choices=type_choices, default=1) + step = models.ForeignKey(Step, verbose_name='关联子工序', on_delete=models.CASCADE) + + class Meta: + verbose_name = '记录表格' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + +class RecordFormField(CommonAModel): + """ + 记录字段表 """ field_type_choices = ( ('string', '字符串'), @@ -93,7 +105,7 @@ class StepOperationItem(CommonAModel): ('selects', '多选下拉'), ('textarea', '文本域'), ) - step = models.ForeignKey(Step, on_delete=models.CASCADE, verbose_name='关联步骤') + 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='字段类型请尽量特殊,避免与系统中关键字冲突') field_name = models.CharField('字段名称', max_length=50) @@ -103,7 +115,7 @@ class StepOperationItem(CommonAModel): help_text='radio,checkbox,select,multiselect类型可供选择的选项,格式为json如:{"1":"中国", "2":"美国"},注意数字也需要引号') sort = models.IntegerField('排序号', default=1) class Meta: - verbose_name = '操作记录条目' + verbose_name = '记录表格字段' verbose_name_plural = verbose_name def __str__(self): diff --git a/hb_server/apps/mtm/serializers.py b/hb_server/apps/mtm/serializers.py index 3bf650f..8103340 100644 --- a/hb_server/apps/mtm/serializers.py +++ b/hb_server/apps/mtm/serializers.py @@ -1,7 +1,7 @@ from apps.em.serializers import EquipmentSerializer, EquipmentSimpleSerializer from rest_framework import serializers from rest_framework.exceptions import ValidationError -from .models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, Step, UsedStep +from .models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, RecordForm, RecordFormField, Step, UsedStep from apps.system.serializers import FileSimpleSerializer, OrganizationSimpleSerializer @@ -137,3 +137,53 @@ class UsedStepListSerializer(serializers.ModelSerializer): """ Perform necessary eager loading of data. """ queryset = queryset.select_related('step') return queryset + +class RecordFormSerializer(serializers.ModelSerializer): + step_ = StepSimpleSerializer(source='step', read_only=True) + """ + 记录表格序列化 + """ + class Meta: + model = RecordForm + fields = '__all__' + + @staticmethod + def setup_eager_loading(queryset): + """ Perform necessary eager loading of data. """ + queryset = queryset.select_related('step') + return queryset + +class RecordFormCreateSerializer(serializers.ModelSerializer): + class Meta: + model = RecordForm + fields = ['name', 'type', 'step'] + +class RecordFormUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = RecordForm + fields = ['name', 'type'] + +class RecordFormFieldSerializer(serializers.ModelSerializer): + class Meta: + model = RecordForm + fields = '__all__' + +class RecordFormFieldCreateSerializer(serializers.ModelSerializer): + class Meta: + model = RecordFormField + fields = ['form', 'field_type', 'field_key', 'field_name', 'boolean_field_display', 'field_choice', 'sort'] + + def validate(self, data): + if RecordFormField.objects.filter(field_key=data['field_key'], form=data['form'], is_deleted=False).exists(): + raise ValidationError('字段key已存在!') + return data + +class RecordFormFieldUpdateSerializer(serializers.ModelSerializer): + class Meta: + model = RecordFormField + fields = ['field_type', 'field_name', 'boolean_field_display', 'field_choice', 'sort'] + +class RecordFormFieldSimpleSerializer(serializers.ModelSerializer): + class Meta: + model = RecordFormField + fields = '__all__' diff --git a/hb_server/apps/mtm/urls.py b/hb_server/apps/mtm/urls.py index 8ac7cf2..db52c29 100644 --- a/hb_server/apps/mtm/urls.py +++ b/hb_server/apps/mtm/urls.py @@ -1,6 +1,6 @@ from django.db.models import base from rest_framework import urlpatterns -from apps.mtm.views import InputMaterialViewSet, MaterialViewSet, OutputMaterialViewSet, ProcessViewSet, StepViewSet, UsedStepViewSet +from apps.mtm.views import InputMaterialViewSet, MaterialViewSet, OutputMaterialViewSet, ProcessViewSet, RecordFormFieldViewSet, RecordFormViewSet, StepViewSet, UsedStepViewSet from django.urls import path, include from rest_framework.routers import DefaultRouter @@ -12,6 +12,8 @@ router.register('step', StepViewSet, basename='step') router.register('inputmaterial', InputMaterialViewSet, basename='inputmaterial') router.register('outputmaterial', OutputMaterialViewSet, basename='outputmaterial') router.register('usedstep', UsedStepViewSet, basename='usedstep') +router.register('recordform', RecordFormViewSet, basename='recordform') +router.register('recordform-field', RecordFormFieldViewSet, basename='recordform-field') urlpatterns = [ path('', include(router.urls)), ] diff --git a/hb_server/apps/mtm/views.py b/hb_server/apps/mtm/views.py index 0c61d43..11c3927 100644 --- a/hb_server/apps/mtm/views.py +++ b/hb_server/apps/mtm/views.py @@ -2,8 +2,8 @@ from django.shortcuts import render from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework.mixins import CreateModelMixin, ListModelMixin, UpdateModelMixin, RetrieveModelMixin, DestroyModelMixin -from apps.mtm.models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, Step, UsedStep -from apps.mtm.serializers import InputMaterialListSerializer, InputMaterialSerializer, InputMaterialUpdateSerializer, MaterialDetailSerializer, MaterialSerializer, MaterialSimpleSerializer, OutputMaterialListSerializer, OutputMaterialSerializer, OutputMaterialUpdateSerializer, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, StepDetailSerializer, StepSerializer, UsedStepCreateSerializer, UsedStepListSerializer +from apps.mtm.models import InputMaterial, Material, OutputMaterial, Process, ProductProcess, RecordForm, RecordFormField, Step, UsedStep +from apps.mtm.serializers import InputMaterialListSerializer, InputMaterialSerializer, InputMaterialUpdateSerializer, MaterialDetailSerializer, MaterialSerializer, MaterialSimpleSerializer, OutputMaterialListSerializer, OutputMaterialSerializer, OutputMaterialUpdateSerializer, ProductProcessListSerializer, ProductProcessUpdateSerializer, ProcessSerializer, RecordFormCreateSerializer, RecordFormFieldCreateSerializer, RecordFormFieldSerializer, RecordFormFieldUpdateSerializer, RecordFormSerializer, RecordFormUpdateSerializer, StepDetailSerializer, StepSerializer, UsedStepCreateSerializer, UsedStepListSerializer from apps.system.mixins import CreateUpdateModelAMixin, OptimizationMixin from rest_framework.decorators import action from rest_framework.response import Response @@ -127,7 +127,7 @@ class OutputMaterialViewSet(CreateUpdateModelAMixin, ModelViewSet): return OutputMaterialUpdateSerializer return OutputMaterialSerializer -class UsedStepViewSet(OptimizationMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet): +class UsedStepViewSet(OptimizationMixin, CreateUpdateModelAMixin, CreateModelMixin, DestroyModelMixin, ListModelMixin, GenericViewSet): """ 产品生产子工序表 """ @@ -139,4 +139,37 @@ class UsedStepViewSet(OptimizationMixin, CreateModelMixin, DestroyModelMixin, Li def get_serializer_class(self): if self.action =='create': return UsedStepCreateSerializer - return UsedStepListSerializer \ No newline at end of file + return UsedStepListSerializer + +class RecordFormViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): + """ + 记录表格增删改查 + """ + perms_map = {'*':'*'} + queryset = RecordForm.objects.all() + filterset_fields = ['step', 'type'] + search_fields = ['name'] + + def get_serializer_class(self): + if self.action =='create': + return RecordFormCreateSerializer + elif self.action == 'update': + return RecordFormUpdateSerializer + return RecordFormSerializer + + +class RecordFormFieldViewSet(OptimizationMixin, CreateUpdateModelAMixin, ModelViewSet): + """ + 表格字段表 增删改查 + """ + perms_map = {'*':'*'} + queryset = RecordFormField.objects.all() + filterset_fields = ['field_type', 'form'] + search_fields = ['field_name', 'field_key'] + + def get_serializer_class(self): + if self.action =='create': + return RecordFormFieldCreateSerializer + elif self.action == 'update': + return RecordFormFieldUpdateSerializer + return RecordFormFieldSerializer \ No newline at end of file diff --git a/hb_server/apps/wf/services.py b/hb_server/apps/wf/services.py index 2337b95..d17de32 100644 --- a/hb_server/apps/wf/services.py +++ b/hb_server/apps/wf/services.py @@ -89,7 +89,7 @@ class WfService(object): @classmethod - def get_next_state_id_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)->object: """ 获取下个节点状态 """ @@ -172,8 +172,37 @@ class WfService(object): if ticket.in_add_node: return dict(permission=False, msg="工单当前处于加签中,请加签完成后操作") return dict(permission=True, msg="") + + @classmethod + def check_dict_has_all_same_value(cls, dict_obj: object)->tuple: + """ + check whether all key are equal in a dict + :param dict_obj: + :return: + """ + value_list = [] + for key, value in dict_obj.items(): + value_list.append(value) + value_0 = value_list[0] + for value in value_list: + if value_0 != value: + return False + return True - - + @classmethod + def get_ticket_all_field_value(cls, ticket: Ticket)->dict: + """ + 工单所有字段的值 + get ticket's all field value + :param ticket: + :return: + """ + # 获取工单基础表中的字段中的字段信息 + field_info_dict = ticket.get_dict() + # 获取自定义字段的值 + custom_fields_queryset = cls.get_workflow_custom_fields(ticket.workflow) + for i in custom_fields_queryset: + field_info_dict[i.field_key] = ticket.ticket_data.get(i.field_key, None) + return field_info_dict diff --git a/hb_server/apps/wf/views.py b/hb_server/apps/wf/views.py index 5bf968a..0823dca 100644 --- a/hb_server/apps/wf/views.py +++ b/hb_server/apps/wf/views.py @@ -9,7 +9,7 @@ from rest_framework.decorators import action, api_view from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow from apps.system.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin, OptimizationMixin from apps.wf.services import WfService -from rest_framework.exceptions import APIException +from rest_framework.exceptions import APIException, PermissionDenied # Create your views here. class WorkflowViewSet(CreateUpdateModelAMixin, ModelViewSet): @@ -117,10 +117,10 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin if key not in ticket_data or not ticket_data[key]: raise APIException('字段{}必填'.format(key)) ticket = serializer.save(state=start_state) # 先创建出来 - next_state = WfService.get_next_state_id_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) destination_participant_type = participant_info.get('destination_participant_type', 0) - destination_participant = participant_info.get('destination_participant', '') + destination_participant = participant_info.get('destination_participant', None) multi_all_person = participant_info.get('multi_all_person', {}) # 多人需要全部处理情况 sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 if next_state.type == State.STATE_TYPE_END: @@ -144,9 +144,9 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin ticket.multi_all_person=multi_all_person ticket.save() # 新增流转记录 - TicketFlow.objects.create(ticket=ticket, state=start_state, ticket_data=ticket_data, + 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) + participant=ticket.create_by, transition=transition, create_by=request.user) return Response(TicketSerializer(instance=ticket).data) @@ -160,7 +160,72 @@ class TicketViewSet(OptimizationMixin, CreateUpdateCustomMixin, CreateModelMixin except: raise APIException('工单不存在') data = request.data - result = WfService.ticket_handle_permission_check() + result = WfService.ticket_handle_permission_check(ticket, request.user) + source_state = ticket.state + source_ticket_data = ticket.ticket_data + if result.get('permission') is False: + raise PermissionDenied(result.get('msg')) + # 校验表单必填项目 + transition = Transition.objects.get(pk=data['transition']) + ticket_data = data['ticket_data'] + if transition.field_require_check: + for key, value in ticket.state.state_fields.items(): + 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) + multi_all_person = ticket.multi_all_person + if multi_all_person: + multi_all_person[request.user.id] =dict(transition=transition.id) + # 判断所有人处理结果是否一致 + 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) + multi_all_person = {} + else: + # 处理人没有没有全部处理完成或者处理动作不一致 + destination_participant_type = ticket.participant_type + destination_state = ticket.state # 保持原状态 + destination_participant = [] + for key, value in multi_all_person.items(): + if not value: + destination_participant.push(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) + multi_all_person = participant_info.get('multi_all_person', {}) + + # 更新工单信息:基础字段及自定义字段, add_relation字段 需要下个处理人是部门、角色等的情况 + ticket.state = destination_state + ticket.participant_type = destination_participant_type + ticket.participant = destination_participant + ticket.multi_all_person = multi_all_person + if destination_state.type == State.STATE_TYPE_END: + ticket.act_state = State.TICKET_ACT_STATE_FINISH + elif destination_state.type == State.STATE_TYPE_START: + ticket.act_state = State.TICKET_ACT_STATE_DRAFT + else: + ticket.act_state = State.TICKET_ACT_STATE_ONGOING + + if transition.attribute_type == State.TRANSITION_ATTRIBUTE_TYPE_REFUSE: + transition.act_state = State.TICKET_ACT_STATE_BACK + + # 只更新必填和可选的字段 + for key, value in ticket.state.state_fields.items(): + if value in (State.STATE_FIELD_REQUIRED, State.STATE_FIELD_OPTIONAL): + source_ticket_data[key] = ticket_data[key] + ticket.ticket_data = source_ticket_data + ticket.save() + + # 更新工单流转记录 + 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, create_by=request.user) + return Response(TicketSerializer(instance=ticket).data) + @action(methods=['get'], detail=True, perms_map={'get':'*'}) def flowsteps(self, request, pk=None):