From c61c53229b93fc593397148e5f8e5bcd41083db1 Mon Sep 17 00:00:00 2001 From: Cyril VINH-TUNG Date: Wed, 13 May 2026 21:27:23 -1000 Subject: [PATCH] [IMP] payroll: add opening values (seniority and leave balances) for contract - helped by Claude Opus-4.7 --- payroll/README.rst | 17 +++++-- payroll/models/hr_contract.py | 37 +++++++++++++- payroll/readme/CONTRIBUTORS.md | 1 + payroll/readme/DESCRIPTION.md | 9 ++++ payroll/static/description/index.html | 39 +++++++-------- payroll/tests/__init__.py | 1 + payroll/tests/test_hr_contract_opening.py | 60 +++++++++++++++++++++++ payroll/views/hr_contract_views.xml | 21 ++++++++ 8 files changed, 159 insertions(+), 26 deletions(-) create mode 100644 payroll/tests/test_hr_contract_opening.py diff --git a/payroll/README.rst b/payroll/README.rst index cfb27271c..4b1fe981a 100644 --- a/payroll/README.rst +++ b/payroll/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======= Payroll ======= @@ -17,7 +13,7 @@ Payroll .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html :alt: License: LGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpayroll-lightgray.png?logo=github @@ -38,6 +34,16 @@ This module is a backport from Odoo SA and as such, it is not included in the OCA CLA. That means we do not have a copy of the copyright on it like all other OCA modules. +Opening Values +-------------- + +Each contract exposes an **Opening Values** tab to carry over reference +data when migrating from another payroll system: seniority date, opening +date, opening paid leave base and opening paid leave days balance. These +fields are created for use by each country's localization modules; in +this module alone they are only informational and drive no computation. +They become read-only once a payslip exists for the contract. + **Table of contents** .. contents:: @@ -70,6 +76,7 @@ Contributors - Nimarosa (Nicolas Rodriguez) - Henrik Norlin (@appstogrow) - Régis Pirard +- Cyril VINH-TUNG Maintainers ----------- diff --git a/payroll/models/hr_contract.py b/payroll/models/hr_contract.py index bb62508ed..863b069bd 100644 --- a/payroll/models/hr_contract.py +++ b/payroll/models/hr_contract.py @@ -1,6 +1,6 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class HrContract(models.Model): @@ -32,6 +32,41 @@ class HrContract(models.Model): required=True, help="Employee's working schedule." ) + seniority_date = fields.Date( + help="Starting date used for seniority computation.", + ) + + opening_date = fields.Date( + help="Cut-off date of the opening balances, not necessarily the " + "contract start; payslips dated after it take over from these values.", + ) + opening_leave_base = fields.Monetary( + string="Opening Paid Leave Base", + currency_field="currency_id", + help="Cumulative paid leave reference base at the opening date.", + ) + opening_leave_days = fields.Float( + string="Opening Paid Leave Days Balance", + digits=(16, 2), + help="Paid leave days acquired and not yet taken at the opening date.", + ) + + payslip_ids = fields.One2many( + "hr.payslip", + "contract_id", + string="Payslips", + help="Payslips generated for this contract.", + ) + payslip_count = fields.Integer( + compute="_compute_payslip_count", + help="Number of payslips on the contract; locks the opening fields.", + ) + + @api.depends("payslip_ids") + def _compute_payslip_count(self): + for contract in self: + contract.payslip_count = len(contract.payslip_ids) + def get_all_structures(self): """ @return: the structures linked to the given contracts, ordered by diff --git a/payroll/readme/CONTRIBUTORS.md b/payroll/readme/CONTRIBUTORS.md index 11cb0ddf0..34d8a0a10 100644 --- a/payroll/readme/CONTRIBUTORS.md +++ b/payroll/readme/CONTRIBUTORS.md @@ -4,3 +4,4 @@ - Nimarosa (Nicolas Rodriguez) \<\> - Henrik Norlin (@appstogrow) - Régis Pirard \<\> +- Cyril VINH-TUNG \<\> diff --git a/payroll/readme/DESCRIPTION.md b/payroll/readme/DESCRIPTION.md index 4fc60e5e5..dab2ed53b 100644 --- a/payroll/readme/DESCRIPTION.md +++ b/payroll/readme/DESCRIPTION.md @@ -3,3 +3,12 @@ Manage your employee payroll records. This module is a backport from Odoo SA and as such, it is not included in the OCA CLA. That means we do not have a copy of the copyright on it like all other OCA modules. + +## Opening Values + +Each contract exposes an **Opening Values** tab to carry over reference +data when migrating from another payroll system: seniority date, opening +date, opening paid leave base and opening paid leave days balance. These +fields are created for use by each country's localization modules; in +this module alone they are only informational and drive no computation. +They become read-only once a payslip exists for the contract. diff --git a/payroll/static/description/index.html b/payroll/static/description/index.html index a6a296851..84c16bc08 100644 --- a/payroll/static/description/index.html +++ b/payroll/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Payroll -
+
+

