512 lines
20 KiB
Python
512 lines
20 KiB
Python
# -*- coding: utf-8 -*-
|
|
from odoo import api, fields, models, tools
|
|
from odoo.exceptions import ValidationError, UserError
|
|
from dateutil.relativedelta import relativedelta
|
|
from datetime import date
|
|
from datetime import timedelta
|
|
|
|
|
|
class HrContract(models.Model):
|
|
_inherit = "hr.contract"
|
|
_description = "员工合同拓展"
|
|
|
|
code = fields.Char(
|
|
string="编号",
|
|
default=lambda self: self.env["ir.sequence"].next_by_code("contract_code"),
|
|
)
|
|
name = fields.Char(string="合同名称", compute="_compute_name", readonly=False)
|
|
is_annual_salary = fields.Boolean(string="是否年薪")
|
|
resource_calendar_id = fields.Many2one("resource.calendar", string="考勤时间表")
|
|
state = fields.Selection(
|
|
[
|
|
("draft", "草稿"),
|
|
("open", "运行中"),
|
|
("close", "完成"),
|
|
("cancel", "已中止"),
|
|
],
|
|
string="状态",
|
|
default="draft",
|
|
readonly=True,
|
|
copy=False,
|
|
)
|
|
date_of_entry = fields.Date(related="employee_id.entry_date", string="入职时间")
|
|
salary_group_id = fields.Many2one(
|
|
"yuthon.salary.group", string="工资单分组", store=True, index=True
|
|
)
|
|
is_internship = fields.Boolean(string="实习期", compute="_compute_name", store=True)
|
|
version = fields.Char(string="版本", default="V1.0")
|
|
date_end = fields.Date(string="合同结束时间")
|
|
termination_date = fields.Date(string="中止时间")
|
|
reason_suspension = fields.Selection(
|
|
[
|
|
("turnover", "离职"),
|
|
("died", "意外离世"),
|
|
("other", "异动"),
|
|
],
|
|
string="中止原因",
|
|
)
|
|
work_entry_source = fields.Selection(
|
|
[
|
|
("calendar", "考勤安排"),
|
|
("attendance", "Attendances"),
|
|
("planning", "特殊排版"),
|
|
]
|
|
)
|
|
is_probation = fields.Boolean(string="试用期")
|
|
is_copy = fields.Boolean(string='是否为复制合同', default=False)
|
|
start_date = fields.Date(
|
|
string="试用开始日期", required=True, default=fields.Date.today
|
|
)
|
|
end_date = fields.Date(
|
|
string="试用结束日期",
|
|
required=True,
|
|
default=lambda self: fields.Date.today()
|
|
+ relativedelta(months=+2)
|
|
- relativedelta(days=1),
|
|
)
|
|
salary_day = fields.Float(string="日薪")
|
|
party_id = fields.Many2one("yuthon.party.setting", string="甲方(用人单位)")
|
|
representatives = fields.Char(
|
|
related="party_id.representatives", string="法定代表人或委托代理人"
|
|
)
|
|
contract_type_id = fields.Many2one(
|
|
"hr.contract.type",
|
|
default=lambda self: self.env["hr.contract.type"]
|
|
.search([("name", "=", "正式合同(一签)")], limit=1)
|
|
.id
|
|
or False,
|
|
string="合同类型",
|
|
)
|
|
economy_type = fields.Char(related="party_id.economy_type", string="经济类型")
|
|
address = fields.Text(related="party_id.address", string="通讯地址")
|
|
contact = fields.Char(related="party_id.contact", string="联系人")
|
|
phone = fields.Char(related="party_id.phone", string="电话")
|
|
documents_ids = fields.Many2many("ir.attachment", string="合同附件")
|
|
current_contract = fields.Boolean(string="当前合同", default=True, compute="_compute_current_contract", store=True)
|
|
is_company = fields.Boolean(
|
|
string="本公司",
|
|
default=True,
|
|
compute="_compute_is_company",
|
|
search="_search_part_of_company",
|
|
)
|
|
change_ids = fields.One2many('hr.contract.change.line', 'contract_id', string="异动信息")
|
|
|
|
@api.onchange('party_id')
|
|
def _onchange_party_company(self):
|
|
"""UI 交互:切换甲方时自动带出对应公司"""
|
|
if self.party_id and self.party_id.work_address:
|
|
company = self.env['res.company'].search(
|
|
[('name', 'ilike', self.party_id.work_address)],
|
|
limit=1
|
|
)
|
|
if company:
|
|
self.company_id = company
|
|
|
|
def _sync_party_company(self):
|
|
"""根据 party_id 同步合同、员工、用户的公司信息"""
|
|
for rec in self:
|
|
company = False
|
|
if rec.party_id and rec.party_id.work_address:
|
|
company = self.env['res.company'].search(
|
|
[('name', 'ilike', rec.party_id.work_address)],
|
|
limit=1
|
|
)
|
|
if not company:
|
|
continue
|
|
rec.company_id = company
|
|
if rec.employee_id:
|
|
rec.employee_id.company_id = company
|
|
if rec.employee_id.user_id:
|
|
rec.employee_id.user_id.write({
|
|
'company_id': company.id,
|
|
'company_ids': [(4, company.id)]
|
|
})
|
|
|
|
@api.constrains('change_ids')
|
|
def _onchange_change_ids(self):
|
|
today = fields.Date.today()
|
|
for contract in self:
|
|
active_line = False
|
|
for line in contract.change_ids.sorted('change_start_date', reverse=True):
|
|
# change_end_date 为空视为无限大
|
|
if line.change_start_date <= today and (not line.change_end_date or line.change_end_date >= today):
|
|
active_line = line
|
|
break
|
|
if active_line:
|
|
contract.department_id = active_line.department_id
|
|
contract.job_id = active_line.job_id
|
|
contract.structure_type_id = active_line.structure_type_id
|
|
contract.salary_group_id = active_line.salary_group_id
|
|
|
|
def init_change_lines(self):
|
|
"""为选中的合同补初始异动行"""
|
|
contract_ids = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
for contract in contract_ids:
|
|
if not contract.change_ids:
|
|
self.env['hr.contract.change.line'].create({
|
|
'contract_id': contract.id,
|
|
'department_id': contract.department_id.id,
|
|
'job_id': contract.job_id.id,
|
|
'structure_type_id': contract.structure_type_id.id,
|
|
'salary_group_id': contract.salary_group_id.id,
|
|
'change_start_date': contract.date_start,
|
|
'change_end_date': contract.date_end,
|
|
})
|
|
|
|
def update_version(self):
|
|
contract_ids = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
for contract in contract_ids:
|
|
contract.version = "V1.0"
|
|
|
|
def yuthon_employee_add(self):
|
|
contract_ids = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
for record in contract_ids:
|
|
if len(record) > 1:
|
|
raise UserError("只能选择一个合同补充")
|
|
return {
|
|
'type': 'ir.actions.act_window',
|
|
'name': '员工补充协议',
|
|
'res_model': 'yuthon.employee.add',
|
|
'view_mode': 'form',
|
|
'target': 'current',
|
|
'context': {
|
|
'default_work_end': False,
|
|
'default_self_users_ids':[(6, 0, [self.env.user.id])],
|
|
'default_contract_id': contract_ids.id,
|
|
'default_employee_id': contract_ids.employee_id.id
|
|
}}
|
|
|
|
@api.depends("contract_type_id", "employee_id", "is_copy")
|
|
def _compute_name(self):
|
|
for record in self:
|
|
if record.contract_type_id and record.employee_id:
|
|
base_name = record.employee_id.name + record.contract_type_id.name + "合同"
|
|
else:
|
|
base_name = "未完善合同信息"
|
|
|
|
if record.is_copy:
|
|
record.name = f"{base_name}(副本)"
|
|
else:
|
|
record.name = base_name
|
|
|
|
record.is_internship = record.contract_type_id.name == "实习协议" if record.contract_type_id else False
|
|
|
|
@api.returns('self', lambda value: value.id)
|
|
def copy(self, default=None):
|
|
self.ensure_one()
|
|
default = dict(default or {})
|
|
|
|
default['is_copy'] = True
|
|
default['date_start'] = fields.Date.today()
|
|
default['date_end'] = False
|
|
version_str = self.version.lstrip('V')
|
|
main_version = version_str.split('.')[0]
|
|
new_main_version = int(main_version) + 1
|
|
new_version = f"V{new_main_version}.0"
|
|
default['version'] = new_version
|
|
new_record = super(HrContract, self).copy(default=default)
|
|
return new_record
|
|
|
|
@api.depends("date_start", "date_end")
|
|
def _compute_current_contract(self):
|
|
today = date.today()
|
|
for contract in self:
|
|
start_valid = contract.date_start and contract.date_start <= today
|
|
end_valid = not contract.date_end or contract.date_end >= today
|
|
contract.current_contract = start_valid and end_valid
|
|
|
|
@api.onchange("contract_type_id")
|
|
def _onchange_contract_type_id(self):
|
|
for record in self:
|
|
if record.contract_type_id:
|
|
if record.contract_type_id.name == "正式合同(一签)":
|
|
record.date_end = (
|
|
record.date_start
|
|
+ relativedelta(years=1)
|
|
- relativedelta(days=1)
|
|
)
|
|
elif record.contract_type_id.name == "正式合同(二签)":
|
|
record.date_end = (
|
|
record.date_start
|
|
+ relativedelta(years=3)
|
|
- relativedelta(days=1)
|
|
)
|
|
|
|
def renew(self):
|
|
contract_ids = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
for record in contract_ids:
|
|
if len(record) > 1:
|
|
raise UserError("只能选择一个合同续签")
|
|
return {
|
|
"name": "续签",
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "yuthon.renewal.approval",
|
|
"view_mode": "form",
|
|
"context": {
|
|
"default_employee_id": self.employee_id.id,
|
|
"default_current_status": self.employee_id.contract_id.date_end,
|
|
"default_department1_id": self.employee_id.department_id.id,
|
|
"default_renewal_date": self.employee_id.contract_id.date_end + relativedelta(years=3) if self.employee_id.contract_id.date_end else False,
|
|
"default_contract_id": self.id,
|
|
},
|
|
}
|
|
|
|
def pub_leaving_employee(self):
|
|
record_id = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
if len(record_id) != 1:
|
|
raise UserError("请选择一个合同")
|
|
form_id = self.env.ref(
|
|
"yuthon_hr_employee_approval.view_yuthon_leaving_approval_form"
|
|
).id
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"name": "离职申请",
|
|
"res_model": "yuthon.leaving.approval",
|
|
"views": [[form_id, "form"]],
|
|
"context": {
|
|
"default_employee_id": self.employee_id.id,
|
|
},
|
|
}
|
|
|
|
def write(self, vals):
|
|
"""在写入时自动更新合同状态"""
|
|
today = fields.Date.today()
|
|
update_state = False
|
|
if "date_start" in vals:
|
|
update_state = True
|
|
if "date_end" in vals:
|
|
update_state = True
|
|
if "termination_date" in vals:
|
|
update_state = True
|
|
if update_state:
|
|
for contract in self:
|
|
# 获取最新的开始日期
|
|
start_date = fields.Date.to_date(
|
|
vals.get("date_start", contract.date_start)
|
|
)
|
|
|
|
# 获取最新的结束日期(空值视为无限大)
|
|
end_date = vals.get("date_end", contract.date_end)
|
|
end_date = fields.Date.to_date(end_date) if end_date else None
|
|
|
|
# 获取最新的中止时间(空值视为未中止)
|
|
termination_date = vals.get(
|
|
"termination_date", contract.termination_date
|
|
)
|
|
termination_date = (
|
|
fields.Date.to_date(termination_date) if termination_date else None
|
|
)
|
|
|
|
# 判断合同状态
|
|
if (
|
|
termination_date and termination_date < today
|
|
): # 新增:优先判断是否已中止
|
|
new_state = "cancel"
|
|
elif start_date > today:
|
|
new_state = "draft"
|
|
elif end_date and end_date < today:
|
|
new_state = "close"
|
|
else:
|
|
new_state = "open"
|
|
|
|
# 更新当前合同的状态
|
|
if "state" not in vals:
|
|
vals["state"] = new_state
|
|
|
|
return super(HrContract, self).write(vals)
|
|
|
|
def update_contract_states(self):
|
|
today = date.today()
|
|
active_contracts = self.search([
|
|
("state", "not in", ["cancel"]),
|
|
])
|
|
for contract in active_contracts:
|
|
new_state = None
|
|
# 退休判断:终止日期 >= 退休日期
|
|
is_retire = False
|
|
if contract.termination_date and contract.employee_id.retire_date:
|
|
if contract.termination_date >= contract.employee_id.retire_date:
|
|
new_state = "cancel" # 合同状态还是取消
|
|
is_retire = True # 标记为退休
|
|
# 正常状态判断
|
|
if not new_state:
|
|
if contract.termination_date:
|
|
new_state = "cancel"
|
|
elif not contract.date_end or contract.date_end >= today:
|
|
new_state = "open"
|
|
else:
|
|
new_state = "close"
|
|
|
|
if new_state == "open":
|
|
conflicting_contracts = self.search([
|
|
("id", "!=", contract.id),
|
|
("employee_id", "=", contract.employee_id.id),
|
|
("state", "=", "open"),
|
|
])
|
|
if conflicting_contracts:
|
|
continue
|
|
|
|
contract.state = new_state
|
|
if contract.employee_id:
|
|
if is_retire:
|
|
contract.employee_id.sudo().write({"state": "withdraw"})
|
|
elif new_state == "cancel":
|
|
contract.employee_id.sudo().write({"state": "leave"})
|
|
|
|
@api.depends_context("uid", "company")
|
|
@api.depends("company_id")
|
|
def _compute_is_company(self):
|
|
active_company_ids = self.env.companies
|
|
for employee in self:
|
|
employee.is_company = employee.company_id in active_company_ids
|
|
|
|
def _search_part_of_company(self, operator, value):
|
|
if operator not in ("=", "!=") or not isinstance(value, bool):
|
|
raise UserError(("Operation not supported"))
|
|
company_ids = self.env.companies.ids # 获取公司ID列表
|
|
# 处理反向操作符
|
|
if not value:
|
|
operator = "!=" if operator == "=" else "="
|
|
# 使用company_id in company_ids的结构
|
|
if operator == "=":
|
|
return [("company_id", "in", company_ids)]
|
|
else: # operator == '!='
|
|
return [("company_id", "not in", company_ids)]
|
|
|
|
@api.depends("contract_type_id")
|
|
def _compute_simplified_name(self):
|
|
for record in self:
|
|
if record.contract_type_id and record.employee_id:
|
|
record.name = record.contract_type_id.name + "合同"
|
|
else:
|
|
record.name = " "
|
|
if record.contract_type_id:
|
|
if record.contract_type_id.name == "实习协议":
|
|
record.is_internship = True
|
|
else:
|
|
record.is_internship = False
|
|
|
|
def action_hr_contract(self):
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"name": "批量导入合同",
|
|
"res_model": "yuthon.contract.wizard",
|
|
"view_mode": "form",
|
|
"target": "new",
|
|
}
|
|
|
|
def update_contract(self):
|
|
contract_ids = self.env["hr.contract"].browse(
|
|
self._context.get("active_ids", self._context.get("active_id"))
|
|
)
|
|
for contract in contract_ids:
|
|
company_id = contract.employee_id.company_id
|
|
resource_calendar_id = self.env["resource.calendar"].search(
|
|
[("company_id", "=", company_id.id)], limit=1
|
|
)
|
|
contract.resource_calendar_id = resource_calendar_id.id
|
|
|
|
@api.onchange("salary_group_id")
|
|
def _compute_salary_group_id(self):
|
|
for contract_id in self:
|
|
employee = self.env["hr.employee"].search(
|
|
[("id", "=", contract_id.employee_id.id)], limit=1
|
|
)
|
|
if employee:
|
|
employee.salary_group_id = contract_id.salary_group_id.id
|
|
else:
|
|
employee.salary_group_id = False
|
|
|
|
@api.onchange("is_annual_salary")
|
|
def _compute_is_annual_salary(self):
|
|
for contract_id in self:
|
|
employee = self.env["hr.employee"].search(
|
|
[("id", "=", contract_id.employee_id.id)], limit=1
|
|
)
|
|
if employee:
|
|
employee.is_annual_salary = contract_id.is_annual_salary
|
|
else:
|
|
employee.is_annual_salary = False
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
today = fields.Date.today()
|
|
|
|
# 先处理状态
|
|
for vals in vals_list:
|
|
start_date = (
|
|
fields.Date.to_date(vals.get("date_start"))
|
|
if vals.get("date_start")
|
|
else None
|
|
)
|
|
end_date = (
|
|
fields.Date.to_date(vals.get("date_end"))
|
|
if vals.get("date_end")
|
|
else None
|
|
)
|
|
termination_date = (
|
|
fields.Date.to_date(vals.get("termination_date"))
|
|
if vals.get("termination_date")
|
|
else None
|
|
)
|
|
|
|
if termination_date and termination_date < today:
|
|
vals["state"] = "cancel"
|
|
elif start_date and start_date > today:
|
|
vals["state"] = "draft"
|
|
elif end_date and end_date < today:
|
|
vals["state"] = "close"
|
|
else:
|
|
vals["state"] = "open"
|
|
|
|
# 创建合同
|
|
contracts = super().create(vals_list)
|
|
|
|
# 更新员工合同字段
|
|
for contract in contracts:
|
|
if contract.employee_id:
|
|
contract.employee_id.write({"contract_id": contract.id,
|
|
'is_accum_salary': True,
|
|
'is_membership': True,
|
|
})
|
|
contracts._sync_party_company()
|
|
|
|
# 创建合同时自动初始化异动信息明细行
|
|
for contract in contracts:
|
|
if not contract.change_ids:
|
|
self.env['hr.contract.change.line'].create({
|
|
'contract_id': contract.id,
|
|
'department_id': contract.department_id.id,
|
|
'job_id': contract.job_id.id,
|
|
'structure_type_id': contract.structure_type_id.id,
|
|
'salary_group_id': contract.salary_group_id.id,
|
|
'change_start_date': contract.date_start,
|
|
'change_end_date': contract.date_end,
|
|
})
|
|
|
|
return contracts
|
|
|
|
|
|
class HrContractChangeLine(models.Model):
|
|
_name = 'hr.contract.change.line'
|
|
_description = '异动信息明细行'
|
|
|
|
contract_id = fields.Many2one('hr.contract', string="关联合同")
|
|
department_id = fields.Many2one('hr.department', string="部门")
|
|
job_id = fields.Many2one('hr.job', string="工作岗位")
|
|
structure_type_id = fields.Many2one('hr.payroll.structure.type', string="工资结构类型")
|
|
salary_group_id = fields.Many2one('yuthon.salary.group', string="工资单分组")
|
|
change_start_date = fields.Date(string="开始日期")
|
|
change_end_date = fields.Date(string="结束日期")
|
|
|