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}"
/>
-
-
+
+
+
+