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()