diff --git a/pms/README.rst b/pms/README.rst index e1d5d560ac..fa40b9e9b8 100644 --- a/pms/README.rst +++ b/pms/README.rst @@ -56,6 +56,22 @@ You will find the hotel settings in PMS Management > Configuration > Properties This module required additional configuration for company, accounting, invoicing and user privileges. +Children and room capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default children take no place in the room, so only the adults are checked +against its capacity. Properties that want to count them can set the system +parameter ``pms.children_occupy_capacity`` to ``True``, in Settings > Technical +> System Parameters. + +Once it is on, children take the regular places of the room first and only +overflow into its children places, the ones where only a child can sleep, such +as child bunk beds. Adults never occupy those, so a room with free children +places can still be full. + +Mind that turning it on refuses reservations that the property may have been +taking until then. + Usage ===== diff --git a/pms/__manifest__.py b/pms/__manifest__.py index 8f3e7ce0ed..04e569dac9 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -4,7 +4,7 @@ { "name": "PMS (Property Management System)", "summary": "A property management system", - "version": "16.0.4.24.0", + "version": "16.0.4.25.0", "development_status": "Beta", "category": "Generic Modules/Property Management System", "website": "https://github.com/OCA/pms", diff --git a/pms/demo/pms_master_data_no_update.xml b/pms/demo/pms_master_data_no_update.xml index 8fe01cd73e..134cfd2b16 100644 --- a/pms/demo/pms_master_data_no_update.xml +++ b/pms/demo/pms_master_data_no_update.xml @@ -326,6 +326,7 @@ 1 + 1 @@ -333,6 +334,7 @@ 1 + 1 @@ -340,6 +342,7 @@ 1 + 1 @@ -347,6 +350,7 @@ 1 + 1 @@ -354,6 +358,7 @@ 1 + 1 diff --git a/pms/migrations/16.0.4.25.0/pre-migration.py b/pms/migrations/16.0.4.25.0/pre-migration.py new file mode 100644 index 0000000000..f90dc667b0 --- /dev/null +++ b/pms/migrations/16.0.4.25.0/pre-migration.py @@ -0,0 +1,21 @@ +# Copyright 2026 Commit [Sun] +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from openupgradelib import openupgrade + +# children_occupying held the children taking a place in the room, apart from +# children, which held them all. Nothing kept both in sync, so a reservation +# could declare an occupying child without counting it in children. Those +# children are folded into children before the column goes away, or they would +# be lost. +_FOLD_CHILDREN_OCCUPYING = """ + UPDATE pms_reservation + SET children = children_occupying + WHERE children_occupying > COALESCE(children, 0) +""" + + +@openupgrade.migrate() +def migrate(env, version): + if openupgrade.column_exists(env.cr, "pms_reservation", "children_occupying"): + openupgrade.logged_query(env.cr, _FOLD_CHILDREN_OCCUPYING) + openupgrade.drop_columns(env.cr, [("pms_reservation", "children_occupying")]) diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 8a93d2df36..7158d55009 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -356,12 +356,9 @@ class PmsReservation(models.Model): compute="_compute_adults", tracking=True, ) - children_occupying = fields.Integer( - help="Number of children there in guest list whose presence counts", - ) children = fields.Integer( - help="Number total of children there in guest list," - "whose presence counts or not", + help="Number of children there in guest list. They take the regular " + "places of the room first, and only overflow into its children places", readonly=False, tracking=True, ) @@ -2215,9 +2212,10 @@ def write(self, vals): for record in self: record._check_services(vals) - # Only check if adult to avoid to check capacity in intermediate states - # (p.e. flush) that not take access to possible extra beds service in vals - if "adults" in vals: + # Only check on occupancy changes to avoid checking capacity in + # intermediate states (p.e. flush) that not take access to possible + # extra beds service in vals + if "adults" in vals or "children" in vals: record._check_capacity() # The room of the lines is recomputed from the preferred room, so # that write does not go through pms.reservation.line.write and the diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index faa7b44a02..5b6733ea9c 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -685,23 +685,31 @@ def _check_room_capacity(self): of the room (capacity + allowed extra beds) instead of the extra beds actually sold, to avoid false positives in intermediate states where the extra bed service is not created yet. + + Children count too: they take regular places first and only overflow + into the children places of the room. """ if self.env.context.get("avoid_capacity_check"): return for record in self: reservation = record.reservation_id - if not record.room_id or reservation.reservation_type == "out": + room = record.room_id + if not room or reservation.reservation_type == "out": continue - occupancy = reservation.adults + reservation.children_occupying - max_capacity = record.room_id.capacity + record.room_id.extra_beds_allowed - if occupancy > max_capacity: + if not room.fits_occupancy( + reservation.adults, reservation.children, room.extra_beds_allowed + ): raise ValidationError( _( - "The room %(room)s can't hold %(occupancy)s guests " - "(maximum capacity: %(capacity)s) (%(reservation)s)", - room=record.room_id.display_name, - occupancy=occupancy, - capacity=max_capacity, + "The room %(room)s can't hold %(adults)s adults and " + "%(children)s children (capacity: %(capacity)s, " + "children places: %(children_capacity)s) " + "(%(reservation)s)", + room=room.display_name, + adults=reservation.adults, + children=reservation.children, + capacity=room.capacity + room.extra_beds_allowed, + children_capacity=room.children_capacity, reservation=reservation.name, ) ) diff --git a/pms/models/pms_room.py b/pms/models/pms_room.py index d4502935a2..cb7ec0ed1e 100644 --- a/pms/models/pms_room.py +++ b/pms/models/pms_room.py @@ -4,6 +4,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools import str2bool class PmsRoom(models.Model): @@ -77,7 +78,14 @@ class PmsRoom(models.Model): check_pms_properties=True, ) capacity = fields.Integer( - help="The maximum number of people that can occupy a room" + help="Number of places where anyone can sleep, adults or children" + ) + children_capacity = fields.Integer( + help="Number of places where only children can sleep, such as child " + "bunk beds. Adults never occupy them, children occupy them only once " + "the regular capacity is full", + required=True, + default=0, ) extra_beds_allowed = fields.Integer( help="Number of extra beds allowed in room", @@ -314,16 +322,15 @@ def _check_adults(self, reservation, service_line_ids=False): and x.product_id.is_extra_bed is True ) num_extra_beds = sum(extra_beds.mapped("day_qty")) if extra_beds else 0 - if line.room_id: - if ( - reservation.adults + reservation.children_occupying - ) > line.room_id.get_capacity(num_extra_beds): - raise ValidationError( - _( - "Persons can't be higher than room capacity (%s)", - reservation.name, - ) + if line.room_id and not line.room_id.fits_occupancy( + reservation.adults, reservation.children, num_extra_beds + ): + raise ValidationError( + _( + "Persons can't be higher than room capacity (%s)", + reservation.name, ) + ) @api.constrains("short_name") def _check_short_name(self): @@ -430,3 +437,36 @@ def get_capacity(self, extra_bed=0): _("Extra beds can't be greater than allowed beds for this room") ) return record.capacity + extra_bed + + @api.model + def _children_occupy_capacity(self): + """Whether children take a place in the room. + + Off unless the property turns it on, which is how it worked until + now: counting them refuses reservations that properties have been + taking, so it is theirs to decide and not something an upgrade does. + """ + return str2bool( + self.env["ir.config_parameter"] + .sudo() + .get_param("pms.children_occupy_capacity"), + default=False, + ) + + def fits_occupancy(self, adults, children=0, extra_bed=0): + """Whether the room can hold the given guests. + + Children sleep in the regular capacity as well, and only overflow + into 'children_capacity' once it is full. Adults never occupy those + places, so a room with free children places can still be full. + + Whether children take a place at all is up to the property, see + _children_occupy_capacity. + """ + self.ensure_one() + capacity = self.get_capacity(extra_bed) + if adults > capacity: + return False + if not self._children_occupy_capacity(): + return True + return adults + children <= capacity + self.children_capacity diff --git a/pms/readme/CONFIGURE.rst b/pms/readme/CONFIGURE.rst index 940574e837..52c20657da 100644 --- a/pms/readme/CONFIGURE.rst +++ b/pms/readme/CONFIGURE.rst @@ -1,3 +1,19 @@ You will find the hotel settings in PMS Management > Configuration > Properties > Your Property. This module required additional configuration for company, accounting, invoicing and user privileges. + +Children and room capacity +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By default children take no place in the room, so only the adults are checked +against its capacity. Properties that want to count them can set the system +parameter ``pms.children_occupy_capacity`` to ``True``, in Settings > Technical +> System Parameters. + +Once it is on, children take the regular places of the room first and only +overflow into its children places, the ones where only a child can sleep, such +as child bunk beds. Adults never occupy those, so a room with free children +places can still be full. + +Mind that turning it on refuses reservations that the property may have been +taking until then. diff --git a/pms/static/description/index.html b/pms/static/description/index.html index 584e80f0d6..daa00b91fd 100644 --- a/pms/static/description/index.html +++ b/pms/static/description/index.html @@ -383,13 +383,16 @@

PMS (Property Management System)

-

Usage

+

Usage

To use this module, please, read the complete user guide at roomdoo.com.

-

Bug Tracker

+

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 @@ -417,15 +433,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Commit [Sun]
-

Contributors

+

Contributors

  • Alexandre Díaz
  • Pablo Quesada
  • @@ -442,7 +458,7 @@

    Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py index 5fcb297d97..3bc109fbff 100644 --- a/pms/tests/test_pms_reservation.py +++ b/pms/tests/test_pms_reservation.py @@ -816,6 +816,39 @@ def test_split_reservation07(self): } ) + def _children_occupy_capacity(self): + """Count children towards the capacity, which is off by default.""" + self.env["ir.config_parameter"].sudo().set_param( + "pms.children_occupy_capacity", "True" + ) + + @freeze_time("2012-01-14") + def test_children_do_not_occupy_by_default(self): + """Children take no place unless the property says otherwise. + + Properties have been taking these reservations all along, so counting + children is theirs to turn on and never something an upgrade does. + """ + # NO ARRANGE + # ACT + reservation = self.env["pms.reservation"].create( + { + "adults": 2, + "children": 3, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": self.room1.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + reservation.flush_recordset() + # ASSERT + self.assertEqual( + reservation.children, 3, "The children should fit in a double room" + ) + @freeze_time("2012-01-14") def test_manage_children_raise(self): # TEST CASE @@ -823,24 +856,106 @@ def test_manage_children_raise(self): Check if the error occurs when trying to put more people than the capacity of the room. -------------- - Create a reservation with a double room whose capacity is two and try to create - it with two adults and a child occupying the room. + Create a reservation with a double room whose capacity is two and no + children places, and try to create it with two adults and a child. """ - # NO ARRANGE + # ARRANGE + self._children_occupy_capacity() # ACT & ASSERT - with self.assertRaises( + with self.assertRaisesRegex( ValidationError, + "capacity", msg="The number of people is greater than the capacity of the room", ): reservation = self.env["pms.reservation"].create( { "adults": 2, - "children_occupying": 1, + "children": 1, "checkin": datetime.datetime.now(), "checkout": datetime.datetime.now() + datetime.timedelta(days=1), "preferred_room_id": self.room1.id, "partner_id": self.partner1.id, "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + reservation.flush_recordset() + + @freeze_time("2012-01-14") + def test_children_use_children_capacity(self): + """ + Check that children overflow into the children places of the room. + -------------- + Create a room with two regular places and one children place, and a + reservation with two adults and one child: the child does not fit in + the regular places but takes the children one. + """ + # ARRANGE + self._children_occupy_capacity() + room = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Double with bunk bed", + "room_type_id": self.room_type_double.id, + "capacity": 2, + "children_capacity": 1, + } + ) + # ACT + reservation = self.env["pms.reservation"].create( + { + "adults": 2, + "children": 1, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": room.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + reservation.flush_recordset() + # ASSERT + self.assertEqual( + reservation.children, + 1, + "The child should take the children place of the room", + ) + + @freeze_time("2012-01-14") + def test_adults_do_not_use_children_capacity(self): + """ + Check that adults never take the children places of the room. + -------------- + Create a room with two regular places and two children places, and try + to book it for three adults: the room holds four guests but only two + of them can be adults. + """ + # ARRANGE + room = self.env["pms.room"].create( + { + "pms_property_id": self.pms_property1.id, + "name": "Double with two bunk beds", + "room_type_id": self.room_type_double.id, + "capacity": 2, + "children_capacity": 2, + } + ) + # ACT & ASSERT + with self.assertRaisesRegex( + ValidationError, + "capacity", + msg="Adults cannot occupy the children places of the room", + ): + reservation = self.env["pms.reservation"].create( + { + "adults": 3, + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=1), + "preferred_room_id": room.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, } ) reservation.flush_recordset() diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 96b783bc38..9e711a25c0 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -443,10 +443,6 @@ name="children" attrs="{'invisible': [('reservation_type','in',('out'))]}" /> - diff --git a/pms/views/pms_room_views.xml b/pms/views/pms_room_views.xml index 913efbd9d0..30d4ee9b5d 100644 --- a/pms/views/pms_room_views.xml +++ b/pms/views/pms_room_views.xml @@ -50,6 +50,7 @@ attrs="{'invisible':[('parent_id', '=', False)]}" /> + @@ -245,6 +246,7 @@ + diff --git a/pms_l10n_es/wizards/wizard_ine.py b/pms_l10n_es/wizards/wizard_ine.py index 7b7d3c0862..cfc12b4fe5 100644 --- a/pms_l10n_es/wizards/wizard_ine.py +++ b/pms_l10n_es/wizards/wizard_ine.py @@ -141,11 +141,7 @@ def ine_rooms(self, start_date, end_date, pms_property_id): and x.occupies_availability ) if reservation_lines: - extra_beds += ( - ebsl.day_qty - - reservation_lines.reservation_id.children_occupying - ) - # children occuppying do not have checkin partner data + extra_beds += ebsl.day_qty # search all rooms all_rooms = (