diff --git a/pms/__manifest__.py b/pms/__manifest__.py
index b541bb84a..a9e6c1db9 100644
--- a/pms/__manifest__.py
+++ b/pms/__manifest__.py
@@ -60,6 +60,7 @@
"views/res_partner_views.xml",
"views/product_pricelist_views.xml",
"views/product_pricelist_item_views.xml",
+ "views/pms_sale_channel.xml",
"views/product_template_views.xml",
"views/webclient_templates.xml",
"views/ir_sequence_views.xml",
diff --git a/pms/data/pms_data.xml b/pms/data/pms_data.xml
index 1a55c1da1..7eafee0f0 100644
--- a/pms/data/pms_data.xml
+++ b/pms/data/pms_data.xml
@@ -34,5 +34,22 @@
+
+
+ Door
+ direct
+
+
+ Phone
+ direct
+
+
+ Mail
+ direct
+
+
+ Agency
+ indirect
+
diff --git a/pms/models/__init__.py b/pms/models/__init__.py
index 5343f0ca8..92f2ace63 100644
--- a/pms/models/__init__.py
+++ b/pms/models/__init__.py
@@ -31,6 +31,7 @@ from . import pms_checkin_partner
from . import product_pricelist
from . import product_pricelist_item
from . import res_partner
+from . import pms_sale_channel
# from . import mail_compose_message
from . import pms_room_type_class
diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py
index d48de2ba4..01460eb2b 100644
--- a/pms/models/pms_folio.py
+++ b/pms/models/pms_folio.py
@@ -50,7 +50,9 @@ class PmsFolio(models.Model):
pms_property_id = fields.Many2one(
"pms.property", default=_get_default_pms_property, required=True
)
- partner_id = fields.Many2one("res.partner", tracking=True, ondelete="restrict")
+ partner_id = fields.Many2one(
+ "res.partner", compute="_compute_partner_id", tracking=True, ondelete="restrict"
+ )
reservation_ids = fields.One2many(
"pms.reservation",
"folio_id",
@@ -102,6 +104,13 @@ class PmsFolio(models.Model):
readonly=False,
help="Pricelist for current folio.",
)
+ commission = fields.Float(
+ string="Commission",
+ compute="_compute_commission",
+ store=True,
+ readonly=True,
+ default=0,
+ )
user_id = fields.Many2one(
"res.users",
string="Salesperson",
@@ -114,10 +123,16 @@ class PmsFolio(models.Model):
)
agency_id = fields.Many2one(
"res.partner",
- "Agency",
+ string="Agency",
ondelete="restrict",
domain=[("is_agency", "=", True)],
)
+ channel_type_id = fields.Many2one(
+ "pms.sale.channel",
+ string="Direct Sale Channel",
+ ondelete="restrict",
+ domain=[("channel_type", "=", "direct")],
+ )
payment_ids = fields.One2many("account.payment", "folio_id", readonly=True)
# return_ids = fields.One2many("payment.return", "folio_id", readonly=True)
payment_term_id = fields.Many2one(
@@ -163,15 +178,6 @@ class PmsFolio(models.Model):
string="Type",
default=lambda *a: "normal",
)
- channel_type = fields.Selection(
- [
- ("direct", "Direct"),
- ("agency", "Agency"),
- ],
- string="Sales Channel",
- compute="_compute_channel_type",
- store=True,
- )
date_order = fields.Datetime(
string="Order Date",
required=True,
@@ -284,17 +290,26 @@ class PmsFolio(models.Model):
folio.reservation_ids.filtered(lambda a: a.state != "cancelled")
)
- @api.depends("partner_id")
+ @api.depends("partner_id", "agency_id")
def _compute_pricelist_id(self):
for folio in self:
- pricelist_id = (
- folio.partner_id.property_product_pricelist
- and folio.partner_id.property_product_pricelist.id
- or self.env.user.pms_property_id.default_pricelist_id.id
- )
+ if folio.partner_id and folio.partner_id.property_product_pricelist:
+ pricelist_id = folio.partner_id.property_product_pricelist.id
+ else:
+ pricelist_id = self.env.user.pms_property_id.default_pricelist_id.id
if folio.pricelist_id.id != pricelist_id:
# TODO: Warning change de pricelist?
folio.pricelist_id = pricelist_id
+ if folio.agency_id and folio.agency_id.apply_pricelist:
+ pricelist_id = folio.agency_id.property_product_pricelist.id
+
+ @api.depends("agency_id")
+ def _compute_partner_id(self):
+ for folio in self:
+ if folio.agency_id and folio.agency_id.invoice_agency:
+ folio.partner_id = folio.agency_id.id
+ elif not folio.partner_id:
+ folio.partner_id = False
@api.depends("partner_id")
def _compute_user_id(self):
@@ -308,24 +323,25 @@ class PmsFolio(models.Model):
addr = folio.partner_id.address_get(["invoice"])
folio.partner_invoice_id = addr["invoice"]
- @api.depends("agency_id")
- def _compute_channel_type(self):
- for folio in self:
- if folio.agency_id:
- folio.channel_type = "agency"
- else:
- folio.channel_type = "direct"
-
@api.depends("partner_id")
def _compute_payment_term_id(self):
self.payment_term_id = False
for folio in self:
folio.payment_term_id = (
- self.partner_id.property_payment_term_id
- and self.partner_id.property_payment_term_id.id
+ folio.partner_id.property_payment_term_id
+ and folio.partner_id.property_payment_term_id.id
or False
)
+ @api.depends("reservation_ids")
+ def _compute_commission(self):
+ for folio in self:
+ for reservation in folio.reservation_ids:
+ if reservation.commission_amount != 0:
+ folio.commission += reservation.commission_amount
+ else:
+ folio.commission = 0
+
@api.depends(
"state", "reservation_ids.invoice_status", "service_ids.invoice_status"
)
@@ -667,3 +683,10 @@ class PmsFolio(models.Model):
(line[0].name, line[1]["amount"], line[1]["base"], len(res)) for line in res
]
return res
+
+ # Check that only one sale channel is selected
+ @api.constrains("agency_id", "channel_type_id")
+ def _check_only_one_channel(self):
+ for record in self:
+ if record.agency_id and record.channel_type_id:
+ raise models.ValidationError(_("There must be only one sale channel"))
diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py
index e29fd69b3..144c7a899 100644
--- a/pms/models/pms_reservation.py
+++ b/pms/models/pms_reservation.py
@@ -147,7 +147,14 @@ class PmsReservation(models.Model):
store=True,
readonly=False,
)
- agency_id = fields.Many2one(related="folio_id.agency_id")
+ agency_id = fields.Many2one(
+ related="folio_id.agency_id",
+ readonly=True,
+ )
+ channel_type_id = fields.Many2one(
+ related="folio_id.channel_type_id",
+ readonly=True,
+ )
partner_invoice_id = fields.Many2one(
"res.partner",
string="Invoice Address",
@@ -186,6 +193,17 @@ class PmsReservation(models.Model):
store=True,
readonly=False,
)
+ commission_percent = fields.Float(
+ string="Commission percent (%)",
+ compute="_compute_commission_percent",
+ store=True,
+ readonly=False,
+ )
+ commission_amount = fields.Float(
+ string="Commission amount",
+ compute="_compute_commission_amount",
+ store=True,
+ )
# TODO: Warning Mens to update pricelist
checkin_partner_ids = fields.One2many(
"pms.checkin.partner",
@@ -342,22 +360,6 @@ class PmsReservation(models.Model):
overbooking = fields.Boolean("Is Overbooking", default=False)
reselling = fields.Boolean("Is Reselling", default=False)
nights = fields.Integer("Nights", compute="_compute_nights", store=True)
- channel_type = fields.Selection(
- selection=[
- ("direct", "Direct"),
- ("agency", "Agency"),
- ],
- string="Sales Channel",
- default="direct",
- )
- subchannel_direct = fields.Selection(
- selection=[
- ("door", "Door"),
- ("mail", "Mail"),
- ("phone", "Phone"),
- ],
- string="Direct Channel",
- )
origin = fields.Char("Origin", compute="_compute_origin", store=True)
detail_origin = fields.Char(
"Detail Origin", compute="_compute_detail_origin", store=True
@@ -519,7 +521,7 @@ class PmsReservation(models.Model):
)
reservation.allowed_room_ids = rooms_available
- @api.depends("reservation_type")
+ @api.depends("reservation_type", "agency_id")
def _compute_partner_id(self):
for reservation in self:
if reservation.reservation_type == "out":
@@ -528,6 +530,8 @@ class PmsReservation(models.Model):
reservation.partner_id = reservation.folio_id.partner_id
else:
reservation.partner_id = False
+ if not reservation.partner_id and reservation.agency_id:
+ reservation.partner_id = reservation.agency_id
@api.depends("partner_id")
def _compute_partner_invoice_id(self):
@@ -760,6 +764,26 @@ class PmsReservation(models.Model):
return [("checkout", searching_for_true, today)]
+ @api.depends("agency_id")
+ def _compute_commission_percent(self):
+ for reservation in self:
+ if reservation.agency_id:
+ reservation.commission_percent = (
+ reservation.agency_id.default_commission
+ )
+ else:
+ reservation.commission_percent = 0
+
+ @api.depends("commission_percent", "price_total")
+ def _compute_commission_amount(self):
+ for reservation in self:
+ if reservation.commission_percent > 0:
+ reservation.commission_amount = (
+ reservation.price_total * reservation.commission_percent
+ )
+ else:
+ reservation.commission_amount = 0
+
# REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿?
@api.depends("preferred_room_id")
def _compute_adults(self):
@@ -1105,19 +1129,11 @@ class PmsReservation(models.Model):
@api.model
def create(self, vals):
- if "folio_id" in vals and "channel_type" not in vals:
+ if "folio_id" in vals:
folio = self.env["pms.folio"].browse(vals["folio_id"])
- channel_type = (
- vals["channel_type"] if "channel_type" in vals else folio.channel_type
- )
- partner_id = (
- vals["partner_id"] if "partner_id" in vals else folio.partner_id.id
- )
- vals.update({"channel_type": channel_type, "partner_id": partner_id})
elif "partner_id" in vals:
folio_vals = {
"partner_id": int(vals.get("partner_id")),
- "channel_type": vals.get("channel_type"),
}
# Create the folio in case of need
# (To allow to create reservations direct)
@@ -1126,7 +1142,6 @@ class PmsReservation(models.Model):
{
"folio_id": folio.id,
"reservation_type": vals.get("reservation_type"),
- "channel_type": vals.get("channel_type"),
}
)
record = super(PmsReservation, self).create(vals)
@@ -1288,20 +1303,6 @@ class PmsReservation(models.Model):
record.checkin_partner_count = 0
record.checkin_partner_pending_count = 0
- @api.depends("channel_type", "subchannel_direct")
- def _compute_origin(self):
- for reservation in self:
- if reservation.channel_type == "direct":
- reservation.origin = reservation.subchannel_direct
- elif reservation.channel_type == "agency":
- reservation.origin = reservation.agency_id.name
-
- @api.depends("origin")
- def _compute_detail_origin(self):
- for reservation in self:
- if reservation.channel_type in ["direct", "agency"]:
- reservation.detail_origin = reservation.sudo().create_uid.name
-
def _search_checkin_partner_pending(self, operator, value):
self.ensure_one()
recs = self.search([]).filtered(lambda x: x.checkin_partner_pending_count > 0)
diff --git a/pms/models/pms_sale_channel.py b/pms/models/pms_sale_channel.py
new file mode 100644
index 000000000..1daa07a59
--- /dev/null
+++ b/pms/models/pms_sale_channel.py
@@ -0,0 +1,12 @@
+from odoo import fields, models
+
+
+class PmsSaleChannel(models.Model):
+ _name = "pms.sale.channel"
+ _description = "Sales Channel"
+
+ # Fields declaration
+ name = fields.Text(string="Sale Channel Name")
+ channel_type = fields.Selection(
+ [("direct", "Direct"), ("indirect", "Indirect")], string="Sale Channel Type"
+ )
diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py
index 1c135639e..d715688e0 100644
--- a/pms/models/res_partner.py
+++ b/pms/models/res_partner.py
@@ -3,7 +3,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
-from odoo import api, fields, models
+from odoo import _, api, fields, models
_logger = logging.getLogger(__name__)
@@ -21,20 +21,41 @@ class ResPartner(models.Model):
folios_count = fields.Integer("Folios", compute="_compute_folios_count")
unconfirmed = fields.Boolean("Unconfirmed", default=True)
is_agency = fields.Boolean("Is Agency")
+ sale_channel_id = fields.Many2one(
+ "pms.sale.channel",
+ string="Sale Channel",
+ ondelete="restrict",
+ domain=[("channel_type", "=", "indirect")],
+ )
+ default_commission = fields.Integer("Commission")
+ apply_pricelist = fields.Boolean("Apply Pricelist")
+ invoice_agency = fields.Boolean("Invoice Agency")
# Compute and Search methods
def _compute_reservations_count(self):
pms_reservation_obj = self.env["pms.reservation"]
for record in self:
record.reservations_count = pms_reservation_obj.search_count(
- [("partner_id.id", "=", record.id)]
+ [
+ (
+ "partner_id.id",
+ "=",
+ record.id if isinstance(record.id, int) else False,
+ )
+ ]
)
def _compute_folios_count(self):
pms_folio_obj = self.env["pms.folio"]
for record in self:
record.folios_count = pms_folio_obj.search_count(
- [("partner_id.id", "=", record.id)]
+ [
+ (
+ "partner_id.id",
+ "=",
+ record.id if isinstance(record.id, int) else False,
+ )
+ ]
)
# ORM Overrides
@@ -64,3 +85,11 @@ class ResPartner(models.Model):
name, args=args, operator=operator, limit=limit_rest
)
return res
+
+ @api.constrains("is_agency", "sale_channel_id")
+ def _check_is_agency(self):
+ for record in self:
+ if record.is_agency and not record.sale_channel_id:
+ raise models.ValidationError(_("Sale Channel must be entered"))
+ if not record.is_agency and record.sale_channel_id:
+ record.sale_channel_id = None
diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv
index aeb970f96..2ebaf1009 100644
--- a/pms/security/ir.model.access.csv
+++ b/pms/security/ir.model.access.csv
@@ -24,6 +24,7 @@ user_access_pms_cancelation_rule,user_access_pms_cancelation_rule,model_pms_canc
user_access_account_full_reconcile,user_access_account_full_reconcile,account.model_account_full_reconcile,pms.group_pms_user,1,1,1,1
user_access_property,user_access_property,model_pms_property,pms.group_pms_user,1,0,0,0
user_access_availability,user_access_availability,model_pms_room_type_availability,pms.group_pms_user,1,0,0,0
+user_access_pms_sale_channel,user_access_pms_sale_channel,model_pms_sale_channel,pms.group_pms_user,1,0,0,0
manager_access_pms_floor,manager_access_pms_floor,model_pms_floor,pms.group_pms_manager,1,1,1,1
manager_access_pms_amenity,manager_access_pms_amenity,model_pms_amenity,pms.group_pms_manager,1,1,1,1
manager_access_pms_amenity_type,manager_access_pms_amenity_type,model_pms_amenity_type,pms.group_pms_manager,1,1,1,1
@@ -47,4 +48,5 @@ manager_access_pms_board_service_line,manager_access_pms_board_service_line,mode
manager_access_property,manager_access_property,model_pms_property,pms.group_pms_manager,1,1,1,1
manager_access_pms_cancelation_rule,manager_access_pms_cancelation_rule,model_pms_cancelation_rule,pms.group_pms_manager,1,1,1,1
manager_access_availability,manager_access_availability,model_pms_room_type_availability,pms.group_pms_manager,1,1,1,1
+manager_access_pms_sale_channel,manager_access_pms_sale_channel,model_pms_sale_channel,pms.group_pms_manager,1,1,1,1
user_access_pms_reservation_wizard,user_access_pms_reservation_wizard,model_pms_reservation_wizard,pms.group_pms_user,1,1,1,1
diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py
index 99f80c5c0..6a10f9f21 100644
--- a/pms/tests/__init__.py
+++ b/pms/tests/__init__.py
@@ -22,3 +22,5 @@
from . import test_pms_reservation
from . import test_pms_pricelist
from . import test_pms_checkin_partner
+from . import test_pms_sale_channel
+from . import test_pms_folio
diff --git a/pms/tests/test_pms_folio.py b/pms/tests/test_pms_folio.py
new file mode 100644
index 000000000..cd1bd45ac
--- /dev/null
+++ b/pms/tests/test_pms_folio.py
@@ -0,0 +1,58 @@
+import datetime
+
+from freezegun import freeze_time
+
+from .common import TestHotel
+
+freeze_time("2000-02-02")
+
+
+class TestPmsFolio(TestHotel):
+ def test_commission_and_partner_correct(self):
+ # ARRANGE
+ PmsFolio = self.env["pms.folio"]
+ PmsReservation = self.env["pms.reservation"]
+ PmsPartner = self.env["res.partner"]
+ PmsSaleChannel = self.env["pms.sale.channel"]
+ # ACT
+ saleChannel = PmsSaleChannel.create(
+ {"name": "saleChannel1", "channel_type": "indirect"}
+ )
+ agency = PmsPartner.create(
+ {
+ "name": "partner1",
+ "is_agency": True,
+ "invoice_agency": True,
+ "default_commission": 15,
+ "sale_channel_id": saleChannel.id,
+ }
+ )
+
+ reservation = PmsReservation.create(
+ {
+ "checkin": datetime.datetime.now(),
+ "checkout": datetime.datetime.now() + datetime.timedelta(days=3),
+ "agency_id": agency.id,
+ }
+ )
+ folio = PmsFolio.create(
+ {
+ "agency_id": agency.id,
+ "reservation_ids": [reservation.id],
+ }
+ )
+
+ commission = 0
+ for reservation in folio:
+ commission += reservation.commission_amount
+
+ # ASSERT
+ self.assertEqual(
+ folio.commission,
+ commission,
+ "Folio commission don't math with his reservation commission",
+ )
+ if folio.agency_id:
+ self.assertEqual(
+ folio.agency_id, folio.partner_id, "Agency has to be the partner"
+ )
diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py
new file mode 100644
index 000000000..f482b773c
--- /dev/null
+++ b/pms/tests/test_pms_sale_channel.py
@@ -0,0 +1,103 @@
+import datetime
+
+from freezegun import freeze_time
+
+from odoo.exceptions import ValidationError
+
+from .common import TestHotel
+
+
+@freeze_time("2010-01-01")
+class TestPmsSaleChannel(TestHotel):
+ def test_not_agency_as_agency(self):
+ # ARRANGE
+ PmsReservation = self.env["pms.reservation"]
+ not_agency = self.env["res.partner"].create(
+ {"name": "partner1", "is_agency": False}
+ )
+
+ # ACT & ASSERT
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ PmsReservation.create(
+ {
+ "checkin": datetime.datetime.now(),
+ "checkout": datetime.datetime.now() + datetime.timedelta(days=3),
+ "agency_id": not_agency.id,
+ }
+ )
+
+ def test_partner_as_direct_channel(self):
+ # ARRANGE
+ PmsReservation = self.env["pms.reservation"]
+ partner = self.env.ref("base.res_partner_12")
+ # ACT & ASSERT
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ PmsReservation.create(
+ {
+ "checkin": datetime.datetime.now(),
+ "checkout": datetime.datetime.now() + datetime.timedelta(days=3),
+ "channel_type_id": partner.id,
+ }
+ )
+
+ def test_channel_type_id_only_directs(self):
+ # ARRANGE
+ PmsReservation = self.env["pms.reservation"]
+ PmsSaleChannel = self.env["pms.sale.channel"]
+ # ACT
+ saleChannel = PmsSaleChannel.create({"channel_type": "direct"})
+ reservation = PmsReservation.create(
+ {
+ "checkin": datetime.datetime.now(),
+ "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3),
+ "channel_type_id": saleChannel.id,
+ }
+ )
+ # ASSERT
+ self.assertEqual(
+ self.browse_ref(reservation.channel_type_id).channel_type,
+ "direct",
+ "Sale channel is not direct",
+ )
+
+ def test_agency_id_is_agency(self):
+ # ARRANGE
+ PmsReservation = self.env["pms.reservation"]
+
+ # ACT
+ agency = self.env["res.partner"].create({"name": "partner1", "is_agency": True})
+ reservation = PmsReservation.create(
+ {
+ "checkin": datetime.datetime.now(),
+ "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3),
+ "agency_id": agency.id,
+ }
+ )
+ # ASSERT
+ self.assertEqual(
+ self.browse_ref(reservation.agency_id).is_agency,
+ True,
+ "Agency_id doesn't correspond to an agency",
+ )
+
+ def test_sale_channel_id_only_indirect(self):
+ # ARRANGE
+ PmsSaleChannel = self.env["pms.sale.channel"]
+ # ACT
+ saleChannel = PmsSaleChannel.create({"channel_type": "indirect"})
+ agency = self.env["res.partner"].create(
+ {"name": "example", "is_agency": True, "sale_channel_id": saleChannel.id}
+ )
+ # ASSERT
+ self.assertEqual(
+ self.browse_ref(agency.sale_channel_id).channel_type,
+ "indirect",
+ "An agency should be a indirect channel",
+ )
+
+ def test_agency_without_sale_channel_id(self):
+ # ARRANGE & ACT & ASSERT
+ with self.assertRaises(ValidationError), self.cr.savepoint():
+ self.env["res.partner"].create(
+ {"name": "example", "is_agency": True, "sale_channel_id": None}
+ )
diff --git a/pms/views/pms_folio_views.xml b/pms/views/pms_folio_views.xml
index ca2e6fb57..aed069ffc 100644
--- a/pms/views/pms_folio_views.xml
+++ b/pms/views/pms_folio_views.xml
@@ -98,14 +98,16 @@
name="reservation_type"
attrs="{'readonly':[('state','not in',('draft'))]}"
/>
-
+ />-->
+
+
diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml
index c1bc8593c..055a51b92 100644
--- a/pms/views/pms_reservation_views.xml
+++ b/pms/views/pms_reservation_views.xml
@@ -255,19 +255,6 @@
'readonly': [('partner_id', '!=', False),
('mobile','!=', False)]}"
/>
-
-
+ />-->
+
+
+
+
+
+
+
-
+ />-->
@@ -736,7 +736,7 @@
domain="[('to_assign','=',True)]"
/>
-
+ />-->
+
+
+ pms.sale.channel.form
+ pms.sale.channel
+
+
+
+
+
+ pms.sale.channel.tree
+ pms.sale.channel
+
+
+
+
+
+
+
+
+ Sale Channel
+ pms.sale.channel
+ tree,form
+
+
+
diff --git a/pms/views/res_partner_views.xml b/pms/views/res_partner_views.xml
index fb3e78067..76827ff04 100644
--- a/pms/views/res_partner_views.xml
+++ b/pms/views/res_partner_views.xml
@@ -39,12 +39,32 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+