From 599a00aa0167364d266bf29111b8808aeeefa239 Mon Sep 17 00:00:00 2001 From: DarioLodeiros Date: Tue, 27 Oct 2026 15:47:49 +0100 Subject: [PATCH] [FIX] pms: don't take a partner pricelist restricted to another property A new folio or reservation took the pricelist of its partner, or of its agency, whenever it was available in the PMS, without checking that it could be used in the record's property. In multi-property databases most PMS pricelists belong to a single property, so creating a folio in another property for that partner failed on the property check with "Incompatible properties on records", even when its reservations were sent with a valid pricelist. Partners without a pricelist of their own are affected too, as Odoo falls back to the first active pricelist. The booking engine proposed that same pricelist and failed the same way. A partner or agency pricelist restricted to other properties is now skipped like a pricelist not available in the PMS, and the pricelist comes from the next rule: the partner after the agency, then the folio or the property default. Agency pricelists not available in the PMS, which agencies cannot be given, are skipped too. While the property is not set yet, as in a new form, only the PMS availability is checked. --- pms/models/pms_folio.py | 14 +++-- pms/models/pms_reservation.py | 10 ++-- pms/models/product_pricelist.py | 17 ++++++ pms/tests/common.py | 58 ++++++++++++++++++++ pms/tests/test_pms_booking_engine.py | 25 +++++++++ pms/tests/test_pms_folio.py | 56 +++++++++++++++++++ pms/tests/test_pms_reservation.py | 80 ++++++++++++++++++++++++++++ pms/wizards/pms_booking_engine.py | 6 ++- 8 files changed, 258 insertions(+), 8 deletions(-) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 5f3d9f94c0..47b21cbd98 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -894,13 +894,21 @@ def _compute_pricelist_id(self): folio.pricelist_id = False elif len(folio.reservation_ids.pricelist_id) == 1: folio.pricelist_id = folio.reservation_ids.pricelist_id - elif is_new and folio.agency_id and folio.agency_id.apply_pricelist: + elif ( + is_new + and folio.agency_id + and folio.agency_id.apply_pricelist + and folio.agency_id.property_product_pricelist._is_pms_usable( + folio.pms_property_id + ) + ): folio.pricelist_id = folio.agency_id.property_product_pricelist elif ( is_new and folio.partner_id - and folio.partner_id.property_product_pricelist - and folio.partner_id.property_product_pricelist.is_pms_available + and folio.partner_id.property_product_pricelist._is_pms_usable( + folio.pms_property_id + ) ): folio.pricelist_id = folio.partner_id.property_product_pricelist elif not folio.pricelist_id: diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 2cef114a50..0dc346ac2c 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -992,17 +992,21 @@ def _compute_pricelist_id(self): is_new and reservation.agency_id and reservation.agency_id.apply_pricelist + and reservation.agency_id.property_product_pricelist._is_pms_usable( + reservation.pms_property_id + ) ): reservation.pricelist_id = ( reservation.agency_id.property_product_pricelist ) # only change de pricelist if the reservation is not yet saved - # and the partner has a pricelist default + # and the partner has a pricelist default usable in its property elif ( is_new and reservation.partner_id - and reservation.partner_id.property_product_pricelist - and reservation.partner_id.property_product_pricelist.is_pms_available + and reservation.partner_id.property_product_pricelist._is_pms_usable( + reservation.pms_property_id + ) ): reservation.pricelist_id = ( reservation.partner_id.property_product_pricelist diff --git a/pms/models/product_pricelist.py b/pms/models/product_pricelist.py index b78c346163..c92845b90a 100644 --- a/pms/models/product_pricelist.py +++ b/pms/models/product_pricelist.py @@ -158,6 +158,23 @@ def _compute_price_rule(self, products, qty, uom=None, date=False, **kwargs): return results + def _is_pms_usable(self, pms_property): + """Return whether the pricelist can be used in the given property. + + It must be available in the PMS and not restricted to other + properties. Without a property (e.g. not chosen yet in a form) + only the PMS availability is checked. An empty recordset is + never usable. + """ + return bool( + self.is_pms_available + and ( + not pms_property + or not self.pms_property_ids + or pms_property in self.pms_property_ids + ) + ) + @api.constrains("is_pms_available", "availability_plan_id") def _check_is_pms_available(self): for record in self: diff --git a/pms/tests/common.py b/pms/tests/common.py index 3909870aa4..93c308342e 100644 --- a/pms/tests/common.py +++ b/pms/tests/common.py @@ -47,3 +47,61 @@ def setUpClass(cls): if not pricelist.availability_plan_id: pricelist.availability_plan_id = cls.availability_plan1.id pricelist.is_pms_available = True + + def create_pricelist_by_property_scenario(self): + """ + - create pms_property2 in company1, with room_type_property2 + and one room of it + - create pricelist_property1 and pricelist_property2, each one + restricted to its property; pricelist_property2 is the default + pricelist of pms_property2 + - create partner_pricelist_property1 with pricelist_property1 + as its pricelist + """ + self.pms_property2 = self.env["pms.property"].create( + { + "name": "Property 2", + "company_id": self.company1.id, + "default_pricelist_id": self.pricelist1.id, + } + ) + self.pricelist_property1 = self.env["product.pricelist"].create( + { + "name": "Pricelist Property 1", + "is_pms_available": True, + "availability_plan_id": self.availability_plan1.id, + "pms_property_ids": [(6, 0, [self.pms_property1.id])], + } + ) + self.pricelist_property2 = self.env["product.pricelist"].create( + { + "name": "Pricelist Property 2", + "is_pms_available": True, + "availability_plan_id": self.availability_plan1.id, + "pms_property_ids": [(6, 0, [self.pms_property2.id])], + } + ) + self.pms_property2.default_pricelist_id = self.pricelist_property2 + self.partner_pricelist_property1 = self.env["res.partner"].create( + { + "name": "Partner Pricelist Property 1", + "property_product_pricelist": self.pricelist_property1.id, + } + ) + self.room_type_property2 = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.pms_property2.id], + "name": "Double Property 2", + "default_code": "DBL_P2", + "class_id": self.room_type_class1.id, + "list_price": 25, + } + ) + self.env["pms.room"].create( + { + "pms_property_id": self.pms_property2.id, + "name": "Double 201", + "room_type_id": self.room_type_property2.id, + "capacity": 2, + } + ) diff --git a/pms/tests/test_pms_booking_engine.py b/pms/tests/test_pms_booking_engine.py index d3250803c9..36b4ca6a01 100644 --- a/pms/tests/test_pms_booking_engine.py +++ b/pms/tests/test_pms_booking_engine.py @@ -839,6 +839,31 @@ def _test_board_service_discount(self): "The discount is not correctly established", ) + def test_partner_pricelist_from_another_property(self): + """ + Check that the booking engine does not propose the partner + pricelist when that pricelist is restricted to another property. + ------------- + Open the booking engine in pms_property2 for a partner whose + pricelist only belongs to pms_property1. The proposed pricelist + must be the default pricelist of pms_property2. + """ + self.create_pricelist_by_property_scenario() + booking_engine = self.env["pms.booking.engine"].create( + { + "start_date": fields.date.today(), + "end_date": fields.date.today() + datetime.timedelta(days=1), + "partner_id": self.partner_pricelist_property1.id, + "pms_property_id": self.pms_property2.id, + "channel_type_id": self.sale_channel_direct1.id, + } + ) + self.assertEqual( + booking_engine.pricelist_id, + self.pricelist_property2, + "The booking engine proposed a pricelist of another property", + ) + def _test_adding_board_services_are_saved_on_lines(self): checkin = fields.date.today() checkout = fields.date.today() + datetime.timedelta(days=1) diff --git a/pms/tests/test_pms_folio.py b/pms/tests/test_pms_folio.py index cf3a35c296..3a5d17f987 100644 --- a/pms/tests/test_pms_folio.py +++ b/pms/tests/test_pms_folio.py @@ -1550,3 +1550,59 @@ def test_pms_folio_priority_fiscal_position_property(self): "The fiscal_position_id of the folio was not set " "correctly from the property", ) + + def test_folio_partner_pricelist_from_another_property(self): + """ + Check that a new folio does not take the partner pricelist + when that pricelist is restricted to another property. + ------------- + Create a folio without reservations in pms_property2 for a + partner whose pricelist only belongs to pms_property1. The + folio must be created with the default pricelist of + pms_property2 instead of failing on the property check. + """ + self.create_pricelist_by_property_scenario() + folio = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property2.id, + "partner_id": self.partner_pricelist_property1.id, + } + ) + self.assertEqual( + folio.pricelist_id, + self.pricelist_property2, + "The folio took a partner pricelist of another property", + ) + + def test_folio_partner_pricelist_from_same_property(self): + """ + Check that a new folio still takes the partner pricelist + when that pricelist belongs to the folio property. + """ + self.create_pricelist_by_property_scenario() + folio = self.env["pms.folio"].create( + { + "pms_property_id": self.pms_property1.id, + "partner_id": self.partner_pricelist_property1.id, + } + ) + self.assertEqual( + folio.pricelist_id, + self.pricelist_property1, + "The folio did not take the partner pricelist", + ) + + def test_folio_form_partner_pricelist_before_property(self): + """ + Check that the folio form keeps the partner pricelist restricted + to a property when the partner is chosen before that property. + """ + self.create_pricelist_by_property_scenario() + folio_form = Form(self.env["pms.folio"]) + folio_form.partner_id = self.partner_pricelist_property1 + folio_form.pms_property_id = self.pms_property1 + self.assertEqual( + folio_form.pricelist_id, + self.pricelist_property1, + "The folio form lost the partner pricelist", + ) diff --git a/pms/tests/test_pms_reservation.py b/pms/tests/test_pms_reservation.py index 247d92574c..cf18101423 100644 --- a/pms/tests/test_pms_reservation.py +++ b/pms/tests/test_pms_reservation.py @@ -1206,6 +1206,86 @@ def test_agency_pricelist(self): "Rervation pricelist doesn't match with Agency pricelist", ) + def test_partner_pricelist_from_another_property(self): + """ + Check that a reservation does not take the partner pricelist + when that pricelist is restricted to another property. + ------------- + Create a reservation without pricelist in pms_property2 for a + partner whose pricelist only belongs to pms_property1. The + reservation and its folio must take the default pricelist of + pms_property2 instead of failing on the property check. + """ + self.create_pricelist_by_property_scenario() + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property2.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "partner_id": self.partner_pricelist_property1.id, + "room_type_id": self.room_type_property2.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertEqual( + reservation.pricelist_id, + self.pricelist_property2, + "The reservation took a partner pricelist of another property", + ) + self.assertEqual( + reservation.folio_id.pricelist_id, + self.pricelist_property2, + "The folio took a partner pricelist of another property", + ) + + def test_agency_pricelist_from_another_property(self): + """ + Check that a reservation does not take the agency pricelist + when that pricelist is restricted to another property. + ------------- + Create a reservation without partner in pms_property2 for an + agency that applies its pricelist, which only belongs to + pms_property1. The reservation and its folio must take the + default pricelist of pms_property2 instead of failing on the + property check. + """ + self.create_pricelist_by_property_scenario() + sale_channel1 = self.env["pms.sale.channel"].create( + { + "name": "Test Indirect", + "channel_type": "indirect", + } + ) + agency = self.env["res.partner"].create( + { + "firstname": "partner1", + "is_agency": True, + "sale_channel_id": sale_channel1.id, + "apply_pricelist": True, + "property_product_pricelist": self.pricelist_property1.id, + } + ) + reservation = self.env["pms.reservation"].create( + { + "pms_property_id": self.pms_property2.id, + "checkin": fields.date.today() + datetime.timedelta(days=150), + "checkout": fields.date.today() + datetime.timedelta(days=152), + "agency_id": agency.id, + "room_type_id": self.room_type_property2.id, + "sale_channel_origin_id": self.sale_channel_direct.id, + } + ) + self.assertEqual( + reservation.pricelist_id, + self.pricelist_property2, + "The reservation took an agency pricelist of another property", + ) + self.assertEqual( + reservation.folio_id.pricelist_id, + self.pricelist_property2, + "The folio took an agency pricelist of another property", + ) + def test_compute_access_url(self): """ Check that the access_url field of the reservation is diff --git a/pms/wizards/pms_booking_engine.py b/pms/wizards/pms_booking_engine.py index ec20f0b497..090b2c432e 100644 --- a/pms/wizards/pms_booking_engine.py +++ b/pms/wizards/pms_booking_engine.py @@ -131,8 +131,10 @@ def _compute_pricelist_id(self): for record in self: record.pricelist_id = ( record.partner_id.property_product_pricelist.id - if record.partner_id.property_product_pricelist.is_pms_available - else self.pms_property_id.default_pricelist_id.id + if record.partner_id.property_product_pricelist._is_pms_usable( + record.pms_property_id + ) + else record.pms_property_id.default_pricelist_id.id ) @api.depends("agency_id")