diff options
| author | Lucas Lefèvre <lul@odoo.com> | 2018-12-19 12:32:05 +0000 |
|---|---|---|
| committer | Yannick Tivisse <yti@odoo.com> | 2019-02-01 12:16:54 +0000 |
| commit | 20bc2bbab5839bdbbf6b86972e677e10102e5bac (patch) | |
| tree | ed25aeb7b0619c5779032abd1d02e905853ad618 /addons/hr_contract | |
| parent | f5bca867ac7f7d8dd8d530f0df740d0931d18e0b (diff) | |
[IMP] hr_payroll: add multi contract support
Purpose
=======
We would like to be able to manage multi contracts payslips and contract transitions.
Specification
=============
General flow:
1/ Benefits are generated based on contracts's calendar.
2/ When validating additionnal benefits, calendar.attendances are created in the contract's calendar
3/ Create one payslip per contract in state [open|pending|close]
4/ For each payslip, generate worked day lines based on the contract's calendar
New Constraints:
1/ An employee cannot have more than one contract at the same time (excluding draft, closed and cancelled contracts).
2/ A leave cannot overlap multiple contracts.
3/ A contract cannot have starting and ending dates such that a leave become in the middle of
two contacts.
New state for a contract: Incoming. It means it's ready and will be running soon.
An incoming contract automatically becomes 'Running' if the start date is passed (cron)
Number of hours and days are always correct (according to the contract's calendar) on payslips as the
flow starts from the contract's calendar.
However, it is not correct in the Time Off app because it's counted based on the employee's calendar.
The number of days/hours for hr.leaves should be computed according to
the contract at the time of the leave.
e.g. If I currently work full time and I request 2 weeks off next month where I'll be working only
3 days/week and 5hours/day (other contract), It should count 6 days and 30 hours.
- in hr.benefits form view show the contract (below employee field)
TaskID: 1903483
closes odoo/odoo#30181
Diffstat (limited to 'addons/hr_contract')
| -rw-r--r-- | addons/hr_contract/data/hr_contract_demo.xml | 4 | ||||
| -rw-r--r-- | addons/hr_contract/models/hr_contract.py | 55 | ||||
| -rw-r--r-- | addons/hr_contract/tests/__init__.py | 1 | ||||
| -rw-r--r-- | addons/hr_contract/tests/test_auto_status.py | 13 | ||||
| -rw-r--r-- | addons/hr_contract/tests/test_contract.py | 86 |
5 files changed, 143 insertions, 16 deletions
diff --git a/addons/hr_contract/data/hr_contract_demo.xml b/addons/hr_contract/data/hr_contract_demo.xml index 6e8326a0203..eef59fd9813 100644 --- a/addons/hr_contract/data/hr_contract_demo.xml +++ b/addons/hr_contract/data/hr_contract_demo.xml @@ -11,7 +11,7 @@ <field name="department_id" model="hr.department" eval="obj().env.ref('hr.employee_admin').department_id.id"/> <field eval="7540.0" name="wage"/> - <field name="state">open</field> + <field name="state">draft</field> </record> <record id="hr_contract_al" model="hr.contract"> @@ -64,7 +64,7 @@ <field name="department_id" model="hr.department" eval="obj().env.ref('hr.employee_qdp').department_id.id"/> <field eval="3750.0" name="wage"/> - <field name="state">open</field> + <field name="state">draft</field> </record> <record id="hr_contract_han" model="hr.contract"> diff --git a/addons/hr_contract/models/hr_contract.py b/addons/hr_contract/models/hr_contract.py index 21aaa77487d..938ae2d06a8 100644 --- a/addons/hr_contract/models/hr_contract.py +++ b/addons/hr_contract/models/hr_contract.py @@ -16,15 +16,9 @@ class Employee(models.Model): medic_exam = fields.Date(string='Medical Examination Date', groups="hr.group_hr_user") 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', compute='_compute_contract_id', string='Current Contract', help='Latest contract of the employee') + contract_id = fields.Many2one('hr.contract', string='Current Contract', help='Current contract of the employee') contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count') - def _compute_contract_id(self): - """ get the lastest contract """ - Contract = self.env['hr.contract'] - for employee in self: - employee.contract_id = Contract.search([('employee_id', '=', employee.id)], order='date_start desc', limit=1) - 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']) @@ -32,25 +26,28 @@ class Employee(models.Model): for employee in self: employee.contracts_count = result.get(employee.id, 0) - def _get_contracts(self, date_from, date_to): + def _get_contracts(self, date_from, date_to, states=['open', 'pending']): """ Returns the contracts of the employee between date_from and date_to """ # a contract is valid if it ends between the given dates - if not self: - employees = self.env['hr.employee'].search([]) - else: - employees = self clause_1 = ['&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)] # OR if it starts between the given dates clause_2 = ['&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)] # OR if it starts before the date_from and finish after the date_end (or never finish) clause_3 = ['&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to)] clause_final = expression.AND([ - [('employee_id', 'in', employees.ids), ('state', '=', 'open')], + [('employee_id', 'in', self.ids), ('state', 'in', states)], expression.OR([clause_1, clause_2, clause_3])]) return self.env['hr.contract'].search(clause_final) + @api.model + def _get_all_contracts(self, date_from, date_to, states=['open', 'pending']): + """ + Returns the contracts of all employees between date_from and date_to + """ + return self.search([])._get_contracts(date_from, date_to, states=states) + class ContractType(models.Model): @@ -88,6 +85,7 @@ class Contract(models.Model): notes = fields.Text('Notes') state = fields.Selection([ ('draft', 'New'), + ('incoming', 'Incoming'), ('open', 'Running'), ('pending', 'To Renew'), ('close', 'Expired'), @@ -112,6 +110,27 @@ class Contract(models.Model): self.department_id = self.employee_id.department_id self.resource_calendar_id = self.employee_id.resource_calendar_id + @api.constrains('employee_id', 'state', 'date_start', 'date_end') + def _check_current_contract(self): + """ Two contracts in state [incoming | pending | open | close] cannot overlap """ + for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']): + domain = [ + ('id', '!=', contract.id), + ('employee_id', '=', contract.employee_id.id), + ('state', 'in', ['incoming', 'pending', 'open', 'close']), + ] + + 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): @@ -141,9 +160,19 @@ class Contract(models.Model): 'state': 'close' }) + self.search([('state', '=', 'incoming'), ('date_start', '<=', fields.Date.to_string(date.today())),]).write({ + 'state': 'open' + }) return True @api.multi + def write(self, vals): + if vals.get('state') == 'open': + for contract in self: + contract.employee_id.contract_id = contract + return super(Contract, self).write(vals) + + @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'pending': diff --git a/addons/hr_contract/tests/__init__.py b/addons/hr_contract/tests/__init__.py index 6f1832811c2..ad627a612a4 100644 --- a/addons/hr_contract/tests/__init__.py +++ b/addons/hr_contract/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_auto_status +from . import test_contract diff --git a/addons/hr_contract/tests/test_auto_status.py b/addons/hr_contract/tests/test_auto_status.py index 580a6d17423..20b8e4007fc 100644 --- a/addons/hr_contract/tests/test_auto_status.py +++ b/addons/hr_contract/tests/test_auto_status.py @@ -10,7 +10,12 @@ class TestHrContracts(TransactionCase): def setUp(self): super(TestHrContracts, self).setUp() self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) - self.employee = self.env.ref('hr.employee_admin') + self.employee = self.env['hr.employee'].create({ + 'name': 'Richard', + 'gender': 'male', + 'birthday': '1984-05-01', + 'country_id': self.ref('base.be'), + }) self.test_contract = dict(name='Test', wage=1, employee_id=self.employee.id, state='open') def apply_cron(self): @@ -51,3 +56,9 @@ class TestHrContracts(TransactionCase): self.contract.write(self.test_contract) self.apply_cron() self.assertEquals(self.contract.state, 'close') + + def test_contract_start_date(self): + self.test_contract.update(dict(date_start=datetime.now(), state='incoming')) + self.contract = self.contracts.create(self.test_contract) + self.apply_cron() + self.assertEquals(self.contract.state, 'open') diff --git a/addons/hr_contract/tests/test_contract.py b/addons/hr_contract/tests/test_contract.py new file mode 100644 index 00000000000..39d6743d275 --- /dev/null +++ b/addons/hr_contract/tests/test_contract.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from dateutil.relativedelta import relativedelta + + +class TestHrContracts(TransactionCase): + + def setUp(self): + super(TestHrContracts, self).setUp() + self.contracts = self.env['hr.contract'].with_context(tracking_disable=True) + self.employee = self.env['hr.employee'].create({ + 'name': 'Richard', + 'gender': 'male', + 'birthday': '1984-05-01', + 'country_id': self.ref('base.be'), + }) + + def create_contract(self, state, start, end=None): + return self.env['hr.contract'].create({ + 'name': 'Contract', + 'employee_id': self.employee.id, + 'state': state, + 'wage': 1, + 'date_start': start, + 'date_end': end, + }) + + def test_incoming_overlapping_contract(self): + start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() + end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() + self.create_contract('open', start, end) + + # Incoming contract + with self.assertRaises(ValidationError, msg="It should not create two contract in state open or incoming"): + start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('incoming', start, end) + + def test_pending_overlapping_contract(self): + start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() + end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() + self.create_contract('open', start, end) + + # Pending contract + with self.assertRaises(ValidationError, msg="It should not create two contract in state open or pending"): + start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('pending', start, end) + + # Draft contract -> should not raise + start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('draft', start, end) + + def test_draft_overlapping_contract(self): + start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() + end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() + self.create_contract('open', start, end) + + # Draft contract -> should not raise even if overlapping + start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('draft', start, end) + + def test_overlapping_contract_no_end(self): + + # No end date + self.create_contract('open', datetime.strptime('2015-11-01', '%Y-%m-%d').date()) + + with self.assertRaises(ValidationError): + start = datetime.strptime('2015-11-15', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('incoming', start, end) + + def test_overlapping_contract_no_end2(self): + + start = datetime.strptime('2015-11-01', '%Y-%m-%d').date() + end = datetime.strptime('2015-12-30', '%Y-%m-%d').date() + self.create_contract('open', start, end) + + with self.assertRaises(ValidationError): + # No end + self.create_contract('incoming', datetime.strptime('2015-01-01', '%Y-%m-%d').date()) |
