Compare commits

...

4 Commits

8 changed files with 228 additions and 8 deletions

View File

@ -0,0 +1,46 @@
# Generated by Django 3.2.12 on 2026-01-23 08:03
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 = [
('wf', '0006_auto_20251215_1645'),
('system', '0006_auto_20241213_1249'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('hrm', '0025_leave'),
]
operations = [
migrations.CreateModel(
name='EmployeeTransfer',
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='删除标记')),
('is_change', models.BooleanField(default=False, verbose_name='是否跨部门调动')),
('is_promotion', models.BooleanField(default=False, verbose_name='是否晋升')),
('transfer_date', models.DateField(verbose_name='调岗日期')),
('original_slary', models.PositiveIntegerField(blank=True, null=True, verbose_name='原岗位薪资')),
('new_slary', models.PositiveIntegerField(blank=True, null=True, verbose_name='调岗后薪资')),
('content', models.TextField(blank=True, null=True, verbose_name='个人工作内容')),
('reason', models.TextField(blank=True, null=True, verbose_name='调动原因')),
('create_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employeetransfer_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hrm.employee', verbose_name='员工')),
('new_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transfer_new_dept', to='system.dept', verbose_name='新部门')),
('new_post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transfer_new_post', to='system.post', verbose_name='新岗位')),
('original_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transfer_original_dept', to='system.dept', verbose_name='原部门')),
('original_post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transfer_original_post', to='system.post', verbose_name='原岗位')),
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='transfer_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='employeetransfer_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2026-01-23 08:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0026_employeetransfer'),
]
operations = [
migrations.AlterField(
model_name='leave',
name='leave_type',
field=models.PositiveSmallIntegerField(choices=[(10, '事假'), (20, '病假'), (30, '婚假'), (40, '丧假'), (50, '公假'), (60, '工伤'), (70, '产假'), (80, '护理假'), (90, '其他')], default=10, verbose_name='请假类型'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2026-01-26 06:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0027_alter_leave_leave_type'),
]
operations = [
migrations.AlterField(
model_name='employeetransfer',
name='new_slary',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='调岗后薪资'),
),
migrations.AlterField(
model_name='employeetransfer',
name='original_slary',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='原岗位薪资'),
),
]

View File

