diff --git a/pms/README.rst b/pms/README.rst index b82dd55db..0022eefdd 100644 --- a/pms/README.rst +++ b/pms/README.rst @@ -1,44 +1,101 @@ -.. This file is going to be generated by oca-gen-addon-readme. Manual changes will be overwritten. +================================ +PMS (Property Management System) +================================ -HOTEL -===== -This module allows you to manage hotel properties with multi-hotel and multi-company support. +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fvertical--hotel-lightgray.png?logo=github + :target: https://github.com/OCA/vertical-hotel/tree/12.0/pms + :alt: OCA/vertical-hotel +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/vertical-hotel-12-0/vertical-hotel-12-0-pms + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/157/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| -Description ------------- This module is an all-in-one property management system (PMS) focused on medium-sized hotels for managing every aspect of your property's daily operations. You can manage hotel properties with multi-hotel and multi-company support, including your rooms inventory, reservations, check-in, daily reports, board services, rate and restriction plans among other hotel functionalities. +**Table of contents** + +.. contents:: + :local: + Installation ------------- +============ + This module depends on modules ``base``, ``sale_stock``, ``account_payment_return``, ``partner_firstname``, and ``account_cancel``. Ensure yourself to have all them in your addons list. Configuration -------------- -You will find the hotel settings in `Settings > Users & Companies > Hotels > Your Hotel. +============= + +You will find the hotel settings in Settings > Users & Companies > Hotels > Your Hotel. This module required additional configuration for company, accounting, invoicing and user privileges. Usage ------ +===== + To use this module, please, read the complete user guide at https://roomdoo.com. +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + Credits -------- +======= Authors -_______ -- Darío Lodeiros -- Alexandre Díaz -- Jose Luis Algara -- Pablo Quesada +~~~~~~~ -Roadmap -------- -* [ ] +* Dario Lodeiros +* Alexadre Diaz +* Pablo Quesada +* Jose Luis Algara -Do you want to contribute? Please, do not hesitate to contact any of the authors or contributors. \ No newline at end of file +Contributors +~~~~~~~~~~~~ + +* Dario Lodeiros +* Alexandre Díaz +* Pablo Quesada +* Jose Luis Algara + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/vertical-hotel `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/pms/__init__.py b/pms/__init__.py index d6c56a95f..4d7a49b5e 100644 --- a/pms/__init__.py +++ b/pms/__init__.py @@ -1,3 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + from . import models from . import wizard diff --git a/pms/__manifest__.py b/pms/__manifest__.py index 59265b75d..ec2c9291f 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -8,10 +8,11 @@ "development_status": "Beta", "category": "Generic Modules/Property Management System", "website": "https://github.com/hootel/hootel", - "author": "Darío Lodeiros, " - "Alexandre Díaz, " + "author": "Dario Lodeiros, " + "Alexadre Diaz, " + "Pablo Quesada, " "Jose Luis Algara, " - "Pablo Quesada ", + "Odoo Community Association (OCA)", "license": "AGPL-3", "application": True, "installable": True, diff --git a/pms/models/inherited_ir_http.py b/pms/models/inherited_ir_http.py index 82ea93440..1593c1923 100644 --- a/pms/models/inherited_ir_http.py +++ b/pms/models/inherited_ir_http.py @@ -2,28 +2,45 @@ # Copyright 2019 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, _ -from odoo.http import request +from odoo import _, models from odoo.exceptions import MissingError +from odoo.http import request class IrHttp(models.AbstractModel): - _inherit = 'ir.http' + _inherit = "ir.http" def session_info(self): res = super().session_info() user = request.env.user - res.update({ - # current_pms_property should be default_property - "user_pms_properties": { - 'current_pms_property': (user.pms_property_id.id, user.pms_property_id.name), - # TODO: filter all properties based on the current set of active companies - 'allowed_pms_properties': [(property.id, property.name) for property in user.pms_property_ids] + res.update( + { + # current_pms_property should be default_property + "user_pms_properties": { + "current_pms_property": ( + user.pms_property_id.id, + user.pms_property_id.name, + ), + # TODO: filter all properties based on the current set of active companies + "allowed_pms_properties": [ + (property.id, property.name) + for property in user.pms_property_ids + ], }, - "display_switch_pms_property_menu": user.has_group('base.group_multi_company') and len(user.pms_property_ids) > 1, - }) - # TODO: This user context update should be placed in other function ¿? - res['user_context'].update({'allowed_pms_property_ids': [(property.id) for property in user.pms_property_ids]}) + "display_switch_pms_property_menu": user.has_group( + "base.group_multi_company" + ) + and len(user.pms_property_ids) > 1, + } + ) + # TODO: This user context update should be placed in other function ¿? + res["user_context"].update( + { + "allowed_pms_property_ids": [ + (property.id) for property in user.pms_property_ids + ] + } + ) # TODO: update current_company based on current_pms_property # if user.pms_property_id.company_id in user.company_ids: # user.company_id = user.pms_property_id.company_id diff --git a/pms/models/inherited_product_pricelist.py b/pms/models/inherited_product_pricelist.py index 6bbfe054d..1907c380c 100644 --- a/pms/models/inherited_product_pricelist.py +++ b/pms/models/inherited_product_pricelist.py @@ -5,7 +5,7 @@ from odoo.exceptions import ValidationError class ProductPricelist(models.Model): - """ Before creating a 'daily' pricelist, you need to consider the following: + """Before creating a 'daily' pricelist, you need to consider the following: A pricelist marked as daily is used as a daily rate plan for room types and therefore is related only with one property. """ @@ -22,7 +22,6 @@ class ProductPricelist(models.Model): pricelist_type = fields.Selection( [("daily", "Daily Plan")], string="Pricelist Type", default="daily" ) - is_staff = fields.Boolean("Is Staff") # Constraints and onchanges @api.constrains("pricelist_type", "pms_property_ids") diff --git a/pms/models/pms_board_service_room_type.py b/pms/models/pms_board_service_room_type.py index 24cbeb3c1..d70440a09 100644 --- a/pms/models/pms_board_service_room_type.py +++ b/pms/models/pms_board_service_room_type.py @@ -18,8 +18,7 @@ class PmsBoardServiceRoomType(models.Model): for res in self: if res.pricelist_id: name = u"{} ({})".format( - res.pms_board_service_id.name, - res.pricelist_id.name, + res.pms_board_service_id.name, res.pricelist_id.name, ) else: name = u"{} ({})".format(res.pms_board_service_id.name, _("Generic")) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index a5a080a29..d70801cbf 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -569,15 +569,6 @@ class PmsFolio(models.Model): values["team_id"] = self.partner_id.team_id.id self.update(values) - @api.onchange("pricelist_id") - def onchange_pricelist_id(self): - values = { - "reservation_type": self.env["pms.folio"].calcule_reservation_type( - self.pricelist_id.is_staff, self.reservation_type - ) - } - self.update(values) - # Action methods def action_pay(self): diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 5de7e7f91..179955892 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -2,8 +2,6 @@ # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -import re -import time from datetime import timedelta from odoo import _, api, fields, models @@ -92,22 +90,59 @@ class PmsReservation(models.Model): return True # Fields declaration - name = fields.Text("Reservation Description", required=True) + name = fields.Text( + "Reservation Description", + required=True, + compute="_compute_name", + store=True, + readonly=False, + ) room_id = fields.Many2one( - "pms.room", string="Room", track_visibility="onchange", ondelete="restrict" + "pms.room", + string="Room", + track_visibility="onchange", + ondelete="restrict", + compute="_compute_room_id", + store=True, + readonly=False, + domain="[('id', 'in', allowed_room_ids)]", + ) + allowed_room_ids = fields.Many2many( + "pms.room", string="Allowed Rooms", compute="_compute_allowed_room_ids", ) folio_id = fields.Many2one( - "pms.folio", string="Folio", track_visibility="onchange", ondelete="cascade" + "pms.folio", string="Folio", track_visibility="onchange", ondelete="cascade", ) board_service_room_id = fields.Many2one( - "pms.board.service.room.type", string="Board Service" + "pms.board.service.room.type", string="Board Service", ) room_type_id = fields.Many2one( - "pms.room.type", string="Room Type", required=True, track_visibility="onchange" + "pms.room.type", + string="Room Type", + required=True, + track_visibility="onchange", + compute="_compute_room_type_id", + store=True, + readonly=False, + ) + partner_id = fields.Many2one( + "res.partner", + track_visibility="onchange", + ondelete="restrict", + compute="_compute_partner_id", + store=True, + readonly=False, ) - partner_id = fields.Many2one(related="folio_id.partner_id") tour_operator_id = fields.Many2one(related="folio_id.tour_operator_id") - partner_invoice_id = fields.Many2one(related="folio_id.partner_invoice_id") + partner_invoice_id = fields.Many2one( + "res.partner", + string="Invoice Address", + required=True, + help="Invoice address for current sales order.", + compute="_compute_partner_invoice_id", + store=True, + readonly=False, + ) partner_invoice_state_id = fields.Many2one(related="partner_invoice_id.state_id") partner_invoice_country_id = fields.Many2one( related="partner_invoice_id.country_id" @@ -121,10 +156,22 @@ class PmsReservation(models.Model): "pms.property", store=True, readonly=True, related="folio_id.pms_property_id" ) reservation_line_ids = fields.One2many( - "pms.reservation.line", "reservation_id", required=True + "pms.reservation.line", + "reservation_id", + compute="_compute_reservation_line_ids", + store=True, + readonly=False, ) service_ids = fields.One2many("pms.service", "reservation_id") - pricelist_id = fields.Many2one("product.pricelist", related="folio_id.pricelist_id") + pricelist_id = fields.Many2one( + "product.pricelist", + string="Pricelist", + required=True, + ondelete="restrict", + compute="_compute_pricelist_id", + store=True, + readonly=False, + ) # TODO: Warning Mens to update pricelist checkin_partner_ids = fields.One2many("pms.checkin.partner", "reservation_id") parent_reservation = fields.Many2one("pms.reservation", string="Parent Reservation") @@ -154,14 +201,14 @@ class PmsReservation(models.Model): localizator = fields.Char( string="Localizator", compute="_compute_localizator", store=True ) - sequence = fields.Integer(string="Sequence", default=10) - reservation_no = fields.Char("Reservation No", size=64, readonly=True) adults = fields.Integer( "Adults", size=64, - readonly=False, track_visibility="onchange", help="List of adults there in guest list. ", + compute="_compute_adults", + store=True, + readonly=False, ) children = fields.Integer( "Children", @@ -180,10 +227,10 @@ class PmsReservation(models.Model): ("cancelled", "Cancelled"), ], string="Status", - readonly=True, default=lambda *a: "draft", copy=False, track_visibility="onchange", + readonly=True, ) reservation_type = fields.Selection( related="folio_id.reservation_type", default=lambda *a: "normal" @@ -198,8 +245,8 @@ class PmsReservation(models.Model): out_service_description = fields.Text("Cause of out of service") checkin = fields.Date("Check In", required=True, default=_get_default_checkin) checkout = fields.Date("Check Out", required=True, default=_get_default_checkout) - real_checkin = fields.Date("Arrival", required=True, track_visibility="onchange") - real_checkout = fields.Date("Departure", required=True, track_visibility="onchange") + real_checkin = fields.Date("From", required=True, track_visibility="onchange") + real_checkout = fields.Date("To", required=True, track_visibility="onchange") arrival_hour = fields.Char( "Arrival Hour", default=_get_default_arrival_hour, @@ -353,6 +400,138 @@ class PmsReservation(models.Model): ) # Compute and Search methods + @api.depends("checkin", "checkin", "room_type_id") + def _compute_name(self): + for reservation in self: + if ( + reservation.room_type_id + and reservation.checkin + and reservation.checkout + ): + checkin_str = reservation.checkin.strftime(DEFAULT_SERVER_DATE_FORMAT) + checkout_str = reservation.checkout.strftime(DEFAULT_SERVER_DATE_FORMAT) + reservation.name = ( + reservation.room_type_id.name + + ": " + + checkin_str + + " - " + + checkout_str + ) + + @api.depends("adults", "room_type_id") + def _compute_room_id(self): + reservations_no_room = self.filtered_domain([("room_id", "=", False)]) + reservations_with_room = self - reservations_no_room + for reservation in reservations_with_room: + if reservation.adults: + reservation._check_adults() + for reservation in reservations_no_room: + if reservation.room_type_id: + reservation.room_id = reservation._autoassign() + if not reservation.room_id: + raise UserError( + _("%s: No rooms available") % (self.room_type_id.name) + ) + # TODO: Allow with confirmation message to + # change de room if the user change the room_type? + + @api.depends("room_id") + def _compute_room_type_id(self): + for reservation in self: + if reservation.room_id and not reservation.room_type_id: + reservation.room_type_id = reservation.room_id.room_type_id.id + + @api.depends("checkin", "checkout", "overbooking", "state", "room_id") + def _compute_allowed_room_ids(self): + for reservation in self: + if reservation.checkin and reservation.checkout: + if reservation.overbooking or reservation.state in ("cancelled"): + reservation.allowed_room_ids = self.env["pms.room"].search( + [("active", "=", True)] + ) + return + rooms_available = ( + self.env["pms.room.type"].check_availability_room_type( + dfrom=reservation.checkin, + dto=reservation.checkout, + room_type_id=False, # Allow chosen any available room + ) + + self.room_id + ) + if ( + reservation.room_id + and reservation.room_id.id not in rooms_available.ids + ): + room_name = reservation.room_id.name + warning_msg = ( + _( + "You tried to change/confirm \ + reservation with room those already reserved in this \ + reservation period: %s " + ) + % room_name + ) + raise ValidationError(warning_msg) + reservation.allowed_room_ids = rooms_available + + @api.depends("reservation_type") + def _compute_partner_id(self): + for reservation in self: + if reservation.reservation_type == "out": + reservation.partner_id = self.env.user.pms_property_id.partner_id.id + + @api.depends("partner_id") + def _compute_partner_invoice_id(self): + for reservation in self: + if reservation.folio_id and reservation.folio_id.partner_id: + addr = reservation.folio_id.partner_id.address_get(["invoice"]) + else: + addr = reservation.partner_id.address_get(["invoice"]) + reservation.partner_invoice_id = addr["invoice"] + + @api.depends("checkin", "checkout") + def _compute_reservation_line_ids(self): + for reservation in self: + cmds = [] + days_diff = (reservation.checkout - reservation.checkin).days + for i in range(0, days_diff): + idate = fields.Date.from_string(reservation.checkin) + timedelta(days=i) + old_line = reservation.reservation_line_ids.filtered( + lambda r: r.date == idate + ) + if not old_line: + cmds.append((0, False, {"date": idate},)) + reservation.reservation_line_ids -= reservation.reservation_line_ids.filtered_domain( + [ + "|", + ("date", ">", reservation.checkout), + ("date", "<", reservation.checkin), + ] + ) + reservation.reservation_line_ids = cmds + + @api.depends("partner_id") + def _compute_pricelist_id(self): + for reservation in self: + pricelist_id = ( + reservation.partner_id.property_product_pricelist + and reservation.partner_id.property_product_pricelist.id + or self.env.user.pms_property_id.default_pricelist_id.id + ) + if reservation.pricelist_id.id != pricelist_id: + # TODO: Warning change de pricelist? + reservation.pricelist_id = pricelist_id + + @api.depends("room_id") + def _compute_adults(self): + for reservation in self: + if reservation.room_id: + if reservation.adults: + reservation._check_adults() + # TODO: Notification if the room capacity is higher than adults? + else: + reservation.adults = reservation.room_id.capacity + @api.depends("state", "qty_to_invoice", "qty_invoiced") def _compute_invoice_status(self): """ @@ -423,21 +602,13 @@ class PmsReservation(models.Model): def _computed_nights(self): for res in self: if res.checkin and res.checkout: - res.nights = ( - fields.Date.from_string(res.checkout) - - fields.Date.from_string(res.checkin) - ).days + res.nights = (res.checkout - res.checkin).days @api.depends("folio_id", "checkin", "checkout") def _compute_localizator(self): + # TODO: Compute localizator by reservation for record in self: - if record.folio_id: - # TODO: Review Datetime type no char v13 - localizator = re.sub("[^0-9]", "", record.folio_id.name) - # checkout = int(re.sub("[^0-9]", "", record.checkout)) - # checkin = int(re.sub("[^0-9]", "", record.checkin)) - # localizator += str((checkin + checkout) % 99) - record.localizator = localizator + record.localizator = fields.date.today() @api.depends("service_ids.price_total") def _compute_amount_room_services(self): @@ -549,165 +720,25 @@ class PmsReservation(models.Model): if len(record.checkin_partner_ids) > record.adults + record.children: raise models.ValidationError(_("The room already is completed")) - @api.onchange("adults", "room_id") - def onchange_room_id(self): - if self.room_id: - write_vals = {} - extra_bed = self.service_ids.filtered( - lambda r: r.product_id.is_extra_bed is True - ) - if self.room_id.get_capacity(len(extra_bed)) < self.adults: - raise UserError( - _("%s people do not fit in this room! ;)") % (self.adults) - ) - if self.adults == 0: - write_vals.update({"adults": self.room_id.capacity}) - if not self.room_type_id: - write_vals.update({"room_type_id": self.room_id.room_type_id.id}) - self.update(write_vals) + # @api.onchange("checkin_partner_ids") + # def onchange_checkin_partner_ids(self): + # _logger.info("----------ONCHANGE2-----------") + # for record in self: + # if len(record.checkin_partner_ids) > record.adults + record.children: + # raise models.ValidationError(_("The room already is completed")) - @api.onchange("cancelled_reason") - def onchange_cancelled_reason(self): - for record in self: - record._compute_cancelled_discount() - - @api.onchange("partner_id") - def onchange_partner_id(self): - addr = self.partner_id.address_get(["invoice"]) - pricelist = ( - self.partner_id.property_product_pricelist - and self.partner_id.property_product_pricelist.id - or self.env.user.pms_property_id.default_pricelist_id.id - ) - values = { - "pricelist_id": pricelist, - "partner_invoice_id": addr["invoice"], - } - self.update(values) - - @api.onchange("pricelist_id") - def onchange_pricelist_id(self): - values = { - "reservation_type": self.env["pms.folio"].calcule_reservation_type( - self.pricelist_id.is_staff, self.reservation_type - ) - } - self.update(values) - - @api.onchange("reservation_type") - def assign_partner_company_on_out_service(self): - if self.reservation_type == "out": - self.update({"partner_id": self.env.user.company_id.partner_id.id}) - - @api.onchange("checkin_partner_ids") - def onchange_checkin_partner_ids(self): - for record in self: - if len(record.checkin_partner_ids) > record.adults + record.children: - raise models.ValidationError(_("The room already is completed")) - - @api.onchange("room_type_id", "pricelist_id", "reservation_type") - def onchange_overwrite_price_by_day(self): - """ - We need to overwrite the prices even if they were already established - """ - if self.room_type_id and self.checkin and self.checkout: - days_diff = ( - fields.Date.from_string(self.checkout) - - fields.Date.from_string(self.checkin) - ).days - self.update( - self.prepare_reservation_lines( - self.checkin, - days_diff, - self.pricelist_id.id, - update_old_prices=True, - ) - ) - - @api.onchange("checkin", "checkout") - def onchange_dates(self): - """ - We need to update prices respecting those that were already established - """ - if not self.checkin: - self.checkin = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - if not self.checkout: - self.checkout = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) - checkin_dt = fields.Date.from_string(self.checkin) - checkout_dt = fields.Date.from_string(self.checkout) - if checkin_dt >= checkout_dt: - self.checkout = ( - fields.Date.from_string(self.checkin) + timedelta(days=1) - ).strftime(DEFAULT_SERVER_DATE_FORMAT) - if self.room_type_id: - days_diff = ( - fields.Date.from_string(self.checkout) - - fields.Date.from_string(self.checkin) - ).days - self.update( - self.prepare_reservation_lines( - self.checkin, - days_diff, - self.pricelist_id.id, - update_old_prices=False, - ) - ) - - @api.onchange("checkin", "checkout", "room_type_id") - def onchange_room_type_id(self): - """ - When change de room_type_id, we calc the line description and tax_ids - """ - if self.room_type_id and self.checkin and self.checkout: - checkin_dt = fields.Date.from_string(self.checkin) - checkout_dt = fields.Date.from_string(self.checkout) - checkin_str = checkin_dt.strftime("%d/%m/%Y") - checkout_str = checkout_dt.strftime("%d/%m/%Y") - self.name = ( - self.room_type_id.name + ": " + checkin_str + " - " + checkout_str - ) - self._compute_tax_ids() + # self._compute_tax_ids() TODO: refact @api.onchange("checkin", "checkout") def onchange_update_service_per_day(self): + _logger.info("----------ONCHANGE4-----------") services = self.service_ids.filtered(lambda r: r.per_day is True) for service in services: service.onchange_product_id() - @api.onchange("checkin", "checkout", "room_id") - def onchange_room_availabiltiy_domain(self): - self.ensure_one() - if self.checkin and self.checkout: - if self.overbooking or self.reselling or self.state in ("cancelled"): - return - occupied = self.env["pms.reservation"].get_reservations( - self.checkin, - (fields.Date.from_string(self.checkout) - timedelta(days=1)).strftime( - DEFAULT_SERVER_DATE_FORMAT - ), - ) - rooms_occupied = occupied.mapped("room_id.id") - if self.room_id: - occupied = occupied.filtered( - lambda r: r.room_id.id == self.room_id.id - and r.id != self._origin.id - ) - if occupied: - occupied_name = ", ".join(str(x.folio_id.name) for x in occupied) - warning_msg = ( - _( - "You tried to change/confirm \ - reservation with room those already reserved in this \ - reservation period: %s " - ) - % occupied_name - ) - raise ValidationError(warning_msg) - domain_rooms = [("id", "not in", rooms_occupied)] - return {"domain": {"room_id": domain_rooms}} - @api.onchange("board_service_room_id") def onchange_board_service(self): + _logger.info("----------ONCHANGE1-----------") if self.board_service_room_id: board_services = [(5, 0, 0)] for line in self.board_service_room_id.board_service_line_ids: @@ -833,8 +864,6 @@ class PmsReservation(models.Model): @api.model def create(self, vals): - if "room_id" not in vals: - vals.update(self._autoassign(vals)) vals.update(self._prepare_add_missing_fields(vals)) if "folio_id" in vals and "channel_type" not in vals: folio = self.env["pms.folio"].browse(vals["folio_id"]) @@ -858,22 +887,9 @@ class PmsReservation(models.Model): for service in vals["service_ids"]: if service[2]: service[2]["folio_id"] = folio.id - user = self.env["res.users"].browse(self.env.uid) - if user.has_group("pms.group_pms_call"): - vals.update({"to_assign": True, "channel_type": "call"}) vals.update( {"last_updated_res": fields.Datetime.now(),} ) - if self.compute_price_out_vals(vals): - days_diff = ( - fields.Date.from_string(vals["checkout"]) - - fields.Date.from_string(vals["checkin"]) - ).days - vals.update( - self.prepare_reservation_lines( - vals["checkin"], days_diff, vals["pricelist_id"], vals=vals - ) - ) # REVISAR el unlink if ( "checkin" in vals and "checkout" in vals @@ -933,17 +949,6 @@ class PmsReservation(models.Model): record.update( {"service_ids": [(6, 0, record.service_ids.ids)] + board_services} ) - if record.compute_price_out_vals(vals): - pricelist_id = ( - vals["pricelist_id"] - if "pricelist_id" in vals - else record.pricelist_id.id - ) - record.update( - record.prepare_reservation_lines( - checkin, days_diff, pricelist_id, vals=vals - ) - ) # REVIEW unlink if record.compute_qty_service_day(vals): service_days_diff = ( fields.Date.from_string(real_checkout) @@ -967,11 +972,6 @@ class PmsReservation(models.Model): or ("state" in vals and record.state != vals["state"]) ): record.update({"to_send": True}) - user = self.env["res.users"].browse(self.env.uid) - if user.has_group("pms.group_pms_call"): - vals.update( - {"to_assign": True,} - ) record = super(PmsReservation, self).write(vals) return record @@ -1025,17 +1025,11 @@ class PmsReservation(models.Model): def _prepare_add_missing_fields(self, values): """ Deduce missing required fields from the onchange """ res = {} - onchange_fields = ["room_id", "tax_ids", "currency_id", "name", "service_ids"] + onchange_fields = ["tax_ids", "currency_id", "service_ids"] if values.get("room_type_id"): if not values.get("reservation_type"): values["reservation_type"] = "normal" line = self.new(values) - if any(f not in values for f in onchange_fields): - line.onchange_room_id() - line.onchange_room_type_id() - if "pricelist_id" not in values: - line.onchange_partner_id() - onchange_fields.append("pricelist_id") for field in onchange_fields: if field == "service_ids": if self.compute_board_services(values): @@ -1047,26 +1041,17 @@ class PmsReservation(models.Model): res[field] = line._fields[field].convert_to_write(line[field], line) return res - @api.model - def _autoassign(self, values): - res = {} - checkin = values.get("checkin") - checkout = values.get("checkout") - room_type_id = values.get("room_type_id") - if checkin and checkout and room_type_id: - if "overbooking" not in values: - room_chosen = self.env["pms.room.type"].check_availability_room_type( - checkin, - (fields.Date.from_string(checkout) - timedelta(days=1)).strftime( - DEFAULT_SERVER_DATE_FORMAT - ), - room_type_id, - )[0] - # Check room_chosen exist - else: - room_chosen = self.env["pms.room.type"].browse(room_type_id).room_ids[0] - res.update({"room_id": room_chosen.id}) - return res + def _autoassign(self): + self.ensure_one() + room_chosen = False + rooms_available = self.env["pms.room.type"].check_availability_room_type( + dfrom=self.checkin, + dto=self.checkout, + room_type_id=self.room_type_id.id or False, + ) + if rooms_available: + room_chosen = rooms_available[0] + return room_chosen @api.model def autocheckout(self): @@ -1123,10 +1108,6 @@ class PmsReservation(models.Model): "real_checkout": self.real_checkout, } - """ - STATE WORKFLOW ----------------------------------------------------- - """ - def confirm(self): """ @param self: object pointer @@ -1183,7 +1164,7 @@ class PmsReservation(models.Model): if self._context.get("no_penalty", False): _logger.info("Modified Reservation - No Penalty") record.write({"state": "cancelled", "cancelled_reason": cancel_reason}) - record._compute_cancelled_discount() + # record._compute_cancelled_discount() if record.splitted: master_reservation = record.parent_reservation or record splitted_reservs = self.env["pms.reservation"].search( @@ -1237,130 +1218,6 @@ class PmsReservation(models.Model): ) splitted_reservs.draft() - """ - PRICE PROCESS ------------------------------------------------------ - """ - - def compute_price_out_vals(self, vals): - """ - Compute if It is necesary calc price in write/create - """ - if not vals: - vals = {} - if "reservation_line_ids" not in vals and ( - "checkout" in vals - or "checkin" in vals - or "room_type_id" in vals - or "pricelist_id" in vals - ): - return True - return False - - def _compute_cancelled_discount(self): - self.ensure_one() - pricelist = self.pricelist_id - if self.state == "cancelled": - # REVIEW: Set 0 qty on cancel room services - # (view constrain service_line_days) - for service in self.service_ids: - service.service_line_ids.write({"day_qty": 0}) - service._compute_days_qty() - if self.cancelled_reason and pricelist and pricelist.cancelation_rule_id: - date_start_dt = fields.Date.from_string( - self.real_checkin or self.checkin - ) - date_end_dt = fields.Date.from_string( - self.real_checkout or self.checkout - ) - days = abs((date_end_dt - date_start_dt).days) - rule = pricelist.cancelation_rule_id - if self.cancelled_reason == "late": - discount = 100 - rule.penalty_late - if rule.apply_on_late == "first": - days = 1 - elif rule.apply_on_late == "days": - days = rule.days_late - elif self.cancelled_reason == "noshow": - discount = 100 - rule.penalty_noshow - if rule.apply_on_noshow == "first": - days = 1 - elif rule.apply_on_noshow == "days": - days = rule.days_late - 1 - elif self.cancelled_reason == "intime": - discount = 100 - - checkin = self.real_checkin or self.checkin - dates = [] - for i in range(0, days): - dates.append( - (fields.Date.from_string(checkin) + timedelta(days=i)).strftime( - DEFAULT_SERVER_DATE_FORMAT - ) - ) - self.reservation_line_ids.filtered(lambda r: r.date in dates).update( - {"cancel_discount": discount} - ) - self.reservation_line_ids.filtered( - lambda r: r.date not in dates - ).update({"cancel_discount": 100}) - else: - self.reservation_line_ids.update({"cancel_discount": 0}) - else: - self.reservation_line_ids.update({"cancel_discount": 0}) - - @api.model - def prepare_reservation_lines( - self, dfrom, days, pricelist_id, vals=False, update_old_prices=False - ): - discount = 0 - cmds = [(5, 0, 0)] - if not vals: - vals = {} - room_type_id = vals.get("room_type_id") or self.room_type_id.id - product = self.env["pms.room.type"].browse(room_type_id).product_id - partner = self.env["res.partner"].browse( - vals.get("partner_id") or self.partner_id.id - ) - if "discount" in vals and vals.get("discount") > 0: - discount = vals.get("discount") - for i in range(0, days): - idate = (fields.Date.from_string(dfrom) + timedelta(days=i)).strftime( - DEFAULT_SERVER_DATE_FORMAT - ) - old_line = self.reservation_line_ids.filtered(lambda r: r.date == idate) - if update_old_prices or not old_line: - product = product.with_context( - lang=partner.lang, - partner=partner.id, - quantity=1, - date=idate, - pricelist=pricelist_id, - uom=product.uom_id.id, - ) - # REVIEW this forces to have configured the taxes - # included in the price - line_price = product.price - if old_line and old_line.id: - cmds.append( - (1, old_line.id, {"price": line_price, "discount": discount}) - ) - else: - cmds.append( - ( - 0, - False, - {"date": idate, "price": line_price, "discount": discount}, - ) - ) - else: - line_price = old_line.price - cmds.append((4, old_line.id)) - return {"reservation_line_ids": cmds} - - """ - AVAILABILTY PROCESS ------------------------------------------------ - """ - @api.model def get_reservations(self, dfrom, dto): """ @@ -1411,10 +1268,6 @@ class PmsReservation(models.Model): ) return reservations_dates - """ - CHECKIN/OUT PROCESS ------------------------------------------------ - """ - def _compute_checkin_partner_count(self): _logger.info("_compute_checkin_partner_count") for record in self: @@ -1468,10 +1321,6 @@ class PmsReservation(models.Model): action["target"] = "new" return action - """ - RESERVATION SPLITTED ----------------------------------------------- - """ - def split(self, nights): for record in self: date_start_dt = fields.Date.from_string(record.checkin) @@ -1650,10 +1499,6 @@ class PmsReservation(models.Model): action["res_id"] = self.parent_reservation.id return action - """ - MAILING PROCESS - """ - def send_reservation_mail(self): return self.folio_id.send_reservation_mail() @@ -1663,10 +1508,6 @@ class PmsReservation(models.Model): def send_cancel_mail(self): return self.folio_id.send_cancel_mail() - """ - INVOICING PROCESS - """ - def _compute_tax_ids(self): for record in self: # If company_id is set, always filter taxes by the company diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 75a8e7152..2110f7a75 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -45,12 +45,112 @@ class PmsReservationLine(models.Model): ) date = fields.Date("Date") state = fields.Selection(related="reservation_id.state") - price = fields.Float(string="Price", digits=("Product Price")) + price = fields.Float( + string="Price", + digits=("Product Price"), + compute="_compute_price", + store=True, + readonly=False, + ) cancel_discount = fields.Float( - string="Cancel Discount (%)", digits=("Discount"), default=0.0 + string="Cancel Discount (%)", + digits=("Discount"), + default=0.0, + compute="_compute_cancel_discount", + store=True, + readonly=False, ) discount = fields.Float(string="Discount (%)", digits=("Discount"), default=0.0) + # Compute and Search methods + @api.depends( + "date", + "reservation_id.pricelist_id", + "reservation_id.room_type_id", + "reservation_id.reservation_type", + ) + def _compute_price(self): + for line in self: + reservation = line.reservation_id + room_type_id = reservation.room_type_id.id + product = self.env["pms.room.type"].browse(room_type_id).product_id + partner = self.env["res.partner"].browse(reservation.partner_id.id) + product = product.with_context( + lang=partner.lang, + partner=partner.id, + quantity=1, + date=line.date, + pricelist=reservation.pricelist_id.id, + uom=product.uom_id.id, + ) + line.price = self.env["account.tax"]._fix_tax_included_price_company( + line._get_display_price(product), + product.taxes_id, + line.reservation_id.tax_ids, + line.reservation_id.company_id, + ) + # TODO: Out of service 0 amount + + # TODO: Refact method and allowed cancelled single days + @api.depends("reservation_id.cancelled_reason") + def _compute_cancel_discount(self): + for line in self: + line.cancel_discount = 0 + # reservation = line.reservation_id + # pricelist = reservation.pricelist_id + # if reservation.state == "cancelled": + # # TODO: Set 0 qty on cancel room services change to compute day_qty + # # (view constrain service_line_days) + # for service in reservation.service_ids: + # service.service_line_ids.write({"day_qty": 0}) + # service._compute_days_qty() + # if ( + # reservation.cancelled_reason + # and pricelist + # and pricelist.cancelation_rule_id + # ): + # date_start_dt = fields.Date.from_string( + # reservation.real_checkin or reservation.checkin + # ) + # date_end_dt = fields.Date.from_string( + # reservation.real_checkout or reservation.checkout + # ) + # days = abs((date_end_dt - date_start_dt).days) + # rule = pricelist.cancelation_rule_id + # if reservation.cancelled_reason == "late": + # discount = 100 - rule.penalty_late + # if rule.apply_on_late == "first": + # days = 1 + # elif rule.apply_on_late == "days": + # days = rule.days_late + # elif reservation.cancelled_reason == "noshow": + # discount = 100 - rule.penalty_noshow + # if rule.apply_on_noshow == "first": + # days = 1 + # elif rule.apply_on_noshow == "days": + # days = rule.days_late - 1 + # elif reservation.cancelled_reason == "intime": + # discount = 100 + + # checkin = reservation.real_checkin or reservation.checkin + # dates = [] + # for i in range(0, days): + # dates.append( + # ( + # fields.Date.from_string(checkin) + timedelta(days=i) + # ).strftime(DEFAULT_SERVER_DATE_FORMAT) + # ) + # reservation.reservation_line_ids.filtered( + # lambda r: r.date in dates + # ).update({"cancel_discount": discount}) + # reservation.reservation_line_ids.filtered( + # lambda r: r.date not in dates + # ).update({"cancel_discount": 100}) + # else: + # reservation.reservation_line_ids.update({"cancel_discount": 0}) + # else: + # reservation.reservation_line_ids.update({"cancel_discount": 0}) + # Constraints and onchanges @api.constrains("date") def constrains_duplicated_date(self): @@ -71,3 +171,33 @@ class PmsReservationLine(models.Model): lambda r: r.date == record.date ) cancel_lines.day_qty = 0 + + def _get_display_price(self, product): + if self.reservation_id.pricelist_id.discount_policy == "with_discount": + return product.with_context( + pricelist=self.reservation_id.pricelist_id.id + ).price + product_context = dict( + self.env.context, + partner_id=self.reservation_id.partner_id.id, + date=self.date, + uom=product.uom_id.id, + ) + + final_price, rule_id = self.reservation_id.pricelist_id.with_context( + product_context + ).get_product_price_rule(product, 1.0, self.reservation_id.partner_id) + base_price, currency = self.with_context( + product_context + )._get_real_price_currency( + product, rule_id, 1, product.uom_id, self.reservation_id.pricelist_id.id + ) + if currency != self.reservation_id.pricelist_id.currency_id: + base_price = currency._convert( + base_price, + self.reservation_id.pricelist_id.currency_id, + self.reservation_id.company_id or self.env.company, + fields.Date.today(), + ) + # negative discounts (= surcharge) are included in the display price + return max(base_price, final_price) diff --git a/pms/models/pms_room.py b/pms/models/pms_room.py index b908e97c3..77ab1e2bf 100644 --- a/pms/models/pms_room.py +++ b/pms/models/pms_room.py @@ -7,10 +7,10 @@ from odoo.exceptions import ValidationError class PmsRoom(models.Model): - """ The rooms for lodging can be for sleeping, usually called rooms, - and also for speeches (conference rooms), parking, - relax with cafe con leche, spa... - """ + """The rooms for lodging can be for sleeping, usually called rooms, + and also for speeches (conference rooms), parking, + relax with cafe con leche, spa... + """ _name = "pms.room" _description = "Property Room" diff --git a/pms/models/pms_room_type.py b/pms/models/pms_room_type.py index de5d263f8..f06b84f05 100644 --- a/pms/models/pms_room_type.py +++ b/pms/models/pms_room_type.py @@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError class PmsRoomType(models.Model): - """ Before creating a 'room type', you need to consider the following: + """Before creating a 'room type', you need to consider the following: With the term 'room type' is meant a sales type of residential accommodation: for example, a Double Room, a Economic Room, an Apartment, a Tent, a Caravan... """ @@ -92,6 +92,7 @@ class PmsRoomType(models.Model): capacities = self.room_ids.mapped("capacity") return min(capacities) if any(capacities) else 0 + # TODO: Change name method by rooms_available() @api.model def check_availability_room_type(self, dfrom, dto, room_type_id=False, notthis=[]): """ @@ -102,7 +103,7 @@ class PmsRoomType(models.Model): reservations_rooms = reservations.mapped("room_id.id") free_rooms = self.env["pms.room"].search( [("id", "not in", reservations_rooms), ("id", "not in", notthis)] - ) + ) # TODO: Review if with the new caché V13 We need notthis []¿? if room_type_id: rooms_linked = ( self.env["pms.room.type"].search([("id", "=", room_type_id)]).room_ids @@ -145,20 +146,21 @@ class PmsRoomType(models.Model): {"partner_id": partner_id if partner_id else False, "discount": discount,} ) rate_vals = {} - for room_type in room_types: - vals.update({"room_type_id": room_type.id}) - room_vals = self.env["pms.reservation"].prepare_reservation_lines( - date_from, - days, - pricelist_id=pricelist_id, - vals=vals, - update_old_prices=False, - ) - rate_vals.update( - { - room_type.id: [ - item[2] for item in room_vals["reservation_line_ids"] if item[2] - ] - } - ) + # TODO: Now it is computed field, We need other way to return rates + # for room_type in room_types: + # vals.update({"room_type_id": room_type.id}) + # room_vals = self.env["pms.reservation"].prepare_reservation_lines( + # date_from, + # days, + # pricelist_id=pricelist_id, + # vals=vals, + # update_old_prices=False, + # ) + # rate_vals.update( + # { + # room_type.id: [ + # item[2] for item in room_vals["reservation_line_ids"] if item[2] + # ] + # } + # ) return rate_vals diff --git a/pms/models/pms_room_type_class.py b/pms/models/pms_room_type_class.py index 3535b8a70..ba448c839 100644 --- a/pms/models/pms_room_type_class.py +++ b/pms/models/pms_room_type_class.py @@ -5,7 +5,7 @@ from odoo import fields, models class PmsRoomTypeClass(models.Model): - """ Before creating a 'room type_class', you need to consider the following: + """Before creating a 'room type_class', you need to consider the following: With the term 'room type class' is meant a physical class of residential accommodation: for example, a Room, a Bed, an Apartment, a Tent, a Caravan... diff --git a/pms/models/pms_room_type_restriction.py b/pms/models/pms_room_type_restriction.py index 42ba993b5..ea27914af 100644 --- a/pms/models/pms_room_type_restriction.py +++ b/pms/models/pms_room_type_restriction.py @@ -4,8 +4,8 @@ from odoo import api, fields, models class PmsRoomTypeRestriction(models.Model): - """ The room type restriction is used as a daily restriction plan for room types - and therefore is related only with one property. """ + """The room type restriction is used as a daily restriction plan for room types + and therefore is related only with one property.""" _name = "pms.room.type.restriction" _description = "Reservation restriction plan" diff --git a/pms/readme/CONFIGURE.rst b/pms/readme/CONFIGURE.rst index c26b4de76..357e6bf8d 100644 --- a/pms/readme/CONFIGURE.rst +++ b/pms/readme/CONFIGURE.rst @@ -1,6 +1,3 @@ -.. [ This file is optional, it should explain how to configure -the module before using it; it is aimed at advanced users. ] - -You will find the hotel settings in `Settings > Users & Companies > Hotels > Your Hotel. +You will find the hotel settings in Settings > Users & Companies > Hotels > Your Hotel. This module required additional configuration for company, accounting, invoicing and user privileges. diff --git a/pms/readme/CONTRIBUTORS.rst b/pms/readme/CONTRIBUTORS.rst index 141a95efb..5316e1c4f 100644 --- a/pms/readme/CONTRIBUTORS.rst +++ b/pms/readme/CONTRIBUTORS.rst @@ -1 +1,4 @@ -.. authors are taken from the __manifest__.py file +* Dario Lodeiros +* Alexandre Díaz +* Pablo Quesada +* Jose Luis Algara diff --git a/pms/readme/CREDITS.rst b/pms/readme/CREDITS.rst deleted file mode 100644 index cbd69e8ad..000000000 --- a/pms/readme/CREDITS.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. [ This file is optional and contains additional credits, other than -authors, contributors, and maintainers. ] diff --git a/pms/readme/DESCRIPTION.rst b/pms/readme/DESCRIPTION.rst index d2b3d8f57..5f6c71a7c 100644 --- a/pms/readme/DESCRIPTION.rst +++ b/pms/readme/DESCRIPTION.rst @@ -1,5 +1,3 @@ -.. [ This file must be max 2-3 paragraphs, and is required. ] - This module is an all-in-one property management system (PMS) focused on medium-sized hotels for managing every aspect of your property's daily operations. diff --git a/pms/readme/INSTALL.rst b/pms/readme/INSTALL.rst index 2e88cc45a..66a8ca6a8 100644 --- a/pms/readme/INSTALL.rst +++ b/pms/readme/INSTALL.rst @@ -1,5 +1,2 @@ -.. [ This file must only be present if there are very specific -installation instructions, such as installing non-python dependencies. The audience is systems administrators. ] - This module depends on modules ``base``, ``sale_stock``, ``account_payment_return``, ``partner_firstname``, and ``account_cancel``. Ensure yourself to have all them in your addons list. diff --git a/pms/readme/ROADMAP.rst b/pms/readme/ROADMAP.rst deleted file mode 100644 index e39c36eaa..000000000 --- a/pms/readme/ROADMAP.rst +++ /dev/null @@ -1,4 +0,0 @@ -.. [ Enumerate known caveats and future potential improvements. -It is mostly intended for end-users, and can also help potential new contributors discovering new features to implement. ] - -- [ ] diff --git a/pms/readme/USAGE.rst b/pms/readme/USAGE.rst index e59fca743..864ca126a 100644 --- a/pms/readme/USAGE.rst +++ b/pms/readme/USAGE.rst @@ -1,6 +1 @@ -.. [ This file must be present and contains the usage instructions -for end-users. As all other rst files included in the README, it MUST NOT contain reStructuredText sections -only body text (paragraphs, lists, tables, etc). Should you need a more elaborate structure to explain the addon, -please create a Sphinx documentation (which may include this file as a "quick start" section). ] - To use this module, please, read the complete user guide at https://roomdoo.com. diff --git a/pms/static/description/index.html b/pms/static/description/index.html new file mode 100644 index 000000000..7083277df --- /dev/null +++ b/pms/static/description/index.html @@ -0,0 +1,445 @@ + + + + + + +PMS (Property Management System) + + + +
+

