Merge pull request #92 from ursais/14.0-ADD-pms_sale

[ADD] pms_sale
This commit is contained in:
Maxime Chambreuil
2022-04-12 12:03:43 -05:00
committed by GitHub
58 changed files with 5734 additions and 0 deletions

View File

@@ -1,2 +1,4 @@
account-financial-tools
contract
stock-logistics-warehouse
web

101
pms_sale/README.rst Normal file
View File

@@ -0,0 +1,101 @@
================================
PMS (Property Management System)
================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github
:target: https://github.com/OCA/pms/tree/14.0/pms
:alt: OCA/pms
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/pms-14-0/pms-14-0-pms
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/293/14.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module is an all-in-one property management system (PMS) focused on medium-sized properties
for managing every aspect of your property's daily operations.
You can manage properties with multi-property and multi-company support, including your rooms inventory,
reservations, check-in, daily reports, board services, rate and availability plans among other property functionalities.
.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_
**Table of contents**
.. contents::
:local:
Installation
============
This module depends on modules ``base``, ``mail``, ``sale`` and ``multi_pms_properties``.
Ensure yourself to have all them in your addons list.
Configuration
=============
You will find the hotel settings in PMS Management > Configuration > Properties > Your Property.
This module required additional configuration for company, accounting, invoicing and user privileges.
Usage
=====
To use this module, please, read the complete user guide at `<roomdoo.com>`_.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/pms/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/pms/issues/new?body=module:%20pms%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Open Source Integrators
Contributors
~~~~~~~~~~~~
* Maxime Chambreuil <mchambreuil@opensourceintegrators.com>
* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/pms <https://github.com/OCA/pms/tree/14.0/pms>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

3
pms_sale/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import models
from . import wizards

34
pms_sale/__manifest__.py Normal file
View File

@@ -0,0 +1,34 @@
# Copyright 2019 Darío Lodeiros, Alexandre Díaz, Jose Luis Algara, Pablo Quesada
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
"name": "PMS - Sale",
"summary": "Manage reservations",
"version": "14.0.1.0.0",
"development_status": "Alpha",
"category": "Generic Modules/Property Management System",
"website": "https://github.com/OCA/pms",
"author": "Commit [Sun], Open Source Integrators, Odoo Community Association (OCA)",
"maintainers": ["eantones"],
"license": "AGPL-3",
"depends": ["pms_account", "sale", "web_timeline", "calendar"],
"data": [
"security/ir.model.access.csv",
"data/ir_sequence.xml",
"data/product_data.xml",
"data/pms_stage.xml",
"views/assets.xml",
"views/product_views.xml",
"views/pms_property_reservation.xml",
"views/pms_mail_views.xml",
"views/pms_property.xml",
"views/pms_reservation_guest_views.xml",
"views/pms_reservation_views.xml",
"wizards/pms_configurator_views.xml",
"views/sale_order_views.xml",
"views/pms_team_views.xml",
"views/menu.xml",
"views/account_move.xml",
],
"qweb": ["static/src/xml/timeline.xml"],
}

View File

@@ -0,0 +1,11 @@
<odoo noupdate="1">
<!-- Sequence for pms.reservation -->
<record id="seq_pms_reservation" model="ir.sequence">
<field name="name">PMS Reservation</field>
<field name="code">pms.reservation</field>
<field name="padding">5</field>
<field name="company_id" eval="False" />
</record>
</odoo>

View File

@@ -0,0 +1,52 @@
<odoo>
<record id="pms_stage_new" model="pms.stage">
<field name="name">New</field>
<field name="sequence">10</field>
<field name="is_default">1</field>
<field name="stage_type">reservation</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
<record id="pms_stage_booked" model="pms.stage">
<field name="name">Booked</field>
<field name="sequence">20</field>
<field name="stage_type">reservation</field>
<field name="custom_color">#FF6961</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
<record id="pms_stage_confirmed" model="pms.stage">
<field name="name">Confirmed</field>
<field name="sequence">30</field>
<field name="stage_type">reservation</field>
<field name="custom_color">#FFB347</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
<record id="pms_stage_checked_in" model="pms.stage">
<field name="name">Checked In</field>
<field name="sequence">40</field>
<field name="stage_type">reservation</field>
<field name="custom_color">#77DD77</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
<record id="pms_stage_checked_out" model="pms.stage">
<field name="name">Checked Out</field>
<field name="sequence">50</field>
<field name="is_closed">1</field>
<field name="stage_type">reservation</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
<record id="pms_stage_cancelled" model="pms.stage">
<field name="name">Cancelled</field>
<field name="sequence">99</field>
<field name="is_closed">1</field>
<field name="fold">1</field>
<field name="stage_type">reservation</field>
<field name="team_ids" eval="[(4, ref('pms_base.pms_team_default'))]" />
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<odoo noupdate="0">
<record id="product_category_reservation" model="product.category">
<field name="parent_id" ref="product.product_category_1" />
<field name="name">Reservations</field>
</record>
<record id="product_product_reservation" model="product.product">
<field name="name">Reservation</field>
<field name="type">service</field>
<field name="reservation_ok" eval="True" />
<field name="categ_id" ref="pms_sale.product_category_reservation" />
</record>
</odoo>

1014
pms_sale/i18n/es.po Normal file

File diff suppressed because it is too large Load Diff

1010
pms_sale/i18n/pms_sale.pot Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import (
product,
pms_property_reservation,
pms_stage,
pms_mail,
pms_property,
pms_reservation_guest,
pms_reservation,
sale_order,
sale_order_line,
pms_team,
account_move,
account_move_line,
)

View File

@@ -0,0 +1,29 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = "account.move"
reservation_count = fields.Integer(
"Reservations Count", compute="_compute_reservation_count"
)
@api.depends("line_ids")
def _compute_reservation_count(self):
for invoice in self:
reservation = invoice.line_ids.mapped("pms_reservation_id")
invoice.reservation_count = len(reservation)
def action_view_reservation_list(self):
for invoice in self:
action = self.env["ir.actions.actions"]._for_xml_id(
"pms_sale.action_pms_reservation"
)
reservation = self.line_ids.mapped("pms_reservation_id")
action["domain"] = [
("id", "in", reservation.ids),
("partner_id", "=", invoice.partner_id.id),
]
return action

View File

@@ -0,0 +1,11 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountMoveLine(models.Model):
_inherit = "account.move.line"
pms_reservation_id = fields.Many2one(
"pms.reservation", string="Reservation", readonly=True, copy=False
)

View File

@@ -0,0 +1,33 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class PMSMailScheduler(models.Model):
_name = "pms.mail"
_description = "PMS Automated Mailing"
name = fields.Char(string="Name", required=True)
notification_type = fields.Selection(
[("email", "Email")], string="Send", default="Email"
)
template_id = fields.Many2one("mail.template", string="Email Template")
interval = fields.Integer("Interval", default=1)
interval_unit = fields.Many2one(
"uom.uom",
string="Unit",
domain=lambda self: [
("category_id", "=", self.env.ref("uom.uom_categ_wtime").id)
],
)
interval_trigger = fields.Selection(
[
("after_resev", "After the reservation"),
("before_checkin", "Before Checkin"),
("after_checkin", "After Checkin"),
("before_checkout", "Before Checkout"),
("after_checkout", "After Checkout"),
],
string="Trigger",
)
property_id = fields.Many2one("pms.property", string="Property")

View File

@@ -0,0 +1,53 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo import api, fields, models
class PmsProperty(models.Model):
_inherit = "pms.property"
checkin = fields.Float(string="Checkin")
checkout = fields.Float(string="Checkout")
reservation_ids = fields.One2many(
"pms.property.reservation", "property_id", string="Reservation Types"
)
pms_mail_ids = fields.One2many("pms.mail", "property_id", string="Communication")
no_of_guests = fields.Integer("Number of Guests")
min_nights = fields.Integer("Minimum Nights")
max_nights = fields.Integer("Maximum Nights")
listing_type = fields.Selection(
string="Listing Type",
selection=[
("private_room", "Private Room"),
("entire_home", "Entire Home"),
("shared_room", "Shared Room"),
],
)
@api.model
def get_property_information(self, vals, domain=False):
domain = domain or []
domain.append(("property_child_ids", "=", False))
if vals.get("city_value") and vals.get("city_value") != "Select City":
domain += [("city", "=", vals.get("city_value"))]
if vals.get("bedrooms_value") and vals.get("bedrooms_value") != 0:
domain += [("qty_bedroom", ">=", vals.get("bedrooms_value"))]
if vals.get("datepicker_value"):
date_range = vals.get("datepicker_value").split("-")
start = date_range[0].strip() + " 00:00:00"
end = date_range[1].strip() + " 23:59:59"
start = datetime.strptime(start, "%m/%d/%Y %H:%M:%S")
end = datetime.strptime(end, "%m/%d/%Y %H:%M:%S")
reservation_ids = self.env["pms.reservation"].search(
[
("start", "<", end),
("stop", ">", start),
]
)
property_ids = reservation_ids.mapped("property_id")
# Remove all the properties with reservations in the date range to only
# show the ones available
domain += [("id", "not in", property_ids.ids)]
return self.search_read(domain, ["ref", "name"])

View File

@@ -0,0 +1,49 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class PmsPropertyReservation(models.Model):
_name = "pms.property.reservation"
_description = "Property Reservation"
def _default_product_id(self):
return self.env.ref(
"pms_sale.product_product_reservation", raise_if_not_found=False
)
@api.depends("product_id")
def _compute_price(self):
for rec in self:
if rec.product_id and rec.product_id.lst_price:
rec.price = rec.product_id.lst_price or 0
elif not rec.price:
rec.price = 0
name = fields.Char(string="Name", required=True)
product_id = fields.Many2one(
"product.product",
string="Product",
required=True,
domain=[("reservation_ok", "=", True)],
default=_default_product_id,
)
price = fields.Float(
string="Price",
compute="_compute_price",
digits="Product Price",
readonly=False,
store=True,
)
property_id = fields.Many2one("pms.property", string="Property")
currency_id = fields.Many2one(
"res.currency",
string="Currency",
default=lambda self: self.env.company.currency_id,
)
def _get_reservation_multiline_description(self):
"""Compute a multiline description of this ticket. It is used when ticket
description are necessary without having to encode it manually, like sales
information."""
return "%s\n%s" % (self.display_name, self.property_id.display_name)

View File

