Files

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,
})