@ -265,9 +265,29 @@ class Leave(CommonADModel):
employee = models.ForeignKey(Employee, verbose_name='员工', on_delete=models.CASCADE) employee = models.ForeignKey(Employee, verbose_name='员工', on_delete=models.CASCADE)
start_date = models.DateTimeField('开始日期') start_date = models.DateTimeField('开始日期')
end_date = models.DateTimeField('结束日期') end_date = models.DateTimeField('结束日期')
leave_type = models.PositiveSmallIntegerField('请假类型', choices=E_TYPE_CHOISE, null=True, blank=True) leave_type = models.PositiveSmallIntegerField('请假类型', choices=E_TYPE_CHOISE, default=10)
reason = models.TextField('请假事由') reason = models.TextField('请假事由')
hour = models.PositiveSmallIntegerField('请假时长', null=True, blank=True) hour = models.PositiveSmallIntegerField('请假时长', null=True, blank=True)
file = models.TextField('证明', null=True, blank=True) file = models.TextField('证明', null=True, blank=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单', ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='leave_ticket', null=True, blank=True) on_delete=models.CASCADE, related_name='leave_ticket', null=True, blank=True)
class EmployeeTransfer(CommonADModel):
"""
TN:员工岗位调动
"""
employee = models.ForeignKey(Employee, verbose_name='员工', on_delete=models.CASCADE)
is_change = models.BooleanField('是否跨部门调动', default=False)
is_promotion = models.BooleanField('是否晋升', default=False)
new_dept = models.ForeignKey('system.Dept', verbose_name='新部门', related_name="transfer_new_dept", on_delete=models.CASCADE, null=True, blank=True)
new_post = models.ForeignKey('system.Post', verbose_name='新岗位', related_name="transfer_new_post", on_delete=models.CASCADE, null=True, blank=True)
original_dept = models.ForeignKey('system.Dept', verbose_name='原部门', related_name="transfer_original_dept", on_delete=models.CASCADE, null=True, blank=True)
original_post = models.ForeignKey('system.Post', verbose_name='原岗位', related_name="transfer_original_post", on_delete=models.CASCADE, null=True, blank=True)
transfer_date = models.DateField('调岗日期')
original_slary = models.DecimalField('原岗位薪资', max_digits=10, decimal_places=2, null=True, blank=True)
new_slary = models.DecimalField('调岗后薪资', max_digits=10, decimal_places=2, null=True, blank=True)
content = models.TextField('个人工作内容', null=True, blank=True)
reason = models.TextField('调动原因', null=True, blank=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='transfer_ticket', null=True, blank=True)

View File

@ -9,7 +9,7 @@ from django.utils import timezone
from apps.utils.serializers import CustomModelSerializer from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS from apps.utils.constants import EXCLUDE_FIELDS
from apps.hrm.models import (Certificate, ClockRecord, Employee, from apps.hrm.models import (Certificate, ClockRecord, Employee,
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, Leave) NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, Leave, EmployeeTransfer)
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
from django.db import transaction from django.db import transaction
from django.core.cache import cache from django.core.cache import cache
@ -377,3 +377,24 @@ class LeaveSerializer(CustomModelSerializer):
model = Leave model = Leave
fields = '__all__' fields = '__all__'
class TransferSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
employee_name = serializers.CharField(source='employee.name', read_only=True)
post_name = serializers.CharField(source="employee.post.name", read_only=True)
belong_dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
new_post_name = serializers.CharField(source="new_post.name", read_only=True)
original_post_name = serializers.CharField(source="original_post.name", read_only=True)
class Meta:
model = EmployeeTransfer
fields = '__all__'
def create(self, validated_data):
is_change = validated_data['is_change']
if is_change:
if validated_data['new_dept'] == validated_data['original_dept']:
raise ParseError('新旧部门相同,无需调动')
else:
if validated_data['new_post'] == validated_data['original_post']:
raise ParseError('新旧岗位相同,无需调动')
return super().create(validated_data)

View File

@ -11,13 +11,16 @@ from django.core.cache import cache
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from apps.system.models import User from apps.system.models import User
from apps.hrm.models import ClockRecord, Employee, Resignation from apps.hrm.models import ClockRecord, Employee, Resignation, EmployeeTransfer
from apps.system.models import UserPost
from apps.third.dahua import dhClient from apps.third.dahua import dhClient
from apps.third.models import TDevice from apps.third.models import TDevice
from apps.third.tapis import dhapis from apps.third.tapis import dhapis
from apps.utils.tools import rannum, ranstr from apps.utils.tools import rannum, ranstr
import numpy as np import numpy as np
from apps.wf.models import Ticket, Transition from apps.wf.models import Ticket, Transition
from django.db import transaction
from django.db.models import F
myLogger = logging.getLogger('log') myLogger = logging.getLogger('log')
@ -497,4 +500,76 @@ class HrmService:
print(f'{ep.name}-办公室打卡权限已删除') print(f'{ep.name}-办公室打卡权限已删除')
# 人员调岗申请
def post_transfer(ticket: Ticket, new_ticket_data: dict, **kwargs):
# 1⃣ 获取调岗单
try:
obj = EmployeeTransfer.objects.select_related(
'employee', 'employee__user'
).get(id=new_ticket_data['t_id'])
except EmployeeTransfer.DoesNotExist:
raise ParseError('调岗申请不存在')
employee = obj.employee
user = employee.user
if not user:
raise ParseError('员工未绑定系统账号,无法执行调岗')
# 2⃣ 先更新 EmployeeTransfer避免 atomic 中用到旧数据)
update_fields = []
for k, v in new_ticket_data.items():
if k in ['t_model', 't_id']:
continue
if k in ['new_post', 'original_post', 'new_dept', 'original_dept']:
setattr(obj, f'{k}_id', v)
else:
setattr(obj, k, v)
update_fields.append(k)
obj.save()
# 3⃣ 调岗事务岗位、部门、UserPost 一致性)
with transaction.atomic():
# 3.1 校验并删除原岗位记录
old_post_qs = UserPost.objects.filter(
user=user,
post=obj.original_post,
dept=obj.original_dept
)
if not old_post_qs.exists():
raise ParseError('原岗位记录不存在,无法调岗')
old_post_qs.delete()
# 3.2 处理岗位排序(新岗位置顶)
UserPost.objects.filter(user=user).update(sort=F('sort') + 1)
UserPost.objects.create(
user=user,
post=obj.new_post,
dept=obj.new_dept,
sort=1
)
# 3.3 更新 Employee
employee.belong_dept = obj.new_dept
employee.post = obj.new_post
employee.save(update_fields=['belong_dept', 'post'])
# 3.4 同步更新 User
User.objects.filter(id=user.id).update(
belong_dept=obj.new_dept,
post=obj.new_post
)
return obj
def validate_userdept(ticket: Ticket, new_ticket_data: dict, **kwargs):
obj = EmployeeTransfer.objects.get(id=new_ticket_data['t_id'])
exist_dept_post = UserPost.objects.filter(user=obj.employee.user, post=obj.original_post, dept=obj.original_dept)
if not exist_dept_post.exists():
raise ParseError('该用户的原部门或原职务不匹配')

View File

@ -1,5 +1,5 @@
from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet, from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet,
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, LeaveViewSet) AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, LeaveViewSet, TransferViewSet)
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
@ -17,6 +17,7 @@ router.register('resignation', ResignationViewSet, basename='resignation')
router.register('empneed', EmpNeedViewSet, basename='empneed') router.register('empneed', EmpNeedViewSet, basename='empneed')
router.register('empjoin', EmpJoinViewSet, basename='empjoin') router.register('empjoin', EmpJoinViewSet, basename='empjoin')
router.register('leave', LeaveViewSet, basename='leave') router.register('leave', LeaveViewSet, basename='leave')
router.register('transfer', TransferViewSet, basename='transfer')
urlpatterns = [ urlpatterns = [
path(API_BASE_URL, include(router.urls)), path(API_BASE_URL, include(router.urls)),
] ]

