Merge branch '14.0' into 14.0-pms_pricelist_rules_priority

This commit is contained in:
Darío Lodeiros
2020-12-01 12:32:44 +01:00
committed by GitHub
26 changed files with 1645 additions and 519 deletions

View File

@@ -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",

View File

@@ -1,6 +1,35 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="0">
<!-- Set reservation like No Show if the client does not show up -->
<record model="ir.cron" id="noshow_reservations">
<field name="name">Automatic No Show Reservation</field>
<field name="interval_number">1</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_pms_reservation" />
<field
name="nextcall"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 09:00:00')"
/>
<field name="code">model.auto_no_show()</field>
</record>
<!-- Set reservation like No Checout if checkout is not confirmed-->
<record model="ir.cron" id="nocheckout_reservations">
<field name="name">Automatic No Checkout Reservations</field>
<field name="interval_number">5</field>
<field name="user_id" ref="base.user_root" />
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False" />
<field name="state">code</field>
<field name="model_id" ref="model_pms_reservation" />
<field name="nextcall" eval="DateTime.now()" />
<field name="code">model.auto_no_checkout()</field>
</record>
<!-- Scheduler For To Inform Guest About Reservation Before 24 Hours -->
<record model="ir.cron" id="autocheckout_reservations">
<field name="name">Automatic Checkout on past reservations</field>

View File

@@ -34,5 +34,22 @@
<field name="pms_property_ids" eval="[(4, ref('main_pms_property'))]" />
<field name="groups_id" eval="[(4,ref('pms.group_pms_manager'))]" />
</record>
<!-- pms.sale.channel-->
<record id="main_pms_sale_channel_0" model="pms.sale.channel">
<field name="name">Door</field>
<field name="channel_type">direct</field>
</record>
<record id="main_pms_sale_channel_1" model="pms.sale.channel">
<field name="name">Phone</field>
<field name="channel_type">direct</field>
</record>
<record id="main_pms_sale_channel_2" model="pms.sale.channel">
<field name="name">Mail</field>
<field name="channel_type">direct</field>
</record>
<record id="main_pms_sale_channel_3" model="pms.sale.channel">
<field name="name">Agency</field>
<field name="channel_type">indirect</field>
</record>
</data>
</odoo>

View File

@@ -34,7 +34,7 @@
/>
</record>
<!-- reservation of 3 single rooms for 3 people with 1 cancelled -->
<!-- TODO: The third reservation is marked from State: Cancelled to Pending Entry at Folio creation -->
<!-- TODO: The third reservation is marked from State: Cancelled to Pending arrival at Folio creation -->
<record id="pms_folio_2" model="pms.folio">
<field name="partner_id" ref="base.res_partner_address_10" />
<field
@@ -344,7 +344,7 @@
/>
</record>
<!--Reservation of the conference room whit cancelled-->
<!-- TODO: The reservation is marked from State: Cancelled to Pending Entry at Folio creation -->
<!-- TODO: The reservation is marked from State: Cancelled to Pending arrival at Folio creation -->
<record id="pms_folio_15" model="pms.folio">
<field name="partner_id" ref="base.res_partner_18" />
<field name="reservation_type">normal</field>
@@ -381,7 +381,7 @@
/>
</record>
<!--Reservation of triple room whit draft state-->
<!-- TODO: The reservation is marked from State: Pre-reservation to Pending Entry at Folio creation -->
<!-- TODO: The reservation is marked from State: Pre-reservation to Pending arrival at Folio creation -->
<record id="pms_folio_17" model="pms.folio">
<field name="partner_id" ref="base.res_partner_address_32" />
<field name="reservation_type">normal</field>

View File

