diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/hr_contract/models | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/hr_contract/models')
| -rw-r--r-- | addons/hr_contract/models/__init__.py | 8 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_contract.py | 232 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_contract_type.py | 15 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_employee.py | 77 | ||||
| -rw-r--r-- | addons/hr_contract/models/res_users.py | 25 | ||||
| -rw-r--r-- | addons/hr_contract/models/resource.py | 28 |
6 files changed, 385 insertions, 0 deletions
diff --git a/addons/hr_contract/models/__init__.py b/addons/hr_contract/models/__init__.py new file mode 100644 index 00000000..4c75f9fb --- /dev/null +++ b/addons/hr_contract/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import hr_employee +from . import hr_contract +from . import res_users +from . import resource +from . import hr_contract_type diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py new file mode 100644 index 00000000..71a0107f --- /dev/null +++ b/addons/hr_contract/models/hr_contract.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + +from odoo.osv import expression + +class Contract(models.Model): + _name = 'hr.contract' + _description = 'Contract' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char('Contract Reference', required=True) + active = fields.Boolean(default=True) + structure_type_id = fields.Many2one('hr.payroll.structure.type', string="Salary Structure Type") + employee_id = fields.Many2one('hr.employee', string='Employee', tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + department_id = fields.Many2one('hr.department', compute='_compute_employee_contract', store=True, readonly=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", string="Department") + job_id = fields.Many2one('hr.job', compute='_compute_employee_contract', store=True, readonly=False, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", string='Job Position') + date_start = fields.Date('Start Date', required=True, default=fields.Date.today, tracking=True, + help="Start date of the contract.") + date_end = fields.Date('End Date', tracking=True, + help="End date of the contract (if it's a fixed-term contract).") + trial_date_end = fields.Date('End of Trial Period', + help="End date of the trial period (if there is one).") + resource_calendar_id = fields.Many2one( + 'resource.calendar', 'Working Schedule', compute='_compute_employee_contract', store=True, readonly=False, + default=lambda self: self.env.company.resource_calendar_id.id, copy=False, index=True, + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") + wage = fields.Monetary('Wage', required=True, tracking=True, help="Employee's monthly gross wage.") + notes = fields.Text('Notes') + state = fields.Selection([ + ('draft', 'New'), + ('open', 'Running'), + ('close', 'Expired'), + ('cancel', 'Cancelled') + ], string='Status', group_expand='_expand_states', copy=False, + tracking=True, help='Status of the contract', default='draft') + company_id = fields.Many2one('res.company', compute='_compute_employee_contract', store=True, readonly=False, + default=lambda self: self.env.company, required=True) + company_country_id = fields.Many2one('res.country', string="Company country", related='company_id.country_id', readonly=True) + + """ + kanban_state: + * draft + green = "Incoming" state (will be set as Open once the contract has started) + * open + red = "Pending" state (will be set as Closed once the contract has ended) + * red = Shows a warning on the employees kanban view + """ + kanban_state = fields.Selection([ + ('normal', 'Grey'), + ('done', 'Green'), + ('blocked', 'Red') + ], string='Kanban State', default='normal', tracking=True, copy=False) + currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True) + permit_no = fields.Char('Work Permit No', related="employee_id.permit_no", readonly=False) + visa_no = fields.Char('Visa No', related="employee_id.visa_no", readonly=False) + visa_expire = fields.Date('Visa Expire Date', related="employee_id.visa_expire", readonly=False) + hr_responsible_id = fields.Many2one('res.users', 'HR Responsible', tracking=True, + help='Person responsible for validating the employee\'s contracts.') + calendar_mismatch = fields.Boolean(compute='_compute_calendar_mismatch') + first_contract_date = fields.Date(related='employee_id.first_contract_date') + + @api.depends('employee_id.resource_calendar_id', 'resource_calendar_id') + def _compute_calendar_mismatch(self): + for contract in self: + contract.calendar_mismatch = contract.resource_calendar_id != contract.employee_id.resource_calendar_id + + def _expand_states(self, states, domain, order): + return [key for key, val in type(self).state.selection] + + @api.depends('employee_id') + def _compute_employee_contract(self): + for contract in self.filtered('employee_id'): + contract.job_id = contract.employee_id.job_id + contract.department_id = contract.employee_id.department_id + contract.resource_calendar_id = contract.employee_id.resource_calendar_id + contract.company_id = contract.employee_id.company_id + + @api.onchange('company_id') + def _onchange_company_id(self): + if self.company_id: + structure_types = self.env['hr.payroll.structure.type'].search([ + '|', + ('country_id', '=', self.company_id.country_id.id), + ('country_id', '=', False)]) + if structure_types: + self.structure_type_id = structure_types[0] + elif self.structure_type_id not in structure_types: + self.structure_type_id = False + + @api.onchange('structure_type_id') + def _onchange_structure_type_id(self): + if self.structure_type_id.default_resource_calendar_id: + self.resource_calendar_id = self.structure_type_id.default_resource_calendar_id + + @api.constrains('employee_id', 'state', 'kanban_state', 'date_start', 'date_end') + def _check_current_contract(self): + """ Two contracts in state [incoming | open | close] cannot overlap """ + for contract in self.filtered(lambda c: (c.state not in ['draft', 'cancel'] or c.state == 'draft' and c.kanban_state == 'done') and c.employee_id): + domain = [ + ('id', '!=', contract.id), + ('employee_id', '=', contract.employee_id.id), + '|', + ('state', 'in', ['open', 'close']), + '&', + ('state', '=', 'draft'), + ('kanban_state', '=', 'done') # replaces incoming + ] + + if not contract.date_end: + start_domain = [] + end_domain = ['|', ('date_end', '>=', contract.date_start), ('date_end', '=', False)] + else: + start_domain = [('date_start', '<=', contract.date_end)] + end_domain = ['|', ('date_end', '>', contract.date_start), ('date_end', '=', False)] + + domain = expression.AND([domain, start_domain, end_domain]) + if self.search_count(domain): + raise ValidationError(_('An employee can only have one contract at the same time. (Excluding Draft and Cancelled contracts)')) + + @api.constrains('date_start', 'date_end') + def _check_dates(self): + if self.filtered(lambda c: c.date_end and c.date_start > c.date_end): + raise ValidationError(_('Contract start date must be earlier than contract end date.')) + + @api.model + def update_state(self): + contracts = self.search([ + ('state', '=', 'open'), ('kanban_state', '!=', 'blocked'), + '|', + '&', + ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=7))), + ('date_end', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), + '&', + ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=60))), + ('visa_expire', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), + ]) + + for contract in contracts: + contract.activity_schedule( + 'mail.mail_activity_data_todo', contract.date_end, + _("The contract of %s is about to expire.", contract.employee_id.name), + user_id=contract.hr_responsible_id.id or self.env.uid) + + contracts.write({'kanban_state': 'blocked'}) + + self.search([ + ('state', '=', 'open'), + '|', + ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), + ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), + ]).write({ + 'state': 'close' + }) + + self.search([('state', '=', 'draft'), ('kanban_state', '=', 'done'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({ + 'state': 'open' + }) + + contract_ids = self.search([('date_end', '=', False), ('state', '=', 'close'), ('employee_id', '!=', False)]) + # Ensure all closed contract followed by a new contract have a end date. + # If closed contract has no closed date, the work entries will be generated for an unlimited period. + for contract in contract_ids: + next_contract = self.search([ + ('employee_id', '=', contract.employee_id.id), + ('state', 'not in', ['cancel', 'new']), + ('date_start', '>', contract.date_start) + ], order="date_start asc", limit=1) + if next_contract: + contract.date_end = next_contract.date_start - relativedelta(days=1) + continue + next_contract = self.search([ + ('employee_id', '=', contract.employee_id.id), + ('date_start', '>', contract.date_start) + ], order="date_start asc", limit=1) + if next_contract: + contract.date_end = next_contract.date_start - relativedelta(days=1) + + return True + + def _assign_open_contract(self): + for contract in self: + contract.employee_id.sudo().write({'contract_id': contract.id}) + + def _get_contract_wage(self): + self.ensure_one() + return self[self._get_contract_wage_field()] + + def _get_contract_wage_field(self): + self.ensure_one() + return 'wage' + + def write(self, vals): + res = super(Contract, self).write(vals) + if vals.get('state') == 'open': + self._assign_open_contract() + if vals.get('state') == 'close': + for contract in self.filtered(lambda c: not c.date_end): + contract.date_end = max(date.today(), contract.date_start) + + calendar = vals.get('resource_calendar_id') + if calendar: + self.filtered(lambda c: c.state == 'open' or (c.state == 'draft' and c.kanban_state == 'done')).mapped('employee_id').write({'resource_calendar_id': calendar}) + + if 'state' in vals and 'kanban_state' not in vals: + self.write({'kanban_state': 'normal'}) + + return res + + @api.model + def create(self, vals): + contracts = super(Contract, self).create(vals) + if vals.get('state') == 'open': + contracts._assign_open_contract() + open_contracts = contracts.filtered(lambda c: c.state == 'open' or c.state == 'draft' and c.kanban_state == 'done') + # sync contract calendar -> calendar employee + for contract in open_contracts.filtered(lambda c: c.employee_id and c.resource_calendar_id): + contract.employee_id.resource_calendar_id = contract.resource_calendar_id + return contracts + + def _track_subtype(self, init_values): + self.ensure_one() + if 'state' in init_values and self.state == 'open' and 'kanban_state' in init_values and self.kanban_state == 'blocked': + return self.env.ref('hr_contract.mt_contract_pending') + elif 'state' in init_values and self.state == 'close': + return self.env.ref('hr_contract.mt_contract_close') + return super(Contract, self)._track_subtype(init_values) diff --git a/addons/hr_contract/models/hr_contract_type.py b/addons/hr_contract/models/hr_contract_type.py new file mode 100644 index 00000000..4e25f859 --- /dev/null +++ b/addons/hr_contract/models/hr_contract_type.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class HrPayrollStructureType(models.Model): + _name = 'hr.payroll.structure.type' + _description = 'Contract Type' + + name = fields.Char('Contract Type') + default_resource_calendar_id = fields.Many2one( + 'resource.calendar', 'Default Working Hours', + default=lambda self: self.env.company.resource_calendar_id) + country_id = fields.Many2one('res.country', string='Country', default=lambda self: self.env.company.country_id) diff --git a/addons/hr_contract/models/hr_employee.py b/addons/hr_contract/models/hr_employee.py new file mode 100644 index 00000000..8da454c2 --- /dev/null +++ b/addons/hr_contract/models/hr_employee.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.osv import expression + + +class Employee(models.Model): + _inherit = "hr.employee" + + vehicle = fields.Char(string='Company Vehicle', groups="hr.group_hr_user") + contract_ids = fields.One2many('hr.contract', 'employee_id', string='Employee Contracts') + contract_id = fields.Many2one('hr.contract', string='Current Contract', + groups="hr.group_hr_user",domain="[('company_id', '=', company_id)]", help='Current contract of the employee') + calendar_mismatch = fields.Boolean(related='contract_id.calendar_mismatch') + contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count') + contract_warning = fields.Boolean(string='Contract Warning', store=True, compute='_compute_contract_warning', groups="hr.group_hr_user") + first_contract_date = fields.Date(compute='_compute_first_contract_date', groups="hr.group_hr_user") + + def _get_first_contracts(self): + self.ensure_one() + return self.sudo().contract_ids.filtered(lambda c: c.state != 'cancel') + + @api.depends('contract_ids.state', 'contract_ids.date_start') + def _compute_first_contract_date(self): + for employee in self: + contracts = employee._get_first_contracts() + if contracts: + employee.first_contract_date = min(contracts.mapped('date_start')) + else: + employee.first_contract_date = False + + @api.depends('contract_id', 'contract_id.state', 'contract_id.kanban_state') + def _compute_contract_warning(self): + for employee in self: + employee.contract_warning = not employee.contract_id or employee.contract_id.kanban_state == 'blocked' or employee.contract_id.state != 'open' + + def _compute_contracts_count(self): + # read_group as sudo, since contract count is displayed on form view + contract_data = self.env['hr.contract'].sudo().read_group([('employee_id', 'in', self.ids)], ['employee_id'], ['employee_id']) + result = dict((data['employee_id'][0], data['employee_id_count']) for data in contract_data) + for employee in self: + employee.contracts_count = result.get(employee.id, 0) + + def _get_contracts(self, date_from, date_to, states=['open'], kanban_state=False): + """ + Returns the contracts of the employee between date_from and date_to + """ + state_domain = [('state', 'in', states)] + if kanban_state: + state_domain = expression.AND([state_domain, [('kanban_state', 'in', kanban_state)]]) + + return self.env['hr.contract'].search( + expression.AND([[('employee_id', 'in', self.ids)], + state_domain, + [('date_start', '<=', date_to), + '|', + ('date_end', '=', False), + ('date_end', '>=', date_from)]])) + + def _get_incoming_contracts(self, date_from, date_to): + return self._get_contracts(date_from, date_to, states=['draft'], kanban_state=['done']) + + @api.model + def _get_all_contracts(self, date_from, date_to, states=['open']): + """ + Returns the contracts of all employees between date_from and date_to + """ + return self.search([])._get_contracts(date_from, date_to, states=states) + + def write(self, vals): + res = super(Employee, self).write(vals) + if vals.get('contract_id'): + for employee in self: + employee.resource_calendar_id.transfer_leaves_to(employee.contract_id.resource_calendar_id, employee.resource_id) + employee.resource_calendar_id = employee.contract_id.resource_calendar_id + return res diff --git a/addons/hr_contract/models/res_users.py b/addons/hr_contract/models/res_users.py new file mode 100644 index 00000000..5bf81915 --- /dev/null +++ b/addons/hr_contract/models/res_users.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, _ + + +class User(models.Model): + _inherit = ['res.users'] + + vehicle = fields.Char(related="employee_id.vehicle") + bank_account_id = fields.Many2one(related="employee_id.bank_account_id") + + def __init__(self, pool, cr): + """ Override of __init__ to add access rights. + Access rights are disabled by default, but allowed + on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. + """ + contract_readable_fields = [ + 'vehicle', + 'bank_account_id', + ] + init_res = super(User, self).__init__(pool, cr) + # duplicate list to avoid modifying the original reference + type(self).SELF_READABLE_FIELDS = type(self).SELF_READABLE_FIELDS + contract_readable_fields + return init_res diff --git a/addons/hr_contract/models/resource.py b/addons/hr_contract/models/resource.py new file mode 100644 index 00000000..865e8733 --- /dev/null +++ b/addons/hr_contract/models/resource.py @@ -0,0 +1,28 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime + +from odoo import models, fields, api +from odoo.osv.expression import AND + + +class ResourceCalendar(models.Model): + _inherit = 'resource.calendar' + + def transfer_leaves_to(self, other_calendar, resources=None, from_date=None): + """ + Transfer some resource.calendar.leaves from 'self' to another calendar 'other_calendar'. + Transfered leaves linked to `resources` (or all if `resources` is None) and starting + after 'from_date' (or today if None). + """ + from_date = from_date or fields.Datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + domain = [ + ('calendar_id', 'in', self.ids), + ('date_from', '>=', from_date), + ] + domain = AND([domain, [('resource_id', 'in', resources.ids)]]) if resources else domain + + self.env['resource.calendar.leaves'].search(domain).write({ + 'calendar_id': other_calendar.id, + }) + |
