diff --git a/pms/__manifest__.py b/pms/__manifest__.py index f52468b5e..809785251 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -67,6 +67,7 @@ "wizards/wizard_reservation.xml", "wizards/wizard_massive_changes.xml", "wizards/wizard_advanced_filters.xml", + "wizards/wizard_folio.xml", ], "demo": [ "demo/pms_master_data.xml", diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index f152291b6..17b06ae08 100644 --- a/pms/security/ir.model.access.csv +++ b/pms/security/ir.model.access.csv @@ -50,3 +50,6 @@ manager_access_pms_sale_channel,manager_access_pms_sale_channel,model_pms_sale_c user_access_pms_reservation_wizard,user_access_pms_reservation_wizard,model_pms_reservation_wizard,pms.group_pms_user,1,1,1,1 user_access_pms_massive_changes_wizard,user_access_pms_massive_changes_wizard,model_pms_massive_changes_wizard,pms.group_pms_user,1,1,1,1 user_access_pms_advanced_filters_wizard,user_access_pms_advanced_filters_wizard,model_pms_advanced_filters_wizard,pms.group_pms_user,1,1,1,1 +user_access_pms_folio_wizard,user_access_pms_folio_wizard,model_pms_folio_wizard,pms.group_pms_user,1,1,1,1 +user_access_pms_folio_availability_wizard,user_access_pms_folio_availability_wizard,model_pms_folio_availability_wizard,pms.group_pms_user,1,1,1,1 +user_access_pms_num_rooms_selection,user_access_pms_num_rooms_selection,model_pms_num_rooms_selection,pms.group_pms_user,1,1,1,1 diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index 4b81ec9a1..6e71f7c33 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -28,3 +28,4 @@ from . import test_pms_folio from . import test_pms_room_type_availability_rules from . import test_pms_room_type from . import test_pms_wizard_massive_changes +from . import test_pms_wizard_folio diff --git a/pms/tests/test_pms_wizard_folio.py b/pms/tests/test_pms_wizard_folio.py new file mode 100644 index 000000000..d331503a3 --- /dev/null +++ b/pms/tests/test_pms_wizard_folio.py @@ -0,0 +1,662 @@ +# import datetime +# from freezegun import freeze_time +# +import datetime + +from freezegun import freeze_time + +from odoo import fields + +from .common import TestHotel + + +@freeze_time("1980-12-01") +class TestPmsWizardMassiveChanges(TestHotel): + def create_common_scenario(self): + # PRICELIST CREATION + self.test_pricelist = self.env["product.pricelist"].create( + { + "name": "test pricelist 1", + } + ) + self.test_pricelist.flush() + + # AVAILABILITY PLAN CREATION + self.test_availability_plan = self.env[ + "pms.room.type.availability.plan" + ].create( + { + "name": "Availability plan for TEST", + "pms_pricelist_ids": [(6, 0, [self.test_pricelist.id])], + } + ) + self.test_availability_plan.flush() + + # PROPERTY CREATION (WITH DEFAULT PRICELIST AND AVAILABILITY PLAN + self.test_property = self.env["pms.property"].create( + { + "name": "MY PMS TEST", + "company_id": self.env.ref("base.main_company").id, + "default_pricelist_id": self.test_pricelist.id, + "default_availability_plan_id": self.test_availability_plan.id, + } + ) + self.test_property.flush() + + # CREATION OF ROOM TYPE CLASS + self.test_room_type_class = self.env["pms.room.type.class"].create( + {"name": "Room"} + ) + self.test_room_type_class.flush() + + # CREATION OF ROOM TYPE (WITH ROOM TYPE CLASS) + self.test_room_type_single = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.test_property.id], + "name": "Single Test", + "code_type": "SNG_Test", + "class_id": self.test_room_type_class.id, + "list_price": 25.0, + } + ) + self.test_room_type_single.flush() + + # CREATION OF ROOM TYPE (WITH ROOM TYPE CLASS) + self.test_room_type_double = self.env["pms.room.type"].create( + { + "pms_property_ids": [self.test_property.id], + "name": "Double Test", + "code_type": "DBL_Test", + "class_id": self.test_room_type_class.id, + "list_price": 40.0, + } + ) + self.test_room_type_double.flush() + + # pms.room + self.test_room1_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 201 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + self.test_room1_double.flush() + + # pms.room + self.test_room2_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 202 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + self.test_room2_double.flush() + + # pms.room + self.test_room3_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 203 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + self.test_room3_double.flush() + + # pms.room + self.test_room4_double = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Double 204 test", + "room_type_id": self.test_room_type_double.id, + "capacity": 2, + } + ) + self.test_room4_double.flush() + + # pms.room + self.test_room1_single = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Single 101 test", + "room_type_id": self.test_room_type_single.id, + "capacity": 1, + } + ) + self.test_room1_single.flush() + + # pms.room + self.test_room2_single = self.env["pms.room"].create( + { + "pms_property_id": self.test_property.id, + "name": "Single 102 test", + "room_type_id": self.test_room_type_single.id, + "capacity": 1, + } + ) + self.test_room2_single.flush() + + # res.partner + self.partner_id = self.env["res.partner"].create( + { + "name": "Miguel", + "phone": "654667733", + "email": "miguel@example.com", + } + ) + self.partner_id.flush() + + def test_price_wizard_correct(self): + # TEST CASE + # Set values for the wizard and the total price is correct + # Also check the discount is correctly applied to get + # the total folio price + # (no pricelist applied) + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + days = (checkout - checkin).days + num_double_rooms = 4 + discounts = [ + { + "discount": 0, + "expected_price": days + * self.test_room_type_double.list_price + * num_double_rooms, + }, + { + "discount": 0.5, + "expected_price": ( + days * self.test_room_type_double.list_price * num_double_rooms + ) + * 0.5, + }, + ] + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + } + ) + + # force pricelist load + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + + # set value for room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", num_double_rooms), + ] + ) + + lines_availability_test[0].num_rooms_selected = value + + for discount in discounts: + with self.subTest(k=discount): + # ACT + wizard_folio.discount = discount["discount"] + + # ASSERT + self.assertEqual( + wizard_folio.total_price_folio, + discount["expected_price"], + "The total price calculation is wrong", + ) + + def test_price_wizard_correct_pricelist_applied(self): + # TEST CASE + # Set values for the wizard and the total price is correct + # (pricelist applied) + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + days = (checkout - checkin).days + + # num. rooms of type double to book + num_double_rooms = 4 + + # price for today + price_today = 38.0 + + # expected price + expected_price_total = days * price_today * num_double_rooms + + # convert dates to datetimes + dates = self.env["pms.folio.wizard"].get_datetime_from_start_end(checkin) + + # set pricelist item for current day + product_tmpl_id = self.test_room_type_double.product_id.product_tmpl_id.id + pricelist_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.test_pricelist.id, + "date_start": dates[0], + "date_end": dates[1], + "compute_price": "fixed", + "applied_on": "1_product", + "product_tmpl_id": product_tmpl_id, + "fixed_price": price_today, + "min_quantity": 0, + } + ) + pricelist_item.flush() + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + + # set value for room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", num_double_rooms), + ] + ) + + # ACT + lines_availability_test[0].num_rooms_selected = value + + # ASSERT + self.assertEqual( + wizard_folio.total_price_folio, + expected_price_total, + "The total price calculation is wrong", + ) + + def test_price_wizard_correct_pricelist_applied_min_qty_applied(self): + # TEST CASE + # Set values for the wizard and the total price is correct + # (pricelist applied) + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + days = (checkout - checkin).days + + # convert dates to datetimes + dates = self.env["pms.folio.wizard"].get_datetime_from_start_end(checkin) + + # set pricelist item for current day + product_tmpl_id = self.test_room_type_double.product_id.product_tmpl_id.id + pricelist_item = self.env["product.pricelist.item"].create( + { + "pricelist_id": self.test_pricelist.id, + "date_start": dates[0], + "date_end": dates[1], + "compute_price": "fixed", + "applied_on": "1_product", + "product_tmpl_id": product_tmpl_id, + "fixed_price": 38.0, + "min_quantity": 4, + } + ) + pricelist_item.flush() + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + + test_cases = [ + { + "num_rooms": 3, + "expected_price": 3 * self.test_room_type_double.list_price * days, + }, + {"num_rooms": 4, "expected_price": 4 * pricelist_item.fixed_price * days}, + ] + for tc in test_cases: + with self.subTest(k=tc): + # ARRANGE + # set value for room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", tc["num_rooms"]), + ] + ) + # ACT + lines_availability_test[0].num_rooms_selected = value + + # ASSERT + self.assertEqual( + wizard_folio.total_price_folio, + tc["expected_price"], + "The total price calculation is wrong", + ) + + def test_check_create_folio(self): + # TEST CASE + # Set values for the wizard check that folio is created + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + + # ACT + wizard_folio.create_folio() + + # ASSERT + folio = self.env["pms.folio"].search_count( + [("partner_id", "=", self.partner_id.id)] + ) + + self.assertTrue(folio, "Folio not created.") + + def test_check_create_reservations(self): + # TEST CASE + # Set values for the wizard check that reservations are created + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", 2), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 2 + lines_availability_test.flush() + wizard_folio.flush() + + # ACT + wizard_folio.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + folio.flush() + + # ASSERT + self.assertEqual(len(folio.reservation_ids), 2, "Reservations not created.") + + def test_values_folio_created(self): + # TEST CASE + # Set values for the wizard and values of folio are correct + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + wizard_folio.create_folio() + vals = { + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + # ASSERT + for key in vals: + with self.subTest(k=key): + self.assertEqual( + folio[key].id, + vals[key], + "The value of " + key + " is not correctly established", + ) + + def test_values_reservation_created(self): + # TEST CASE + # Set values for the wizard and values of reservations are correct + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + wizard_folio.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + vals = { + "folio_id": folio.id, + "checkin": checkin, + "checkout": checkout, + "room_type_id": self.test_room_type_double, + "partner_id": self.partner_id.id, + "pricelist_id": folio.pricelist_id.id, + } + + # ASSERT + for reservation in folio.reservation_ids: + for key in vals: + with self.subTest(k=key): + self.assertEqual( + reservation[key].id + if key in ["folio_id", "partner_id", "pricelist_id"] + else reservation[key], + vals[key], + "The value of " + key + " is not correctly established", + ) + + def test_reservation_line_discounts(self): + # TEST CASE + # set a discount and its applied to the reservation line + + # ARRANGE + # common scenario + self.create_common_scenario() + + # checkin & checkout + checkin = fields.date.today() + checkout = fields.date.today() + datetime.timedelta(days=1) + discount = 0.5 + + # create folio wizard with partner id => pricelist & start-end dates + wizard_folio = self.env["pms.folio.wizard"].create( + { + "start_date": checkin, + "end_date": checkout, + "partner_id": self.partner_id.id, + "pricelist_id": self.test_pricelist.id, + "discount": discount, + } + ) + wizard_folio.flush() + wizard_folio.availability_results._compute_dynamic_selection() + + # availability items belonging to test property + lines_availability_test = self.env["pms.folio.availability.wizard"].search( + [ + ("room_type_id.pms_property_ids", "in", self.test_property.id), + ] + ) + # set one room type double + value = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", str(self.test_room_type_double.id)), + ("value", "=", 1), + ] + ) + lines_availability_test[0].num_rooms_selected = value + lines_availability_test[0].value_num_rooms_selected = 1 + + # ACT + wizard_folio.create_folio() + + folio = self.env["pms.folio"].search([("partner_id", "=", self.partner_id.id)]) + + # ASSERT + for reservation in folio.reservation_ids: + for line in reservation.reservation_line_ids: + with self.subTest(k=line): + self.assertEqual( + line.discount, + discount * 100, + "The discount is not correctly established", + ) diff --git a/pms/wizards/__init__.py b/pms/wizards/__init__.py index eb093ebce..e637b53e9 100644 --- a/pms/wizards/__init__.py +++ b/pms/wizards/__init__.py @@ -1,3 +1,5 @@ from . import wizard_reservation from . import wizard_massive_changes from . import wizard_advanced_filters +from . import wizard_folio +from . import wizard_folio_availability diff --git a/pms/wizards/wizard_folio.py b/pms/wizards/wizard_folio.py new file mode 100644 index 000000000..0db382726 --- /dev/null +++ b/pms/wizards/wizard_folio.py @@ -0,0 +1,214 @@ +import datetime + +import pytz + +from odoo import api, fields, models + + +class FolioWizard(models.TransientModel): + _name = "pms.folio.wizard" + _description = ( + "Wizard to check availability by room type and pricelist &" + " creation of folios with its reservations" + ) + # Fields declaration + start_date = fields.Date( + string="From:", + required=True, + ) + end_date = fields.Date( + string="To:", + required=True, + ) + pricelist_id = fields.Many2one( + comodel_name="product.pricelist", + string="Pricelist", + compute="_compute_pricelist_id", + store=True, + readonly=False, + ) + partner_id = fields.Many2one( + "res.partner", + ) + availability_results = fields.One2many( + comodel_name="pms.folio.availability.wizard", + inverse_name="folio_wizard_id", + compute="_compute_availability_results", + store=True, + readonly=False, + ) + total_price_folio = fields.Float( + string="Total Price", compute="_compute_total_price_folio" + ) + discount = fields.Float( + string="Discount", + default=0, + ) + can_create_folio = fields.Boolean(compute="_compute_can_create_folio") + + @api.depends("availability_results.value_num_rooms_selected") + def _compute_can_create_folio(self): + for record in self: + record.can_create_folio = any( + record.availability_results.mapped("value_num_rooms_selected") + ) + + @api.depends("partner_id") + def _compute_pricelist_id(self): + for record in self: + record.pricelist_id = record.partner_id.property_product_pricelist.id + + @api.depends("availability_results.price_total", "discount") + def _compute_total_price_folio(self): + for record in self: + record.total_price_folio = 0 + for line in record.availability_results: + record.total_price_folio += line.price_total + record.total_price_folio = record.total_price_folio * (1 - record.discount) + + @api.depends( + "start_date", + "end_date", + "pricelist_id", + ) + def _compute_availability_results(self): + + for record in self: + record.availability_results = False + + if record.start_date and record.end_date and record.pricelist_id: + if record.end_date == record.start_date: + record.end_date = record.end_date + datetime.timedelta(days=1) + + cmds = [(5, 0, 0)] + + for room_type_iterator in self.env["pms.room.type"].search([]): + + num_rooms_available_by_date = [] + room_type_total_price_per_room = 0 + + for date_iterator in [ + record.start_date + datetime.timedelta(days=x) + for x in range(0, (record.end_date - record.start_date).days) + ]: + rooms_available = self.env[ + "pms.room.type.availability.plan" + ].rooms_available( + date_iterator, + date_iterator + datetime.timedelta(days=1), + room_type_id=room_type_iterator.id, + pricelist=record.pricelist_id.id, + ) + + num_rooms_available_by_date.append(len(rooms_available)) + datetimes = self.get_datetime_from_start_end(date_iterator) + + pricelist_item = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", record.pricelist_id.id), + ("date_start", ">=", datetimes[0]), + ("date_end", "<=", datetimes[1]), + ("applied_on", "=", "1_product"), + ( + "product_tmpl_id", + "=", + room_type_iterator.product_id.product_tmpl_id.id, + ), + ] + ) + + # if pricelist_item exists for the date and without + # min_quantity (min_quantity = 0) + if pricelist_item and pricelist_item.min_quantity == 0: + pricelist_item.ensure_one() + room_type_total_price_per_room += float( + pricelist_item.price[2:] + ) + else: + # default price from room_type + room_type_total_price_per_room += ( + room_type_iterator.product_id.list_price + ) + + # check there are rooms of the type + if room_type_iterator.total_rooms_count > 0: + + # get min availability between start date & end date + num_rooms_available = min(num_rooms_available_by_date) + + cmds.append( + ( + 0, + 0, + { + "folio_wizard_id": record.id, + "checkin": record.start_date, + "checkout": record.end_date, + "room_type_id": room_type_iterator.id, + "num_rooms_available": num_rooms_available, + "price_per_room": room_type_total_price_per_room + if num_rooms_available + > 0 # not showing price if there's no availability + else 0, + }, + ) + ) + # remove old items + old_lines = record.availability_results.mapped("id") + for old_line in old_lines: + cmds.append((2, old_line)) + + record.availability_results = cmds + + + record.availability_results = record.availability_results.sorted( + key=lambda s: s.num_rooms_available, reverse=True + ) + + @api.model + def get_datetime_from_start_end(self, date): + tz = "Europe/Madrid" + dt_from = datetime.datetime.combine( + date, + datetime.time.min, + ) + dt_to = datetime.datetime.combine( + date, + datetime.time.max, + ) + dt_from = pytz.timezone(tz).localize(dt_from) + dt_to = pytz.timezone(tz).localize(dt_to) + + dt_from = dt_from.astimezone(pytz.utc) + dt_to = dt_to.astimezone(pytz.utc) + + dt_from = dt_from.replace(tzinfo=None) + dt_to = dt_to.replace(tzinfo=None) + return dt_from, dt_to + + # actions + def create_folio(self): + for record in self: + folio = self.env["pms.folio"].create( + { + "pricelist_id": record.pricelist_id.id, + "partner_id": record.partner_id.id, + } + ) + for line in record.availability_results: + for _reservations_to_create in range(0, line.value_num_rooms_selected): + res = self.env["pms.reservation"].create( + { + "folio_id": folio.id, + "checkin": line.checkin, + "checkout": line.checkout, + "room_type_id": line.room_type_id.id, + "partner_id": folio.partner_id.id, + "pricelist_id": folio.pricelist_id.id, + } + ) + res.reservation_line_ids.discount = record.discount * 100 + action = self.env.ref("pms.open_pms_folio1_form_tree_all").read()[0] + action["views"] = [(self.env.ref("pms.pms_folio_view_form").id, "form")] + action["res_id"] = folio.id + return action diff --git a/pms/wizards/wizard_folio.xml b/pms/wizards/wizard_folio.xml new file mode 100644 index 000000000..919c98971 --- /dev/null +++ b/pms/wizards/wizard_folio.xml @@ -0,0 +1,137 @@ + + + + Folio Wizard + pms.folio.wizard + +
+
+
+ + + + +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + +
+ + + +
+
+
+
+ +
+
+
+
+ + + Folio creation + ir.actions.act_window + pms.folio.wizard + + form + new + + + +
diff --git a/pms/wizards/wizard_folio_availability.py b/pms/wizards/wizard_folio_availability.py new file mode 100644 index 000000000..39059f863 --- /dev/null +++ b/pms/wizards/wizard_folio_availability.py @@ -0,0 +1,164 @@ +import datetime + +from odoo import api, fields, models + + +class NumRoomsSelectionModel(models.TransientModel): + _name = "pms.num.rooms.selection" + _rec_name = "value" + value = fields.Integer() + room_type_id = fields.Char() + folio_wizard_id = fields.One2many( + comodel_name="pms.folio.availability.wizard", + inverse_name="id", + ) + + +class AvailabilityWizard(models.TransientModel): + _name = "pms.folio.availability.wizard" + + # Fields declarations + folio_wizard_id = fields.Many2one( + comodel_name="pms.folio.wizard", + ) + checkin = fields.Date( + string="From:", + required=True, + ) + checkout = fields.Date( + string="To:", + required=True, + ) + room_type_id = fields.Many2one(comodel_name="pms.room.type") + + num_rooms_available = fields.Integer( + string="Available rooms", + default=0, + ) + price_per_room = fields.Float( + string="Price per room", + default=0, + ) + num_rooms_selected = fields.Many2one( + comodel_name="pms.num.rooms.selection", + inverse_name="folio_wizard_id", + string="Selected rooms", + compute="_compute_dynamic_selection", + store=True, + readonly=False, + domain="[('value', '<=', num_rooms_available), " + "('room_type_id', '=', room_type_id)]", + ) + value_num_rooms_selected = fields.Integer(default=0) + price_total = fields.Float( + string="Total price", default=0, compute="_compute_price_total" + ) + + splitted_availability = fields.Boolean( + compute="_compute_splitted_availability", + store=True, + readonly=False, + ) + + @api.depends("num_rooms_selected", "checkin", "checkout") + def _compute_price_total(self): + for record in self: + record.price_total = 0 + + # this field refresh is just to update it and take into account @ xml + record.value_num_rooms_selected = record.num_rooms_selected.value + + num_rooms_available_by_date = [] + room_type_total_price_per_room = 0 + + for date_iterator in [ + record.checkin + datetime.timedelta(days=x) + for x in range(0, (record.checkout - record.checkin).days) + ]: + rooms_available = self.env[ + "pms.room.type.availability.plan" + ].rooms_available( + date_iterator, + date_iterator + datetime.timedelta(days=1), + room_type_id=record.room_type_id.id, + pricelist=record.folio_wizard_id.pricelist_id.id, + ) + num_rooms_available_by_date.append(len(rooms_available)) + datetimes = self.env["pms.folio.wizard"].get_datetime_from_start_end( + date_iterator + ) + + # get pricelist item + pricelist_item = self.env["product.pricelist.item"].search( + [ + ("pricelist_id", "=", record.folio_wizard_id.pricelist_id.id), + ("date_start", ">=", datetimes[0]), + ("date_end", "<=", datetimes[1]), + ("applied_on", "=", "1_product"), + ( + "product_tmpl_id", + "=", + record.room_type_id.product_id.product_tmpl_id.id, + ), + ] + ) + + # check if applies pricelist item + if ( + pricelist_item + and record.num_rooms_selected.value >= pricelist_item.min_quantity + ): + pricelist_item.ensure_one() + room_type_total_price_per_room += float(pricelist_item.price[2:]) + else: + room_type_total_price_per_room += ( + record.room_type_id.product_id.list_price + ) + + # get the availability for the entire stay (min of all dates) + if num_rooms_available_by_date: + record.num_rooms_available = min(num_rooms_available_by_date) + + # udpate the price per room + record.price_per_room = room_type_total_price_per_room + + # if there's no rooms available + if record.num_rooms_available == 0: + # change the selector num_rooms_availabe to 0 + value_selected = self.env["pms.num.rooms.selection"].search( + [ + ("room_type_id", "=", record.room_type_id.id), + ("value", "=", 0), + ] + ) + if value_selected: + record.num_rooms_selected = value_selected + record.value_num_rooms_selected = 0 + + # change the price per room to 0 + record.price_per_room = 0 + + record.price_total = record.price_per_room * record.num_rooms_selected.value + + def _compute_dynamic_selection(self): + for record in self: + for elem_to_insert in range(0, record.num_rooms_available + 1): + if ( + self.env["pms.num.rooms.selection"].search_count( + [ + ("value", "=", elem_to_insert), + ("room_type_id", "=", record.room_type_id.id), + ] + ) + == 0 + ): + self.env["pms.num.rooms.selection"].create( + { + "value": elem_to_insert, + "room_type_id": record.room_type_id.id, + } + ) + default = self.env["pms.num.rooms.selection"].search( + [("value", "=", 0), ("room_type_id", "=", record.room_type_id.id)] + ) + record.num_rooms_selected = default