This commit is contained in:
caoqianming 2026-03-06 11:05:03 +08:00
commit c99e74eee8
21 changed files with 946 additions and 56 deletions

View File

@ -0,0 +1,103 @@
# Generated by Django 3.2.12 on 2026-02-05 05:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0028_auto_20260126_1445'),
]
operations = [
migrations.AddField(
model_name='employee',
name='address',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='现住址'),
),
migrations.AddField(
model_name='employee',
name='birthpart',
field=models.CharField(blank=True, max_length=40, null=True, verbose_name='出生地'),
),
migrations.AddField(
model_name='employee',
name='birthplace',
field=models.CharField(blank=True, max_length=40, null=True, verbose_name='籍贯'),
),
migrations.AddField(
model_name='employee',
name='card_type',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='证件类型'),
),
migrations.AddField(
model_name='employee',
name='categories',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='员工类别'),
),
migrations.AddField(
model_name='employee',
name='company',
field=models.CharField(blank=True, max_length=30, null=True, verbose_name='所属公司'),
),
migrations.AddField(
model_name='employee',
name='country',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='国籍'),
),
migrations.AddField(
model_name='employee',
name='health',
field=models.CharField(blank=True, max_length=10, null=True, verbose_name='健康状况'),
),
migrations.AddField(
model_name='employee',
name='join_job',
field=models.DateField(blank=True, null=True, verbose_name='参加工作时间'),
),
migrations.AddField(
model_name='employee',
name='join_partisan_date',
field=models.DateField(blank=True, null=True, verbose_name='入党时间'),
),
migrations.AddField(
model_name='employee',
name='join_system',
field=models.DateField(blank=True, null=True, verbose_name='入职系统时间'),
),
migrations.AddField(
model_name='employee',
name='marriage',
field=models.CharField(blank=True, max_length=5, null=True, verbose_name='婚姻状况'),
),
migrations.AddField(
model_name='employee',
name='nation',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='民族'),
),
migrations.AddField(
model_name='employee',
name='office_date',
field=models.DateField(blank=True, null=True, verbose_name='任职时间'),
),
migrations.AddField(
model_name='employee',
name='partisan',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='政治面貌'),
),
migrations.AddField(
model_name='employee',
name='position',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='职务'),
),
migrations.AddField(
model_name='employee',
name='rank',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='职级'),
),
migrations.AddField(
model_name='employee',
name='record_data',
field=models.DateField(blank=True, null=True, verbose_name='录入日期'),
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 3.2.12 on 2026-02-09 07:59
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', '0007_alter_dept_create_by_alter_dept_third_info_and_more'),
('wf', '0006_auto_20251215_1645'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('hrm', '0029_auto_20260205_1337'),
]
operations = [
migrations.CreateModel(
name='Probation',
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='删除标记')),
('trial_date', models.DateField(verbose_name='试用日期')),
('regular_date', models.DateField(verbose_name='转正日期')),
('content', models.TextField(blank=True, null=True, verbose_name='个人工作内容')),
('record_file', models.CharField(blank=True, max_length=100, null=True, verbose_name='应聘人员登记表')),
('apply_file', models.CharField(blank=True, max_length=100, null=True, verbose_name='员工转正申请表')),
('ppt_file', models.CharField(blank=True, max_length=100, null=True, verbose_name='PPT 汇报考核')),
('belong_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='probation_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='probation_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('empperson', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hrm.emppersoninfo', verbose_name='人员信息')),
('reg_dept', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.dept', verbose_name='转正部门')),
('reg_post', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='system.post', verbose_name='转正岗位')),
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='probation_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='probation_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,44 @@
# Generated by Django 3.2.12 on 2026-02-27 01:33
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wf', '0006_auto_20251215_1645'),
('hrm', '0030_probation'),
]
operations = [
migrations.AddField(
model_name='employee',
name='end_contract_date',
field=models.DateField(blank=True, null=True, verbose_name='合同到期日期'),
),
migrations.CreateModel(
name='EmpContract',
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='删除标记')),
('counts', models.PositiveSmallIntegerField(default=1, verbose_name='合同变更次数')),
('plan_renewal', models.DateField(blank=True, null=True, verbose_name='应续签')),
('normal_renewal', models.DateField(blank=True, null=True, verbose_name='正常续签')),
('change_date', models.IntegerField(blank=True, null=True, verbose_name='续签/变更(年)')),
('change_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='empcontract_create_by', to=settings.AUTH_USER_MODEL, verbose_name='创建人')),
('employee', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hrm.employee', verbose_name='人员信息')),
('ticket', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='contract_ticket', to='wf.ticket', verbose_name='关联工单')),
('update_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='empcontract_update_by', to=settings.AUTH_USER_MODEL, verbose_name='最后编辑人')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,207 @@
# Generated by Django 3.2.12 on 2026-03-02 07:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0031_auto_20260227_0933'),
]
operations = [
migrations.RemoveField(
model_name='employee',
name='birthpart',
),
migrations.RemoveField(
model_name='employee',
name='card_type',
),
migrations.RemoveField(
model_name='employee',
name='categories',
),
migrations.RemoveField(
model_name='employee',
name='company',
),
migrations.RemoveField(
model_name='employee',
name='country',
),
migrations.RemoveField(
model_name='employee',
name='end_contract_date',
),
migrations.RemoveField(
model_name='employee',
name='health',
),
migrations.RemoveField(
model_name='employee',
name='join_job',
),
migrations.RemoveField(
model_name='employee',
name='join_system',
),
migrations.RemoveField(
model_name='employee',
name='rank',
),
migrations.RemoveField(
model_name='employee',
name='record_data',
),
migrations.AddField(
model_name='employee',
name='age',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='年龄'),
),
migrations.AddField(
model_name='employee',
name='band_card',
field=models.CharField(blank=True, max_length=30, null=True, verbose_name='银行卡号'),
),
migrations.AddField(
model_name='employee',
name='contract_end_date',
field=models.DateField(blank=True, null=True, verbose_name='合同到期日'),
),
migrations.AddField(
model_name='employee',
name='emergency_contact',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='紧急联系人姓名'),
),
migrations.AddField(
model_name='employee',
name='emergency_phone',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='紧急联系人电话'),
),
migrations.AddField(
model_name='employee',
name='full_edu',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='全日制最高学历'),
),
migrations.AddField(
model_name='employee',
name='full_edu_major',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='全日制最高学历专业'),
),
migrations.AddField(
model_name='employee',
name='full_edu_school',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='全日制最高学历学校名称'),
),
migrations.AddField(
model_name='employee',
name='full_edu_time',
field=models.DateField(blank=True, null=True, verbose_name='全日制最高学历毕业时间'),
),
migrations.AddField(
model_name='employee',
name='honor',
field=models.TextField(blank=True, null=True, verbose_name='所获荣誉'),
),
migrations.AddField(
model_name='employee',
name='hukou_address',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='户籍地址'),
),
migrations.AddField(
model_name='employee',
name='hukou_type',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='户口性质'),
),
migrations.AddField(
model_name='employee',
name='part_edu',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='非全日制最高学历'),
),
migrations.AddField(
model_name='employee',
name='part_edu_major',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='非全日制最高学历专业'),
),
migrations.AddField(
model_name='employee',
name='part_edu_school',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='非全日制最高学历学校名称'),
),
migrations.AddField(
model_name='employee',
name='part_edu_time',
field=models.DateField(blank=True, null=True, verbose_name='非全日制最高学历毕业时间'),
),
migrations.AddField(
model_name='employee',
name='regular_date',
field=models.DateField(blank=True, null=True, verbose_name='转正日期'),
),
migrations.AddField(
model_name='employee',
name='skill_rank',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='职业技能等级'),
),
migrations.AddField(
model_name='employee',
name='skill_rank_date',
field=models.DateField(blank=True, null=True, verbose_name='获得技能等级证书日期'),
),
migrations.AddField(
model_name='employee',
name='work_years',
field=models.PositiveIntegerField(blank=True, null=True, verbose_name='工龄'),
),
migrations.AddField(
model_name='employee',
name='workshop',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='车间'),
),
migrations.AddField(
model_name='employee',
name='zhuanzhi',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='枣庄市职称'),
),
migrations.AddField(
model_name='employee',
name='zhuanzhi_date',
field=models.DateField(blank=True, null=True, verbose_name='获得枣庄市职称日期'),
),
migrations.AddField(
model_name='employee',
name='zhuanzhi_img_1',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='职称证书图片1'),
),
migrations.AddField(
model_name='employee',
name='zhuanzhi_img_2',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='职称证书图片2'),
),
migrations.AddField(
model_name='employee',
name='zyjt_zhuanzhi',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='总院/集团职称'),
),
migrations.AddField(
model_name='employee',
name='zyjt_zhuanzhi_date',
field=models.DateField(blank=True, null=True, verbose_name='获得职称日期'),
),
migrations.AddField(
model_name='employee',
name='zyjt_zhuanzhi_img_1',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='总院/集团职称证书图片1'),
),
migrations.AlterField(
model_name='employee',
name='address',
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='现住地址'),
),
migrations.AlterField(
model_name='employee',
name='office_date',
field=models.DateField(blank=True, null=True, verbose_name='职务聘任日期'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2026-03-03 01:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0032_auto_20260302_1530'),
]
operations = [
migrations.AddField(
model_name='employee',
name='first_social_security_date',
field=models.DateField(blank=True, null=True, verbose_name='首次缴纳社保日期'),
),
migrations.AddField(
model_name='employee',
name='is_veteran',
field=models.BooleanField(default=False, verbose_name='是否为退役军人'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 3.2.12 on 2026-03-03 03:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hrm', '0033_auto_20260303_0956'),
]
operations = [
migrations.AlterField(
model_name='employee',
name='full_edu_time',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='全日制最高学历毕业时间'),
),
migrations.AlterField(
model_name='employee',
name='part_edu_time',
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='非全日制最高学历毕业时间'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2026-03-03 03:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('hrm', '0034_auto_20260303_1107'),
]
operations = [
migrations.RenameField(
model_name='employee',
old_name='band_card',
new_name='bank_card',
),
]

View File

@ -25,8 +25,7 @@ class Employee(CommonBModel):
('visitor', '访客'),
('driver', '司机')
)
type = models.CharField('人员类型', default='employee',
max_length=10, choices=PEOPLE_TYPE_CHOICES)
user = models.OneToOneField(User,
verbose_name='系统账号',
related_name='employee',
@ -36,11 +35,8 @@ class Employee(CommonBModel):
email = models.EmailField('邮箱号', null=True, blank=True)
number = models.CharField('人员编号', max_length=50, null=True, blank=True)
photo = models.CharField('证件照', max_length=1000, null=True, blank=True)
id_number = models.CharField(
'身份证号', max_length=100, null=True, blank=True, unique=True)
gender = models.CharField('性别', max_length=10, default='')
signature = models.CharField('签名图片', max_length=200, null=True, blank=True)
birthday = models.DateField('出生年月日', null=True, blank=True)
qualification = models.CharField(
'学历', max_length=50, null=True, blank=True)
job_state = models.IntegerField(
@ -56,8 +52,52 @@ class Employee(CommonBModel):
on_delete=models.SET_NULL, null=True, blank=True)
face_data = models.JSONField(
'人脸数据', null=True, blank=True) # 存储的是字典(模型名:人脸数据)
start_date = models.DateField('入职日期', null=True, blank=True)
end_date = models.DateField('离职日期', null=True, blank=True)
workshop = models.CharField('车间', max_length=20, null=True, blank=True)
position = models.CharField('职务', max_length=20, null=True, blank=True)
office_date = models.DateField('职务聘任日期', null=True, blank=True)
type = models.CharField('人员类型', default='employee',
max_length=10, choices=PEOPLE_TYPE_CHOICES)
gender = models.CharField('性别', max_length=10, default='')
id_number = models.CharField(
'身份证号', max_length=100, null=True, blank=True, unique=True)
bank_card = models.CharField('银行卡号', max_length=30, null=True, blank=True)
start_date = models.DateField('入职日期', null=True, blank=True)
contract_end_date = models.DateField('合同到期日', null=True, blank=True)
regular_date = models.DateField('转正日期', null=True, blank=True)
birthday = models.DateField('出生年月日', null=True, blank=True)
age = models.PositiveIntegerField('年龄', null=True, blank=True)
work_years = models.PositiveIntegerField('工龄', null=True, blank=True)
partisan = models.CharField('政治面貌', max_length=20, null=True, blank=True)
join_partisan_date = models.DateField('入党时间', null=True, blank=True)
nation = models.CharField('民族', max_length=20, null=True, blank=True)
marriage = models.CharField('婚姻状况', max_length=5, null=True, blank=True)
hukou_type = models.CharField('户口性质', max_length=20, null=True, blank=True)
birthplace = models.CharField('籍贯', max_length=40, null=True, blank=True)
hukou_address = models.CharField('户籍地址', max_length=100, null=True, blank=True)
address = models.CharField('现住地址', max_length=100, null=True, blank=True)
zhuanzhi = models.CharField('枣庄市职称', max_length=50, null=True, blank=True)
zhuanzhi_img_1 = models.CharField('职称证书图片1', max_length=100, null=True, blank=True)
zhuanzhi_img_2 = models.CharField('职称证书图片2', max_length=100, null=True, blank=True)
zhuanzhi_date = models.DateField('获得枣庄市职称日期', null=True, blank=True)
zyjt_zhuanzhi = models.CharField('总院/集团职称', max_length=50, null=True, blank=True)
zyjt_zhuanzhi_img_1 = models.CharField('总院/集团职称证书图片1', max_length=100, null=True, blank=True)
zyjt_zhuanzhi_date = models.DateField('获得职称日期', null=True, blank=True)
skill_rank = models.CharField('职业技能等级', max_length=50, null=True, blank=True)
skill_rank_date = models.DateField('获得技能等级证书日期', null=True, blank=True)
full_edu = models.CharField('全日制最高学历', max_length=50, null=True, blank=True)
full_edu_school = models.CharField('全日制最高学历学校名称', max_length=100, null=True, blank=True)
full_edu_major = models.CharField('全日制最高学历专业', max_length=100, null=True, blank=True)
full_edu_time = models.CharField('全日制最高学历毕业时间', max_length=50, null=True, blank=True)
part_edu = models.CharField('非全日制最高学历', max_length=50, null=True, blank=True)
part_edu_school = models.CharField('非全日制最高学历学校名称', max_length=100, null=True, blank=True)
part_edu_major = models.CharField('非全日制最高学历专业', max_length=100, null=True, blank=True)
part_edu_time = models.CharField('非全日制最高学历毕业时间', max_length=50, null=True, blank=True)
emergency_contact = models.CharField('紧急联系人姓名', max_length=20, null=True, blank=True)
emergency_phone = models.CharField('紧急联系人电话', max_length=20, null=True, blank=True)
honor = models.TextField('所获荣誉', null=True, blank=True)
first_social_security_date = models.DateField('首次缴纳社保日期', null=True, blank=True)
is_veteran = models.BooleanField('是否为退役军人', default=False)
class Meta:
verbose_name = '员工补充信息'
@ -290,4 +330,38 @@ class EmployeeTransfer(CommonADModel):
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)
on_delete=models.CASCADE, related_name='transfer_ticket', null=True, blank=True)
class Probation(CommonBModel):
"""
TN:员工转正
"""
empperson = models.OneToOneField(EmpPersonInfo, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='probation_ticket', null=True, blank=True)
reg_dept = models.ForeignKey('system.Dept', verbose_name='转正部门', on_delete=models.SET_NULL, null=True, blank=True)
reg_post = models.ForeignKey('system.Post', verbose_name='转正岗位', on_delete=models.SET_NULL, null=True, blank=True)
trial_date = models.DateField('试用日期')
regular_date = models.DateField('转正日期')
content = models.TextField('个人工作内容', null=True, blank=True)
record_file = models.CharField('应聘人员登记表', null=True, blank=True, max_length=100)
apply_file = models.CharField('员工转正申请表', null=True, blank=True, max_length=100)
ppt_file = models.CharField('PPT 汇报考核', null=True, blank=True, max_length=100)
class EmpContract(CommonAModel):
"""
TN:劳动合同
"""
employee = models.OneToOneField(Employee, verbose_name='人员信息', on_delete=models.SET_NULL, null=True, blank=True)
ticket = models.OneToOneField('wf.ticket', verbose_name='关联工单',
on_delete=models.CASCADE, related_name='contract_ticket', null=True, blank=True)
counts = models.PositiveSmallIntegerField('合同变更次数', default=1)
plan_renewal = models.DateField('应续签', null=True, blank=True)
normal_renewal = models.DateField('正常续签', null=True, blank=True)
change_date = models.IntegerField('续签/变更(年)', null=True, blank=True)
change_reason = models.TextField('缩短续签/变更年限原因', null=True, blank=True)

View File

@ -8,8 +8,8 @@ from django.utils import timezone
from apps.utils.serializers import CustomModelSerializer
from apps.utils.constants import EXCLUDE_FIELDS
from apps.hrm.models import (Certificate, ClockRecord, Employee,
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, Leave, EmployeeTransfer)
from apps.hrm.models import (Certificate, ClockRecord, Employee, Probation,
NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, EmpPersonInfo, EmpContract, Leave, EmployeeTransfer)
from apps.system.serializers import DeptSimpleSerializer, UserSimpleSerializer
from django.db import transaction
from django.core.cache import cache
@ -349,6 +349,7 @@ class EmpNeedSerializer(CustomModelSerializer):
model = EmpNeed
fields = '__all__'
class EmpJoinSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
dept_name = serializers.CharField(source='dept_need.name', read_only=True)
@ -356,6 +357,7 @@ class EmpJoinSerializer(CustomModelSerializer):
model = EmpJoin
fields = '__all__'
class EmpPersonInfoSerializer(CustomModelSerializer):
class Meta:
model = EmpPersonInfo
@ -368,6 +370,7 @@ class EmpPersonInfoSerializer(CustomModelSerializer):
'note',
)
class LeaveSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
employee_name = serializers.CharField(source='employee.name', read_only=True)
@ -377,6 +380,7 @@ class LeaveSerializer(CustomModelSerializer):
model = Leave
fields = '__all__'
class TransferSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
employee_name = serializers.CharField(source='employee.name', read_only=True)
@ -397,4 +401,28 @@ class TransferSerializer(CustomModelSerializer):
if validated_data['new_post'] == validated_data['original_post']:
raise ParseError('新旧岗位相同,无需调动')
return super().create(validated_data)
return super().create(validated_data)
class ProbationSerializer(CustomModelSerializer):
ticket_ = TicketSimpleSerializer(source='ticket', read_only=True)
employee_name = serializers.CharField(source='empperson.name', read_only=True)
post_name = serializers.CharField(source="empperson.reg_post", read_only=True)
reg_dept_name = serializers.CharField(source='empperson.reg_dept.name', read_only=True)
IDcard = serializers.CharField(source="empperson.IDcard", read_only=True)
class Meta:
model = Probation
fields = '__all__'
class EmpContractSerializer(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)
dept_name = serializers.CharField(source='employee.belong_dept.name', read_only=True)
gender = serializers.CharField(source="employee.gender", read_only=True)
join_date = serializers.CharField(source="employee.join_date", read_only=True)
end_contract = serializers.CharField(source="employee.end_contract_date", read_only=True)
class Meta:
model = EmpContract
fields = '__all__'