@@ -0,0 +1,327 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime, timedelta
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
AVAILABLE_PRIORITIES = [("0", "Normal"), ("1", "Low"), ("2", "High"), ("3", "Urgent")]
class PmsReservation(models.Model):
_name = "pms.reservation"
_description = "Reservation"
_order = "start, id"
_inherit = ["mail.thread", "mail.activity.mixin"]
def _default_stage_id(self):
return self.env.ref("pms_sale.pms_stage_new", raise_if_not_found=False)
def _get_duration(self, start, stop):
"""Get the duration value between the 2 given dates."""
if not start or not stop:
return 0
duration = (stop - start).total_seconds() / (24 * 3600)
return round(duration, 0)
@api.depends("guest_ids")
def _compute_no_of_guests(self):
self.no_of_guests = 0
if self.guest_ids:
self.no_of_guests = len(self.guest_ids)
name = fields.Char(
string="Reservation #",
help="Reservation Number",
required=True,
readonly=True,
index=True,
copy=False,
default=lambda self: _("New"),
)
start = fields.Datetime(
"Checkin",
required=True,
tracking=True,
default=fields.Date.today,
help="Start date of the reservation",
)
stop = fields.Datetime(
"Checkout",
required=True,
tracking=True,
default=fields.Date.today,
compute="_compute_stop",
readonly=False,
store=True,
help="Stop date of the reservation",
)
duration = fields.Integer(
"Nights", compute="_compute_duration", store=True, readonly=False
)
date = fields.Datetime(string="Date", default=lambda self: fields.Datetime.now())
stage_id = fields.Many2one(
"pms.stage",
string="Stage",
store=True,
tracking=True,
index=True,
default=_default_stage_id,
group_expand="_read_group_stage_ids",
)
team_id = fields.Many2one(
"pms.team", string="Team", related="property_id.team_id", store=True
)
property_id = fields.Many2one("pms.property", string="Property")
sale_order_id = fields.Many2one("sale.order", string="Sales Order")
sale_order_line_id = fields.Many2one("sale.order.line", string="Sales Order Line")
invoice_status = fields.Selection(
related="sale_order_id.invoice_status", store=True, index=True
)
partner_id = fields.Many2one("res.partner", string="Booked by")
user_id = fields.Many2one(
"res.users", string="Responsible", default=lambda self: self.env.user.id
)
company_id = fields.Many2one(
"res.company",
string="Company",
default=lambda self: self.env.company.id,
)
adults = fields.Integer(string="Adults")
children = fields.Integer(string="Children")
no_of_guests = fields.Integer(
"Number of Guests", compute="_compute_no_of_guests", store=True
)
guest_ids = fields.One2many(
"pms.reservation.guest", "reservation_id", string="Guests"
)
priority = fields.Selection(
AVAILABLE_PRIORITIES,
string="Priority",
index=True,
default=AVAILABLE_PRIORITIES[0][0],
)
tag_ids = fields.Many2many("pms.tag", string="Tags")
color = fields.Integer("Color Index", default=0)
invoice_count = fields.Integer(
string="Invoice Count",
compute="_compute_invoice_count",
readonly=True,
copy=False,
)
@api.depends("stop", "start")
def _compute_duration(self):
for reservation in self.with_context(dont_notify=True):
reservation.duration = self._get_duration(
reservation.start, reservation.stop
)
@api.depends("start", "duration")
def _compute_stop(self):
# stop and duration fields both depends on the start field.
# But they also depends on each other.
# When start is updated, we want to update the stop datetime based on
# the *current* duration.
# In other words, we want: change start => keep the duration fixed and
# recompute stop accordingly.
# However, while computing stop, duration is marked to be recomputed.
# Calling `reservation.duration` would trigger its recomputation.
# To avoid this we manually mark the field as computed.
duration_field = self._fields["duration"]
self.env.remove_to_compute(duration_field, self)
for reservation in self:
reservation.stop = reservation.start + timedelta(days=reservation.duration)
def _compute_invoice_count(self):
for reservation in self:
invoices = (
self.env["account.move.line"]
.search([("pms_reservation_id", "=", reservation.id)])
.mapped("move_id")
.filtered(lambda r: r.move_type in ("out_invoice", "out_refund"))
)
reservation.invoice_count = len(invoices)
@api.model
def _read_group_stage_ids(self, stages, domain, order):
search_domain = [("stage_type", "=", "reservation")]
if self.env.context.get("default_team_id"):
search_domain = [
"&",
("team_ids", "in", self.env.context["default_team_id"]),
] + search_domain
return stages.search(search_domain, order=order)
@api.model
def create(self, vals):
if vals.get("name", _("New")) == _("New"):
vals["name"] = self.env["ir.sequence"].next_by_code("pms.reservation") or _(
"New"
)
return super().create(vals)
@api.onchange("property_id")
def onchange_property_id(self):
user_tz = self.env.user.tz or "UTC"
utc = pytz.timezone("UTC")
timezone = pytz.timezone(user_tz)
if (
self.property_id
and self.start
and self.stop
and self.property_id.checkin
and self.property_id.checkout
):
start_datetime = (
str(self.start.date())
+ " "
+ str(timedelta(hours=self.property_id.checkin))
)
with_timezone = timezone.localize(
datetime.strptime(start_datetime, DEFAULT_SERVER_DATETIME_FORMAT)
)
start_datetime = with_timezone.astimezone(utc)
self.start = start_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
end_datetime = (
str(self.stop.date())
+ " "
+ str(timedelta(hours=self.property_id.checkout))
)
with_timezone = timezone.localize(
datetime.strptime(end_datetime, DEFAULT_SERVER_DATETIME_FORMAT)
)
end_datetime = with_timezone.astimezone(utc)
self.stop = end_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
else:
self.start = self.start.date()
self.stop = self.stop.date()
@api.constrains("property_id", "no_of_guests")
def _check_max_no_of_guests(self):
for reservation in self:
if reservation.no_of_guests > reservation.property_id.no_of_guests:
raise ValidationError(
_(
"""Too many guests (%s) on the reservation: the property
accepts a maximum of %s guests."""
% (
reservation.no_of_guests,
reservation.property_id.no_of_guests,
)
)
)
@api.constrains("property_id", "stage_id", "start", "stop")
def _check_no_of_reservations(self):
stage_ids = [
self.env.ref("pms_sale.pms_stage_new", raise_if_not_found=False).id,
self.env.ref("pms_sale.pms_stage_cancelled", raise_if_not_found=False).id,
]
for rec in self:
if rec.stage_id.id not in stage_ids:
reservation = self.search(
[
("property_id", "=", rec.property_id.id),
("stage_id", "not in", stage_ids),
("id", "!=", rec.id),
("start", "<=", rec.stop),
("stop", ">=", rec.start),
]
)
if reservation:
raise ValidationError(
_(
"You cannot have 2 reservations on the same night at the "
"same property."
)
)
@api.constrains("property_id", "duration")
def _check_no_of_nights(self):
for rec in self:
if (
rec.duration > rec.property_id.min_nights
and rec.property_id.max_nights < rec.duration
):
raise ValidationError(
_(
"The number of nights must be between %s and %s."
% (
rec.property_id.min_nights,
rec.property_id.max_nights,
)
)
)
def action_book(self):
return self.write(
{
"stage_id": self.env.ref(
"pms_sale.pms_stage_booked", raise_if_not_found=False
).id,
}
)
def action_confirm(self):
return self.write(
{
"stage_id": self.env.ref(
"pms_sale.pms_stage_confirmed", raise_if_not_found=False
).id
}
)
def action_check_in(self):
return self.write(
{
"stage_id": self.env.ref(
"pms_sale.pms_stage_checked_in", raise_if_not_found=False
).id,
"start": fields.Datetime.now(),
}
)
def action_check_out(self):
return self.write(
{
"stage_id": self.env.ref(
"pms_sale.pms_stage_checked_out", raise_if_not_found=False
).id,
"stop": fields.Datetime.now(),
}
)
def action_cancel(self):
self.write(
{
"stage_id": self.env.ref(
"pms_sale.pms_stage_cancelled", raise_if_not_found=False
).id
}
)
def action_view_invoices(self):
for reservation in self:
action = self.env.ref("account.action_move_out_invoice_type").read()[0]
invoices = (
self.env["account.move.line"]
.search([("pms_reservation_id", "=", reservation.id)])
.mapped("move_id")
.filtered(lambda r: r.move_type in ("out_invoice", "out_refund"))
)
action["domain"] = [("id", "in", invoices.ids)]
return action
@api.model
def get_selections(self):
cities = list(
{rec.city for rec in self.env["pms.property"].search([]) if rec.city}
)
cities.sort()
values = {"city": cities}
return values

View File

@@ -0,0 +1,22 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class PMSReservationGuest(models.Model):
_name = "pms.reservation.guest"
_description = "PMS Reservation guest"
name = fields.Char(string="Name", required=True)
phone = fields.Char(string="Phone")
email = fields.Char(string="Email")
reservation_id = fields.Many2one("pms.reservation", string="Reservation")
order_line_id = fields.Many2one("sale.order.line", string="Sale Order")
partner_id = fields.Many2one("res.partner", string="Partner")
@api.onchange("partner_id")
def _onchange_partner_id(self):
if self.partner_id:
self.name = self.partner_id.name
self.phone = self.partner_id.phone
self.email = self.partner_id.email

View File

@@ -0,0 +1,27 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class PMSStage(models.Model):
_inherit = "pms.stage"
stage_type = fields.Selection(
selection_add=[("reservation", "Reservation")],
ondelete={"reservation": "cascade"},
)
def get_color_information(self):
# get stage ids
stage_ids = self.search([])
color_information_dict = []
for stage in stage_ids:
color_information_dict.append(
{
"color": stage.custom_color,
"field": "stage_id",
"opt": "==",
"value": stage.name,
}
)
return color_information_dict

View File

@@ -0,0 +1,83 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import timedelta
from odoo import fields, models
class PMSTeam(models.Model):
_inherit = "pms.team"
no_today_reservation = fields.Integer(
string="Today Reservations", compute="_compute_no_reservations"
)
no_tomorrow_reservation = fields.Integer(
string="Tomorrow Reservations", compute="_compute_no_reservations"
)
no_week_reservation = fields.Integer(
string="This Week Reservations", compute="_compute_no_reservations"
)
total_reservation = fields.Integer(
string="This Week Reservations", compute="_compute_no_reservations"
)
def _compute_no_reservations(self):
start = fields.date.today() - timedelta(days=fields.date.today().weekday())
end = start + timedelta(days=6)
reservation_obj = self.env["pms.reservation"]
for rec in self:
today_reservation_count = reservation_obj.search_count(
[
("team_id", "=", rec.id),
("start", ">=", fields.date.today()),
("start", "<=", fields.date.today()),
(
"stage_id",
"!=",
self.env.ref("pms_sale.pms_stage_checked_out").id,
),
("stage_id", "!=", self.env.ref("pms_sale.pms_stage_cancelled").id),
]
)
tomorrow_reservation_count = reservation_obj.search_count(
[
("team_id", "=", rec.id),
("start", ">=", fields.date.today()),
("stop", "<=", fields.date.today() + timedelta(1)),
(
"stage_id",
"!=",
self.env.ref("pms_sale.pms_stage_checked_out").id,
),
("stage_id", "!=", self.env.ref("pms_sale.pms_stage_cancelled").id),
]
)
this_week_reservation_count = reservation_obj.search_count(
[
("team_id", "=", rec.id),
("start", ">=", start),
("stop", "<=", end),
(
"stage_id",
"!=",
self.env.ref("pms_sale.pms_stage_checked_out").id,
),
("stage_id", "!=", self.env.ref("pms_sale.pms_stage_cancelled").id),
]
)
total_reservation_count = reservation_obj.search_count(
[
("team_id", "=", rec.id),
(
"stage_id",
"!=",
self.env.ref("pms_sale.pms_stage_checked_out").id,
),
("stage_id", "!=", self.env.ref("pms_sale.pms_stage_cancelled").id),
]
)
rec.no_today_reservation = today_reservation_count
rec.no_tomorrow_reservation = tomorrow_reservation_count
rec.no_week_reservation = this_week_reservation_count
rec.total_reservation = total_reservation_count