Payroll

- - -Odoo Community Association - -
-

Payroll

-

Beta License: LGPL-3 OCA/payroll Translate me on Weblate Try me on Runboat

+

Beta License: LGPL-3 OCA/payroll Translate me on Weblate Try me on Runboat

Manage your employee payroll records.

This module is a backport from Odoo SA and as such, it is not included in the OCA CLA. That means we do not have a copy of the copyright on it like all other OCA modules.

+
+

Opening Values

+

Each contract exposes an Opening Values tab to carry over reference +data when migrating from another payroll system: seniority date, opening +date, opening paid leave base and opening paid leave days balance. These +fields are created for use by each country’s localization modules; in +this module alone they are only informational and drive no computation. +They become read-only once a payslip exists for the contract.

Table of contents

@@ -401,14 +399,16 @@

Bug Tracker

+
-

Authors

+

Authors

  • Odoo SA
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -433,7 +434,5 @@

Maintainers

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

-
-
diff --git a/payroll/tests/__init__.py b/payroll/tests/__init__.py index 69286796c..6d3298390 100644 --- a/payroll/tests/__init__.py +++ b/payroll/tests/__init__.py @@ -1,6 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_browsable_object +from . import test_hr_contract_opening from . import test_hr_payslip_worked_days from . import test_hr_salary_rule from . import test_payslip_flow diff --git a/payroll/tests/test_hr_contract_opening.py b/payroll/tests/test_hr_contract_opening.py new file mode 100644 index 000000000..9327d186a --- /dev/null +++ b/payroll/tests/test_hr_contract_opening.py @@ -0,0 +1,60 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.fields import Date + +from .common import TestPayslipBase + + +class TestHrContractOpening(TestPayslipBase): + def setUp(self): + super().setUp() + self.employee = self.env["hr.employee"].create({"name": "Test Employee"}) + self.contract = self.Contract.create( + { + "name": "Test Contract", + "employee_id": self.employee.id, + "wage": 200000.0, + "state": "open", + "date_start": Date.from_string("2026-01-01"), + } + ) + + def test_opening_fields_default(self): + """Opening fields are empty by default and contract has no payslips.""" + self.assertFalse(self.contract.opening_date) + self.assertFalse(self.contract.seniority_date) + self.assertEqual(self.contract.opening_leave_base, 0.0) + self.assertEqual(self.contract.opening_leave_days, 0.0) + self.assertEqual(self.contract.payslip_count, 0) + + def test_opening_fields_writable(self): + """Opening fields can be written when no payslip exists.""" + self.contract.write( + { + "opening_date": Date.from_string("2026-05-31"), + "seniority_date": Date.from_string("2018-03-15"), + "opening_leave_base": 1200000.0, + "opening_leave_days": 12.5, + } + ) + self.assertEqual(self.contract.opening_date, Date.from_string("2026-05-31")) + self.assertEqual( + self.contract.seniority_date, + Date.from_string("2018-03-15"), + ) + self.assertEqual(self.contract.opening_leave_base, 1200000.0) + self.assertEqual(self.contract.opening_leave_days, 12.5) + + def test_payslip_count(self): + """payslip_count reflects the number of payslips for the contract.""" + self.assertEqual(self.contract.payslip_count, 0) + self.Payslip.create( + { + "name": "Test Payslip", + "employee_id": self.employee.id, + "contract_id": self.contract.id, + "date_from": Date.from_string("2026-06-01"), + "date_to": Date.from_string("2026-06-30"), + } + ) + self.assertEqual(self.contract.payslip_count, 1) diff --git a/payroll/views/hr_contract_views.xml b/payroll/views/hr_contract_views.xml index e7802cf88..739af3116 100644 --- a/payroll/views/hr_contract_views.xml +++ b/payroll/views/hr_contract_views.xml @@ -11,6 +11,27 @@ + + + + + + + + + +

+ Opening values are locked once payslips exist for this + contract. +

+
+