View File

@ -11,7 +11,7 @@ from django.core.cache import cache
from rest_framework.exceptions import ParseError
from apps.system.models import User
from apps.hrm.models import ClockRecord, Employee, Resignation, EmployeeTransfer
from apps.hrm.models import ClockRecord, Employee, Resignation, EmployeeTransfer, Probation
from apps.system.models import UserPost
from apps.third.dahua import dhClient
from apps.third.models import TDevice
@ -512,10 +512,7 @@ def post_transfer(ticket: Ticket, new_ticket_data: dict, **kwargs):
employee = obj.employee
user = employee.user
if not user:
raise ParseError('员工未绑定系统账号,无法执行调岗')
# 2⃣ 先更新 EmployeeTransfer避免 atomic 中用到旧数据)
update_fields = []
for k, v in new_ticket_data.items():
@ -529,47 +526,52 @@ def post_transfer(ticket: Ticket, new_ticket_data: dict, **kwargs):
obj.save()
# 3⃣ 调岗事务岗位、部门、UserPost 一致性)
with transaction.atomic():
# 更新 Employee的部门和岗位
employee.belong_dept = obj.new_dept
employee.post = obj.new_post
employee.save(update_fields=['belong_dept', 'post'])
# 3.1 校验并删除原岗位记录
old_post_qs = UserPost.objects.filter(
user=user,
post=obj.original_post,
dept=obj.original_dept
)
if user:
# 3⃣ 调岗事务岗位、部门、UserPost 一致性)
with transaction.atomic():
# 3.0 校验Employee中的user是否存在
# 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('原岗位记录不存在,无法调岗')
if not old_post_qs.exists():
raise ParseError('原岗位记录不存在,无法调岗')
old_post_qs.delete()
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
# 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.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('该用户的原部门或原职务不匹配')
raise ParseError('该用户的原部门或原职务不匹配')
def probation_to_employee(ticket: Ticket, new_ticket_data: dict, **kwargs):
obj = Probation.objects.get(id=new_ticket_data['t_id'])
pass
# 需要解析上传的员工信息表,存入 employee表