View File

@@ -0,0 +1,23 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = "product.template"
reservation_ok = fields.Boolean(string="Reservation")
@api.onchange("reservation_ok")
def _onchange_reservation_ok(self):
if self.reservation_ok:
self.type = "service"
class Product(models.Model):
_inherit = "product.product"
@api.onchange("reservation_ok")
def _onchange_reservation_ok(self):
if self.reservation_ok:
self.type = "service"

View File

@@ -0,0 +1,43 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = "sale.order"
def _compute_reservation_count(self):
sale_orders_data = self.env["pms.reservation"].read_group(
[("sale_order_id", "in", self.ids)], ["sale_order_id"], ["sale_order_id"]
)
reservation_count_data = {
sale_order_data["sale_order_id"][0]: sale_order_data["sale_order_id_count"]
for sale_order_data in sale_orders_data
}
for sale_order in self:
sale_order.reservation_count = reservation_count_data.get(sale_order.id, 0)
reservation_count = fields.Integer(
"Reservations Count", compute="_compute_reservation_count"
)
def action_view_reservation_list(self):
action = self.env["ir.actions.actions"]._for_xml_id(
"pms_sale.action_pms_reservation"
)
action["domain"] = [("sale_order_id", "in", self.ids)]
return action
def action_confirm(self):
res = super(SaleOrder, self).action_confirm()
for sale in self:
reservation = self.env["pms.reservation"].search(
[("sale_order_id", "=", sale.id)]
)
if reservation:
reservation.action_book()
# Set reservation confirm when payment is done by Generate a Payment Link
if not sale.has_to_be_paid():
reservation.action_confirm()
return res

View File

@@ -0,0 +1,193 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo import api, fields, models
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
property_id = fields.Many2one("pms.property", string="Property")
reservation_id = fields.Many2one(
"pms.property.reservation", string="Reservation Type"
)
reservation_ok = fields.Boolean(
related="product_id.reservation_ok", readonly=True, string="Is Reservation?"
)
start = fields.Datetime("From")
stop = fields.Datetime("To")
no_of_guests = fields.Integer("Number of Guests")
guest_ids = fields.One2many(
"pms.reservation.guest", "order_line_id", string="Guests"
)
pms_reservation_id = fields.Many2one("pms.reservation", string="Reservation")
@api.onchange("reservation_id", "no_of_guests")
def _onchange_reservation_id(self):
# we call this to force update the default name
self.product_id_change()
@api.onchange("property_id")
def _onchange_property_id(self):
if self.property_id and self.property_id.analytic_id:
self.order_id.analytic_account_id = self.property_id.analytic_id.id
def get_sale_order_line_multiline_description_sale(self, product):
if self.reservation_id:
return (
"".join(
[
self.reservation_id.display_name,
" (",
str(self.no_of_guests),
" Guests)",
]
)
+ self._get_sale_order_line_multiline_description_variants()
)
else:
return super(
SaleOrderLine, self
).get_sale_order_line_multiline_description_sale(product)
@api.model
def create(self, values):
rec = super(SaleOrderLine, self).create(values)
if (
values.get("product_id")
and values.get("reservation_id")
and values.get("property_id")
and not values.get("pms_reservation_id", False)
):
reservation_vals = {
"partner_id": rec.order_id.partner_id.id,
"sale_order_id": rec.order_id.id,
"sale_order_line_id": rec.id,
}
reservation = self._create_pms_reservation(values, reservation_vals)
if reservation:
rec.pms_reservation_id = reservation.id
if values.get("property_id"):
rec.order_id.analytic_account_id = rec.property_id.analytic_id.id
return rec
def write(self, values):
rec = super(SaleOrderLine, self).write(values)
if self.pms_reservation_id:
reserv_vals = {}
if values.get(
"property_id"
) and self.pms_reservation_id.property_id.id != values.get("property_id"):
reserv_vals.update({"property_id": values.get("property_id")})
if values.get("start") and self.pms_reservation_id.start != values.get(
"start"
):
reserv_vals.update({"start": values.get("start")})
if values.get("stop") and self.pms_reservation_id.stop != values.get(
"stop"
):
reserv_vals.update({"stop": values.get("stop")})
if values.get("guest_ids"):
reserv_vals.update({"guest_ids": values.get("guest_ids")})
self.pms_reservation_id.sudo().write(reserv_vals)
if (
(
values.get("product_id")
or (values.get("reservation_id") and values.get("property_id"))
)
and self.product_id.reservation_ok
and not self.pms_reservation_id
):
reservation_vals = {
"partner_id": self.order_id.partner_id.id,
"sale_order_id": self.order_id.id,
"sale_order_line_id": self.id,
}
reservation = self._create_pms_reservation(values, reservation_vals)
if reservation:
self.pms_reservation_id = reservation.id
if values.get("property_id"):
self.order_id.analytic_account_id = self.property_id.analytic_id.id
return rec
def _create_pms_reservation(self, values, reservation_vals):
reservation = False
if reservation_vals:
reservation_vals.update(
{
"date": datetime.now(),
"property_id": values.get("property_id") or self.property_id.id,
"start": values.get("start") or self.start or datetime.now(),
"stop": values.get("stop") or self.stop or datetime.now(),
"guest_ids": values.get("guest_ids") or False,
}
)
reservation = self.env["pms.reservation"].sudo().create(reservation_vals)
return reservation
def unlink(self):
for line in self:
if line.product_id.reservation_ok and line.pms_reservation_id:
line.pms_reservation_id.action_cancel()
return super(SaleOrderLine, self).unlink()
@api.onchange("product_id")
def product_id_change(self):
super(SaleOrderLine, self).product_id_change()
if self.reservation_id:
self.price_unit = self.reservation_id.price
if self.order_id.pricelist_id:
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id,
fiscal_position=self.env.context.get("fiscal_position"),
)
price = self.env["account.tax"]._fix_tax_included_price_company(
self._get_display_price(product),
product.taxes_id,
self.tax_id,
self.company_id,
)
if price != product.lst_price:
self.price_unit = price
@api.onchange("product_uom", "product_uom_qty")
def product_uom_change(self):
super(SaleOrderLine, self).product_uom_change()
if self.reservation_id:
self.price_unit = self.reservation_id.price
if self.order_id.pricelist_id:
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
partner=self.order_id.partner_id,
quantity=self.product_uom_qty,
date=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id,
fiscal_position=self.env.context.get("fiscal_position"),
)
price = self.env["account.tax"]._fix_tax_included_price_company(
self._get_display_price(product),
product.taxes_id,
self.tax_id,
self.company_id,
)
if price != product.lst_price:
self.price_unit = price
def _prepare_invoice_line(self, **optional_values):
result = super()._prepare_invoice_line(**optional_values)
self.ensure_one()
if self.pms_reservation_id and self.property_id:
result.update(
{
"pms_reservation_id": self.pms_reservation_id.id,
"property_ids": [(6, 0, self.property_id.ids)],
}
)
return result

View File

@@ -0,0 +1 @@
* Go to Properties > Configuration > Settings.

View File

@@ -0,0 +1,4 @@
* `Open Source Integrators <https://www.opensourceintegrators.com>`:
* Maxime Chambreuil <mchambreuil@opensourceintegrators.com>
* Serpent Consulting Services Pvt. Ltd. <support@serpentcs.com>

View File

@@ -0,0 +1,2 @@
This module allows you to manage the sale and financial information of the
properties.

View File

@@ -0,0 +1 @@
To use this module, please read the complete user guide at `<roomdoo.com>`_.

View File