@@ -24,6 +24,20 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_0" />
<field name="adults">2</field>
<field name="state">onboard</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {
'partner_id': ref('base.res_partner_address_28'),
'state': 'onboard'
}),
(0, 0, {
'partner_id': ref('base.res_partner_12'),
'state': 'onboard'
}),
]"
/>
<field name="checkin" eval="DateTime.today()" />
<field name="checkout" eval="DateTime.today() + timedelta(1)" />
<field name="board_service_room_id" ref="pms_board_service_room_1" />
@@ -58,6 +72,15 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_1" />
<field name="adults">1</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {
'partner_id': ref('base.res_partner_address_27'),
'state': 'onboard'
}),
]"
/>
<field name="checkin" eval="DateTime.today()" />
<field name="checkout" eval="DateTime.today() + timedelta(3)" />
<field name="board_service_room_id" ref="pms_board_service_room_2" />
@@ -67,6 +90,14 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_1" />
<field name="adults">1</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {
'partner_id': ref('base.res_partner_address_10'),
}),
]"
/>
<field name="checkin" eval="DateTime.today()" />
<field name="checkout" eval="DateTime.today() + timedelta(3)" />
<field name="board_service_room_id" ref="pms_board_service_room_0" />
@@ -129,6 +160,13 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_2" />
<field name="adults">2</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {'partner_id': ref('base.res_partner_12')}),
(0, 0, {'partner_id': ref('base.res_partner_18')}),
]"
/>
<field name="checkin" eval="DateTime.today() + timedelta(6)" />
<field name="checkout" eval="DateTime.today() + timedelta(8)" />
</record>
@@ -164,6 +202,13 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_2" />
<field name="adults">2</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {'partner_id': ref('base.res_partner_18')}),
(0, 0, {'partner_id': ref('base.res_partner_12')}),
]"
/>
<field name="checkin" eval="DateTime.today() + timedelta(11)" />
<field name="checkout" eval="DateTime.today() + timedelta(13)" />
</record>
@@ -217,6 +262,14 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_3" />
<field name="adults">3</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {'partner_id': ref('base.res_partner_10')}),
(0, 0, {'partner_id': ref('base.res_partner_address_10')}),
(0, 0, {'partner_id': ref('base.res_partner_address_18')}),
]"
/>
<field name="checkin" eval="DateTime.today() + timedelta(6)" />
<field name="checkout" eval="DateTime.today() + timedelta(8)" />
</record>
@@ -225,6 +278,12 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_3" />
<field name="adults">1</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {'partner_id': ref('base.res_partner_3')}),
]"
/>
<field name="children">2</field>
<field name="checkin" eval="DateTime.today() + timedelta(10)" />
<field name="checkout" eval="DateTime.today() + timedelta(11)" />
@@ -235,6 +294,14 @@
<field name="pricelist_id" ref="product.list0" />
<field name="room_type_id" ref="pms_room_type_3" />
<field name="adults">3</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {'partner_id': ref('base.res_partner_3')}),
(0, 0, {'partner_id': ref('base.res_partner_address_14')}),
(0, 0, {'partner_id': ref('base.res_partner_address_33')}),
]"
/>
<field name="checkin" eval="DateTime.today() + timedelta(4)" />
<field name="checkout" eval="DateTime.today() + timedelta(6)" />
</record>
@@ -245,6 +312,19 @@
<field name="adults">2</field>
<field name="children">1</field>
<field name="state">onboard</field>
<field
name="checkin_partner_ids"
eval="[(5, 0),
(0, 0, {
'partner_id': ref('base.res_partner_12'),
'state': 'onboard'
}),
(0, 0, {
'partner_id': ref('base.res_partner_2'),
'state': 'onboard'
}),
]"
/>
<field name="checkin" eval="DateTime.today()" />
<field name="checkout" eval="DateTime.today() + timedelta(4)" />
<field name="board_service_room_id" ref="pms_board_service_room_2" />

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"))

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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"
)

View File

@@ -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"

View File

@@ -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

View File

