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')]"
/>