@@ -0,0 +1,12 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_pms_property_reservation,access_pms_property_reservation,model_pms_property_reservation,base.group_user,1,1,1,0
access_pms_mail,access_pms_mail,model_pms_mail,base.group_user,1,1,1,0
access_pms_reservation_guest,access_pms_reservation_guest,model_pms_reservation_guest,base.group_user,1,1,1,0
access_pms_configurator,access_pms_configurator,model_pms_configurator,base.group_user,1,1,1,0
access_pms_reservation_guest_wizard,access_pms_reservation_guest_wizard,model_pms_reservation_guest_wizard,base.group_user,1,1,1,0
access_pms_property_reservation_manager,access_pms_property_reservation_manager,model_pms_property_reservation,pms_base.group_pms_manager,1,1,1,1
access_pms_mail_manager,access_pms_mail_manager,model_pms_mail,pms_base.group_pms_manager,1,1,1,1
access_pms_reservation_guest_manager,access_pms_reservation_guest_manager,model_pms_reservation_guest,pms_base.group_pms_manager,1,1,1,1
access_pms_reservation_manager,access_pms_reservation_manager,model_pms_reservation,pms_base.group_pms_manager,1,1,1,1
access_pms_reservation_user,access_pms_reservation_user,model_pms_reservation,pms_base.group_pms_user,1,1,1,0
access_pms_reservation,access_pms_reservation,model_pms_reservation,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_pms_property_reservation access_pms_property_reservation model_pms_property_reservation base.group_user 1 1 1 0
3 access_pms_mail access_pms_mail model_pms_mail base.group_user 1 1 1 0
4 access_pms_reservation_guest access_pms_reservation_guest model_pms_reservation_guest base.group_user 1 1 1 0
5 access_pms_configurator access_pms_configurator model_pms_configurator base.group_user 1 1 1 0
6 access_pms_reservation_guest_wizard access_pms_reservation_guest_wizard model_pms_reservation_guest_wizard base.group_user 1 1 1 0
7 access_pms_property_reservation_manager access_pms_property_reservation_manager model_pms_property_reservation pms_base.group_pms_manager 1 1 1 1
8 access_pms_mail_manager access_pms_mail_manager model_pms_mail pms_base.group_pms_manager 1 1 1 1
9 access_pms_reservation_guest_manager access_pms_reservation_guest_manager model_pms_reservation_guest pms_base.group_pms_manager 1 1 1 1
10 access_pms_reservation_manager access_pms_reservation_manager model_pms_reservation pms_base.group_pms_manager 1 1 1 1
11 access_pms_reservation_user access_pms_reservation_user model_pms_reservation pms_base.group_pms_user 1 1 1 0
12 access_pms_reservation access_pms_reservation model_pms_reservation base.group_user 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,455 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>PMS (Property Management System)</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="pms-property-management-system">
<h1 class="title">PMS (Property Management System)</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/pms/tree/14.0/pms"><img alt="OCA/pms" src="https://img.shields.io/badge/github-OCA%2Fpms-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/pms-14-0/pms-14-0-pms"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/293/14.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module is an all-in-one property management system (PMS) focused on medium-sized properties
for managing every aspect of your propertys daily operations.</p>
<p>You can manage properties with multi-property and multi-company support, including your rooms inventory,
reservations, check-in, daily reports, board services, rate and availability plans among other property functionalities.</p>
<div class="admonition important">
<p class="first admonition-title">Important</p>
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
</div>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#installation" id="id1">Installation</a></li>
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id4">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id5">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id6">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id7">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id8">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="installation">
<h1><a class="toc-backref" href="#id1">Installation</a></h1>
<p>This module depends on modules <tt class="docutils literal">base</tt>, <tt class="docutils literal">mail</tt>, <tt class="docutils literal">sale</tt> and <tt class="docutils literal">multi_pms_properties</tt>.
Ensure yourself to have all them in your addons list.</p>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
<p>You will find the hotel settings in PMS Management &gt; Configuration &gt; Properties &gt; Your Property.</p>
<p>This module required additional configuration for company, accounting, invoicing and user privileges.</p>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id3">Usage</a></h1>
<p>To use this module, please, read the complete user guide at <a class="reference external" href="roomdoo.com">roomdoo.com</a>.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id4">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/pms/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/pms/issues/new?body=module:%20pms%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id5">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id6">Authors</a></h2>
<ul class="simple">
<li>Commit [Sun]</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id7">Contributors</a></h2>
<ul class="simple">
<li>Alexandre Díaz</li>
<li>Pablo Quesada</li>
<li>Jose Luis Algara</li>
<li><cite>Commit [Sun] &lt;https://www.commitsun.com&gt;</cite>:<ul>
<li>Dario Lodeiros</li>
<li>Eric Antones</li>
<li>Sara Lago</li>
<li>Brais Abeijon</li>
<li>Miguel Padin</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id8">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/pms/tree/14.0/pms">OCA/pms</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,55 @@
odoo.define("pms_sale.PMSConfiguratorFormController", function (require) {
"use strict";
var FormController = require("web.FormController");
/**
* This controller is overridden to allow configuring sale_order_lines through a
* popup window when a product with 'reservation_ok' is selected.
*
* This allows keeping an editable list view for sales order and remove the noise of
* those 2 fields ('property_id' + 'reservation_id')
*/
var PMSConfiguratorFormController = FormController.extend({
/**
* We let the regular process take place to allow the validation of the required
* fields to happen.
*
* Then we can manually close the window, providing event information to the
* caller.
*
* @override
*/
saveRecord: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var state = self.renderer.state.data;
var guest_ids = [[5, 0, 0]];
_.each(state.guest_ids.data, function (data) {
if (data.data && data.data.name) {
if (data.data.partner_id) {
data.data.partner_id = data.data.partner_id.data.id;
}
guest_ids.push([0, 0, data.data]);
}
});
self.do_action({
type: "ir.actions.act_window_close",
infos: {
ReservationConfiguration: {
property_id: {id: state.property_id.data.id},
reservation_id: {id: state.reservation_id.data.id},
start: state.start,
stop: state.stop,
no_of_guests: state.no_of_guests,
product_uom_qty: state.duration,
guest_ids: guest_ids,
},
},
});
});
},
});
return PMSConfiguratorFormController;
});

View File

@@ -0,0 +1,20 @@
odoo.define("pms_sale.PMSConfiguratorFormView", function (require) {
"use strict";
var PMSConfiguratorFormController = require("pms_sale.PMSConfiguratorFormController");
var FormView = require("web.FormView");
var viewRegistry = require("web.view_registry");
/**
* @see EventConfiguratorFormController for more information
*/
var PMSConfiguratorFormView = FormView.extend({
config: _.extend({}, FormView.prototype.config, {
Controller: PMSConfiguratorFormController,
}),
});
viewRegistry.add("pms_configurator_form", PMSConfiguratorFormView);
return PMSConfiguratorFormView;
});

View File

@@ -0,0 +1,189 @@
odoo.define("pms_sale.product_configurator", function (require) {
"use strict";
var ProductConfiguratorWidget = require("sale.product_configurator");
/**
* Extension of the ProductConfiguratorWidget to support event product
* configuration. It opens when an event product_product is set.
*
* The event information include:
* - property_id
* - reservation_id
*
*/
ProductConfiguratorWidget.include({
/**
* @returns {Boolean}
*
* @override
* @private
*/
_isConfigurableLine: function () {
return this.recordData.reservation_ok || this._super.apply(this, arguments);
},
/**
* @param {integer} productId
* @param {String} dataPointId
* @returns {Promise<Boolean>} stopPropagation true if a suitable configurator
* has been found.
*
* @override
* @private
*/
_onProductChange: function (productId, dataPointId) {
var self = this;
return this._super.apply(this, arguments).then(function (stopPropagation) {
if (stopPropagation || productId === undefined) {
return Promise.resolve(true);
}
return self._checkForReservation(productId, dataPointId);
});
},
get_parent_partner: function () {
var self = this;
if (
self.getParent() &&
self.getParent().getParent() &&
self.getParent().getParent().recordData &&
self.getParent().getParent().recordData.partner_id &&
self.getParent().getParent().recordData.partner_id.res_id
) {
return self.getParent().getParent().recordData.partner_id.res_id;
}
return false;
},
/**
* This method will check if the productId needs configuration or not:
*
* @param {integer} productId
* @param {String} dataPointId
* @returns {Promise<Boolean>} stopPropagation true if the product is an event
* ticket.
*
* @private
*/
_checkForReservation: function (productId, dataPointId) {
var self = this;
return this._rpc({
model: "product.product",
method: "read",
args: [productId, ["reservation_ok"]],
}).then(function (result) {
if (
Array.isArray(result) &&
result.length &&
result[0].reservation_ok
) {
var web_partner_id = self.get_parent_partner();
var result_vals = {
default_product_id: productId,
};
if (web_partner_id) {
result_vals.web_partner_id = web_partner_id;
}
if (self.recordData && self.recordData.currency_id) {
result_vals.default_currency_id =
self.recordData.currency_id.data.id;
}
self._openReservationConfigurator(result_vals, dataPointId);
return Promise.resolve(true);
}
return Promise.resolve(false);
});
},
/**
* Opens the event configurator in 'edit' mode.
*
* @override
* @private
*/
_onEditLineConfiguration: function () {
if (this.recordData.reservation_ok) {
var defaultValues = {
default_product_id: this.recordData.product_id.data.id,
};
if (this.recordData.property_id) {
defaultValues.default_property_id = this.recordData.property_id.data.id;
}
if (this.recordData.reservation_id) {
defaultValues.default_reservation_id = this.recordData.reservation_id.data.id;
}
if (this.recordData.start) {
defaultValues.default_start = this.recordData.start;
}
if (this.recordData.stop) {
defaultValues.default_stop = this.recordData.stop;
}
if (this.recordData.currency_id) {
defaultValues.default_currency_id = this.recordData.currency_id.data.id;
}
if (this.recordData.id) {
defaultValues.sale_line_ine = this.recordData.id;
}
var web_partner_id = this.get_parent_partner();
if (web_partner_id) {
defaultValues.web_partner_id = web_partner_id;
}
this._openReservationConfigurator(defaultValues, this.dataPointID);
} else {
this._super.apply(this, arguments);
}
},
/**
* Opens the event configurator to allow configuring the SO line with events
* information.
*
* When the window is closed, configured values are used to trigger a
* 'field_changed' event to modify the current SO line.
*
* If the window is closed without providing the required values 'property_id'
* and 'reservation_id', the product_id field is cleaned.
*
* @param {Object} data various "default_" values
* @param {String} dataPointId
*
* @private
*/
_openReservationConfigurator: function (data, dataPointId) {
var self = this;
this.do_action("pms_sale.pms_configurator_action", {
additional_context: data,
on_close: function (result) {
if (result && !result.special) {
self.trigger_up("field_changed", {
dataPointID: dataPointId,
changes: result.ReservationConfiguration,
onSuccess: function () {
// Call post-init function.
self._onLineConfigured();
},
});
} else if (
!self.recordData.property_id ||
!self.recordData.reservation_id
) {
self.trigger_up("field_changed", {
dataPointID: dataPointId,
changes: {
product_id: false,
name: "",
},
});
}
},
});
},
});
return ProductConfiguratorWidget;
});

View File

