From 026d524533358c2328744fc0ab3a5e9db5328991 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 2 Oct 2026 20:00:13 +0200 Subject: [PATCH] [IMP] pms: non-refundable option for cancellation rules days_intime was labelled as the opposite of what the code does: cancelling at least that many days before check-in is free, and 0 means free until arrival. Non-refundable rates had to be emulated with a huge number of days. Add a Non-refundable flag that always applies the late penalty, and fix the label and help of days_intime. --- pms/i18n/es.po | 31 ++++++++--- pms/models/pms_cancelation_rule.py | 11 +++- pms/models/pms_reservation.py | 2 +- pms/tests/test_pms_reservation.py | 67 ++++++++++++++++++++++++ pms/views/pms_cancelation_rule_views.xml | 13 ++++- 5 files changed, 113 insertions(+), 11 deletions(-) diff --git a/pms/i18n/es.po b/pms/i18n/es.po index e9e5f6710e..b6adce7707 100644 --- a/pms/i18n/es.po +++ b/pms/i18n/es.po @@ -3853,8 +3853,8 @@ msgstr "" #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_cancelation_rule__days_intime -msgid "Days Late" -msgstr "Nº de días pre-checkin" +msgid "Free Cancellation Days" +msgstr "Días de cancelación gratuita" #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_folio__days_to_checkin @@ -6734,8 +6734,8 @@ msgstr "La Estancia Máxima no puede ser menor que cero" #. module: pms #: model_terms:ir.ui.view,arch_db:pms.pms_cancelation_rule_form -msgid "Max. days InTime before Checkin" -msgstr "Máximo de días a tiempo antes del Checkin" +msgid "Free cancellation up to (days before check-in)" +msgstr "Cancelación gratuita hasta (días antes del check-in)" #. module: pms #: model:ir.model.fields,field_description:pms.field_pms_room_type__tourist_tax_max_age @@ -6751,9 +6751,14 @@ msgstr "Importe máximo para crear la factura simplificada" #. module: pms #: model:ir.model.fields,help:pms.field_pms_cancelation_rule__days_intime -msgid "Maximum number of days for free cancellation before Checkin" +msgid "" +"Minimum number of days before check-in to cancel free of charge. Later " +"cancellations apply the late penalty. With 0, cancellation is free until the " +"day of arrival." msgstr "" -"Número máximo de días para que la cancelación sea gratis antes del checkin" +"Antelación mínima, en días antes del check-in, para cancelar sin coste. Las " +"cancelaciones posteriores aplican la penalización por cancelación tardía. Con " +"0, la cancelación es gratuita hasta el día de llegada." #. module: pms #: model:ir.model.fields,help:pms.field_pms_availability_plan_rule__max_avail @@ -15190,3 +15195,17 @@ msgstr "" #~ msgid "host state" #~ msgstr "Provincia" + +#. module: pms +#: model:ir.model.fields,field_description:pms.field_pms_cancelation_rule__is_non_refundable +msgid "Non-refundable" +msgstr "No reembolsable" + +#. module: pms +#: model:ir.model.fields,help:pms.field_pms_cancelation_rule__is_non_refundable +msgid "" +"There is no free cancellation period: the late penalty applies whenever the " +"reservation is cancelled before check-in." +msgstr "" +"No hay periodo de cancelación gratuita: la penalización por cancelación tardía " +"se aplica siempre que se cancele la reserva antes del check-in." diff --git a/pms/models/pms_cancelation_rule.py b/pms/models/pms_cancelation_rule.py index 542e62b017..bc3434c4cd 100644 --- a/pms/models/pms_cancelation_rule.py +++ b/pms/models/pms_cancelation_rule.py @@ -37,9 +37,16 @@ class PmsCancelationRule(models.Model): active = fields.Boolean( help="Determines if cancelation rule is active", default=True ) + is_non_refundable = fields.Boolean( + string="Non-refundable", + help="There is no free cancellation period: the late penalty applies " + "whenever the reservation is cancelled before check-in.", + ) days_intime = fields.Integer( - string="Days Late", - help="Maximum number of days for free cancellation before Checkin", + string="Free Cancellation Days", + help="Minimum number of days before check-in to cancel free of charge. " + "Later cancellations apply the late penalty. With 0, cancellation " + "is free until the day of arrival.", ) penalty_late = fields.Integer( string="% Penalty Late", diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 8a93d2df36..2cef114a50 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -2454,7 +2454,7 @@ def _check_cancel_penalty(self): days = 1 elif rule.apply_on_noshow == "days": days = rule.days_late - 1 - elif days_diff < pricelist.cancelation_rule_id.days_intime: + elif rule.is_non_refundable or days_diff < rule.days_intime: record.cancelled_reason = "late" penalty_percent = rule.penalty_late if rule.apply_on_late == "first": diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py index 5fcb297d97..247d92574c 100644 --- a/pms/tests/test_pms_reservation.py +++ b/pms/tests/test_pms_reservation.py @@ -1732,6 +1732,73 @@ def test_cancelation_reason_late(self): reservation.flush_recordset() self.assertEqual(reservation.cancelled_reason, "late", "-----------") + def _create_reservation_with_rule(self, rule_vals, days_to_checkin): + rule = self.env["pms.cancelation.rule"].create( + dict( + { + "name": "Cancelation Rule Test", + "pms_property_ids": [self.pms_property1.id], + }, + **rule_vals, + ) + ) + pricelist = self.env["product.pricelist"].create( + { + "name": "Pricelist Test", + "pms_property_ids": [self.pms_property1.id], + "cancelation_rule_id": rule.id, + "availability_plan_id": self.availability_plan1.id, + "is_pms_available": True, + } + ) + partner = self.env["res.partner"].create({"firstname": "Host1"}) + checkin = fields.date.today() + datetime.timedelta(days=days_to_checkin) + return self.env["pms.reservation"].create( + { + "checkin": checkin, + "checkout": checkin + datetime.timedelta(days=3), + "room_type_id": self.room_type_double.id, + "partner_id": partner.id, + "pms_property_id": self.pms_property1.id, + "pricelist_id": pricelist.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + + @freeze_time("2012-01-14") + def test_cancelation_reason_non_refundable(self): + """ + A non-refundable rule applies the late penalty whenever the + reservation is cancelled before check-in, however far ahead. + """ + reservation = self._create_reservation_with_rule( + {"is_non_refundable": True, "days_intime": 0}, days_to_checkin=30 + ) + reservation.action_cancel() + reservation.flush_recordset() + self.assertEqual( + reservation.cancelled_reason, + "late", + "A non-refundable rule must always apply the late penalty", + ) + + @freeze_time("2012-01-14") + def test_cancelation_reason_zero_days_is_free_until_arrival(self): + """ + Without the non-refundable flag, 0 free cancellation days means + that cancellation is free until the day of arrival. + """ + reservation = self._create_reservation_with_rule( + {"days_intime": 0}, days_to_checkin=30 + ) + reservation.action_cancel() + reservation.flush_recordset() + self.assertEqual( + reservation.cancelled_reason, + "intime", + "With 0 free cancellation days, cancelling before arrival is free", + ) + @freeze_time("2012-01-14") def test_compute_checkin_partner_count(self): """ diff --git a/pms/views/pms_cancelation_rule_views.xml b/pms/views/pms_cancelation_rule_views.xml index e9f714baa2..6a00963bf8 100644 --- a/pms/views/pms_cancelation_rule_views.xml +++ b/pms/views/pms_cancelation_rule_views.xml @@ -16,11 +16,19 @@ options="{'no_create': True,'no_open': True}" /> -