From 0be882d7b97103da747d0d35554f358d3557892e Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Fri, 15 May 2026 13:29:16 +0200 Subject: [PATCH] [REF] pms: scoped recompute of real_avail on pms.room changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ``pms.availability.real_avail`` field used to declare ``room_type_id.total_rooms_count`` as one of its ``@api.depends``. Because ``total_rooms_count`` is itself a stored compute on ``pms.room.type`` that depends on ``room_ids`` / ``room_ids.active``, every single change on a room (activate / deactivate, re-segment to another room type, create a new room, delete one) triggered a recompute of EVERY ``pms.availability`` row of the affected room type(s) — historical rows included. On busy properties this locked the table for minutes. This commit replaces that wide dependency with a SCOPED, future-only recompute driven from ``pms.room.create / write / unlink``: * Remove ``room_type_id.total_rooms_count`` from the depends of ``_compute_real_avail`` (history rows no longer recompute by reflex). * Add ``pms.availability._recompute_real_avail_for_room_change( pms_property_ids, room_type_ids, date_from=None)`` that schedules a recompute only for the matching (property, room_type) tuples and only for ``date >= today()`` by default. * Override ``pms.room.create / write / unlink`` to call the helper with the affected (property, room_type) sets. For ``write``, the union of OLD and NEW pairs is used so re-segmentation (moving a room between room types) correctly recomputes both the source and the target room_type's future avail. The recompute uses ``env.add_to_compute`` so it integrates with Odoo's normal flush mechanism — the cache stays consistent and any reader within the same transaction sees the new values. --- pms/models/pms_availability.py | 50 +++++++++++++++++++++++++++++++++- pms/models/pms_room.py | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/pms/models/pms_availability.py b/pms/models/pms_availability.py index e0dcbba6b5..ed134d1204 100644 --- a/pms/models/pms_availability.py +++ b/pms/models/pms_availability.py @@ -85,10 +85,20 @@ class PmsAvailability(models.Model): ) ] + # Note: a dependency on ``room_type_id.total_rooms_count`` is + # deliberately NOT declared here. That field is itself a stored + # compute on ``pms.room_type`` whose value changes whenever any of + # the room_type's ``room_ids`` toggles ``active`` or moves to a + # different ``room_type_id``. Declaring it as a dependency caused + # every such room change to trigger a recompute of EVERY + # ``pms.availability`` row tied to the affected room type(s) — + # historical rows included — which locked the table for minutes on + # a busy property. Instead, ``pms.room.write/create/unlink`` + # triggers a SCOPED, future-only recompute via + # ``_recompute_real_avail_for_room_change`` below. @api.depends( "reservation_line_ids", "reservation_line_ids.occupies_availability", - "room_type_id.total_rooms_count", "reservation_line_ids.room_id", "reservation_line_ids.date", "parent_avail_id", @@ -118,6 +128,44 @@ def _compute_real_avail(self): ) record.real_avail = len(room_ids) - count_rooms_not_avail + @api.model + def _recompute_real_avail_for_room_change( + self, pms_property_ids, room_type_ids, date_from=None + ): + """Trigger a scoped recompute of ``real_avail`` after a + ``pms.room`` change (active toggle, ``room_type_id`` move, + new/deleted room). + + Limits the recompute to (property, room_type) pairs in the + given sets and to dates ``>= date_from`` (defaults to today). + Replaces the wider ``room_type_id.total_rooms_count`` automatic + dependency — see the comment on ``_compute_real_avail`` for the + rationale. + + :param pms_property_ids: iterable of ``pms.property`` ids whose + rooms changed. + :param room_type_ids: iterable of ``pms.room.type`` ids whose + total room count is affected (use both OLD and NEW types + when handling a re-segmentation). + :param date_from: optional lower-bound date for the recompute + scope. Defaults to today. + """ + property_ids = [pid for pid in pms_property_ids if pid] + room_type_ids = [rid for rid in room_type_ids if rid] + if not property_ids or not room_type_ids: + return + if date_from is None: + date_from = fields.Date.today() + avails = self.search( + [ + ("pms_property_id", "in", property_ids), + ("room_type_id", "in", room_type_ids), + ("date", ">=", date_from), + ] + ) + if avails: + self.env.add_to_compute(self._fields["real_avail"], avails) + @api.depends("reservation_line_ids", "reservation_line_ids.room_id") def _compute_parent_avail_id(self): for record in self: diff --git a/pms/models/pms_room.py b/pms/models/pms_room.py index be54a6a17e..d3d5dae326 100644 --- a/pms/models/pms_room.py +++ b/pms/models/pms_room.py @@ -325,6 +325,23 @@ def _check_short_name(self): _("The short name can't contain more than 4 characters") ) + # Fields whose change on a room shifts the room_type's effective + # room count for one or more (property, room_type) pairs and so + # invalidates ``pms.availability.real_avail`` for FUTURE dates. + _PMS_AVAIL_RELEVANT_FIELDS = { + "active", + "room_type_id", + "pms_property_id", + "parent_id", + } + + def _trigger_avail_recompute(self, property_ids, room_type_ids): + if not property_ids or not room_type_ids: + return + self.env["pms.availability"]._recompute_real_avail_for_room_change( + property_ids, room_type_ids + ) + @api.model_create_multi def create(self, vals_list): for vals in vals_list: @@ -334,7 +351,12 @@ def create(self, vals_list): vals.update({"short_name": short_name}) else: vals.update({"short_name": vals["name"]}) - return super().create(vals_list) + records = super().create(vals_list) + records._trigger_avail_recompute( + records.mapped("pms_property_id").ids, + records.mapped("room_type_id").ids, + ) + return records def write(self, vals): if vals.get("name") and not vals.get("short_name"): @@ -343,7 +365,30 @@ def write(self, vals): vals.update({"short_name": short_name}) else: vals.update({"short_name": vals["name"]}) - return super().write(vals) + relevant_change = bool(set(vals) & self._PMS_AVAIL_RELEVANT_FIELDS) + old_property_ids = old_room_type_ids = [] + if relevant_change: + old_property_ids = self.mapped("pms_property_id").ids + old_room_type_ids = self.mapped("room_type_id").ids + result = super().write(vals) + if relevant_change: + new_property_ids = self.mapped("pms_property_id").ids + new_room_type_ids = self.mapped("room_type_id").ids + # Union: a re-segmentation moves a room out of one type and + # into another, so BOTH the old and new (property, type) + # pairs need their future avail recomputed. + self._trigger_avail_recompute( + list(set(old_property_ids) | set(new_property_ids)), + list(set(old_room_type_ids) | set(new_room_type_ids)), + ) + return result + + def unlink(self): + property_ids = self.mapped("pms_property_id").ids + room_type_ids = self.mapped("room_type_id").ids + result = super().unlink() + self.env["pms.room"]._trigger_avail_recompute(property_ids, room_type_ids) + return result def calculate_short_name(self, vals): short_name = vals["name"][:2].upper()