diff --git a/apps/am/migrations/0001_initial.py b/apps/am/migrations/0001_initial.py deleted file mode 100644 index 24964ac7..00000000 --- a/apps/am/migrations/0001_initial.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 3.2.12 on 2022-05-09 01:05 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('system', '0004_auto_20220421_1511'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Access', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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.PositiveSmallIntegerField(choices=[(10, '准入'), (20, '禁入')], verbose_name='准入类型')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Area', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), - ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), - ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('name', models.CharField(max_length=20, verbose_name='名称')), - ('level', models.PositiveSmallIntegerField(verbose_name='区域等级')), - ('sort_str', models.CharField(default='1', max_length=12, verbose_name='排序字符')), - ('visitor_no', models.BooleanField(default=True, verbose_name='不准许访客')), - ('rparty_no', models.BooleanField(default=True, verbose_name='不准许相关方')), - ('third_info', models.JSONField(blank=True, default=dict, verbose_name='三方信息')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='area_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), - ('manager', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='区域负责人')), - ('posts_access', models.ManyToManyField(through='am.Access', to='system.Post')), - ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='area_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='access', - name='area', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='am.area', verbose_name='关联区域'), - ), - migrations.AddField( - model_name='access', - name='post', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.post', verbose_name='关联岗位'), - ), - ] diff --git a/apps/am/models.py b/apps/am/models.py index 8234b9bb..2b7faac9 100755 --- a/apps/am/models.py +++ b/apps/am/models.py @@ -1,28 +1,37 @@ from django.db import models from apps.system.models import Post, User -from apps.utils.models import BaseModel, CommonAModel +from apps.utils.models import CommonADModel, CommonBModel # Create your models here. -class Area(CommonAModel): +class Area(CommonBModel): """ 地图区域 """ + AREA_LEVEL_1 = 10 + AREA_LEVEL_2 = 20 + AREA_LEVEL_3 = 30 + AREA_LEVEL_4 = 40 + AREA_LEVEL_HOICES = ( + (AREA_LEVEL_1, '办公'), + (AREA_LEVEL_2, '生产一般'), + (AREA_LEVEL_3, '生产重点'), + (AREA_LEVEL_4, '四级') + ) name = models.CharField('名称', max_length=20) level = models.PositiveSmallIntegerField('区域等级') sort_str = models.CharField('排序字符', max_length=12, default='1') - manager = models.ForeignKey(User, on_delete=models.CASCADE, - verbose_name='区域负责人') - visitor_no = models.BooleanField('不准许访客', default=True) - rparty_no = models.BooleanField('不准许相关方', default=True) + visitor_yes = models.BooleanField('准许访客人员', default=False) + remployee_yes = models.BooleanField('准许相关方人员', default=False) + employee_yes = models.BooleanField('准许全部员工', default=True) posts_access = models.ManyToManyField(Post, through='am.access') third_info = models.JSONField('三方信息', default=dict, null=False, blank=True) -class Access(BaseModel): +class Access(CommonADModel): """ - 准入权限 + 岗位准入权限 """ ACCESS_IN_YES = 10 ACCESS_IN_NO = 20 diff --git a/apps/am/serializers.py b/apps/am/serializers.py index 59c02c21..bb058d37 100644 --- a/apps/am/serializers.py +++ b/apps/am/serializers.py @@ -1,4 +1,4 @@ -from apps.am.models import Area +from apps.am.models import Access, Area from apps.utils.serializers import CustomModelSerializer @@ -6,3 +6,15 @@ class AreaSimpleSerializer(CustomModelSerializer): class Meta: model = Area fields = ['id', 'name', 'level'] + + +class AreaCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = Area + fields = ['name', 'level', 'sort_srt', 'visitor_yes', 'remployee_yes', 'employee_yes', 'belong_dept'] + + +class AccessCreateSerializer(CustomModelSerializer): + class Meta: + model = Access + fields = ['type', 'area', 'post'] diff --git a/apps/am/urls.py b/apps/am/urls.py new file mode 100644 index 00000000..8085a149 --- /dev/null +++ b/apps/am/urls.py @@ -0,0 +1,13 @@ +from apps.am.views import AreaViewSet, AccessViewSet +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +API_BASE_URL = 'api/am/' +HTML_BASE_URL = 'am/' + +router = DefaultRouter() +router.register('area', AreaViewSet, basename='area') +router.register('access', AccessViewSet, basename='access') +urlpatterns = [ + path(API_BASE_URL, include(router.urls)), +] diff --git a/apps/am/views.py b/apps/am/views.py index 91ea44a2..1818d45a 100755 --- a/apps/am/views.py +++ b/apps/am/views.py @@ -1,3 +1,46 @@ -from django.shortcuts import render +from apps.am.models import Area +from apps.am.serializers import AccessCreateSerializer, AreaCreateUpdateSerializer +from apps.utils.viewsets import CustomModelViewSet, CustomGenericViewSet +from django.db import transaction +from rest_framework.decorators import action +from rest_framework import serializers +from apps.third.clients import xxClient +from apps.third.tapis import xxapis +from rest_framework.response import Response +from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin + # Create your views here. +class AreaViewSet(CustomModelViewSet): + queryset = Area.objects.all() + create_serializer_class = AreaCreateUpdateSerializer + update_serializer_class = AreaCreateUpdateSerializer + + @transaction.atomic + @action(methods=['post'], detail=True, perms_map={'post': 'area:bind_rail'}, + serializer_class=serializers.Serializer) + def bind_rail(self, request, pk=None): + """ + 绑定围栏 + """ + data = request.data + obj = self.get_object() + third_info = obj.third_info + if 'xx_rail' in third_info: + data['uuid'] = third_info['xx_rail']['uuid'] + _, res = xxClient.request(**xxapis['rail_update'], json=data) + third_info['xx_rail']['detail'] = data + obj.third_info = third_info + obj.save() + else: + _, res = xxClient.request(**xxapis['rail_create'], json=data) + rail_info = {'uuid': res, 'detail': data} + third_info['xx_rail'] = rail_info + obj.third_info = third_info + obj.save() + return Response() + + +class AccessViewSet(ListModelMixin, CreateModelMixin, DestroyModelMixin, CustomGenericViewSet): + perms_map = {'post': 'access:create', 'delete': 'access:delete'} + create_serializer_class = AccessCreateSerializer diff --git a/apps/develop/views.py b/apps/develop/views.py index 83262db3..0c020328 100755 --- a/apps/develop/views.py +++ b/apps/develop/views.py @@ -7,7 +7,8 @@ from rest_framework.exceptions import APIException class ReloadServerGit(APIView): - permission_classes = [IsAdminUser] + authentication_classes = [] + permission_classes = [] def post(self, request): """ diff --git a/apps/am/migrations/__init__.py b/apps/ecm/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from apps/am/migrations/__init__.py rename to apps/ecm/__init__.py diff --git a/apps/ecm/admin.py b/apps/ecm/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/ecm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/ecm/apps.py b/apps/ecm/apps.py new file mode 100644 index 00000000..b223bfa1 --- /dev/null +++ b/apps/ecm/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EcmConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.ecm' diff --git a/apps/ecm/models.py b/apps/ecm/models.py new file mode 100644 index 00000000..198f294c --- /dev/null +++ b/apps/ecm/models.py @@ -0,0 +1,85 @@ +from django.db import models +from apps.am.models import Area +from apps.hrm.models import Employee +from apps.utils.models import BaseModel, CommonAModel, CommonBModel +from apps.system.models import Dept, Post, User +from django.utils import timezone +# Create your models here. + + +class EventCate(CommonAModel): + """ + 事件种类 + """ + EVENT_TRIGGER_CHOICES = ( + (10, '监控'), + (20, '定位') + ) + code = models.CharField('标识', max_length=10, unique=True) + name = models.CharField('名称', max_length=20, unique=True) + trigger = models.PositiveSmallIntegerField('触发方式', default=10, choices=EVENT_TRIGGER_CHOICES) + speaker_on = models.BooleanField('开启音响报警', default=True) + speakers = models.ManyToManyField('third.tdevice', verbose_name='固定音响') + filter_area_level = models.PositiveSmallIntegerField('固定音响区域级别过滤', choices=Area.AREA_LEVEL_HOICES, + default=Area.AREA_LEVEL_1) + + +class PushSetting(CommonAModel): + """ + 推送配置 + """ + PUSH_FILTER1_CHOICES = ( + (10, '当事人部门'), + (20, '当事人部门以上'), + (30, '属地部门'), + (40, '属地部门以上') + ) + event_cate = models.ForeignKey(EventCate, verbose_name='关联事件种类', + to_field='code', on_delete=models.CASCADE) + post = models.ForeignKey(Post, verbose_name='推送岗位', + on_delete=models.CASCADE, null=True, blank=True) + filter_to_user = models.PositiveSmallIntegerField('推送人员过滤', null=True, blank=True) + filter_area_level = models.PositiveSmallIntegerField('区域级别过滤', null=True, blank=True) + sms_enable = models.BooleanField('短信通知', default=False) + can_handle = models.BooleanField('是否可处理', default=False) + sort = models.PositiveIntegerField('排序', default=1) + + +class Event(CommonBModel): + """ + 事件 + """ + cate = models.ForeignKey(EventCate, verbose_name='事件种类') + imgs = models.JSONField('事件图片', default=list, null=False, blank=True) + area = models.ForeignKey(Area, verbose_name='发生区域', on_delete=models.CASCADE) + location = models.JSONField('事件点位坐标', default=dict, null=False, blank=True) + peope_type = models.CharField('当事人员类型', choices=Employee.PEOPLE_TYPE_CHOICES, + max_length=20, null=True, blank=True) + people = models.ForeignKey(Employee, verbose_name='当事人', + on_delete=models.CASCADE, null=True, blank=True) + handle_time = models.DateTimeField('处理时间', null=True, blank=True) + handle_user = models.ForeignKey(User, verbose_name='处理人', + on_delete=models.CASCADE, null=True, blank=True) + handle_desc = models.TextField('处理描述', null=True, blank=True) + is_pushed = models.BooleanField('是否已推送', default=False) + + +class Push(BaseModel): + """ + 推送情况 + """ + event = models.ForeignKey(Event, verbose_name='关联事件', + on_delete=models.CASCADE) + pusher = models.ForeignKey(User, verbose_name='推送人', + on_delete=models.CASCADE) + push_setting = models.ForeignKey(PushSetting, verbose_name='通过哪个配置', + on_delete=models.CASCADE, null=True, blank=True) + post = models.ForeignKey(Post, verbose_name='岗位', + on_delete=models.CASCADE, null=True, blank=True) + dept = models.ForeignKey(Dept, verbose_name='部门', + on_delete=models.CASCADE, null=True, blank=True) + msg = models.TextField('推送文本') + is_read = models.BooleanField('站内信已读', default=False) + is_sms_send = models.BooleanField('短信已发送', default=False) + can_handle = models.BooleanField('是否可处理', default=False) + last_push_time = models.DateTimeField('最后推送时间', default=timezone.now) diff --git a/apps/ecm/serializers.py b/apps/ecm/serializers.py new file mode 100644 index 00000000..94bb9b48 --- /dev/null +++ b/apps/ecm/serializers.py @@ -0,0 +1,54 @@ +from apps.am.serializers import AreaSimpleSerializer +from apps.ecm.models import EventCate, Push, PushSetting, Event +from apps.utils.serializers import CustomModelSerializer +from rest_framework import serializers +from apps.system.serializers import UserSimpleSerializer + + +class EventCateSimpleSerializer(CustomModelSerializer): + class Meta: + model = Event + fields = ['id', 'name', 'code'] + + +class EventCateListSerializer(CustomModelSerializer): + class Meta: + model = EventCate + fields = '__all__' + + +class EventCateUpdateSerializer(CustomModelSerializer): + class Meta: + model = EventCate + fields = ['speaker_on', 'speakers', 'filter_area_level'] + + +class PushSettingsSerializer(CustomModelSerializer): + class Meta: + model = PushSetting + fields = '__all__' + + +class EventSerializer(CustomModelSerializer): + area_ = AreaSimpleSerializer(source='area', read_only=True) + cate_ = EventCateSimpleSerializer(source='cate', read_only=True) + people_name = serializers.CharField(source='people.name', read_only=True) + handle_user_name = serializers.CharField(source='handle_user.name', read_only=True) + + class Meta: + model = Event + fields = '__all__' + + +class EventHandleSerializer(CustomModelSerializer): + class Meta: + model = Event + fields = ['handle_desc'] + + +class PushSerializer(CustomModelSerializer): + pusher_ = UserSimpleSerializer(source='pusher', read_only=True) + + class Meta: + model = Push + fields = '__all__' diff --git a/apps/ecm/tests.py b/apps/ecm/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/ecm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/ecm/urls.py b/apps/ecm/urls.py new file mode 100644 index 00000000..a5b4ffdf --- /dev/null +++ b/apps/ecm/urls.py @@ -0,0 +1,15 @@ +from apps.ecm.views import EventCateViewSet, PushSettingViewSet, EventViewSet, PushViewSet +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +API_BASE_URL = 'api/ecm/' +HTML_BASE_URL = 'ecm/' + +router = DefaultRouter() +router.register('event_cate', EventCateViewSet, basename='event_cate') +router.register('event', EventViewSet, basename='event') +router.register('push_setting', PushSettingViewSet, basename='push_setting') +router.register('push', PushViewSet, basename='push') +urlpatterns = [ + path(API_BASE_URL, include(router.urls)), +] diff --git a/apps/ecm/views.py b/apps/ecm/views.py new file mode 100644 index 00000000..c1e786ae --- /dev/null +++ b/apps/ecm/views.py @@ -0,0 +1,68 @@ + +from apps.ecm.models import Event, EventCate, Push, PushSetting +from apps.ecm.serializers import (EventCateListSerializer, EventCateUpdateSerializer, EventHandleSerializer, + EventSerializer, PushSerializer, PushSettingsSerializer) +from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet +from rest_framework.mixins import UpdateModelMixin, ListModelMixin, RetrieveModelMixin +from django.db import transaction +from rest_framework.decorators import action +from rest_framework import serializers +from django.utils import timezone +from rest_framework.response import Response + + +# Create your views here. +class EventCateViewSet(UpdateModelMixin, ListModelMixin, CustomGenericViewSet): + perms_map = {'put': 'eventcate:update'} + queryset = EventCate.objects.all() + list_serializer_class = EventCateListSerializer + update_serializer_class = EventCateUpdateSerializer + + +class PushSettingViewSet(CustomModelViewSet): + queryset = PushSetting.objects.all() + serializer_class = PushSettingsSerializer + + +class EventViewSet(ListModelMixin, RetrieveModelMixin, CustomGenericViewSet): + perms_map = {'get': 'event:view'} + queryset = Event.objects.all() + serializer_class = EventSerializer + + @transaction.atomic + @action(methods=['post'], detail=True, perms_map={'post': 'event:handle'}, + serializer_class=EventHandleSerializer) + def handle(self, request, pk=None): + """ + 处理事件 + """ + obj = self.get_object() + data = request.data + obj.handle_desc = data.get('handle_desc', '') + obj.handle_user = request.user + obj.handle_time = timezone.now() + obj.save() + return Response() + + +class PushViewSet(ListModelMixin, CustomGenericViewSet): + perms_map = {'get': 'push:view'} + queryset = Push.objects.all() + serializer_class = PushSerializer + + @action(methods=['get'], detail=False, perms_map={'get': '*'}) + def my(self, request, *args, **kwargs): + """ + 推送给我的 + """ + user = self.request.user + queryset = self.filter_queryset(self.get_queryset().filter(pusher=user)) + + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + diff --git a/apps/hrm/migrations/0001_initial.py b/apps/hrm/migrations/0001_initial.py deleted file mode 100755 index 9471866d..00000000 --- a/apps/hrm/migrations/0001_initial.py +++ /dev/null @@ -1,85 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-13 06:34 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('system', '0003_remove_user_phone'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='NotWorkRemark', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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='删除标记')), - ('not_work_date', models.DateField(verbose_name='未打卡日期')), - ('remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='未打卡说明')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='notworkremark_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='notworkremark_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='Employee', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), - ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), - ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('name', models.CharField(max_length=20, verbose_name='姓名')), - ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')), - ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='邮箱号')), - ('number', models.CharField(blank=True, max_length=50, null=True, verbose_name='人员编号')), - ('photo', models.CharField(blank=True, max_length=1000, null=True, verbose_name='证件照')), - ('id_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='身份证号')), - ('gender', models.CharField(default='男', max_length=10, verbose_name='性别')), - ('signature', models.CharField(blank=True, max_length=200, null=True, verbose_name='签名图片')), - ('birthday', models.DateField(blank=True, null=True, verbose_name='出生年月日')), - ('qualification', models.CharField(blank=True, max_length=50, null=True, verbose_name='学历')), - ('job_state', models.IntegerField(choices=[(10, '在职'), (20, '离职')], default=1, verbose_name='在职状态')), - ('face_data', models.JSONField(blank=True, null=True, verbose_name='人脸识别数据')), - ('is_atwork', models.BooleanField(default=False, verbose_name='当前在岗')), - ('show_atwork', models.BooleanField(default=True, verbose_name='是否展示在岗状态')), - ('last_check_time', models.DateTimeField(blank=True, null=True, verbose_name='打卡时间')), - ('not_work_remark', models.CharField(blank=True, max_length=200, null=True, verbose_name='当前未打卡说明')), - ('third_info', models.JSONField(blank=True, default=dict, null=True, verbose_name='三方信息')), - ('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_belong_dept', to='system.dept', verbose_name='所属部门')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employee_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='employee_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ('user', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='employee_user', to=settings.AUTH_USER_MODEL, verbose_name='系统账号')), - ], - options={ - 'verbose_name': '员工补充信息', - 'verbose_name_plural': '员工补充信息', - }, - ), - migrations.CreateModel( - name='ClockRecord', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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.PositiveSmallIntegerField(choices=[(10, '上班打卡')], default=10, verbose_name='打卡类型')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='clockrecord_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='clockrecord_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/apps/hrm/migrations/0002_alter_employee_third_info.py b/apps/hrm/migrations/0002_alter_employee_third_info.py deleted file mode 100755 index a38f7391..00000000 --- a/apps/hrm/migrations/0002_alter_employee_third_info.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-21 07:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hrm', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='employee', - name='third_info', - field=models.JSONField(blank=True, default=dict, verbose_name='三方信息'), - ), - ] diff --git a/apps/hrm/models.py b/apps/hrm/models.py index 1f7fa7b6..1a9afe75 100755 --- a/apps/hrm/models.py +++ b/apps/hrm/models.py @@ -1,7 +1,7 @@ from django.db import models from apps.system.models import User -from apps.utils.models import CommonADModel, CommonAModel, CommonBModel +from apps.utils.models import BaseModel, CommonADModel, CommonAModel, CommonBModel class Employee(CommonBModel): @@ -14,9 +14,15 @@ class Employee(CommonBModel): (JOB_ON, '在职'), (JOB_OFF, '离职'), ) + PEOPLE_TYPE_CHOICES = ( + ('employee', '内部员工'), + ('remployee', '相关方人员'), + ('visitor', '访客') + ) + type = models.CharField('人员类型', default='employee', max_length=10, choices=PEOPLE_TYPE_CHOICES) user = models.OneToOneField(User, verbose_name='系统账号', - on_delete=models.PROTECT, related_name='employee_user', null=True, blank=True) + on_delete=models.PROTECT, null=True, blank=True) name = models.CharField('姓名', max_length=20) phone = models.CharField('手机号', max_length=11, null=True, blank=True) email = models.EmailField('邮箱号', null=True, blank=True) @@ -33,7 +39,7 @@ class Employee(CommonBModel): show_atwork = models.BooleanField('是否展示在岗状态', default=True) last_check_time = models.DateTimeField('打卡时间', null=True, blank=True) not_work_remark = models.CharField('当前未打卡说明', null=True, blank=True, max_length=200) - third_info = models.JSONField('三方信息', default=dict, null=False, blank=True) + third_info = models.JSONField('三方信息', default=dict, null=False, blank=True) # 主要是定位卡信息 class Meta: verbose_name = '员工补充信息' @@ -69,3 +75,22 @@ class ClockRecord(CommonADModel): (ClOCK_WORK1, '上班打卡'), ) type = models.PositiveSmallIntegerField('打卡类型', choices=type_choice, default=ClOCK_WORK1) + + +class Certificate(CommonAModel): + """ + 证书 + """ + CERTIFICATE_TYPE_CHOICES = ( + (10, '特种作业证书'), + (20, '特种设备操作证书'), + (30, '安全管理人员证书') + ) + employee = models.ForeignKey(Employee, verbose_name='对应人员', on_delete=models.CASCADE) + name = models.CharField('证书名称', max_length=20) + number = models.CharField('证书编号', max_length=20) + type = models.PositiveSmallIntegerField('证书类型', default=10) + issue_date = models.DateField('发证日期') + expiration_date = models.DateField('有效期') + review_date = models.DateField('下一次复审日期') + file = models.CharField('文件地址', max_length=1000, null=True, blank=True) diff --git a/apps/hrm/serializers.py b/apps/hrm/serializers.py index 9e1a9272..ea04020b 100755 --- a/apps/hrm/serializers.py +++ b/apps/hrm/serializers.py @@ -3,9 +3,9 @@ from rest_framework.serializers import ModelSerializer from rest_framework import serializers from apps.utils.serializers import CustomModelSerializer -from apps.utils.constants import EXCLUDE_FIELDS +from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE from apps.utils.tools import rannum -from apps.hrm.models import ClockRecord, Employee, NotWorkRemark +from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer from django.db import transaction from apps.third.clients import dhClient @@ -37,7 +37,7 @@ class EmployeeCreateUpdateSerializer(EmployeeBaseSerializer): model = Employee exclude = EXCLUDE_FIELDS + ['face_data', 'is_atwork', 'last_check_time', - 'not_work_remark', 'third_info'] + 'not_work_remark', 'third_info', 'type'] extra_kwargs = { 'phone': {'required': True}, 'number': {'required': True}, @@ -214,3 +214,15 @@ class NotWorkRemarkListSerializer(serializers.ModelSerializer): class Meta: model = NotWorkRemark fields = '__all__' + + +class CertificateCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = Certificate + exclude = EXCLUDE_FIELDS + + +class CertificateSerializer(CustomModelSerializer): + class Meta: + model = Certificate + fields = '__all__' diff --git a/apps/hrm/signals.py b/apps/hrm/signals.py index 6c1013b0..aa5d34c3 100755 --- a/apps/hrm/signals.py +++ b/apps/hrm/signals.py @@ -7,9 +7,10 @@ from apps.hrm.models import Employee @receiver(post_save, sender=User) def updateEmployee(sender, instance, created, **kwargs): # if created: - # 如果账号所属部门有变动, 更新关联人员的所属部门 - ep = Employee.objects.filter(user=instance).first() - if ep: - if ep.belong_dept and ep.belong_dept != instance.belong_dept: - ep.belong_dept = instance.belong_dept - ep.save() + # 如果账号所属部门有变动, 更新关联人员的所属部门, 只限内部人员 + if not instance.is_superuser: + ep = Employee.objects.filter(user=instance).first() + if ep and ep.type == 'employee': + if ep.belong_dept and ep.belong_dept != instance.belong_dept: + ep.belong_dept = instance.belong_dept + ep.save() diff --git a/apps/hrm/views.py b/apps/hrm/views.py index 7dcbf693..a87d093c 100755 --- a/apps/hrm/views.py +++ b/apps/hrm/views.py @@ -12,8 +12,8 @@ from rest_framework.response import Response from apps.hrm.errors import NO_NEED_LEVEL_REMARK from apps.hrm.filters import (ClockRecordFilterSet, EmployeeFilterSet, NotWorkRemarkFilterSet) -from apps.hrm.models import ClockRecord, Employee, NotWorkRemark -from apps.hrm.serializers import (ChannelAuthoritySerializer, +from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark +from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, ClockRecordListSerializer, EmployeeCreateUpdateSerializer, EmployeeNotWorkRemarkSerializer, @@ -182,3 +182,10 @@ class NotWorkRemarkViewSet(ListModelMixin, CustomGenericViewSet): serializer_class = NotWorkRemarkListSerializer filterset_class = NotWorkRemarkFilterSet ordering = ['-pk'] + + +class CertificateViewSet(CustomModelViewSet): + queryset = Certificate.objects.filter(employee__type='employee') + create_serializer_class = CertificateCreateUpdateSerializer + update_serializer_class = CertificateCreateUpdateSerializer + serializer_class = CertificateSerializer diff --git a/apps/monitor/migrations/0001_initial.py b/apps/monitor/migrations/0001_initial.py old mode 100755 new mode 100644 index 87192dd8..7c471222 --- a/apps/monitor/migrations/0001_initial.py +++ b/apps/monitor/migrations/0001_initial.py @@ -1,9 +1,10 @@ -# Generated by Django 3.2.12 on 2022-04-11 03:20 +# Generated by Django 3.2.12 on 2022-06-06 07:59 from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone +import uuid class Migration(migrations.Migration): @@ -21,12 +22,12 @@ class Migration(migrations.Migration): ('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='删除标记')), - ('id', models.UUIDField(primary_key=True, serialize=False)), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('requested_at', models.DateTimeField(db_index=True)), ('response_ms', models.PositiveIntegerField(default=0)), ('path', models.CharField(db_index=True, help_text='请求地址', max_length=400)), ('view', models.CharField(blank=True, db_index=True, help_text='执行视图', max_length=400, null=True)), - ('view_method', models.CharField(blank=True, db_index=True, max_length=6, null=True)), + ('view_method', models.CharField(blank=True, db_index=True, max_length=20, null=True)), ('remote_addr', models.GenericIPAddressField()), ('host', models.URLField()), ('method', models.CharField(max_length=10)), diff --git a/apps/monitor/migrations/0002_alter_drfrequestlog_id.py b/apps/monitor/migrations/0002_alter_drfrequestlog_id.py deleted file mode 100755 index 76302287..00000000 --- a/apps/monitor/migrations/0002_alter_drfrequestlog_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-11 03:27 - -from django.db import migrations, models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('monitor', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='drfrequestlog', - name='id', - field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), - ), - ] diff --git a/apps/monitor/migrations/0003_alter_drfrequestlog_view_method.py b/apps/monitor/migrations/0003_alter_drfrequestlog_view_method.py deleted file mode 100755 index f8605082..00000000 --- a/apps/monitor/migrations/0003_alter_drfrequestlog_view_method.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-18 02:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('monitor', '0002_alter_drfrequestlog_id'), - ] - - operations = [ - migrations.AlterField( - model_name='drfrequestlog', - name='view_method', - field=models.CharField(blank=True, db_index=True, max_length=20, null=True), - ), - ] diff --git a/apps/monitor/migrations/__init__.py b/apps/monitor/migrations/__init__.py old mode 100755 new mode 100644 diff --git a/apps/auth1/migrations/__init__.py b/apps/opm/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from apps/auth1/migrations/__init__.py rename to apps/opm/__init__.py diff --git a/apps/opm/admin.py b/apps/opm/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/opm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/opm/apps.py b/apps/opm/apps.py new file mode 100644 index 00000000..fdc31b82 --- /dev/null +++ b/apps/opm/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OpmConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.opm' diff --git a/apps/opm/models.py b/apps/opm/models.py new file mode 100644 index 00000000..cfeef758 --- /dev/null +++ b/apps/opm/models.py @@ -0,0 +1,69 @@ +from django.db import models +from apps.system.models import Dept, Dictionary, User +from apps.utils.models import BaseModel, CommonAModel, CommonBModel +from apps.am.models import Area +from apps.wf.models import Ticket, Workflow + + +# Create your models here. +class OptCate(CommonAModel): + """ + 作业票种类 + """ + code = models.CharField('标识', max_length=10, unique=True) + name = models.CharField('名称', max_length=20, unique=True) + template_export = models.TextField('导出word模板', null=True, blank=True) + workflow = models.ForeignKey(Workflow, verbose_name='所用工作流', + on_delete=models.CASCADE, null=True, blank=True) + + +class Operation(CommonBModel): + OP_STATE_CHOICES = ( + (10, '创建中'), + (20, '审批中'), + (30, '待作业'), + (40, '待关闭'), + (50, '已关闭') + ) + OP_WORK_CHOICES = ( + ('work', '运行'), + ('stop', '停机'), + ('repair', '检修') + ) + number = models.CharField('作业编号', max_length=20, null=True, blank=True) + name = models.CharField('作业简述', max_length=200) + area = models.ForeignKey(Area, verbose_name='作业区域', + on_delete=models.CASCADE) + place = models.TextField('具体地点', null=True, blank=True) + start_time = models.DateTimeField('作业开始时间') + end_time = models.DateTimeField('作业结束时间') + dept_sd = models.ForeignKey(Dept, verbose_name='属地部门', + on_delete=models.CASCADE) + dept_yw = models.ForeignKey(Dept, verbose_name='业务部门', + on_delete=models.CASCADE) + user_xt = models.ForeignKey(User, verbose_name='业务部门协调员', + on_delete=models.CASCADE) + user_fz = models.ForeignKey(User, verbose_name='作业负责人', + on_delete=models.CASCADE) + user_jh = models.ForeignKey(User, verbose_name='作业监护人', + ) + state_work = models.CharField('生产状态', choices=OP_WORK_CHOICES) + + +class Opt(CommonBModel): + """ + 作业票 + """ + operation = models.ForeignKey(Operation, verbose_name='关联作业', + on_delete=models.CASCADE) + number = models.CharField('作业票编号', max_length=20, null=True, blank=True) + cate = models.ForeignKey(OptCate, verbose_name='作业票种类', + on_delete=models.CASCADE) + dept_zy = models.ForeignKey(Dept, verbose_name='作业部门', + on_delete=models.CASCADE) + user_fz = models.ForeignKey(User, verbose_name='作业负责人', + on_delete=models.CASCADE) + user_jh = models.ForeignKey(User, verbose_name='作业监护人', + ) + ticket = models.ForeignKey(Ticket, verbose_name='关联工单', + on_delete=models.CASCADE, null=True, blank=True) diff --git a/apps/opm/serializers.py b/apps/opm/serializers.py new file mode 100644 index 00000000..d6ec2071 --- /dev/null +++ b/apps/opm/serializers.py @@ -0,0 +1,30 @@ +from apps.opm.models import Operation, OptCate +from apps.utils.serializers import CustomModelSerializer +from apps.wf.serializers import WorkflowSimpleSerializer +from apps.utils.constants import EXCLUDE_FIELDS + + +class OptCateCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = OptCate + fields = ['code', 'name', 'template_export', 'workflow'] + + +class OptCateSerializer(CustomModelSerializer): + workflow_ = WorkflowSimpleSerializer(source='workflow', read_only=True) + + class Meta: + model = OptCate + fields = '__all__' + + +class OperationCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = Operation + fields = EXCLUDE_FIELDS + ['number'] + + +class OperationSerializer(CustomModelSerializer): + class Meta: + model = Operation + fields = "__all__" diff --git a/apps/opm/services.py b/apps/opm/services.py new file mode 100644 index 00000000..86defa84 --- /dev/null +++ b/apps/opm/services.py @@ -0,0 +1,5 @@ +from apps.wf.models import Ticket + + +def create_opt(ticket: Ticket): + pass \ No newline at end of file diff --git a/apps/opm/tests.py b/apps/opm/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/opm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/opm/views.py b/apps/opm/views.py new file mode 100644 index 00000000..68467073 --- /dev/null +++ b/apps/opm/views.py @@ -0,0 +1,19 @@ +from django.shortcuts import render +from apps.opm.models import Operation, OptCate +from apps.opm.serializers import OperationCreateUpdateSerializer, OptCateCreateUpdateSerializer, OptCateSerializer +from apps.utils.viewsets import CustomModelViewSet + + +# Create your views here. +class OptCateViewSet(CustomModelViewSet): + queryset = OptCate.objects.all() + create_serializer_class = OptCateCreateUpdateSerializer + update_serializer_class = OptCateCreateUpdateSerializer + serializer_class = OptCateSerializer + + +class OperationViewSet(CustomModelViewSet): + queryset = Operation.objects.all() + create_serializer_class = OperationCreateUpdateSerializer + update_serializer_class = OperationCreateUpdateSerializer + \ No newline at end of file diff --git a/apps/develop/migrations/__init__.py b/apps/rpm/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from apps/develop/migrations/__init__.py rename to apps/rpm/__init__.py diff --git a/apps/rpm/admin.py b/apps/rpm/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/rpm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/rpm/apps.py b/apps/rpm/apps.py new file mode 100644 index 00000000..7ff3a4cc --- /dev/null +++ b/apps/rpm/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RpmConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.rpm' diff --git a/apps/rpm/models.py b/apps/rpm/models.py new file mode 100644 index 00000000..a67c2e3d --- /dev/null +++ b/apps/rpm/models.py @@ -0,0 +1,111 @@ +from django.db import models +from apps.hrm.models import Certificate, Employee +from apps.system.models import Dept, Dictionary, File, User +from apps.utils.models import CommonBModel, BaseModel + + +# Create your models here. +class Rparty(CommonBModel): + """ + 相关方 + """ + dept = models.OneToOneField(Dept, verbose_name='关联部门', + on_delete=models.CASCADE, + null=True, blank=True) + name = models.CharField('名称', max_length=20) + number = models.CharField('信用代码', max_length=50, null=True, blank=True) + lawer = models.CharField('法人', max_length=20, null=True, blank=True) + contacter = models.CharField('联系人', max_length=20) + phone = models.CharField('联系电话', max_length=20) + email = models.EmailField('邮箱', null=True, blank=True) + addresss = models.CharField('企业地址', max_length=200, null=True, blank=True) + description = models.TextField('概述', null=True, blank=True) + # belong_dept是归属部门 + + +class RpartyFile(BaseModel): + """ + 相关方文件库 + """ + file_cate = models.ForeignKey(Dictionary, verbose_name='文件种类', + on_delete=models.CASCADE, null=True, blank=True) + files = models.ManyToManyField(File, verbose_name='文件') + rparty = models.ForeignKey(Rparty, verbose_name='关联相关方', on_delete=models.CASCADE) + + +class Rproject(CommonBModel): + """ + 相关方项目 + """ + RP_START = 10 + RP_APPROVAL = 20 + RP_ENTER = 30 + RP_WORKING = 40 + RP_DONE = 50 + RP_STATE_CHOICES = ( + (10, '创建中'), + (20, '审批中'), + (30, '待入厂'), + (40, '进行中'), + (50, '已完成') + ) + RP_TYPE_CHOICES = ( + (10, '建筑施工'), + (20, '设备设施检维修'), + (30, '保安保洁服务'), + (40, '其他') + ) + name = models.CharField('名称', max_length=20) + type = models.PositiveSmallIntegerField('项目类型', default=10) + contract_number = models.CharField('合同编号', max_length=20) + come_time = models.DateTimeField('进厂时间') + leave_time = models.DateTimeField('离厂时间') + state = models.PositiveSmallIntegerField('状态', default=10) + rparty = models.ForeignKey(Rparty, verbose_name='关联相关方', on_delete=models.CASCADE) + # belong_dept是业务部门可以带过来 + + +class RprojectFile(BaseModel): + """ + 相关方项目文件库 + """ + file_cate = models.ForeignKey(Dictionary, verbose_name='文件种类', + on_delete=models.CASCADE, null=True, blank=True) + files = models.ManyToManyField(File, verbose_name='文件') + rproject = models.ForeignKey(Rproject, verbose_name='关联相关方项目', on_delete=models.CASCADE) + + +class Remployee(CommonBModel): + """ + 相关方成员 + """ + employee = models.OneToOneField(Employee, verbose_name='成员信息', on_delete=models.CASCADE) + rparty = models.ForeignKey(Rparty, verbose_name='所属相关方', on_delete=models.CASCADE) + rproject = models.ForeignKey(Rproject, verbose_name='当前所属相关方项目', on_delete=models.CASCADE, + null=True, blank=True) + + +class Rpeople(BaseModel): + """ + 相关方项目人员 + """ + rproject = models.ForeignKey(Rproject, verbose_name='关联项目', on_delete=models.CASCADE) + employee = models.ForeignKey(Employee, verbose_name='关联人员', + on_delete=models.CASCADE, null=True, blank=True) + is_manager = models.BooleanField('是否项目负责人', default=False) + + +class Rcertificate(BaseModel): + """ + 相关方项目人员证书 + """ + rproject = models.ForeignKey(Rproject, verbose_name='关联项目', on_delete=models.CASCADE) + employee = models.ForeignKey(Employee, verbose_name='关联人员', + on_delete=models.CASCADE, null=True, blank=True) + name = models.CharField('证书名称', max_length=20) + number = models.CharField('证书编号', max_length=20) + type = models.PositiveSmallIntegerField('证书类型', default=10, choices=Certificate.CERTIFICATE_TYPE_CHOICES) + issue_date = models.DateField('发证日期') + expiration_date = models.DateField('有效期') + review_date = models.DateField('下一次复审日期') + file = models.CharField('文件地址', max_length=1000, null=True, blank=True) diff --git a/apps/rpm/serializers.py b/apps/rpm/serializers.py new file mode 100644 index 00000000..41f5405f --- /dev/null +++ b/apps/rpm/serializers.py @@ -0,0 +1,96 @@ +from apps.hrm.models import Employee +from apps.hrm.serializers import phone_check +from apps.rpm.models import Rparty, Rpeople, Rproject +from apps.system.models import Dept +from apps.utils.constants import EXCLUDE_FIELDS +from apps.utils.serializers import CustomModelSerializer +from apps.system.serializers import DictSerializer, FileSerializer +from rest_framework import serializers + + +class RpartyCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = Rparty + fields = ['name', 'number', 'lawer', 'contacter', 'phone', 'email', + 'description', 'belong_dept'] + extra_kwargs = { + 'belong_dept': {'required': True} + } + + def create(self, validated_data): + instance = super().create(validated_data) + dept = Dept.objects.create(name=instance.name, + parent=instance.belong_dept) + instance.dept = dept + instance.save() + return instance + + def update(self, instance, validated_data): + instance = super().update(instance, validated_data) + dept = instance.dept + dept.name = instance.name + dept.parent = instance.belong_dept + dept.save() + return instance + + +class RpartySerializer(CustomModelSerializer): + class Meta: + model = Rparty + fields = '__all__' + + +class RpartySimpleSerializer(CustomModelSerializer): + class Meta: + model = Rparty + fields = ['id', 'name'] + + +class RpartyAssignSerializer(serializers.Serializer): + username = serializers.CharField('用户名') + + +class RpartyFileListSerializer(CustomModelSerializer): + rparty_ = RpartySimpleSerializer(source='rparty', read_only=True) + file_cate_ = DictSerializer(source='file_cate', read_only=True) + files_ = FileSerializer(source='files', many=True, read_only=True) + + +class RprojectCreateUpdateSerializer(CustomModelSerializer): + class Meta: + model = Rproject + fields = ['name', 'contract_number', 'type', 'come_time', 'leave_time', 'belong_dept'] + + +class RprojectListSerializer(CustomModelSerializer): + class Meta: + model = Rproject + fields = '__all__' + + +class RemployeeCreateUpdateSerializer(CustomModelSerializer): + phone = serializers.CharField(label="手机号", validators=[phone_check]) + rparty = serializers.PrimaryKeyRelatedField(queryset=Rparty.objects.all(), label='相关方ID', required=False) + + class Meta: + model = Employee + exclude = EXCLUDE_FIELDS + ['face_data', + 'is_atwork', 'last_check_time', + 'not_work_remark', 'third_info', 'type'] + extra_kwargs = { + 'phone': {'required': True}, + 'number': {'required': True}, + 'photo': {'required': True}, + 'id_number': {'required': True}, + } + + +class RpeopleCreatesSerializer(CustomModelSerializer): + employees = serializers.PrimaryKeyRelatedField( + queryset=Employee.objects.filter(type='remployee'), + many=True, label='员工ID列表' + ) + + class Meta: + model = Rpeople + fields = ['employees', 'rproject'] diff --git a/apps/rpm/tests.py b/apps/rpm/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/rpm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/hrm/migrations/__init__.py b/apps/rpm/urls.py old mode 100755 new mode 100644 similarity index 100% rename from apps/hrm/migrations/__init__.py rename to apps/rpm/urls.py diff --git a/apps/rpm/views.py b/apps/rpm/views.py new file mode 100644 index 00000000..64c1cfaa --- /dev/null +++ b/apps/rpm/views.py @@ -0,0 +1,176 @@ +from django.shortcuts import render +from apps.hrm.models import Certificate, Employee +from apps.hrm.serializers import CertificateCreateUpdateSerializer, CertificateSerializer +from apps.rpm.models import Remployee, Rparty, RpartyFile, Rpeople, Rproject +from apps.rpm.serializers import RemployeeCreateUpdateSerializer, RpartyAssignSerializer, RpartyCreateUpdateSerializer, RpartyFileListSerializer, RpartySerializer, RpeopleCreatesSerializer, RprojectCreateUpdateSerializer +from apps.system.models import Post, User, UserPost +from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet +from rest_framework.mixins import CreateModelMixin, ListModelMixin +from rest_framework.decorators import action +from rest_framework.response import Response +from django.contrib.auth.hashers import check_password, make_password +from django.db import transaction +from rest_framework.exceptions import ParseError +from rest_framework import serializers + + +# Create your views here. +class RpartyViewSet(CustomModelViewSet): + queryset = Rparty.objects.all() + create_serializer_class = RpartyCreateUpdateSerializer + update_serializer_class = RpartyCreateUpdateSerializer + serializer_class = RpartySerializer + + @action(methods=['post'], detail=True, perms_map={'post': 'rparty:assgin'}, serializer_class=RpartyAssignSerializer) + @transaction.atomic + def assign(self, request, *args, **kwargs): + """ + 分配账号 + """ + obj = self.get_object() + username = request.data.get('username') + password = make_password('0000') + user = User.objects.create(username=username, + password=password, + type='remployee', + belong_dept=obj.dept) + obj.user = user + obj.save() + post, _ = Post.objects.get_or_create(code='rparty_admin', + defaults={ + 'name': '本相关方管理员', + 'code': 'rparty_admin' + }) + UserPost.objects.create(user=user, dept=obj.dept, + post=post) + return Response() + + +class RpartyFileViewSet(ListModelMixin, CustomGenericViewSet): + perms_map = {'get': 'rparty_file:view'} + queryset = RpartyFile.objects.all() + list_serializer_class = RpartyFileListSerializer + + def get_queryset(self): + queryset = super().get_queryset() + # 防止越权,加入的逻辑,可以通过岗位控权实现 + if self.request.user.type == 'remployee': + queryset = queryset.filter(rparty=self.request.user.belong_dept) + return queryset + + +class RemployeeViewSet(CustomModelViewSet): + queryset = Employee.objects.filter(type='remployee') + create_serializer_class = RemployeeCreateUpdateSerializer + update_serializer_class = RemployeeCreateUpdateSerializer + + def get_queryset(self): + user = self.request.user + queryset = super().get_queryset() + if user.type == 'remployee': + queryset = queryset.filter(belong_dept=user.belong_dept) + return queryset + + @transaction.atomic + def create(self, request, *args, **kwargs): + """ + 添加人员 + """ + user = self.request.user + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + vdata = serializer.validated_data + if user.type == 'remployee': # 如果是相关方账号 + ep = serializer.save() + Remployee.objects.get_or_create(employee=ep, rparty=user.belong_dept.rparty, + defaults={ + "employee": ep, + "rparty": user.belong_dept.rparty + }) + else: + if 'rparty' not in vdata: + raise ParseError('未指定相关方') + serializer.save() + return Response(serializer.data, status=201) + + +class Rcertificate(CustomModelViewSet): + queryset = Certificate.objects.filter(employee__type='remployee') + create_serializer_class = CertificateCreateUpdateSerializer + update_serializer_class = CertificateCreateUpdateSerializer + serializer_class = CertificateSerializer + + def get_queryset(self): + user = self.request.user + queryset = super().get_queryset() + if user.type == 'remployee': + queryset = queryset.filter(belong_dept=user.belong_dept) + return queryset + + +class RprojectViewSet(CustomModelViewSet): + queryset = Rproject.objects.all() + create_serializer_class = RprojectCreateUpdateSerializer + update_serializer_class = RprojectCreateUpdateSerializer + + def get_queryset(self): + user = self.request.user + queryset = super().get_queryset() + if user.type == 'remployee': + queryset = queryset.filter(belong_dept=user.belong_dept) + return queryset + + def create(self, request, *args, **kwargs): + user = self.request.user + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + vdata = serializer.validated_data + if user.type == 'remployee': + vdata['rparty'] = user.belong_dept.rparty + if not vdata.get('rparty', None): + raise ParseError('请指定相关方') + if not vdata.get('belong_dept', None): + vdata['belong_dept'] = vdata['rparty'].belong_dept + return super().create(request, *args, **kwargs) + + def update(self, request, *args, **kwargs): + obj = self.get_object() + if obj.state == Rproject.RP_START: + return super().update(request, *args, **kwargs) + raise ParseError('项目非创建状态不可更改') + + +class RpeopleViewSet(CustomGenericViewSet): + perms_map = {'get': '*'} + queryset = Rpeople.objects.all() + + @action(methods=['post'], detail=False, + perms_map={'post': 'rproject:update'}, serializer_class=RpeopleCreatesSerializer) + @transaction.atomic + def creates(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + vdata = serializer.validated_data + rp = vdata['rproject'] + if rp.state != Rproject.RP_START: + raise ParseError('项目非创建状态不可更改') + for i in vdata['employees']: + Rpeople.objects.create(employee=i, rproject=rp) + # 有证书的添加人员证书 + + return Response() + + @action(methods=['put'], detail=True, + perms_map={'put': 'rproject:update'}, serializer_class=serializers.Serializer) + @transaction.atomic + def make_manager(self, request): + """ + 设为项目负责人 + """ + obj = self.get_object() + if obj.rproject.state != Rproject.RP_START: + raise ParseError('项目非创建状态不可更改') + Rpeople.objects.filter(rproject=obj.rproject).update(is_manager=False) + obj.is_manager = True + obj.save() + return Response() diff --git a/apps/system/admin.py b/apps/system/admin.py index 5a5edbed..249639fc 100755 --- a/apps/system/admin.py +++ b/apps/system/admin.py @@ -1,5 +1,4 @@ from django.contrib import admin -from simple_history.admin import SimpleHistoryAdmin from .models import User, Dept, Role, Permission, DictType, Dictionary, File # Register your models here. admin.site.register(User) @@ -7,5 +6,5 @@ admin.site.register(Dept) admin.site.register(Role) admin.site.register(Permission) admin.site.register(DictType) -admin.site.register(Dictionary, SimpleHistoryAdmin) +admin.site.register(Dictionary) admin.site.register(File) diff --git a/apps/system/migrations/0001_initial.py b/apps/system/migrations/0001_initial.py old mode 100755 new mode 100644 index 0486b6c5..5b995f28 --- a/apps/system/migrations/0001_initial.py +++ b/apps/system/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.12 on 2022-04-11 03:20 +# Generated by Django 3.2.12 on 2022-06-06 07:58 from django.conf import settings import django.contrib.auth.models @@ -34,8 +34,9 @@ class Migration(migrations.Migration): ('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.CharField(default='employee', max_length=10, verbose_name='账号类型')), ('name', models.CharField(blank=True, max_length=20, null=True, verbose_name='姓名')), - ('phone', models.CharField(blank=True, max_length=11, null=True, unique=True, verbose_name='手机号')), + ('phone', models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号')), ('avatar', models.CharField(blank=True, default='/media/default/avatar.png', max_length=100, null=True, verbose_name='头像')), ], options={ @@ -55,7 +56,7 @@ class Migration(migrations.Migration): ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), ('name', models.CharField(max_length=60, verbose_name='名称')), - ('type', models.PositiveSmallIntegerField(choices=[(10, '公司'), (20, '部门')], default=20, verbose_name='类型')), + ('type', models.CharField(default='dept', max_length=20, verbose_name='类型')), ('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序标记')), ('third_info', models.JSONField(default=dict, verbose_name='三方系统信息')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dept_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), @@ -94,11 +95,14 @@ class Migration(migrations.Migration): ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('name', models.CharField(max_length=32, unique=True, verbose_name='名称')), - ('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='岗位标识')), + ('name', models.CharField(max_length=32, verbose_name='名称')), + ('code', models.CharField(blank=True, max_length=32, null=True, verbose_name='岗位标识')), ('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')), - ('data_range', models.PositiveSmallIntegerField(choices=[(10, '全部'), (30, '同级及以下'), (40, '本级及以下'), (50, '本级'), (60, '仅本人')], default=40, verbose_name='数据权限范围')), + ('min_hour', models.PositiveSmallIntegerField(default=0, verbose_name='最小在岗时间')), + ('max_hour', models.PositiveSmallIntegerField(default=12, verbose_name='最长在岗时间')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.post', verbose_name='父')), + ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), ], options={ 'verbose_name': '职位/岗位', @@ -132,8 +136,8 @@ class Migration(migrations.Migration): ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('name', models.CharField(max_length=32, unique=True, verbose_name='角色')), - ('code', models.CharField(blank=True, max_length=32, null=True, unique=True, verbose_name='角色标识')), + ('name', models.CharField(max_length=32, verbose_name='名称')), + ('code', models.CharField(blank=True, max_length=32, null=True, verbose_name='角色标识')), ('description', models.CharField(blank=True, max_length=50, null=True, verbose_name='描述')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='role_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), ('perms', models.ManyToManyField(blank=True, related_name='role_perms', to='system.Permission', verbose_name='功能权限')), @@ -145,15 +149,20 @@ class Migration(migrations.Migration): 'ordering': ['code'], }, ), - migrations.AddField( - model_name='post', - name='roles', - field=models.ManyToManyField(related_name='post_roles', to='system.Role', verbose_name='关联角色'), - ), - migrations.AddField( - model_name='post', - name='update_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='post_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人'), + migrations.CreateModel( + name='PostRole', + fields=[ + ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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='删除标记')), + ('data_range', models.PositiveSmallIntegerField(choices=[(10, '全部'), (30, '同级及以下'), (40, '本级及以下'), (50, '本级'), (60, '仅本人')], default=40, verbose_name='数据权限范围')), + ('post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='system.post', verbose_name='关联岗位')), + ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='system.role', verbose_name='关联角色')), + ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='File', @@ -184,7 +193,7 @@ class Migration(migrations.Migration): ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), ('name', models.CharField(max_length=30, verbose_name='名称')), - ('code', models.CharField(max_length=30, unique=True, verbose_name='标识')), + ('code', models.CharField(max_length=30, verbose_name='标识')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dicttype', verbose_name='父')), ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dicttype_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), @@ -220,6 +229,11 @@ class Migration(migrations.Migration): name='posts', field=models.ManyToManyField(related_name='user_posts', through='system.UserPost', to='system.Post'), ), + migrations.AddField( + model_name='user', + name='roles', + field=models.ManyToManyField(to='system.Role', verbose_name='关联角色'), + ), migrations.AddField( model_name='user', name='superior', @@ -236,7 +250,7 @@ class Migration(migrations.Migration): field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions'), ), migrations.CreateModel( - name='Dict', + name='Dictionary', fields=[ ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), @@ -248,10 +262,10 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True, verbose_name='描述')), ('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')), ('is_used', models.BooleanField(default=True, verbose_name='是否有效')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dict', verbose_name='父')), + ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dictionary_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dictionary', verbose_name='父')), ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dicttype', verbose_name='类型')), - ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dict_update_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='dictionary_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), ], options={ 'verbose_name': '字典', diff --git a/apps/system/migrations/0002_auto_20220411_2148.py b/apps/system/migrations/0002_auto_20220411_2148.py deleted file mode 100755 index 53201cf7..00000000 --- a/apps/system/migrations/0002_auto_20220411_2148.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-11 13:48 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('system', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='dicttype', - name='code', - field=models.CharField(max_length=30, verbose_name='标识'), - ), - migrations.AlterField( - model_name='post', - name='code', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='岗位标识'), - ), - migrations.AlterField( - model_name='post', - name='name', - field=models.CharField(max_length=32, verbose_name='名称'), - ), - migrations.AlterField( - model_name='role', - name='code', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='角色标识'), - ), - migrations.AlterField( - model_name='role', - name='name', - field=models.CharField(max_length=32, verbose_name='名称'), - ), - migrations.AlterField( - model_name='user', - name='phone', - field=models.CharField(blank=True, max_length=11, null=True, verbose_name='手机号'), - ), - ] diff --git a/apps/system/migrations/0003_remove_user_phone.py b/apps/system/migrations/0003_remove_user_phone.py deleted file mode 100755 index 5903b6fc..00000000 --- a/apps/system/migrations/0003_remove_user_phone.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-12 05:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('system', '0002_auto_20220411_2148'), - ] - - operations = [ - migrations.RemoveField( - model_name='user', - name='phone', - ), - ] diff --git a/apps/system/migrations/0004_auto_20220421_1511.py b/apps/system/migrations/0004_auto_20220421_1511.py deleted file mode 100755 index bec49b2f..00000000 --- a/apps/system/migrations/0004_auto_20220421_1511.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.12 on 2022-04-21 07:11 - -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 = [ - ('system', '0003_remove_user_phone'), - ] - - operations = [ - migrations.CreateModel( - name='Dictionary', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, primary_key=True, serialize=False, verbose_name='主键ID')), - ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), - ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), - ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('name', models.CharField(max_length=60, verbose_name='名称')), - ('value', models.CharField(max_length=10, verbose_name='值')), - ('code', models.CharField(blank=True, max_length=30, null=True, verbose_name='标识')), - ('description', models.TextField(blank=True, null=True, verbose_name='描述')), - ('sort', models.PositiveSmallIntegerField(default=1, verbose_name='排序')), - ('is_used', models.BooleanField(default=True, verbose_name='是否有效')), - ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dictionary_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dictionary', verbose_name='父')), - ('type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='system.dicttype', verbose_name='类型')), - ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dictionary_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), - ], - options={ - 'verbose_name': '字典', - 'verbose_name_plural': '字典', - 'ordering': ['sort'], - 'unique_together': {('name', 'is_used', 'type')}, - }, - ), - migrations.DeleteModel( - name='Dict', - ), - ] diff --git a/apps/system/migrations/__init__.py b/apps/system/migrations/__init__.py old mode 100755 new mode 100644 diff --git a/apps/system/models.py b/apps/system/models.py index 2755ce1f..6160f89b 100755 --- a/apps/system/models.py +++ b/apps/system/models.py @@ -1,8 +1,17 @@ +import enum from django.db import models from django.contrib.auth.models import AbstractUser from apps.utils.models import CommonAModel, CommonBModel, BaseModel +class DataFilter(models.IntegerChoices): + ALL = 10, '全部' + SAMELEVE_AND_BELOW = 30, '同级及以下' + THISLEVEL_AND_BELOW = 40, '本级及以下' + THISLEVEL = 50, '本级' + MYSELF = 60, '仅本人' + + class Permission(BaseModel): """ 功能权限:目录,菜单,按钮 @@ -35,14 +44,8 @@ class Dept(CommonAModel): """ 部门 """ - DEPT_TYPE_COMPANY = 10 - DEPT_TYPE_DEPT = 20 - dept_type_choices = ( - (DEPT_TYPE_COMPANY, '公司'), - (DEPT_TYPE_DEPT, '部门') - ) name = models.CharField('名称', max_length=60) - type = models.PositiveSmallIntegerField('类型', choices=dept_type_choices, default=20) + type = models.CharField('类型', max_length=20, default='dept') parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='父') sort = models.PositiveSmallIntegerField('排序标记', default=1) @@ -82,22 +85,10 @@ class Post(CommonAModel): name = models.CharField('名称', max_length=32) code = models.CharField('岗位标识', max_length=32, null=True, blank=True) description = models.CharField('描述', max_length=50, blank=True, null=True) - roles = models.ManyToManyField(Role, verbose_name='关联角色', related_name='post_roles') - POST_DATA_ALL = 10 - POST_DATA_CUSTOM = 20 - POST_DATA_SAMELEVE_AND_BELOW = 30 - POST_DATA_THISLEVEL_AND_BELOW = 40 - POST_DATA_THISLEVEL = 50 - POST_DATA_MYSELF = 60 - data_type_choices = ( - (POST_DATA_ALL, '全部'), - (POST_DATA_SAMELEVE_AND_BELOW, '同级及以下'), - (POST_DATA_THISLEVEL_AND_BELOW, '本级及以下'), - (POST_DATA_THISLEVEL, '本级'), - (POST_DATA_MYSELF, '仅本人') - ) - data_range = models.PositiveSmallIntegerField('数据权限范围', choices=data_type_choices, - default=POST_DATA_THISLEVEL_AND_BELOW) + parent = models.ForeignKey('self', null=True, blank=True, + on_delete=models.SET_NULL, verbose_name='父') + min_hour = models.PositiveSmallIntegerField('最小在岗时间', default=0) + max_hour = models.PositiveSmallIntegerField('最长在岗时间', default=12) class Meta: verbose_name = '职位/岗位' @@ -108,17 +99,32 @@ class Post(CommonAModel): return self.name +class PostRole(BaseModel): + """ + 岗位角色关系 + """ + data_range = models.PositiveSmallIntegerField('数据权限范围', choices=DataFilter.choices, + default=DataFilter.THISLEVEL_AND_BELOW) + post = models.ForeignKey(Post, verbose_name='关联岗位', + on_delete=models.CASCADE, null=True, blank=True) + role = models.ForeignKey(Role, verbose_name='关联角色', + on_delete=models.CASCADE, null=True, blank=True) + + class User(AbstractUser, CommonBModel): """ 用户 """ + type = models.CharField('账号类型', max_length=10, default='employee') name = models.CharField('姓名', max_length=20, null=True, blank=True) + phone = models.CharField('手机号', max_length=11, null=True, blank=True) avatar = models.CharField( '头像', default='/media/default/avatar.png', max_length=100, null=True, blank=True) superior = models.ForeignKey( 'self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name='上级主管') posts = models.ManyToManyField(Post, through='system.userpost', related_name='user_posts') depts = models.ManyToManyField(Dept, through='system.userpost') + roles = models.ManyToManyField(Role, verbose_name='关联角色') class Meta: verbose_name = '用户信息' diff --git a/apps/system/serializers.py b/apps/system/serializers.py index 3fb8b457..762cb31a 100755 --- a/apps/system/serializers.py +++ b/apps/system/serializers.py @@ -5,7 +5,7 @@ from django_celery_results.models import TaskResult from apps.system.errors import USERNAME_EXIST from apps.utils.serializers import CustomModelSerializer from apps.utils.constants import EXCLUDE_FIELDS, EXCLUDE_FIELDS_BASE -from .models import (Dictionary, DictType, File, Dept, Permission, Post, +from .models import (Dictionary, DictType, File, Dept, Permission, Post, PostRole, Role, User, UserPost) from rest_framework.exceptions import ParseError from django.db import transaction @@ -224,9 +224,6 @@ class DeptSerializer(CustomModelSerializer): """ 组织架构序列化 """ - type = serializers.ChoiceField( - choices=Dept.dept_type_choices, default='部门') - class Meta: model = Dept fields = '__all__' @@ -367,6 +364,26 @@ class UserPostCreateSerializer(CustomModelSerializer): exclude = EXCLUDE_FIELDS_BASE +class PostRoleSerializer(CustomModelSerializer): + """ + 岗位-角色序列化 + """ + post_ = PostSimpleSerializer(source='post', read_only=True) + role_ = RoleSimpleSerializer(source='role', read_only=True) + + class Meta: + model = PostRole + fields = '__all__' + + +class PostRoleCreateSerializer(CustomModelSerializer): + """ + 岗位-角色创建序列化 + """ + class Meta: + model = PostRole + exclude = EXCLUDE_FIELDS_BASE + class UserInfoSerializer(CustomModelSerializer): posts_ = UserPostSerializer(source='post', read_only=True) diff --git a/apps/system/urls.py b/apps/system/urls.py index 927eede4..aa19a1a7 100755 --- a/apps/system/urls.py +++ b/apps/system/urls.py @@ -1,5 +1,5 @@ from django.urls import path, include -from .views import FileViewSet, PTaskViewSet, PTaskResultViewSet, TaskList, \ +from .views import FileViewSet, PTaskViewSet, PTaskResultViewSet, PostRoleViewSet, TaskList, \ UserPostViewSet, UserViewSet, DeptViewSet, \ PermissionViewSet, RoleViewSet, PostViewSet, \ DictTypeViewSet, DictViewSet @@ -21,6 +21,7 @@ router.register('ptask_result', PTaskResultViewSet, basename="ptask_result") # router.register('qschedule', QScheduleViewSet, basename="qschedule") # router.register('qtask_result', QTaskResultViewSet, basename="qtask_result") router.register('user_post', UserPostViewSet, basename='user_post') +router.register('post_role', PostRoleViewSet, basename='post_role') router2 = routers.DefaultRouter() router2.register('file', FileViewSet, basename='file') diff --git a/apps/system/views.py b/apps/system/views.py index fc9cd2d4..606028cd 100755 --- a/apps/system/views.py +++ b/apps/system/views.py @@ -22,12 +22,12 @@ from apps.utils.permission import ALL_PERMS, get_user_perms_map from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet from server.celery import app as celery_app from .filters import UserFilter -from .models import (Dept, Dictionary, DictType, File, Permission, Post, Role, User, +from .models import (Dept, Dictionary, DictType, File, Permission, Post, PostRole, Role, User, UserPost) from .serializers import (DeptCreateUpdateSerializer, DeptSerializer, DictCreateUpdateSerializer, DictSerializer, DictTypeCreateUpdateSerializer, DictTypeSerializer, FileSerializer, PasswordChangeSerializer, PermissionCreateUpdateSerializer, - PermissionSerializer, PostCreateUpdateSerializer, PostSerializer, + PermissionSerializer, PostCreateUpdateSerializer, PostRoleCreateSerializer, PostRoleSerializer, PostSerializer, PTaskSerializer, PTaskCreateUpdateSerializer, PTaskResultSerializer, RoleCreateUpdateSerializer, RoleSerializer, TaskRunSerializer, UserCreateSerializer, UserListSerializer, UserPostCreateSerializer, @@ -305,7 +305,7 @@ class DeptViewSet(CustomModelViewSet): 部门-增删改查 """ - queryset = Dept.objects.all() + queryset = Dept.objects.filter(type__in=['dept', 'company']) serializer_class = DeptSerializer create_serializer_class = DeptCreateUpdateSerializer update_serializer_class = DeptCreateUpdateSerializer @@ -327,6 +327,18 @@ class RoleViewSet(CustomModelViewSet): search_fields = ['name', 'code'] +class PostRoleViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): + """岗位/角色关系 + + 岗位/角色关系 + """ + perms_map = {'get': '*', 'post': 'post:update', 'delete': 'post:update'} + queryset = PostRole.objects.select_related('post', 'role').all() + serializer_class = PostRoleSerializer + create_serializer_class = PostRoleCreateSerializer + filterset_fields = ['post', 'role'] + + class UserPostViewSet(CreateModelMixin, DestroyModelMixin, ListModelMixin, CustomGenericViewSet): """用户/岗位关系 diff --git a/apps/third/migrations/0001_initial.py b/apps/third/migrations/0001_initial.py deleted file mode 100644 index e9ceeb61..00000000 --- a/apps/third/migrations/0001_initial.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 3.2.12 on 2022-05-09 01:05 - -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('am', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='TDevice', - fields=[ - ('id', models.CharField(editable=False, help_text='主键ID', max_length=20, 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.PositiveSmallIntegerField(choices=[(10, '定位基站'), (20, '定位信标'), (30, '定位标签'), (40, 'aoa引擎'), (50, '音响'), (60, '视频通道'), (70, '闸机通道'), (80, '面板机')], verbose_name='设备类型')), - ('code', models.CharField(max_length=20, verbose_name='设备唯一标识')), - ('location', models.JSONField(blank=True, default=dict, verbose_name='位置信息')), - ('third_info', models.JSONField(blank=True, default=dict, verbose_name='三方信息')), - ('area', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='am.area', verbose_name='所在区')), - ('areas', models.ManyToManyField(related_name='tareas', to='am.Area', verbose_name='覆盖区')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/apps/third/tapis.py b/apps/third/tapis.py index 6a16bde8..7f8d0b8b 100755 --- a/apps/third/tapis.py +++ b/apps/third/tapis.py @@ -95,6 +95,14 @@ xxapis = { "ibeacon_list": { "url": "/api/devicesV3/ibeacons", "method": "post" + }, + "rail_create": { + "url": '/api/railsV2/add', + "method": "post" + }, + "rail_update": { + "url": '/api/railsV2/edit', + "method": "post" } } diff --git a/apps/utils/dahua.py b/apps/utils/dahua.py index 24ec1a3b..b8bbbba3 100755 --- a/apps/utils/dahua.py +++ b/apps/utils/dahua.py @@ -7,6 +7,7 @@ from rest_framework.exceptions import APIException, ParseError from apps.utils.errors import DH_REQUEST_ERROR from apps.utils.tools import print_roundtrip + requests.packages.urllib3.disable_warnings() @@ -69,6 +70,8 @@ class DhClient: def request(self, url: str, method: str, params=dict(), json=dict(), timeout=10, file_path_rela=None, raise_exception=True): + if self is None: + raise ParseError('大华对接未启用') if self.isGetingToken: req_num = 0 while True: diff --git a/apps/utils/permission.py b/apps/utils/permission.py index c3ea74f1..46f42652 100755 --- a/apps/utils/permission.py +++ b/apps/utils/permission.py @@ -1,7 +1,7 @@ from django.core.cache import cache from rest_framework.permissions import BasePermission from apps.utils.queryset import get_child_queryset2 -from apps.system.models import Dept, Permission, Post, UserPost +from apps.system.models import DataFilter, Dept, Permission, Post, PostRole, UserPost from django.db.models.query import QuerySet ALL_PERMS = [ @@ -11,7 +11,7 @@ ALL_PERMS = [ def get_user_perms_map(user): """ - 获取权限字典,可用redis存取 + 获取权限字典,可用redis存取(包括功能和数据权限) """ user_perms_map = {} if user.is_superuser: @@ -20,16 +20,19 @@ def get_user_perms_map(user): objs = UserPost.objects.filter(user=user) for i in objs: dept_id = str(i.dept.id) - if i.post.roles: # 岗位下有角色 - for perm in Permission.objects.filter(role__perms__in=i.post.roles): + for pr in PostRole.objects.filter(post=i.post).exists(): + """ + 岗位角色 + """ + for perm in Permission.objects.filter(role__perms=pr.role): if perm.codes: for code in perm.codes: if code in user_perms_map: data_range = user_perms_map[code].get(dept_id, -1) - if i.post.data_range < data_range: + if pr.data_range < data_range: user_perms_map[code][dept_id] = data_range else: - user_perms_map[code] = {dept_id: i.post.data_range} + user_perms_map[code] = {dept_id: pr.data_range} cache.set('perms_' + user.id, user_perms_map, 60*60) return user_perms_map @@ -109,20 +112,20 @@ class RbacDataMixin: new_queryset = queryset.none() for dept_id, data_range in user_perms_map[action_str].items: dept = Dept.objects.get(id=dept_id) - if data_range == Post.POST_DATA_ALL: + if data_range == DataFilter.ALL: return queryset - elif data_range == Post.POST_DATA_SAMELEVE_AND_BELOW: + elif data_range == DataFilter.SAMELEVE_AND_BELOW: if dept.parent: belong_depts = get_child_queryset2(dept.parent) else: belong_depts = get_child_queryset2(dept) queryset = queryset.filter(belong_dept__in=belong_depts) - elif data_range == Post.POST_DATA_THISLEVEL_AND_BELOW: + elif data_range == DataFilter.THISLEVEL_AND_BELOW: belong_depts = get_child_queryset2(dept) queryset = queryset.filter(belong_dept__in=belong_depts) - elif data_range == Post.POST_DATA_THISLEVEL: + elif data_range == DataFilter.THISLEVEL: queryset = queryset.filter(belong_dept=dept) - elif data_range == Post.POST_DATA_THISLEVEL: + elif data_range == DataFilter.MYSELF: queryset = queryset.filter(create_by=user) new_queryset = new_queryset | queryset return new_queryset diff --git a/apps/utils/speaker.py b/apps/utils/speaker.py index 3703b559..720d0635 100644 --- a/apps/utils/speaker.py +++ b/apps/utils/speaker.py @@ -2,12 +2,12 @@ import time from threading import Thread import requests -# from django.conf import settings -from server import settings from rest_framework.exceptions import APIException, ParseError +from django.conf import settings from apps.utils.errors import SP_REQUEST_ERROR from apps.utils.tools import print_roundtrip + requests.packages.urllib3.disable_warnings() @@ -69,6 +69,8 @@ class SpClient: def request(self, url: str, method: str, params=dict(), json=dict(), timeout=10, file_path_rela=None, raise_exception=True): + if self is None: + raise ParseError('音响对接未启用') if self.isGetingToken: req_num = 0 while True: diff --git a/apps/utils/viewsets.py b/apps/utils/viewsets.py index 4ed72006..7fb721a9 100755 --- a/apps/utils/viewsets.py +++ b/apps/utils/viewsets.py @@ -8,7 +8,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet -from apps.system.models import Dept, Post +from apps.system.models import DataFilter, Dept from apps.utils.errors import PKS_ERROR from apps.utils.mixins import CustomDestoryModelMixin, MyLoggingMixin from apps.utils.permission import ALL_PERMS, RbacPermission, get_user_perms_map @@ -78,20 +78,20 @@ class CustomGenericViewSet(MyLoggingMixin, GenericViewSet): new_queryset = queryset.none() for dept_id, data_range in user_perms_map[action_str].items: dept = Dept.objects.get(id=dept_id) - if data_range == Post.POST_DATA_ALL: + if data_range == DataFilter.ALL: return queryset - elif data_range == Post.POST_DATA_SAMELEVE_AND_BELOW: + elif data_range == DataFilter.SAMELEVE_AND_BELOW: if dept.parent: belong_depts = get_child_queryset2(dept.parent) else: belong_depts = get_child_queryset2(dept) queryset = queryset.filter(belong_dept__in=belong_depts) - elif data_range == Post.POST_DATA_THISLEVEL_AND_BELOW: + elif data_range == DataFilter.THISLEVEL_AND_BELOW: belong_depts = get_child_queryset2(dept) queryset = queryset.filter(belong_dept__in=belong_depts) - elif data_range == Post.POST_DATA_THISLEVEL: + elif data_range == DataFilter.THISLEVEL: queryset = queryset.filter(belong_dept=dept) - elif data_range == Post.POST_DATA_THISLEVEL: + elif data_range == DataFilter.MYSELF: queryset = queryset.filter(create_by=user) new_queryset = new_queryset | queryset return new_queryset diff --git a/apps/utils/xunxi.py b/apps/utils/xunxi.py index 57a062f9..e1dd1a24 100755 --- a/apps/utils/xunxi.py +++ b/apps/utils/xunxi.py @@ -1,10 +1,13 @@ +import time from threading import Thread + import requests +from django.conf import settings +from rest_framework.exceptions import APIException, ParseError + from apps.utils.errors import XX_REQUEST_ERROR from apps.utils.tools import print_roundtrip -from django.conf import settings -import time -from rest_framework.exceptions import APIException, ParseError + requests.packages.urllib3.disable_warnings() @@ -58,6 +61,8 @@ class XxClient: self.t.join() def request(self, url: str, method: str = 'post', params=dict(), json=dict(), timeout=4, raise_exception=True): + if self is None: + raise ParseError('寻息对接未启用') params['accessToken'] = self.token json['username'] = self.username json['buildId'] = settings.XX_BUILDID diff --git a/apps/third/migrations/__init__.py b/apps/vm/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from apps/third/migrations/__init__.py rename to apps/vm/__init__.py diff --git a/apps/vm/admin.py b/apps/vm/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/vm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/vm/apps.py b/apps/vm/apps.py new file mode 100644 index 00000000..26f4fdb1 --- /dev/null +++ b/apps/vm/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class VmConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.vm' + verbose_name = "访客管理" diff --git a/apps/vm/models.py b/apps/vm/models.py new file mode 100644 index 00000000..c1e241ed --- /dev/null +++ b/apps/vm/models.py @@ -0,0 +1,31 @@ +from django.db import models +from apps.hrm.models import Employee +from apps.utils.models import CommonAModel, CommonBModel, BaseModel +from apps.system.models import User + + +# Create your models here. +class Visit(CommonBModel): + """ + 访问项目 + """ + VISIT_PURPOSE_CHOICES = ( + (10, '参观'), + (20, '拜访'), + (30, '面试'), + (40, '开会') + ) + purpose = models.PositiveSmallIntegerField('来访事由') + description = models.CharField('来访详述', max_length=200) + visit_time = models.DateTimeField('来访时间') + leave_time = models.DateTimeField('离开时间') + receptionist = models.ForeignKey(User, verbose_name='接待人', on_delete=models.CASCADE) + + +class VisitPeople(BaseModel): + """ + 访客项目人员 + """ + visit = models.ForeignKey(Visit, verbose_name='关联访问项目', on_delete=models.CASCADE) + visitor = models.ForeignKey(Employee, verbose_name='访客', on_delete=models.CASCADE) + is_manager = models.BooleanField('是否主访人', default=False) diff --git a/apps/vm/tests.py b/apps/vm/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/vm/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/vm/views.py b/apps/vm/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/apps/vm/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/wf/migrations/0001_initial.py b/apps/wf/migrations/0001_initial.py index 62bab698..670d65d8 100644 --- a/apps/wf/migrations/0001_initial.py +++ b/apps/wf/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.12 on 2022-05-10 02:06 +# Generated by Django 3.2.12 on 2022-06-06 07:58 from django.conf import settings from django.db import migrations, models @@ -27,12 +27,14 @@ class Migration(migrations.Migration): ('sort', models.IntegerField(default=0, help_text='用于工单步骤接口时,step上状态的顺序(因为存在网状情况,所以需要人为设定顺序),值越小越靠前', verbose_name='状态顺序')), ('type', models.IntegerField(choices=[(0, '普通'), (1, '开始'), (2, '结束')], default=0, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)', verbose_name='状态类型')), ('enable_retreat', models.BooleanField(default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态', verbose_name='允许撤回')), - ('participant_type', models.IntegerField(blank=True, 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='参与者类型')), + ('enable_deliver', models.BooleanField(default=False, verbose_name='允许转交')), + ('participant_type', models.IntegerField(blank=True, choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=1, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by', verbose_name='参与者类型')), ('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表\\部门id\\角色id\\变量(create_by,create_by_tl)\\脚本记录的id等,包含子工作流的需要设置处理人为loonrobot', verbose_name='参与者')), ('state_fields', models.JSONField(default=dict, help_text='json格式字典存储,包括读写属性1:只读,2:必填,3:可选, 4:隐藏 示例:{"create_time":1,"title":2, "sn":1}, 内置特殊字段participant_info.participant_name:当前处理人信息(部门名称、角色名称),state.state_name:当前状态的状态名,workflow.workflow_name:工作流名称', verbose_name='表单字段')), ('distribute_type', models.IntegerField(choices=[(1, '主动接单'), (2, '直接处理'), (3, '随机分配'), (4, '全部处理')], default=1, help_text='1.主动接单(如果当前处理人实际为多人的时候,需要先接单才能处理) 2.直接处理(即使当前处理人实际为多人,也可以直接处理) 3.随机分配(如果实际为多人,则系统会随机分配给其中一个人) 4.全部处理(要求所有参与人都要处理一遍,才能进入下一步)', verbose_name='分配方式')), ('filter_policy', models.IntegerField(choices=[(0, '无'), (1, '和工单同属一及上级部门'), (2, '和创建人同属一及上级部门'), (3, '和上步处理人同属一及上级部门')], default=0, verbose_name='参与人过滤策略')), ('participant_cc', models.JSONField(blank=True, default=list, help_text='抄送给(userid列表)', verbose_name='抄送给')), + ('on_reach_func', models.CharField(blank=True, max_length=100, null=True, verbose_name='到达时调用方法')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='state_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='state_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), ], @@ -52,7 +54,7 @@ class Migration(migrations.Migration): ('ticket_data', models.JSONField(default=dict, help_text='工单自定义字段内容', verbose_name='工单数据')), ('in_add_node', models.BooleanField(default=False, help_text='是否处于加签状态下', verbose_name='加签状态中')), ('script_run_last_result', models.BooleanField(default=True, verbose_name='脚本最后一次执行结果')), - ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型')), + ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人', verbose_name='当前处理人类型')), ('participant', models.JSONField(blank=True, default=list, help_text='可以为空(无处理人的情况,如结束状态)、userid、userid列表', verbose_name='当前处理人')), ('act_state', models.IntegerField(choices=[(0, '草稿中'), (1, '进行中'), (2, '被退回'), (3, '被撤回'), (4, '已完成'), (5, '已关闭')], default=1, help_text='当前工单的进行状态', verbose_name='进行状态')), ('multi_all_person', models.JSONField(blank=True, default=dict, help_text='需要当前状态处理人全部处理时实际的处理结果,json格式', verbose_name='全部处理的结果')), @@ -102,6 +104,7 @@ class Migration(migrations.Migration): ('condition_expression', models.JSONField(default=list, help_text='流转条件表达式,根据表达式中的条件来确定流转的下个状态,格式为[{"expression":"{days} > 3 and {days}<10", "target_state":11}] 其中{}用于填充工单的字段key,运算时会换算成实际的值,当符合条件下个状态将变为target_state_id中的值,表达式只支持简单的运算或datetime/time运算.loonflow会以首次匹配成功的条件为准,所以多个条件不要有冲突', max_length=1000, verbose_name='条件表达式')), ('attribute_type', models.IntegerField(choices=[(1, '同意'), (2, '拒绝'), (3, '其他')], default=1, help_text='属性类型,1.同意,2.拒绝,3.其他', verbose_name='属性类型')), ('field_require_check', models.BooleanField(default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容', verbose_name='是否校验必填项')), + ('on_submit_func', models.CharField(blank=True, max_length=100, null=True, verbose_name='提交操作调用方法')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transition_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), ('destination_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dstate_transition', to='wf.state', verbose_name='目的状态')), ('source_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sstate_transition', to='wf.state', verbose_name='源状态')), @@ -120,7 +123,7 @@ class Migration(migrations.Migration): ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), ('suggestion', models.CharField(blank=True, default='', max_length=10000, verbose_name='处理意见')), - ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (4, '角色'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人等', verbose_name='处理人类型')), + ('participant_type', models.IntegerField(choices=[(0, '无处理人'), (1, '个人'), (2, '多人'), (3, '部门'), (4, '角色'), (10, '岗位'), (6, '脚本'), (7, '工单的字段'), (9, '代码获取')], default=0, help_text='0.无处理人,1.个人,2.多人等', verbose_name='处理人类型')), ('participant_str', models.CharField(blank=True, help_text='非人工处理的处理人相关信息', max_length=200, null=True, verbose_name='处理人')), ('ticket_data', models.JSONField(blank=True, default=dict, help_text='可以用于记录当前表单数据,json格式', verbose_name='工单数据')), ('intervene_type', models.IntegerField(choices=[(0, '正常处理'), (1, '转交'), (2, '加签'), (3, '加签处理完成'), (4, '接单'), (5, '评论'), (6, '删除'), (7, '强制关闭'), (8, '强制修改状态'), (9, 'hook操作'), (10, '撤回'), (11, '抄送')], default=0, help_text='流转类型', verbose_name='干预类型')), @@ -151,7 +154,7 @@ class Migration(migrations.Migration): ('create_time', models.DateTimeField(default=django.utils.timezone.now, help_text='创建时间', verbose_name='创建时间')), ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间', verbose_name='修改时间')), ('is_deleted', models.BooleanField(default=False, help_text='删除标记', verbose_name='删除标记')), - ('field_type', models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('cascader', '单选级联'), ('cascaders', '多选级联'), ('select_dg', '弹框单选'), ('select_dgs', '弹框多选'), ('textarea', '文本域'), ('file', '附件')], help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file', max_length=50, verbose_name='类型')), + ('field_type', models.CharField(choices=[('string', '字符串'), ('int', '整型'), ('float', '浮点'), ('boolean', '布尔'), ('date', '日期'), ('datetime', '日期时间'), ('radio', '单选'), ('checkbox', '多选'), ('select', '单选下拉'), ('selects', '多选下拉'), ('cascader', '单选级联'), ('cascaders', '多选级联'), ('select_dg', '弹框单选'), ('select_dgs', '弹框多选'), ('textarea', '文本域'), ('file', '附件'), ('table', '表格')], help_text='string, int, float, date, datetime, radio, checkbox, select, selects, cascader, cascaders, select_dg, select_dgs,textarea, file', max_length=50, verbose_name='类型')), ('field_key', models.CharField(help_text='字段类型请尽量特殊,避免与系统中关键字冲突', max_length=50, verbose_name='字段标识')), ('field_name', models.CharField(max_length=50, verbose_name='字段名称')), ('sort', models.IntegerField(default=0, help_text='工单基础字段在表单中排序为:流水号0,标题20,状态id40,状态名41,创建人80,创建时间100,更新时间120.前端展示工单信息的表单可以根据这个id顺序排列', verbose_name='排序')), @@ -163,7 +166,9 @@ class Migration(migrations.Migration): ('field_choice', models.JSONField(blank=True, default=list, help_text='选项值,格式为list, 例["id":1, "name":"张三"]', verbose_name='选项值')), ('label', models.CharField(default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项', max_length=1000, verbose_name='标签')), ('is_hidden', models.BooleanField(default=False, help_text='可用于携带不需要用户查看的字段信息', verbose_name='是否隐藏')), + ('group', models.CharField(blank=True, max_length=100, null=True, verbose_name='字段分组')), ('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')), + ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='wf.customfield', verbose_name='父字段')), ('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customfield_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')), ('workflow', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wf.workflow', verbose_name='所属工作流')), ], diff --git a/apps/wf/models.py b/apps/wf/models.py index 4b7871db..4cc7e754 100755 --- a/apps/wf/models.py +++ b/apps/wf/models.py @@ -43,12 +43,14 @@ class State(CommonAModel): PARTICIPANT_TYPE_FIELD = 7 PARTICIPANT_TYPE_PARENT_FIELD = 8 PARTICIPANT_TYPE_FORMCODE = 9 + PARTICIPANT_TYPE_POST = 10 state_participanttype_choices = ( (0, '无处理人'), (PARTICIPANT_TYPE_PERSONAL, '个人'), (PARTICIPANT_TYPE_MULTI, '多人'), - # (PARTICIPANT_TYPE_DEPT, '部门'), + (PARTICIPANT_TYPE_DEPT, '部门'), (PARTICIPANT_TYPE_ROLE, '角色'), + (PARTICIPANT_TYPE_POST, '岗位'), # (PARTICIPANT_TYPE_VARIABLE, '变量'), (PARTICIPANT_TYPE_ROBOT, '脚本'), (PARTICIPANT_TYPE_FIELD, '工单的字段'), @@ -83,6 +85,7 @@ class State(CommonAModel): type = models.IntegerField('状态类型', default=0, choices=type_choices, help_text='0.普通类型 1.初始状态(用于新建工单时,获取对应的字段必填及transition信息) 2.结束状态(此状态下的工单不得再处理,即没有对应的transition)') enable_retreat = models.BooleanField('允许撤回', default=False, help_text='开启后允许工单创建人在此状态直接撤回工单到初始状态') + enable_deliver = models.BooleanField('允许转交', default=False) participant_type = models.IntegerField('参与者类型', choices=state_participanttype_choices, default=1, blank=True, help_text='0.无处理人,1.个人,2.多人,3.部门,4.角色,5.变量(支持工单创建人,创建人的leader),6.脚本,7.工单的字段内容(如表单中的"测试负责人",需要为用户名或者逗号隔开的多个用户名),8.父工单的字段内容。 初始状态请选择类型5,参与人填create_by') participant = models.JSONField('参与者', default=list, blank=True, @@ -94,6 +97,7 @@ class State(CommonAModel): 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列表)') + on_reach_func = models.CharField('到达时调用方法', max_length=100, null=True, blank=True) class Transition(CommonAModel): @@ -148,6 +152,7 @@ class Transition(CommonAModel): '属性类型', default=1, choices=attribute_type_choices, help_text='属性类型,1.同意,2.拒绝,3.其他') field_require_check = models.BooleanField( '是否校验必填项', default=True, help_text='默认在用户点击操作的时候需要校验工单表单的必填项,如果设置为否则不检查。用于如"退回"属性的操作,不需要填写表单内容') + on_submit_func = models.CharField('提交操作调用方法', max_length=100, null=True, blank=True) class CustomField(CommonAModel): @@ -168,7 +173,8 @@ class CustomField(CommonAModel): ('select_dg', '弹框单选'), ('select_dgs', '弹框多选'), ('textarea', '文本域'), - ('file', '附件') + ('file', '附件'), + ('table', '表格') ) workflow = models.ForeignKey(Workflow, on_delete=models.CASCADE, verbose_name='所属工作流') field_type = models.CharField('类型', max_length=50, choices=field_type_choices, @@ -192,6 +198,8 @@ class CustomField(CommonAModel): label = models.CharField('标签', max_length=1000, default='', help_text='处理特殊逻辑使用,比如sys_user用于获取用户作为选项') # hook = models.CharField('hook', max_length=1000, default='', help_text='获取下拉选项用于动态选项值') is_hidden = models.BooleanField('是否隐藏', default=False, help_text='可用于携带不需要用户查看的字段信息') + group = models.CharField('字段分组', max_length=100, null=True, blank=True) + parent = models.ForeignKey('self', verbose_name='父字段', on_delete=models.SET_NULL, null=True, blank=True) class Ticket(CommonAModel): diff --git a/apps/wf/serializers.py b/apps/wf/serializers.py index be930900..0744325e 100755 --- a/apps/wf/serializers.py +++ b/apps/wf/serializers.py @@ -26,7 +26,7 @@ class WorkflowSimpleSerializer(serializers.ModelSerializer): class StateSimpleSerializer(serializers.ModelSerializer): class Meta: model = State - fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat'] + fields = ['id', 'name', 'type', 'distribute_type', 'enable_retreat', 'enable_deliver'] class TransitionSerializer(serializers.ModelSerializer): @@ -199,6 +199,11 @@ class TicketHandleSerializer(serializers.Serializer): suggestion = serializers.CharField(label="处理意见", required=False, allow_blank=True) +class TicketDeliverSerializer(serializers.Serializer): + target_user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all(), label='转交人') + suggestion = serializers.CharField(label="转交原因", required=False) + + class TicketRetreatSerializer(serializers.Serializer): suggestion = serializers.CharField(label="撤回原因", required=False) diff --git a/apps/wf/services.py b/apps/wf/services.py index 029ef918..406b0c27 100755 --- a/apps/wf/services.py +++ b/apps/wf/services.py @@ -1,3 +1,4 @@ +import importlib from apps.wf.serializers import TicketSimpleSerializer from apps.system.models import User from apps.wf.models import CustomField, State, Ticket, TicketFlow, Transition, Workflow @@ -157,11 +158,17 @@ class WfService(object): destination_participant_type, destination_participant = state.participant_type, state.participant if destination_participant_type == State.PARTICIPANT_TYPE_FIELD: destination_participant = new_ticket_data.get(destination_participant, 0) if destination_participant \ - in new_ticket_data else Ticket.ticket_data.get(destination_participant, 0) + in new_ticket_data else Ticket.ticket_data.get(destination_participant, 0) elif destination_participant_type == State.PARTICIPANT_TYPE_FORMCODE: # 代码获取 - destination_participant = getattr(GetParticipants, destination_participant)( - state=state, ticket=ticket, new_ticket_data=new_ticket_data, hander=handler) + # if '.' in destination_participant: + module, func = destination_participant.rsplit(".", 1) + m = importlib.import_module(module) + f = getattr(m, func) + destination_participant = f(state=state, ticket=ticket, new_ticket_data=new_ticket_data, hander=handler) + # else: + # destination_participant = getattr(GetParticipants, destination_participant)( + # state=state, ticket=ticket, new_ticket_data=new_ticket_data, hander=handler) elif destination_participant_type == State.PARTICIPANT_TYPE_DEPT: # 部门 destination_participant = list(User.objects.filter( @@ -269,6 +276,13 @@ class WfService(object): source_state = ticket.state source_ticket_data = ticket.ticket_data + # 提交时可能进行的操作 + if transition.on_submit_func: + module, func = transition.on_submit_func.rsplit(".", 1) + m = importlib.import_module(module) + f = getattr(m, func) + f(ticket=ticket, transition=transition, new_ticket_data=new_ticket_data) + # 校验处理权限 if not handler or not created: # 没有处理人意味着系统触发不校验处理权限 result = WfService.ticket_handle_permission_check(ticket, handler) @@ -351,8 +365,35 @@ class WfService(object): participant_type=0, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_CC, participant=None, participant_cc=destination_state.participant_cc) - # 如果目标状态是脚本则执行 - if destination_state.participant_type == State.PARTICIPANT_TYPE_ROBOT: - getattr(HandleScripts, destination_state.participant)(ticket) - return ticket + + @classmethod + def task_ticket(cls, ticket: Ticket): + """ + 执行任务(自定义任务或通知) + """ + # 如果目标状态是脚本则异步执行 + state = ticket.state + if state.participant_type == State.PARTICIPANT_TYPE_ROBOT: + module, func = state.participant.rsplit(".", 1) + m = importlib.import_module(module) + f = getattr(m, func) + ticket.script_run_last_result = False + ticket.save() + f.delay(ticket_id=ticket.id, script_str=state.participant) # 里面要加入回调才能继续流转 + + # 如果目标状态有func,由func执行额外操作(比如发送通知) + if state.on_reach_func: + module, func = state.func.rsplit(".", 1) + m = importlib.import_module(module) + f = getattr(m, func) + f.delay(ticket_id=ticket.id) # 不用加入回调 + else: + # wf默认发送通知 + last_log = TicketFlow.objects.filter(ticket=ticket).order_by('-create_time').first() + if (last_log.state != state or + last_log.intervene_type == Transition.TRANSITION_INTERVENE_TYPE_DELIVER or + ticket.in_add_node): + # 如果状态变化或是转交加签的情况再发送通知 + from tasks import send_ticket_notice + send_ticket_notice.delay(ticket_id=ticket.id) diff --git a/apps/wf/tasks.py b/apps/wf/tasks.py new file mode 100644 index 00000000..579539e2 --- /dev/null +++ b/apps/wf/tasks.py @@ -0,0 +1,15 @@ +# Create your tasks here +from __future__ import absolute_import, unicode_literals +from apps.utils.task import CustomTask +from celery import shared_task + +from apps.wf.serializers import TicketDetailSerializer + + +@shared_task(base=CustomTask) +def send_ticket_notice(ticket): + """ + 发送通知 + """ + data = TicketDetailSerializer(instance=ticket).data + \ No newline at end of file diff --git a/apps/wf/views.py b/apps/wf/views.py index 8de134be..94407daa 100755 --- a/apps/wf/views.py +++ b/apps/wf/views.py @@ -8,7 +8,7 @@ from rest_framework.mixins import CreateModelMixin, DestroyModelMixin, ListModel RetrieveModelMixin, UpdateModelMixin from apps.wf.serializers import CustomFieldCreateUpdateSerializer, CustomFieldSerializer, StateSerializer, \ TicketAddNodeEndSerializer, TicketAddNodeSerializer, TicketCloseSerializer, \ - TicketCreateSerializer, TicketDestorySerializer, TicketFlowSerializer, \ + TicketCreateSerializer, TicketDeliverSerializer, TicketDestorySerializer, TicketFlowSerializer, \ TicketHandleSerializer, TicketRetreatSerializer, \ TicketSerializer, TransitionSerializer, WorkflowSerializer, \ TicketListSerializer, TicketDetailSerializer @@ -17,7 +17,7 @@ from rest_framework.decorators import action from apps.wf.models import CustomField, Ticket, Workflow, State, Transition, TicketFlow from apps.utils.mixins import CreateUpdateCustomMixin, CreateUpdateModelAMixin from apps.wf.services import WfService -from rest_framework.exceptions import APIException +from rest_framework.exceptions import ParseError from rest_framework import status from django.db.models import Count from .scripts import GetParticipants @@ -148,14 +148,15 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R return TicketListSerializer elif self.action == 'retrieve': return TicketDetailSerializer + elif self.action == 'deliver': + return TicketDeliverSerializer return super().get_serializer_class() def filter_queryset(self, queryset): if not self.detail and not self.request.query_params.get('category', None): - raise APIException('请指定查询分类') + raise ParseError('请指定查询分类') return super().filter_queryset(queryset) - @transaction.atomic def create(self, request, *args, **kwargs): """ 新建工单 @@ -174,29 +175,30 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R for key, value in start_state.state_fields.items(): if int(value) == State.STATE_FIELD_REQUIRED: if key not in ticket_data and not ticket_data[key]: - raise APIException('字段{}必填'.format(key)) + raise ParseError('字段{}必填'.format(key)) save_ticket_data[key] = ticket_data[key] elif int(value) == State.STATE_FIELD_OPTIONAL: save_ticket_data[key] = ticket_data[key] - - ticket = serializer.save(state=start_state, - create_by=request.user, - create_time=timezone.now(), - act_state=Ticket.TICKET_ACT_STATE_DRAFT, - belong_dept=request.user.dept, - ticket_data=save_ticket_data) # 先创建出来 - # 更新title和sn - title = vdata.get('title', '') - title_template = ticket.workflow.title_template - if title_template: - all_ticket_data = {**rdata, **ticket_data} - title = title_template.format(**all_ticket_data) - sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 - ticket.sn = sn - ticket.title = title - ticket.save() - ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data, - handler=request.user, created=True) + with transaction.atomic(): + ticket = serializer.save(state=start_state, + create_by=request.user, + create_time=timezone.now(), + act_state=Ticket.TICKET_ACT_STATE_DRAFT, + belong_dept=request.user.dept, + ticket_data=save_ticket_data) # 先创建出来 + # 更新title和sn + title = vdata.get('title', '') + title_template = ticket.workflow.title_template + if title_template: + all_ticket_data = {**rdata, **ticket_data} + title = title_template.format(**all_ticket_data) + sn = WfService.get_ticket_sn(ticket.workflow) # 流水号 + ticket.sn = sn + ticket.title = title + ticket.save() + ticket = WfService.handle_ticket(ticket=ticket, transition=transition, new_ticket_data=ticket_data, + handler=request.user, created=True) + WfService.task_ticket(ticket) return Response(TicketSerializer(instance=ticket).data) @action(methods=['get'], detail=False, perms_map={'get': '*'}) @@ -212,7 +214,6 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R return Response(ret) @action(methods=['post'], detail=True, perms_map={'post': '*'}) - @transaction.atomic def handle(self, request, pk=None): """ 处理工单 @@ -223,12 +224,37 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R vdata = serializer.validated_data new_ticket_data = ticket.ticket_data new_ticket_data.update(**vdata['ticket_data']) - - ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'], - new_ticket_data=new_ticket_data, handler=request.user, - suggestion=vdata['suggestion']) + with transaction.atomic(): + ticket = WfService.handle_ticket(ticket=ticket, transition=vdata['transition'], + new_ticket_data=new_ticket_data, handler=request.user, + suggestion=vdata['suggestion']) + WfService.task_ticket(ticket) return Response(TicketSerializer(instance=ticket).data) + @action(methods=['post'], detail=True, perms_map={'post': '*'}) + def deliver(self, request, pk=None): + """ + 转交工单 + """ + ticket = self.get_object() + rdata = request.data + serializer = self.get_serializer(data=rdata) + serializer.is_valid(raise_exception=True) + vdata = serializer.validated_data # 校验之后的数据 + if not ticket.state.enable_deliver: + raise ParseError('不允许转交') + with transaction.atomic(): + ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL + ticket.participant = vdata['target_user'] + ticket.save() + TicketFlow.objects.create(ticket=ticket, state=ticket.state, + ticket_data=WfService.get_ticket_all_field_value(ticket), + suggestion=vdata['suggestion'], participant_type=State.PARTICIPANT_TYPE_PERSONAL, + intervene_type=Transition.TRANSITION_INTERVENE_TYPE_DELIVER, + participant=request.user, transition=None) + WfService.task_ticket(ticket) + return Response() + @action(methods=['get'], detail=True, perms_map={'get': '*'}) def flowsteps(self, request, pk=None): """ @@ -277,7 +303,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R participant=request.user, transition=None) return Response() else: - raise APIException('无需接单') + raise ParseError('无需接单') @action(methods=['post'], detail=True, perms_map={'post': '*'}) def retreat(self, request, pk=None): @@ -286,9 +312,9 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R """ ticket = self.get_object() if ticket.create_by != request.user: - raise APIException('非创建人不可撤回') + raise ParseError('非创建人不可撤回') if not ticket.state.enable_retreat: - raise APIException('该状态不可撤回') + raise ParseError('该状态不可撤回') start_state = WfService.get_workflow_start_state(ticket.workflow) ticket.state = start_state ticket.participant_type = State.PARTICIPANT_TYPE_PERSONAL @@ -324,6 +350,7 @@ class TicketViewSet(CreateUpdateCustomMixin, CreateModelMixin, ListModelMixin, R suggestion=suggestion, participant_type=State.PARTICIPANT_TYPE_PERSONAL, intervene_type=Transition.TRANSITION_INTERVENE_TYPE_ADD_NODE, participant=request.user, transition=None) + WfService.task_ticket(ticket) return Response() @action(methods=['post'], detail=True, perms_map={'post': '*'}, serializer_class=TicketAddNodeEndSerializer) diff --git a/server/celery.py b/server/celery.py index 16eaf672..59449c0d 100755 --- a/server/celery.py +++ b/server/celery.py @@ -5,7 +5,7 @@ from celery import Celery # set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') -app = Celery('server') +app = Celery('ehs') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. diff --git a/server/settings.py b/server/settings.py index a5ef0b4f..af438faf 100755 --- a/server/settings.py +++ b/server/settings.py @@ -55,7 +55,9 @@ INSTALLED_APPS = [ 'apps.monitor', 'apps.wf', 'apps.hrm', - 'apps.am' + 'apps.am', + 'apps.vm', + 'apps.rpm' ] MIDDLEWARE = [ @@ -141,10 +143,10 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' -STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static_collect') -STATICFILES_DIRS = ( - os.path.join(BASE_DIR, 'dist/static'), -) +STATIC_ROOT = os.path.join(BASE_DIR, 'dist/static') +# STATICFILES_DIRS = ( +# os.path.join(BASE_DIR, 'dist/static'), +# ) MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media') @@ -224,7 +226,7 @@ AUTHENTICATION_BACKENDS = ( # } # celery配置,celery正常运行必须安装redis -CELERY_BROKER_URL = "redis://127.0.0.1:6379/1" # 任务存储 +CELERY_BROKER_URL = "redis://localhost:6379/2" # 任务存储 CELERYD_MAX_TASKS_PER_CHILD = 100 # 每个worker最多执行100个任务就会被销毁,可防止内存泄露 CELERY_TIMEZONE = 'Asia/Shanghai' # 设置时区 CELERY_ENABLE_UTC = True # 启动时区设置 diff --git a/server/urls.py b/server/urls.py index caa05cb0..2dfb5da8 100755 --- a/server/urls.py +++ b/server/urls.py @@ -31,6 +31,7 @@ schema_view = get_schema_view( ), public=True, permission_classes=[], + url=settings.BASE_URL ) urlpatterns = [