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 @@ - open + draft @@ -64,7 +64,7 @@ - open + draft 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,8 +160,18 @@ 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() 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()) diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py index bd99e999a12..c25ebfd8afb 100644 --- a/addons/hr_payroll/models/hr_benefit.py +++ b/addons/hr_payroll/models/hr_benefit.py @@ -21,6 +21,7 @@ class HrBenefit(models.Model): date_start = fields.Datetime(required=True, string='From') date_stop = fields.Datetime(string='To') duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Period") + contract_id = fields.Many2one('hr.contract', string="Contract", required=True) benefit_type_id = fields.Many2one('hr.benefit.type') color = fields.Integer(related='benefit_type_id.color', readonly=True) state = fields.Selection([ @@ -190,6 +191,7 @@ class HrBenefit(models.Model): 'name': benefit.name, 'employee_id': benefit.employee_id.id, 'benefit_type_id': benefit.benefit_type_id.id, + 'contract_id': benefit.contract_id.id, } benefit_state = benefit.state benefit.unlink() @@ -256,7 +258,7 @@ class HrBenefit(models.Model): 'date_to': end.date(), 'hour_from':start.hour + start.minute/60, 'hour_to': end.hour + end.minute/60, - 'calendar_id': benefit.employee_id.resource_calendar_id.id, + 'calendar_id': benefit.contract_id.resource_calendar_id.id, 'day_period': 'morning' if end.hour <= 12 else 'afternoon', 'resource_id': benefit.employee_id.resource_id.id, 'benefit_type_id': benefit.benefit_type_id.id, diff --git a/addons/hr_payroll/models/hr_contract.py b/addons/hr_payroll/models/hr_contract.py index 3fb14c8988f..53217a4b4b9 100644 --- a/addons/hr_payroll/models/hr_contract.py +++ b/addons/hr_payroll/models/hr_contract.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import api, fields, models - +from datetime import date class HrContract(models.Model): _inherit = 'hr.contract' @@ -25,6 +25,18 @@ class HrContract(models.Model): full_time_required_hours = fields.Float(related='resource_calendar_id.full_time_required_hours') is_fulltime = fields.Boolean(related='resource_calendar_id.is_fulltime') + @api.constrains('date_start', 'date_end', 'state') + def _check_contracts(self): + self._get_leaves()._check_contracts() + + @api.multi + def _get_leaves(self): + return self.env['hr.leave'].search([ + ('employee_id', 'in', self.mapped('employee_id.id')), + ('date_from', '<=', max([end or date.max for end in self.mapped('date_end')])), + ('date_to', '>=', min(self.mapped('date_start'))), + ]) + @api.multi def get_all_structures(self): """ diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py index 2ab7a292149..ec3d6df3198 100644 --- a/addons/hr_payroll/models/hr_employee.py +++ b/addons/hr_payroll/models/hr_employee.py @@ -33,8 +33,14 @@ class HrEmployee(models.Model): ('state', 'in', ['draft', 'confirmed']) ])) - - + @api.multi + def write(self, vals): + res = super(HrEmployee, 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 @api.model def generate_benefit(self, date_start, date_stop): @@ -47,7 +53,7 @@ class HrEmployee(models.Model): date_start = _format_datetime(date_start) date_stop = _format_datetime(date_stop) - current_contracts = self.env['hr.employee']._get_contracts(date_start, date_stop) + current_contracts = self.env['hr.employee']._get_all_contracts(date_start, date_stop, states=['open', 'pending', 'close']) current_employees = current_contracts.mapped('employee_id') mapped_data = dict.fromkeys(current_employees, self.env['hr.contract']) @@ -55,6 +61,7 @@ class HrEmployee(models.Model): mapped_data[contract.employee_id] |= contract for employee, contracts in mapped_data.items(): + # Approved leaves emp_leaves = employee.resource_calendar_id.leave_ids.filtered( lambda r: @@ -63,9 +70,7 @@ class HrEmployee(models.Model): r.date_to.replace(tzinfo=pytz.utc) >= date_start ) global_leaves = employee.resource_calendar_id.global_leave_ids - for calendar_leave in emp_leaves | global_leaves: - hr_leave = calendar_leave.holiday_id - hr_leave.copy_to_benefits() + (emp_leaves | global_leaves).mapped('holiday_id').copy_to_benefits() new_benefits = self.env['hr.benefit'] for contract in contracts: @@ -85,6 +90,7 @@ class HrEmployee(models.Model): 'date_stop': interval[1].astimezone(pytz.utc), 'benefit_type_id': benefit_type_id.id, 'employee_id': employee.id, + 'contract_id': contract.id, 'state': 'confirmed', }) diff --git a/addons/hr_payroll/models/hr_leave.py b/addons/hr_payroll/models/hr_leave.py index aaa49384762..b659386f985 100644 --- a/addons/hr_payroll/models/hr_leave.py +++ b/addons/hr_payroll/models/hr_leave.py @@ -1,8 +1,12 @@ # -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import math from dateutil.relativedelta import relativedelta +from datetime import datetime, date from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError from odoo.addons.resource.models.resource import Intervals +from odoo.addons.resource.models.resource import HOURS_PER_DAY class HrLeaveType(models.Model): _inherit = 'hr.leave.type' @@ -16,23 +20,87 @@ class HrLeave(models.Model): @api.multi def copy_to_benefits(self): for leave in self: + contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close']) + start = max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())) + end = min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())) benefit_type = leave.holiday_status_id.benefit_type_id self.env['hr.benefit'].safe_duplicate_create({ 'name': "%s%s" % (benefit_type.name + ": " if benefit_type else "", leave.employee_id.name), - 'date_start': leave.date_from, - 'date_stop': leave.date_to, + 'date_start': start, + 'date_stop': end, 'benefit_type_id': benefit_type.id, 'employee_id': leave.employee_id.id, - 'leave_id': self.id, + 'leave_id': leave.id, 'state': 'confirmed', + 'contract_id': contract.id, }) + @api.multi + def _create_resource_leave(self): + """ + Add a resource leave in calendars of contracts running at the same period. + This is needed in order to compute the correct number of hours/days of the leave + according to the contract's calender. + """ + super(HrLeave, self)._create_resource_leave() + resource_leave_values = [] + + for leave in self.filtered(lambda l: l.employee_id): + + contract = leave.employee_id._get_contracts(leave.date_from, leave.date_to, states=['open', 'pending', 'close']) + if contract and contract.resource_calendar_id != leave.employee_id.resource_calendar_id: + resource_leave_values += [{ + 'name': leave.name, + 'holiday_id': leave.id, + 'resource_id': leave.employee_id.resource_id.id, + 'benefit_type_id': leave.holiday_status_id.benefit_type_id.id, + 'time_type': leave.holiday_status_id.time_type, + 'date_from': max(leave.date_from, datetime.combine(contract.date_start, datetime.min.time())), + 'date_to': min(leave.date_to, datetime.combine(contract.date_end or date.max, datetime.max.time())), + 'calendar_id': contract.resource_calendar_id.id, + }] + + self.env['resource.calendar.leaves'].create(resource_leave_values) + return True + + @api.constrains('date_from', 'date_to') + def _check_contracts(self): + """ + A leave cannot be set across multiple contracts. + Note: a leave can be across multiple contracts despite this constraint. + It happens if a leave is correctly created (not accross multiple contracts) but + contracts are later modifed/created in the middle of the leave. + """ + for holiday in self: + domain = [ + ('employee_id', '=', holiday.employee_id.id), + ('date_start', '<=', holiday.date_to), + ('state', 'not in', ['draft', 'cancel']), + '|', + ('date_end', '>=', holiday.date_from), + ('date_end', '=', False), + ] + nbr_contracts = self.env['hr.contract'].sudo().search_count(domain) + if nbr_contracts > 1: + raise ValidationError(_('A leave cannot be set across multiple contracts.')) + @api.multi def _cancel_benefit_conflict(self): + """ + Unlink any benefit linked to a leave in self. + Re-create new benefits where the leaves do not cover the full range of the deleted benefits. + Create a leave benefit for each leave in self. + Return True if one or more benefits are unlinked. + e.g.: + |---------------- benefit ----------------| + |------ leave ------| + || + vv + |-benef-|---benefit leave---|----benefit---| + """ benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)]) if benefits: self.copy_to_benefits() - # create new benefits where the leave does not cover the full benefit benefits_intervals = Intervals(intervals=[(b.date_start, b.date_stop, b) for b in benefits]) leave_intervals = Intervals(intervals=[(l.date_from, l.date_to, l) for l in self]) @@ -52,17 +120,18 @@ class HrLeave(models.Model): 'date_start': benefit_start, 'date_stop': benefit_stop, 'benefit_type_id': benefit_type.id, + 'contract_id': benefit.contract_id.id, 'employee_id': employee.id, 'state': 'confirmed', }) benefits.unlink() + return True + return False @api.multi def action_validate(self): super(HrLeave, self).action_validate() - self._cancel_benefit_conflict() - calendar_leaves = self.env['resource.calendar.leaves'].search([('holiday_id', 'in', self.ids)]) - calendar_leaves.write({'benefit_type_id': self.holiday_status_id.benefit_type_id.id}) + self._cancel_benefit_conflict() # delete preexisting conflicting benefits return True @api.multi @@ -71,3 +140,32 @@ class HrLeave(models.Model): benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)]) benefits.write({'display_warning': False, 'active': False}) return True + + def _get_number_of_days(self, date_from, date_to, employee_id): + """ If an employee is currently working full time but requests a leave next month + where he has a new contract working only 3 days/week. This should be taken into + account when computing the number of days for the leave (2 weeks leave = 6 days). + Override this method to get number of days according to the contract's calendar + at the time of the leave. + """ + days = super(HrLeave, self)._get_number_of_days(date_from, date_to, employee_id) + if employee_id: + employee = self.env['hr.employee'].browse(employee_id) + # Use sudo otherwise base users can't compute number of days + contracts = employee.sudo()._get_contracts(date_from, date_to, states=['incoming', 'open', 'pending']) + calendar = contracts[:1].resource_calendar_id if contracts else None # Note: if len(contracts)>1, the leave creation will crash because of unicity constaint + return employee.get_work_days_data(date_from, date_to, calendar=calendar)['days'] + + return days + + @api.multi + @api.depends('number_of_days') + def _compute_number_of_hours_display(self): + """ Override for the same reason as _get_number_of_days()""" + super(HrLeave, self)._compute_number_of_hours_display() + for holiday in self: + if holiday.date_from and holiday.date_to: + contracts = holiday.employee_id.sudo()._get_contracts(holiday.date_from, holiday.date_to, states=['incoming', 'open', 'pending']) + contract_calendar = contracts[:1].resource_calendar_id if contracts else None + calendar = contract_calendar or holiday.employee_id.resource_calendar_id or self.env.user.company_id.resource_calendar_id + holiday.number_of_hours_display = holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY) diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index 110432fcadf..de7ce63761e 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -23,7 +23,7 @@ class HrPayslip(models.Model): 'to the contract chosen. If you let empty the field contract, this field isn\'t ' 'mandatory anymore and thus the rules applied will be all the rules set on the ' 'structure of all contracts of the employee valid for the chosen period') - name = fields.Char(string='Payslip Name', readonly=True, + name = fields.Char(string='Payslip Name', readonly=True, required=True, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}) number = fields.Char(string='Reference', readonly=True, copy=False, states={'draft': [('readonly', False)], 'verify': [('readonly', False)]}) @@ -143,31 +143,27 @@ class HrPayslip(models.Model): number = payslip.number or self.env['ir.sequence'].next_by_code('salary.slip') # delete old payslip lines payslip.line_ids.unlink() - # set the list of contract for which the rules have to be applied - # if we don't give the contract, then the rules to apply should be for all current contracts of the employee - contracts = payslip.contract_id or \ - payslip.employee_id._get_contracts(payslip.date_from, payslip.date_to) - lines = [(0, 0, line) for line in payslip._get_payslip_lines(contracts)] + lines = [(0, 0, line) for line in payslip._get_payslip_lines(payslip.contract_id)] payslip.write({'line_ids': lines, 'number': number, 'state': 'verify', 'compute_date': fields.Date.today()}) return True @api.model - def get_worked_day_lines(self, contracts, date_from, date_to): + def get_worked_day_lines(self, contract, date_from, date_to): """ @param contract: Browse record of contracts @return: returns a list of dict containing the worked days that should be applied for the given contract between date_from and date_to """ res = [] # fill only if the contract as a working schedule linked - for contract in contracts.filtered(lambda contract: contract.resource_calendar_id): - day_from = datetime.combine(fields.Date.from_string(date_from), time.min) - day_to = datetime.combine(fields.Date.from_string(date_to), time.max) + if contract.resource_calendar_id: + day_from = datetime.combine(max(fields.Date.from_string(date_from), contract.date_start), time.min) + day_to = datetime.combine(min(fields.Date.from_string(date_to), contract.date_end or date.max), time.max) calendar = contract.resource_calendar_id benefit_types = self.env['hr.benefit.type'].search([('code', '!=', False)]) for benefit_type in benefit_types: - hours = contract.employee_id.get_benefit_days_data(benefit_type, day_from, day_to)['hours'] + hours = contract.employee_id.get_benefit_days_data(benefit_type, day_from, day_to, calendar=calendar)['hours'] if hours: line = { 'name': benefit_type.name, @@ -265,7 +261,7 @@ class HrPayslip(models.Model): return list(result_dict.values()) - @api.onchange('employee_id', 'date_from', 'date_to') + @api.onchange('employee_id', 'contract_id', 'date_from', 'date_to') def onchange_employee(self): if (not self.employee_id) or (not self.date_from) or (not self.date_to): @@ -274,22 +270,20 @@ class HrPayslip(models.Model): employee = self.employee_id date_from = self.date_from date_to = self.date_to - + self.company_id = employee.company_id - contracts = self.env['hr.contract'] - if not self.env.context.get('contract') or not self.contract_id: + if not self.contract_id: # Add a default contract if not already defined contracts = employee._get_contracts(date_from, date_to) - if not contracts: + + if not contracts or not contracts[0].struct_id: return self.contract_id = contracts[0] + self.struct_id = contracts[0].struct_id - if not self.contract_id.struct_id: - return - self.struct_id = self.contract_id.struct_id #computation of the salary worked days - worked_days_line_ids = self.get_worked_day_lines(contracts, date_from, date_to) + worked_days_line_ids = self.get_worked_day_lines(self.contract_id, date_from, date_to) worked_days_lines = self.worked_days_line_ids.browse([]) for r in worked_days_line_ids: worked_days_lines += worked_days_lines.new(r) @@ -302,13 +296,6 @@ class HrPayslip(models.Model): payslip_name = self.struct_id.payslip_name or _('Salary Slip') self.name = '%s - %s - %s' % (payslip_name, self.employee_id.name, tools.ustr(babel.dates.format_date(date=ttyme, format='MMMM-y', locale=locale))) - @api.onchange('contract_id') - def onchange_contract(self): - if not self.contract_id: - self.struct_id = False - self.with_context(contract=True).onchange_employee() - return - def get_salary_line_total(self, code): self.ensure_one() line = self.line_ids.filtered(lambda line: line.code == code) diff --git a/addons/hr_payroll/models/resource.py b/addons/hr_payroll/models/resource.py index 72593757d06..4763312c4f5 100644 --- a/addons/hr_payroll/models/resource.py +++ b/addons/hr_payroll/models/resource.py @@ -1,8 +1,10 @@ # -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import models, fields, api -from odoo.osv.expression import AND, OR +from odoo.osv.expression import AND from odoo.tools import float_compare +from datetime import datetime + class ResourceCalendar(models.Model): _inherit = 'resource.calendar' @@ -39,6 +41,25 @@ class ResourceCalendar(models.Model): def _onchange_hours_per_day(self): return super(ResourceCalendar, self)._onchange_hours_per_day() + @api.multi + 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 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, + }) + + class ResourceCalendarAttendance(models.Model): _inherit = 'resource.calendar.attendance' diff --git a/addons/hr_payroll/tests/__init__.py b/addons/hr_payroll/tests/__init__.py index 1a7d794de5a..98f0f93e77b 100644 --- a/addons/hr_payroll/tests/__init__.py +++ b/addons/hr_payroll/tests/__init__.py @@ -3,3 +3,5 @@ from . import test_payslip_flow from . import test_benefit +from . import test_multi_contract +from . import test_calendar_sync diff --git a/addons/hr_payroll/tests/common.py b/addons/hr_payroll/tests/common.py index 3848011849c..c64419282e5 100644 --- a/addons/hr_payroll/tests/common.py +++ b/addons/hr_payroll/tests/common.py @@ -47,3 +47,64 @@ class TestPayslipBase(TransactionCase): 'employee_id': self.richard_emp.id, 'struct_id': self.developer_pay_structure.id, }) + + self.benefit_type_leave = self.env['hr.benefit.type'].create({ + 'name': 'Leave', + 'is_leave': True, + 'code': 'LEAVE100' + }) + self.leave_type = self.env['hr.leave.type'].create({ + 'name': 'Legal Leaves', + 'time_type': 'leave', + 'allocation_type': 'no', + 'benefit_type_id': self.benefit_type_leave.id + }) + +class TestPayslipContractBase(TestPayslipBase): + + def setUp(self): + super(TestPayslipContractBase, self).setUp() + self.calendar_richard = self.env['resource.calendar'].create({'name': 'Calendar of Richard'}) + self.calendar_40h = self.env['resource.calendar'].create({'name': 'Default calendar'}) + self.calendar_35h = self.env['resource.calendar'].create({ + 'name': '35h calendar', + 'attendance_ids': [ + (0, 0, {'name': 'Monday Morning', 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Monday Evening', 'dayofweek': '0', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Tuesday Morning', 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Tuesday Evening', 'dayofweek': '1', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Wednesday Morning', 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Wednesday Evening', 'dayofweek': '2', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Thursday Morning', 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Thursday Evening', 'dayofweek': '3', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}), + (0, 0, {'name': 'Friday Morning', 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': 'Friday Evening', 'dayofweek': '4', 'hour_from': 13, 'hour_to': 16, 'day_period': 'afternoon'}) + ] + }) + self.calendar_35h._onchange_hours_per_day() # update hours/day + self.richard_emp.resource_calendar_id = self.calendar_richard + + # This contract ends at the 15th of the month + self.contract_cdd = self.env['hr.contract'].create({ # Fixed term contract + 'date_end': datetime.strptime('2015-11-15', '%Y-%m-%d'), + 'date_start': datetime.strptime('2015-01-01', '%Y-%m-%d'), + 'name': 'First CDD Contract for Richard', + 'resource_calendar_id': self.calendar_40h.id, + 'wage': 5000.0, + 'type_id': self.ref('hr_contract.hr_contract_type_emp'), + 'employee_id': self.richard_emp.id, + 'struct_id': self.developer_pay_structure.id, + 'state': 'close', + }) + + # This contract starts the next day + self.contract_cdi = self.env['hr.contract'].create({ # Fixed term contract + 'date_start': datetime.strptime('2015-11-16', '%Y-%m-%d'), + 'name': 'Contract for Richard', + 'resource_calendar_id': self.calendar_35h.id, + 'wage': 5000.0, + 'type_id': self.ref('hr_contract.hr_contract_type_emp'), + 'employee_id': self.richard_emp.id, + 'struct_id': self.developer_pay_structure.id, + 'state': 'open', + }) diff --git a/addons/hr_payroll/tests/test_benefit.py b/addons/hr_payroll/tests/test_benefit.py index 7e7ea89382c..92a5d1daa6b 100644 --- a/addons/hr_payroll/tests/test_benefit.py +++ b/addons/hr_payroll/tests/test_benefit.py @@ -18,7 +18,7 @@ class TestBenefit(TestPayslipBase): self.start = datetime.strptime('2015-11-01 01:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) self.end = datetime.strptime('2015-11-30 23:59:59', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) self.resource_calendar_id = self.env['resource.calendar'].create({'name': 'Zboub'}) - self.env['hr.contract'].create({ + contract = self.env['hr.contract'].create({ 'date_start': self.start - relativedelta(days=5), 'name': 'dodo', 'resource_calendar_id': self.resource_calendar_id.id, @@ -28,18 +28,7 @@ class TestBenefit(TestPayslipBase): 'state': 'open', }) self.richard_emp.resource_calendar_id = self.resource_calendar_id - - self.benefit_type_leave = self.env['hr.benefit.type'].create({ - 'name': 'Leave', - 'is_leave': True, - 'code': 'LEAVE100' - }) - self.leave_type = self.env['hr.leave.type'].create({ - 'name': 'Paid Time Off', - 'time_type': 'leave', - 'allocation_type': 'no', - 'benefit_type_id': self.benefit_type_leave.id - }) + self.richard_emp.contract_id = contract self.benefit_type = self.env['hr.benefit.type'].create({ 'name': 'Extra attendance', 'is_leave': False, @@ -69,6 +58,7 @@ class TestBenefit(TestPayslipBase): benefit = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, }) @@ -91,6 +81,7 @@ class TestBenefit(TestPayslipBase): benefit = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, }) @@ -114,6 +105,7 @@ class TestBenefit(TestPayslipBase): benefit = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, 'benefit_type_id': self.benefit_type.id, @@ -131,6 +123,7 @@ class TestBenefit(TestPayslipBase): 'name': '1', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, }) @@ -150,6 +143,7 @@ class TestBenefit(TestPayslipBase): 'name': '1', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.benefit_type.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, }) @@ -186,6 +180,7 @@ class TestBenefit(TestPayslipBase): 'name': 'Richard leave from benef', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.benefit_type_leave.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end, }) @@ -201,6 +196,7 @@ class TestBenefit(TestPayslipBase): 'name': '1', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start, 'date_stop': end + relativedelta(hours=5), }) @@ -208,6 +204,7 @@ class TestBenefit(TestPayslipBase): 'name': '2', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': start + relativedelta(hours=3), 'date_stop': end, }) @@ -220,6 +217,7 @@ class TestBenefit(TestPayslipBase): 'name': '1', 'employee_id': self.richard_emp.id, 'benefit_type_id': self.benefit_type_leave.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': self.start, 'date_stop': self.end, }) @@ -238,6 +236,7 @@ class TestBenefit(TestPayslipBase): benef1 = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'date_start': self.start, 'date_stop': self.end, }) @@ -257,10 +256,11 @@ class TestBenefit(TestPayslipBase): benef = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type.id, 'date_start': start, 'date_stop': end, - 'leave_id': leave.id + 'leave_id': leave.id, # benefit conflicts with this leave }) leave.action_approve() @@ -295,6 +295,7 @@ class TestBenefit(TestPayslipBase): benef = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type.id, 'date_start': start, 'date_stop': end, @@ -316,6 +317,7 @@ class TestBenefit(TestPayslipBase): benef = self.env['hr.benefit'].create({ 'name': '1', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type.id, 'date_start': start, 'date_stop': end, @@ -331,6 +333,7 @@ class TestBenefit(TestPayslipBase): leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type_leave.id, 'date_start': start, 'date_stop': end, @@ -346,6 +349,7 @@ class TestBenefit(TestPayslipBase): leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type_leave.id, 'date_start': start, 'date_stop': end, @@ -361,6 +365,7 @@ class TestBenefit(TestPayslipBase): leave_benef = self.env['hr.benefit'].create({ 'name': '1leave', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type_leave.id, 'date_start': start, 'date_stop': end, @@ -384,6 +389,7 @@ class TestBenefit(TestPayslipBase): benef = self.env['hr.benefit'].create({ 'name': 'Extra', 'employee_id': self.richard_emp.id, + 'contract_id': self.richard_emp.contract_id.id, 'benefit_type_id': self.benefit_type.id, 'date_start': start, 'date_stop': end, diff --git a/addons/hr_payroll/tests/test_calendar_sync.py b/addons/hr_payroll/tests/test_calendar_sync.py new file mode 100644 index 00000000000..f108fd90eec --- /dev/null +++ b/addons/hr_payroll/tests/test_calendar_sync.py @@ -0,0 +1,52 @@ + +import os +from datetime import datetime +from odoo.tools import config, test_reports +from odoo.tests.common import tagged +from odoo.addons.hr_payroll.tests.common import TestPayslipContractBase + +class TestPayslipCalendars(TestPayslipContractBase): + + + def test_contract_state_incoming_to_open(self): + # Employee's calendar should change + self.assertEqual(self.richard_emp.resource_calendar_id, self.calendar_richard) + self.contract_cdd.state = 'open' + self.assertEqual(self.richard_emp.resource_calendar_id, self.contract_cdd.resource_calendar_id, "The employee should have the calendar of its contract.") + + def test_contract_transfer_leaves(self): + + def create_calendar_leave(start, end, resource=None): + return self.env['resource.calendar.leaves'].create({ + 'name': 'leave name', + 'date_from': start, + 'date_to': end, + 'resource_id': resource.id if resource else None, + 'calendar_id': self.richard_emp.resource_calendar_id.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'time_type': 'leave', + }) + + start = datetime.strptime('2015-11-17 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-20 18:00:00', '%Y-%m-%d %H:%M:%S') + leave1 = create_calendar_leave(start, end, resource=self.richard_emp.resource_id) + + start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-28 18:00:00', '%Y-%m-%d %H:%M:%S') + leave2 = create_calendar_leave(start, end, resource=self.richard_emp.resource_id) + + # global leave + start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-28 18:00:00', '%Y-%m-%d %H:%M:%S') + leave3 = create_calendar_leave(start, end) + + self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=self.richard_emp.resource_id, from_date=datetime.strptime('2015-11-21', '%Y-%m-%d').date()) + + self.assertEqual(leave1.calendar_id, self.calendar_richard, "It should stay in Richard's calendar") + self.assertEqual(leave3.calendar_id, self.calendar_richard, "Global leave should stay in original calendar") + self.assertEqual(leave2.calendar_id, self.calendar_35h, "It should be transfered to the other calendar") + + # Transfer global leaves + self.calendar_richard.transfer_leaves_to(self.calendar_35h, resources=None, from_date=datetime.strptime('2015-11-21', '%Y-%m-%d').date()) + + self.assertEqual(leave3.calendar_id, self.calendar_35h, "Global leave should be transfered") \ No newline at end of file diff --git a/addons/hr_payroll/tests/test_multi_contract.py b/addons/hr_payroll/tests/test_multi_contract.py new file mode 100644 index 00000000000..75f07871e45 --- /dev/null +++ b/addons/hr_payroll/tests/test_multi_contract.py @@ -0,0 +1,149 @@ + +import os +from datetime import datetime +from odoo.tools import config, test_reports +from odoo.tests.common import tagged +from odoo.exceptions import ValidationError +from odoo.addons.hr_payroll.tests.common import TestPayslipContractBase + +@tagged('payslips_multi_contract') +class TestPayslipMultiContract(TestPayslipContractBase): + + def create_leave(self, start, end): + return self.env['hr.leave'].create({ + 'name': 'Doctor Appointment', + 'employee_id': self.richard_emp.id, + 'holiday_status_id': self.leave_type.id, + 'date_from': start, + 'date_to': end, + }) + + def test_multi_contract(self): + # First contact: 40h, start of the month + payslip = self.env['hr.payslip'].create({ + 'name': 'November 2015', + 'employee_id': self.richard_emp.id, + 'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'), + 'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'), + 'contract_id': self.contract_cdd.id, + }) + payslip.onchange_employee() + self.assertEqual(payslip.worked_days_line_ids.number_of_hours, 80, "It should be 80 hours of work this month for this contract") + self.assertEqual(payslip.worked_days_line_ids.number_of_days, 10, "It should be 10 days of work this month for this contract") + + # Second contract: 35h, end of the month + payslip = self.env['hr.payslip'].create({ + 'name': 'November 2015', + 'employee_id': self.richard_emp.id, + 'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'), + 'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'), + 'contract_id': self.contract_cdi.id, + }) + payslip.onchange_employee() + self.assertEqual(payslip.worked_days_line_ids.number_of_hours, 77, "It should be 77 hours of work this month for this contract") + self.assertEqual(payslip.worked_days_line_ids.number_of_days, 11, "It should be 11 days of work this month for this contract") + + def test_multi_contract_holiday(self): + # Leave during second contract + leave = self.env['resource.calendar.leaves'].create({ + 'name': 'leave name', + 'date_from': datetime.strptime('2015-11-17 07:00:00', '%Y-%m-%d %H:%M:%S'), + 'date_to': datetime.strptime('2015-11-20 18:00:00', '%Y-%m-%d %H:%M:%S'), + 'resource_id': self.richard_emp.resource_id.id, + 'calendar_id': self.calendar_35h.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'time_type': 'leave', + }) + payslip = self.env['hr.payslip'].create({ + 'name': 'November 2015', + 'employee_id': self.richard_emp.id, + 'date_from': datetime.strptime('2015-11-01', '%Y-%m-%d'), + 'date_to': datetime.strptime('2015-11-30', '%Y-%m-%d'), + 'contract_id': self.contract_cdi.id, + }) + payslip.onchange_employee() + work = payslip.worked_days_line_ids.filtered(lambda line: line.code == 'WORK100') + leave = payslip.worked_days_line_ids.filtered(lambda line: line.code == 'LEAVE100') + self.assertEqual(work.number_of_hours, 49, "It should be 49 hours of work this month for this contract") + self.assertEqual(leave.number_of_hours, 28, "It should be 28 hours of leave this month for this contract") + self.assertEqual(work.number_of_days, 7, "It should be 7 days of work this month for this contract") + self.assertEqual(leave.number_of_days, 4, "It should be 4 days of leave this month for this contract") + + def test_move_contract_in_leave(self): + # test move contract dates such that a leave is accross two contracts + start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S') + self.contract_cdi.write({'date_start': datetime.strptime('2015-12-30', '%Y-%m-%d').date()}) + print(self.contract_cdi.date_start, 'test') + # begins during contract, ends after contract + leave = self.create_leave(start, end) + leave.action_approve() + # move contract in the middle of the leave + with self.assertRaises(ValidationError): + self.contract_cdi.date_start = datetime.strptime('2015-11-17', '%Y-%m-%d').date() + + def test_create_contract_in_leave(self): + # test create contract such that a leave is accross two contracts + start = datetime.strptime('2015-11-05 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-12-15 18:00:00', '%Y-%m-%d %H:%M:%S') + self.contract_cdi.date_start = datetime.strptime('2015-12-30', '%Y-%m-%d').date() # remove this contract to be able to create the leave + # begins during contract, ends after contract + leave = self.create_leave(start, end) + leave.action_approve() + # move contract in the middle of the leave + with self.assertRaises(ValidationError): + contract = self.env['hr.contract'].create({ + 'date_start': datetime.strptime('2015-11-30', '%Y-%m-%d').date(), + 'name': 'Contract for Richard', + 'resource_calendar_id': self.calendar_40h.id, + 'wage': 5000.0, + 'type_id': self.ref('hr_contract.hr_contract_type_emp'), + 'employee_id': self.richard_emp.id, + 'struct_id': self.developer_pay_structure.id, + 'state': 'open', + }) + + def test_leave_outside_contract(self): + + # Leave outside contract => should not raise + start = datetime.strptime('2014-10-18 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2014-10-20 09:00:00', '%Y-%m-%d %H:%M:%S') + self.create_leave(start, end) + + # begins before contract, ends during contract => should not raise + start = datetime.strptime('2014-10-25 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-01-15 18:00:00', '%Y-%m-%d %H:%M:%S') + self.create_leave(start, end) + + # begins during contract, ends after contract => should not raise + self.contract_cdi.date_end = datetime.strptime('2015-11-30', '%Y-%m-%d').date() + start = datetime.strptime('2015-11-25 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-12-5 18:00:00', '%Y-%m-%d %H:%M:%S') + self.create_leave(start, end) + + def test_no_leave_overlapping_contracts(self): + + with self.assertRaises(ValidationError): + # Overlap two contracts + start = datetime.strptime('2015-11-12 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-17 18:00:00', '%Y-%m-%d %H:%M:%S') + leave = self.create_leave(start, end) + + # Leave inside fixed term contract => should not raise + start = datetime.strptime('2015-11-04 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-07 09:00:00', '%Y-%m-%d %H:%M:%S') + leave = self.create_leave(start, end) + + # Leave inside contract (no end) => should not raise + start = datetime.strptime('2015-11-18 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-20 09:00:00', '%Y-%m-%d %H:%M:%S') + leave = self.create_leave(start, end) + + def test_leave_request_next_contracts(self): + + start = datetime.strptime('2015-11-23 07:00:00', '%Y-%m-%d %H:%M:%S') + end = datetime.strptime('2015-11-24 18:00:00', '%Y-%m-%d %H:%M:%S') + leave = self.create_leave(start, end) + + leave._compute_number_of_hours_display() + self.assertEqual(leave.number_of_hours_display, 14, "It should count hours according to the future contract.") diff --git a/addons/hr_payroll/views/hr_benefit_views.xml b/addons/hr_payroll/views/hr_benefit_views.xml index f3cb80eae0c..5b05cff1f11 100644 --- a/addons/hr_payroll/views/hr_benefit_views.xml +++ b/addons/hr_payroll/views/hr_benefit_views.xml @@ -82,6 +82,7 @@ + diff --git a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py index 35647d78265..6cbb6c80d78 100644 --- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py +++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py @@ -3,7 +3,7 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError -from datetime import datetime +from datetime import datetime, date class HrPayslipEmployees(models.TransientModel): @@ -43,17 +43,20 @@ class HrPayslipEmployees(models.TransientModel): values = Payslip.default_get(Payslip.fields_get()) values.update({ 'employee_id': employee.id, - 'date_from': payslip_run.date_start, - 'date_to': payslip_run.date_end, 'credit_note': payslip_run.credit_note, 'payslip_run_id': payslip_run.id, + 'date_from': payslip_run.date_start, + 'date_to': payslip_run.date_end, }) - payslip = self.env['hr.payslip'].new(values) - payslip.onchange_employee() - payslip.onchange_contract() - payslip._onchange_struct_id() - values = payslip._convert_to_write(payslip._cache) - payslips += Payslip.create(values) + for contract in employee._get_contracts(payslip_run.date_start, payslip_run.date_end, states=['open', 'pending', 'close']): + values.update({ + 'contract_id': contract.id, + }) + payslip = self.env['hr.payslip'].new(values) + payslip.onchange_employee() + payslip._onchange_struct_id() + values = payslip._convert_to_write(payslip._cache) + payslips += Payslip.create(values) payslips.compute_sheet() payslip_run.state = 'verify' diff --git a/addons/hr_payroll_account/models/hr_payroll_account.py b/addons/hr_payroll_account/models/hr_payroll_account.py index 82117e38cdd..9263b8f69e3 100644 --- a/addons/hr_payroll_account/models/hr_payroll_account.py +++ b/addons/hr_payroll_account/models/hr_payroll_account.py @@ -39,9 +39,9 @@ class HrPayslip(models.Model): vals['journal_id'] = self.env.context.get('journal_id') return super(HrPayslip, self).create(vals) - @api.onchange('contract_id') - def onchange_contract(self): - super(HrPayslip, self).onchange_contract() + @api.onchange('employee_id', 'contract_id', 'date_from', 'date_to') + def onchange_employee(self): + super(HrPayslip, self).onchange_employee() self.journal_id = self.contract_id.journal_id.id or self.default_get(['journal_id'])['journal_id'] @api.multi diff --git a/addons/hr_payroll_account/tests/test_hr_payroll_account.py b/addons/hr_payroll_account/tests/test_hr_payroll_account.py index 5d86e607440..0813ffd635d 100644 --- a/addons/hr_payroll_account/tests/test_hr_payroll_account.py +++ b/addons/hr_payroll_account/tests/test_hr_payroll_account.py @@ -80,7 +80,6 @@ class TestHrPayrollAccount(common.TransactionCase): self.hr_payslip.date_from = time.strftime('%Y-%m-01') self.hr_payslip.date_to = str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10] self.hr_payslip.onchange_employee() - self.hr_payslip.onchange_contract() self.hr_payslip._onchange_struct_id() # I assign the amount to Input data.