View File

@ -12,7 +12,7 @@ from rest_framework.response import Response
from apps.hrm.errors import NO_NEED_LEVEL_REMARK from apps.hrm.errors import NO_NEED_LEVEL_REMARK
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet, from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
NotWorkRemarkFilterSet) NotWorkRemarkFilterSet)
from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, Leave from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, Leave, EmployeeTransfer
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer, from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
ClockRecordListSerializer, ClockRecordListSerializer,
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer, EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
@ -20,7 +20,7 @@ from apps.hrm.serializers import (CertificateCreateUpdateSerializer, Certificate
EmployeeSerializer, EmployeeSerializer,
ClockRecordSimpleSerializer, ClockRecordCreateSerializer, ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer, NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
ResignationSerializer, EmpNeedSerializer, LeaveSerializer) ResignationSerializer, EmpNeedSerializer, LeaveSerializer, TransferSerializer)
from apps.hrm.services import HrmService from apps.hrm.services import HrmService
from apps.third.dahua import dhClient from apps.third.dahua import dhClient
@ -468,4 +468,20 @@ class LeaveViewSet(TicketMixin, EuModelViewSet):
workflow_key = "wf_leave" workflow_key = "wf_leave"
def gen_other_ticket_data(self, instance): def gen_other_ticket_data(self, instance):
return {"hour": instance.hour if instance.hour else None} return {"hour": instance.hour if instance.hour else None}
class TransferViewSet(TicketMixin, EuModelViewSet):
select_related_fields = [
'employee',
'employee__belong_dept',
'employee__post',
'ticket',
]
queryset = EmployeeTransfer.objects.all()
serializer_class = TransferSerializer
filterset_fields = ['employee__belong_dept']
search_fields = ["employee__name", "employee__post", "employee__belong_dept"]
workflow_key = "wf_transfer"
def gen_other_ticket_data(self, instance):
return {"name": instance.employee.name if instance.employee.name else None}