@@ -0,0 +1,300 @@
odoo.define("pms_sale.timeline", function (require) {
"use strict";
const core = require("web.core");
const time = require("web.time");
const TimelineRenderer = require("web_timeline.TimelineRenderer");
const TimelineView = require("web_timeline.TimelineView");
const _t = core._t;
TimelineView.prototype.jsLibs.push(
"/web/static/lib/daterangepicker/daterangepicker.js"
);
TimelineView.prototype.jsLibs.push("/web/static/src/js/libs/daterangepicker.js");
TimelineView.prototype.cssLibs.push(
"/web/static/lib/daterangepicker/daterangepicker.css"
);
TimelineRenderer.include({
willStart: function () {
this.city = [];
this.values = {};
return Promise.all([
this._super.apply(this, arguments),
this.get_selections(),
]);
},
get_selections: function () {
var self = this;
return this._rpc({
model: "pms.reservation",
method: "get_selections",
args: [],
}).then(function (rec) {
self.values = rec;
self.city = rec.city;
});
},
init: function (parent, state, params) {
var self = this;
this._super.apply(this, arguments);
this.modelName = params.model;
this.date_start = params.date_start;
this.date_stop = params.date_stop;
this.view = params.view;
this.city_value = false;
this.$filter_reservation = false;
this.datepicker_value = false;
this.bedrooms_value = false;
// Find their matches
if (this.modelName === "pms.reservation") {
// Find custom color if mentioned
if (params.arch.attrs.custom_color === "true") {
this._rpc({
model: "pms.stage",
method: "get_color_information",
args: [[]],
}).then(function (result) {
self.colors = result;
});
}
}
},
start: function () {
var self = this;
this._super.apply(this, arguments);
if (this.modelName === "pms.reservation") {
const $filter_reservation = $(
core.qweb.render("TimelineReservationFilter")
);
self.$filter_reservation = $filter_reservation;
_.each(this.city, function (city) {
const newOption = new Option(city, city);
$filter_reservation
.find(".oe_timeline_select_city")
.append(newOption, undefined);
});
this.$el.find(".oe_timeline_buttons").append($filter_reservation);
$filter_reservation
.find(".oe_timeline_text_datepicker")
.daterangepicker({
autoApply: true,
});
$filter_reservation
.find(".oe_timeline_button_search")
.click(function () {
self._onsearchbutton();
});
}
},
_onsearchbutton: function () {
if (this.$el.find(".oe_timeline_select_city").val() !== "Select City") {
this.city_value = this.$el.find(".oe_timeline_select_city").val();
} else {
this.city_value = false;
}
if (this.$el.find(".oe_timeline_text_datepicker").val()) {
this.datepicker_value = this.$el
.find(".oe_timeline_text_datepicker")
.val();
} else {
this.datepicker_value = false;
}
if (this.$el.find(".oe_timeline_text_bedrooms").val()) {
this.bedrooms_value = this.$el.find(".oe_timeline_text_bedrooms").val();
} else {
this.bedrooms_value = false;
}
this.on_data_loaded(this.view.model.data.data, this.last_group_bys);
},
split_groups: function (events, group_bys) {
if (group_bys.length === 0) {
return events;
}
const groups = [];
for (const evt of events) {
const group_name = evt[_.first(group_bys)];
if (group_name) {
if (group_name instanceof Array) {
const group = _.find(
groups,
(existing_group) => existing_group.id === group_name[0]
);
if (_.isUndefined(group)) {
groups.push({
id: group_name[0],
content: group_name[1],
});
}
}
}
}
return groups;
},
get_vals: function () {
return {
city_value: this.city_value,
datepicker_value: this.datepicker_value,
bedrooms_value: this.bedrooms_value,
};
},
on_data_loaded_2: function (events, group_bys) {
var self = this;
if (this.modelName === "pms.reservation") {
var data = [];
var groups = [];
this.grouped_by = group_bys;
_.each(events, function (event) {
if (event[self.date_start]) {
data.push(self.event_data_transform(event));
}
});
groups = self.split_groups(events, group_bys);
if (group_bys[0] === "property_id") {
var groups_user_ids = [];
for (var g in groups) {
groups_user_ids.push(groups[g].id);
}
self._rpc({
model: "pms.property",
method: "get_property_information",
args: [this.get_vals()],
}).then(function (result) {
self.property_ids = [];
self.properties = [];
self.properties.push(result);
for (var r in result) {
self.property_ids.push(result[r].id);
}
var res_user_groups = [];
var res_user_groups_ids = [];
for (var u in self.property_ids) {
if (
!(self.property_ids[u] in groups_user_ids) ||
self.property_ids[u] !== -1
) {
// Get User Name
var user_name = "-";
for (var n in self.properties[0]) {
if (
self.properties[0][n].id ===
self.property_ids[u]
) {
user_name =
self.properties[0][n].ref ||
self.properties[0][n].name;
}
}
var is_available = false;
for (var i in groups) {
if (groups[i].id === self.property_ids[u]) {
if (
!res_user_groups_ids.includes(
self.property_ids[u]
)
) {
res_user_groups.push({
id: self.property_ids[u],
content: _t(user_name),
});
res_user_groups_ids.push(
self.property_ids[u]
);
}
}
}
if (!is_available) {
if (
!res_user_groups_ids.includes(
self.property_ids[u]
)
) {
res_user_groups.push({
id: self.property_ids[u],
content: _t(user_name),
});
res_user_groups_ids.push(self.property_ids[u]);
}
}
}
}
self.timeline.setGroups(res_user_groups);
self.timeline.setItems(data);
self.timeline.setOptions({
orientation: "top",
});
if (self.datepicker_value) {
var value = self.datepicker_value.split("-");
const date_value = new moment(value[0], "MM/DD/YYYY");
self.timeline.moveTo(date_value);
}
});
}
}
return this._super.apply(this, arguments);
},
event_data_transform: function (evt) {
if (this.modelName === "pms.reservation") {
var self = this;
var date_start = new moment();
var date_stop = null;
date_start = time.auto_str_to_date(evt[this.date_start]);
date_stop = this.date_stop
? time.auto_str_to_date(evt[this.date_stop])
: null;
var group = evt[self.last_group_bys[0]];
if (group && group instanceof Array) {
group = _.first(group);
} else {
group = -1;
}
_.each(self.colors, function (color) {
if (
Function(
'"use strict";return\'' +
evt[color.field] +
"' " +
color.opt +
" '" +
color.value +
"'"
)()
) {
self.color = color.color;
} else if (
Function(
'"use strict";return\'' +
evt[color.field][1] +
"' " +
color.opt +
" '" +
color.value +
"'"
)
) {
self.color = color.color;
}
});
var content = _.isUndefined(evt.__name) ? evt.display_name : evt.__name;
if (this.arch.children.length) {
content = this.render_timeline_item(evt);
}
var r = {
start: date_start,
content: content,
id: evt.id,
group: group,
evt: evt,
style: "background-color: " + self.color + ";",
};
if (date_stop && !moment(date_start).isSame(date_stop)) {
r.end = date_stop;
}
self.color = null;
return r;
}
return this._super.apply(this, arguments);
},
});
});

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<template>
<t t-name="TimelineReservationFilter">
<div class="btn-group btn-sm">
<select
class="btn btn-default btn-sm oe_timeline_select_city"
style="width:20%;border-bottom:1px solid;margin-right:10px;"
>
<option>Select City</option>
</select>
<input
class="btn btn-default btn-sm oe_timeline_text_datepicker"
placeholder="Date..."
style="border-bottom:1px solid;margin-right:10px;"
/>
<input
class="btn btn-default btn-sm oe_timeline_text_bedrooms"
type="number"
placeholder="Bedrooms..."
style="width:20%;border-bottom:1px solid;margin-right:10px;"
/>
<button class="btn btn-default oe_timeline_button_search">Search</button>
</div>
</t>
</template>

View File

@@ -0,0 +1,3 @@
from . import test_account_move
from . import test_pms_property
from . import test_pms_reservation

View File

