diff --git a/hr_holidays_public_city/README.rst b/hr_holidays_public_city/README.rst new file mode 100644 index 000000000..0b9049a11 --- /dev/null +++ b/hr_holidays_public_city/README.rst @@ -0,0 +1,100 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +======================= +HR Holidays Public City +======================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5f936bdf53ab4a15cf0b735495ac3e0306aedfb9d60cc3a47f869d5060851072 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fhr--holidays-lightgray.png?logo=github + :target: https://github.com/OCA/hr-holidays/tree/19.0/hr_holidays_public_city + :alt: OCA/hr-holidays +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/hr-holidays-19-0/hr-holidays-19-0-hr_holidays_public_city + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/hr-holidays&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds the cities at the public vacation line level as an +extra discriminant. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__ + + - Pedro M. Baeza + - Víctor Martínez + +- `APSL `__: + + - Antoni Marroig + +- `Niboo `__: + + - Simon Falesse + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-victoralmau| image:: https://github.com/victoralmau.png?size=40px + :target: https://github.com/victoralmau + :alt: victoralmau + +Current `maintainer `__: + +|maintainer-victoralmau| + +This module is part of the `OCA/hr-holidays `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/hr_holidays_public_city/__init__.py b/hr_holidays_public_city/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/hr_holidays_public_city/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/hr_holidays_public_city/__manifest__.py b/hr_holidays_public_city/__manifest__.py new file mode 100644 index 000000000..d82652ac0 --- /dev/null +++ b/hr_holidays_public_city/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# Copyright 2023 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "HR Holidays Public City", + "version": "19.0.1.0.0", + "category": "Tools", + "website": "https://github.com/OCA/hr-holidays", + "author": "Tecnativa, Odoo Community Association (OCA)", + "license": "AGPL-3", + "depends": ["hr_holidays_public", "base_address_extended"], + "data": [ + "views/calendar_public_holiday_view.xml", + ], + "installable": True, + "maintainers": ["victoralmau"], +} diff --git a/hr_holidays_public_city/models/__init__.py b/hr_holidays_public_city/models/__init__.py new file mode 100644 index 000000000..452061717 --- /dev/null +++ b/hr_holidays_public_city/models/__init__.py @@ -0,0 +1,3 @@ +from . import calendar_public_holiday +from . import calendar_public_holiday_line +from . import hr_leave diff --git a/hr_holidays_public_city/models/calendar_public_holiday.py b/hr_holidays_public_city/models/calendar_public_holiday.py new file mode 100644 index 000000000..9dbd24831 --- /dev/null +++ b/hr_holidays_public_city/models/calendar_public_holiday.py @@ -0,0 +1,24 @@ +# Copyright 2026 Tecnativa - Víctor Martínez +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import models + + +class ResourceCalendarPublicHoliday(models.Model): + _inherit = "calendar.public.holiday" + + def _get_domain_states_filter(self, pholidays, start_dt, end_dt, partner_id=None): + domain = super()._get_domain_states_filter( + pholidays=pholidays, start_dt=start_dt, end_dt=end_dt, partner_id=partner_id + ) + partner = partner_model = self.env["res.partner"] + if partner_id: + partner = partner_model.browse(partner_id) + if partner and partner.city_id: + domain += [ + "|", + ("city_ids", "=", False), + ("city_ids", "=", partner.city_id.id), + ] + else: + domain.append(("city_ids", "=", False)) + return domain diff --git a/hr_holidays_public_city/models/calendar_public_holiday_line.py b/hr_holidays_public_city/models/calendar_public_holiday_line.py new file mode 100644 index 000000000..3015cfb5d --- /dev/null +++ b/hr_holidays_public_city/models/calendar_public_holiday_line.py @@ -0,0 +1,40 @@ +# Copyright 2023-2025 Tecnativa - Víctor Martínez +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import api, fields, models + + +class CalendarHolidaysPublicLine(models.Model): + _inherit = "calendar.public.holiday.line" + + city_ids = fields.Many2many( + "res.city", + "hr_holiday_public_city_rel", + "line_id", + "city_id", + "Related Cities", + ) + + @api.constrains("city_ids") + def _check_date_state_city_ids(self): + self._check_date_state() + + @api.constrains("city_ids") + def _update_calendar_event_city_ids(self): + self._update_calendar_event() + + def _get_domain_check_date_state_one_state_ids(self): + domain = super()._get_domain_check_date_state_one_state_ids() + if self.city_ids: + domain += [("city_ids", "!=", False)] + return domain + + def _get_domain_check_date_state_one(self): + domain = super()._get_domain_check_date_state_one() + domain += [("city_ids", "=", False)] + return domain + + def _prepare_holidays_meeting_values(self): + res = super()._prepare_holidays_meeting_values() + if self.city_ids: + res["description"] += ": " + ", ".join(self.city_ids.mapped("name")) + return res diff --git a/hr_holidays_public_city/models/hr_leave.py b/hr_holidays_public_city/models/hr_leave.py new file mode 100644 index 000000000..a5996ae48 --- /dev/null +++ b/hr_holidays_public_city/models/hr_leave.py @@ -0,0 +1,32 @@ +# Copyright 2023 Tecnativa - Víctor Martínez +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). +from odoo import models + + +class HrLeave(models.Model): + _inherit = "hr.leave" + + def _get_domain_from_get_unusual_days(self, date_from, date_to=None): + domain = super()._get_domain_from_get_unusual_days( + date_from=date_from, date_to=date_to + ) + # Use the employee of the user or the one who has the context + employee_id = self.env.context.get("employee_id", False) + employee = ( + self.env["hr.employee"].browse(employee_id) + if employee_id + else self.env.user.employee_id + ) + # Add city domain + city_id = employee.address_id.city_id.id + if not city_id: + city_id = self.env.company.partner_id.city_id.id or False + if city_id: + domain.extend( + [ + "|", + ("city_ids", "=", city_id), + ("city_ids", "=", False), + ] + ) + return domain diff --git a/hr_holidays_public_city/pyproject.toml b/hr_holidays_public_city/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/hr_holidays_public_city/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/hr_holidays_public_city/readme/CONTRIBUTORS.md b/hr_holidays_public_city/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..85a703f27 --- /dev/null +++ b/hr_holidays_public_city/readme/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +- [Tecnativa](https://www.tecnativa.com) + - Pedro M. Baeza + - Víctor Martínez +- [APSL](https://apsl.tech): + - Antoni Marroig \<\> +- [Niboo](https://www.niboo.com): + - Simon Falesse diff --git a/hr_holidays_public_city/readme/DESCRIPTION.md b/hr_holidays_public_city/readme/DESCRIPTION.md new file mode 100644 index 000000000..6f1fca4f1 --- /dev/null +++ b/hr_holidays_public_city/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module adds the cities at the public vacation line level as an +extra discriminant. diff --git a/hr_holidays_public_city/static/description/icon.png b/hr_holidays_public_city/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/hr_holidays_public_city/static/description/icon.png differ diff --git a/hr_holidays_public_city/static/description/icon.svg b/hr_holidays_public_city/static/description/icon.svg new file mode 100644 index 000000000..a7a26d093 --- /dev/null +++ b/hr_holidays_public_city/static/description/icon.svg @@ -0,0 +1,79 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/hr_holidays_public_city/static/description/index.html b/hr_holidays_public_city/static/description/index.html new file mode 100644 index 000000000..8e08e1950 --- /dev/null +++ b/hr_holidays_public_city/static/description/index.html @@ -0,0 +1,444 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

