257 lines
12 KiB
Python
257 lines
12 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import date
|
|
|
|
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
|
|
from odoo.tests import tagged
|
|
from odoo.tools import float_compare
|
|
|
|
|
|
@tagged('post_install', 'post_install_l10n', '-at_install', 'payslips_validation')
|
|
class TestPayslipValidationCommon(AccountTestInvoicingCommon):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super().setUpClass()
|
|
cls.env.user.group_ids += cls.env.ref('hr_payroll.group_hr_payroll_user')
|
|
|
|
@classmethod
|
|
def _setup_common(cls, country, structure, structure_type, resource_calendar=False, car=False, contract_fields=False, employee_fields=False, tz=False):
|
|
"""
|
|
This method setups a set of common models that will be used to test payslip validation.
|
|
|
|
It will create a partner and an employee that are in the company in cls.env.company.
|
|
The employee and company resource calendar will be a standard full-time 40h/week calendar.
|
|
The default timezone is 'America/Los_Angeles'.
|
|
|
|
It will also create a contract for the employee in the same company with:
|
|
- a default start date at the 1st of January 2016
|
|
- a default wage amounting to 1000.0 of whatever the currency of the company is.
|
|
|
|
In the following parameter descriptions, 'xx' is the standard 2-letter country code.
|
|
:param country: Record of the country (usually base.xx)
|
|
:param structure: Record of the default structure (usually l10n_xx_hr_payroll.hr_payroll_structure_xx_employee_salary)
|
|
:param structure_type: Record of the default structure type (usually l10n_xx_hr_payroll.structure_type_employee_xx)
|
|
:param resource_calendar: Record of a resource calendar if you want to replace the default one
|
|
:param car: Record of the employee's fleet car, False by default
|
|
:param contract_fields: Dict of field names: value, that will be overridden on the contract created
|
|
:param employee_fields: Dict of field names: value, that will be overridden on the employee created
|
|
:param tz: String of a timezone that is set for both the user and the resource calendar
|
|
:return:
|
|
"""
|
|
cls.country = country
|
|
country_code = country.code.upper()
|
|
|
|
cls.structure = structure
|
|
|
|
cls.tz = tz or 'America/Los_Angeles'
|
|
cls.env.user.tz = cls.tz
|
|
|
|
cls.work_contact = cls.env['res.partner'].create({
|
|
'name': country_code + ' Employee',
|
|
'company_id': cls.env.company.id,
|
|
})
|
|
cls.resource_calendar = resource_calendar or cls.env['resource.calendar'].sudo().create([{
|
|
'name': "Standard Calendar : 40 Hours/Week",
|
|
'company_id': cls.env.company.id,
|
|
'hours_per_day': 8.0,
|
|
'tz': cls.tz,
|
|
'two_weeks_calendar': False,
|
|
'hours_per_week': 40.0,
|
|
'full_time_required_hours': 40.0,
|
|
'attendance_ids': [(5, 0, 0)] + [(0, 0, {
|
|
'name': "Attendance",
|
|
'dayofweek': dayofweek,
|
|
'hour_from': hour_from,
|
|
'hour_to': hour_to,
|
|
'day_period': day_period,
|
|
'work_entry_type_id': cls.env.ref('hr_work_entry.work_entry_type_attendance').id
|
|
}) for dayofweek, hour_from, hour_to, day_period in [
|
|
("0", 8.0, 12.0, "morning"),
|
|
("0", 12.0, 13.0, "lunch"),
|
|
("0", 13.0, 17.0, "afternoon"),
|
|
("1", 8.0, 12.0, "morning"),
|
|
("1", 12.0, 13.0, "lunch"),
|
|
("1", 13.0, 17.0, "afternoon"),
|
|
("2", 8.0, 12.0, "morning"),
|
|
("2", 12.0, 13.0, "lunch"),
|
|
("2", 13.0, 17.0, "afternoon"),
|
|
("3", 8.0, 12.0, "morning"),
|
|
("3", 12.0, 13.0, "lunch"),
|
|
("3", 13.0, 17.0, "afternoon"),
|
|
("4", 8.0, 12.0, "morning"),
|
|
("4", 12.0, 13.0, "lunch"),
|
|
("4", 13.0, 17.0, "afternoon"),
|
|
]],
|
|
}]).sudo(False)
|
|
cls.env.company.write({
|
|
'resource_calendar_id': cls.resource_calendar.id,
|
|
})
|
|
|
|
cls.employee = cls.env['hr.employee'].sudo().create({
|
|
'name': country_code + ' Employee',
|
|
'work_contact_id': cls.work_contact.id,
|
|
'address_id': cls.work_contact.id,
|
|
'resource_calendar_id': cls.resource_calendar.id,
|
|
'company_id': cls.env.company.id,
|
|
'country_id': country.id,
|
|
'structure_type_id': structure_type.id,
|
|
'contract_date_start': date(2016, 1, 1),
|
|
'date_version': date(2016, 1, 1),
|
|
'wage': 1000.0,
|
|
**(employee_fields or {})
|
|
}).sudo(False)
|
|
|
|
contract = cls.employee.sudo().version_id
|
|
if contract_fields:
|
|
contract.write(contract_fields)
|
|
cls.contract = contract.sudo(False)
|
|
|
|
cls.car = car
|
|
if cls.car:
|
|
cls.car.sudo().write({'driver_id': cls.employee.work_contact_id.id})
|
|
# This field only exists if fleet is installed
|
|
cls.contract.sudo().write({'car_id': cls.car.id})
|
|
|
|
@classmethod
|
|
def _generate_payslip(cls, date_from, date_to, struct_id=False, input_line_ids=False, version_id=False, employee_id=False):
|
|
work_entries = cls.contract.generate_work_entries(date_from, date_to)
|
|
payslip = cls.env['hr.payslip'].create([{
|
|
'name': "Test Payslip",
|
|
'employee_id': employee_id or cls.employee.id,
|
|
'version_id': version_id or cls.contract.id,
|
|
'company_id': cls.env.company.id,
|
|
'struct_id': struct_id or cls.structure.id,
|
|
'date_from': date_from,
|
|
'date_to': date_to,
|
|
'input_line_ids': input_line_ids or [],
|
|
}])
|
|
# This field only exists if fleet is installed
|
|
if cls.car:
|
|
payslip.write({'vehicle_id': cls.car.id})
|
|
work_entries.action_validate()
|
|
payslip.compute_sheet()
|
|
return payslip
|
|
|
|
@classmethod
|
|
def _generate_leave(cls, date_from, date_to, holiday_status_id):
|
|
leave = cls.env['hr.leave'].sudo().create({
|
|
'employee_id': cls.employee.id,
|
|
'request_date_from': date_from,
|
|
'request_date_to': date_to,
|
|
'holiday_status_id': holiday_status_id.id,
|
|
})
|
|
|
|
if holiday_status_id.leave_validation_type != 'no_validation':
|
|
leave.action_approve()
|
|
|
|
def _validate_payslip(self, payslip, results, skip_lines=False):
|
|
error = []
|
|
payslip_lines = payslip.line_ids.filtered(lambda l: not l.salary_rule_id.title)
|
|
line_values = payslip._get_line_values(set(payslip_lines.mapped('code')))
|
|
for code, value in results.items():
|
|
if code in line_values:
|
|
payslip_line_value = line_values[code][payslip.id]['total']
|
|
if float_compare(payslip_line_value, value, 2):
|
|
error.append(f"{'WRONG CALCULATION':>20} │ {code:<30} │ {value:>15} │ {payslip_line_value:>15} │ {round(payslip_line_value-value, 2):>15} │")
|
|
if not skip_lines:
|
|
for code, value in results.items():
|
|
if code not in payslip_lines.mapped('code'):
|
|
error.append(
|
|
f"{'UNNECESSARY LINE':>20} │ {code:<30} │ {value:>15} │ {'/':>15} │")
|
|
for line in payslip_lines:
|
|
if line.code not in results:
|
|
error.append(
|
|
f"{'MISSING LINE':>20} │ {line.code:<30} │ {'/':>15} │ {line_values[line.code][payslip.id]['total']:>15} │")
|
|
if error:
|
|
error.insert(
|
|
0,
|
|
f"{'ERROR':>20} │ {'CODE':<30} │ {'EXPECTED':>15} │ {'REALITY':>15} │ {'DIFFERENCE':>15} │\n"
|
|
f"{'':>20} │ {'':<30} │ {'':>15} │ {'':>15} │ {'':>15} │")
|
|
error.extend([
|
|
"",
|
|
f"Payslip Period: {payslip.date_from} - {payslip.date_to}",
|
|
"Payslip Actual Values: ",
|
|
" payslip_results = {" + ', '.join(f"'{line.code}': {line_values[line.code][payslip.id]['total']}" for line in payslip_lines) + "}"
|
|
])
|
|
self.assertEqual(len(error), 0, '\n\n' + '\n'.join(error))
|
|
|
|
def _validate_worked_days(self, payslip, results, skip_lines=False):
|
|
"""
|
|
Validate the worked days lines of a payslip.
|
|
:param payslip: The payslip
|
|
:param results: A dict of {work_entry_code: (number_of_days, number_of_hours, amount)}
|
|
:param skip_lines: If True, we don't check that all lines are present in the payslip
|
|
:return:
|
|
"""
|
|
error = []
|
|
line_values = payslip._get_worked_days_line_values(set(results.keys()) | set(payslip.worked_days_line_ids.mapped('code')), ['number_of_days', 'number_of_hours', 'amount'])
|
|
for code, (number_of_days, number_of_hours, amount) in results.items():
|
|
payslip_line_value = line_values[code][payslip.id]['number_of_days']
|
|
if float_compare(payslip_line_value, number_of_days, 2):
|
|
error.append("Code: %s - Expected Number of Days: %s - Reality: %s" % (code, number_of_days, payslip_line_value))
|
|
payslip_line_value = line_values[code][payslip.id]['number_of_hours']
|
|
if float_compare(payslip_line_value, number_of_hours, 2):
|
|
error.append("Code: %s - Expected Number of Hours: %s - Reality: %s" % (code, number_of_hours, payslip_line_value))
|
|
payslip_line_value = line_values[code][payslip.id]['amount']
|
|
if float_compare(payslip_line_value, amount, 2):
|
|
error.append("Code: %s - Expected Amount: %s - Reality: %s" % (code, amount, payslip_line_value))
|
|
if not skip_lines:
|
|
for line in payslip.worked_days_line_ids:
|
|
if line.code not in results:
|
|
error.append("Missing Line: '%s' - %s Days - %s Hours - %s CHF," % (
|
|
line.code,
|
|
line_values[line.code][payslip.id]['number_of_days'],
|
|
line_values[line.code][payslip.id]['number_of_hours'],
|
|
line_values[line.code][payslip.id]['amount'],
|
|
))
|
|
if error:
|
|
error.extend([
|
|
f"Payslip Period: {payslip.date_from} - {payslip.date_to}",
|
|
"Payslip Actual Values: ",
|
|
" {"
|
|
])
|
|
for line in payslip.worked_days_line_ids:
|
|
error.append(" '%s': (%s, %s, %s)," % (
|
|
line.code,
|
|
line_values[line.code][payslip.id]['number_of_days'],
|
|
line_values[line.code][payslip.id]['number_of_hours'],
|
|
line_values[line.code][payslip.id]['amount'],
|
|
))
|
|
error.append(" }")
|
|
self.assertEqual(len(error), 0, '\n' + '\n'.join(error))
|
|
|
|
def _validate_move_lines(self, lines, results):
|
|
error = []
|
|
for code, move_type, amount in results:
|
|
if not any(l.account_id.code == code and not float_compare(l[move_type], amount, 2) for l in lines):
|
|
error.append("Couldn't find %s move line on account %s with amount %s" % (move_type, code, amount))
|
|
if error:
|
|
for line in lines:
|
|
for move_type in ['credit', 'debit']:
|
|
if line[move_type]:
|
|
error.append('%s - %s - %s' % (line.account_id.code, move_type, line[move_type]))
|
|
self.assertEqual(len(error), 0, '\n' + '\n'.join(error))
|
|
|
|
def _add_other_input(self, payslip_id, other_input_id, amount):
|
|
self.env['hr.payslip.input'].create({
|
|
'payslip_id': payslip_id.id,
|
|
'input_type_id': other_input_id.id,
|
|
'amount': amount,
|
|
})
|
|
|
|
def _add_rule_parameter_value(self, rule_parameter_code, value, date):
|
|
rule_parameter_id = self.env['hr.rule.parameter'].search([('code', '=', rule_parameter_code)]).id
|
|
value_on_same_date = self.env['hr.rule.parameter.value'].search([
|
|
('rule_parameter_id', '=', rule_parameter_id),
|
|
('date_from', '=', date)
|
|
])
|
|
if value_on_same_date:
|
|
value_on_same_date.sudo().write({'parameter_value': value})
|
|
else:
|
|
self.env['hr.rule.parameter.value'].sudo().create({
|
|
'rule_parameter_id': rule_parameter_id,
|
|
'parameter_value': value,
|
|
'date_from': date,
|
|
})
|