@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
8 user_access_room_closure_reason user_access_room_closure_reason model_room_closure_reason pms.group_pms_user 1 0 0 0
9 user_access_pms_service_line user_access_pms_service_line model_pms_service_line pms.group_pms_user 1 1 1 1
10 user_access_pms_board_service user_access_pms_board_service model_pms_board_service pms.group_pms_user 1 0 0 0
11 user_access_pms_checkin_partner user_access_pms_checkin_partner model_pms_checkin_partner pms.group_pms_user 1 1 1 1 0
12 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
13 user_access_pms_room user_access_pms_room model_pms_room pms.group_pms_user 1 0 0 0
14 user_access_shared_pms_room user_access_pms_shared_room model_pms_shared_room pms.group_pms_user 1 0 0 0
24 user_access_account_full_reconcile user_access_account_full_reconcile account.model_account_full_reconcile pms.group_pms_user 1 1 1 1
25 user_access_property user_access_property model_pms_property pms.group_pms_user 1 0 0 0
26 user_access_availability user_access_availability model_pms_room_type_availability pms.group_pms_user 1 0 0 0
27 user_access_pms_sale_channel user_access_pms_sale_channel model_pms_sale_channel pms.group_pms_user 1 0 0 0
28 manager_access_pms_floor manager_access_pms_floor model_pms_floor pms.group_pms_manager 1 1 1 1
29 manager_access_pms_amenity manager_access_pms_amenity model_pms_amenity pms.group_pms_manager 1 1 1 1
30 manager_access_pms_amenity_type manager_access_pms_amenity_type model_pms_amenity_type pms.group_pms_manager 1 1 1 1
34 manager_access_room_closure_reason manager_access_room_closure_reason model_room_closure_reason pms.group_pms_manager 1 1 1 1
35 manager_access_pms_service_line manager_access_pms_service_line model_pms_service_line pms.group_pms_manager 1 1 1 1
36 manager_access_pms_board_service manager_access_pms_board_service model_pms_board_service pms.group_pms_manager 1 1 1 1
37 manager_access_pms_checkin_partner manager_access_pms_checkin_partner model_pms_checkin_partner pms.group_pms_manager 1 1 1 1 0
38 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
39 manager_access_pms_room manager_access_pms_room model_pms_room pms.group_pms_manager 1 1 1 1
40 manager_access_pms_shared_room manager_access_pms_shared_room model_pms_shared_room pms.group_pms_manager 1 1 1 1
48 manager_access_property manager_access_property model_pms_property pms.group_pms_manager 1 1 1 1
49 manager_access_pms_cancelation_rule manager_access_pms_cancelation_rule model_pms_cancelation_rule pms.group_pms_manager 1 1 1 1
50 manager_access_availability manager_access_availability model_pms_room_type_availability pms.group_pms_manager 1 1 1 1
51 manager_access_pms_sale_channel manager_access_pms_sale_channel model_pms_sale_channel pms.group_pms_manager 1 1 1 1
52 user_access_pms_reservation_wizard user_access_pms_reservation_wizard model_pms_reservation_wizard pms.group_pms_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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"
)

View File

@@ -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}
)

View File