@@ -0,0 +1,53 @@
# Copyright (c) 2022 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import date, timedelta
from odoo.tests import SavepointCase
class TestAccountMove(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product = cls.env.ref("pms_sale.product_product_reservation")
cls.partner_owner = cls.env["res.partner"].create({"name": "Property Owner"})
cls.partner_property = cls.env["res.partner"].create({"name": "Property"})
cls.property = cls.env["pms.property"].create(
{"owner_id": cls.partner_owner.id, "partner_id": cls.partner_property.id}
)
cls.reservation = cls.env["pms.reservation"].create(
{"name": "Test Reservation", "property_id": cls.property.id}
)
cls.sale_order_obj = cls.env["sale.order"]
cls.partner = cls.env["res.partner"].create({"name": "TEST CUSTOMER"})
cls.sale_pricelist = cls.env["product.pricelist"].create(
{"name": "Test Pricelist", "currency_id": cls.env.ref("base.USD").id}
)
cls.so = cls.sale_order_obj.create(
{
"partner_id": cls.partner.id,
"date_order": date.today() + timedelta(days=1),
"pricelist_id": cls.sale_pricelist.id,
"order_line": [
(
0,
0,
{
"name": cls.reservation.name,
"product_id": cls.product.id,
"product_uom_qty": 5.0,
"product_uom": cls.product.uom_po_id.id,
"price_unit": 10.0,
"qty_delivered": 1,
"pms_reservation_id": cls.reservation.id,
"property_id": cls.reservation.property_id.id,
},
),
],
}
)
def test_compute_reservation_count(self):
self.so.sudo().action_confirm()
invoice = self.so._create_invoices()
self.assertEqual(invoice.reservation_count, 1)

View File

@@ -0,0 +1,107 @@
# Copyright (c) 2022 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests import SavepointCase
class TestPMSProperty(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product = cls.env.ref("pms_sale.product_product_reservation")
cls.partner_owner = cls.env["res.partner"].create({"name": "Property Owner"})
cls.partner_property = cls.env["res.partner"].create({"name": "Property"})
cls.property = cls.env["pms.property"].create(
{"owner_id": cls.partner_owner.id, "partner_id": cls.partner_property.id}
)
cls.partner_owner_2 = cls.env["res.partner"].create(
{"name": "Property Owner 2"}
)
cls.partner_property_2 = cls.env["res.partner"].create({"name": "Property 2"})
cls.property_2 = cls.env["pms.property"].create(
{
"owner_id": cls.partner_owner.id,
"partner_id": cls.partner_property_2.id,
"max_nights": 21,
}
)
cls.my_pms_property_reservation = cls.env["pms.property.reservation"].create(
{
"name": "PMS property reservation 1",
"product_id": cls.product.id,
"property_id": cls.property.id,
}
)
cls.my_pms_property_reservation_2 = cls.env["pms.property.reservation"].create(
{
"name": "PMS property reservation 2",
"product_id": cls.product.id,
"property_id": cls.property.id,
}
)
cls.pms_property2 = cls.env["pms.property"].create(
{
"name": "Property_2",
"ref": "test ref",
"owner_id": cls.partner_owner.id,
"city": "la",
"room_ids": [
(
0,
0,
{
"name": "Room 101",
"type_id": 7,
},
),
(
0,
0,
{
"name": "Room 102",
"type_id": 7,
},
),
],
"property_child_ids": [],
"reservation_ids": [
(
0,
0,
{
"name": cls.my_pms_property_reservation.name,
"product_id": cls.product.id,
"property_id": cls.property.id,
},
),
(
0,
0,
{
"name": cls.my_pms_property_reservation_2.name,
"product_id": cls.product.id,
"property_id": cls.property.id,
},
),
],
}
)
cls.reservation = cls.env["pms.reservation"].create(
{
"name": "Test Reservation",
"property_id": cls.property_2.id,
"start": "2022-06-01",
"stop": "2022-06-15",
}
)
def test_get_property_information(self):
vals = {
"city_value": "la",
"bedrooms_value": 2,
"datepicker_value": "12/1/2022-12/15/2022",
}
self.assertEqual(len(self.pms_property2.get_property_information(vals)), 1)

View File

@@ -0,0 +1,117 @@
# Copyright (c) 2022 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.tests import SavepointCase
class TestPMSReservation(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.product = cls.env.ref("pms_sale.product_product_reservation")
cls.partner_owner = cls.env["res.partner"].create({"name": "Property Owner"})
cls.partner_property = cls.env["res.partner"].create({"name": "Property"})
cls.property = cls.env["pms.property"].create(
{
"owner_id": cls.partner_owner.id,
"partner_id": cls.partner_property.id,
"checkin": 12.0,
"checkout": 15.0,
}
)
cls.property_reservation = cls.env["pms.property.reservation"].create(
{
"name": "PMS property reservation 1",
"product_id": cls.product.id,
"property_id": cls.property.id,
}
)
cls.property.write(
{
"name": "Property",
"ref": "test ref",
"city": "la",
"no_of_guests": 4,
"min_nights": 1,
"max_nights": 30,
"reservation_ids": [
(
0,
0,
{
"name": cls.property_reservation.name,
"product_id": cls.product.id,
"property_id": cls.property.id,
},
)
],
}
)
cls.reservation = cls.env["pms.reservation"].create(
{
"name": "Test Reservation",
"property_id": cls.property.id,
"start": "2022-06-01",
"stop": "2022-06-15",
}
)
def test_read_group_stage_ids(self):
stages = self.env["pms.stage"]
stages = self.reservation._read_group_stage_ids(stages, [], False)
self.assertEqual(len(stages), 6)
def test_onchange_property_id(self):
self.reservation.onchange_property_id()
self.assertEqual(
self.reservation.start.strftime("%m/%d/%Y %H:%M"), "06/01/2022 10:00"
)
self.assertEqual(
self.reservation.stop.strftime("%m/%d/%Y %H:%M"), "06/15/2022 13:00"
)
def test_check_max_no_of_guests(self):
self.reservation._check_max_no_of_guests()
def test_check_no_of_reservations(self):
self.reservation._check_no_of_reservations()
def test_check_no_of_nights(self):
self.reservation._check_no_of_nights()
def test_action_book(self):
self.reservation.action_book()
self.assertEqual(
self.reservation.stage_id.id,
self.env.ref("pms_sale.pms_stage_booked", raise_if_not_found=False).id,
)
def test_action_confirm(self):
self.reservation.action_confirm()
self.assertEqual(
self.reservation.stage_id.id,
self.env.ref("pms_sale.pms_stage_confirmed", raise_if_not_found=False).id,
)
def test_action_check_in(self):
self.reservation.action_check_in()
self.assertEqual(
self.reservation.stage_id.id,
self.env.ref("pms_sale.pms_stage_checked_in", raise_if_not_found=False).id,
)
def test_action_check_out(self):
self.reservation.action_check_out()
self.assertEqual(
self.reservation.stage_id.id,
self.env.ref("pms_sale.pms_stage_checked_out", raise_if_not_found=False).id,
)
def test_action_cancel(self):
self.reservation.action_cancel()
self.assertEqual(
self.reservation.stage_id.id,
self.env.ref("pms_sale.pms_stage_cancelled", raise_if_not_found=False).id,
)
def test_action_view_invoices(self):
self.reservation.action_view_invoices()

View File

@@ -0,0 +1,35 @@
<odoo>
<record id="view_move_form_inherit_pms_sale" model="ir.ui.view">
<field name="name">pms.property.invoice.form.pms</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account.view_move_form" />
<field name="groups_id" eval="[(4, ref('pms_base.group_pms_user'))]" />
<field name="arch" type="xml">
<div name="button_box" position="inside">
<button
name="action_view_reservation_list"
type="object"
class="oe_stat_button"
icon="fa-users"
attrs="{'invisible': [('reservation_count', '=', 0)]}"
>
<field
name="reservation_count"
widget="statinfo"
string="Reservations"
/>
</button>
</div>
<xpath
expr="//field[@name='invoice_line_ids']/form//field[@name='name']"
position="after"
>
<group>
<field name="pms_reservation_id" />
</group>
</xpath>
</field>
</record>
</odoo>

25
pms_sale/views/assets.xml Normal file
View File

@@ -0,0 +1,25 @@
<odoo>
<template
id="assets_backend"
inherit_id="web.assets_backend"
name="pms_sale assets backend"
>
<xpath expr="." position="inside">
<script
type="text/javascript"
src="/pms_sale/static/src/js/pms_configurator_controller.js"
/>
<script
type="text/javascript"
src="/pms_sale/static/src/js/pms_configurator_view.js"
/>
<script
type="text/javascript"
src="/pms_sale/static/src/js/pms_configurator_widget.js"
/>
<script type="text/javascript" src="/pms_sale/static/src/js/timeline.js" />
</xpath>
</template>
</odoo>

71
pms_sale/views/menu.xml Normal file
View File

@@ -0,0 +1,71 @@
<odoo>
<!-- Dashboard -->
<menuitem
name="Reservations"
id="menu_board_reservation"
sequence="20"
parent="pms_base.menu_board"
action="action_board_reservation"
/>
<!-- Operations -->
<menuitem
name="Reservations"
id="menu_operations_reservation"
sequence="10"
parent="pms_base.menu_operations"
action="action_operations_reservation"
/>
<!-- Reporting -->
<menuitem
name="Reservations"
id="menu_report_reservation"
sequence="10"
parent="pms_base.menu_report"
action="action_report_reservation"
/>
<!-- Configuration -->
<menuitem
name="Reservations"
id="menu_config_reservation"
sequence="2"
parent="pms_base.menu_config"
/>
<!--
<menuitem
name="Types"
id="pms_pms_property_reservation_menu"
sequence="3"
parent="pms_sale.menu_config_reservation"
action="pms_property_reservation_action"/> -->
<!--
<menuitem
name="Mails"
id="pms_mail_menu"
sequence="4"
parent="pms_sale.menu_config_reservation"
action="action_pms_mail"/> -->
<!--
<menuitem
name="Guests"
id="pms_reservation_guest_menu"
sequence="5"
parent="pms_sale.menu_config_reservation"
action="pms_reservation_guest_action"/> -->
<menuitem
id="pms_reservation_timeline"
name="Reservations"
parent="sale.sale_order_menu"
sequence="2"
action="action_sale_reservation"
groups="sales_team.group_sale_salesman"
/>
</odoo>

View File

@@ -0,0 +1,63 @@
<odoo>
<record id="view_pms_mail_form" model="ir.ui.view">
<field name="name">pms.mail.form</field>
<field name="model">pms.mail</field>
<field name="arch" type="xml">
<form string="Mail Scheduler">
<sheet>
<group>
<group>
<field name="name" />
<field name="notification_type" />
<field
name="template_id"
attrs="{'required': [('notification_type', '=', 'email')]}"
/>
</group>
<group>
<label for="interval" />
<div class="o_row">
<field name="interval" />
<field name="interval_unit" />
</div>
<field name="interval_trigger" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_pms_mail_tree" model="ir.ui.view">
<field name="name">pms.mail.tree</field>
<field name="model">pms.mail</field>
<field name="arch" type="xml">
<tree string="Mail Schedulers">
<field name="name" />
<field name="notification_type" />
<field
name="template_id"
attrs="{'required': [('notification_type', '=', 'email')]}"
/>
</tree>
</field>
</record>
<record id="pms_mail_view_search" model="ir.ui.view">
<field name="name">pms.mail.search</field>
<field name="model">pms.mail</field>
<field name="arch" type="xml">
<search string="Property">
<field name="property_id" />
</search>
</field>
</record>
<record id="action_pms_mail" model="ir.actions.act_window">
<field name="name">Mail Schedulers</field>
<field name="res_model">pms.mail</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,34 @@
<odoo>
<record id="view_pms_property_form_inherit_pms_sale" model="ir.ui.view">
<field name="name">pms.property.form.inherit.pms.sale</field>
<field name="model">pms.property</field>
<field name="inherit_id" ref="pms_base.view_pms_property_form" />
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page name="reservation" string="Reservation">
<group id="reservation">
<group id="reservation-left">
<field name="listing_type" />
</group>
<group id="reservation-right" />
<group id="defaults" string="Defaults">
<field name="checkin" widget="float_time" />
<field name="checkout" widget="float_time" />
</group>
<group id="limits" string="Limits">
<field name="no_of_guests" />
<field name="min_nights" />
<field name="max_nights" />
</group>
</group>
<field name="reservation_ids" />
</page>
<page name="communication" string="Communication">
<field name="pms_mail_ids" />
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,47 @@
<odoo>
<record id="pms_property_reservation_view_form" model="ir.ui.view">
<field name="name">pms.property.reservation.view.from</field>
<field name="model">pms.property.reservation</field>
<field name="arch" type="xml">
<form string="Property Reservation">
<sheet>
<group>
<field name="name" />
<field name="product_id" />
<field name="price" />
<field
name="currency_id"
options="{'no_create': True}"
groups="base.group_multi_currency"
/>
</group>
</sheet>
</form>
</field>
</record>
<record id="pms_property_reservation_view_tree" model="ir.ui.view">
<field name="name">pms.property.reservation.view.tree</field>
<field name="model">pms.property.reservation</field>
<field name="arch" type="xml">
<tree string="Property Reservation">
<field name="name" />
<field name="product_id" />
<field name="price" />
<field
name="currency_id"
options="{'no_create': True}"
groups="base.group_multi_currency"
/>
</tree>
</field>
</record>
<record id="pms_property_reservation_action" model="ir.actions.act_window">
<field name="name">Property Reservation</field>
<field name="res_model">pms.property.reservation</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,41 @@
<odoo>
<record id="pms_reservation_guest_view_form" model="ir.ui.view">
<field name="name">pms.reservation.guest.view.form</field>
<field name="model">pms.reservation.guest</field>
<field name="arch" type="xml">
<form string="Reservation Stage">
<sheet>
<group>
<group>
<field name="name" />
<field name="phone" widget="phone" />
</group>
<group>
<field name="email" />
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="pms_reservation_guest_view_tree" model="ir.ui.view">
<field name="name">pms.reservation.guest.view.tree</field>
<field name="model">pms.reservation.guest</field>
<field name="arch" type="xml">
<tree string="Reservation Stage">
<field name="name" />
<field name="phone" />
<field name="email" />
</tree>
</field>
</record>
<record id="pms_reservation_guest_action" model="ir.actions.act_window">
<field name="name">Reservation Guest</field>
<field name="res_model">pms.reservation.guest</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,396 @@
<odoo>
<record id="view_reservation_form" model="ir.ui.view">
<field name="name">pms.reservation.form</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<form string="Reservation">
<header>
<button
id="action_book"
name="action_book"
string="Book"
class="oe_highlight"
type="object"
groups="pms_base.group_pms_manager"
attrs="{'invisible': [('stage_id', '!=', (%(pms_sale.pms_stage_new)d))]}"
/>
<button
id="action_confirm"
name="action_confirm"
string="Confirm"
class="oe_highlight"
type="object"
groups="pms_base.group_pms_manager"
attrs="{'invisible': [('stage_id', '!=', (%(pms_sale.pms_stage_booked)d))]}"
/>
<button
id="action_check_in"
name="action_check_in"
string="Check In"
class="oe_highlight"
type="object"
groups="pms_base.group_pms_manager"
attrs="{'invisible': [('stage_id', '!=', (%(pms_sale.pms_stage_confirmed)d))]}"
/>
<button
id="action_check_out"
name="action_check_out"
string="Check Out"
class="oe_highlight"
type="object"
groups="pms_base.group_pms_manager"
attrs="{'invisible': [('stage_id', '!=', (%(pms_sale.pms_stage_checked_in)d))]}"
/>
<field
name="stage_id"
widget="statusbar"
options="{'fold_field': 'fold'}"
domain="[('stage_type', '=', 'reservation'),
('team_ids', 'in', (team_id, False))]"
/>
</header>
<sheet string="Reservation">
<div class="oe_button_box" name="button_box">
<button
name="action_view_invoices"
type="object"
class="oe_stat_button"
icon="fa-pencil-square-o"
groups="account.group_account_invoice"
attrs="{'invisible': [('invoice_count', '=', 0)]}"
>
<field
name="invoice_count"
widget="statinfo"
string="Invoices"
/>
</button>
</div>
<h1>
<field nolabel="1" name="name" class="oe_inline" />
</h1>
<group id="main">
<group id="main-left">
<field name="property_id" required="1" />
<field
name="team_id"
required="1"
groups="pms_base.group_pms_show_team"
/>
<field name="partner_id" required="1" />
<field name="user_id" required="1" />
<field name="sale_order_id" invisible="1" />
<field name="sale_order_line_id" invisible="1" />
</group>
<group id="main-right">
<field name="date" />
<field name="start" required="1" string="Check In" />
<field name="duration" />
<field name="stop" required="1" string="Check Out" />
<field
name="company_id"
options="{'no_create': True}"
groups="base.group_multi_company"
/>
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color', 'no_create_edit': True}"
/>
</group>
</group>
<notebook>
<page name="guest_ids" string="Guests">
<group>
<field name="no_of_guests" />
</group>
<field name="guest_ids" nolabel="1">
<tree string="Guests" editable="bottom">
<field
name="partner_id"
domain="[('is_property', '=', False)]"
/>
<field name="name" />
<field name="phone" />
<field name="email" />
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers" />
<field name="activity_ids" widget="mail_activity" />
<field
name="message_ids"
widget="mail_thread"
options="{'post_refresh': 'recipients'}"
/>
</div>
</form>
</field>
</record>
<record id="view_reservation_tree" model="ir.ui.view">
<field name="name">pms.reservation.tree</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<tree string="Reservation">
<field name="name" />
<field name="partner_id" />
<field name="start" />
<field name="stop" />
<field name="property_id" />
<field name="stage_id" />
</tree>
</field>
</record>
<record id="view_reservation_search" model="ir.ui.view">
<field name="name">pms.reservation.search</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<search string="Reservation Detail">
<field name="name" />
<field name="partner_id" />
<field name="start" />
<field name="stop" />
<field name="property_id" />
<field name="team_id" />
<newline />
<searchpanel>
<field
name="stage_id"
string="State"
enable_counters="1"
select="multi"
/>
<field
name="invoice_status"
string="Payment Status"
enable_counters="1"
select="multi"
/>
</searchpanel>
<filter
string="Today Reservations"
name="today_reservation"
domain="[('start','&lt;=', (datetime.date.today()).strftime('%Y-%m-%d')),('start','&gt;=',(datetime.date.today()).strftime('%Y-%m-%d'))]"
/>
<filter
string="Tomorrow Reservations"
domain="[('start','&gt;=',datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')),('stop','&lt;=', ((context_today() + relativedelta(days=1)).strftime('%Y-%m-%d')))]"
name="tomorrow_reservation"
/>
<filter
string="This week Reservations"
domain="[('start','&gt;=', (context_today() - relativedelta(weeks=1)).strftime('%Y-%m-%d')),('stop','&lt;=',(context_today() -relativedelta(weeks=1, weekday=0) + relativedelta(weeks=0,day=0, weekday=6)).strftime('%Y-%m-%d'))]"
name="this_reservation"
/>
<filter
string="Total Reservations"
name="total_reservation"
domain="[('stage_id','!=',%(pms_sale.pms_stage_checked_out)d),('stage_id','!=',%(pms_sale.pms_stage_cancelled)d)]"
/>
</search>
</field>
</record>
<record id="view_reservation_timeline" model="ir.ui.view">
<field name="name">pms.reservation.timeline</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<timeline
date_start="start"
date_stop="stop"
string="Reservations"
default_group_by="property_id"
event_open_popup="true"
mode="week"
colors="#ffffff:stage_id=='New';"
custom_color="true"
/>
</field>
</record>
<record id="view_reservation_calendar" model="ir.ui.view">
<field name="name">pms.reservation.calendar</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<calendar
string="Reservations"
date_start="start"
date_delay="stop"
color="stage_id"
>
<field name="name" />
<field name="stage_id" />
<field name="property_id" />
<field name="partner_id" />
</calendar>
</field>
</record>
<record id="view_reservation_kanban" model="ir.ui.view">
<field name="name">pms.reservation.kanban</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<kanban default_group_by="stage_id" class="o_kanban_small_column">
<field
name="stage_id"
options='{"group_by_tooltip": {"description": "Description"}}'
/>
<field name="name" />
<field name="priority" />
<field name="property_id" />
<field name="partner_id" />
<field name="user_id" />
<field name="color" />
<templates>
<t t-name="kanban-box">
<div
t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click"
name="pms_reservation"
>
<div class="o_dropdown_kanban dropdown">
<a
role="button"
class="dropdown-toggle o-no-caret btn"
data-toggle="dropdown"
href="#"
aria-label="Dropdown menu"
title="Dropdown menu"
>
<span class="fa fa-ellipsis-v" />
</a>
<div class="dropdown-menu" role="menu">
<a
t-if="widget.editable"
role="menuitem"
type="edit"
class="dropdown-item"
>Edit
</a>
<a
t-if="widget.deletable"
role="menuitem"
type="delete"
class="dropdown-item"
>
Delete
</a>
<div role="separator" class="dropdown-divider" />
<ul
class="oe_kanban_colorpicker"
data-field="color"
/>
</div>
</div>
<div class="oe_kanban_content">
<div>
<strong class="o_kanban_record_title">
<field name="name" />
</strong>
</div>
<div>
<field
name="tag_ids"
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
</div><div>
<field name="partner_id" /> @ <field
name="property_id"
/>
</div>
<div><field name="duration" /> night(s)</div>
<div class="o_kanban_record_bottom">
<div class="oe_kanban_bottom_left">
<field name="priority" widget="priority" />
<field
name="activity_ids"
widget="kanban_activity"
/>
</div>
<div class="oe_kanban_bottom_right">
<field
name="user_id"
widget="many2one_avatar_user"
/>
</div>
</div>
</div>
<div class="oe_clear" />
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_reservation_pivot" model="ir.ui.view">
<field name="name">pms.reservation.pivot</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<pivot string="Reservations">
<field name="stop" type="row" interval="month" />
<field name="stage_id" type="row" />
</pivot>
</field>
</record>
<record id="view_reservation_graph" model="ir.ui.view">
<field name="name">pms.reservation.graph</field>
<field name="model">pms.reservation</field>
<field name="arch" type="xml">
<graph string="Reservations">
<field name="stop" type="row" interval="day" />
<field name="stage_id" type="row" />
</graph>
</field>
</record>
<record id="action_board_reservation" model="ir.actions.act_window">
<field name="name">Reservations</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">timeline,kanban,calendar,tree,form</field>
</record>
<record id="action_sale_reservation" model="ir.actions.act_window">
<field name="name">Reservations</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">timeline</field>
<field name="target">current</field>
</record>
<record id="action_operations_reservation" model="ir.actions.act_window">
<field name="name">Reservations</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">kanban,tree,form,timeline,calendar</field>
</record>
<record id="action_report_reservation" model="ir.actions.act_window">
<field name="name">Reservations</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">graph,pivot</field>
</record>
<record id="action_pms_reservation" model="ir.actions.act_window">
<field name="name">Reservations</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">tree,form</field>
</record>
<record id="reservation_action_from_dashboard" model="ir.actions.act_window">
<field name="name">Orders</field>
<field name="res_model">pms.reservation</field>
<field name="view_mode">kanban,tree,form,calendar</field>
<field name="context">{'default_team_id': active_id}</field>
<field name="domain">[('team_id', '=', active_id)]</field>
</record>
</odoo>

View File

@@ -0,0 +1,74 @@
<odoo>
<record id="view_pms_team_kanban_inherit_pms_sale" model="ir.ui.view">
<field name="name">pms.team.kanban</field>
<field name="model">pms.team</field>
<field name="inherit_id" ref="pms_base.view_pms_team_kanban" />
<field name="arch" type="xml">
<field name="name" position="after">
<field name="no_today_reservation" />
<field name="no_tomorrow_reservation" />
<field name="no_week_reservation" />
<field name="total_reservation" />
</field>
<xpath expr="//button[1]" position="after">
<button
class="btn btn-primary"
name="%(pms_sale.reservation_action_from_dashboard)d"
type="action"
style="width: inherit;"
context="{'search_default_total_reservation': 1}"
>
<t t-esc="record.total_reservation.value" />
<br />
RESERVATION(S)
</button>
</xpath>
<xpath expr="//div[hasclass('o_kanban_primary_left')]" position="after">
<div class="col-7 o_kanban_primary_right">
<div class="row" style="margin-bottom: 5px;">
<div class="col-12">
Today:
<a
name="%(pms_sale.reservation_action_from_dashboard)d"
type="action"
context="{'search_default_today_reservation': 1}"
>
<t
t-esc="record.no_today_reservation.value"
/> Reservation(s)
</a>
</div>
</div>
<div class="row" style="margin-bottom: 5px;">
<div class="col-12">
Tomorrow:
<a
name="%(pms_sale.reservation_action_from_dashboard)d"
type="action"
context="{'search_default_tomorrow_reservation': 1}"
>
<t t-esc="record.no_tomorrow_reservation.value" />
Reservation(s)
</a>
</div>
</div>
<div class="row">
<div class="col-12">
This Week:
<a
name="%(pms_sale.reservation_action_from_dashboard)d"
type="action"
context="{'search_default_this_reservation': 1}"
>
<t t-esc="record.no_week_reservation.value" />
Reservation(s)
</a>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,38 @@
<odoo>
<record id="product_template_form_view_inherit_pms_sale" model="ir.ui.view">
<field name="name">product.template.event.form.inherit.pms.sale</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view" />
<field name="arch" type="xml">
<group name="sale" position="inside">
<group string="Reservation">
<field name="reservation_ok" />
</group>
</group>
</field>
</record>
<record id="product_template_search_view_inherit_pms_sale" model="ir.ui.view">
<field name="name">product.template.search</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view" />
<field name="arch" type="xml">
<field name="name" position="after">
<field
name="reservation_ok"
string="Reservation"
filter_domain="[('reservation_ok','=',True)]"
/>
</field>
<xpath expr="//filter[@name='categ_id']" position="after">
<filter
string="Reservation"
name="reservation_ok"
context="{'group_by':'reservation_ok'}"
/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="sale_order_view_form_inherit_pms_sale" model="ir.ui.view">
<field name="name">sale.order.form.inherit.pms.sale</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath expr="//button[@name='preview_sale_order']" position="before">
<button
name="action_view_reservation_list"
type="object"
class="oe_stat_button"
icon="fa-users"
attrs="{'invisible': [('reservation_count', '=', 0)]}"
>
<field
name="reservation_count"
widget="statinfo"
string="Reservations"
/>
</button>
</xpath>
<xpath
expr="//field[@name='order_line']//form//field[@name='product_id']"
position="after"
>
<field name="reservation_ok" invisible="1" />
<field
name="property_id"
attrs="{'invisible': [('reservation_ok', '=', False)], 'required': [('reservation_ok', '!=', False)]}"
options="{'no_open': True, 'no_create': True}"
/>
<field
name="reservation_id"
attrs="{'invisible': ['|', ('reservation_ok', '=', False), ('property_id', '=', False)],
'required': [('reservation_ok', '!=', False), ('property_id', '!=', False)]}"
options="{'no_open': True, 'no_create': True}"
/>
<field name="start" invisible="1" />
<field name="stop" invisible="1" />
</xpath>
<xpath
expr="//field[@name='order_line']//form//field[@name='invoice_lines']"
position="after"
>
<field name="no_of_guests" invisible="1" />
<field name="guest_ids" invisible="1" />
<field name="pms_reservation_id" invisible="1" />
</xpath>
<xpath
expr="//field[@name='order_line']//tree//field[@name='product_template_id']"
position="after"
>
<field name="reservation_ok" invisible="1" />
<field name="property_id" optional="hide" />
<field name="reservation_id" optional="hide" />
<field name="currency_id" invisible="1" />
<field name="start" invisible="1" />
<field name="stop" invisible="1" />
</xpath>
<field name="partner_id" position="attributes">
<attribute
name="domain"
>['|', ('company_id', '=', False), ('company_id', '=', company_id), ('is_property', '=', False)]</attribute>
</field>
<xpath
expr="//field[@name='order_line']//tree//field[@name='product_uom_qty']"
position="attributes"
>
<attribute
name="attrs"
>{'readonly': [('reservation_ok', '=', True)]}</attribute>
<attribute name="force_save">1</attribute>
</xpath>
<xpath
expr="//field[@name='order_line']//form//field[@name='product_uom_qty']"
position="attributes"
>
<attribute
name="attrs"
>{'readonly': [('reservation_ok', '=', True)]}</attribute>
<attribute name="force_save">1</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import pms_configurator
from . import account_payment_register

View File

@@ -0,0 +1,16 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models
class AccountPaymentRegister(models.TransientModel):
_inherit = "account.payment.register"
def _create_payments(self):
payment = super()._create_payments()
reservations = payment.reconciled_invoice_ids.line_ids.mapped(
"pms_reservation_id"
)
for reservation in reservations:
reservation.action_confirm()
return payment

View File

@@ -0,0 +1,224 @@
# Copyright (c) 2021 Open Source Integrators
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime, timedelta
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
class PMSConfigurator(models.TransientModel):
_name = "pms.configurator"
_description = "PMS Configurator"
def _get_duration(self, start, stop):
"""Get the duration value between the 2 given dates."""
if not start or not stop:
return 0
duration = (stop - start).total_seconds() / (24 * 3600)
return round(duration, 0)
@api.depends("stop", "start")
def _compute_duration(self):
for reservation in self.with_context(dont_notify=True):
reservation.duration = self._get_duration(
reservation.start, reservation.stop
)
@api.depends("start", "duration")
def _compute_stop(self):
# stop and duration fields both depends on the start field.
# But they also depends on each other.
# When start is updated, we want to update the stop datetime based on
# the *current* duration.
# In other words, we want: change start => keep the duration fixed and
# recompute stop accordingly.
# However, while computing stop, duration is marked to be recomputed.
# Calling `reservation.duration` would trigger its recomputation.
# To avoid this we manually mark the field as computed.
duration_field = self._fields["duration"]
self.env.remove_to_compute(duration_field, self)
for reservation in self:
reservation.stop = reservation.start + timedelta(days=reservation.duration)
@api.depends("guest_ids")
def _compute_no_of_guests(self):
self.no_of_guests = 0
if self.guest_ids:
self.no_of_guests = len(self.guest_ids)
product_id = fields.Many2one("product.product", string="Product", readonly=True)
property_id = fields.Many2one("pms.property", string="Property")
reservation_id = fields.Many2one(
"pms.property.reservation", string="Reservation Type"
)
start = fields.Datetime(
"From",
required=True,
help="Start date of the reservation",
)
stop = fields.Datetime(
"To",
required=True,
compute="_compute_stop",
readonly=False,
store=True,
help="Stop date of the reservation",
)
duration = fields.Integer(
"Nights", compute="_compute_duration", store=True, readonly=False
)
no_of_guests = fields.Integer(
"Number of Guests", compute="_compute_no_of_guests", store=True
)
guest_ids = fields.One2many(
"pms.reservation.guest.wizard", "configurator_id", string="Guests"
)
currency_id = fields.Many2one("res.currency", string="Currency")
reservation_ids = fields.Many2many("pms.reservation")
timeline_html = fields.Html("Timeline HTML", readonly=True)
@api.onchange("property_id")
def onchange_property_id(self):
user_tz = self.env.user.tz or "UTC"
utc = pytz.timezone("UTC")
timezone = pytz.timezone(user_tz)
if (
self.property_id
and self.start
and self.stop
and self.property_id.checkin
and self.property_id.checkout
):
if (
str(self.start) != (self._context.get("default_start") or "")
) or self.property_id.id != self._context.get("default_property_id"):
start_datetime = (
str(self.start.date())
+ " "
+ str(timedelta(hours=self.property_id.checkin))
)
with_timezone = timezone.localize(
datetime.strptime(start_datetime, DEFAULT_SERVER_DATETIME_FORMAT)
)
start_datetime = with_timezone.astimezone(utc)
self.start = start_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
if (
str(self.stop) != (self._context.get("default_stop") or "")
) or self.property_id.id != self._context.get("default_property_id"):
end_datetime = (
str(self.stop.date())
+ " "
+ str(timedelta(hours=self.property_id.checkout))
)
with_timezone = timezone.localize(
datetime.strptime(end_datetime, DEFAULT_SERVER_DATETIME_FORMAT)
)
end_datetime = with_timezone.astimezone(utc)
self.stop = end_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
stages = [
self.env.ref("pms_sale.pms_stage_booked").id,
self.env.ref("pms_sale.pms_stage_confirmed").id,
self.env.ref("pms_sale.pms_stage_checked_in").id,
]
reservations = self.env["pms.reservation"].search(
[
("property_id", "=", self.property_id.id),
("stop", ">", fields.Datetime.now()),
("stage_id", "in", stages),
]
)
self.reservation_ids = [(6, 0, reservations.ids)]
@api.constrains("property_id", "no_of_guests")
def _check_max_no_of_guests(self):
for configurator in self:
if configurator.no_of_guests > configurator.property_id.no_of_guests:
raise ValidationError(
_(
"%s of guests is lower than the %s of guests of the property."
% (
configurator.no_of_guests,
configurator.property_id.no_of_guests,
)
)
)
@api.model
def default_get(self, fields_vals):
result = super(PMSConfigurator, self).default_get(fields_vals)
if not result.get("start"):
result.update({"start": fields.Date.today()})
if not result.get("stop"):
result.update({"stop": fields.Date.today()})
if self._context.get("web_partner_id"):
partner_rec = self.env["res.partner"].browse(
self._context.get("web_partner_id")
)
if partner_rec:
result.update(
{
"guest_ids": [
(
0,
0,
{
"partner_id": partner_rec.id,
"name": partner_rec.name,
"email": partner_rec.email,
"phone": partner_rec.phone,
},
)
]
}
)
guest_list = []
if self._context.get("sale_line_ine"):
guest_ids = self.env["pms.reservation.guest"].search_read(
[("order_line_id", "=", self._context.get("sale_line_ine"))]
)
for guest in guest_ids:
guest_list.append(
(
0,
0,
{
"partner_id": guest.get("partner_id"),
"name": guest.get("name"),
"email": guest.get("email"),
"phone": guest.get("phone"),
},
)
)
if guest_list:
result.update({"guest_ids": guest_list})
ref_id = self.env.ref("pms_sale.action_sale_reservation")
timeline_url = "%s/web?#action=%s&model=pms.reservation&view_type=schedule" % (
self.env["ir.config_parameter"].sudo().get_param("web.base.url"),
ref_id and str(ref_id.id) or "",
)
result["timeline_html"] = (
"<a class='btn btn-primary' href='%s' alt='Timeline View' target='_blank'"
" >Timeline</a>" % (timeline_url)
)
return result
class PMSReservationGuestWizard(models.TransientModel):
_name = "pms.reservation.guest.wizard"
_description = "PMS Reservation guest"
name = fields.Char(string="Name", required=True)
phone = fields.Char(string="Phone")
email = fields.Char(string="Email")
configurator_id = fields.Many2one("pms.configurator", string="Configurator")
partner_id = fields.Many2one("res.partner", string="Partner")
@api.onchange("partner_id")
def _onchange_partner_id(self):
if self.partner_id:
self.name = self.partner_id.name
self.phone = self.partner_id.phone
self.email = self.partner_id.email

View File

@@ -0,0 +1,80 @@
<odoo>
<record id="pms_configurator_view_form" model="ir.ui.view">
<field name="name">pms.configurator.view.form</field>
<field name="model">pms.configurator</field>
<field name="arch" type="xml">
<form js_class="pms_configurator_form">
<group>
<field
name="property_id"
domain="[('property_child_ids', '=', False)]"
required="1"
options="{'no_open': True, 'no_create': True}"
/>
<field name="currency_id" invisible="1" />
<field
name="reservation_id"
attrs="{
'invisible': [('property_id', '=', False)],
'required': [('property_id', '!=', False)],
}"
domain="[('property_id', '=', property_id), ('currency_id', '=', currency_id)]"
options="{'no_open': True, 'no_create': True}"
/>
<field name="product_id" invisible="1" />
</group>
<group col="4">
<field name="start" string="Check In" />
<field name="stop" string="Check Out" />
<field name="duration" invisible="1" />
</group>
<group>
<field name="timeline_html" nolabel="1" />
</group>
<notebook>
<page name="guest_ids" string="Guests">
<group>
<field name="no_of_guests" />
</group>
<field name="guest_ids" nolabel="1">
<tree string="Guests" editable="bottom">
<field
name="partner_id"
domain="[('is_property', '=', False)]"
/>
<field name="name" />
<field name="phone" />
<field name="email" />
</tree>
</field>
</page>
<page string="Bookings">
<field name="reservation_ids" readonly="1">
<tree>
<field name="name" />
<field name="start" string="Check In" />
<field name="stop" string="Check Out" />
<field name="duration" />
<field name="stage_id" />
</tree>
</field>
</page>
</notebook>
<footer>
<button string="Ok" class="btn-primary" special="save" />
<button string="Cancel" class="btn-secondary" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="pms_configurator_action" model="ir.actions.act_window">
<field name="name">Configure Reservation</field>
<field name="res_model">pms.configurator</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="pms_configurator_view_form" />
</record>
</odoo>

View File

@@ -0,0 +1 @@
../../../../pms_sale

6
setup/pms_sale/setup.py Normal file
View File

@@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)