HR Holidays Public City

+ +

Beta License: AGPL-3 OCA/hr-holidays Translate me on Weblate Try me on Runboat

+

This module adds the cities at the public vacation line level as an +extra discriminant.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

victoralmau

+

This module is part of the OCA/hr-holidays project on GitHub.

+

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

+
+
+
+
+ + diff --git a/hr_holidays_public_city/tests/__init__.py b/hr_holidays_public_city/tests/__init__.py new file mode 100644 index 000000000..cd3e40a1e --- /dev/null +++ b/hr_holidays_public_city/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_holidays_calculation +from . import test_holidays_public diff --git a/hr_holidays_public_city/tests/test_holidays_calculation.py b/hr_holidays_public_city/tests/test_holidays_calculation.py new file mode 100644 index 000000000..e5ac7e5d6 --- /dev/null +++ b/hr_holidays_public_city/tests/test_holidays_calculation.py @@ -0,0 +1,77 @@ +# Copyright 2023-2026 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.hr_holidays_public.tests import test_holidays_calculation + + +class TestHolidaysComputeDays(test_holidays_calculation.TestHolidaysComputeDays): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.es_city_a = cls.env["res.city"].create( + { + "name": "Test city A", + "state_id": cls.env.ref("base.state_es_cr").id, + "country_id": cls.env.ref("base.es").id, + } + ) + cls.address_2.city_id = cls.es_city_a + cls.public_holiday_country.line_ids.city_ids = cls.address_2.city_id.ids + + # Run all tests of hr_holidays_public + + +class TestHolidaysComputeDaysExtra(test_holidays_calculation.TestHolidaysComputeDays): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.es_city_a = cls.env["res.city"].create( + { + "name": "Test city A", + "state_id": cls.env.ref("base.state_es_cr").id, + "country_id": cls.env.ref("base.es").id, + } + ) + cls.es_city_b = cls.env["res.city"].create( + { + "name": "Test city B", + "state_id": cls.env.ref("base.state_es_cr").id, + "country_id": cls.env.ref("base.es").id, + } + ) + cls.address_2.city_id = cls.es_city_b + cls.public_holiday_country.line_ids.city_ids = cls.es_city_a.ids + + def test_number_days_excluding_employee_2(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + self.assertEqual(leave_request.number_of_days, 4) + + def test_number_days_across_year_2(self): + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1947-01-03 23:59:59", # Friday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + self.assertEqual(leave_request.number_of_days, 7) + + def test_number_of_hours_excluding_employee_2(self): + self.holiday_type.request_unit = "hour" + leave_request = self.HrLeave.new( + { + "date_from": "1946-12-23 00:00:00", # Monday + "date_to": "1946-12-29 23:59:59", # Sunday + "holiday_status_id": self.holiday_type.id, + "employee_id": self.employee_2.id, + } + ) + self.assertEqual(leave_request.number_of_days, 4) diff --git a/hr_holidays_public_city/tests/test_holidays_public.py b/hr_holidays_public_city/tests/test_holidays_public.py new file mode 100644 index 000000000..eb356fb33 --- /dev/null +++ b/hr_holidays_public_city/tests/test_holidays_public.py @@ -0,0 +1,147 @@ +# Copyright 2023-2026 Tecnativa - Víctor Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import Command +from odoo.tests import new_test_user + +from odoo.addons.calendar_public_holiday.tests.test_calendar_public_holiday import ( + TestCalendarPublicHoliday, +) + + +class TestHolidaysPublic(TestCalendarPublicHoliday): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.employee_model = cls.env["hr.employee"] + cls.leave_model = cls.env["hr.leave"] + cls.st_state_1 = cls.env["res.country.state"].create( + {"name": "DE State 1", "code": "de", "country_id": cls.country_1.id} + ) + cls.st_state_2 = cls.env["res.country.state"].create( + {"name": "ST State 2", "code": "st", "country_id": cls.country_1.id} + ) + cls.user = new_test_user( + cls.env, + login="test-user_us_city_a", + ) + cls.user.action_create_employee() + cls.employee = cls.user.employee_id + cls.address = cls.env["res.partner"].create( + { + "name": "Test address", + } + ) + cls.employee.address_id = cls.address + cls.us_country = cls.env.ref("base.us") + cls.state_us_4 = cls.env.ref("base.state_us_4") + cls.us_city_a = cls.env["res.city"].create( + { + "name": "Test city A", + "state_id": cls.state_us_4.id, + "country_id": cls.us_country.id, + } + ) + cls.us_city_b = cls.env["res.city"].create( + { + "name": "Test city B", + "state_id": cls.state_us_4.id, + "country_id": cls.us_country.id, + } + ) + cls.holiday_model.create( + { + "year": 2019, + "country_id": cls.us_country.id, + "line_ids": [ + Command.create( + { + "name": "holiday city a + b", + "date": "2019-07-29", + "state_ids": [Command.set(cls.state_us_4.ids)], + "city_ids": [ + Command.set((cls.us_city_a + cls.us_city_b).ids) + ], + }, + ), + Command.create( + { + "name": "holiday city a", + "date": "2019-07-30", + "state_ids": [Command.set(cls.state_us_4.ids)], + "city_ids": [Command.set(cls.us_city_a.ids)], + } + ), + ], + } + ) + + def test_public_holidays_context(self): + self.address.country_id = False + self.address.state_id = False + self.address.city_id = False + # The employee has no address at all, so the country/state/city + # filters can only exclude the US-restricted holidays below if the + # company itself resolves to a different country (an unset company + # country means no country filter is applied at all). + self.user.company_id.partner_id.country_id = self.country_1 + unusual_days = self.leave_model.with_user(self.user.id).get_unusual_days( + "2019-07-01 00:00:00", date_to="2019-07-31 23:59:59" + ) + self.assertFalse(unusual_days["2019-07-29"]) + self.assertFalse(unusual_days["2019-07-30"]) + self.assertFalse(unusual_days["2019-07-31"]) + + def test_get_unusual_days_return_public_holidays_same_state_same_city(self): + self.address.country_id = self.us_country + self.address.state_id = self.state_us_4 + self.address.city_id = self.us_city_a + unusual_days = self.leave_model.with_user(self.user.id).get_unusual_days( + "2019-07-01 00:00:00", date_to="2019-07-31 23:59:59" + ) + self.assertTrue(unusual_days["2019-07-29"]) + self.assertTrue(unusual_days["2019-07-30"]) + self.assertFalse(unusual_days["2019-07-31"]) + + def test_get_unusual_days_return_public_holidays_same_state_differente_city(self): + self.address.country_id = self.us_country + self.address.state_id = self.state_us_4 + self.address.city_id = self.us_city_b + unusual_days = self.leave_model.with_user(self.user.id).get_unusual_days( + "2019-07-01 00:00:00", date_to="2019-07-31 23:59:59" + ) + self.assertTrue(unusual_days["2019-07-29"]) + self.assertFalse(unusual_days["2019-07-30"]) + self.assertFalse(unusual_days["2019-07-31"]) + + def test_get_unusual_days_return_public_holidays_fallback_to_company_state_city( + self, + ): + self.user.employee_id = False + self.user.company_id.partner_id.country_id = self.us_country + self.user.company_id.partner_id.state_id = self.state_us_4 + self.user.company_id.partner_id.city_id = self.us_city_a + unusual_days = self.leave_model.with_user(self.user.id).get_unusual_days( + "2019-07-01 00:00:00", date_to="2019-07-31 23:59:59" + ) + self.assertTrue(unusual_days["2019-07-29"]) + self.assertTrue(unusual_days["2019-07-30"]) + self.assertFalse(unusual_days["2019-07-31"]) + + def test_get_unusual_days_not_return_public_holidays_fallback_to_company_state_city( + self, + ): + self.address.country_id = self.us_country + self.address.city_id = False + # The employee has no state, so the state filter only excludes the + # state_us_4-restricted holidays below if the company resolves to a + # different state (an unset company state means no state filter is + # applied at all, letting the city match through unchecked). + self.user.company_id.partner_id.state_id = self.st_state_2 + self.user.company_id.partner_id.city_id = self.us_city_a + unusual_days = self.leave_model.with_user(self.user.id).get_unusual_days( + "2019-07-01 00:00:00", date_to="2019-07-31 23:59:59" + ) + self.assertFalse(unusual_days["2019-07-29"]) + self.assertFalse(unusual_days["2019-07-30"]) + self.assertFalse(unusual_days["2019-07-31"]) diff --git a/hr_holidays_public_city/views/calendar_public_holiday_view.xml b/hr_holidays_public_city/views/calendar_public_holiday_view.xml new file mode 100644 index 000000000..564ef7a37 --- /dev/null +++ b/hr_holidays_public_city/views/calendar_public_holiday_view.xml @@ -0,0 +1,20 @@ + + + + calendar.public.holiday.form + calendar.public.holiday + + + + + + + +