diff --git a/pms/__manifest__.py b/pms/__manifest__.py index b541bb84a..a9e6c1db9 100644 --- a/pms/__manifest__.py +++ b/pms/__manifest__.py @@ -60,6 +60,7 @@ "views/res_partner_views.xml", "views/product_pricelist_views.xml", "views/product_pricelist_item_views.xml", + "views/pms_sale_channel.xml", "views/product_template_views.xml", "views/webclient_templates.xml", "views/ir_sequence_views.xml", diff --git a/pms/data/cron_jobs.xml b/pms/data/cron_jobs.xml index 7adf1a6a5..015e40e32 100644 --- a/pms/data/cron_jobs.xml +++ b/pms/data/cron_jobs.xml @@ -1,6 +1,35 @@ + + + Automatic No Show Reservation + 1 + + days + -1 + + code + + + model.auto_no_show() + + + + Automatic No Checkout Reservations + 5 + + minutes + -1 + + code + + + model.auto_no_checkout() + Automatic Checkout on past reservations diff --git a/pms/data/pms_data.xml b/pms/data/pms_data.xml index 1a55c1da1..7eafee0f0 100644 --- a/pms/data/pms_data.xml +++ b/pms/data/pms_data.xml @@ -34,5 +34,22 @@ + + + Door + direct + + + Phone + direct + + + Mail + direct + + + Agency + indirect + diff --git a/pms/demo/pms_folio.xml b/pms/demo/pms_folio.xml index be9214c38..a5b48079e 100644 --- a/pms/demo/pms_folio.xml +++ b/pms/demo/pms_folio.xml @@ -34,7 +34,7 @@ /> - + - + normal @@ -381,7 +381,7 @@ /> - + normal diff --git a/pms/demo/pms_reservation.xml b/pms/demo/pms_reservation.xml index 8f201a7d7..fb6c62c86 100644 --- a/pms/demo/pms_reservation.xml +++ b/pms/demo/pms_reservation.xml @@ -24,6 +24,20 @@ 2 + onboard + @@ -58,6 +72,15 @@ 1 + @@ -67,6 +90,14 @@ 1 + @@ -129,6 +160,13 @@ 2 + @@ -164,6 +202,13 @@ 2 + @@ -217,6 +262,14 @@ 3 + @@ -225,6 +278,12 @@ 1 + 2 @@ -235,6 +294,14 @@ 3 + @@ -245,6 +312,19 @@ 2 1 onboard + diff --git a/pms/i18n/es.po b/pms/i18n/es.po index 0eca6fec0..8e101c9fa 100644 --- a/pms/i18n/es.po +++ b/pms/i18n/es.po @@ -5700,12 +5700,12 @@ msgid "End Date" msgstr "Fecha de finalización" #. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_enter_date +#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_arrival msgid "Enter Date" msgstr "Fecha de entrada" #. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_exit_date +#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_departure msgid "Exit Date" msgstr "Fecha salida" @@ -5877,11 +5877,6 @@ msgstr "Generica" msgid "Get in" msgstr "Entrar" -#. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_auto_booking -msgid "Get in Now" -msgstr "Entra ahora!" - #. module: hotel #: model:ir.model.fields,help:hotel.field_hotel_room_type_packaging_ids msgid "Gives the different ways to package the same product." @@ -6671,7 +6666,7 @@ msgstr "Mail" #: model:ir.model.fields,help:hotel.field_hotel_room_type_property_valuation msgid "" "Manual: The accounting entries to value the inventory are not posted automatically.\n" -" Automated: An accounting entry is automatically created to value the inventory when a product enters or leaves the company." +" Automated: An accounting arrival is automatically created to value the inventory when a product enters or leaves the company." msgstr "" "Manual: Los registros contables de valoración del inventario no se publican automáticamente.\n" " Automatizado: Se crea automáticamente un registro contable para evaluar el inventario cuando un producto entra o sale de la empresa." @@ -7215,7 +7210,7 @@ msgstr "Pagos" #. module: hotel #: selection:hotel.checkin.partner,state:0 selection:hotel.reservation,state:0 -msgid "Pending Entry" +msgid "Pending arrival" msgstr "Por entrar" #. module: hotel diff --git a/pms/models/__init__.py b/pms/models/__init__.py index 5343f0ca8..92f2ace63 100644 --- a/pms/models/__init__.py +++ b/pms/models/__init__.py @@ -31,6 +31,7 @@ from . import pms_checkin_partner from . import product_pricelist from . import product_pricelist_item from . import res_partner +from . import pms_sale_channel # from . import mail_compose_message from . import pms_room_type_class diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 4acc25465..c364c8e2e 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -1,229 +1,171 @@ # Copyright 2017 Dario Lodeiros # Copyright 2018 Alexandre Diaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import datetime from odoo import _, api, fields, models from odoo.exceptions import ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT class PmsCheckinPartner(models.Model): _name = "pms.checkin.partner" _description = "Partner Checkins" - # Default Methods ang Gets - def _default_reservation_id(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation - return False - - def _default_partner_id(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - partner_ids = [] - if reservation.folio_id: - for room in reservation.folio_id.reservation_ids: - partner_ids.append(room.mapped("checkin_partner_ids.partner_id.id")) - if "checkin_partner_ids" in self.env.context: - for checkin in self.env.context["checkin_partner_ids"]: - if checkin[0] == 0: - partner_ids.append(checkin[2].get("partner_id")) - if ( - self._context.get("include_customer") - and reservation.partner_id.id not in partner_ids - and not reservation.partner_id.is_company - ): - return reservation.partner_id - return False - - def _default_folio_id(self): - if "folio_id" in self.env.context: - folio = self.env["pms.folio"].browse([self.env.context["folio_id"]]) - return folio - if "reservation_id" in self.env.context: - folio = ( - self.env["pms.reservation"] - .browse([self.env.context["reservation_id"]]) - .folio_id - ) - return folio - return False - - def _default_enter_date(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation.checkin - return False - - def _default_exit_date(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation.checkout - return False - @api.model def _get_default_pms_property(self): + # TODO: Change by property env variable (like company) return self.env.user.pms_property_id # Fields declaration - partner_id = fields.Many2one( - "res.partner", default=_default_partner_id, required=True + identifier = fields.Char( + "Identifier", + compute="_compute_identifier", + readonly=False, + store=True, ) - reservation_id = fields.Many2one("pms.reservation", default=_default_reservation_id) + partner_id = fields.Many2one( + "res.partner", + domain="[('is_company', '=', False)]", + ) + reservation_id = fields.Many2one("pms.reservation") folio_id = fields.Many2one( - "pms.folio", default=_default_folio_id, readonly=True, required=True + "pms.folio", + compute="_compute_folio_id", + store=True, ) pms_property_id = fields.Many2one( "pms.property", default=_get_default_pms_property, required=True ) + name = fields.Char("Name", related="partner_id.name") email = fields.Char("E-mail", related="partner_id.email") mobile = fields.Char("Mobile", related="partner_id.mobile") - enter_date = fields.Date(default=_default_enter_date, required=True) - exit_date = fields.Date(default=_default_exit_date, required=True) - arrival_hour = fields.Char("Arrival Hour", help="Default Arrival Hour (HH:MM)") - departure_hour = fields.Char( - "Departure Hour", help="Default Departure Hour (HH:MM)" + image_128 = fields.Image(related="partner_id.image_128") + segmentation_ids = fields.Many2many( + related="reservation_id.segmentation_ids", + readonly=True, ) - auto_booking = fields.Boolean("Get in Now", default=False) + arrival = fields.Datetime("Enter") + departure = fields.Datetime("Exit") state = fields.Selection( selection=[ - ("draft", "Pending Entry"), + ("draft", "Unkown Guest"), + ("precheckin", "Pending arrival"), ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), ], string="State", + compute="_compute_state", + store=True, readonly=True, - default=lambda *a: "draft", - tracking=True, ) + # Compute + @api.depends("reservation_id", "folio_id", "reservation_id.preferred_room_id") + def _compute_identifier(self): + for record in self: + # TODO: Identifier + checkins = [] + if record.reservation_id.filtered("preferred_room_id"): + checkins = record.reservation_id.checkin_partner_ids + record.identifier = ( + record.reservation_id.preferred_room_id.name + + "-" + + str(len(checkins) - 1) + ) + elif record.folio_id: + record.identifier = record.folio_id.name + "-" + str(len(checkins) - 1) + else: + record.identifier = False + + @api.depends("reservation_id", "reservation_id.folio_id") + def _compute_folio_id(self): + for record in self.filtered("reservation_id"): + record.folio_id = record.reservation_id.folio_id + + @api.depends(lambda self: self._checkin_mandatory_fields(depends=True)) + def _compute_state(self): + for record in self: + if not record.state: + record.state = "draft" + if record.reservation_id.state == "cancelled": + record.state = "cancelled" + elif record.state in ("draft", "cancelled"): + if any( + not getattr(record, field) + for field in record._checkin_mandatory_fields() + ): + record.state = "draft" + else: + record.state = "precheckin" + + @api.model + def _checkin_mandatory_fields(self, depends=False): + # api.depends need "reservation_id.state" in de lambda function + if depends: + return ["reservation_id.state", "name"] + return ["name"] + # Constraints and onchanges - @api.constrains("exit_date", "enter_date") - def _check_exit_date(self): + @api.constrains("departure", "arrival") + def _check_departure(self): for record in self: - date_in = fields.Date.from_string(record.enter_date) - date_out = fields.Date.from_string(record.exit_date) - if date_out < date_in: - raise models.ValidationError( + if record.departure and record.arrival > record.departure: + raise ValidationError( _("Departure date (%s) is prior to arrival on %s") - % (date_out, date_in) + % (record.departure, record.arrival) ) - @api.onchange("enter_date", "exit_date") - def _onchange_enter_date(self): - date_in = fields.Date.from_string(self.enter_date) - date_out = fields.Date.from_string(self.exit_date) - if date_out <= date_in: - date_out = date_in + datetime.timedelta(days=1) - self.update({"exit_date": date_out}) - raise ValidationError( - _("Departure date, is prior to arrival. Check it now. %s") % date_out - ) - - @api.onchange("partner_id") + @api.constrains("partner_id") def _check_partner_id(self): for record in self: if record.partner_id: - if record.partner_id.is_company: - raise models.ValidationError( - _( - "A Checkin Guest is configured like a company, \ - modify it in contact form if its a mistake" - ) - ) indoor_partner_ids = record.reservation_id.checkin_partner_ids.filtered( lambda r: r.id != record.id ).mapped("partner_id.id") if indoor_partner_ids.count(record.partner_id.id) > 1: record.partner_id = None - raise models.ValidationError( + raise ValidationError( _("This guest is already registered in the room") ) + # CRUD + @api.model + def create(self, vals): + # The checkin records are created automatically from adult depends + # if you try to create one manually, we update one unassigned checkin + if not self._context.get("auto_create_checkin"): + reservation_id = vals.get("reservation_id") + if reservation_id: + reservation = self.env["pms.reservation"].browse(reservation_id) + draft_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state == "draft" + ) + if len(draft_checkins) > 0 and vals.get("partner_id"): + draft_checkins[0].sudo().unlink() + return super(PmsCheckinPartner, self).create(vals) + # Action methods def action_on_board(self): for record in self: if record.reservation_id.checkin > fields.Date.today(): - raise models.ValidationError(_("It is not yet checkin day!")) - hour = record._get_arrival_hour() + raise ValidationError(_("It is not yet checkin day!")) + if record.reservation_id.checkout <= fields.Date.today(): + raise ValidationError(_("Its too late to checkin")) vals = { "state": "onboard", - "arrival_hour": hour, + "arrival": fields.Datetime.now(), } record.update(vals) - if record.reservation_id.state == "confirm": + if record.reservation_id.left_for_checkin: record.reservation_id.state = "onboard" - return { - "type": "ir.actions.do_nothing", - } def action_done(self): - for record in self: - if record.state == "onboard": - hour = record._get_departure_hour() - vals = { - "state": "done", - "departure_hour": hour, - } - record.update(vals) + for record in self.filtered(lambda c: c.state == "onboard"): + vals = { + "state": "done", + "departure": fields.Datetime.now(), + } + record.update(vals) return True - - # ORM Overrides - @api.model - def create(self, vals): - record = super(PmsCheckinPartner, self).create(vals) - if vals.get("auto_booking", False): - record.action_on_board() - return record - - # Business methods - def _get_arrival_hour(self): - self.ensure_one() - tz_property = self.env.user.pms_property_id.tz - today = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT), - ) - default_arrival_hour = self.env.user.pms_property_id.default_arrival_hour - if self.reservation_id.checkin < today.strftime(DEFAULT_SERVER_DATE_FORMAT): - return default_arrival_hour - now = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime( - fields.Datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT - ), - ) - arrival_hour = now.strftime("%H:%M") - return arrival_hour - - def _get_departure_hour(self): - self.ensure_one() - tz_property = self.env.user.pms_property_id.tz - today = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT), - ) - default_departure_hour = self.env.user.pms_property_id.default_departure_hour - if self.reservation_id.checkout < today.strftime(DEFAULT_SERVER_DATE_FORMAT): - return default_departure_hour - now = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime( - fields.Datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT - ), - ) - departure_hour = now.strftime("%H:%M") - return departure_hour diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 94149e51e..01460eb2b 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -50,7 +50,9 @@ class PmsFolio(models.Model): pms_property_id = fields.Many2one( "pms.property", default=_get_default_pms_property, required=True ) - partner_id = fields.Many2one("res.partner", tracking=True, ondelete="restrict") + partner_id = fields.Many2one( + "res.partner", compute="_compute_partner_id", tracking=True, ondelete="restrict" + ) reservation_ids = fields.One2many( "pms.reservation", "folio_id", @@ -102,6 +104,13 @@ class PmsFolio(models.Model): readonly=False, help="Pricelist for current folio.", ) + commission = fields.Float( + string="Commission", + compute="_compute_commission", + store=True, + readonly=True, + default=0, + ) user_id = fields.Many2one( "res.users", string="Salesperson", @@ -114,10 +123,16 @@ class PmsFolio(models.Model): ) agency_id = fields.Many2one( "res.partner", - "Agency", + string="Agency", ondelete="restrict", domain=[("is_agency", "=", True)], ) + channel_type_id = fields.Many2one( + "pms.sale.channel", + string="Direct Sale Channel", + ondelete="restrict", + domain=[("channel_type", "=", "direct")], + ) payment_ids = fields.One2many("account.payment", "folio_id", readonly=True) # return_ids = fields.One2many("payment.return", "folio_id", readonly=True) payment_term_id = fields.Many2one( @@ -163,15 +178,6 @@ class PmsFolio(models.Model): string="Type", default=lambda *a: "normal", ) - channel_type = fields.Selection( - [ - ("direct", "Direct"), - ("agency", "Agency"), - ], - string="Sales Channel", - compute="_compute_channel_type", - store=True, - ) date_order = fields.Datetime( string="Order Date", required=True, @@ -244,14 +250,13 @@ class PmsFolio(models.Model): tracking=True, ) # Checkin Fields----------------------------------------------------- - booking_pending = fields.Integer( - "Booking pending", compute="_compute_checkin_partner_count" + reservation_pending_arrival_ids = fields.One2many( + comodel_name="pms.checkin.partner", + string="Pending Arrival Rooms", + compute="_compute_reservations_pending_arrival", ) - checkin_partner_count = fields.Integer( - "Checkin counter", compute="_compute_checkin_partner_count" - ) - checkin_partner_pending_count = fields.Integer( - "Checkin Pending", compute="_compute_checkin_partner_count" + reservations_pending_count = fields.Integer( + compute="_compute_reservations_pending_arrival" ) # Invoice Fields----------------------------------------------------- invoice_status = fields.Selection( @@ -285,17 +290,26 @@ class PmsFolio(models.Model): folio.reservation_ids.filtered(lambda a: a.state != "cancelled") ) - @api.depends("partner_id") + @api.depends("partner_id", "agency_id") def _compute_pricelist_id(self): for folio in self: - pricelist_id = ( - folio.partner_id.property_product_pricelist - and folio.partner_id.property_product_pricelist.id - or self.env.user.pms_property_id.default_pricelist_id.id - ) + if folio.partner_id and folio.partner_id.property_product_pricelist: + pricelist_id = folio.partner_id.property_product_pricelist.id + else: + pricelist_id = self.env.user.pms_property_id.default_pricelist_id.id if folio.pricelist_id.id != pricelist_id: # TODO: Warning change de pricelist? folio.pricelist_id = pricelist_id + if folio.agency_id and folio.agency_id.apply_pricelist: + pricelist_id = folio.agency_id.property_product_pricelist.id + + @api.depends("agency_id") + def _compute_partner_id(self): + for folio in self: + if folio.agency_id and folio.agency_id.invoice_agency: + folio.partner_id = folio.agency_id.id + elif not folio.partner_id: + folio.partner_id = False @api.depends("partner_id") def _compute_user_id(self): @@ -309,24 +323,25 @@ class PmsFolio(models.Model): addr = folio.partner_id.address_get(["invoice"]) folio.partner_invoice_id = addr["invoice"] - @api.depends("agency_id") - def _compute_channel_type(self): - for folio in self: - if folio.agency_id: - folio.channel_type = "agency" - else: - folio.channel_type = "direct" - @api.depends("partner_id") def _compute_payment_term_id(self): self.payment_term_id = False for folio in self: folio.payment_term_id = ( - self.partner_id.property_payment_term_id - and self.partner_id.property_payment_term_id.id + folio.partner_id.property_payment_term_id + and folio.partner_id.property_payment_term_id.id or False ) + @api.depends("reservation_ids") + def _compute_commission(self): + for folio in self: + for reservation in folio.reservation_ids: + if reservation.commission_amount != 0: + folio.commission += reservation.commission_amount + else: + folio.commission = 0 + @api.depends( "state", "reservation_ids.invoice_status", "service_ids.invoice_status" ) @@ -444,6 +459,16 @@ class PmsFolio(models.Model): } ) + @api.depends("reservation_ids", "reservation_ids.state") + def _compute_reservations_pending_arrival(self): + for record in self: + record.reservation_pending_arrival_ids = record.reservation_ids.filtered( + lambda r: r.state in ("draft", "precheckin") + ) + record.reservations_pending_count = len( + record.reservations_pending_arrival_ids + ) + # TODO: Add return_ids to depends @api.depends("amount_total", "payment_ids", "reservation_type", "state") def _compute_amount(self): @@ -658,3 +683,10 @@ class PmsFolio(models.Model): (line[0].name, line[1]["amount"], line[1]["base"], len(res)) for line in res ] return res + + # Check that only one sale channel is selected + @api.constrains("agency_id", "channel_type_id") + def _check_only_one_channel(self): + for record in self: + if record.agency_id and record.channel_type_id: + raise models.ValidationError(_("There must be only one sale channel")) diff --git a/pms/models/pms_property.py b/pms/models/pms_property.py index 0a67406c0..76295607d 100644 --- a/pms/models/pms_property.py +++ b/pms/models/pms_property.py @@ -2,11 +2,15 @@ # Copyright 2019 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import re +import time + +import pytz from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.addons.base.models.res_partner import _tz_get + class PmsProperty(models.Model): _name = "pms.property" @@ -54,12 +58,50 @@ class PmsProperty(models.Model): folio_sequence_id = fields.Many2one( "ir.sequence", "Folio Sequence", check_company=True, copy=False ) + tz = fields.Selection( + _tz_get, + string="Timezone", + required=True, + default=lambda self: self.env.user.tz or "UTC", + help="This field is used in order to define \ + in which timezone the arrival/departure will work.", + ) # Constraints and onchanges - @api.constrains("default_arrival_hour", "default_departure_hour") - def _check_hours(self): - r = re.compile("[0-2][0-9]:[0-5][0-9]") - if not r.match(self.default_arrival_hour): - raise ValidationError(_("Invalid arrival hour (Format: HH:mm)")) - if not r.match(self.default_departure_hour): - raise ValidationError(_("Invalid departure hour (Format: HH:mm)")) + @api.constrains("default_arrival_hour") + def _check_arrival_hour(self): + for record in self: + try: + time.strptime(record.default_arrival_hour, "%H:%M") + return True + except ValueError: + raise ValidationError( + _( + "Format Arrival Hour (HH:MM) Error: %s", + record.default_arrival_hour, + ) + ) + + @api.constrains("default_departure_hour") + def _check_departure_hour(self): + for record in self: + try: + time.strptime(record.default_departure_hour, "%H:%M") + return True + except ValueError: + raise ValidationError( + _( + "Format Departure Hour (HH:MM) Error: %s", + record.default_departure_hour, + ) + ) + + def date_property_timezone(self, date): + self.ensure_one() + tz_property = self.tz + date = pytz.timezone(tz_property).localize(date) + date = date.replace(tzinfo=None) + date = pytz.timezone(self.env.user.tz).localize(date) + date = date.astimezone(pytz.utc) + date = date.replace(tzinfo=None) + return date diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index cedccf2bd..144c7a899 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -1,11 +1,12 @@ # Copyright 2017-2018 Alexandre Díaz # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import datetime import logging -from datetime import timedelta +import time from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -39,7 +40,7 @@ class PmsReservation(models.Model): if folio and folio.reservation_ids: return folio.reservation_ids[0].checkout else: - return fields.Date.today() + timedelta(1) + return fields.Date.today() + datetime.timedelta(1) def _get_default_arrival_hour(self): folio = False @@ -146,7 +147,14 @@ class PmsReservation(models.Model): store=True, readonly=False, ) - agency_id = fields.Many2one(related="folio_id.agency_id") + agency_id = fields.Many2one( + related="folio_id.agency_id", + readonly=True, + ) + channel_type_id = fields.Many2one( + related="folio_id.channel_type_id", + readonly=True, + ) partner_invoice_id = fields.Many2one( "res.partner", string="Invoice Address", @@ -185,8 +193,53 @@ class PmsReservation(models.Model): store=True, readonly=False, ) + commission_percent = fields.Float( + string="Commission percent (%)", + compute="_compute_commission_percent", + store=True, + readonly=False, + ) + commission_amount = fields.Float( + string="Commission amount", + compute="_compute_commission_amount", + store=True, + ) # TODO: Warning Mens to update pricelist - checkin_partner_ids = fields.One2many("pms.checkin.partner", "reservation_id") + checkin_partner_ids = fields.One2many( + "pms.checkin.partner", + "reservation_id", + compute="_compute_checkin_partner_ids", + store=True, + readonly=False, + ) + count_pending_arrival = fields.Integer( + "Pending Arrival", + compute="_compute_count_pending_arrival", + store=True, + ) + checkins_ratio = fields.Integer( + string="Pending Arrival Ratio", + compute="_compute_checkins_ratio", + ) + pending_checkin_data = fields.Integer( + "Checkin Data", + compute="_compute_pending_checkin_data", + store=True, + ) + ratio_checkin_data = fields.Integer( + string="Pending Checkin Data", + compute="_compute_ratio_checkin_data", + ) + ready_for_checkin = fields.Boolean(compute="_compute_ready_for_checkin") + left_for_checkin = fields.Boolean( + compute="_compute_left_for_checkin", search="_search_left_for_checkin" + ) + checkin_today = fields.Boolean( + compute="_compute_checkin_today", search="_search_checkin_today" + ) + departure_today = fields.Boolean( + compute="_compute_departure_today", search="_search_departure_today" + ) segmentation_ids = fields.Many2many( "res.partner.category", string="Segmentation", @@ -239,14 +292,17 @@ class PmsReservation(models.Model): state = fields.Selection( [ ("draft", "Pre-reservation"), - ("confirm", "Pending Entry"), + ("confirm", "Pending arrival"), ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), + ("no_show", "No Show"), + ("no_checkout", "No Checkout"), ], string="Status", default=lambda *a: "draft", copy=False, + index=True, tracking=True, readonly=True, ) @@ -280,6 +336,14 @@ class PmsReservation(models.Model): default=_get_default_departure_hour, help="Default Departure Hour (HH:MM)", ) + checkin_datetime = fields.Datetime( + "Exact Arrival", + compute="_compute_checkin_datetime", + ) + checkout_datetime = fields.Datetime( + "Exact Departure", + compute="_compute_checkout_datetime", + ) # TODO: As checkin_partner_count is a computed field, it can't not # be used in a domain filer Non-stored field # pms.reservation.checkin_partner_count cannot be searched @@ -296,22 +360,6 @@ class PmsReservation(models.Model): overbooking = fields.Boolean("Is Overbooking", default=False) reselling = fields.Boolean("Is Reselling", default=False) nights = fields.Integer("Nights", compute="_compute_nights", store=True) - channel_type = fields.Selection( - selection=[ - ("direct", "Direct"), - ("agency", "Agency"), - ], - string="Sales Channel", - default="direct", - ) - subchannel_direct = fields.Selection( - selection=[ - ("door", "Door"), - ("mail", "Mail"), - ("phone", "Phone"), - ], - string="Direct Channel", - ) origin = fields.Char("Origin", compute="_compute_origin", store=True) detail_origin = fields.Char( "Detail Origin", compute="_compute_detail_origin", store=True @@ -426,6 +474,32 @@ class PmsReservation(models.Model): elif not reservation.room_type_id: reservation.room_type_id = False + @api.depends("checkin", "arrival_hour") + def _compute_checkin_datetime(self): + for reservation in self: + checkin_hour = int(reservation.arrival_hour[0:2]) + checkin_minut = int(reservation.arrival_hour[3:5]) + checkin_time = datetime.time(checkin_hour, checkin_minut) + checkin_datetime = datetime.datetime.combine( + reservation.checkin, checkin_time + ) + reservation.checkin_datetime = ( + reservation.pms_property_id.date_property_timezone(checkin_datetime) + ) + + @api.depends("checkout", "departure_hour") + def _compute_checkout_datetime(self): + for reservation in self: + checkout_hour = int(reservation.departure_hour[0:2]) + checkout_minut = int(reservation.departure_hour[3:5]) + checkout_time = datetime.time(checkout_hour, checkout_minut) + checkout_datetime = datetime.datetime.combine( + reservation.checkout, checkout_time + ) + reservation.checkout_datetime = ( + reservation.pms_property_id.date_property_timezone(checkout_datetime) + ) + @api.depends( "reservation_line_ids.date", "overbooking", "state", "preferred_room_id" ) @@ -447,7 +521,7 @@ class PmsReservation(models.Model): ) reservation.allowed_room_ids = rooms_available - @api.depends("reservation_type") + @api.depends("reservation_type", "agency_id") def _compute_partner_id(self): for reservation in self: if reservation.reservation_type == "out": @@ -456,6 +530,8 @@ class PmsReservation(models.Model): reservation.partner_id = reservation.folio_id.partner_id else: reservation.partner_id = False + if not reservation.partner_id and reservation.agency_id: + reservation.partner_id = reservation.agency_id @api.depends("partner_id") def _compute_partner_invoice_id(self): @@ -472,7 +548,7 @@ class PmsReservation(models.Model): cmds = [] days_diff = (reservation.checkout - reservation.checkin).days for i in range(0, days_diff): - idate = reservation.checkin + timedelta(days=i) + idate = reservation.checkin + datetime.timedelta(days=i) old_line = reservation.reservation_line_ids.filtered( lambda r: r.date == idate ) @@ -534,6 +610,180 @@ class PmsReservation(models.Model): # TODO: Warning change de pricelist? reservation.pricelist_id = pricelist_id + @api.depends("adults") + def _compute_checkin_partner_ids(self): + for reservation in self: + assigned_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state in ("precheckin", "onboard", "done") + ) + unassigned_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state == "draft" + ) + leftover_unassigneds_count = ( + len(assigned_checkins) + len(unassigned_checkins) - reservation.adults + ) + if len(assigned_checkins) > reservation.adults: + raise UserError( + _("Remove some of the leftover assigned checkins first") + ) + elif leftover_unassigneds_count > 0: + for i in range(0, leftover_unassigneds_count): + unassigned_checkins[i].sudo().unlink() + elif reservation.adults > len(reservation.checkin_partner_ids): + checkins_lst = [] + count_new_checkins = reservation.adults - len( + reservation.checkin_partner_ids + ) + for _i in range(0, count_new_checkins): + checkins_lst.append( + ( + 0, + False, + { + "reservation_id": reservation.id, + }, + ) + ) + reservation.with_context( + {"auto_create_checkin": True} + ).checkin_partner_ids = checkins_lst + + @api.depends("checkin_partner_ids", "checkin_partner_ids.state") + def _compute_count_pending_arrival(self): + for reservation in self: + reservation.count_pending_arrival = len( + reservation.checkin_partner_ids.filtered( + lambda c: c.state in ("draft", "precheckin") + ) + ) + + @api.depends("count_pending_arrival") + def _compute_checkins_ratio(self): + self.checkins_ratio = 0 + for reservation in self.filtered(lambda r: r.adults > 0): + reservation.checkins_ratio = ( + (reservation.adults - reservation.count_pending_arrival) + * 100 + / reservation.adults + ) + + @api.depends("checkin_partner_ids", "checkin_partner_ids.state") + def _compute_pending_checkin_data(self): + for reservation in self: + reservation.pending_checkin_data = len( + reservation.checkin_partner_ids.filtered(lambda c: c.state == "draft") + ) + + @api.depends("pending_checkin_data") + def _compute_ratio_checkin_data(self): + self.ratio_checkin_data = 0 + for reservation in self.filtered(lambda r: r.adults > 0): + reservation.ratio_checkin_data = ( + (reservation.adults - reservation.pending_checkin_data) + * 100 + / reservation.adults + ) + + def _compute_left_for_checkin(self): + # Reservations still pending entry today + for record in self: + record.left_for_checkin = ( + True + if ( + record.state in ["draft", "confirm", "no_show"] + and record.checkin <= fields.Date.today() + ) + else False + ) + + def _search_left_for_checkin(self, operator, value): + if operator not in ("=",): + raise UserError( + _("Invalid domain operator %s for left of checkin", operator) + ) + + if value not in (True,): + raise UserError( + _("Invalid domain right operand %s for left of checkin", value) + ) + + today = fields.Date.context_today(self) + return [ + ("state", "in", ("draft", "confirm", "no_show")), + ("checkin", "<=", today), + ] + + def _compute_ready_for_checkin(self): + # Reservations with hosts data enought to checkin + for record in self: + record.ready_for_checkin = ( + record.left_for_checkin + and len( + record.checkin_partner_ids.filtered( + lambda c: c.state == "precheckin" + ) + ) + >= 1 + ) + + def _compute_checkin_today(self): + for record in self: + record.checkin_today = ( + True if record.checkin == fields.Date.today() else False + ) + # REVIEW: Late checkin?? (next day) + + def _search_checkin_today(self, operator, value): + if operator not in ("=", "!="): + raise UserError(_("Invalid domain operator %s", operator)) + + if value not in (False, True): + raise UserError(_("Invalid domain right operand %s", value)) + + today = fields.Date.context_today(self) + + return [("checkin", operator, today)] + + def _compute_departure_today(self): + for record in self: + record.departure_today = ( + True if record.checkout == fields.Date.today() else False + ) + + def _search_departure_today(self, operator, value): + if operator not in ("=", "!="): + raise UserError(_("Invalid domain operator %s", operator)) + + if value not in (False, True): + raise UserError(_("Invalid domain right operand %s", value)) + + searching_for_true = (operator == "=" and value) or ( + operator == "!=" and not value + ) + today = fields.Date.context_today(self) + + return [("checkout", searching_for_true, today)] + + @api.depends("agency_id") + def _compute_commission_percent(self): + for reservation in self: + if reservation.agency_id: + reservation.commission_percent = ( + reservation.agency_id.default_commission + ) + else: + reservation.commission_percent = 0 + + @api.depends("commission_percent", "price_total") + def _compute_commission_amount(self): + for reservation in self: + if reservation.commission_percent > 0: + reservation.commission_amount = ( + reservation.price_total * reservation.commission_percent + ) + else: + reservation.commission_amount = 0 + # REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿? @api.depends("preferred_room_id") def _compute_adults(self): @@ -572,7 +822,7 @@ class PmsReservation(models.Model): "Product Unit of Measure" ) for line in self: - if line.state in ("draft"): + if line.state == "draft": line.invoice_status = "no" elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = "to invoice" @@ -707,11 +957,63 @@ class PmsReservation(models.Model): ) ) - @api.constrains("checkin_partner_ids") + @api.constrains("checkin_partner_ids", "adults") def _max_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")) + if len(record.checkin_partner_ids) > record.adults: + raise models.ValidationError( + _("The room already is completed (%s)", record.name) + ) + + @api.constrains("adults") + def _check_adults(self): + for record in self: + extra_bed = record.service_ids.filtered( + lambda r: r.product_id.is_extra_bed is True + ) + for room in record.reservation_line_ids.room_id: + if record.adults + record.children_occupying > room.get_capacity( + len(extra_bed) + ): + raise ValidationError( + _( + "Persons can't be higher than room capacity (%s)", + record.name, + ) + ) + + @api.constrains("state") + def _check_onboard_reservation(self): + for record in self: + if ( + not record.checkin_partner_ids.filtered(lambda c: c.state == "onboard") + and record.state == "onboard" + ): + raise ValidationError( + _("No person from reserve %s has arrived", record.name) + ) + + @api.constrains("arrival_hour") + def _check_arrival_hour(self): + for record in self: + try: + time.strptime(record.arrival_hour, "%H:%M") + return True + except ValueError: + raise ValidationError( + _("Format Arrival Hour (HH:MM) Error: %s", record.arrival_hour) + ) + + @api.constrains("departure_hour") + def _check_departure_hour(self): + for record in self: + try: + time.strptime(record.departure_hour, "%H:%M") + return True + except ValueError: + raise ValidationError( + _("Format Departure Hour (HH:MM) Error: %s", record.departure_hour) + ) # @api.constrains("reservation_type", "partner_id") # def _check_partner_reservation(self): @@ -734,12 +1036,6 @@ class PmsReservation(models.Model): # _("Only the out reservations can has a clousure reason") # ) - # @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")) - # self._compute_tax_ids() TODO: refact # Action methods @@ -833,19 +1129,11 @@ class PmsReservation(models.Model): @api.model def create(self, vals): - if "folio_id" in vals and "channel_type" not in vals: + if "folio_id" in vals: folio = self.env["pms.folio"].browse(vals["folio_id"]) - channel_type = ( - vals["channel_type"] if "channel_type" in vals else folio.channel_type - ) - partner_id = ( - vals["partner_id"] if "partner_id" in vals else folio.partner_id.id - ) - vals.update({"channel_type": channel_type, "partner_id": partner_id}) elif "partner_id" in vals: folio_vals = { "partner_id": int(vals.get("partner_id")), - "channel_type": vals.get("channel_type"), } # Create the folio in case of need # (To allow to create reservations direct) @@ -854,7 +1142,6 @@ class PmsReservation(models.Model): { "folio_id": folio.id, "reservation_type": vals.get("reservation_type"), - "channel_type": vals.get("channel_type"), } ) record = super(PmsReservation, self).create(vals) @@ -896,7 +1183,7 @@ class PmsReservation(models.Model): def autocheckout(self): reservations = self.env["pms.reservation"].search( [ - ("state", "not in", ("done", "cancelled")), + ("state", "not in", ["done", "cancelled"]), ("checkout", "<", fields.Date.today()), ] ) @@ -935,7 +1222,7 @@ class PmsReservation(models.Model): def confirm(self): for record in self: vals = {} - if record.checkin_partner_ids: + if record.checkin_partner_ids.filtered(lambda c: c.state == "onboard"): vals.update({"state": "onboard"}) else: vals.update({"state": "confirm"}) @@ -945,14 +1232,6 @@ class PmsReservation(models.Model): record.folio_id.action_confirm() return True - def button_done(self): - """ - @param self: object pointer - """ - for record in self: - record.action_reservation_checkout() - return True - def action_cancel(self): for record in self: cancel_reason = ( @@ -1024,20 +1303,6 @@ class PmsReservation(models.Model): record.checkin_partner_count = 0 record.checkin_partner_pending_count = 0 - @api.depends("channel_type", "subchannel_direct") - def _compute_origin(self): - for reservation in self: - if reservation.channel_type == "direct": - reservation.origin = reservation.subchannel_direct - elif reservation.channel_type == "agency": - reservation.origin = reservation.agency_id.name - - @api.depends("origin") - def _compute_detail_origin(self): - for reservation in self: - if reservation.channel_type in ["direct", "agency"]: - reservation.detail_origin = reservation.sudo().create_uid.name - def _search_checkin_partner_pending(self, operator, value): self.ensure_one() recs = self.search([]).filtered(lambda x: x.checkin_partner_pending_count > 0) @@ -1054,13 +1319,61 @@ class PmsReservation(models.Model): def action_checks(self): self.ensure_one() - action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0] - action["views"] = [ - (self.env.ref("pms.pms_reservation_checkin_view_form").id, "form") - ] - action["res_id"] = self.id - action["target"] = "new" - return action + tree_id = self.env.ref("pms.pms_checkin_partner_reservation_view_tree").id + return { + "name": _("Register Partners"), + "views": [[tree_id, "tree"]], + "res_model": "pms.checkin.partner", + "type": "ir.actions.act_window", + "context": { + "create": False, + "edit": True, + "popup": True, + }, + "domain": [("reservation_id", "=", self.id), ("state", "=", "draft")], + "target": "new", + } + + def action_onboard(self): + self.ensure_one() + kanban_id = self.env.ref("pms.pms_checkin_partner_kanban_view").id + return { + "name": _("Register Checkins"), + "views": [[kanban_id, "kanban"]], + "res_model": "pms.checkin.partner", + "type": "ir.actions.act_window", + "context": { + "create": False, + "edit": True, + "popup": True, + }, + "domain": [("reservation_id", "=", self.id)], + "target": "new", + } + + @api.model + def auto_no_show(self): + # No show when pass 1 day from checkin day + no_show_reservations = self.env["pms.reservation"].search( + [ + ("state", "in", ("draft", "confirm")), + ("checkin", "<", fields.Date.today()), + ] + ) + no_show_reservations.state = "no_show" + + @api.model + def auto_no_checkout(self): + # No checkout when pass checkout hour + reservations = self.env["pms.reservation"].search( + [ + ("state", "in", ("onboard",)), + ("checkout", "=", fields.Datetime.today()), + ] + ) + for reservation in reservations: + if reservation.checkout_datetime <= fields.Datetime.now(): + reservations.state = "no_checkout" def unify(self): # TODO diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index 8391191c4..3415178ff 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -250,7 +250,6 @@ class PmsReservationLine(models.Model): line.reservation_id.tax_ids, line.reservation_id.company_id, ) - # _logger.info(line.price) # TODO: Out of service 0 amount else: line.price = line._origin.price @@ -395,7 +394,7 @@ class PmsReservationLine(models.Model): # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) - @api.constrains("reservation_id.adults", "room_id") + @api.constrains("room_id") def _check_adults(self): for record in self.filtered("room_id"): extra_bed = record.reservation_id.service_ids.filtered( diff --git a/pms/models/pms_sale_channel.py b/pms/models/pms_sale_channel.py new file mode 100644 index 000000000..1daa07a59 --- /dev/null +++ b/pms/models/pms_sale_channel.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class PmsSaleChannel(models.Model): + _name = "pms.sale.channel" + _description = "Sales Channel" + + # Fields declaration + name = fields.Text(string="Sale Channel Name") + channel_type = fields.Selection( + [("direct", "Direct"), ("indirect", "Indirect")], string="Sale Channel Type" + ) diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py index 5a36ecc30..6ff3facee 100644 --- a/pms/models/pms_service.py +++ b/pms/models/pms_service.py @@ -459,7 +459,7 @@ class PmsService(models.Model): ) for line in self: state = line.folio_id.state or "draft" - if state in ("draft"): + if state == "draft": line.invoice_status = "no" elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = "to invoice" diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py index 1c135639e..d715688e0 100644 --- a/pms/models/res_partner.py +++ b/pms/models/res_partner.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from odoo import api, fields, models +from odoo import _, api, fields, models _logger = logging.getLogger(__name__) @@ -21,20 +21,41 @@ class ResPartner(models.Model): folios_count = fields.Integer("Folios", compute="_compute_folios_count") unconfirmed = fields.Boolean("Unconfirmed", default=True) is_agency = fields.Boolean("Is Agency") + sale_channel_id = fields.Many2one( + "pms.sale.channel", + string="Sale Channel", + ondelete="restrict", + domain=[("channel_type", "=", "indirect")], + ) + default_commission = fields.Integer("Commission") + apply_pricelist = fields.Boolean("Apply Pricelist") + invoice_agency = fields.Boolean("Invoice Agency") # Compute and Search methods def _compute_reservations_count(self): pms_reservation_obj = self.env["pms.reservation"] for record in self: record.reservations_count = pms_reservation_obj.search_count( - [("partner_id.id", "=", record.id)] + [ + ( + "partner_id.id", + "=", + record.id if isinstance(record.id, int) else False, + ) + ] ) def _compute_folios_count(self): pms_folio_obj = self.env["pms.folio"] for record in self: record.folios_count = pms_folio_obj.search_count( - [("partner_id.id", "=", record.id)] + [ + ( + "partner_id.id", + "=", + record.id if isinstance(record.id, int) else False, + ) + ] ) # ORM Overrides @@ -64,3 +85,11 @@ class ResPartner(models.Model): name, args=args, operator=operator, limit=limit_rest ) return res + + @api.constrains("is_agency", "sale_channel_id") + def _check_is_agency(self): + for record in self: + if record.is_agency and not record.sale_channel_id: + raise models.ValidationError(_("Sale Channel must be entered")) + if not record.is_agency and record.sale_channel_id: + record.sale_channel_id = None diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index 02f4a382b..2ebaf1009 100644 --- a/pms/security/ir.model.access.csv +++ b/pms/security/ir.model.access.csv @@ -8,7 +8,7 @@ user_access_pms_reservation_line,user_access_pms_reservation_line,model_pms_rese user_access_room_closure_reason,user_access_room_closure_reason,model_room_closure_reason,pms.group_pms_user,1,0,0,0 user_access_pms_service_line,user_access_pms_service_line,model_pms_service_line,pms.group_pms_user,1,1,1,1 user_access_pms_board_service,user_access_pms_board_service,model_pms_board_service,pms.group_pms_user,1,0,0,0 -user_access_pms_checkin_partner,user_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_user,1,1,1,1 +user_access_pms_checkin_partner,user_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_user,1,1,1,0 user_access_pms_room_type_class,user_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_user,1,0,0,0 user_access_pms_room,user_access_pms_room,model_pms_room,pms.group_pms_user,1,0,0,0 user_access_shared_pms_room,user_access_pms_shared_room,model_pms_shared_room,pms.group_pms_user,1,0,0,0 @@ -24,6 +24,7 @@ user_access_pms_cancelation_rule,user_access_pms_cancelation_rule,model_pms_canc user_access_account_full_reconcile,user_access_account_full_reconcile,account.model_account_full_reconcile,pms.group_pms_user,1,1,1,1 user_access_property,user_access_property,model_pms_property,pms.group_pms_user,1,0,0,0 user_access_availability,user_access_availability,model_pms_room_type_availability,pms.group_pms_user,1,0,0,0 +user_access_pms_sale_channel,user_access_pms_sale_channel,model_pms_sale_channel,pms.group_pms_user,1,0,0,0 manager_access_pms_floor,manager_access_pms_floor,model_pms_floor,pms.group_pms_manager,1,1,1,1 manager_access_pms_amenity,manager_access_pms_amenity,model_pms_amenity,pms.group_pms_manager,1,1,1,1 manager_access_pms_amenity_type,manager_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_manager,1,1,1,1 @@ -33,7 +34,7 @@ manager_access_pms_reservation_line,manager_access_pms_reservation_line,model_pm manager_access_room_closure_reason,manager_access_room_closure_reason,model_room_closure_reason,pms.group_pms_manager,1,1,1,1 manager_access_pms_service_line,manager_access_pms_service_line,model_pms_service_line,pms.group_pms_manager,1,1,1,1 manager_access_pms_board_service,manager_access_pms_board_service,model_pms_board_service,pms.group_pms_manager,1,1,1,1 -manager_access_pms_checkin_partner,manager_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_manager,1,1,1,1 +manager_access_pms_checkin_partner,manager_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_manager,1,1,1,0 manager_access_pms_room_type_class,manager_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_manager,1,1,1,1 manager_access_pms_room,manager_access_pms_room,model_pms_room,pms.group_pms_manager,1,1,1,1 manager_access_pms_shared_room,manager_access_pms_shared_room,model_pms_shared_room,pms.group_pms_manager,1,1,1,1 @@ -47,4 +48,5 @@ manager_access_pms_board_service_line,manager_access_pms_board_service_line,mode manager_access_property,manager_access_property,model_pms_property,pms.group_pms_manager,1,1,1,1 manager_access_pms_cancelation_rule,manager_access_pms_cancelation_rule,model_pms_cancelation_rule,pms.group_pms_manager,1,1,1,1 manager_access_availability,manager_access_availability,model_pms_room_type_availability,pms.group_pms_manager,1,1,1,1 +manager_access_pms_sale_channel,manager_access_pms_sale_channel,model_pms_sale_channel,pms.group_pms_manager,1,1,1,1 user_access_pms_reservation_wizard,user_access_pms_reservation_wizard,model_pms_reservation_wizard,pms.group_pms_user,1,1,1,1 diff --git a/pms/static/description/avatar.png b/pms/static/description/avatar.png new file mode 100644 index 000000000..74861de3f Binary files /dev/null and b/pms/static/description/avatar.png differ diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index 32033ab63..9e8f0d527 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -22,3 +22,6 @@ from . import test_pms_reservation from . import test_pms_pricelist from . import test_pms_pricelist_priority +from . import test_pms_checkin_partner +from . import test_pms_sale_channel +from . import test_pms_folio \ No newline at end of file diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py new file mode 100644 index 000000000..743239cab --- /dev/null +++ b/pms/tests/test_pms_checkin_partner.py @@ -0,0 +1,384 @@ +import logging + +from freezegun import freeze_time + +from odoo import fields +from odoo.exceptions import ValidationError + +from .common import TestHotel + +_logger = logging.getLogger(__name__) + + +@freeze_time("2012-01-14") +class TestPmsCheckinPartner(TestHotel): + @classmethod + def arrange_single_checkin(cls): + # Arrange for one checkin on one reservation + cls.host1 = cls.env["res.partner"].create( + { + "name": "Miguel", + "phone": "654667733", + "email": "miguel@example.com", + } + ) + reservation_vals = { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "room_type_id": cls.env.ref("pms.pms_room_type_3").id, + "partner_id": cls.host1.id, + "adults": 3, + "pms_property_id": cls.env.ref("pms.main_pms_property").id, + } + demo_user = cls.env.ref("base.user_demo") + cls.reservation_1 = ( + cls.env["pms.reservation"].with_user(demo_user).create(reservation_vals) + ) + cls.checkin1 = cls.env["pms.checkin.partner"].create( + { + "partner_id": cls.host1.id, + "reservation_id": cls.reservation_1.id, + } + ) + + def test_auto_create_checkins(self): + + # ACTION + self.arrange_single_checkin() + checkins_count = len(self.reservation_1.checkin_partner_ids) + + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not created successful", + ) + + def test_auto_unlink_checkins(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACTION + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "phone": "654667733", + "email": "carlos@example.com", + } + ) + self.reservation_1.checkin_partner_ids = [ + ( + 0, + False, + { + "partner_id": host2.id, + }, + ) + ] + + checkins_count = len(self.reservation_1.checkin_partner_ids) + + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not updated successful", + ) + + def test_onboard_checkin(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.reservation_1.state = "onboard" + + def test_onboard_reservation(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.reservation_1.state, + "onboard", + "the reservation checkin was not successful", + ) + + def test_premature_checkin(self): + # ARRANGE + self.arrange_single_checkin() + self.reservation_1.write( + { + "checkin": "2012-01-15", + } + ) + + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.checkin1.action_on_board() + + def test_late_checkin(self): + # ARRANGE + self.arrange_single_checkin() + self.reservation_1.write( + { + "checkin": "2012-01-13", + } + ) + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.checkin1.arrival, + fields.datetime.now(), + "the late checkin has problems", + ) + + def test_too_many_people_checkin(self): + # ARRANGE + self.arrange_single_checkin() + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "phone": "654667733", + "email": "carlos@example.com", + } + ) + host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "phone": "654667733", + "email": "enmanuel@example.com", + } + ) + host4 = self.env["res.partner"].create( + { + "name": "Enrique", + "phone": "654667733", + "email": "enrique@example.com", + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host2.id, + "reservation_id": self.reservation_1.id, + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host3.id, + "reservation_id": self.reservation_1.id, + } + ) + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.reservation_1.write( + { + "checkin_partner_ids": [ + ( + 0, + 0, + { + "partner_id": host4.id, + "reservation_id": self.reservation_1.id, + }, + ) + ] + } + ) + + @classmethod + def arrange_folio_reservations(cls): + # Arrange on one folio with 3 reservations + demo_user = cls.env.ref("base.user_demo") + cls.host1 = cls.env["res.partner"].create( + { + "name": "Miguel", + "phone": "654667733", + "email": "miguel@example.com", + } + ) + cls.host2 = cls.env["res.partner"].create( + { + "name": "Carlos", + "phone": "654667733", + "email": "carlos@example.com", + } + ) + cls.host3 = cls.env["res.partner"].create( + { + "name": "Enmanuel", + "phone": "654667733", + "email": "enmanuel@example.com", + } + ) + cls.host4 = cls.env["res.partner"].create( + { + "name": "Enrique", + "phone": "654667733", + "email": "enrique@example.com", + } + ) + folio_vals = { + "partner_id": cls.host1.id, + } + cls.folio_1 = cls.env["pms.folio"].with_user(demo_user).create(folio_vals) + reservation1_vals = { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "room_type_id": cls.env.ref("pms.pms_room_type_3").id, + "partner_id": cls.host1.id, + "adults": 3, + "pms_property_id": cls.env.ref("pms.main_pms_property").id, + "folio_id": cls.folio_1.id, + } + reservation2_vals = { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "room_type_id": cls.env.ref("pms.pms_room_type_2").id, + "partner_id": cls.host1.id, + "adults": 2, + "pms_property_id": cls.env.ref("pms.main_pms_property").id, + "folio_id": cls.folio_1.id, + } + reservation3_vals = { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "room_type_id": cls.env.ref("pms.pms_room_type_2").id, + "partner_id": cls.host1.id, + "adults": 2, + "pms_property_id": cls.env.ref("pms.main_pms_property").id, + "folio_id": cls.folio_1.id, + } + cls.reservation_1 = ( + cls.env["pms.reservation"].with_user(demo_user).create(reservation1_vals) + ) + cls.reservation_2 = ( + cls.env["pms.reservation"].with_user(demo_user).create(reservation2_vals) + ) + cls.reservation_3 = ( + cls.env["pms.reservation"].with_user(demo_user).create(reservation3_vals) + ) + + def test_count_pending_arrival_persons(self): + + # ARRANGE + self.arrange_folio_reservations() + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation_1.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation_1.id, + } + ) + self.checkin3 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host3.id, + "reservation_id": self.reservation_1.id, + } + ) + + # ACT + self.checkin1.action_on_board() + self.checkin2.action_on_board() + + # ASSERT + self.assertEqual( + self.reservation_1.count_pending_arrival, + 1, + "Fail the count pending arrival on reservation", + ) + self.assertEqual( + self.reservation_1.checkins_ratio, + int(2 * 100 / 3), + "Fail the checkins ratio on reservation", + ) + + def test_complete_checkin_data(self): + + # ARRANGE + self.arrange_folio_reservations() + + # ACT + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation_1.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation_1.id, + } + ) + pending_checkin_data = self.reservation_1.pending_checkin_data + ratio_checkin_data = self.reservation_1.ratio_checkin_data + # ASSERT + self.assertEqual( + pending_checkin_data, + 1, + "Fail the count pending checkin data on reservation", + ) + self.assertEqual( + ratio_checkin_data, + int(2 * 100 / 3), + "Fail the checkins data ratio on reservation", + ) + + def test_auto_no_show(self): + + # ARRANGE + self.arrange_folio_reservations() + PmsReservation = self.env["pms.reservation"] + + # ACTION + freezer = freeze_time("2012-01-15 10:00:00") + freezer.start() + PmsReservation.auto_no_show() + + no_show_reservations = PmsReservation.search([("state", "=", "no_show")]) + + # ASSERT + self.assertEqual( + len(no_show_reservations), + 3, + "Reservations not set like No Show", + ) + freezer.stop() + + def test_auto_no_checkout(self): + + # ARRANGE + self.arrange_single_checkin() + PmsReservation = self.env["pms.reservation"] + self.checkin1.action_on_board() + + # ACTION + freezer = freeze_time("2012-01-17 12:00:00") + freezer.start() + PmsReservation.auto_no_checkout() + + no_checkout_reservations = PmsReservation.search( + [("state", "=", "no_checkout")] + ) + freezer.stop() + # ASSERT + self.assertEqual( + len(no_checkout_reservations), + 1, + "Reservations not set like No checkout", + ) diff --git a/pms/tests/test_pms_folio.py b/pms/tests/test_pms_folio.py new file mode 100644 index 000000000..cd1bd45ac --- /dev/null +++ b/pms/tests/test_pms_folio.py @@ -0,0 +1,58 @@ +import datetime + +from freezegun import freeze_time + +from .common import TestHotel + +freeze_time("2000-02-02") + + +class TestPmsFolio(TestHotel): + def test_commission_and_partner_correct(self): + # ARRANGE + PmsFolio = self.env["pms.folio"] + PmsReservation = self.env["pms.reservation"] + PmsPartner = self.env["res.partner"] + PmsSaleChannel = self.env["pms.sale.channel"] + # ACT + saleChannel = PmsSaleChannel.create( + {"name": "saleChannel1", "channel_type": "indirect"} + ) + agency = PmsPartner.create( + { + "name": "partner1", + "is_agency": True, + "invoice_agency": True, + "default_commission": 15, + "sale_channel_id": saleChannel.id, + } + ) + + reservation = PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "agency_id": agency.id, + } + ) + folio = PmsFolio.create( + { + "agency_id": agency.id, + "reservation_ids": [reservation.id], + } + ) + + commission = 0 + for reservation in folio: + commission += reservation.commission_amount + + # ASSERT + self.assertEqual( + folio.commission, + commission, + "Folio commission don't math with his reservation commission", + ) + if folio.agency_id: + self.assertEqual( + folio.agency_id, folio.partner_id, "Agency has to be the partner" + ) diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py new file mode 100644 index 000000000..f482b773c --- /dev/null +++ b/pms/tests/test_pms_sale_channel.py @@ -0,0 +1,103 @@ +import datetime + +from freezegun import freeze_time + +from odoo.exceptions import ValidationError + +from .common import TestHotel + + +@freeze_time("2010-01-01") +class TestPmsSaleChannel(TestHotel): + def test_not_agency_as_agency(self): + # ARRANGE + PmsReservation = self.env["pms.reservation"] + not_agency = self.env["res.partner"].create( + {"name": "partner1", "is_agency": False} + ) + + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "agency_id": not_agency.id, + } + ) + + def test_partner_as_direct_channel(self): + # ARRANGE + PmsReservation = self.env["pms.reservation"] + partner = self.env.ref("base.res_partner_12") + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.timedelta(days=3), + "channel_type_id": partner.id, + } + ) + + def test_channel_type_id_only_directs(self): + # ARRANGE + PmsReservation = self.env["pms.reservation"] + PmsSaleChannel = self.env["pms.sale.channel"] + # ACT + saleChannel = PmsSaleChannel.create({"channel_type": "direct"}) + reservation = PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3), + "channel_type_id": saleChannel.id, + } + ) + # ASSERT + self.assertEqual( + self.browse_ref(reservation.channel_type_id).channel_type, + "direct", + "Sale channel is not direct", + ) + + def test_agency_id_is_agency(self): + # ARRANGE + PmsReservation = self.env["pms.reservation"] + + # ACT + agency = self.env["res.partner"].create({"name": "partner1", "is_agency": True}) + reservation = PmsReservation.create( + { + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3), + "agency_id": agency.id, + } + ) + # ASSERT + self.assertEqual( + self.browse_ref(reservation.agency_id).is_agency, + True, + "Agency_id doesn't correspond to an agency", + ) + + def test_sale_channel_id_only_indirect(self): + # ARRANGE + PmsSaleChannel = self.env["pms.sale.channel"] + # ACT + saleChannel = PmsSaleChannel.create({"channel_type": "indirect"}) + agency = self.env["res.partner"].create( + {"name": "example", "is_agency": True, "sale_channel_id": saleChannel.id} + ) + # ASSERT + self.assertEqual( + self.browse_ref(agency.sale_channel_id).channel_type, + "indirect", + "An agency should be a indirect channel", + ) + + def test_agency_without_sale_channel_id(self): + # ARRANGE & ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.env["res.partner"].create( + {"name": "example", "is_agency": True, "sale_channel_id": None} + ) diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index f17d2deed..6e985c9ae 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -4,7 +4,8 @@ id="action_checkin_partner" name="Action checkin" res_model="pms.checkin.partner" - view_mode="tree,form" + view_mode="kanban,tree,form" + domain="[('state', '!=', 'draft')]" /> pms.checkin.partner
+
+ +
@@ -27,13 +31,13 @@ domain="[('is_company','=', False)]" /> - - - - + + + + @@ -47,7 +51,6 @@ - + - - + + - + @@ -89,9 +92,10 @@ class="oe_stat_button" icon="fa fa-2x fa-check-circle" name="action_on_board" - attrs="{'invisible':[('state','not in', ['draft'])]}" + attrs="{'invisible':[('state','not in', ['preconfirm'])]}" help="Get in" /> + - - + + + + pms.checkin.partner.kanban + pms.checkin.partner + + + + + + + + + + + + + + + + + +
+
+ Contact image + + + Draft + Cancelled + +
+
+
+
+ +
+ +
+ + +
+ + + + + + +
+
+ +
    +
  • + +
  • +
  • + +
  • +
