diff --git a/addons/hr_payroll/__manifest__.py b/addons/hr_payroll/__manifest__.py index 73bb35e4a66..e26ea532ab9 100644 --- a/addons/hr_payroll/__manifest__.py +++ b/addons/hr_payroll/__manifest__.py @@ -28,6 +28,12 @@ 'views/report_contributionregister_templates.xml', 'views/report_payslip_templates.xml', 'views/report_payslipdetails_templates.xml', + 'views/hr_benefit_views.xml', + 'views/hr_benefit_views.xml', + 'views/hr_leave_views.xml', + 'views/resource_views.xml', + 'views/hr_benefit_template.xml', + 'wizard/hr_benefit_employee_views.xml', ], 'demo': ['data/hr_payroll_demo.xml'], } diff --git a/addons/hr_payroll/data/hr_payroll_data.xml b/addons/hr_payroll/data/hr_payroll_data.xml index 2eede6d92bb..87d00a5c6fc 100644 --- a/addons/hr_payroll/data/hr_payroll_data.xml +++ b/addons/hr_payroll/data/hr_payroll_data.xml @@ -89,6 +89,20 @@ 4 + + + + Attendance + 0 + WORK100 + + + + Generic Leave + LEAVE100 + 3 + True + diff --git a/addons/hr_payroll/data/hr_payroll_demo.xml b/addons/hr_payroll/data/hr_payroll_demo.xml index 21e1b150abe..34a9d091fe6 100644 --- a/addons/hr_payroll/data/hr_payroll_demo.xml +++ b/addons/hr_payroll/data/hr_payroll_demo.xml @@ -146,6 +146,7 @@ Default contract for marketing executives + open @@ -159,4 +160,71 @@ + + Contract For Mitchell Admin + + + + + + This is Mitchell Admin's contract + + open + + + + + Home Working + WORK200 + 2 + + + + Extra hours + WORK300 + 2 + + + + Long Term Leave + LEAVE200 + True + 4 + + + + Unpaid Leave + LEAVE300 + True + 5 + + + + Sick Leave + LEAVE400 + True + 5 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_payroll/models/__init__.py b/addons/hr_payroll/models/__init__.py index 47e89d21c90..9088be882e9 100644 --- a/addons/hr_payroll/models/__init__.py +++ b/addons/hr_payroll/models/__init__.py @@ -7,3 +7,6 @@ from . import hr_employee from . import res_config_settings from . import hr_salary_rule from . import hr_payslip +from . import hr_benefit +from . import hr_leave +from . import resource diff --git a/addons/hr_payroll/models/hr_benefit.py b/addons/hr_payroll/models/hr_benefit.py new file mode 100644 index 00000000000..ec27e044d59 --- /dev/null +++ b/addons/hr_payroll/models/hr_benefit.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import pytz +import itertools +from psycopg2 import IntegrityError +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models, exceptions, _ +from odoo.tools import mute_logger +from odoo.exceptions import UserError, ValidationError +from odoo.addons.resource.models.resource import Intervals + +class HrBenefit(models.Model): + _name = 'hr.benefit' + _description = 'hr.benefit' + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + employee_id = fields.Many2one('hr.employee', required=True, + domain=lambda self: [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)]) + date_start = fields.Datetime(required=True, string='Start') + date_stop = fields.Datetime(string='End') + duration = fields.Float(compute='_compute_duration', inverse='_inverse_duration', store=True, string="Hours") + benefit_type_id = fields.Many2one('hr.benefit.type') + color = fields.Integer(related='benefit_type_id.color', readonly=True) + state = fields.Selection([ + ('draft', 'Draft'), + ('confirmed', 'Confirmed'), + ('validated', 'Validated'), + ('cancelled', 'Cancelled') + ], default='draft') + display_warning = fields.Boolean(string="Error") + leave_id = fields.Many2one('hr.leave', string='Leave') + + _sql_constraints = [ + ('_unique', 'unique (employee_id, date_start, date_stop, benefit_type_id)', "Benefit already exists for this attendence"), + ] + + @api.model + def create(self, vals): + vals['state'] = 'confirmed' + return super(HrBenefit, self).create(vals) + + + @api.constrains('date_stop', 'duration') + def _check_validity_benefit_ends(self): + """ verifies if benefit has an end. """ + for benefit in self: + if not (benefit.date_stop or benefit.duration): + raise exceptions.ValidationError(_('Benefit must end. Please define an end date or a duration.')) + + @api.constrains('date_start', 'date_stop') + def _check_validity_start_before_end(self): + """ verifies if benefit has an end. """ + for benefit in self: + if benefit.date_stop < benefit.date_start: + raise exceptions.ValidationError(_('Starting time should be before end time.')) + + + @api.onchange('duration') + def _onchange_duration(self): + self._inverse_duration() + + @api.depends('date_stop', 'date_start') + def _compute_duration(self): + for benefit in self: + if benefit.date_start and benefit.date_stop: + dt = benefit.date_stop - benefit.date_start + benefit.duration = dt.days * 24 + dt.seconds / 3600 # Number of hours + + def _inverse_duration(self): + for benefit in self: + if benefit.date_start and benefit.duration: + benefit.date_stop = benefit.date_start + relativedelta(hours=benefit.duration) + + @api.multi + def check_if_error(self): + if not self: + return False + undefined_type = self.filtered(lambda b: not b.benefit_type_id) + undefined_type.write({'display_warning': True}) + conflict = self._compute_schedule_conflicts() + conflict_with_leaves = self.compute_conflicts_leaves_to_approve() + return undefined_type or conflict or conflict_with_leaves + + @api.multi + def _compute_schedule_conflicts(self): + conflict = False + date_start_benefits = min(self.mapped('date_start')) + date_stop_benefits = max(self.mapped('date_stop')) + domain = [ + ('date_start', '<', date_stop_benefits), + ('date_stop', '>', date_start_benefits), + ] + + benefs = self.search(domain) + benefits_by_employee = itertools.groupby(benefs, lambda b: b.employee_id) + for employee, benefs in benefits_by_employee: + intervals = Intervals(intervals=((b.date_start, b.date_stop, b) for b in benefs)) + for interval in intervals: + if len(interval[2]) > 1: + interval[2].write({'display_warning': True}) + conflict = True + return conflict + + @api.multi + def compute_conflicts_leaves_to_approve(self): + + if not self: + return False + + query = """ + SELECT + b.id AS benefit_id, + l.id AS leave_id + FROM hr_benefit b + INNER JOIN hr_leave l ON b.employee_id = l.employee_id + WHERE + b.id IN %s AND + l.date_from <= b.date_stop AND + l.date_to >= b.date_start AND + l.state IN ('confirm', 'validate1'); + """ + self.env.cr.execute(query, [tuple(self.ids)]) + conflicts = self.env.cr.dictfetchall() + for res in conflicts: + self.browse(res.get('benefit_id')).write({ + 'display_warning': True, + 'leave_id': res.get('leave_id') + }) + return bool(conflicts) + + def safe_duplicate_create(self, vals): + """ + Create benefit but silently abord if it already exists in the database (according to the _unique constraints). + @return: new record if it didn't exist, empty recordset otherwise. + """ + try: + with mute_logger('odoo.sql_db'), self.env.cr.savepoint(): + # Any database call will be run inside a postgresql savepoint which is + # rolledback if an exception occurs allowing to make subsequent calls in the transaction. + # (Otherwise InternalError is raised by any subsequent database operations) + return self.create(vals) + except IntegrityError as err: + if 'hr_benefit__unique' not in err.pgerror: + raise err + return self.env['hr.benefit'] + + def action_leave(self): + leave = self.leave_id + return { + 'type':'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_id': leave.id, + 'res_model': 'hr.leave', + 'views': [[False, 'form']], + } + + @api.multi + def action_draft(self): + for benefit in self: + if benefit.state not in ['confirmed', 'cancelled']: + raise UserError(_('Benefit state must be "Confirmed" or "Cancelled" in order to be reset to draft.')) + benefit.write({ + 'state': 'draft', + 'active': True, + }) + return True + + @api.multi + def action_cancel(self): + for benefit in self: + if benefit.state not in ['confirmed']: + raise UserError(_('Benefit state must be "Confirmed" in order to be cancelled.')) + benefit.write({ + 'state': 'cancelled', + 'active': False, + }) + return True + + def split_by_day(self): + """ + Split the benefit by days and unlink the original benefit. + @return recordset + """ + def _split_range_by_day(start, end): + days = [] + current_start = start + current_end = start.replace(hour=23, minute=59, second=59) + while current_end < end: + days.append((current_start, current_end)) + current_start = current_end + relativedelta(seconds=1) + current_end = current_end + relativedelta(days=1) + + days.append((current_start, end)) + + # filter to avoid dummy intervals starting and ending at the same time + return [(start, end) for start, end in days if start != end] + + new_benefits = self.env['hr.benefit'] + for benefit in self: + if benefit.date_start.date() == benefit.date_stop.date(): + new_benefits |= benefit + else: + tz = pytz.timezone(benefit.employee_id.tz) + benefit_start, benefit_stop = tz.localize(benefit.date_start), tz.localize(benefit.date_stop) + values = { + 'name': benefit.name, + 'employee_id': benefit.employee_id.id, + 'benefit_type_id': benefit.benefit_type_id.id, + } + benefit_state = benefit.state + benefit.unlink() + for start, stop in _split_range_by_day(benefit_start, benefit_stop): + values['date_start'] = start.astimezone(pytz.utc) + values['date_stop'] = stop.astimezone(pytz.utc) + new_benefit = self.create(values) + # Write the state after the creation due to the ir.rule on benefit state + # LUL TODO: Write a test for this use case + new_benefit.state = benefit_state + new_benefits |= new_benefit + + return new_benefits + + @api.multi + def _duplicate_to_calendar(self): + """ + Duplicate data to keep the complexity in benefit and not mess up payroll, etc. + """ + attendance_benefits = self.filtered(lambda b: + not b.benefit_type_id.is_leave and + # Normal benefit are global to all employees -> avoid duplicating it + not b.benefit_type_id == self.env.ref('hr_payroll.benefit_type_attendance')) + leave_benefits = self.filtered(lambda b: b.benefit_type_id.is_leave) + + for benefit in attendance_benefits: + benefit = benefit.split_by_day() + benefit._duplicate_to_calendar_attendance() + leave_benefits._duplicate_to_calendar_leave() + + @api.multi + def _duplicate_to_calendar_leave(self): + + for benefit in self: + if not benefit.leave_id: + tz = pytz.timezone(benefit.employee_id.tz) + self.env['resource.calendar.leaves'].create({ + 'name': benefit.name, + 'date_from': benefit.date_start, + 'date_to': benefit.date_stop, + 'calendar_id': benefit.employee_id.resource_calendar_id.id, + 'resource_id': benefit.employee_id.resource_id.id, + 'benefit_type_id': benefit.benefit_type_id.id, + }) + + @api.multi + def _duplicate_to_calendar_attendance(self): + mapped_data = { + benefit: [ + pytz.utc.localize(benefit.date_start).astimezone(pytz.timezone(benefit.employee_id.tz)), # Start date + pytz.utc.localize(benefit.date_stop).astimezone(pytz.timezone(benefit.employee_id.tz)) # End date + ] for benefit in self + } + + if any(data[0].date() != data[1].date() for data in mapped_data.values()): + raise ValidationError(_("You can't validate a benefit that covers several days.")) + + for benefit in self: + start, end = mapped_data.get(benefit) + + self.env['resource.calendar.attendance'].create({ + 'name': benefit.name, + 'dayofweek': str(start.weekday()), + 'date_from': start.date(), + '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, + '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, + }) + + @api.model + def action_validate(self, ids): + benefits = self.env['hr.benefit'].search([('id', 'in', ids), ('state', '!=', 'validated')]) + benefits.write({'display_warning': False}) + if not benefits.check_if_error(): + benefits.write({'state': 'validated'}) + benefits._duplicate_to_calendar() + return True + return False + +class HrBenefitType(models.Model): + _name = 'hr.benefit.type' + _description = 'hr.benefit.type' + + name = fields.Char(required=True) + code = fields.Char() + color = fields.Integer(default=1) # Will be used with the new calendar/kanban view + sequence = fields.Integer(default=25) + active = fields.Boolean('Active', default=True, + help="If the active field is set to false, it will allow you to hide the benefit type without removing it.") + is_leave = fields.Boolean(default=False, string="Leave") + diff --git a/addons/hr_payroll/models/hr_employee.py b/addons/hr_payroll/models/hr_employee.py index 0d5a88d2ccd..161fd5414aa 100644 --- a/addons/hr_payroll/models/hr_employee.py +++ b/addons/hr_payroll/models/hr_employee.py @@ -1,7 +1,10 @@ # -*- coding:utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.addons.resource.models.resource import Intervals +from datetime import datetime, timedelta +import pytz class HrEmployee(models.Model): @@ -15,3 +18,46 @@ class HrEmployee(models.Model): def _compute_payslip_count(self): for employee in self: employee.payslip_count = len(employee.slip_ids) + + + @api.multi + def generate_benefit(self, date_start, date_stop): + + date_start = date_start.replace(tzinfo=pytz.utc) + date_stop = date_stop.replace(tzinfo=pytz.utc) + + for employee in self: + + # Approved leaves + emp_leaves = employee.resource_calendar_id.leave_ids.filtered( + lambda r: + r.resource_id == employee.resource_id and + r.date_from.replace(tzinfo=pytz.utc) <= date_stop and + 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() + + new_benefits = self.env['hr.benefit'] + for contract in employee._get_contracts(date_start, date_stop): + + date_start_benefits = max(date_start, datetime.combine(contract.date_start, datetime.min.time()).replace(tzinfo=pytz.utc)) + date_stop_benefits = min(date_stop, datetime.combine(contract.date_end or datetime.max.date(), datetime.max.time()).replace(tzinfo=pytz.utc)) + + calendar = contract.resource_calendar_id + resource = employee.resource_id + attendances = calendar._work_intervals(date_start_benefits, date_stop_benefits, resource=resource) + # Attendances + for interval in attendances: + benefit_type_id = interval[2].mapped('benefit_type_id')[:1] or self.env.ref('hr_payroll.benefit_type_attendance') + new_benefits |= self.env['hr.benefit'].safe_duplicate_create({ + 'name': "%s: %s" % (benefit_type_id.name, employee.name), + 'date_start': interval[0].astimezone(pytz.utc), + 'date_stop': interval[1].astimezone(pytz.utc), + 'benefit_type_id': benefit_type_id.id, + 'employee_id': employee.id, + }) + + new_benefits.compute_conflicts_leaves_to_approve() diff --git a/addons/hr_payroll/models/hr_leave.py b/addons/hr_payroll/models/hr_leave.py new file mode 100644 index 00000000000..4a7c4cdc252 --- /dev/null +++ b/addons/hr_payroll/models/hr_leave.py @@ -0,0 +1,71 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from dateutil.relativedelta import relativedelta +from odoo import api, fields, models, _ +from odoo.addons.resource.models.resource import Intervals + +class HrLeaveType(models.Model): + _inherit = 'hr.leave.type' + + benefit_type_id = fields.Many2one('hr.benefit.type', string='Benefit Type') + + +class HrLeave(models.Model): + _inherit = 'hr.leave' + + @api.multi + def copy_to_benefits(self): + for leave in self: + 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, + 'benefit_type_id': benefit_type.id, + 'employee_id': leave.employee_id.id, + 'leave_id': self.id, + }) + + @api.multi + def _cancel_benefit_conflict(self): + 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]) + remaining_benefits = benefits_intervals - leave_intervals + + for interval in remaining_benefits: + benefit = interval[2] + leave = benefit.leave_id + benefit_type = benefit.benefit_type_id + employee = benefit.employee_id + + benefit_start = interval[0] + relativedelta(seconds=1) if leave.date_to == interval[0] else interval[0] + benefit_stop = interval[1] - relativedelta(seconds=1) if leave.date_from == interval[1] else interval[1] + + self.env['hr.benefit'].safe_duplicate_create({ + 'name': "%s: %s" % (benefit_type.name, employee.name), + 'date_start': benefit_start, + 'date_stop': benefit_stop, + 'benefit_type_id': benefit_type.id, + 'employee_id': employee.id, + }) + benefits.unlink() + + @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}) + return True + + @api.multi + def action_refuse(self): + super(HrLeave, self).action_refuse() + benefits = self.env['hr.benefit'].search([('leave_id', 'in', self.ids)]) + benefits.write({'display_warning': False, 'leave_id': None}) + return True diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py index 609fa316907..51f84c60b86 100644 --- a/addons/hr_payroll/models/hr_payslip.py +++ b/addons/hr_payroll/models/hr_payslip.py @@ -151,45 +151,25 @@ class HrPayslip(models.Model): day_from = datetime.combine(fields.Date.from_string(date_from), time.min) day_to = datetime.combine(fields.Date.from_string(date_to), time.max) - # compute leave days - leaves = {} calendar = contract.resource_calendar_id - tz = timezone(calendar.tz) - day_leave_intervals = contract.employee_id.list_leaves(day_from, day_to, calendar=contract.resource_calendar_id) - for day, hours, leave in day_leave_intervals: - holiday = leave.holiday_id - current_leave_struct = leaves.setdefault(holiday.holiday_status_id, { - 'name': holiday.holiday_status_id.name, - 'sequence': 5, - 'code': holiday.holiday_status_id.code or holiday.holiday_status_id.name, - 'number_of_days': 0.0, - 'number_of_hours': 0.0, - 'contract_id': contract.id, - }) - current_leave_struct['number_of_hours'] += hours - work_hours = calendar.get_work_hours_count( - tz.localize(datetime.combine(day, time.min)), - tz.localize(datetime.combine(day, time.max)), - compute_leaves=False, - ) - if work_hours: - current_leave_struct['number_of_days'] += hours / work_hours - # compute worked days - work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id) - attendances = { - 'name': _("Normal Working Days paid at 100%"), - 'sequence': 1, - 'code': 'WORK100', - 'number_of_days': work_data['days'], - 'number_of_hours': work_data['hours'], - 'contract_id': contract.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'] + if hours: + line = { + 'name': benefit_type.name, + 'sequence': benefit_type.sequence, + 'code': benefit_type.code, + 'number_of_days': hours / calendar.hours_per_day, # n_days returned by benefit_days_data doesn't make sense for extra work + 'number_of_hours': hours, + 'contract_id': contract.id, + } + res.append(line) - res.append(attendances) - res.extend(leaves.values()) return res + @api.model def get_inputs(self, contracts, date_from, date_to): res = [] diff --git a/addons/hr_payroll/models/resource.py b/addons/hr_payroll/models/resource.py new file mode 100644 index 00000000000..440820f2017 --- /dev/null +++ b/addons/hr_payroll/models/resource.py @@ -0,0 +1,62 @@ +# -*- coding:utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from odoo import models, fields +from odoo.osv.expression import AND, OR + + +class ResourceCalendar(models.Model): + _inherit = 'resource.calendar' + + # UI fields + normal_attendance_ids = fields.One2many( + 'resource.calendar.attendance', 'calendar_id', 'Normal working Time', + domain=[('resource_id', '=', False)]) + + extra_attendance_ids = fields.One2many( + 'resource.calendar.attendance', 'calendar_id', 'Employees working Time', + domain=[('resource_id', '!=', False)]) + + +class ResourceCalendarAttendance(models.Model): + _inherit = 'resource.calendar.attendance' + + def _default_benefit_type_id(self): + return self.env.ref('hr_payroll.benefit_type_attendance', raise_if_not_found=False) + + benefit_type_id = fields.Many2one('hr.benefit.type', 'Benefit Type', default=_default_benefit_type_id) + +class ResourceCalendarLeave(models.Model): + _inherit = 'resource.calendar.leaves' + benefit_type_id = fields.Many2one('hr.benefit.type', 'Benefit Type') + + +class ResourceMixin(models.AbstractModel): + _inherit = "resource.mixin" + + def get_benefit_days_data(self, benefit_type, from_datetime, to_datetime, calendar=None): + """ + By default the resource calendar is used, but it can be + changed using the `calendar` argument. + + Returns a dict {'days': n, 'hours': h} containing the number of leaves + expressed as days and as hours. + """ + resource = self.resource_id + calendar = calendar or self.resource_calendar_id + benefit_type_ids = benefit_type.ids + if benefit_type == self.env.ref('hr_payroll.benefit_type_attendance'): # special case for global attendances + benefit_type_ids += [False]# no benefit type = normal/global attendance + domain = [('benefit_type_id', 'in', benefit_type_ids)] + + # naive datetimes are made explicit in UTC + from_datetime = self._timezone_datetime(from_datetime) + to_datetime = self._timezone_datetime(to_datetime) + + day_total = self._get_day_total(from_datetime, to_datetime, calendar, resource) + # actual hours per day + if benefit_type.is_leave: + intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource) & calendar._leave_intervals(from_datetime, to_datetime, resource, domain) # use domain to only retrieve leaves of this type + else: + intervals = calendar._attendance_intervals(from_datetime, to_datetime, resource, domain) - calendar._leave_intervals(from_datetime, to_datetime, resource) + + return self._get_days_data(intervals, day_total) diff --git a/addons/hr_payroll/security/hr_payroll_security.xml b/addons/hr_payroll/security/hr_payroll_security.xml index 93bc89ead02..423cfa4dd13 100644 --- a/addons/hr_payroll/security/hr_payroll_security.xml +++ b/addons/hr_payroll/security/hr_payroll_security.xml @@ -33,5 +33,16 @@ + + hr.benefit: only non validated benefits updated + + [('state', '!=', 'validated')] + + + + + + + diff --git a/addons/hr_payroll/security/ir.model.access.csv b/addons/hr_payroll/security/ir.model.access.csv index 65def2231fa..853f5ef67f5 100644 --- a/addons/hr_payroll/security/ir.model.access.csv +++ b/addons/hr_payroll/security/ir.model.access.csv @@ -12,3 +12,5 @@ access_hr_rule_input_officer,hr.rule.input.office,model_hr_rule_input,hr_payroll access_hr_salary_rule_user,hr.salary.rule.user,model_hr_salary_rule,hr_payroll.group_hr_payroll_user,1,1,1,1 access_hr_contract_advantage_template,hr.contract.advantage.template.user,model_hr_contract_advantage_template,hr_payroll.group_hr_payroll_user,1,1,1,1 access_hr_contract_advantage_template_hr_user,hr.contract.advantage.template.hr.user,model_hr_contract_advantage_template,hr.group_hr_user,1,0,0,0 +access_hr_benefit,access_hr_benefit,model_hr_benefit,group_hr_payroll_user,1,1,1,1 +access_hr_benefit_type,access_hr_benefit_type,model_hr_benefit_type,group_hr_payroll_user,1,1,1,1 \ No newline at end of file diff --git a/addons/hr_payroll/static/src/js/benefit.js b/addons/hr_payroll/static/src/js/benefit.js new file mode 100644 index 00000000000..63f4799ddd7 --- /dev/null +++ b/addons/hr_payroll/static/src/js/benefit.js @@ -0,0 +1,128 @@ +odoo.define('hr_payroll.benefit.view_custo', function(require) { + 'use strict'; + + var core = require('web.core'); + var CalendarController = require("web.CalendarController"); + + var _t = core._t; + CalendarController.include({ + + update: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self._renderBenefitButtons(); + }); + }, + + _renderGenerateButton: function(date_from, date_to, employee_ids, secondary) { + var self = this; + var primary = !secondary ? 'btn-primary' : 'btn-secondary'; + var txt = _t("Generate Benefits"); + this.$buttons.find('.o_calendar_button_month').after( + $('') + .off('click') + .on('click', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + var date_fmt = 'YYYY-MM-DD HH:mm:ss'; + var options = { + on_close: function () { + self.reload(); + }, + }; + self.do_action({ + type: 'ir.actions.act_window', + name: txt, + res_model: 'hr.benefit.employees', + view_type: 'form', + views: [[false,'form']], + target: 'new', + context: { + 'start_benefits': date_from.format(date_fmt), + 'stop_benefits':date_to.format(date_fmt), + }, + }, options); + }) + ); + }, + + _renderBenefitButtons: function () { + if (this.modelName !== "hr.benefit") { + return; + } + + var firstDay = this.model.data.target_date.clone().startOf('month'); + var lastDay = this.model.data.target_date.clone().endOf('month'); + var events = this._checkDataInRange(firstDay, lastDay, this.model.data.data); + var is_validated = this._checkValidation(events); + this.$buttons.find('.btn-benefit').remove(); + var employee_ids = _.map(events, function (event) { return event.record.employee_id[0]; }); + employee_ids = _.uniq(employee_ids); + var self = this; + if (this.model.data.domain.length !== 0) { // select by default the employee in the domain + var employee_search_id = (this.model.data.domain[0][0] === 'employee_id' && this.model.data.domain[0][1] === '=')? [this.model.data.domain[0][2]]: null; + } + if (events.length === 0) { // Generate button + this._renderGenerateButton(firstDay, lastDay, employee_search_id); + } else { + this._renderGenerateButton(firstDay, lastDay, employee_ids, true); + } + if (is_validated && events.length !== 0) { // Generate Payslip button + this.$buttons.find('.o_calendar_button_month').after( + $('') + .off('click') + // action_hr_payslip_by_employees + .on('click', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + var date_fmt = 'YYYY-MM-DD'; + self.do_action('hr_payroll.action_hr_payslip_by_employees', { + additional_context: { + default_employee_ids: employee_ids || [], + default_date_start: firstDay.format(date_fmt), + default_date_end: lastDay.format(date_fmt), + }, + }); + }) + ); + } + else if (!is_validated) { // Validate button + this.$buttons.find('.o_calendar_button_month').after( + $('') + .off('click') + .on('click', function (e) { + e.preventDefault(); + e.stopImmediatePropagation(); + self._rpc({ + model: 'hr.benefit', + method: 'action_validate', + args: [_.map(events, function (event) { return event.record.id; })], + }).then(function () { + return self.reload(); + }); + }) + ); + } + }, + + _checkDataInRange: function (firstDay, lastDay, events) { + var res = _.filter(events, function (event) { + // Filter records that are not inside the current month + // (because in calendar view some days of prev. and next month are visible) + return event.record.date_start.isBefore(lastDay) && event.record.date_stop.isAfter(firstDay); + }); + return res; + }, + + _checkValidation: function (records) { + return _.every(records, function (record) { + return record.record.state === 'validated'; + }); + }, + + renderButtons: function () { + this._super.apply(this, arguments); + this._renderBenefitButtons(); + }, + }); +}); \ No newline at end of file diff --git a/addons/hr_payroll/tests/__init__.py b/addons/hr_payroll/tests/__init__.py index 530540f8158..1a7d794de5a 100644 --- a/addons/hr_payroll/tests/__init__.py +++ b/addons/hr_payroll/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_payslip_flow +from . import test_benefit diff --git a/addons/hr_payroll/tests/test_benefit.py b/addons/hr_payroll/tests/test_benefit.py new file mode 100644 index 00000000000..1624a5b32d1 --- /dev/null +++ b/addons/hr_payroll/tests/test_benefit.py @@ -0,0 +1,337 @@ +# # -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime +import pytz +from dateutil.relativedelta import relativedelta +from odoo import exceptions +from odoo.tests.common import tagged +from odoo.addons.hr_payroll.tests.common import TestPayslipBase + + +@tagged('benefit') +class TestBenefit(TestPayslipBase): + + def setUp(self): + super(TestBenefit, self).setUp() + self.tz = pytz.timezone(self.richard_emp.tz) + 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({ + 'date_start': self.start - relativedelta(days=5), + 'name': 'dodo', + 'resource_calendar_id': self.resource_calendar_id.id, + 'wage': 1000, + 'employee_id': self.richard_emp.id, + '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': 'Legal Leaves', + 'time_type': 'leave', + 'allocation_type': 'no', + 'benefit_type_id': self.benefit_type_leave.id + }) + self.benefit_type = self.env['hr.benefit.type'].create({ + 'name': 'attendance', + 'is_leave': False, + 'code': 'LEAVE100' + }) + + def test_no_duplicate(self): + self.richard_emp.generate_benefit(self.start, self.end) + pou1 = self.env['hr.benefit'].search_count([]) + self.richard_emp.generate_benefit(self.start, self.end) + pou2 = self.env['hr.benefit'].search_count([]) + self.assertEqual(pou1, pou2, "Benefits should not be duplicated") + + def test_benefit(self): + + self.richard_emp.generate_benefit(self.start, self.end) + + attendance_nb = len(self.resource_calendar_id._attendance_intervals(self.start, self.end)) + benefit_nb = self.env['hr.benefit'].search_count([('employee_id', '=', self.richard_emp.id)]) + self.assertEqual(attendance_nb, benefit_nb, "One benefit should be generated for each calendar attendance") + + def test_split_benefit_by_day(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + + # Benefit of type attendance should be split in three + benefit = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'date_start': start, + 'date_stop': end, + }) + + benefits = benefit.split_by_day() + self.assertEqual(len(benefits), 3, "Benefit should be split in three") + self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2015-11-01 23:59:59', '%Y-%m-%d %H:%M:%S'))) + + self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2015-11-02 00:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2015-11-02 23:59:59', '%Y-%m-%d %H:%M:%S'))) + + self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2015-11-03 00:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S'))) + + # Test with end at mid-night -> should not create benefit starting and ending at the same time (at 00:00) + start = datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2013-11-04 00:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + + benefit = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'date_start': start, + 'date_stop': end, + }) + benefits = benefit.split_by_day() + self.assertEqual(len(benefits), 3, "Benefit should be split in three") + self.assertEqual(pytz.utc.localize(benefits[0].date_start), self.tz.localize(datetime.strptime('2013-11-01 00:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[0].date_stop), self.tz.localize(datetime.strptime('2013-11-01 23:59:59', '%Y-%m-%d %H:%M:%S'))) + + self.assertEqual(pytz.utc.localize(benefits[1].date_start), self.tz.localize(datetime.strptime('2013-11-02 00:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[1].date_stop), self.tz.localize(datetime.strptime('2013-11-02 23:59:59', '%Y-%m-%d %H:%M:%S'))) + + self.assertEqual(pytz.utc.localize(benefits[2].date_start), self.tz.localize(datetime.strptime('2013-11-03 00:00:00', '%Y-%m-%d %H:%M:%S'))) + self.assertEqual(pytz.utc.localize(benefits[2].date_stop), self.tz.localize(datetime.strptime('2013-11-03 23:59:59', '%Y-%m-%d %H:%M:%S'))) + + + def test_duplicate_global_benefit_to_attendance(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + + benef = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'date_start': start, + 'date_stop': end, + }) + benef._duplicate_to_calendar() + attendance_nb = self.env['resource.calendar.attendance'].search_count([ + ('date_from', '>=', start.date()), + ('date_to', '<=', end.date()) + ]) + self.assertEqual(attendance_nb, 0, "It should not duplicate the 'normal/global' benefit type") + + def test_duplicate_benefit_to_attendance(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + + # Benefit (not leave) should be split in three attendance + benef = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type.id, + 'date_start': start, + 'date_stop': end, + }) + benef._duplicate_to_calendar() + attendance_nb = self.env['resource.calendar.attendance'].search_count([ + ('date_from', '>=', start.date()), + ('date_to', '<=', end.date()) + ]) + self.assertEqual(attendance_nb, 3, "It should create one calendar attendance per day") + self.assertTrue(self.env['resource.calendar.attendance'].search([ + ('date_from', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()), + ('date_to', '=', datetime.strptime('2015-11-01', '%Y-%m-%d').date()), + ('hour_from', '=', 9.0), + ('hour_to', '>=', 23.9) + ])) + self.assertTrue(self.env['resource.calendar.attendance'].search([ + ('date_from', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()), + ('date_to', '=', datetime.strptime('2015-11-02', '%Y-%m-%d').date()), + ('hour_from', '=', 0.0), + ('hour_to', '>=', 23.9) + ])) + self.assertTrue(self.env['resource.calendar.attendance'].search([ + ('date_from', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()), + ('date_to', '=', datetime.strptime('2015-11-03', '%Y-%m-%d').date()), + ('hour_from', '=', 0.0), + ('hour_to', '=', 18.0) + ])) + + def test_create_benefit_leave(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 18:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + + benef = self.env['hr.benefit'].create({ + 'name': 'Richard leave from benef', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'date_start': start, + 'date_stop': end, + }) + benef.action_validate(benef.ids) + calendar_leave = self.env['resource.calendar.leaves'].search([('name', '=', 'Richard leave from benef')]) + self.assertTrue(calendar_leave, "It should have created a leave in the calendar") + self.assertEqual(calendar_leave.benefit_type_id, benef.benefit_type_id, "It should have the same benefit type") + + def test_validate_conflict_benefit(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-01 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + benef1 = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'date_start': start, + 'date_stop': end + relativedelta(hours=5), + }) + benef2 = self.env['hr.benefit'].create({ + 'name': '2', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.env.ref('hr_payroll.benefit_type_attendance').id, + 'date_start': start + relativedelta(hours=3), + 'date_stop': end, + }) + self.assertFalse(benef1.action_validate(benef1.ids), "It should not validate benefits conflicting with others") + self.assertTrue(benef1.display_warning) + self.assertNotEqual(benef1.state, 'validated') + + def test_validate_non_approved_leave_benefit(self): + benef1 = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'date_start': self.start, + 'date_stop': self.end, + }) + leave_1 = self.env['hr.leave'].create({ + 'name': 'Doctor Appointment', + 'employee_id': self.richard_emp.id, + 'holiday_status_id': self.leave_type.id, + 'date_from': self.start - relativedelta(days=1), + 'date_to': self.start + relativedelta(days=1), + 'number_of_days': 2, + }) + self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits conflicting with non approved leaves") + self.assertTrue(benef1.display_warning) + + def test_validate_undefined_benefit(self): + benef1 = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'date_start': self.start, + 'date_stop': self.end, + }) + self.assertFalse(benef1.action_validate(benef1.ids),"It should not validate benefits without a type") + + def test_approve_leave_benefit(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + leave = 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': start + relativedelta(days=1), + 'number_of_days': 2, + }) + benef = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type.id, + 'date_start': start, + 'date_stop': end, + 'leave_id': leave.id + }) + leave.action_approve() + + new_leave_benef = self.env['hr.benefit'].search([ + ('date_start', '=', datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S')), + ('date_stop', '=', datetime.strptime('2015-11-02 09:00:00', '%Y-%m-%d %H:%M:%S')), + ('benefit_type_id.is_leave', '=', True) + ]) + + new_benef = self.env['hr.benefit'].search([ + ('date_start', '=', datetime.strptime('2015-11-02 09:00:01', '%Y-%m-%d %H:%M:%S')), + ('date_stop', '=', end), + ('benefit_type_id.is_leave', '=', False) + ]) + + self.assertTrue(new_benef, "It should have created a benefit for the last two days") + self.assertTrue(new_leave_benef, "It should have created a leave benefit for the first day") + + self.assertTrue(benef.action_validate((new_benef|new_leave_benef).ids), "It should be able to validate the benefits") + + def test_refuse_leave_benefit(self): + start = datetime.strptime('2015-11-01 09:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-03 13:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + leave = 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': start + relativedelta(days=1), + 'number_of_days': 2, + }) + benef = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type.id, + 'date_start': start, + 'date_stop': end, + 'leave_id': leave.id + }) + benef.action_validate(benef.ids) + self.assertTrue(benef.display_warning, "It should have an error (conflicting leave to approve") + leave.action_refuse() + self.assertFalse(benef.display_warning, "It should not have an error") + + def test_time_normal_benefit(self): + # Normal attendances (global to all employees) + data = self.richard_emp.get_benefit_days_data(self.env.ref('hr_payroll.benefit_type_attendance'), self.start, self.end) + self.assertEqual(data['hours'], 168.0) + + def test_time_extra_benefit(self): + start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=self.tz) + benef = self.env['hr.benefit'].create({ + 'name': '1', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type.id, + 'date_start': start, + 'date_stop': end, + }) + benef.action_validate(benef.ids) + data = self.richard_emp.get_benefit_days_data(self.benefit_type, self.start, self.end) + self.assertEqual(data['hours'], 7.0) + + def test_time_week_leave_benefit(self): + # /!\ this is a week day => it exists an calendar attendance at this time + start = datetime.strptime('2015-11-02 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC + end = datetime.strptime('2015-11-02 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + leave_benef = self.env['hr.benefit'].create({ + 'name': '1leave', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'date_start': start, + 'date_stop': end, + }) + leave_benef.action_validate(leave_benef.ids) + data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end) + self.assertEqual(data['hours'], 5.0, "It should equal the number of hours richard should have worked") + + def test_time_weekend_leave_benefit(self): + # /!\ this is in the weekend => no calendar attendance at this time + start = datetime.strptime('2015-11-01 10:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) # UTC + end = datetime.strptime('2015-11-01 17:00:00', '%Y-%m-%d %H:%M:%S').replace(tzinfo=pytz.utc) + leave_benef = self.env['hr.benefit'].create({ + 'name': '1leave', + 'employee_id': self.richard_emp.id, + 'benefit_type_id': self.benefit_type_leave.id, + 'date_start': start, + 'date_stop': end, + }) + leave_benef.action_validate(leave_benef.ids) + data = self.richard_emp.get_benefit_days_data(self.benefit_type_leave, self.start, self.end) + self.assertEqual(data['hours'], 0.0, "It should equal the number of hours richard should have worked") diff --git a/addons/hr_payroll/views/hr_benefit_template.xml b/addons/hr_payroll/views/hr_benefit_template.xml new file mode 100644 index 00000000000..1db60d24ac0 --- /dev/null +++ b/addons/hr_payroll/views/hr_benefit_template.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/addons/hr_payroll/views/hr_benefit_views.xml b/addons/hr_payroll/views/hr_benefit_views.xml new file mode 100644 index 00000000000..d187ebded06 --- /dev/null +++ b/addons/hr_payroll/views/hr_benefit_views.xml @@ -0,0 +1,165 @@ + + + + + Generate Benefit + + + code + +if records: + records.generate_benefit() + + + + + Benefit + hr.benefit + form + calendar,tree,form + + + + hr.benefit.calendar + hr.benefit + + + + + + + + + +
+ +
+
+ + + + + + + + + + +
+
+ + + +
+ + + + hr.benefit.form + hr.benefit + +
+
+ + +
+ +
+ +
+ + +
+
+
+

+ +

+
+ + + + + + + + + + + + +
+
+
+
+ + + hr.benefit.filter + hr.benefit + + + + + + + + + + + + hr.benefit.type.form + hr.benefit.type + +
+ +
+

+ +

+
+ + + + + + + + + +
+
+
+
+ + + + + Benefit Types + hr.benefit.type + tree,kanban,form + + + + \ No newline at end of file diff --git a/addons/hr_payroll/views/hr_leave_views.xml b/addons/hr_payroll/views/hr_leave_views.xml new file mode 100644 index 00000000000..ecdd983bc3f --- /dev/null +++ b/addons/hr_payroll/views/hr_leave_views.xml @@ -0,0 +1,16 @@ + + + + + benefit.type.leave.form.inherit + hr.leave.type + + + + + + + + + + diff --git a/addons/hr_payroll/views/hr_payslip_views.xml b/addons/hr_payroll/views/hr_payslip_views.xml index 4970b637716..8dbc78f0274 100644 --- a/addons/hr_payroll/views/hr_payslip_views.xml +++ b/addons/hr_payroll/views/hr_payslip_views.xml @@ -385,7 +385,14 @@ - + + + + + + + + diff --git a/addons/hr_payroll/views/resource_views.xml b/addons/hr_payroll/views/resource_views.xml new file mode 100644 index 00000000000..5fe67b67128 --- /dev/null +++ b/addons/hr_payroll/views/resource_views.xml @@ -0,0 +1,31 @@ + + + + + + payroll.resource.calendar.view.form + resource.calendar + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/hr_payroll/wizard/__init__.py b/addons/hr_payroll/wizard/__init__.py index f433d2bbf05..86fd3512f4d 100644 --- a/addons/hr_payroll/wizard/__init__.py +++ b/addons/hr_payroll/wizard/__init__.py @@ -3,3 +3,4 @@ from . import hr_payroll_payslips_by_employees from . import hr_payroll_contribution_register_report +from . import hr_benefit_employee diff --git a/addons/hr_payroll/wizard/hr_benefit_employee.py b/addons/hr_payroll/wizard/hr_benefit_employee.py new file mode 100644 index 00000000000..cdbf8b6345a --- /dev/null +++ b/addons/hr_payroll/wizard/hr_benefit_employee.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class HrPayslipEmployees(models.TransientModel): + _name = 'hr.benefit.employees' + _description = 'Generate benefits for all selected employees' + + def _get_domain(self): + return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)] + + employee_ids = fields.Many2many('hr.employee', string='Employees', domain=lambda self: self._get_domain()) + + + def generate_benefit(self): + if not self.employee_ids: + raise UserError(_("You must select employee(s) to generate benefits.")) + date_start = self.env.context.get('start_benefits') + date_stop = self.env.context.get('stop_benefits') + + date_start = fields.Datetime.from_string(date_start) + date_stop = fields.Datetime.from_string(date_stop) + self.employee_ids.generate_benefit(date_start, date_stop) diff --git a/addons/hr_payroll/wizard/hr_benefit_employee_views.xml b/addons/hr_payroll/wizard/hr_benefit_employee_views.xml new file mode 100644 index 00000000000..7db54da6264 --- /dev/null +++ b/addons/hr_payroll/wizard/hr_benefit_employee_views.xml @@ -0,0 +1,23 @@ + + + + hr_benefit_employees + hr.benefit.employees + +
+
+
+ + This wizard will generate benefits for all selected employee(s) based on attendances. + + + + + + + +
+
+
+
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 d2097da5306..8a986015c8b 100644 --- a/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py +++ b/addons/hr_payroll/wizard/hr_payroll_payslips_by_employees.py @@ -3,17 +3,22 @@ from odoo import api, fields, models, _ from odoo.exceptions import UserError +from datetime import datetime class HrPayslipEmployees(models.TransientModel): _name = 'hr.payslip.employees' _description = 'Generate payslips for all selected employees' - employee_ids = fields.Many2many('hr.employee', 'hr_employee_group_rel', 'payslip_id', 'employee_id', 'Employees', - domain=lambda self: self._get_available_contracts_domain()) - def _get_available_contracts_domain(self): - return [('contract_ids.state', 'in', ('open', 'pending'))] + return [('contract_ids.state', 'in', ('open', 'pending')), ('company_id', '=', self.env.user.company_id.id)] + + def _get_employees(self): + return self.env['hr.employee'].search(self._get_available_contracts_domain()) + + employee_ids = fields.Many2many('hr.employee', 'hr_employee_group_rel', 'payslip_id', 'employee_id', 'Employees', + domain=lambda self: self._get_available_contracts_domain(), + default=lambda self: self._get_employees()) @api.multi @@ -23,8 +28,20 @@ class HrPayslipEmployees(models.TransientModel): active_id = self.env.context.get('active_id') if active_id: [run_data] = self.env['hr.payslip.run'].browse(active_id).read(['date_start', 'date_end', 'credit_note']) - from_date = run_data.get('date_start') - to_date = run_data.get('date_end') + from_date = run_data.get('date_start') + to_date = run_data.get('date_end') + else: + from_date = datetime.strptime(self.env.context.get('default_date_start'), '%Y-%m-%d') + to_date = datetime.strptime(self.env.context.get('default_date_end'), '%Y-%m-%d') + payslip_run = self.env['hr.payslip.run'].create({ + 'name': from_date.strftime('%B %Y'), + 'date_start': from_date, + 'date_end': to_date, + }) + + active_id = payslip_run.id + [run_data] = payslip_run.read(['date_start', 'date_end', 'credit_note']) + if not data['employee_ids']: raise UserError(_("You must select employee(s) to generate payslip(s).")) for employee in self.env['hr.employee'].browse(data['employee_ids']): @@ -44,4 +61,10 @@ class HrPayslipEmployees(models.TransientModel): } payslips += self.env['hr.payslip'].create(res) payslips.compute_sheet() - return {'type': 'ir.actions.act_window_close'} + + return {'type': 'ir.actions.act_window', + 'res_model': 'hr.payslip.run', + 'view_type': 'form', + 'views': [[False, 'form']], + 'res_id': active_id, + }