@@ -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')]"
/>
<menuitem
id="menu_pms_checkin_partner"
@@ -18,6 +19,9 @@
<field name="model">pms.checkin.partner</field>
<field name="arch" type="xml">
<form create="false">
<header>
<field name="state" widget="statusbar" />
</header>
<sheet>
<group name="group_top">
<group name="group_left">
@@ -27,13 +31,13 @@
domain="[('is_company','=', False)]"
/>
<field name="pms_property_id" invisible="1" />
<field name="enter_date" />
<field name="exit_date" />
<field name="arrival_hour" />
<field name="departure_hour" />
<field name="arrival" />
<field name="departure" />
</group>
<group name="group_left">
<field name="reservation_id" />
<field name="folio_id" />
<field name="identifier" />
</group>
</group>
</sheet>
@@ -47,7 +51,6 @@
<field name="arch" type="xml">
<tree
editable="bottom"
create="1"
decoration-danger="state == 'draft'"
decoration-info="state == 'done'"
decoration-muted="state == 'cancelled'"
@@ -56,20 +59,20 @@
<button
type="object"
class="oe_read_only oe_stat_button"
icon="fa fa-2x fa-check-circle"
icon="fa-2x fa-check-circle"
name="action_on_board"
help="Get in"
attrs="{'invisible': [('state','!=','draft')]}"
attrs="{'invisible': [('state','!=','precheckin')]}"
/>
<field name="auto_booking" invisible="1" />
<field name="identifier" />
<field name="partner_id" required="True" />
<field name="mobile" />
<field name="email" />
<field name="enter_date" />
<field name="exit_date" />
<field name="arrival" />
<field name="departure" />
<field name="reservation_id" invisible="1" />
<field name="folio_id" force_save="1" invisible="1" />
<field name="state" invisible="1" />
<field name="state" invisible="0" />
</tree>
</field>
</record>
@@ -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"
/>
<field name="identifier" />
<field
name="partner_id"
required="True"
@@ -99,14 +103,140 @@
/>
<field name="mobile" />
<field name="email" />
<field name="enter_date" />
<field name="exit_date" />
<field name="arrival" />
<field name="departure" />
<field name="reservation_id" />
<field name="folio_id" force_save="1" invisible="1" />
<field name="state" />
</tree>
</field>
</record>
<record model="ir.ui.view" id="pms_checkin_partner_kanban_view">
<field name="name">pms.checkin.partner.kanban</field>
<field name="model">pms.checkin.partner</field>
<field name="arch" type="xml">
<kanban
default_group_by="state"
class="o_res_partner_kanban"
sample="1"
create="false"
>
<field name="id" />
<field name="identifier" />
<field name="partner_id" />
<field name="reservation_id" />
<field name="folio_id" />
<field name="pms_property_id" />
<field name="name" />
<field name="email" />
<field name="mobile" />
<field name="arrival" />
<field name="departure" />
<field name="state" />
<field name="image_128" />
<templates>
<t t-name="kanban-box">
<div
class="oe_kanban_global_click o_kanban_record_has_image_fill o_res_partner_kanban"
>
<div class="o_kanban_image">
<img
alt="Contact image"
t-if="record.image_128.raw_value"
t-att-src="kanban_image('pms.checkin.partner', 'image_128', record.id.raw_value)"
/>
<t t-if="!record.image_128.raw_value">
<!--TODO: Use npm avatar generation? https://github.com/Ashwinvalento/cartoon-avatar-->
<img
alt="Draft"
t-if="record.state.raw_value === 'draft'"
t-att-src="&quot;pms/static/description/avatar.png&quot;"
/>
<img
alt="Cancelled"
t-if="record.state.raw_value === 'cancelled'"
t-att-src="&quot;pms/static/description/avatar.png&quot;"
/>
</t>
</div>
<div class="oe_kanban_details">
<div class="o_kanban_record_top">
<div class="o_kanban_record_headings">
<strong class="o_kanban_record_title">
<div class="float-right">
<field
name="state"
widget="label_selection"
options="{'classes': {'draft': 'default', 'precheckin': 'info', 'onboard': 'warning', 'onboard': 'success', 'done': 'secondary'}}"
/>
</div>
<field
name="name"
placeholder="Hosted's Name"
/>
<field
name="reservation_id"
placeholder="Room Reservation"
/>
</strong>
<span
t-if="record.arrival.raw_value"
class="o_kanban_record_subtitle"
>
<field name="arrival" />
</span>
<span
t-if="record.departure.raw_value"
class="o_kanban_record_subtitle"
>
<field name="departure" />
</span>
</div>
</div>
<field
name="segmentation_ids"
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
<ul>
<li
t-if="record.email.raw_value"
class="o_kanban_record_subtitle"
>
<field name="email" />
</li>
<li
t-if="record.mobile.raw_value"
class="o_kanban_record_subtitle"
>
<field name="mobile" />
</li>
</ul>
<div class="oe_kanban_bottom_right">
<a
name="action_on_board"
tabindex="-1"
type="object"
attrs="{'invisible': [('state', '!=', 'precheckin')]}"
class="o_project_kanban_box"
>
<div>
<span class="o_label">
<i
class="fa fa-lg fa-play-circle text-success"
/>
Check-in
</span>
</div>
</a>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record model="ir.ui.view" id="pms_checkin_partner_view_search">
<field name="name">pms.checkin.partner.search</field>
<field name="model">pms.checkin.partner</field>
@@ -129,21 +259,21 @@
<filter
string="Checkins Tomorrow"
name="enter_tomorrow"
domain="[('enter_date', '=', (context_today()+datetime.timedelta(days=1)).strftime('%Y-%m-%d')),
domain="[('arrival', '=', (context_today()+datetime.timedelta(days=1)).strftime('%Y-%m-%d')),
('state', '=', 'confirm')]"
help="Show all checkins for enter tomorrow"
/>
<filter
string="Checkins to 7 days"
name="next_res_week"
domain="[('enter_date', '&lt;', (context_today()+datetime.timedelta(days=7)).strftime('%Y-%m-%d')),
domain="[('arrival', '&lt;', (context_today()+datetime.timedelta(days=7)).strftime('%Y-%m-%d')),
('state', '=', 'confirm')]"
help="Show all reservations for which date enter is before than 7 days"
/>
<filter
string="On Board Tomorrow"
name="next_res_2week"
domain="[('enter_date', '&lt;', (context_today()+datetime.timedelta(days=14)).strftime('%Y-%m-%d')),
domain="[('arrival', '&lt;', (context_today()+datetime.timedelta(days=14)).strftime('%Y-%m-%d')),
('state', 'in', ['confirm','onboard'])]"
help="Show all checkins for Tomorrow"
/>
@@ -167,36 +297,44 @@
<filter
string="Checkin by Month"
name="checkin_by_month"
context="{'group_by':'enter_date', 'default_order': 'enter_date asc'}"
context="{'group_by':'arrival', 'default_order': 'arrival asc'}"
/>
<filter
string="Checkin by Week"
name="checkin_by_week"
context="{'group_by':'enter_date:week', 'default_order': 'enter_date'}"
context="{'group_by':'arrival:week', 'default_order': 'arrival'}"
/>
<filter
string="Checkin by Day"
name="checkin_by_week"
context="{'group_by':'enter_date:day', 'default_order': 'enter_date'}"
context="{'group_by':'arrival:day', 'default_order': 'arrival'}"
/>
<separator />
<filter
string="Checkout by Month"
name="checkout_by_month"
context="{'group_by':'exit_date', 'default_order': 'exit_date asc'}"
context="{'group_by':'departure', 'default_order': 'departure asc'}"
/>
<filter
string="Checkout by Week"
name="checkout_by_week"
context="{'group_by':'exit_date:week', 'default_order': 'exit_date'}"
context="{'group_by':'departure:week', 'default_order': 'departure'}"
/>
<filter
string="Checkout by Day"
name="checkout_by_week"
context="{'group_by':'exit_date:day', 'default_order': 'exit_date'}"
context="{'group_by':'departure:day', 'default_order': 'departure'}"
/>
<separator />
</group>
<searchpanel>
<field
name="state"
string="State"
enable_counters="1"
select="multi"
/>
</searchpanel>
</search>
</field>
</record>