+ +
+
+
+
+
+
+
pms.checkin.partner.search pms.checkin.partner @@ -129,21 +259,21 @@ @@ -167,36 +297,44 @@ + + +
diff --git a/pms/views/pms_folio_views.xml b/pms/views/pms_folio_views.xml index c4b310a55..aed069ffc 100644 --- a/pms/views/pms_folio_views.xml +++ b/pms/views/pms_folio_views.xml @@ -98,14 +98,16 @@ name="reservation_type" attrs="{'readonly':[('state','not in',('draft'))]}" /> - + />--> + + @@ -278,9 +280,6 @@ - - - @@ -295,27 +294,6 @@ -
    -
  • - -
  • - - - - - - - - -
diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 928d8659f..055a51b92 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -14,6 +14,7 @@
+ - + - + />--> + + + + + + + @@ -516,10 +527,10 @@ - + />--> @@ -539,12 +550,12 @@ pms.reservation @@ -552,108 +563,6 @@ - - - pms.reservation.checkin.form - pms.reservation - 100 - - - - - - - - - - - - - - - Cancelled Reservation! - - - OverBooking! - -

- - -

- From -

-

- - - - -
- -
-
pms.reservation.tree pms.reservation @@ -669,12 +578,7 @@ - + - - - + + + + + + + + + + + + + + +