PMS (Property Management System)

+ + +

Beta License: AGPL-3 OCA/vertical-hotel Translate me on Weblate Try me on Runbot

+

This module is an all-in-one property management system (PMS) focused on medium-sized hotels +for managing every aspect of your property’s daily operations.

+

You can manage hotel properties with multi-hotel and multi-company support, including your rooms inventory, +reservations, check-in, daily reports, board services, rate and restriction plans among other hotel functionalities.

+

Table of contents

+ +
+

Installation

+

This module depends on modules base, sale_stock, account_payment_return, partner_firstname, +and account_cancel. Ensure yourself to have all them in your addons list.

+
+
+

Configuration

+

You will find the hotel settings in Settings > Users & Companies > Hotels > Your Hotel.

+

This module required additional configuration for company, accounting, invoicing and user privileges.

+
+
+

Usage

+

To use this module, please, read the complete user guide at https://roomdoo.com.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Dario Lodeiros
  • +
  • Alexadre Diaz
  • +
  • Pablo Quesada
  • +
  • Jose Luis Algara
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/vertical-hotel project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/pms/static/src/js/inherited_abstract_web_client.js b/pms/static/src/js/inherited_abstract_web_client.js index 312481bd2..9296030b6 100644 --- a/pms/static/src/js/inherited_abstract_web_client.js +++ b/pms/static/src/js/inherited_abstract_web_client.js @@ -1,29 +1,40 @@ -odoo.define('pms.AbstractWebClient', function (require) { +odoo.define("pms.AbstractWebClient", function(require) { "use strict"; - - var AbstractWebClient = require('web.AbstractWebClient'); - var session = require('web.session'); - var utils = require('web.utils'); + var AbstractWebClient = require("web.AbstractWebClient"); + var session = require("web.session"); + var utils = require("web.utils"); return AbstractWebClient.include({ - start: function () { + start: function() { var state = $.bbq.getState(); - var current_pms_property_id = session.user_pms_properties.current_pms_property[0] + var current_pms_property_id = + session.user_pms_properties.current_pms_property[0]; if (!state.pms_pids) { - state.pms_pids = utils.get_cookie('pms_pids') !== null ? utils.get_cookie('pms_pids') : String(current_pms_property_id); + state.pms_pids = + utils.get_cookie("pms_pids") !== null + ? utils.get_cookie("pms_pids") + : String(current_pms_property_id); } - var statePmsPropertyIDS = _.map(state.pms_pids.split(','), function (pms_pid) { return parseInt(pms_pid) }); - var userPmsPropertyIDS = _.map(session.user_pms_properties.allowed_pms_properties, function(pms_property) {return pms_property[0]}); + var statePmsPropertyIDS = _.map(state.pms_pids.split(","), function( + pms_pid + ) { + return parseInt(pms_pid); + }); + var userPmsPropertyIDS = _.map( + session.user_pms_properties.allowed_pms_properties, + function(pms_property) { + return pms_property[0]; + } + ); // Check that the user has access to all the companies if (!_.isEmpty(_.difference(statePmsPropertyIDS, userPmsPropertyIDS))) { state.pms_pids = String(current_pms_property_id); - statePmsPropertyIDS = [current_pms_property_id] + statePmsPropertyIDS = [current_pms_property_id]; } session.user_context.allowed_pms_property_ids = statePmsPropertyIDS; - + return this._super.apply(this, arguments); - }, }); -}); \ No newline at end of file +}); diff --git a/pms/static/src/js/session.js b/pms/static/src/js/session.js index ffa36b060..cbef3fa3b 100644 --- a/pms/static/src/js/session.js +++ b/pms/static/src/js/session.js @@ -1,32 +1,35 @@ -odoo.define('pms.session', function (require) { +odoo.define("pms.session", function(require) { "use strict"; - - var Session = require('web.Session'); - var utils = require('web.utils'); + + var Session = require("web.Session"); + var utils = require("web.utils"); var modules = odoo._modules; var inherited_Session = Session.extend({ // TODO: require test and debug - setPmsProperties: function (pms_main_property_id, pms_property_ids) { - var hash = $.bbq.getState() - hash.pms_pids = pms_property_ids.sort(function(a, b) { - if (a === pms_main_property_id) { - return -1; - } else if (b === pms_main_property_id) { - return 1; - } else { + setPmsProperties: function(pms_main_property_id, pms_property_ids) { + var hash = $.bbq.getState(); + hash.pms_pids = pms_property_ids + .sort(function(a, b) { + if (a === pms_main_property_id) { + return -1; + } else if (b === pms_main_property_id) { + return 1; + } return a - b; - } - }).join(','); - utils.set_cookie('pms_pids', hash.pms_pids || String(pms_main_property_id)); - $.bbq.pushState({'pms_pids': hash.pms_pids}, 0); + }) + .join(","); + utils.set_cookie("pms_pids", hash.pms_pids || String(pms_main_property_id)); + $.bbq.pushState({pms_pids: hash.pms_pids}, 0); location.reload(); }, }); - - var pms_session = new inherited_Session(undefined, undefined, {modules: modules, use_cors: false}); + + var pms_session = new inherited_Session(undefined, undefined, { + modules: modules, + use_cors: false, + }); pms_session.is_bound = pms_session.session_bind(); - + return pms_session; - - }); \ No newline at end of file +}); diff --git a/pms/static/src/js/widgets/switch_property_menu.js b/pms/static/src/js/widgets/switch_property_menu.js index 2d6ab06e3..9a3b12be5 100644 --- a/pms/static/src/js/widgets/switch_property_menu.js +++ b/pms/static/src/js/widgets/switch_property_menu.js @@ -1,126 +1,166 @@ -odoo.define('web.SwitchPmsMenu', function(require) { +odoo.define("web.SwitchPmsMenu", function(require) { "use strict"; - + /** * When Odoo is configured in multi-property mode, users should obviously be able * to switch their interface from one property to the other. This is the purpose * of this widget, by displaying a dropdown menu in the systray. */ - - var config = require('web.config'); - var core = require('web.core'); - var session = require('pms.session'); - var SystrayMenu = require('web.SystrayMenu'); - var Widget = require('web.Widget'); - + + var config = require("web.config"); + var core = require("web.core"); + var session = require("pms.session"); + var SystrayMenu = require("web.SystrayMenu"); + var Widget = require("web.Widget"); + var _t = core._t; - + var SwitchPmsMenu = Widget.extend({ - template: 'SwitchPmsMenu', + template: "SwitchPmsMenu", events: { - 'click .dropdown-item[data-menu] div.pms_log_into': '_onSwitchPmsPropertyClick', - 'keydown .dropdown-item[data-menu] div.pms_log_into': '_onSwitchPmsPropertyClick', - 'click .dropdown-item[data-menu] div.pms_toggle_property': '_onTogglePmsPropertyClick', - 'keydown .dropdown-item[data-menu] div.pms_toggle_property': '_onTogglePmsPropertyClick', + "click .dropdown-item[data-menu] div.pms_log_into": + "_onSwitchPmsPropertyClick", + "keydown .dropdown-item[data-menu] div.pms_log_into": + "_onSwitchPmsPropertyClick", + "click .dropdown-item[data-menu] div.pms_toggle_property": + "_onTogglePmsPropertyClick", + "keydown .dropdown-item[data-menu] div.pms_toggle_property": + "_onTogglePmsPropertyClick", }, /** * @override */ - init: function () { + init: function() { this._super.apply(this, arguments); this.isMobile = config.device.isMobile; - this._onSwitchPmsPropertyClick = _.debounce(this._onSwitchPmsPropertyClick, 1500, true); + this._onSwitchPmsPropertyClick = _.debounce( + this._onSwitchPmsPropertyClick, + 1500, + true + ); }, - + /** * @override */ - willStart: function () { + willStart: function() { var self = this; - this.allowed_pms_property_ids = String(session.user_context.allowed_pms_property_ids) - .split(',') - .map(function (id) {return parseInt(id);}); - this.user_pms_properties = session.user_pms_properties.allowed_pms_properties; + this.allowed_pms_property_ids = String( + session.user_context.allowed_pms_property_ids + ) + .split(",") + .map(function(id) { + return parseInt(id); + }); + this.user_pms_properties = + session.user_pms_properties.allowed_pms_properties; this.current_pms_property = this.allowed_pms_property_ids[0]; - this.current_pms_property_name = _.find(session.user_pms_properties.allowed_pms_properties, function (pms_property) { - return pms_property[0] === self.current_pms_property; - })[1]; + this.current_pms_property_name = _.find( + session.user_pms_properties.allowed_pms_properties, + function(pms_property) { + return pms_property[0] === self.current_pms_property; + } + )[1]; return this._super.apply(this, arguments); }, - - //-------------------------------------------------------------------------- + + // -------------------------------------------------------------------------- // Handlers - //-------------------------------------------------------------------------- - + // -------------------------------------------------------------------------- + /** * @private * @param {MouseEvent|KeyEvent} ev */ - _onSwitchPmsPropertyClick: function (ev) { - if (ev.type == 'keydown' && ev.which != $.ui.keyCode.ENTER && ev.which != $.ui.keyCode.SPACE) { + _onSwitchPmsPropertyClick: function(ev) { + if ( + ev.type == "keydown" && + ev.which != $.ui.keyCode.ENTER && + ev.which != $.ui.keyCode.SPACE + ) { return; } ev.preventDefault(); ev.stopPropagation(); var dropdownItem = $(ev.currentTarget).parent(); var dropdownMenu = dropdownItem.parent(); - var pms_propertyID = dropdownItem.data('pms_property-id'); + var pms_propertyID = dropdownItem.data("pms_property-id"); var allowed_pms_property_ids = this.allowed_pms_property_ids; - if (dropdownItem.find('.fa-square-o').length) { + if (dropdownItem.find(".fa-square-o").length) { // 1 enabled pms_property: Stay in single pms proeprty mode if (this.allowed_pms_property_ids.length === 1) { if (this.isMobile) { dropdownMenu = dropdownMenu.parent(); } - dropdownMenu.find('.fa-check-square').removeClass('fa-check-square').addClass('fa-square-o'); - dropdownItem.find('.fa-square-o').removeClass('fa-square-o').addClass('fa-check-square'); + dropdownMenu + .find(".fa-check-square") + .removeClass("fa-check-square") + .addClass("fa-square-o"); + dropdownItem + .find(".fa-square-o") + .removeClass("fa-square-o") + .addClass("fa-check-square"); allowed_pms_property_ids = [pms_propertyID]; - } else { // Multi pms proeprty mode + } else { + // Multi pms proeprty mode allowed_pms_property_ids.push(pms_propertyID); - dropdownItem.find('.fa-square-o').removeClass('fa-square-o').addClass('fa-check-square'); + dropdownItem + .find(".fa-square-o") + .removeClass("fa-square-o") + .addClass("fa-check-square"); } } - $(ev.currentTarget).attr('aria-pressed', 'true'); + $(ev.currentTarget).attr("aria-pressed", "true"); session.setPmsProperties(pms_propertyID, allowed_pms_property_ids); }, - - //-------------------------------------------------------------------------- + + // -------------------------------------------------------------------------- // Handlers - //-------------------------------------------------------------------------- - + // -------------------------------------------------------------------------- + /** * @private * @param {MouseEvent|KeyEvent} ev */ - _onTogglePmsPropertyClick: function (ev) { - if (ev.type == 'keydown' && ev.which != $.ui.keyCode.ENTER && ev.which != $.ui.keyCode.SPACE) { + _onTogglePmsPropertyClick: function(ev) { + if ( + ev.type == "keydown" && + ev.which != $.ui.keyCode.ENTER && + ev.which != $.ui.keyCode.SPACE + ) { return; } ev.preventDefault(); ev.stopPropagation(); var dropdownItem = $(ev.currentTarget).parent(); - var pms_propertyID = dropdownItem.data('pms_property-id'); + var pms_propertyID = dropdownItem.data("pms_property-id"); var allowed_pms_property_ids = this.allowed_pms_property_ids; var current_pms_property_id = allowed_pms_property_ids[0]; - if (dropdownItem.find('.fa-square-o').length) { + if (dropdownItem.find(".fa-square-o").length) { allowed_pms_property_ids.push(pms_propertyID); - dropdownItem.find('.fa-square-o').removeClass('fa-square-o').addClass('fa-check-square'); - $(ev.currentTarget).attr('aria-checked', 'true'); + dropdownItem + .find(".fa-square-o") + .removeClass("fa-square-o") + .addClass("fa-check-square"); + $(ev.currentTarget).attr("aria-checked", "true"); } else { - allowed_pms_property_ids.splice(allowed_pms_property_ids.indexOf(pms_propertyID), 1); - dropdownItem.find('.fa-check-square').addClass('fa-square-o').removeClass('fa-check-square'); - $(ev.currentTarget).attr('aria-checked', 'false'); + allowed_pms_property_ids.splice( + allowed_pms_property_ids.indexOf(pms_propertyID), + 1 + ); + dropdownItem + .find(".fa-check-square") + .addClass("fa-square-o") + .removeClass("fa-check-square"); + $(ev.currentTarget).attr("aria-checked", "false"); } session.setPmsProperties(current_pms_property_id, allowed_pms_property_ids); }, - }); - + if (session.display_switch_pms_property_menu) { SystrayMenu.Items.push(SwitchPmsMenu); } - + return SwitchPmsMenu; - - }); - \ No newline at end of file +}); diff --git a/pms/static/src/xml/pms_base_templates.xml b/pms/static/src/xml/pms_base_templates.xml index 796407df8..b054fb56c 100644 --- a/pms/static/src/xml/pms_base_templates.xml +++ b/pms/static/src/xml/pms_base_templates.xml @@ -1,43 +1,76 @@ diff --git a/pms/views/inherited_webclient_templates.xml b/pms/views/inherited_webclient_templates.xml index e625c8d59..d8805c83e 100644 --- a/pms/views/inherited_webclient_templates.xml +++ b/pms/views/inherited_webclient_templates.xml @@ -3,9 +3,15 @@