View File

@@ -98,14 +98,16 @@
name="reservation_type"
attrs="{'readonly':[('state','not in',('draft'))]}"
/>
<field
<!--<field
name="channel_type"
attrs="{'required':[('reservation_type','=','normal')]}"
/>
<field
name="agency_id"
options="{'no_create': True,'no_open': True}"
/>
/>-->
<field name="agency_id" />
<field name="channel_type_id" />
</group>
<group>
<field name="partner_internal_comment" />
@@ -278,9 +280,6 @@
<field name="pending_amount" />
<!-- <field name="refund_amount" /> -->
<field name="invoices_paid" />
<field name="booking_pending" />
<field name="checkin_partner_count" />
<field name="checkin_partner_pending_count" />
<field name="partner_internal_comment" />
<field name="cancelled_reason" />
<field name="prepaid_warning_days" />
@@ -295,27 +294,6 @@
<strong class="oe_partner_heading">
<field name="partner_id" />
</strong>
<ul>
<li t-if="record.name.raw_value">
<field name="name" />
</li>
<span
t-if="record.checkin_partner_count.value&gt;0"
class="badge"
>
<i class="fa fa-fw fa-bed" />
<t t-esc="record.checkin_partner_count.value" />
</span>
<span
t-if="record.checkin_partner_pending_count.value&gt;0"
class="badge"
>
<i class="fa fa-fw fa-user-plus" />
<t
t-esc="record.checkin_partner_pending_count.value"
/>
</span>
</ul>
<div class="oe_kanban_partner_links" />
</div>
</div>

