mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
@@ -1,2 +1,4 @@
|
||||
account-financial-tools
|
||||
contract
|
||||
stock-logistics-warehouse
|
||||
web
|
||||
|
||||
101
pms_sale/README.rst
Normal file
101
pms_sale/README.rst
Normal 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
3
pms_sale/__init__.py
Normal 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
34
pms_sale/__manifest__.py
Normal 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"],
|
||||
}
|
||||
11
pms_sale/data/ir_sequence.xml
Normal file
11
pms_sale/data/ir_sequence.xml
Normal 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>
|
||||
52
pms_sale/data/pms_stage.xml
Normal file
52
pms_sale/data/pms_stage.xml
Normal 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>
|
||||
15
pms_sale/data/product_data.xml
Normal file
15
pms_sale/data/product_data.xml
Normal 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
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
1010
pms_sale/i18n/pms_sale.pot
Normal file
File diff suppressed because it is too large
Load Diff
15
pms_sale/models/__init__.py
Normal file
15
pms_sale/models/__init__.py
Normal 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,
|
||||
)
|
||||
29
pms_sale/models/account_move.py
Normal file
29
pms_sale/models/account_move.py
Normal 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
|
||||
11
pms_sale/models/account_move_line.py
Normal file
11
pms_sale/models/account_move_line.py
Normal 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
|
||||
)
|
||||
33
pms_sale/models/pms_mail.py
Normal file
33
pms_sale/models/pms_mail.py
Normal 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")
|
||||
53
pms_sale/models/pms_property.py
Normal file
53
pms_sale/models/pms_property.py
Normal 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"])
|
||||
49
pms_sale/models/pms_property_reservation.py
Normal file
49
pms_sale/models/pms_property_reservation.py
Normal 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)
|
||||
327
pms_sale/models/pms_reservation.py
Normal file
327
pms_sale/models/pms_reservation.py
Normal 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
|
||||
22
pms_sale/models/pms_reservation_guest.py
Normal file
22
pms_sale/models/pms_reservation_guest.py
Normal 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
|
||||
27
pms_sale/models/pms_stage.py
Normal file
27
pms_sale/models/pms_stage.py
Normal 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
|
||||
83
pms_sale/models/pms_team.py
Normal file
83
pms_sale/models/pms_team.py
Normal 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
|
||||
23
pms_sale/models/product.py
Normal file
23
pms_sale/models/product.py
Normal 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"
|
||||
43
pms_sale/models/sale_order.py
Normal file
43
pms_sale/models/sale_order.py
Normal 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
|
||||
193
pms_sale/models/sale_order_line.py
Normal file
193
pms_sale/models/sale_order_line.py
Normal 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
|
||||
1
pms_sale/readme/CONFIGURE.rst
Normal file
1
pms_sale/readme/CONFIGURE.rst
Normal file
@@ -0,0 +1 @@
|
||||
* Go to Properties > Configuration > Settings.
|
||||
4
pms_sale/readme/CONTRIBUTORS.rst
Normal file
4
pms_sale/readme/CONTRIBUTORS.rst
Normal 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>
|
||||
2
pms_sale/readme/DESCRIPTION.rst
Normal file
2
pms_sale/readme/DESCRIPTION.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
This module allows you to manage the sale and financial information of the
|
||||
properties.
|
||||
1
pms_sale/readme/USAGE.rst
Normal file
1
pms_sale/readme/USAGE.rst
Normal file
@@ -0,0 +1 @@
|
||||
To use this module, please read the complete user guide at `<roomdoo.com>`_.
|
||||
12
pms_sale/security/ir.model.access.csv
Normal file
12
pms_sale/security/ir.model.access.csv
Normal 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
|
||||
|
BIN
pms_sale/static/description/agency_logo.png
Normal file
BIN
pms_sale/static/description/agency_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
pms_sale/static/description/agency_logo1.png
Normal file
BIN
pms_sale/static/description/agency_logo1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
BIN
pms_sale/static/description/avatar.png
Normal file
BIN
pms_sale/static/description/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
pms_sale/static/description/icon.png
Normal file
BIN
pms_sale/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
455
pms_sale/static/description/index.html
Normal file
455
pms_sale/static/description/index.html
Normal 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 property’s 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 > Configuration > Properties > 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] <https://www.commitsun.com></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>
|
||||
55
pms_sale/static/src/js/pms_configurator_controller.js
Normal file
55
pms_sale/static/src/js/pms_configurator_controller.js
Normal 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;
|
||||
});
|
||||
20
pms_sale/static/src/js/pms_configurator_view.js
Normal file
20
pms_sale/static/src/js/pms_configurator_view.js
Normal 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;
|
||||
});
|
||||
189
pms_sale/static/src/js/pms_configurator_widget.js
Normal file
189
pms_sale/static/src/js/pms_configurator_widget.js
Normal 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;
|
||||
});
|
||||
300
pms_sale/static/src/js/timeline.js
Normal file
300
pms_sale/static/src/js/timeline.js
Normal 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);
|
||||
},
|
||||
});
|
||||
});
|
||||
25
pms_sale/static/src/xml/timeline.xml
Normal file
25
pms_sale/static/src/xml/timeline.xml
Normal 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>
|
||||
3
pms_sale/tests/__init__.py
Normal file
3
pms_sale/tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import test_account_move
|
||||
from . import test_pms_property
|
||||
from . import test_pms_reservation
|
||||
53
pms_sale/tests/test_account_move.py
Normal file
53
pms_sale/tests/test_account_move.py
Normal 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)
|
||||
107
pms_sale/tests/test_pms_property.py
Normal file
107
pms_sale/tests/test_pms_property.py
Normal 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)
|
||||
117
pms_sale/tests/test_pms_reservation.py
Normal file
117
pms_sale/tests/test_pms_reservation.py
Normal 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()
|
||||
35
pms_sale/views/account_move.xml
Normal file
35
pms_sale/views/account_move.xml
Normal 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
25
pms_sale/views/assets.xml
Normal 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
71
pms_sale/views/menu.xml
Normal 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>
|
||||
63
pms_sale/views/pms_mail_views.xml
Normal file
63
pms_sale/views/pms_mail_views.xml
Normal 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>
|
||||
34
pms_sale/views/pms_property.xml
Normal file
34
pms_sale/views/pms_property.xml
Normal 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>
|
||||
47
pms_sale/views/pms_property_reservation.xml
Normal file
47
pms_sale/views/pms_property_reservation.xml
Normal 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>
|
||||
41
pms_sale/views/pms_reservation_guest_views.xml
Normal file
41
pms_sale/views/pms_reservation_guest_views.xml
Normal 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>
|
||||
396
pms_sale/views/pms_reservation_views.xml
Normal file
396
pms_sale/views/pms_reservation_views.xml
Normal 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','<=', (datetime.date.today()).strftime('%Y-%m-%d')),('start','>=',(datetime.date.today()).strftime('%Y-%m-%d'))]"
|
||||
/>
|
||||
|
||||
<filter
|
||||
string="Tomorrow Reservations"
|
||||
domain="[('start','>=',datetime.datetime.now().strftime('%Y-%m-%d 00:00:00')),('stop','<=', ((context_today() + relativedelta(days=1)).strftime('%Y-%m-%d')))]"
|
||||
name="tomorrow_reservation"
|
||||
/>
|
||||
|
||||
<filter
|
||||
string="This week Reservations"
|
||||
domain="[('start','>=', (context_today() - relativedelta(weeks=1)).strftime('%Y-%m-%d')),('stop','<=',(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>
|
||||
74
pms_sale/views/pms_team_views.xml
Normal file
74
pms_sale/views/pms_team_views.xml
Normal 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>
|
||||
38
pms_sale/views/product_views.xml
Normal file
38
pms_sale/views/product_views.xml
Normal 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>
|
||||
86
pms_sale/views/sale_order_views.xml
Normal file
86
pms_sale/views/sale_order_views.xml
Normal 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>
|
||||
3
pms_sale/wizards/__init__.py
Normal file
3
pms_sale/wizards/__init__.py
Normal 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
|
||||
16
pms_sale/wizards/account_payment_register.py
Normal file
16
pms_sale/wizards/account_payment_register.py
Normal 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
|
||||
224
pms_sale/wizards/pms_configurator.py
Normal file
224
pms_sale/wizards/pms_configurator.py
Normal 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
|
||||
80
pms_sale/wizards/pms_configurator_views.xml
Normal file
80
pms_sale/wizards/pms_configurator_views.xml
Normal 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>
|
||||
1
setup/pms_sale/odoo/addons/pms_sale
Symbolic link
1
setup/pms_sale/odoo/addons/pms_sale
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../pms_sale
|
||||
6
setup/pms_sale/setup.py
Normal file
6
setup/pms_sale/setup.py
Normal file
@@ -0,0 +1,6 @@
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['setuptools-odoo'],
|
||||
odoo_addon=True,
|
||||
)
|
||||
Reference in New Issue
Block a user