View File

@ -1,5 +1,5 @@
from apps.hrm.views import (CertificateViewSet, ClockRecordViewSet, EmployeeViewSet, NotWorkRemarkViewSet, EmpNeedViewSet,
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, LeaveViewSet, TransferViewSet)
AttendanceViewSet, ResignationViewSet, EmpJoinViewSet, EmpPersonInfoViewSet, LeaveViewSet, TransferViewSet, ProbationViewSet, EmpContractViewSet)
from django.urls import path, include
from rest_framework.routers import DefaultRouter
@ -16,8 +16,11 @@ router.register('attendance', AttendanceViewSet, basename='attendance')
router.register('resignation', ResignationViewSet, basename='resignation')
router.register('empneed', EmpNeedViewSet, basename='empneed')
router.register('empjoin', EmpJoinViewSet, basename='empjoin')
router.register('empperson', EmpPersonInfoViewSet, basename='empperson')
router.register('leave', LeaveViewSet, basename='leave')
router.register('transfer', TransferViewSet, basename='transfer')
router.register('probation', ProbationViewSet, basename='probation')
router.register('contract', EmpContractViewSet, basename='emp_contract')
urlpatterns = [
path(API_BASE_URL, include(router.urls)),
]

View File

@ -12,7 +12,8 @@ from rest_framework.response import Response
from apps.hrm.errors import NO_NEED_LEVEL_REMARK
from apps.hrm.filters import (CertificateFilterSet, ClockRecordFilterSet, EmployeeFilterSet,
NotWorkRemarkFilterSet)
from apps.hrm.models import Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation, EmpNeed, EmpJoin, Leave, EmployeeTransfer
from apps.hrm.models import (Certificate, ClockRecord, Employee, NotWorkRemark, Attendance, Resignation,
EmpNeed, EmpJoin, Leave, EmployeeTransfer, Probation, EmpPersonInfo, EmpContract)
from apps.hrm.serializers import (CertificateCreateUpdateSerializer, CertificateSerializer, ChannelAuthoritySerializer, EmpJoinSerializer,
ClockRecordListSerializer,
EmployeeCreateUpdateSerializer, EmployeeDetailSerializer, EmployeeImproveSerializer,
@ -20,24 +21,87 @@ from apps.hrm.serializers import (CertificateCreateUpdateSerializer, Certificate
EmployeeSerializer,
ClockRecordSimpleSerializer, ClockRecordCreateSerializer,
NotWorkRemarkListSerializer, CorrectSerializer, AttendanceSerializer,
ResignationSerializer, EmpNeedSerializer, LeaveSerializer, TransferSerializer)
ResignationSerializer, EmpNeedSerializer, LeaveSerializer, TransferSerializer, ProbationSerializer, EmpContractSerializer)
from apps.hrm.services import HrmService
from apps.third.dahua import dhClient
from apps.third.tapis import dhapis
from openpyxl import load_workbook
from django.db import transaction
from django.core.exceptions import ValidationError
from datetime import datetime
from apps.utils.export import export_excel
from apps.utils.viewsets import CustomGenericViewSet, CustomModelViewSet, EuModelViewSet
from apps.utils.mixins import BulkCreateModelMixin, BulkDestroyModelMixin, CustomListModelMixin, RetrieveModelMixin
from apps.wf.models import Ticket
from apps.wf.mixins import TicketMixin
from apps.system.models import Post, Dept
from django.db.models import DateField
from datetime import datetime, date
from openpyxl.utils.datetime import from_excel
import os
import re
epTypeOptions = {'employee': '正式员工', 'remployee': '相关方',
'visitor': '访客', 'driver': '货车司机'}
epStateOptions = {10: '在职', 20: '离职', 30: '退休'}
crOptions = {10: '上班打卡', 20: '下班打卡', 30: ''}
crEoptions = {10: '在岗时间短', 20: '在岗时间长', 30: '缺卡', 40: '加班'}
# Create your views here.
HEAD_MAP = {
"所属部门": "belong_dept",
"车间": "workshop",
"职务": "position",
"职务聘任日期": "office_date",
"类别": "type",
"性别": "gender",
"身份证号码:姓名": "name",
"身份证号码:身份证号": "id_number",
"银行卡号码": "bank_card",
"入职日期": "start_date",
"合同到期日": "contract_end_date",
"转正日期": "regular_date",
"出生日期": "birthday",
"年龄": "age",
"工龄": "work_years",
"政治面貌": "partisan",
"入党时间": "join_partisan_date",
"民族": "nation",
"婚姻状况": "marriage",
"户口性质": "hukou_type",
"籍贯": "birthplace",
"户籍地址": "hukou_address",
"现住地址": "address",
"枣庄市职称": "zhuanzhi",
"枣庄市职称证书:图片_1": "zhuanzhi_img_1",
"枣庄市职称证书:图片_2": "zhuanzhi_img_2",
"获得枣庄市职称日期": "zhuanzhi_date",
"总院/集团职称": "zyjt_zhuanzhi",
"总院/集团职称证书:图片_1": "zyjt_zhuanzhi_img_1",
"获得职称日期": "zyjt_zhuanzhi_date",
"职业技能等级": "skill_rank",
"获得技能等级证书日期": "skill_rank_date",
"全日制最高学历": "full_edu",
"全日制最高学历学校名称": "full_edu_school",
"全日制最高学历所学专业": "full_edu_major",
"全日制最高学历入学时间-毕业时间": "full_edu_time",
"非全日制最高学历": "part_edu",
"非全日制最高学历学校名称": "part_edu_school",
"非全日制最高学历所学专业": "part_edu_major",
"非全日制最高学历入学时间-毕业时间": "part_edu_time",
"本人联系电话": "phone",
"紧急联系人姓名": "emergency_contact",
"紧急联系人电话": "emergency_phone",
"所获荣誉": "honor",
"首次缴纳社保日期": "first_social_security_date",
"是否为退役军人": "is_veteran",
"个人邮箱":"email",
}
def cell_value(cell):
val = cell.value
if isinstance(val, datetime):
return val.date()
return val.strip() if isinstance(val, str) else val
class EmployeeViewSet(CustomModelViewSet):
"""
@ -244,6 +308,203 @@ class EmployeeViewSet(CustomModelViewSet):
)
return Response({'path': export_excel(field_data, data, '人员信息')})
@action(methods=['post'], detail=False, perms_map={'post': 'employee.import_excel'},
serializer_class=serializers.Serializer)
def import_excel(self, request, pk=None):
import logging
myLogger = logging.getLogger('log')
"""导入excel"""
file_path = request.data.get('file_path')
relative_path = file_path.lstrip('/')
abs_path = os.path.join(settings.BASE_DIR, relative_path)
wb = load_workbook(abs_path, data_only=True)
sheet = wb.active
rows = list(sheet.iter_rows())
if len(rows) < 2:
raise ParseError('Excel 没有数据')
# 获取表头
headers = [cell_value(c) for c in rows[0]]
field_index = {}
for idx, header in enumerate(headers):
if header in HEAD_MAP:
field_index[HEAD_MAP[header]] = idx
# 外键缓存
dept_map = {d.name: d.id for d in Dept.objects.all()}
TYPE_MAPPING = {
'光芯': 'employee',
'外包': 'remployee'
}
success = 0
errors = []
# 获取模型字段
model_fields = {f.name: f for f in Employee._meta.fields}
for row_num, row in enumerate(rows[1:], start=2):
try:
data = {}
for field, idx in field_index.items():
raw_value = cell_value(row[idx])
model_field = model_fields.get(field)
value = convert_field_value(model_field, raw_value, row_num)
data[field] = value
# 移除可能的id字段
if 'id' in data:
del data['id']
# 获取唯一标识
id_number = data.get("id_number")
name = data.get("name")
if not id_number or not name:
raise ParseError(f'{row_num}行,身份证号或姓名为空')
myLogger.info(f"处理第{row_num}行:{name} - {id_number}")
# 处理人员类型
if 'type' in data and data['type']:
excel_type = data['type']
if excel_type in TYPE_MAPPING:
data['type'] = TYPE_MAPPING[excel_type]
else:
raise ParseError(f'{row_num}行,人员类型"{excel_type}"无效,有效类型:{", ".join(TYPE_MAPPING.keys())}')
# 处理部门外键
if 'belong_dept' in data and data['belong_dept']:
dept_name = data.pop('belong_dept')
if dept_name not in dept_map:
raise ParseError(f'{row_num}行,部门"{dept_name}"不存在')
data['belong_dept_id'] = dept_map[dept_name]
# 数据验证
if data.get('phone'):
if not re.match(r'^1[3-9]\d{9}$', data['phone']):
raise ParseError(f'{row_num}行,手机号格式不正确')
if data.get('id_number'):
if not re.match(r'^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$', data['id_number']):
raise ParseError(f'{row_num}行,身份证号格式不正确')
# 查找或更新
try:
with transaction.atomic():
obj, created = Employee.objects.update_or_create(
id_number=id_number,
name=name,
defaults=data
)
except Exception as e:
raise
if created:
myLogger.info(f"✅ 第{row_num}行新增成功:{name}")
else:
myLogger.info(f"✅ 第{row_num}行更新成功:{name}")
success += 1
except Exception as e:
error_msg = f'{row_num}行处理失败:{str(e)}'
myLogger.error(f"{error_msg}")
errors.append({'row': row_num, 'error': str(e)})
myLogger.info(f"\n处理完成:成功{success}条,失败{len(errors)}")
return Response({'success': success, 'errors': errors})
def convert_field_value(model_field, value, row_num):
"""
根据模型字段类型自动转换 Excel
"""
from django.db.models import DateField, BooleanField, IntegerField, FloatField, CharField
if value in [None, ""]:
return None
# ===== 日期字段 =====
if isinstance(model_field, CharField):
if isinstance(value, str):
if 'e' in value.lower():
try:
return str(int(float(value)))
except ValueError:
pass
if '.' in value:
try:
return str(int(float(value)))
except ValueError:
pass
return str(value)
if isinstance(model_field, DateField):
# 已经是 date
if isinstance(value, date):
return value
# datetime
if isinstance(value, datetime):
return value.date()
# Excel 数字日期
if isinstance(value, (int, float)):
try:
return from_excel(value).date()
except Exception:
raise ParseError(f'{row_num}行,字段 {model_field.name} 日期格式错误')
# 字符串日期
if isinstance(value, str):
value = value.strip()
formats = [
"%Y-%m-%d",
"%Y/%m/%d",
"%Y.%m.%d",
"%Y%m%d",
"%Y年%m月%d",
]
for fmt in formats:
try:
return datetime.strptime(value, fmt).date()
except ValueError:
continue
raise ParseError(f'{row_num}行,字段 {model_field.name} 日期格式不正确')
raise ParseError(f'{row_num}行,字段 {model_field.name} 日期类型错误')
# ===== 布尔字段 =====
if isinstance(model_field, BooleanField):
if value in [True, 1, '1']:
return True
if value in [False, 0, '0']:
return False
if isinstance(value, str):
value = value.strip()
if value in ['', 'Y', 'y', 'yes', 'YES', 'True', 'true']:
return True
if value in ['', 'N', 'n', 'no', 'NO', 'False', 'false']:
return False
raise ParseError(f'{row_num}行,字段 {model_field.name} 只能填写 是 或 否')
# ===== 整数字段 =====
if isinstance(model_field, IntegerField):
try:
return int(value)
except:
raise ParseError(f'{row_num}行,字段 {model_field.name} 必须为整数')
# ===== 浮点字段 =====
if isinstance(model_field, FloatField):
try:
return float(value)
except:
raise ParseError(f'{row_num}行,字段 {model_field.name} 必须为数字')
return value
class AttendanceViewSet(CustomModelViewSet):
"""
@ -395,6 +656,9 @@ class CertificateViewSet(CustomModelViewSet):
class ResignationViewSet(TicketMixin, EuModelViewSet):
"""
离职记录
"""
select_related_fields = ['employee', 'employee__belong_dept', 'employee__post']
queryset = Resignation.objects.all()
serializer_class = ResignationSerializer
@ -426,6 +690,9 @@ class ResignationViewSet(TicketMixin, EuModelViewSet):
class EmpNeedViewSet(TicketMixin, EuModelViewSet):
"""
员工需求
"""
queryset = EmpNeed.objects.all()
serializer_class = EmpNeedSerializer
filterset_fields = ['dept_need']
@ -437,6 +704,9 @@ class EmpNeedViewSet(TicketMixin, EuModelViewSet):
class EmpJoinViewSet(TicketMixin, EuModelViewSet):
"""
员工入职
"""
queryset = EmpJoin.objects.all()
serializer_class = EmpJoinSerializer
workflow_key = "wf_empjoin"
@ -456,6 +726,9 @@ class EmpJoinViewSet(TicketMixin, EuModelViewSet):
class LeaveViewSet(TicketMixin, EuModelViewSet):
"""
员工请假
"""
select_related_fields = [
'employee',
'employee__belong_dept',
@ -469,8 +742,22 @@ class LeaveViewSet(TicketMixin, EuModelViewSet):
def gen_other_ticket_data(self, instance):
return {"hour": instance.hour if instance.hour else None}
class EmpPersonInfoViewSet(CustomModelViewSet, EuModelViewSet):
"""
入职人员信息
"""
queryset = EmpPersonInfo.objects.all()
serializer_class = EmpPersonInfoSerializer
filterset_fields = ['post']
search_fields = ['name', 'post']
class TransferViewSet(TicketMixin, EuModelViewSet):
"""
员工岗位调动
"""
select_related_fields = [
'employee',
'employee__belong_dept',
@ -484,4 +771,38 @@ class TransferViewSet(TicketMixin, EuModelViewSet):
workflow_key = "wf_transfer"
def gen_other_ticket_data(self, instance):
return {"name": instance.employee.name if instance.employee.name else None}
return {"name": instance.employee.name if instance.employee.name else None}
class ProbationViewSet(TicketMixin, EuModelViewSet):
"""
员工转正
"""
select_related_fields = [
'empperson',
'ticket',
'reg_dept',
'reg_post'
]
queryset = Probation.objects.all()
serializer_class = ProbationSerializer
filterset_fields = ['empperson__name', 'reg_dept']
search_fields = ["empperson__name", "reg_post__name", "reg_dept__name"]
workflow_key = "wf_probation"
def gen_other_ticket_data(self, instance):
return {"name": instance.empperson.name if instance.empperson.name else None}
class EmpContractViewSet(TicketMixin, EuModelViewSet):
"""
员工合同
"""
queryset = EmpContract.objects.all()
serializer_class = EmpContractSerializer
filterset_fields = ['employee', 'employee__belong_dept','employee__name', 'employee__post']
search_fields = ["employee__name", "employee__post", "employee__belong_dept"]
workflow_key = "wf_contract"
def gen_other_ticket_data(self, instance):
return {"name": instance.employee.name if instance.employee.name else None}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB