Skip to content

[ADD] connector_pms: charge a fee per child - #171

Draft
jesusVMayor wants to merge 1 commit into
16.0from
16.0-add-children-fee
Draft

jesusVMayor wants to merge 1 commit into
16.0from
16.0-add-children-fee

Conversation

@jesusVMayor

Copy link
Copy Markdown
Member

Depends on OCA/pms#464, which adds the pms.pricelist.occupancy model this
extends. connector_pms will not install until that one is merged.

Room types are priced by the adults staying in them, as that is what the
occupancy of a room type counts, so children came along for free however many
they were. Channel managers charge them as a fee on top of that price, and the
PMS had no way to express it.

What this adds

  • children_fee on the occupancy configuration of a pricelist, charged for
    each child and night on top of the price derived for the adults.
  • children_max_age on the property: the age up to which a guest is charged as
    a child. A commercial decision, unrelated to the ages the law goes by.
  • children_ages on the reservation, a fields.Json holding the age of each
    child as the channel declares them ([5, 13]).

The ages change nothing by themselves

The price is derived from the declared number of guests and never from the
birthdate of the guests checking in, so filling the check-in data in does not
reprice a reservation: nobody is going to retarify at the desk because a child
had a birthday.

Three age thresholds now coexist and must stay apart: the legal one in
pms_l10n_es (AGE_OF_MAJORITY, and 14 for the document, read from the real
birthdate), the fiscal one of the tourist tax, and this commercial one. Only
the first two look at birthdates.

children_ages is stored for the connectors to fill in when importing, as
Channex sends rooms[].occupancy.ages. Nothing reads it yet.

Room types are priced by the adults staying in them, as that is what the
occupancy of a room type counts, so children came along for free however
many they were. Channel managers charge them as a fee on top of that
price, and the PMS had no way to express it.

A pricelist now carries the fee for each child and night of a room type,
the property carries the age up to which a guest is charged as a child,
and reservations keep the age of each child as the channel declares them.

Those ages change nothing by themselves. The price is derived from the
declared number of guests and never from the birthdate of the guests
checking in, so filling the check-in data in does not reprice a
reservation: nobody is going to retarify at the desk because a child had
a birthday. The ages the law goes by are another matter, live in
pms_l10n_es and are read from the real birthdate, untouched by this.

Assisted-by: Claude Opus 5
@jesusVMayor
jesusVMayor marked this pull request as ready for review October 9, 2026 10:15
@jesusVMayor
jesusVMayor marked this pull request as draft October 9, 2026 10:15
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Diff Coverage

Diff: origin/16.0...HEAD, staged and unstaged changes

  • connector_pms/models/pms_pricelist_occupancy.py (0.0%): Missing lines 2-3,6-7,9,16-18,20,22-24,27,29-33
  • connector_pms/models/pms_property.py (0.0%): Missing lines 2,5-6,8
  • connector_pms/models/pms_reservation.py (0.0%): Missing lines 18,25,27,32,35
  • connector_pms/models/pms_reservation_line.py (0.0%): Missing lines 2,5-6,8,10-12
  • connector_pms/models/product_pricelist_item.py (0.0%): Missing lines 2,5-6,8,15-20,23-25

Summary

  • Total: 47 lines
  • Missing: 47 lines
  • Coverage: 0%

connector_pms/models/pms_pricelist_occupancy.py

Lines 1-13

   1 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
!  2 from odoo import _, api, fields, models
!  3 from odoo.exceptions import ValidationError
   4 
   5 
!  6 class PmsPricelistOccupancy(models.Model):
!  7     _inherit = "pms.pricelist.occupancy"
   8 
!  9     children_fee = fields.Float(
  10         help="Charged for each child and night, on top of the price for the "
  11         "adults staying in the room",
  12         digits=("Product Price"),
  13     )

Lines 12-34

  12         digits=("Product Price"),
  13     )
  14 
  15     # pylint: disable=W8110
! 16     @api.depends("children_fee")
! 17     def _compute_summary(self):
! 18         return super()._compute_summary()
  19 
! 20     def _summary_parts(self):
  21         """Children show up in the summary as what each of them adds."""
! 22         parts = super()._summary_parts()
! 23         if self.children_fee:
! 24             parts.append(
  25                 _("child +%s") % self._format_modifier("amount", self.children_fee)
  26             )
! 27         return parts
  28 
! 29     @api.constrains("children_fee")
! 30     def _check_children_fee_not_negative(self):
! 31         for record in self:
! 32             if record.children_fee < 0:
! 33                 raise ValidationError(_("The children fee can't be negative."))

connector_pms/models/pms_property.py

Lines 1-12

   1 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
!  2 from odoo import fields, models
   3 
   4 
!  5 class PmsProperty(models.Model):
!  6     _inherit = "pms.property"
   7 
!  8     children_max_age = fields.Integer(
   9         string="Children Max. Age",
  10         help="Age up to which a guest is charged as a child. This is a "
  11         "commercial decision of the property and has nothing to do with the "
  12         "ages the law goes by, which are read from the birthdate of the guest",

connector_pms/models/pms_reservation.py

Lines 14-22

  14     ota_reservation_code = fields.Char(
  15         string="OTA Reservation Code",
  16         readonly=True,
  17     )
! 18     children_ages = fields.Json(
  19         help="Age of each child of the reservation, as the channel declares "
  20         "them, such as [5, 13]. The price is derived from the declared number "
  21         "of guests, never from this, so filling it in changes nothing by "
  22         "itself",

Lines 21-39

  21         "of guests, never from this, so filling it in changes nothing by "
  22         "itself",
  23     )
  24 
! 25     def _occupancy_price_fields(self):
  26         """Children are charged too, so changing how many there are reprices."""
! 27         return super()._occupancy_price_fields() + ["children"]
  28 
  29     # pylint: disable=W8110
  30     @api.depends("ota_reservation_code")
  31     def _compute_external_reference(self):
! 32         super()._compute_external_reference()
  33 
  34     def _get_reservation_external_reference(self):
! 35         reference = super()._get_reservation_external_reference()
  36         if self.ota_reservation_code:
  37             reference = self.ota_reservation_code
  38         return reference

connector_pms/models/pms_reservation_line.py

Lines 1-13

   1 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
!  2 from odoo import models
   3 
   4 
!  5 class PmsReservationLine(models.Model):
!  6     _inherit = "pms.reservation.line"
   7 
!  8     def _get_price_kwargs(self):
   9         """Children are charged on top of the price for the adults."""
! 10         res = super()._get_price_kwargs()
! 11         res["children"] = self.reservation_id.children
! 12         return res

connector_pms/models/product_pricelist_item.py

Lines 1-12

   1 # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
!  2 from odoo import models
   3 
   4 
!  5 class ProductPricelistItem(models.Model):
!  6     _inherit = "product.pricelist.item"
   7 
!  8     def _apply_occupancy_price(self, product, price, **kwargs):
   9         """Add the fee of the children staying to the price of the room.
  10 
  11         The price derived from the occupancy is the one for the adults, as the
  12         occupancy of a room type counts them alone. Children are charged on top

Lines 11-26

  11         The price derived from the occupancy is the one for the adults, as the
  12         occupancy of a room type counts them alone. Children are charged on top
  13         of it, the same way channel managers do.
  14         """
! 15         price = super()._apply_occupancy_price(product, price, **kwargs)
! 16         children = kwargs.get("children")
! 17         room_type = product.room_type_id if children else False
! 18         if not room_type:
! 19             return price
! 20         occupancy_rule = room_type._get_occupancy_rule(
  21             kwargs.get("pricelist") or self.pricelist_id
  22         )
! 23         if not occupancy_rule.children_fee:
! 24             return price
! 25         return price + occupancy_rule.children_fee * children

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant