From 1dea2b77117be985da10bcbbfa62a9c0c973a9f4 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 14 May 2018 16:08:30 -0700 Subject: [PATCH] Initial commit of `hr_payroll_attendance` and `hr_payroll_attendance_holidays` modules for 11.0. The purpose of this new functionality is to better distinguish between timesheets and attendance, as well as preventing the stock "salaried" time computations from working. --- hr_payroll_attendance/__init__.py | 2 + hr_payroll_attendance/__manifest__.py | 16 ++++ hr_payroll_attendance/hr_contract.py | 7 ++ hr_payroll_attendance/hr_contract_view.xml | 22 +++++ hr_payroll_attendance/hr_payslip.py | 93 +++++++++++++++++++ hr_payroll_attendance_holidays/__init__.py | 1 + .../__manifest__.py | 15 +++ hr_payroll_attendance_holidays/hr_payslip.py | 52 +++++++++++ hr_payroll_holidays/__manifest__.py | 2 +- 9 files changed, 209 insertions(+), 1 deletion(-) create mode 100755 hr_payroll_attendance/__init__.py create mode 100755 hr_payroll_attendance/__manifest__.py create mode 100755 hr_payroll_attendance/hr_contract.py create mode 100755 hr_payroll_attendance/hr_contract_view.xml create mode 100755 hr_payroll_attendance/hr_payslip.py create mode 100755 hr_payroll_attendance_holidays/__init__.py create mode 100755 hr_payroll_attendance_holidays/__manifest__.py create mode 100755 hr_payroll_attendance_holidays/hr_payslip.py diff --git a/hr_payroll_attendance/__init__.py b/hr_payroll_attendance/__init__.py new file mode 100755 index 00000000..f1f0fe2e --- /dev/null +++ b/hr_payroll_attendance/__init__.py @@ -0,0 +1,2 @@ +from . import hr_payslip +from . import hr_contract diff --git a/hr_payroll_attendance/__manifest__.py b/hr_payroll_attendance/__manifest__.py new file mode 100755 index 00000000..4aa42648 --- /dev/null +++ b/hr_payroll_attendance/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Attendance on Payslips', + 'description': 'Get Attendence numbers onto Employee Payslips.', + 'version': '11.0.1.0.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Human Resources', + 'data': [ + 'hr_contract_view.xml', + ], + 'depends': [ + 'hr_payroll', + 'hr_attendance', + ], +} diff --git a/hr_payroll_attendance/hr_contract.py b/hr_payroll_attendance/hr_contract.py new file mode 100755 index 00000000..ebb0381f --- /dev/null +++ b/hr_payroll_attendance/hr_contract.py @@ -0,0 +1,7 @@ +from odoo import models, fields + + +class HrContract(models.Model): + _inherit = 'hr.contract' + + paid_hourly_attendance = fields.Boolean(string="Paid Hourly Attendance", default=False) diff --git a/hr_payroll_attendance/hr_contract_view.xml b/hr_payroll_attendance/hr_contract_view.xml new file mode 100755 index 00000000..5022cc3c --- /dev/null +++ b/hr_payroll_attendance/hr_contract_view.xml @@ -0,0 +1,22 @@ + + + + + hr.contract.form.inherit + hr.contract + 20 + + + + + + + + / pay period + / hour + + + + + + diff --git a/hr_payroll_attendance/hr_payslip.py b/hr_payroll_attendance/hr_payslip.py new file mode 100755 index 00000000..e4561032 --- /dev/null +++ b/hr_payroll_attendance/hr_payslip.py @@ -0,0 +1,93 @@ +from datetime import datetime +from collections import defaultdict +from odoo import api, models +from odoo.exceptions import ValidationError +from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + @api.model + def get_worked_day_lines(self, contracts, date_from, date_to): + def create_empty_worked_lines(employee, contract, date_from, date_to): + attn = { + 'name': 'Attendance', + 'sequence': 10, + 'code': 'ATTN', + 'number_of_days': 0.0, + 'number_of_hours': 0.0, + 'contract_id': contract.id, + } + + valid_attn = [ + ('employee_id', '=', employee.id), + ('check_in', '>=', date_from), + ('check_in', '<=', date_to), + ] + return attn, valid_attn + + work = [] + for contract in contracts.filtered(lambda c: c.paid_hourly_attendance): + worked_attn, valid_attn = create_empty_worked_lines( + contract.employee_id, + contract, + date_from, + date_to + ) + days = set() + for attn in self.env['hr.attendance'].search(valid_attn): + if not attn.check_out: + raise ValidationError('This pay period must not have any open attendances.') + if attn.worked_hours: + # Avoid in/outs + attn_start_time = datetime.strptime(attn.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + attn_iso = attn_start_time.isocalendar() + if not attn_iso in days: + worked_attn['number_of_days'] += 1 + days.add(attn_iso) + worked_attn['number_of_hours'] += attn.worked_hours + worked_attn['number_of_hours'] = round(worked_attn['number_of_hours'], 2) + work.append(worked_attn) + + res = super(HrPayslip, self).get_worked_day_lines(contracts.filtered(lambda c: not c.paid_hourly_attendance), date_from, date_to) + res.extend(work) + return res + + @api.multi + def hour_break_down(self, code): + """ + :param code: what kind of worked days you need aggregated + :return: dict: keys are isocalendar tuples, values are hours. + """ + self.ensure_one() + if code == 'ATTN': + attns = self.env['hr.attendance'].search([ + ('employee_id', '=', self.employee_id.id), + ('check_in', '>=', self.date_from), + ('check_in', '<=', self.date_to), + ]) + day_values = defaultdict(float) + for attn in attns: + if not attn.check_out: + raise ValidationError('This pay period must not have any open attendances.') + if attn.worked_hours: + # Avoid in/outs + attn_start_time = datetime.strptime(attn.check_in, DEFAULT_SERVER_DATETIME_FORMAT) + attn_iso = attn_start_time.isocalendar() + day_values[attn_iso] += attn.worked_hours + return day_values + elif hasattr(super(HrPayslip, self), 'hour_break_down'): + return super(HrPayslip, self).hour_break_down(code) + + @api.multi + def hours_break_down_week(self, code): + """ + :param code: hat kind of worked days you need aggregated + :return: dict: keys are isocalendar weeks, values are hours. + """ + days = self.hour_break_down(code) + weeks = defaultdict(float) + for isoday, hours in days.items(): + weeks[isoday[1]] += hours + return weeks diff --git a/hr_payroll_attendance_holidays/__init__.py b/hr_payroll_attendance_holidays/__init__.py new file mode 100755 index 00000000..5d988fa2 --- /dev/null +++ b/hr_payroll_attendance_holidays/__init__.py @@ -0,0 +1 @@ +from . import hr_payslip diff --git a/hr_payroll_attendance_holidays/__manifest__.py b/hr_payroll_attendance_holidays/__manifest__.py new file mode 100755 index 00000000..b9ca046e --- /dev/null +++ b/hr_payroll_attendance_holidays/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': 'Payroll Attendance Holidays', + 'author': 'Hibou Corp. ', + 'version': '11.0.1.0.0', + 'category': 'Human Resources', + 'sequence': 95, + 'summary': 'Holiday Pay', + 'description': """ +Simplifies getting approved Holiday Leaves onto an employee Payslip. + """, + 'website': 'https://hibou.io/', + 'depends': ['hr_payroll_attendance', 'hr_holidays'], + 'installable': True, + 'application': False, +} diff --git a/hr_payroll_attendance_holidays/hr_payslip.py b/hr_payroll_attendance_holidays/hr_payslip.py new file mode 100755 index 00000000..542e791d --- /dev/null +++ b/hr_payroll_attendance_holidays/hr_payslip.py @@ -0,0 +1,52 @@ +from odoo import models, api +from odoo.addons.hr_holidays.models.hr_holidays import HOURS_PER_DAY + + +class HrPayslip(models.Model): + _inherit = 'hr.payslip' + + @api.model + def get_worked_day_lines(self, contracts, date_from, date_to): + leaves = {} + + for contract in contracts.filtered(lambda c: c.paid_hourly_attendance): + for leave in self._fetch_valid_leaves(contract.employee_id.id, date_from, date_to): + leave_code = self._create_leave_code(leave.holiday_status_id.name) + if leave_code in leaves: + leaves[leave_code]['number_of_days'] += leave.number_of_days_temp + leaves[leave_code]['number_of_hours'] += leave.number_of_days_temp * HOURS_PER_DAY + else: + leaves[leave_code] = { + 'name': leave.holiday_status_id.name, + 'sequence': 15, + 'code': leave_code, + 'number_of_days': leave.number_of_days_temp, + 'number_of_hours': leave.number_of_days_temp * HOURS_PER_DAY, + 'contract_id': contract.id, + } + + res = super(HrPayslip, self).get_worked_day_lines(contracts.filtered(lambda c: not c.paid_hourly_attendance), date_from, date_to) + res.extend(leaves.values()) + return res + + @api.multi + def action_payslip_done(self): + for slip in self.filtered(lambda s: s.contract_id.paid_hourly_attendance): + leaves = self._fetch_valid_leaves(slip.employee_id.id, slip.date_from, slip.date_to) + leaves.write({'payslip_status': True}) + return super(HrPayslip, self).action_payslip_done() + + def _fetch_valid_leaves(self, employee_id, date_from, date_to): + valid_leaves = [ + ('employee_id', '=', employee_id), + ('state', '=', 'validate'), + ('date_from', '>=', date_from), + ('date_from', '<=', date_to), + ('payslip_status', '=', False), + ('type', '=', 'remove'), + ] + + return self.env['hr.holidays'].search(valid_leaves) + + def _create_leave_code(self, name): + return 'L_' + name.replace(' ', '_') diff --git a/hr_payroll_holidays/__manifest__.py b/hr_payroll_holidays/__manifest__.py index 9a4cac2c..dfca2408 100755 --- a/hr_payroll_holidays/__manifest__.py +++ b/hr_payroll_holidays/__manifest__.py @@ -6,7 +6,7 @@ 'version': '11.0.0.0.0', 'category': 'Human Resources', 'sequence': 95, - 'summary': 'Register payments for Payroll Payslips', + 'summary': 'Holiday Pay', 'description': """ Simplifies getting approved Holiday Leaves onto an employee Payslip. """,