# Part of Odoo. See LICENSE file for full copyright and licensing details. import odoo.tests from datetime import date, datetime from odoo.addons.base.tests.common import BaseCommon @odoo.tests.tagged('post_install', '-at_install') class TestAnonymizedPayruns(BaseCommon): @classmethod def setUpClass(cls): super().setUpClass() cls.credit_account_full, cls.credit_account_half, cls.debit_account_full, cls.debit_account_half = cls.env['account.account'].create([ { 'name': 'Credit Account Full', 'account_type': 'asset_cash', 'code': 0 }, { 'name': 'Credit Account Half', 'account_type': 'asset_cash', 'code': 1 }, { 'name': 'Debit Account', 'account_type': 'asset_cash', 'code': 2 }, { 'name': 'Debit Account Half', 'account_type': 'asset_cash', 'code': 3 }, ]) cls.test_journal = cls.env['account.journal'].create({ 'name': 'Test Journal', 'type': 'credit', 'code': 'TEST', 'default_account_id': cls.credit_account_full.id, 'currency_id': cls.env.ref('base.USD').id, 'company_id': cls.company.id, }) cls.test_structure_type_id = cls.env['hr.payroll.structure.type'].create({ 'name': 'Nordic Warrior', 'country_id': False, }) cls.plan = cls.env['account.analytic.plan'].create({'name': 'Plan'}) cls.aa_1, cls.aa_2 = cls.env['account.analytic.account'].create([ { 'name': 'Analytic Account 1', 'plan_id': cls.plan.id, }, { 'name': 'Analytic Account 2', 'plan_id': cls.plan.id, } ]) cls.nordic_warrior_structure = cls.env['hr.payroll.structure'].create({ 'name': 'Salary Structure for Nordic Warriors', 'rule_ids': [ (0, 0, { 'name': 'Basic Salary', 'amount_select': 'percentage', 'amount_percentage': 100, 'amount_percentage_base': 'version.wage', 'code': 'BASIC', 'category_id': cls.env.ref('hr_payroll.BASIC').id, 'sequence': 1, 'account_credit': cls.credit_account_full.id, 'account_debit': cls.debit_account_full.id, 'analytic_distribution': {str(cls.aa_1.id) + ',' + str(cls.aa_2.id): 100} }), (0, 0, { 'name': 'Additional Basic Salary', 'amount_select': 'percentage', 'amount_percentage': 50, 'amount_percentage_base': 'version.wage', 'code': 'BASICADD', 'category_id': cls.env.ref('hr_payroll.BASIC').id, 'sequence': 2, 'account_credit': cls.credit_account_half.id, 'account_debit': cls.debit_account_half.id }), ], 'type_id': cls.test_structure_type_id.id, 'journal_id': cls.test_journal.id }) cls.test_structure_type_id['default_struct_id'] = cls.nordic_warrior_structure.id cls.emp_thorfinn, cls.emp_einar, cls.emp_canute = cls.env['hr.employee'].create([ { 'name': 'Thorfinn', 'company_id': cls.company.id, 'version_ids': [ (0, 0, { 'name': 'Thorfinn Version', 'company_id': cls.company.id, 'contract_date_start': datetime(2026, 3, 1), 'contract_date_end': datetime(2026, 3, 31), 'wage': 3000, 'date_version': datetime(2026, 3, 1).date(), 'structure_id': cls.nordic_warrior_structure.id, 'structure_type_id': cls.nordic_warrior_structure.type_id.id, })] }, { 'name': 'Einar', 'company_id': cls.company.id, 'version_ids': [ (0, 0, { 'name': 'Einar Version', 'company_id': cls.company.id, 'contract_date_start': datetime(2026, 3, 1), 'contract_date_end': datetime(2026, 3, 31), 'wage': 3000, 'date_version': datetime(2026, 3, 1).date(), 'structure_id': cls.nordic_warrior_structure.id, 'structure_type_id': cls.nordic_warrior_structure.type_id.id, })] }, { 'name': 'Canute', 'company_id': cls.company.id, 'version_ids': [ (0, 0, { 'name': 'Canute Version', 'company_id': cls.company.id, 'contract_date_start': datetime(2026, 3, 1), 'contract_date_end': datetime(2026, 3, 31), 'wage': 3000, 'date_version': datetime(2026, 3, 1).date(), 'structure_id': cls.nordic_warrior_structure.id, 'structure_type_id': cls.nordic_warrior_structure.type_id.id, })] }, ]) for emp in [cls.emp_thorfinn, cls.emp_einar, cls.emp_canute]: emp.version_ids = emp.version_ids.filtered('name') # Remove unnamed versions created by default def test_check_correct_anonymization_with_analytic_distribution(self): self.company.batch_payroll_move_lines = True payrun = self.env['hr.payslip.run'].create({ 'name': 'Test Payrun', 'date_start': date(2026, 3, 1), 'date_end': date(2026, 3, 31), }) employees = [self.emp_thorfinn.id, self.emp_einar.id, self.emp_canute.id] payrun.generate_payslips(employee_ids=employees) payrun.action_validate() expected_result = [ ('Basic Salary', {str(self.aa_1.id) + ',' + str(self.aa_2.id): 100}, 0.0, 9000.0), ('Basic Salary', {str(self.aa_1.id) + ',' + str(self.aa_2.id): 100}, 9000.0, 0.0), ('Additional Basic Salary', False, 0.0, 4500.0), ('Additional Basic Salary', False, 4500.0, 0.0), ] real_result = [(line.name, line.analytic_distribution, line.credit, line.debit) for line in payrun.move_id.line_ids] self.assertEqual(real_result, expected_result) def test_batch_move_lines_keeps_distinct_analytic_percentages(self): """ With batch_payroll_move_lines, employees sharing the same analytic account but with different percentages must not be merged into a single move line — each distribution must keep its own line. """ self.company.batch_payroll_move_lines = True self.nordic_warrior_structure.rule_ids.analytic_distribution = False self.emp_thorfinn.version_id.analytic_distribution = {str(self.aa_1.id): 50} self.emp_einar.version_id.analytic_distribution = {str(self.aa_1.id): 70} self.emp_canute.version_id.analytic_distribution = {str(self.aa_1.id): 50} payrun = self.env['hr.payslip.run'].create({ 'name': 'Test Payrun Distinct Distributions', 'date_start': date(2026, 3, 1), 'date_end': date(2026, 3, 31), }) payrun.generate_payslips(employee_ids=[self.emp_thorfinn.id, self.emp_einar.id, self.emp_canute.id]) payrun.action_validate() expected_result = [ ('Basic Salary', {str(self.aa_1.id): 50}, 0.0, 6000.0), ('Basic Salary', {str(self.aa_1.id): 50}, 6000.0, 0.0), ('Additional Basic Salary', {str(self.aa_1.id): 50}, 0.0, 3000.0), ('Additional Basic Salary', {str(self.aa_1.id): 50}, 3000.0, 0.0), ('Basic Salary', {str(self.aa_1.id): 70}, 0.0, 3000.0), ('Basic Salary', {str(self.aa_1.id): 70}, 3000.0, 0.0), ('Additional Basic Salary', {str(self.aa_1.id): 70}, 0.0, 1500.0), ('Additional Basic Salary', {str(self.aa_1.id): 70}, 1500.0, 0.0), ] real_result = [(line.name, line.analytic_distribution, line.credit, line.debit) for line in payrun.move_id.line_ids] self.assertEqual(real_result, expected_result) def test_batch_move_lines_merges_identical_version_distributions(self): """ With batch_payroll_move_lines, employees sharing the exact same version-level analytic distribution must be merged into a single line. """ self.company.batch_payroll_move_lines = True self.nordic_warrior_structure.rule_ids.analytic_distribution = False distribution = {str(self.aa_1.id): 60} (self.emp_thorfinn + self.emp_einar + self.emp_canute).version_id.analytic_distribution = distribution payrun = self.env['hr.payslip.run'].create({ 'name': 'Test Payrun Identical Distributions', 'date_start': date(2026, 3, 1), 'date_end': date(2026, 3, 31), }) payrun.generate_payslips(employee_ids=[self.emp_thorfinn.id, self.emp_einar.id, self.emp_canute.id]) payrun.action_validate() expected_result = [ ('Basic Salary', distribution, 0.0, 9000.0), ('Basic Salary', distribution, 9000.0, 0.0), ('Additional Basic Salary', distribution, 0.0, 4500.0), ('Additional Basic Salary', distribution, 4500.0, 0.0), ] real_result = [(line.name, line.analytic_distribution, line.credit, line.debit) for line in payrun.move_id.line_ids] self.assertEqual(real_result, expected_result)