View File

@@ -14,6 +14,7 @@
<header>
<field name="splitted" invisible="True" />
<field name="tax_ids" invisible="1" />
<field name="checkin_today" invisible="1" />
<field name="checkin_partner_count" invisible="1" />
<button
name="confirm"
@@ -30,7 +31,7 @@
/>
<button
name="action_reservation_checkout"
string="Done"
string="Check Out"
states="onboard"
type="object"
/>
@@ -79,11 +80,24 @@
</div>
<sheet>
<field name="shared_folio" invisible="1" />
<field name="left_for_checkin" invisible="1" />
<field name="ready_for_checkin" invisible="1" />
<field name="departure_today" invisible="1" />
<div
class="oe_button_box"
name="button_box"
attrs="{'invisible': [('folio_id','=',False)]}"
>
<button
type="object"
class="oe_stat_button"
icon="fa-sign-out"
name="action_reservation_checkout"
>
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Checkout</span>
</div>
</button>
<button
type="object"
class="oe_stat_button"
@@ -96,18 +110,6 @@
</span>
</div>
</button>
<button
type="action"
class="oe_stat_button"
icon="fa-list-ul"
attrs="{'invisible': ['|', ('partner_id','=',False), ('reservation_type','in',('out'))]}"
name="%(open_pms_reservation_form_tree_all)d"
context="{'search_default_partner_id': partner_id}"
>
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">Books</span>
</div>
</button>
<button
type="object"
class="oe_stat_button"
@@ -131,20 +133,30 @@
<button
type="object"
class="oe_stat_button"
icon="fa-users"
name="action_checks"
attrs="{'invisible': [('checkin_partner_pending_count','&lt;=',0)]}"
name="action_onboard"
attrs="{'invisible': [
('ready_for_checkin', '!=', True),
]}"
>
<div class="o_form_field o_stat_info">
<span class="o_stat_value">
<field
name="checkin_partner_pending_count"
widget="statinfo"
nolabel="1"
/>
</span>
<span class="o_stat_text">Pending Checks</span>
</div>
<field
name="checkins_ratio"
string="On Board"
widget="percentpie"
/>
</button>
<button
type="object"
class="oe_stat_button"
name="action_checks"
attrs="{'invisible': [
('left_for_checkin', '!=', True),
]}"
>
<field
name="ratio_checkin_data"
string="Checkin Data"
widget="percentpie"
/>
</button>
</div>
<span
@@ -243,19 +255,6 @@
'readonly': [('partner_id', '!=', False),
('mobile','!=', False)]}"
/>
<field
name="phone"
colspan="2"
nolabel="1"
placeholder="phone"
widget="phone"
force_save="1"
attrs="{'invisible': [('reservation_type','in',('out'))],
'required': [('channel_type','in',('door','mail','phone')),
('mobile','=','')],
'readonly': [('partner_id', '!=', False),
('phone','!=', False)]}"
/>
<field
placeholder="Partner Notes"
colspan="2"
@@ -285,10 +284,10 @@
name="pricelist_id"
attrs="{'invisible': [('reservation_type','in',('out'))]}"
/>
<field
<!--<field
name="agency_id"
options="{'no_create': True,'no_open': True}"
/>
/>-->
<field
name="cancelled_reason"
attrs="{'invisible': [('state', 'not in', ('cancelled'))]}"
@@ -329,6 +328,8 @@
nolabel="1"
placeholder="Reservation Notes"
/>
<field name="agency_id" />
<field name="channel_type_id" />
</group>
<group
colspan="2"
@@ -368,6 +369,17 @@
string="Only Room"
widget="monetary"
/>
<field
name="commission_percent"
string="Commission percent (%)"
>
<field
name="commission_amount"
string="Commission Amount"
>
</field>
</field>
<field name="invoice_status" invisible="1" />
<field name="currency_id" invisible="1" />
<field
@@ -490,11 +502,10 @@
</group>
<field
name="checkin_partner_ids"
mode="kanban"
options="{'no_create': True, 'no_delete': True}"
context="{
'checkin_partner_ids': checkin_partner_ids,
'default_reservation_id': id,
'reservation_id': id,
'tree_view_ref':'pms.pms_checkin_partner_reservation_view_tree',
'kanban_view_ref':'pms.pms_checkin_partner_kanban_view',
}"
/>
</page>
@@ -516,10 +527,10 @@
<field name="overbooking" />
</group>
<group>
<field
<!--<field
name="channel_type"
attrs="{'required':[('reservation_type','not in',('staff','out'))]}"
/>
/>-->
<field name="reservation_type" />
</group>
</page>
@@ -539,12 +550,12 @@
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<calendar
date_start="checkin"
date_stop="checkout"
date_start="checkin_datetime"
date_stop="checkout_datetime"
string="Reservations"
quick_add="False"
mode="month"
scales="month,year"
scales="week,month,year"
>
<field name="partner_id" avatar_field="image_128" />
<field name="room_type_id" />
@@ -552,108 +563,6 @@
</calendar>
</field>
</record>
<!-- Form view Checkin Partners from reservation -->
<record model="ir.ui.view" id="pms_reservation_checkin_view_form">
<field name="name">pms.reservation.checkin.form</field>
<field name="model">pms.reservation</field>
<field name="priority">100</field>
<field name="arch" type="xml">
<form string="Reservation">
<field name="reservation_type" invisible="1" />
<field name="splitted" invisible="1" />
<field name="state" invisible="1" />
<field name="overbooking" invisible="1" />
<field name="folio_id" invisible="1" />
<field name="adults" invisible="1" />
<field name="children" invisible="1" />
<div
class="alert alert-info"
role="alert"
style="margin-bottom:0px;"
attrs="{'invisible': ['|',('shared_folio','=',False),('splitted', '=', True)]}"
>
This reservation has other reservantions and/or services in the
folio, you can check it in the
<bold>
<button
class="alert-link"
type="object"
name="open_folio"
string="Folio Form"
/>
</bold>
</div>
<field name="shared_folio" invisible="1" />
<field name="allowed_room_ids" invisible="1" />
<sheet>
<span
class="label label-danger"
attrs="{'invisible': [('state', 'not in', ('cancelled'))]}"
>
Cancelled Reservation!
</span>
<span
class="label label-warning"
attrs="{'invisible': [('overbooking', '=', False)]}"
>
OverBooking!
</span>
<h2>
<field
name="preferred_room_id"
readonly="1"
nolabel="1"
options="{'no_create': True,'no_open': True}"
style="margin-right: 30px;"
/>
<!-- <field
name="partner_id"
readonly="1"
options="{'no_open': True}"
/>
<span
class="fa-user"
style="margin-left:20px;"
attrs="{'invisible': [('reservation_type','not in',('normal'))]}"
/>
<span
class="fa-black-tie"
style="margin-left:20px; color: #C67;"
attrs="{'invisible': [('reservation_type','not in',('staff'))]}"
/> -->
<h3>
From
<span class="fa-sign-in" style="margin: 5px;" />
<field
name="checkin"
style="margin-right: 10px;"
readonly="1"
/>
to
<span class="fa-sign-out" style="margin-right: 5px;" />
<field name="checkout" readonly="1" />
</h3>
</h2>
<group>
<field
name="segmentation_ids"
widget="many2many_tags"
placeholder="Segmentation..."
options="{'no_create': True,'no_open': True}"
/>
</group>
<field
name="checkin_partner_ids"
context="{
'default_reservation_id': id,
'reservation_id': id,
'tree_view_ref':'pms.pms_checkin_partner_reservation_view_tree',
}"
/>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="pms_reservation_view_tree">
<field name="name">pms.reservation.tree</field>
<field name="model">pms.reservation</field>
@@ -669,12 +578,7 @@
<field name="splitted" invisible="1" />
<field name="pricelist_id" invisible="1" />
<field name="rooms" />
<field
name="checkin"
widget="daterange"
class="oe_inline"
options="{'related_end_date': 'checkout'}"
/>
<field name="checkin" />
<field name="checkout" widget="date" />
<field name="nights" />
<button
@@ -687,7 +591,8 @@
<field name="allowed_room_ids" invisible="1" />
<field name="partner_id" />
<field name="room_type_id" invisible="1" />
<field name="adults" string="Persons" />
<field name="adults" />
<field name="ratio_checkin_data" widget="progressbar" optional="show" />
<field name="overbooking" invisible="1" />
<field name="activity_ids" widget="list_activity" optional="show" />
<field
@@ -711,8 +616,12 @@
/>
<field
name="state"
decoration-success="state == 'done'"
decoration-success="state == 'onboard'"
decoration-info="state == 'confirm'"
decoration-primary="state == 'no_checkout'"
decoration-danger="state == 'cancelled'"
decoration-bf="state == 'draft'"
decoration-warning="state == 'no_show'"
widget="badge"
optional="show"
/>
@@ -793,7 +702,7 @@
<filter
string="To enter"
name="to_enter"
domain="[('state', '=', 'confirm')]"
domain="[('left_for_checkin', '=', True)]"
/>
<filter
string="Overbookings"
@@ -827,7 +736,7 @@
domain="[('to_assign','=',True)]"
/>
<separator />
<filter
<!--<filter
string="Web"
name="web"
domain="[('channel_type', '=', 'web')]"
@@ -846,7 +755,7 @@
string="Phone"
name="phone"
domain="[('channel_type', '=', 'phone')]"
/>
/>-->
<separator />
<filter
string="Still to be paid"

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record model="ir.ui.view" id="pms_sale_channel_view_form">
<field name="name">pms.sale.channel.form</field>
<field name="model">pms.sale.channel</field>
<field name="arch" type="xml">
<form string="Sale Channel">
<sheet>
<group>
<field name="name" colspan="1" />
<field name="channel_type" />
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="pms_sale_channel_view_tree">
<field name="name">pms.sale.channel.tree</field>
<field name="model">pms.sale.channel</field>
<field name="arch" type="xml">
<tree string=" Sale Channel">
<field name="name" />
<field name="channel_type" />
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="open_pms_sale_channel_form_tree">
<field name="name">Sale Channel</field>
<field name="res_model">pms.sale.channel</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
name="Sale Channel"
id="menu_open_pms_sale_channel_form_tree"
action="open_pms_sale_channel_form_tree"
sequence="21"
parent="pms.configuration_others"
/>
</odoo>

View File

@@ -39,12 +39,32 @@
<field string="Folios" name="folios_count" widget="statinfo" />
</button>
</xpath>
<xpath expr="//field[@name='user_id']" position="after">
<field name="company_type" invisible="1" />
<field
name="is_agency"
attrs="{'invisible': [('company_type','!=','company')]}"
/>
<xpath expr="//field[@name='name']" position="after">
<group>
<field name="is_agency" />
</group>
</xpath>
<xpath expr="//page[@name='internal_notes']" position="after">
<page
name="agency"
string="Agency"
attrs="{'invisible':[('is_agency','!=',True)]}"
>
<group>
<field
name="sale_channel_id"
options="{'no_create': True,'no_open': True}"
/>
<field name="default_commission" />
<!-- <label for="price_discount"/>
<div class="o_row">
<field name="price_discount"/>
<span>%%</span>
</div>-->
<field name="apply_pricelist" />
<field name="invoice_agency" />
</group>
</page>
</xpath>
</field>
</record>