From 2be103239f68d19a539848bb73ed0ab6ee195caa Mon Sep 17 00:00:00 2001 From: fw-bot Date: Thu, 15 Oct 2020 12:09:09 +0000 Subject: [PATCH] [MERGE] l10n_be_hr_payroll: Validate the CP200 different payslips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Purpose ======= In Belgium, the job market is divided into "sectors": chemicals, metals, banks, department stores, pharmacies ... This breakdown is made on the basis of the activities of the company. In order to be able to ensure the functioning of the sectors, each sector has its own consultation body, called the "joint commission". A joint committee is a consultation body in which representatives of employers and unions set, after negotiation, the minimum wage conditions as well as the working conditions in legal texts, called “collective labor agreements”. These are applicable to all employees in a specific sector. These are agreements that employers and workers in a given sector must make and respect. These agreements may be different from one sector to another. Each sector has its label and number. The CP 200 is the auxiliary joint committee of employees (CPAE). About a quarter of private sector companies and employees in Belgium are found in CP 200. This makes it the largest and most diverse CP in the country! TaskID: 2352568 Specification ============= Most of the different use cases that we can face in belgian payroll for CP200 have been tested and validated by a fiscal expert. Briefly, it covers the following use cases: - Unpaid time off - Legal time off - Credit time - Classical - With paid time off - With unpaid time off - With sick time off - Starting in the middle of a month - Full time (O hours / week) - Half-days in working hours - Classical - With full-day time off - With half-day time off - Low salaries - Back-to-work - Sick time off - Relapse - Guaranteed salary - Long sick off (+1 month) - Student contracts - Posted Employess - Without ONSS - Wihtout withholding taxes - Public Holiday - Classical (during the working hours) - Compensation (during the week end) - After the end of the contract - Note: Variable compensation is not handled yet, but it's planned - Brief Holiday (In french: "Petit Chômage") - 1 day - 1 week - Note: Variable compensation is not handled yet, but it's planned - 1 day for unpredictable reason - End of contract in the middle of the month - Half-days time off - New design of the payslip to improve readability The detailed list of the fixes/improvements is the following: - 1/ hr_work_entry_contract: Define 1 leave work entry per presence Purpose: Some sick time off have to be defined into different work entry types, according to the type of sick leaves. Example: after 30 calendar days of sick leaves, the employee is not paid by the same organism and therefore, the time off should be reported into different work entry types. Specification: Instead of creating a big work entry per time off, create as many work entries as there should have been presence work entries. - 2/ hr_work_entries: Prevent users deleting work entries Prefer to archive them instead. - 3/ hr_work_entry: Display work entry type on calendar views - 4/ web: Add styling classes for report Purpose: Those classes are useful for the payslip printed pdf reports, but some of them are useless for any kind of report, and thus the classes have been defined in the web addons. Task-2341886 - 5/ resources: Take company attendances/global leaves on creation Purpose: When creating a new calendar (38 hours/week) on a Belgian company, the new calendar is prefilled on a 40 hours/week basis, even if the company calendar is correctly defined. Specification: Copy the global time off + the attendances on the company calendar on company modification - 6/ hr_holidays: Allow to refuse overlapping time offs Purpose: It's normally not possible to create 2 time off that overlaps on the same period. But, if for a given reason it happens on a production database, it should be possible to refuse those time off. Otherwise, the user is stuck with this wrong configuration. - 7/ hr_holidays: Prevent creating time off of 0 hours Purpose: It makes no sense + it could cause conflicts when generating the work entries - 8/ resource: Add method to retrieve the longest work day Purpose: Add a method on the calendar to retrieve the maximum number of hours an employee is supposed to work. Example: Monday: 8-12, 13-17 Tuesday: 8-12, 13-17 Wednesday: 8-12 Thurday: Off Friday: Off The method will return 8 - 9/ hr_holidays: Adapt query counter due to changes on hr_payroll See: https://github.com/odoo/enterprise/pull/13848/ - 10/ hr_payroll: Update translations Lots of new terms have been added on the printed reports, etc... - 11/ l10n_be_hr_payroll: Consider out of contract public holiday Purpose: If there is a public holiday less than 30 days after the end of the contract, the employee should be paid for that day too - 12/ l10n_be_hr_payroll_posted_employee: Add an easy way to create a public time off Purpose: Public time off should be created via the calendar (at least for belgian companies using payroll). The reason behind this is that there should be a resource.calendar.leaves available even if the employee ends its contract. If it's created using a hr.leave, there won't be any resource.calendar.leave as the employee is not supposed to work anymore at that time. When an employee leaves the company, it's possible that he's paid for public time off if it occurs less than 30 days after the end of his contract. - 13/ l10n_be_hr_payroll: Rename bank holiday into Public holiday Purpose: A bank holiday is a specific holiday name, not the generic one. - 14/ hr_work_entry_contract: Add menuitem to access calendars It shouldn't be required to enter debug mode to access this information. - 15/ l10n_be_hr_payroll_posted_employee: Add support for posted employees Purpose: https://www.socialsecurity.be/citizen/fr/travailleur-salarie-occupe-en-belgique-detache-a-l-etranger - 16/ l10n_be_hr_payroll: Implement guaranteed wage/relapse for sick leaves Purpose: Guaranteed salary for employees: The employee who is incapacitated for work as a result of a work accident, an accident on the way to work or an occupational disease retains, at the employer's expense, the right to his remuneration for the first 30 days of incapacity for work. The daily allowances relating to this period are paid to the employer by the body responsible for industrial accidents or by the Fund for Occupational Diseases. Successive disabilities - relapse: It is possible that a worker will be unable to work during different successive periods. When two incapacities follow one another without interruption (for example, accident followed by illness; successive illnesses of a different nature), there is only one period of incapacity and the guaranteed remuneration is not due again. time. On the other hand, we talk about relapse when a new disability occurs after the worker has returned to work normally. In this case, the guaranteed remuneration is not due again when the new incapacity for work occurs within the first 14 days following the end of a period of incapacity for work giving rise to the payment of the guaranteed remuneration. However, there is still a right to the guaranteed salary for the part remaining to run, if the first period of incapacity has not exhausted the entire right to the guaranteed salary during the first period of incapacity. A new guaranteed wage is also due again when the worker establishes by a medical certificate that this new incapacity for work is due to another illness or to another accident. - 17/ hr_work_entry_contract: Define 1 leave work entry per presence Purpose: Some sick time off have to be defined into different work entry types, according to the type of sick leaves. Example: after 30 calendar days of sick leaves, the employee is not paid by the same organism and therefore, the time off should be reported into different work entry types. Specification: Instead of creating a big work entry per time off, create as many work entries as there should have been presence work entries. - 18/ test_l10n_be_hr_payroll: Add test for full time credit time - 19/ test_l10n_be_hr_payroll: Add test for out of contract credit time - 20/ test_l10n_be_hr_payroll: Avoid undeterministic bug on generated tests - 21/ hr_payroll: Don't display out of contract line on pdf - 22/ test_l10n_be_hr_payroll: Fix undeterministic test crashes - 23/ payroll : Improve the printed payslip report Improves the design of the payslip report Generates the report in the employee's language (add a message if the lang is not official) Adds some data to Laurie Poiret Task-2341886 - 24/ test_l10n_be_hr_payroll_account: Add test for unjustified reasons - 25/ test_l10n_be_hr_payroll_account: Add tests for small unemployment - 26/ test_l10n_be_hr_payroll_account: Add tests for half days time off - 27/ test_l10n_be_hr_payroll_account: Fix false positive - 28/ hr_holidays: Prevent creating time off of 0 hours Purpose: It makes no sense + it could cause conflicts when generating the work entries - 29/ test_l10n_be_hr_payroll_account: Add test for end of contract - 30/ test_l10n_be_hr_payroll_account: Add tests for public holidays - 31/ hr_payroll: Display amount if hourly wage - 32/ hr_work_entry_contract: Allow creating record on gantt view - 33/ l10n_be_hr_payroll: Add new structure for student employees Purpose: Also bring some fixes on fields display + hours/days counts TaskID: 2347843 - 34/ test_l10n_be_hr_payroll_account: Add test for low salaries Ensure the correct computation of the employment bonus. - 35/ l10n_be_hr_payroll: Make separate lines for half/full worked days Purpose: Worked days should be separated between the complete full days and the half days. Example: I work on Monday, Tuesday, Wednesday (half day) The calculation is OK, but the number of days is, in principle, 14 in the month of September 2020 (9 full and 5 in half-days). This number of days should be the subject of 2 separate lines: 9 days - 68.4 hours - € 1,100.77 5 days (in the form of half-days) - 19 hours - 224.33 €. In this case, we cannot use the hourly formula since the monthly salary must always be the same, without having an identical number of working days. The worker benefits from 14 meal vouchers. The number of days must however correspond to the number of meal vouchers so that the worker can check. - 36/ test_l10n_be_hr_payroll: Test credit time with sick leaves - 37/ test_l10n_be_hr_payroll_account: Test credit time with unpaid leaves - 38/ l10n_be_hr_payroll: Don't consider time off as credit times Purpose: When generating the work entries, we compare the standard attendances (i.e. the full time contract) and the credit time attendances to deduct the intervals on which the employee is on credit time. But if the employee is on a time off during a period, those intervals will be removed from the supposed attendances, and then those periods are considered as credit time. - 39/ l10n_be_hr_payroll: Fix credit time amount on payslip computation Purpose: The time credit days must not include a corresponding amount. In fact, no remuneration is linked to the taking of time credits. The necessary modifications are: Code LEAVE300 -> no amount - 40/ hr_payroll: Export standard calendar if needed on payslip export - 41/ l10n_be_hr_payroll: Fix worked days lines amount computation Purpose: The amount was computed as a simple ratio based on the number of days. To be correct and compliant with the law, we should consider the hourly equivalent wage, and compute the amount based on the worked hours. - 42/ l10n_be_hr_payroll: Fix unpaid worked days amount computation Purpose: Don't use the normal wage which is an integer btw. Compute the worked days amount using the classic hourly computation formula - 43/ l10n_be_hr_payroll: Fix unpaid leaves retenues formula Purpose: Unpaid leave must be deducted from the usual gross monthly salary up to the number of working hours concerned. The formula is in principle the following: gross monthly salary * 3/12/38 -> “usual” hourly salary. So, in the case of 2 days of unpaid leave, corresponding to 15.2 hours, the formula is as follows: 2650 * 3/13/38 * 15.2 = 244.61 €. The gross for the days actually worked is € 2,650 - € 244.61, or € 2,405.39 - 44/ hr_payroll: Generate python test from arbitrary payslip When the payroll is incorrect, the feedback that we receive is generally: "this doesn't work". This commit add a route in the controller of hr_payroll, to export in a python file, all data useful to reproduce the exact employee situation about the incorrect payslip. Before testing and fixing the incorrect payslip, we need to anonymize all sensitive data about the employee. This commit converts all sensitive data into demo data. This commit add a button in payslip form view to use the new route to export all data about the incorrect payroll. This button is only displayed when we are in debug mode and the current user is a payroll manager. task-2337786 closes odoo/odoo#60136 X-original-commit: 60eaecb749ad68429e4d8ba00ff0200c3360cdcb Related: odoo/enterprise#14147 Signed-off-by: Yannick Tivisse (yti) --- addons/hr_holidays/i18n/hr_holidays.pot | 8 ++ addons/hr_holidays/models/hr_leave.py | 10 ++- .../tests/test_automatic_leave_dates.py | 83 +++++++++-------- .../security/ir.model.access.csv | 3 +- addons/resource/models/resource.py | 89 ++++++++++++++++--- addons/web/static/src/scss/report.scss | 17 ++++ 6 files changed, 154 insertions(+), 56 deletions(-) diff --git a/addons/hr_holidays/i18n/hr_holidays.pot b/addons/hr_holidays/i18n/hr_holidays.pot index a696285504a..5c00fc18ecb 100644 --- a/addons/hr_holidays/i18n/hr_holidays.pot +++ b/addons/hr_holidays/i18n/hr_holidays.pot @@ -2534,6 +2534,14 @@ msgid "" "missing. Please make sure that your user login is linked to an employee." msgstr "" +#. module: hr_holidays +#: code:addons/hr_holidays/models/hr_leave.py:0 +#, python-format +msgid "" +"The following employees are not supposed to work during that period:\n" +" %s" +msgstr "" + #. module: hr_holidays #: model:ir.model.constraint,message:hr_holidays.constraint_hr_leave_allocation_interval_number_check msgid "The interval number should be greater than 0" diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 04dad838585..06d6dfad818 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -548,7 +548,13 @@ class HolidaysRequest(models.Model): else: holiday.can_approve = True - @api.constrains('date_from', 'date_to', 'state', 'employee_id') + @api.constrains('date_from', 'date_to') + def _check_number_of_days(self): + leaves = self.filtered(lambda l: l.employee_id and not l.number_of_days) + if leaves: + raise ValidationError(_('The following employees are not supposed to work during that period:\n %s') % ','.join(leaves.mapped('employee_id.name'))) + + @api.constrains('date_from', 'date_to', 'employee_id') def _check_date(self): for holiday in self.filtered('employee_id'): domain = [ @@ -923,7 +929,7 @@ class HolidaysRequest(models.Model): 'parent_id': self.id, 'employee_id': employee.id, 'state': 'validate', - } for employee in employees] + } for employee in employees if work_days_data[employee.id]['days']] def action_draft(self): if any(holiday.state not in ['confirm', 'refuse'] for holiday in self): diff --git a/addons/hr_holidays/tests/test_automatic_leave_dates.py b/addons/hr_holidays/tests/test_automatic_leave_dates.py index 3fb05d7f9e5..c19bf449ccc 100644 --- a/addons/hr_holidays/tests/test_automatic_leave_dates.py +++ b/addons/hr_holidays/tests/test_automatic_leave_dates.py @@ -4,6 +4,7 @@ from datetime import date, datetime from odoo.tests.common import Form from odoo.addons.hr_holidays.tests.common import TestHrHolidaysCommon +from odoo.exceptions import ValidationError class TestAutomaticLeaveDates(TestHrHolidaysCommon): @@ -25,15 +26,16 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon): employee = self.employee_emp employee.resource_calendar_id = calendar - with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: - leave_form.holiday_status_id = self.leave_type - leave_form.request_date_from = date(2019, 9, 2) - leave_form.request_date_to = date(2019, 9, 2) - leave_form.request_unit_half = True - leave_form.request_date_from_period = 'am' + with self.assertRaises(ValidationError): + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' - self.assertEqual(leave_form.number_of_days_display, 0) - self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') def test_single_attendance_on_morning_and_afternoon(self): calendar = self.env['resource.calendar'].create({ @@ -164,18 +166,19 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon): employee = self.employee_emp employee.resource_calendar_id = calendar - with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: - leave_form.holiday_status_id = self.leave_type - leave_form.request_date_from = date(2019, 9, 2) - leave_form.request_date_to = date(2019, 9, 2) - leave_form.request_unit_half = True - leave_form.request_date_from_period = 'am' + with self.assertRaises(ValidationError): + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' - self.assertEqual(leave_form.number_of_days_display, 0) - self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') - self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) - self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) def test_attendance_previous_day(self): self.env.user.tz = 'Europe/Brussels' @@ -193,18 +196,19 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon): employee = self.employee_emp employee.resource_calendar_id = calendar - with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: - leave_form.holiday_status_id = self.leave_type - leave_form.request_date_from = date(2019, 9, 3) - leave_form.request_date_to = date(2019, 9, 3) - leave_form.request_unit_half = True - leave_form.request_date_from_period = 'am' + with self.assertRaises(ValidationError): + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + leave_form.request_date_from = date(2019, 9, 3) + leave_form.request_date_to = date(2019, 9, 3) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' - self.assertEqual(leave_form.number_of_days_display, 0) - self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') - self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0)) - self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0)) + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0)) def test_2weeks_calendar(self): self.env.user.tz = 'Europe/Brussels' @@ -276,15 +280,16 @@ class TestAutomaticLeaveDates(TestHrHolidaysCommon): employee = self.employee_emp employee.resource_calendar_id = calendar - with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: - leave_form.holiday_status_id = self.leave_type - # even week, does not work - leave_form.request_date_from = date(2019, 9, 2) - leave_form.request_date_to = date(2019, 9, 2) - leave_form.request_unit_half = True - leave_form.request_date_from_period = 'am' + with self.assertRaises(ValidationError): + with Form(self.env['hr.leave'].with_context(default_employee_id=employee.id)) as leave_form: + leave_form.holiday_status_id = self.leave_type + # even week, does not work + leave_form.request_date_from = date(2019, 9, 2) + leave_form.request_date_to = date(2019, 9, 2) + leave_form.request_unit_half = True + leave_form.request_date_from_period = 'am' - self.assertEqual(leave_form.number_of_days_display, 0) - self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') - self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) - self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) + self.assertEqual(leave_form.number_of_days_display, 0) + self.assertEqual(leave_form.number_of_hours_text, '0.0 Hours') + self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0)) + self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0)) diff --git a/addons/hr_work_entry/security/ir.model.access.csv b/addons/hr_work_entry/security/ir.model.access.csv index 7350f08cb78..ac50de79e81 100644 --- a/addons/hr_work_entry/security/ir.model.access.csv +++ b/addons/hr_work_entry/security/ir.model.access.csv @@ -1,5 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_hr_work_entry_officer,access_hr_work_entry_officer,model_hr_work_entry,hr.group_hr_user,1,1,1,1 +access_hr_work_entry_officer,access_hr_work_entry_officer,model_hr_work_entry,hr.group_hr_user,1,1,1,0 +access_hr_work_entry_system,access_hr_work_entry_system,model_hr_work_entry,base.group_system,1,1,1,1 access_hr_work_entry_type_officer,access_hr_work_entry_type_officer,model_hr_work_entry_type,hr.group_hr_user,1,0,0,0 access_hr_work_entry_type_manager,access_hr_work_entry_type_manager,model_hr_work_entry_type,hr.group_hr_manager,1,1,1,1 access_hr_work_entry_employee,access_hr_work_entry_employee,model_hr_user_work_entry_employee,hr.group_hr_user,1,1,1,1 \ No newline at end of file diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py index bd427edd891..85d550ff5e3 100644 --- a/addons/resource/models/resource.py +++ b/addons/resource/models/resource.py @@ -159,18 +159,33 @@ class ResourceCalendar(models.Model): if not res.get('name') and res.get('company_id'): res['name'] = _('Working Hours of %s', self.env['res.company'].browse(res['company_id']).name) if 'attendance_ids' in fields and not res.get('attendance_ids'): - res['attendance_ids'] = [ - (0, 0, {'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), - (0, 0, {'name': _('Monday Afternoon'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), - (0, 0, {'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), - (0, 0, {'name': _('Tuesday Afternoon'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), - (0, 0, {'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), - (0, 0, {'name': _('Wednesday Afternoon'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), - (0, 0, {'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), - (0, 0, {'name': _('Thursday Afternoon'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), - (0, 0, {'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), - (0, 0, {'name': _('Friday Afternoon'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}) - ] + company_id = res.get('company_id', self.env.company.id) + company = self.env['res.company'].browse(company_id) + company_attendance_ids = company.resource_calendar_id.attendance_ids + if company_attendance_ids: + res['attendance_ids'] = [ + (0, 0, { + 'name': attendance.name, + 'dayofweek': attendance.dayofweek, + 'hour_from': attendance.hour_from, + 'hour_to': attendance.hour_to, + 'day_period': attendance.day_period, + }) + for attendance in company_attendance_ids + ] + else: + res['attendance_ids'] = [ + (0, 0, {'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': _('Monday Afternoon'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': _('Tuesday Afternoon'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': _('Wednesday Afternoon'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': _('Thursday Afternoon'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}), + (0, 0, {'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning'}), + (0, 0, {'name': _('Friday Afternoon'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon'}) + ] return res name = fields.Char(required=True) @@ -181,13 +196,14 @@ class ResourceCalendar(models.Model): default=lambda self: self.env.company) attendance_ids = fields.One2many( 'resource.calendar.attendance', 'calendar_id', 'Working Time', - copy=True) + compute='_compute_attendance_ids', store=True, readonly=False, copy=True) leave_ids = fields.One2many( 'resource.calendar.leaves', 'calendar_id', 'Time Off') global_leave_ids = fields.One2many( 'resource.calendar.leaves', 'calendar_id', 'Global Time Off', + compute='_compute_global_leave_ids', store=True, readonly=False, domain=[('resource_id', '=', False)], copy=True, - ) + ) hours_per_day = fields.Float("Average Hour per Day", default=HOURS_PER_DAY, help="Average hours per day a resource is supposed to work with this calendar.") tz = fields.Selection( @@ -197,6 +213,42 @@ class ResourceCalendar(models.Model): two_weeks_calendar = fields.Boolean(string="Calendar in 2 weeks mode") two_weeks_explanation = fields.Char('Explanation', compute="_compute_two_weeks_explanation") + @api.depends('company_id') + def _compute_attendance_ids(self): + for calendar in self.filtered(lambda c: not c._origin or c._origin.company_id != c.company_id): + company_calendar = calendar.company_id.resource_calendar_id + calendar.write({ + 'two_weeks_calendar': company_calendar.two_weeks_calendar, + 'hours_per_day': company_calendar.hours_per_day, + 'tz': company_calendar.tz, + 'attendance_ids': [(5, 0, 0)] + [ + (0, 0, { + 'name': attendance.name, + 'dayofweek': attendance.dayofweek, + 'date_from': attendance.date_from, + 'date_to': attendance.date_to, + 'hour_from': attendance.hour_from, + 'hour_to': attendance.hour_to, + 'day_period': attendance.day_period, + 'week_type': attendance.week_type, + 'display_type': attendance.display_type, + 'sequence': attendance.sequence, + }) for attendance in company_calendar.attendance_ids if not attendance.resource_id] + }) + + @api.depends('company_id') + def _compute_global_leave_ids(self): + for calendar in self.filtered(lambda c: not c._origin or c._origin.company_id != c.company_id): + calendar.write({ + 'global_leave_ids': [(5, 0, 0)] + [ + (0, 0, { + 'name': leave.name, + 'date_from': leave.date_from, + 'date_to': leave.date_to, + 'time_type': leave.time_type, + }) for leave in calendar.company_id.resource_calendar_id.global_leave_ids] + }) + @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() @@ -744,6 +796,15 @@ class ResourceCalendar(models.Model): else: return revert(day_dt) + def _get_max_number_of_hours(self, start, end): + self.ensure_one() + if not self.attendance_ids: + return 0 + mapped_data = defaultdict(lambda: 0) + for attendance in self.attendance_ids.filtered(lambda a: (not a.date_from or not a.date_to) or (a.date_from <= end and a.date_to >= start)): + mapped_data[(attendance.week_type, attendance.dayofweek)] += attendance.hour_to - attendance.hour_from + return max(mapped_data.values()) + class ResourceCalendarAttendance(models.Model): _name = "resource.calendar.attendance" diff --git a/addons/web/static/src/scss/report.scss b/addons/web/static/src/scss/report.scss index 91500a689a7..0edc31005f4 100644 --- a/addons/web/static/src/scss/report.scss +++ b/addons/web/static/src/scss/report.scss @@ -60,6 +60,23 @@ div#total { td { border-top: 1px solid black !important; } + &.o_border_bottom { + td { + border-bottom: 1px solid black !important; + } + } + } + } + } +} + +table { + thead { + &.o_black_border { + tr { + th { + border-bottom: 2px solid black !important; + } } } }