diff --git a/calendar_public_holiday/README.rst b/calendar_public_holiday/README.rst new file mode 100644 index 00000000..12dc8d26 --- /dev/null +++ b/calendar_public_holiday/README.rst @@ -0,0 +1,96 @@ +======================== +Calendar Holidays Public +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:323fe101af3ba04c9322fb644c273896c1fb66484ed662a3ca0984279d457570 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-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%2Fcalendar-lightgray.png?logo=github + :target: https://github.com/OCA/calendar/tree/18.0/calendar_public_holiday + :alt: OCA/calendar +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/calendar-18-0/calendar-18-0-calendar_public_holiday + :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/calendar&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module handles public holidays. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +For adding public holidays: + +1. Go to the menu *Calendar > Public Holidays > Public Holidays*. +2. Create your public holidays. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues <https://github.com/OCA/calendar/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 <https://github.com/OCA/calendar/issues/new?body=module:%20calendar_public_holiday%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Michael Telahun Makonnen +* Tecnativa +* Fekete Mihai (Forest and Biomass Services Romania) +* Druidoo +* Camptocamp +* + +Contributors +------------ + +- [Trobz](https://trobz.com): + + - Do Anh Duy <<duyda@trobz.com>> + +Other credits +------------- + +The creation of this module was financially supported by Camptocamp + +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. + +This module is part of the `OCA/calendar <https://github.com/OCA/calendar/tree/18.0/calendar_public_holiday>`_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/calendar_public_holiday/__init__.py b/calendar_public_holiday/__init__.py new file mode 100644 index 00000000..e68a3e61 --- /dev/null +++ b/calendar_public_holiday/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizards +from .hooks import pre_init_hook diff --git a/calendar_public_holiday/__manifest__.py b/calendar_public_holiday/__manifest__.py new file mode 100644 index 00000000..635462bf --- /dev/null +++ b/calendar_public_holiday/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright 2015 2011,2013 Michael Telahun Makonnen <mmakonnen@gmail.com> +# Copyright 2020 InitOS Gmbh +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Calendar Holidays Public", + "summary": """ + Manage Public Holidays + """, + "version": "18.0.1.0.0", + "license": "AGPL-3", + "category": "HR/Calendar", + "author": "Michael Telahun Makonnen, " + "Tecnativa, " + "Fekete Mihai (Forest and Biomass Services Romania), " + "Druidoo, " + "Odoo Community Association (OCA), " + "Camptocamp,", + "website": "https://github.com/OCA/calendar", + "depends": ["calendar"], + "external_dependencies": {"python": ["openupgradelib"]}, + "data": [ + "data/data.xml", + "security/ir.model.access.csv", + "views/calendar_public_holiday_view.xml", + "wizards/calendar_public_holiday_next_year_wizard.xml", + ], + "pre_init_hook": "pre_init_hook", +} diff --git a/calendar_public_holiday/data/data.xml b/calendar_public_holiday/data/data.xml new file mode 100644 index 00000000..74bcb333 --- /dev/null +++ b/calendar_public_holiday/data/data.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!--Copyright 2024 Camptocamp + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).--> +<odoo> + <record id="event_type_holiday" model="calendar.event.type"> + <field name="name">Public Holidays</field> + </record> +</odoo> diff --git a/calendar_public_holiday/hooks.py b/calendar_public_holiday/hooks.py new file mode 100644 index 00000000..72f026ed --- /dev/null +++ b/calendar_public_holiday/hooks.py @@ -0,0 +1,55 @@ +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openupgradelib import openupgrade + + +def migrate_rename_xmlid_event_type_holiday(env): + if not openupgrade.is_module_installed(env.cr, "hr_holidays_public"): + return + xmlid_renames = [ + ( + "hr_holidays_public.event_type_holiday", + "calendar_public_holiday.event_type_holiday", + ), + ] + openupgrade.rename_xmlids(env.cr, xmlid_renames) + + +def migrate_rename_field_model_hr_holidays_public_line(env): + field_renames = [ + ( + "hr.holidays.public.line", + "hr_holidays_public_line", + "year_id", + "public_holiday_id", + ), + ] + openupgrade.rename_fields(env, field_renames, no_deep=True) + + +def migrate_rename_model_hr_holidays_public_line(env): + if not openupgrade.table_exists(env.cr, "hr_holidays_public_line"): + return + model_renames = [("hr.holidays.public.line", "calendar.public.holiday.line")] + openupgrade.rename_models(env.cr, model_renames) + tables_renames = [("hr_holidays_public_line", "calendar_public_holiday_line")] + openupgrade.rename_tables(env.cr, tables_renames) + + +def migrate_rename_model_hr_holidays_public(env): + if not openupgrade.table_exists(env.cr, "hr_holidays_public"): + return + model_renames = [ + ("hr.holidays.public", "calendar.public.holiday"), + ] + openupgrade.rename_models(env.cr, model_renames) + tables_renames = [("hr_holidays_public", "calendar_public_holiday")] + openupgrade.rename_tables(env.cr, tables_renames) + + +def pre_init_hook(env): + migrate_rename_xmlid_event_type_holiday(env) + migrate_rename_field_model_hr_holidays_public_line(env) + migrate_rename_model_hr_holidays_public_line(env) + migrate_rename_model_hr_holidays_public(env) diff --git a/calendar_public_holiday/models/__init__.py b/calendar_public_holiday/models/__init__.py new file mode 100644 index 00000000..a3e864da --- /dev/null +++ b/calendar_public_holiday/models/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import calendar_public_holiday +from . import calendar_public_holiday_line diff --git a/calendar_public_holiday/models/calendar_public_holiday.py b/calendar_public_holiday/models/calendar_public_holiday.py new file mode 100644 index 00000000..3f2c8b51 --- /dev/null +++ b/calendar_public_holiday/models/calendar_public_holiday.py @@ -0,0 +1,128 @@ +# Copyright 2015 2011,2013 Michael Telahun Makonnen <mmakonnen@gmail.com> +# Copyright 2020 InitOS Gmbh +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import datetime + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class ResourceCalendarPublicHoliday(models.Model): + _name = "calendar.public.holiday" + _description = "Calendar Public Holiday" + _rec_name = "year" + _order = "year desc" + + year = fields.Integer( + "Calendar Year", + required=True, + default=lambda self: fields.Date.context_today(self).year, + ) + line_ids = fields.One2many( + "calendar.public.holiday.line", + "public_holiday_id", + "Holiday Dates", + ) + country_id = fields.Many2one("res.country", "Country") + + @api.constrains("year", "country_id") + def _check_year(self): + for line in self: + line._check_year_one() + + def _check_year_one(self): + if self.search_count( + [ + ("year", "=", self.year), + ("country_id", "=", self.country_id.id), + ("id", "!=", self.id), + ] + ): + raise ValidationError( + self.env._( + "You can't create duplicate public holiday per year and/or" + " country" + ) + ) + return True + + @api.depends("country_id") + def _compute_display_name(self): + for line in self: + if line.country_id: + line.display_name = f"{line.year} ({line.country_id.name})" + else: + line.display_name = line.year + + def _get_domain_states_filter(self, pholidays, start_dt, end_dt, partner_id=None): + partner = self.env["res.partner"].browse(partner_id) + states_filter = [ + ("public_holiday_id", "in", pholidays.ids), + ("date", ">=", start_dt), + ("date", "<=", end_dt), + ] + if partner and partner.state_id: + states_filter.extend( + [ + "|", + ("state_ids", "in", partner.state_id.ids), + ("state_ids", "=", False), + ] + ) + else: + states_filter.append(("state_ids", "=", False)) + return states_filter + + @api.model + @api.returns("calendar.public.holiday.line") + def get_holidays_list(self, year=None, start_dt=None, end_dt=None, partner_id=None): + """Returns recordset of calendar.public.holiday.line + for the specified year and employee + :param year: year as string (optional if start_dt and end_dt defined) + :param start_dt: start_dt as date + :param end_dt: end_dt as date + :param partner_id: ID of the partner + :return: recordset of calendar.public.holiday.line + """ + partner = self.env["res.partner"].browse(partner_id) + if not start_dt and not end_dt: + start_dt = datetime.date(year, 1, 1) + end_dt = datetime.date(year, 12, 31) + years = list(range(start_dt.year, end_dt.year + 1)) + holidays_filter = [("year", "in", years)] + if partner: + if partner.country_id: + holidays_filter.append( + ("country_id", "in", (False, partner.country_id.id)) + ) + else: + holidays_filter.append(("country_id", "=", False)) + public_holidays = self.search(holidays_filter) + public_holiday_line = self.env["calendar.public.holiday.line"] + if not public_holidays: + return public_holiday_line + states_filter = self._get_domain_states_filter( + public_holidays, start_dt, end_dt, partner_id=partner.id + ) + return public_holiday_line.search(states_filter) + + @api.model + def is_public_holiday(self, selected_date, partner_id=None): + """ + Returns True if selected_date is a public holiday for the employee + :param selected_date: datetime object + :param partner_id: ID of the partner + :return: bool + """ + partner = self.env["res.partner"].browse(partner_id) + partner_id = partner.id if partner else None + holidays_lines = self.get_holidays_list( + year=selected_date.year, partner_id=partner_id + ) + if holidays_lines: + hol_date = holidays_lines.filtered(lambda r: r.date == selected_date) + if hol_date: + return True + return False diff --git a/calendar_public_holiday/models/calendar_public_holiday_line.py b/calendar_public_holiday/models/calendar_public_holiday_line.py new file mode 100644 index 00000000..a207f14e --- /dev/null +++ b/calendar_public_holiday/models/calendar_public_holiday_line.py @@ -0,0 +1,123 @@ +# Copyright 2015 2011,2013 Michael Telahun Makonnen <mmakonnen@gmail.com> +# Copyright 2020 InitOS Gmbh +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import SUPERUSER_ID, api, fields, models +from odoo.exceptions import ValidationError + + +class CalendarHolidaysPublicLine(models.Model): + _name = "calendar.public.holiday.line" + _description = "Calendar Public Holiday Line" + _order = "date, name desc" + + name = fields.Char(required=True) + date = fields.Date(required=True) + public_holiday_id = fields.Many2one( + "calendar.public.holiday", + "Calendar Year", + required=True, + ondelete="cascade", + ) + variable_date = fields.Boolean("Date may change", default=True) + state_ids = fields.Many2many( + "res.country.state", + "public_holiday_state_rel", + "public_holiday_line_id", + "state_id", + "Related States", + ) + meeting_id = fields.Many2one( + "calendar.event", + string="Meeting", + copy=False, + ) + + @api.constrains("date", "state_ids") + def _check_date_state(self): + for line in self: + line._check_date_state_one() + + def _get_domain_check_date_state_one_state_ids(self): + return [ + ("date", "=", self.date), + ("public_holiday_id", "=", self.public_holiday_id.id), + ("state_ids", "!=", False), + ("id", "!=", self.id), + ] + + def _get_domain_check_date_state_one(self): + return [ + ("date", "=", self.date), + ("public_holiday_id", "=", self.public_holiday_id.id), + ("state_ids", "=", False), + ] + + def _check_date_state_one(self): + if self.date.year != self.public_holiday_id.year: + raise ValidationError( + self.env._( + "Dates of holidays should be the same year as the calendar" + " year they are being assigned to" + ) + ) + if self.state_ids: + domain = self._get_domain_check_date_state_one_state_ids() + holidays = self.search(domain) + for holiday in holidays: + if self.state_ids & holiday.state_ids: + raise ValidationError( + self.env._( + "You can't create duplicate public holiday per date" + f" {self.date} and one of the country states." + ) + ) + domain = self._get_domain_check_date_state_one() + if self.search_count(domain) > 1: + raise ValidationError( + self.env._( + f"You can't create duplicate public holiday per date {self.date}." + ) + ) + return True + + def _prepare_holidays_meeting_values(self): + self.ensure_one() + categ_id = self.env.ref("calendar_public_holiday.event_type_holiday", False) + meeting_values = { + "name": ( + f"{self.name} ({self.public_holiday_id.country_id.name})" + if self.public_holiday_id.country_id + else self.name + ), + "description": ", ".join(self.state_ids.mapped("name")), + "start": self.date, + "stop": self.date, + "allday": True, + "user_id": SUPERUSER_ID, + "privacy": "confidential", + "show_as": "busy", + } + if categ_id: + meeting_values.update({"categ_ids": [(6, 0, categ_id.ids)]}) + return meeting_values + + @api.constrains("date", "name", "public_holiday_id", "state_ids") + def _update_calendar_event(self): + for rec in self: + if rec.meeting_id: + rec.meeting_id.write(rec._prepare_holidays_meeting_values()) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for record in res: + record.meeting_id = self.env["calendar.event"].create( + record._prepare_holidays_meeting_values() + ) + return res + + def unlink(self): + self.mapped("meeting_id").unlink() + return super().unlink() diff --git a/calendar_public_holiday/pyproject.toml b/calendar_public_holiday/pyproject.toml new file mode 100644 index 00000000..4231d0cc --- /dev/null +++ b/calendar_public_holiday/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/calendar_public_holiday/readme/CONTRIBUTORS.md b/calendar_public_holiday/readme/CONTRIBUTORS.md new file mode 100644 index 00000000..d9381e9b --- /dev/null +++ b/calendar_public_holiday/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- \[Trobz\](<https://trobz.com>): + + - Do Anh Duy \<\<<duyda@trobz.com>\>\> diff --git a/calendar_public_holiday/readme/CREDITS.md b/calendar_public_holiday/readme/CREDITS.md new file mode 100644 index 00000000..35e239a0 --- /dev/null +++ b/calendar_public_holiday/readme/CREDITS.md @@ -0,0 +1 @@ +The creation of this module was financially supported by Camptocamp diff --git a/calendar_public_holiday/readme/DESCRIPTION.md b/calendar_public_holiday/readme/DESCRIPTION.md new file mode 100644 index 00000000..2a7afcc9 --- /dev/null +++ b/calendar_public_holiday/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module handles public holidays. diff --git a/calendar_public_holiday/readme/USAGE.md b/calendar_public_holiday/readme/USAGE.md new file mode 100644 index 00000000..185826a2 --- /dev/null +++ b/calendar_public_holiday/readme/USAGE.md @@ -0,0 +1,4 @@ +For adding public holidays: + +1. Go to the menu *Calendar \> Public Holidays \> Public Holidays*. +2. Create your public holidays. diff --git a/calendar_public_holiday/security/ir.model.access.csv b/calendar_public_holiday/security/ir.model.access.csv new file mode 100644 index 00000000..26d7f805 --- /dev/null +++ b/calendar_public_holiday/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_calendar_public_holiday_user,access_calendar_public_holiday,model_calendar_public_holiday,base.group_user,1,0,0,0 +access_calendar_public_holiday_manager,access_calendar_public_holiday,model_calendar_public_holiday,base.group_system,1,1,1,1 +access_calendar_public_holiday_line_user,access_calendar_public_holiday_line,model_calendar_public_holiday_line,base.group_user,1,0,0,0 +access_calendar_public_holiday_line_manager,access_calendar_public_holiday_line,model_calendar_public_holiday_line,base.group_system,1,1,1,1 +access_calendar_public_holiday_manager_next_year,access_calendar_public_holiday_next_year,model_calendar_public_holiday_next_year,base.group_system,1,1,1,1 diff --git a/calendar_public_holiday/static/description/icon.png b/calendar_public_holiday/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/calendar_public_holiday/static/description/icon.png differ diff --git a/calendar_public_holiday/static/description/index.html b/calendar_public_holiday/static/description/index.html new file mode 100644 index 00000000..ceb354a7 --- /dev/null +++ b/calendar_public_holiday/static/description/index.html @@ -0,0 +1,445 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> +<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" /> +<title>Calendar Holidays Public</title> +<style type="text/css"> + +/* +:Author: David Goodger (goodger@python.org) +:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Copyright: This stylesheet has been placed in the public domain. + +Default cascading style sheet for the HTML output of Docutils. +Despite the name, some widely supported CSS2 features are used. + +See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to +customize this style sheet. +*/ + +/* used to remove borders from tables and images */ +.borderless, table.borderless td, table.borderless th { + border: 0 } + +table.borderless td, table.borderless th { + /* Override padding for "table.docutils td" with "! important". + The right padding separates the table cells. */ + padding: 0 0.5em 0 0 ! important } + +.first { + /* Override more specific margin styles with "! important". */ + margin-top: 0 ! important } + +.last, .with-subtitle { + margin-bottom: 0 ! important } + +.hidden { + display: none } + +.subscript { + vertical-align: sub; + font-size: smaller } + +.superscript { + vertical-align: super; + font-size: smaller } + +a.toc-backref { + text-decoration: none ; + color: black } + +blockquote.epigraph { + margin: 2em 5em ; } + +dl.docutils dd { + margin-bottom: 0.5em } + +object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { + overflow: hidden; +} + +/* Uncomment (and remove this text!) to get bold-faced definition list terms +dl.docutils dt { + font-weight: bold } +*/ + +div.abstract { + margin: 2em 5em } + +div.abstract p.topic-title { + font-weight: bold ; + text-align: center } + +div.admonition, div.attention, div.caution, div.danger, div.error, +div.hint, div.important, div.note, div.tip, div.warning { + margin: 2em ; + border: medium outset ; + padding: 1em } + +div.admonition p.admonition-title, div.hint p.admonition-title, +div.important p.admonition-title, div.note p.admonition-title, +div.tip p.admonition-title { + font-weight: bold ; + font-family: sans-serif } + +div.attention p.admonition-title, div.caution p.admonition-title, +div.danger p.admonition-title, div.error p.admonition-title, +div.warning p.admonition-title, .code .error { + color: red ; + font-weight: bold ; + font-family: sans-serif } + +/* Uncomment (and remove this text!) to get reduced vertical space in + compound paragraphs. +div.compound .compound-first, div.compound .compound-middle { + margin-bottom: 0.5em } + +div.compound .compound-last, div.compound .compound-middle { + margin-top: 0.5em } +*/ + +div.dedication { + margin: 2em 5em ; + text-align: center ; + font-style: italic } + +div.dedication p.topic-title { + font-weight: bold ; + font-style: normal } + +div.figure { + margin-left: 2em ; + margin-right: 2em } + +div.footer, div.header { + clear: both; + font-size: smaller } + +div.line-block { + display: block ; + margin-top: 1em ; + margin-bottom: 1em } + +div.line-block div.line-block { + margin-top: 0 ; + margin-bottom: 0 ; + margin-left: 1.5em } + +div.sidebar { + margin: 0 0 0.5em 1em ; + border: medium outset ; + padding: 1em ; + background-color: #ffffee ; + width: 40% ; + float: right ; + clear: right } + +div.sidebar p.rubric { + font-family: sans-serif ; + font-size: medium } + +div.system-messages { + margin: 5em } + +div.system-messages h1 { + color: red } + +div.system-message { + border: medium outset ; + padding: 1em } + +div.system-message p.system-message-title { + color: red ; + font-weight: bold } + +div.topic { + margin: 2em } + +h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, +h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { + margin-top: 0.4em } + +h1.title { + text-align: center } + +h2.subtitle { + text-align: center } + +hr.docutils { + width: 75% } + +img.align-left, .figure.align-left, object.align-left, table.align-left { + clear: left ; + float: left ; + margin-right: 1em } + +img.align-right, .figure.align-right, object.align-right, table.align-right { + clear: right ; + float: right ; + margin-left: 1em } + +img.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left } + +.align-center { + clear: both ; + text-align: center } + +.align-right { + text-align: right } + +/* reset inner alignment in figures */ +div.align-right { + text-align: inherit } + +/* div.align-center * { */ +/* text-align: left } */ + +.align-top { + vertical-align: top } + +.align-middle { + vertical-align: middle } + +.align-bottom { + vertical-align: bottom } + +ol.simple, ul.simple { + margin-bottom: 1em } + +ol.arabic { + list-style: decimal } + +ol.loweralpha { + list-style: lower-alpha } + +ol.upperalpha { + list-style: upper-alpha } + +ol.lowerroman { + list-style: lower-roman } + +ol.upperroman { + list-style: upper-roman } + +p.attribution { + text-align: right ; + margin-left: 50% } + +p.caption { + font-style: italic } + +p.credits { + font-style: italic ; + font-size: smaller } + +p.label { + white-space: nowrap } + +p.rubric { + font-weight: bold ; + font-size: larger ; + color: maroon ; + text-align: center } + +p.sidebar-title { + font-family: sans-serif ; + font-weight: bold ; + font-size: larger } + +p.sidebar-subtitle { + font-family: sans-serif ; + font-weight: bold } + +p.topic-title { + font-weight: bold } + +pre.address { + margin-bottom: 0 ; + margin-top: 0 ; + font: inherit } + +pre.literal-block, pre.doctest-block, pre.math, pre.code { + margin-left: 2em ; + margin-right: 2em } + +pre.code .ln { color: gray; } /* line numbers */ +pre.code, code { background-color: #eeeeee } +pre.code .comment, code .comment { color: #5C6576 } +pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } +pre.code .literal.string, code .literal.string { color: #0C5404 } +pre.code .name.builtin, code .name.builtin { color: #352B84 } +pre.code .deleted, code .deleted { background-color: #DEB0A1} +pre.code .inserted, code .inserted { background-color: #A3D289} + +span.classifier { + font-family: sans-serif ; + font-style: oblique } + +span.classifier-delimiter { + font-family: sans-serif ; + font-weight: bold } + +span.interpreted { + font-family: sans-serif } + +span.option { + white-space: nowrap } + +span.pre { + white-space: pre } + +span.problematic, pre.problematic { + color: red } + +span.section-subtitle { + /* font-size relative to parent (h1..h6 element) */ + font-size: 80% } + +table.citation { + border-left: solid 1px gray; + margin-left: 1px } + +table.docinfo { + margin: 2em 4em } + +table.docutils { + margin-top: 0.5em ; + margin-bottom: 0.5em } + +table.footnote { + border-left: solid 1px black; + margin-left: 1px } + +table.docutils td, table.docutils th, +table.docinfo td, table.docinfo th { + padding-left: 0.5em ; + padding-right: 0.5em ; + vertical-align: top } + +table.docutils th.field-name, table.docinfo th.docinfo-name { + font-weight: bold ; + text-align: left ; + white-space: nowrap ; + padding-left: 0 } + +/* "booktabs" style (no vertical lines) */ +table.docutils.booktabs { + border: 0px; + border-top: 2px solid; + border-bottom: 2px solid; + border-collapse: collapse; +} +table.docutils.booktabs * { + border: 0px; +} +table.docutils.booktabs th { + border-bottom: thin solid; + text-align: left; +} + +h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, +h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { + font-size: 100% } + +ul.auto-toc { + list-style-type: none } + +</style> +</head> +<body> +<div class="document" id="calendar-holidays-public"> +<h1 class="title">Calendar Holidays Public</h1> + +<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! This file is generated by oca-gen-addon-readme !! +!! changes will be overwritten. !! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +!! source digest: sha256:323fe101af3ba04c9322fb644c273896c1fb66484ed662a3ca0984279d457570 +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> +<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/calendar/tree/18.0/calendar_public_holiday"><img alt="OCA/calendar" src="https://img.shields.io/badge/github-OCA%2Fcalendar-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/calendar-18-0/calendar-18-0-calendar_public_holiday"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/calendar&target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p> +<p>This module handles public holidays.</p> +<p><strong>Table of contents</strong></p> +<div class="contents local topic" id="contents"> +<ul class="simple"> +<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li> +<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li> +<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul> +<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li> +<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li> +<li><a class="reference internal" href="#other-credits" id="toc-entry-6">Other credits</a></li> +<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li> +</ul> +</li> +</ul> +</div> +<div class="section" id="usage"> +<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1> +<p>For adding public holidays:</p> +<ol class="arabic simple"> +<li>Go to the menu <em>Calendar > Public Holidays > Public Holidays</em>.</li> +<li>Create your public holidays.</li> +</ol> +</div> +<div class="section" id="bug-tracker"> +<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1> +<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/calendar/issues">GitHub Issues</a>. +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 +<a class="reference external" href="https://github.com/OCA/calendar/issues/new?body=module:%20calendar_public_holiday%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p> +<p>Do not contact contributors directly about support or help with technical issues.</p> +</div> +<div class="section" id="credits"> +<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1> +<div class="section" id="authors"> +<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2> +<ul class="simple"> +<li>Michael Telahun Makonnen</li> +<li>Tecnativa</li> +<li>Fekete Mihai (Forest and Biomass Services Romania)</li> +<li>Druidoo</li> +<li>Camptocamp</li> +<li></li> +</ul> +</div> +<div class="section" id="contributors"> +<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2> +<ul class="simple"> +<li>[Trobz](<a class="reference external" href="https://trobz.com">https://trobz.com</a>):<ul> +<li>Do Anh Duy <<<a class="reference external" href="mailto:duyda@trobz.com">duyda@trobz.com</a>>></li> +</ul> +</li> +</ul> +</div> +<div class="section" id="other-credits"> +<h2><a class="toc-backref" href="#toc-entry-6">Other credits</a></h2> +<p>The creation of this module was financially supported by Camptocamp</p> +</div> +<div class="section" id="maintainers"> +<h2><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h2> +<p>This module is maintained by the OCA.</p> +<a class="reference external image-reference" href="https://odoo-community.org"> +<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /> +</a> +<p>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.</p> +<p>This module is part of the <a class="reference external" href="https://github.com/OCA/calendar/tree/18.0/calendar_public_holiday">OCA/calendar</a> project on GitHub.</p> +<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p> +</div> +</div> +</div> +</body> +</html> diff --git a/calendar_public_holiday/tests/__init__.py b/calendar_public_holiday/tests/__init__.py new file mode 100644 index 00000000..e052af02 --- /dev/null +++ b/calendar_public_holiday/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_calendar_public_holiday diff --git a/calendar_public_holiday/tests/test_calendar_public_holiday.py b/calendar_public_holiday/tests/test_calendar_public_holiday.py new file mode 100644 index 00000000..0bd83ffb --- /dev/null +++ b/calendar_public_holiday/tests/test_calendar_public_holiday.py @@ -0,0 +1,249 @@ +# Copyright 2015 iDT LABS (http://www.@idtlabs.sl) +# Copyright 2017-2018 Tecnativa - Pedro M. Baeza +# Copyright 2018 Brainbean Apps +# Copyright 2020 InitOS Gmbh +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from datetime import date + +from odoo.exceptions import UserError, ValidationError + +from odoo.addons.base.tests.common import BaseCommon + + +class TestCalendarPublicHoliday(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.holiday_model = cls.env["calendar.public.holiday"] + cls.holiday_line_model = cls.env["calendar.public.holiday.line"] + cls.calendar_event = cls.env["calendar.event"] + cls.wizard_next_year = cls.env["calendar.public.holiday.next.year"] + + # Remove possibly existing public holidays that would interfer. + cls.holiday_line_model.search([]).unlink() + cls.holiday_model.search([]).unlink() + cls.calendar_event.search([]).unlink() + + cls.country_1 = cls.env["res.country"].create( + { + "name": "Country 1", + "code": "XX", + } + ) + cls.country_2 = cls.env["res.country"].create( + { + "name": "Country 2", + "code": "YY", + } + ) + cls.country_3 = cls.env["res.country"].create( + { + "name": "Country 3", + "code": "ZZ", + } + ) + cls.res_partner = cls.env["res.partner"].create( + {"name": "Partner 1", "country_id": cls.country_1.id} + ) + cls.holiday_1 = cls.holiday_model.create( + { + "year": 2024, + "country_id": cls.country_1.id, + "line_ids": [ + ( + 0, + 0, + { + "name": "Christmas Day for Country 1", + "date": "2024-12-25", + }, + ) + ], + } + ) + cls.holiday_2 = cls.holiday_model.create( + { + "year": 2024, + "country_id": cls.country_2.id, + "line_ids": [ + ( + 0, + 0, + { + "name": "Christmas Day for Country 2", + "date": "2024-12-25", + }, + ) + ], + } + ) + cls.holiday_3 = cls.holiday_model.create({"year": 2025}) + ls_dates = ["2025-01-02", "2025-01-05", "2025-01-07"] + for i in range(len(ls_dates)): + cls.holiday_line_model.create( + { + "name": f"Public Holiday Line {i + 1}", + "date": ls_dates[i], + "public_holiday_id": cls.holiday_3.id, + } + ) + + def test_display_name(self): + holiday_1_display_name = self.holiday_1.display_name + expect_display_name = ( + f"{self.holiday_1.year} ({self.holiday_1.country_id.name})" + ) + self.assertEqual(holiday_1_display_name, expect_display_name) + + # without country + holiday_3_display_name = self.holiday_3.display_name + expect_display_name = f"{self.holiday_3.year}" + self.assertEqual(holiday_3_display_name, expect_display_name) + + def test_duplicate_year_country_fail(self): + # ensures that duplicate year cannot be created for the same country + with self.assertRaises(ValidationError): + # same year with country = False + self.holiday_model.create({"year": 2025}) + with self.assertRaises(ValidationError): + # same country with holiday_1 + self.holiday_model.create({"year": 2024, "country_id": self.country_1.id}) + + def test_duplicate_date_state_fail(self): + # ensures that duplicate date cannot be created for the same country + # state or with state null + holiday_4 = self.holiday_model.create( + {"year": 2024, "country_id": self.country_3.id} + ) + holiday_4_line = self.holiday_line_model.create( + { + "name": "holiday x", + "date": "2024-12-25", + "public_holiday_id": holiday_4.id, + } + ) + with self.assertRaises(ValidationError): + self.holiday_line_model.create( + { + "name": "holiday x", + "date": "2024-12-25", + "public_holiday_id": holiday_4.id, + } + ) + holiday_4_line.state_ids = [(6, 0, [self.country_3.id])] + with self.assertRaises(ValidationError): + self.holiday_line_model.create( + { + "name": "holiday x", + "date": "2024-12-25", + "public_holiday_id": holiday_4.id, + "state_ids": [(6, 0, [self.country_3.id])], + } + ) + + def test_holiday_in_country(self): + # ensures that correct holidays are identified for a country + self.assertTrue( + self.holiday_model.is_public_holiday( + date(2024, 12, 25), partner_id=self.res_partner.id + ) + ) + self.assertFalse( + self.holiday_model.is_public_holiday( + date(2024, 12, 23), partner_id=self.res_partner.id + ) + ) + + def test_holiday_line_same_year_with_parent(self): + # ensures that line year and holiday year are the same + with self.assertRaises(ValidationError): + self.holiday_model.create( + { + "year": 2026, + "line_ids": [ + ( + 0, + 0, + { + "name": "Line with not the same year", + "date": "2027-12-25", + }, + ) + ], + } + ) + + def test_list_holidays_in_list_country_specific(self): + # ensures that correct holidays are identified for a country + lines = self.holiday_model.get_holidays_list( + 2024, partner_id=self.res_partner.id + ) + res = lines.filtered(lambda r: r.date == date(2024, 12, 25)) + self.assertEqual(len(res), 1) + self.assertEqual(len(lines), 1) + + def test_list_holidays_in_list(self): + # ensures that correct holidays are identified for a country + lines = self.holiday_model.get_holidays_list(2025) + res = lines.filtered(lambda r: r.date == date(2025, 1, 2)) + self.assertEqual(len(res), 1) + self.assertEqual(len(lines), 3) + + def test_create_year_2026_public_holidays(self): + # holiday_1 and holiday_2 have the same line in 2024 but different country + ph_start_ids = self.holiday_model.search([("year", "=", 2024)]) + vals = {"public_holiday_ids": ph_start_ids, "year": 2026} + wizard = self.wizard_next_year.new(values=vals) + wizard.create_public_holidays() + lines = self.holiday_model.get_holidays_list(2026) + self.assertEqual(len(lines), 2) + res = lines.filtered( + lambda r: r.public_holiday_id.country_id.id == self.country_1.id + ) + self.assertEqual(len(res), 1) + + def test_create_year_2027_public_holidays(self): + # holiday_3 have 3 line in year 2025 + ph_start_ids = self.holiday_model.search([("year", "=", 2025)]) + wizard = self.wizard_next_year.new( + values={ + "public_holiday_ids": ph_start_ids, + "year": 2027, + } + ) + wizard.create_public_holidays() + lines = self.holiday_model.get_holidays_list(2027) + self.assertEqual(len(lines), 3) + + def test_february_29th(self): + # Ensures that users get a UserError (not a nasty Exception) when + # trying to create public holidays from year including 29th of + # February + holiday_tw_2024 = self.holiday_model.create( + {"year": 2024, "country_id": self.country_3.id} + ) + self.holiday_line_model.create( + { + "name": "Peace Memorial Holiday", + "date": "2024-02-29", + "public_holiday_id": holiday_tw_2024.id, + } + ) + vals = {"public_holiday_ids": holiday_tw_2024} + wz_create_ph = self.wizard_next_year.new(values=vals) + + with self.assertRaises(UserError): + wz_create_ph.create_public_holidays() + + def test_calendar_event_created(self): + holiday_1_line = self.holiday_1.line_ids[0] + meeting_id = holiday_1_line.meeting_id + self.assertTrue(meeting_id) + holiday_1_line.unlink() + self.assertFalse(meeting_id.exists()) + all_lines = self.holiday_line_model.search([]) + categ_id = self.env.ref("calendar_public_holiday.event_type_holiday", False) + all_meetings = self.calendar_event.search([("categ_ids", "in", categ_id.id)]) + self.assertEqual(len(all_lines), len(all_meetings)) diff --git a/calendar_public_holiday/views/calendar_public_holiday_view.xml b/calendar_public_holiday/views/calendar_public_holiday_view.xml new file mode 100644 index 00000000..b6186c50 --- /dev/null +++ b/calendar_public_holiday/views/calendar_public_holiday_view.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright 2024 Camptocamp + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> +<odoo> + <record id="view_calendar_public_holiday_list" model="ir.ui.view"> + <field name="name">calendar.public.holiday.list</field> + <field name="model">calendar.public.holiday</field> + <field name="arch" type="xml"> + <list> + <field name="display_name" /> + <field name="country_id" /> + </list> + </field> + </record> + <record id="view_calendar_public_holiday_form" model="ir.ui.view"> + <field name="name">calendar.public.holiday.form</field> + <field name="model">calendar.public.holiday</field> + <field name="arch" type="xml"> + <form> + <group name="group_main"> + <group name="group_main_left"> + <field name="year" options="{'type': 'number'}" /> + <field name="country_id" /> + </group> + <group name="group_main_right"> + <!-- Left empty for extensions --> + </group> + </group> + <separator string="Public Holidays" name="group_detail" /> + <field name="line_ids" nolabel="1"> + <list editable="top"> + <field + name="date" + readonly="not variable_date" + force_save="1" + /> + <field name="name" /> + <field + name="state_ids" + widget="many2many_tags" + domain="[('country_id', '=', parent.country_id)]" + /> + <field name="variable_date" /> + </list> + </field> + </form> + </field> + </record> + <record id="open_calendar_public_holiday_view" model="ir.actions.act_window"> + <field name="name">Public Holidays</field> + <field name="res_model">calendar.public.holiday</field> + <field name="view_mode">list,form</field> + </record> + <menuitem + id="menu_calendar_public_holiday" + name="Public Holidays" + parent="calendar.calendar_menu_config" + sequence="60" + /> + <menuitem + action="open_calendar_public_holiday_view" + id="menu_calendar_public_holiday_view" + parent="menu_calendar_public_holiday" + sequence="61" + /> +</odoo> diff --git a/calendar_public_holiday/wizards/__init__.py b/calendar_public_holiday/wizards/__init__.py new file mode 100644 index 00000000..5c75b1aa --- /dev/null +++ b/calendar_public_holiday/wizards/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import calendar_public_holiday_next_year_wizard diff --git a/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.py b/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.py new file mode 100644 index 00000000..0a95c45d --- /dev/null +++ b/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.py @@ -0,0 +1,82 @@ +# Copyright 2016 Trobz +# Copyright 2024 Camptocamp +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import logging + +from odoo import fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class CalendarPublicHolidayNextYear(models.TransientModel): + _name = "calendar.public.holiday.next.year" + _description = "Create Public Holiday From Existing Ones" + + public_holiday_ids = fields.Many2many( + comodel_name="calendar.public.holiday", + string="Templates", + help="Select the public holidays to use as template. " + "If not set, latest public holidays of each country will be used. " + "Only the last templates of each country for each year will " + "be taken into account (If you select templates from 2012 and 2015, " + "only the templates from 2015 will be taken into account.", + ) + year = fields.Integer( + help="Year for which you want to create the public holidays. " + "By default, the year following the template." + ) + + def create_public_holidays(self): + self.ensure_one() + last_ph_dict = {} + ph_env = self.env["calendar.public.holiday"] + pholidays = self.public_holiday_ids or ph_env.search([]) + if not pholidays: + raise UserError( + self.env._( + "No Public Holidays found as template. " + "Please create the first Public Holidays manually." + ) + ) + for ph in pholidays: + last_ph_country = last_ph_dict.get(ph.country_id, False) + if last_ph_country: + if last_ph_country.year < ph.year: + last_ph_dict[ph.country_id] = ph + else: + last_ph_dict[ph.country_id] = ph + new_ph_ids = [] + for last_ph in last_ph_dict.values(): + new_year = self.year or last_ph.year + 1 + new_ph_vals = {"year": new_year} + new_ph = last_ph.copy(new_ph_vals) + new_ph_ids.append(new_ph.id) + for last_ph_line in last_ph.line_ids: + feb_29 = last_ph_line.date.month == 2 and last_ph_line.date.day == 29 + if feb_29: + # Handling this rare case would mean quite a lot of + # complexity because previous or next day might also be a + # public holiday. + raise UserError( + self.env._( + "You cannot use as template the public holidays " + "of a year that " + "includes public holidays on 29th of February " + "(2016, 2020...), please select a template from " + "another year." + ) + ) + new_date = last_ph_line.date.replace(year=new_year) + new_ph_line_vals = {"date": new_date, "public_holiday_id": new_ph.id} + last_ph_line.copy(new_ph_line_vals) + domain = [["id", "in", new_ph_ids]] + action = { + "type": "ir.actions.act_window", + "name": self.env._("New public holidays"), + "view_mode": "list,form", + "res_model": ph_env._name, + "domain": domain, + } + return action diff --git a/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.xml b/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.xml new file mode 100644 index 00000000..6c11ebed --- /dev/null +++ b/calendar_public_holiday/wizards/calendar_public_holiday_next_year_wizard.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="utf-8" ?> +<!-- Copyright 2024 Camptocamp + License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> +<odoo> + <record id="calendar_public_holiday_next_year_view" model="ir.ui.view"> + <field name="name">Create Next Year Public Holidays</field> + <field name="model">calendar.public.holiday.next.year</field> + <field name="arch" type="xml"> + <form string="Create Next Year Public Holidays"> + <sheet> + <div> + Use this wizard to create public holidays based on the + existing ones.<br /> + Only the last templates of each country + will be taken into account (If you select templates + from 2012 and 2015 of the same country; ' + only the templates from 2015 will be taken into + account). + </div> + <notebook> + <page name="defaults" string="Defaults"> + <div> + By default, the most recent public holidays + for each country are used as template to create + public holidays for the year following the templates. + <br /><br /> + Normally, you should not need to input anything in + optional fields and only need to click on the button + "Create". + </div> + </page> + <page name="optional" string="Optional"> + <div> + The below optional fields are here only to handle + special situations like "2011 was a special year with + an additional public holiday for the 150th + anniversary of the Italian unification, so you want to + replicate the 2010 Italian holidays to 2012." + </div> + <group> + <field name="public_holiday_ids" /> + <field name="year" options="{'type': 'number'}" /> + </group> + </page> + </notebook> + </sheet> + <footer> + <button + name="create_public_holidays" + string="Create" + type="object" + class="btn-primary" + /> + <button string="Cancel" class="btn-default" special="cancel" /> + </footer> + </form> + </field> + </record> + <record id="action_create_next_year_public_holidays" model="ir.actions.act_window"> + <field name="name">Create Next Year Public Holidays</field> + <field name="res_model">calendar.public.holiday.next.year</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + <menuitem + action="action_create_next_year_public_holidays" + id="menu_create_next_year_public_holidays" + parent="menu_calendar_public_holiday" + sequence="71" + /> +</odoo> diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..180fc497 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +openupgradelib