From b0eaffe7fc503b579d8ced2138a0556b1361898a Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Fri, 4 Sep 2026 10:27:33 +0200 Subject: [PATCH] [FIX] pms: validate reservation dates once both are computed check_in_out_dates() was called from inside _compute_checkin, _compute_checkout and _compute_reservation_line_ids, so it validated the checkin/checkout pair while it was still being computed. A reservation created without dates defaults its checkin to the one of the first reservation of its folio. When the same write supplies the reservation lines too, that default is only replaced once the lines are stored, so a reservation of the batch can still be holding the inherited checkin while its checkout has already been computed from its own lines. Adding to a folio several reservations whose dates are earlier than the ones already in it -a channel modification bringing a multi-room stay forward, for instance- then failed with "Room line Check In Date Should be less than the Check Out Date!" even though the values being stored were consistent. Turn the check into a constraint on checkin and checkout so that it runs at flush time, once every compute has been resolved, and drop the three calls made from inside the computes. Two defects in the same path are fixed as well: * the default branch of _compute_checkout wrote checkin instead of checkout, so a reservation added to a folio with neither dates nor lines never got its default checkout; * checkin_checkout_consecutive_dates raised "min() arg is an empty sequence" on a reservation without lines, which is the state an inconsistent checkin/checkout pair leaves behind. Those are now left to check_in_out_dates, which reports them with a readable message. --- pms/models/pms_reservation.py | 11 ++-- pms/tests/test_pms_reservation.py | 84 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 5 deletions(-) diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index d844c7c7fc..42caeea24c 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -940,7 +940,6 @@ def _compute_reservation_line_ids(self): else: if not reservation.reservation_line_ids: reservation.reservation_line_ids = False - reservation.check_in_out_dates() @api.depends("board_service_room_id", "adults", "children") def _compute_board_service_ids(self): @@ -1199,7 +1198,6 @@ def _compute_checkin(self): record.checkin = record.folio_id.reservation_ids[0].checkin else: record.checkin = fields.date.today() - record.check_in_out_dates() @api.depends("reservation_line_ids", "checkin") def _compute_checkout(self): @@ -1218,13 +1216,11 @@ def _compute_checkout(self): # default checkout if checkin is set elif record.checkin and not record.checkout: if len(record.folio_id.reservation_ids) > 1: - record.checkin = record.folio_id.reservation_ids[0].checkout + record.checkout = record.folio_id.reservation_ids[0].checkout else: record.checkout = record.checkin + datetime.timedelta(days=1) elif not record.checkout: record.checkout = False - # date checking - record.check_in_out_dates() # pylint: disable=W8110 def _compute_precheckin_url(self): @@ -1767,6 +1763,7 @@ def _get_default_sale_channel_origin(self): sale_channel_origin_id = folio.sale_channel_origin_id return sale_channel_origin_id + @api.constrains("checkin", "checkout") def check_in_out_dates(self): """ 1.-When date_order is less then checkin date or @@ -1797,6 +1794,10 @@ def checkin_checkout_consecutive_dates(self): of the set of ordinal dates is one more than the length of the set """ for record in self: + if not record.reservation_line_ids: + # an inconsistent checkin/checkout pair builds no lines at all; + # check_in_out_dates is the one reporting it + continue if min(record.reservation_line_ids.mapped("date")) != record.checkin: raise UserError( _( diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py index 3c19df3131..5fcb297d97 100644 --- a/pms/tests/test_pms_reservation.py +++ b/pms/tests/test_pms_reservation.py @@ -231,6 +231,90 @@ def test_reservation_dates_compute_checkin_out(self): not correspond to the last day indicated in the dates", ) + @freeze_time("2012-01-14") + def test_reservation_default_dates_from_folio(self): + """ + Check the dates defaulted to a reservation added to a folio that + already holds one. + ---------------- + A reservation created with neither dates nor reservation lines takes + them from the first reservation of its folio, so both stays run in + parallel. The checkout was being written on the checkin field, leaving + the new reservation without a checkout of its own. + """ + # ARRANGE + checkin = fields.date.today() + datetime.timedelta(days=8) + checkout = checkin + datetime.timedelta(days=2) + folio = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": folio.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ACT + reservation = self.env["pms.reservation"].create( + { + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "folio_id": folio.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + # ASSERT + self.assertEqual( + reservation.checkin, + checkin, + "The reservation should start the day the folio does", + ) + self.assertEqual( + reservation.checkout, + checkout, + "The reservation should end the day the folio does", + ) + + @freeze_time("2012-01-14") + def test_reservation_checkout_before_checkin(self): + """ + Check that a reservation cannot end before it starts. + ---------------- + The dates are validated by a constraint, so the pair is checked once + both of them have been computed. + """ + # ARRANGE + checkin = fields.date.today() + datetime.timedelta(days=8) + + # ACT & ASSERT + with self.assertRaises( + UserError, + msg="Error, it has been allowed to create a reservation ending " + "before it starts", + ): + self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkin - datetime.timedelta(days=1), + "room_type_id": self.room_type_double.id, + "partner_id": self.partner1.id, + "pms_property_id": self.pms_property1.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + @freeze_time("2012-01-14") def test_create_reservation_start_date(self): """