From df08533c6c49d47bdf231ea8b20722e7c866c626 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 3 Nov 2020 12:51:39 +0100 Subject: [PATCH 1/9] [ADD] Sales Channel model --- pms/models/pms_sales_channel.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 pms/models/pms_sales_channel.py diff --git a/pms/models/pms_sales_channel.py b/pms/models/pms_sales_channel.py new file mode 100644 index 000000000..0c4a1aef9 --- /dev/null +++ b/pms/models/pms_sales_channel.py @@ -0,0 +1,16 @@ +from odoo import models, fields +class PmsSalesChannel(models.Model): + _name="pms.room.sales.channel" + _description="Sales Channel" + _order="sequence, channel_type, name" + + name=fields.Char("Sale Channel Name", required=True) + channel_type=field.Selection( + selection=[ + ("direct","Direct"), + ("indirect","Indirect"), + ], + string="Type" + ) + is_offline=fields.Boolean("Is Offline") + is_online=fields.Boolean("Is Online") From ed6039b7a3db94412b553e9941a9c56ff6dcf310 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 11 Nov 2020 10:03:50 +0100 Subject: [PATCH 2/9] [IMP] Test_pms_sale_channel --- pms/models/pms_folio.py | 4 +-- pms/models/pms_reservation.py | 44 ++++++++++++------------------ pms/models/pms_sales_channel.py | 16 ----------- pms/tests/__init__.py | 1 + pms/tests/test_pms_sale_channel.py | 32 ++++++++++++++++++++++ 5 files changed, 52 insertions(+), 45 deletions(-) delete mode 100644 pms/models/pms_sales_channel.py create mode 100644 pms/tests/test_pms_sale_channel.py diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 94149e51e..57d40699e 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -166,7 +166,7 @@ class PmsFolio(models.Model): channel_type = fields.Selection( [ ("direct", "Direct"), - ("agency", "Agency"), + ("indirect", "Indirect"), ], string="Sales Channel", compute="_compute_channel_type", @@ -313,7 +313,7 @@ class PmsFolio(models.Model): def _compute_channel_type(self): for folio in self: if folio.agency_id: - folio.channel_type = "agency" + folio.channel_type = "indirect" else: folio.channel_type = "direct" diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index cedccf2bd..313bdc3b3 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -147,6 +147,7 @@ class PmsReservation(models.Model): readonly=False, ) agency_id = fields.Many2one(related="folio_id.agency_id") + partner_invoice_id = fields.Many2one( "res.partner", string="Invoice Address", @@ -297,20 +298,13 @@ class PmsReservation(models.Model): 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"), + [ + ("direct","Direct"), + ("indirect","Indirect") ], - string="Sales Channel", - default="direct", - ) - subchannel_direct = fields.Selection( - selection=[ - ("door", "Door"), - ("mail", "Mail"), - ("phone", "Phone"), - ], - string="Direct Channel", + string="Channel type", + required = True, + store=True ) origin = fields.Char("Origin", compute="_compute_origin", store=True) detail_origin = fields.Char( @@ -742,6 +736,16 @@ class PmsReservation(models.Model): # self._compute_tax_ids() TODO: refact + @api.constrains("channel_type") + def check_channel_type(self): + for record in self: + if (record.channel_type == "indirect" and record.partner_id.is_agency != True): + raise ValidationError( + _( + "Indirect Sale Channel must have an agency associated!" + ) + ) + # Action methods def open_folio(self): @@ -1024,20 +1028,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_sales_channel.py b/pms/models/pms_sales_channel.py deleted file mode 100644 index 0c4a1aef9..000000000 --- a/pms/models/pms_sales_channel.py +++ /dev/null @@ -1,16 +0,0 @@ -from odoo import models, fields -class PmsSalesChannel(models.Model): - _name="pms.room.sales.channel" - _description="Sales Channel" - _order="sequence, channel_type, name" - - name=fields.Char("Sale Channel Name", required=True) - channel_type=field.Selection( - selection=[ - ("direct","Direct"), - ("indirect","Indirect"), - ], - string="Type" - ) - is_offline=fields.Boolean("Is Offline") - is_online=fields.Boolean("Is Online") diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index cfbd2f1f4..77e4bb457 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -21,3 +21,4 @@ ############################################################################## from . import test_pms_reservation from . import test_pms_pricelist +from . import test_pms_sale_channel diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py new file mode 100644 index 000000000..f58ac8c88 --- /dev/null +++ b/pms/tests/test_pms_sale_channel.py @@ -0,0 +1,32 @@ +from .common import TestHotel +from freezegun import freeze_time +from odoo.exceptions import ValidationError +import datetime +from odoo import fields + +@freeze_time("2010-01-01") +class TestPmsSaleChannel(TestHotel): + def test_reservation_indirect_channel(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), + "channel_type":"indirect", + "partner_id":not_agency.id + } + ) + + + + From 4c390532748a5a60087de8bc27f86ab41eb0f62b Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Wed, 11 Nov 2020 11:49:11 +0100 Subject: [PATCH 3/9] [IMP] pms.folio add sale channel constraint --- pms/models/pms_folio.py | 11 +++++++++++ pms/models/pms_reservation.py | 18 ++---------------- pms/tests/test_pms_sale_channel.py | 25 ++++++++++++++++++++++++- 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 57d40699e..493c1546b 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -443,6 +443,17 @@ class PmsFolio(models.Model): "amount_total": amount_untaxed + amount_tax, } ) + # Check channel type + @api.constrains("channel_type") + def check_channel_type(self): + for record in self: + if (record.channel_type == "indirect" and record.partner_id.is_agency != True + or record.channel_type == "direct" and record.partner_id.is_agency == True): + raise ValidationError( + _( + "Indirect Sale Channel must have an agency associated!" + ) + ) # TODO: Add return_ids to depends @api.depends("amount_total", "payment_ids", "reservation_type", "state") diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 313bdc3b3..7225a0a09 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -298,13 +298,9 @@ class PmsReservation(models.Model): reselling = fields.Boolean("Is Reselling", default=False) nights = fields.Integer("Nights", compute="_compute_nights", store=True) channel_type = fields.Selection( - [ - ("direct","Direct"), - ("indirect","Indirect") - ], - string="Channel type", + related="folio_id.channel_type", required = True, - store=True + readonly = True, ) origin = fields.Char("Origin", compute="_compute_origin", store=True) detail_origin = fields.Char( @@ -736,16 +732,6 @@ class PmsReservation(models.Model): # self._compute_tax_ids() TODO: refact - @api.constrains("channel_type") - def check_channel_type(self): - for record in self: - if (record.channel_type == "indirect" and record.partner_id.is_agency != True): - raise ValidationError( - _( - "Indirect Sale Channel must have an agency associated!" - ) - ) - # Action methods def open_folio(self): diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py index f58ac8c88..e54c03b3f 100644 --- a/pms/tests/test_pms_sale_channel.py +++ b/pms/tests/test_pms_sale_channel.py @@ -23,10 +23,33 @@ class TestPmsSaleChannel(TestHotel): "checkin": datetime.datetime.now(), "checkout":datetime.datetime.now() + datetime.timedelta(days=3), "channel_type":"indirect", - "partner_id":not_agency.id + "partner_id":not_agency.id, } ) + def test_reservation_direct_channel(self): + PmsReservation = self.env["pms.reservation"] + agency = self.env["res.partner"].create( + { + "name":"partner2", + "is_agency":True, + } + ) + #ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + PmsReservation.create( + { + "checkin": datetime.datetime.now() +datetime.timedelta(days=5), + "checkout":datetime.datetime.now() + datetime.timedelta(days=8), + "channel_type":"direct", + "partner_id":agency.id, + } + ) + + + + + From 54f8ff9c0ba61d01c04fca84c45bb8b9acaff5f3 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 16 Nov 2020 11:08:30 +0100 Subject: [PATCH 4/9] [IMP] add test to check sales channel domain --- pms/models/pms_folio.py | 36 ++-------- pms/models/pms_reservation.py | 15 ++-- pms/models/pms_sale_channel.py | 15 ++++ pms/models/res_partner.py | 14 ++++ pms/tests/test_pms_sale_channel.py | 105 +++++++++++++++++++++++----- pms/views/pms_folio_views.xml | 6 +- pms/views/pms_reservation_views.xml | 18 ++--- pms/views/res_partner_views.xml | 9 ++- 8 files changed, 152 insertions(+), 66 deletions(-) create mode 100644 pms/models/pms_sale_channel.py diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 493c1546b..83362bfbe 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -114,10 +114,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 +169,6 @@ class PmsFolio(models.Model): string="Type", default=lambda *a: "normal", ) - channel_type = fields.Selection( - [ - ("direct", "Direct"), - ("indirect", "Indirect"), - ], - string="Sales Channel", - compute="_compute_channel_type", - store=True, - ) date_order = fields.Datetime( string="Order Date", required=True, @@ -309,14 +306,6 @@ 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 = "indirect" - else: - folio.channel_type = "direct" - @api.depends("partner_id") def _compute_payment_term_id(self): self.payment_term_id = False @@ -443,17 +432,6 @@ class PmsFolio(models.Model): "amount_total": amount_untaxed + amount_tax, } ) - # Check channel type - @api.constrains("channel_type") - def check_channel_type(self): - for record in self: - if (record.channel_type == "indirect" and record.partner_id.is_agency != True - or record.channel_type == "direct" and record.partner_id.is_agency == True): - raise ValidationError( - _( - "Indirect Sale Channel must have an agency associated!" - ) - ) # TODO: Add return_ids to depends @api.depends("amount_total", "payment_ids", "reservation_type", "state") diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 7225a0a09..dfd2c39aa 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -146,8 +146,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.agency_id", + readonly=True, + ) partner_invoice_id = fields.Many2one( "res.partner", string="Invoice Address", @@ -297,11 +303,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( - related="folio_id.channel_type", - required = True, - readonly = True, - ) origin = fields.Char("Origin", compute="_compute_origin", store=True) detail_origin = fields.Char( "Detail Origin", compute="_compute_detail_origin", store=True diff --git a/pms/models/pms_sale_channel.py b/pms/models/pms_sale_channel.py new file mode 100644 index 000000000..7f72c7af9 --- /dev/null +++ b/pms/models/pms_sale_channel.py @@ -0,0 +1,15 @@ +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..14ce1e831 100644 --- a/pms/models/res_partner.py +++ b/pms/models/res_partner.py @@ -21,6 +21,12 @@ 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")], + ) # Compute and Search methods def _compute_reservations_count(self): @@ -64,3 +70,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/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py index e54c03b3f..a955ce117 100644 --- a/pms/tests/test_pms_sale_channel.py +++ b/pms/tests/test_pms_sale_channel.py @@ -6,7 +6,7 @@ from odoo import fields @freeze_time("2010-01-01") class TestPmsSaleChannel(TestHotel): - def test_reservation_indirect_channel(self): + def test_not_agency_as_agency(self): #ARRANGE PmsReservation = self.env["pms.reservation"] not_agency = self.env["res.partner"].create( @@ -22,34 +22,103 @@ class TestPmsSaleChannel(TestHotel): { "checkin": datetime.datetime.now(), "checkout":datetime.datetime.now() + datetime.timedelta(days=3), - "channel_type":"indirect", - "partner_id":not_agency.id, + "agency_id":not_agency.id, } ) - def test_reservation_direct_channel(self): + def test_partner_as_direct_channel(self): + #ARRANGE PmsReservation = self.env["pms.reservation"] - agency = self.env["res.partner"].create( - { - "name":"partner2", - "is_agency":True, - } - ) + partner = customer = self.env.ref("base.res_partner_12") #ACT & ASSERT with self.assertRaises(ValidationError), self.cr.savepoint(): PmsReservation.create( - { - "checkin": datetime.datetime.now() +datetime.timedelta(days=5), - "checkout":datetime.datetime.now() + datetime.timedelta(days=8), - "channel_type":"direct", - "partner_id":agency.id, + { + "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 c4b310a55..a60137167 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 928d8659f..056cf9601 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -243,7 +243,7 @@ 'readonly': [('partner_id', '!=', False), ('mobile','!=', False)]}" /> - - + />--> + + - + />--> @@ -827,7 +829,7 @@ domain="[('to_assign','=',True)]" /> - + />--> - + + + Date: Tue, 17 Nov 2020 09:21:02 +0100 Subject: [PATCH 5/9] [IMP] precommit fixed --- pms/models/pms_folio.py | 4 +- pms/models/pms_reservation.py | 6 +- pms/models/pms_sale_channel.py | 15 ++-- pms/models/res_partner.py | 12 +-- pms/tests/test_pms_sale_channel.py | 133 ++++++++++++---------------- pms/views/pms_folio_views.xml | 4 +- pms/views/pms_reservation_views.xml | 4 +- pms/views/res_partner_views.xml | 2 +- 8 files changed, 78 insertions(+), 102 deletions(-) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 83362bfbe..6ee800782 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -121,8 +121,8 @@ class PmsFolio(models.Model): channel_type_id = fields.Many2one( "pms.sale.channel", string="Direct Sale Channel", - ondelete = "restrict", - domain=[("channel_type","=","direct")], + 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) diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index dfd2c39aa..ac5077310 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -149,11 +149,11 @@ class PmsReservation(models.Model): agency_id = fields.Many2one( related="folio_id.agency_id", readonly=True, - ) + ) channel_type_id = fields.Many2one( - related="folio_id.agency_id", + related="folio_id.channel_type_id", readonly=True, - ) + ) partner_invoice_id = fields.Many2one( "res.partner", string="Invoice Address", diff --git a/pms/models/pms_sale_channel.py b/pms/models/pms_sale_channel.py index 7f72c7af9..1daa07a59 100644 --- a/pms/models/pms_sale_channel.py +++ b/pms/models/pms_sale_channel.py @@ -1,15 +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" - ) + # Fields declaration + name = fields.Text(string="Sale Channel Name") channel_type = fields.Selection( - [ - ("direct","Direct"), - ("indirect","Indirect") - ], - string = "Sale Channel Type" + [("direct", "Direct"), ("indirect", "Indirect")], string="Sale Channel Type" ) diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py index 14ce1e831..33461f377 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__) @@ -24,8 +24,8 @@ class ResPartner(models.Model): sale_channel_id = fields.Many2one( "pms.sale.channel", string="Sale Channel", - ondelete = "restrict", - domain=[("channel_type","=","indirect")], + ondelete="restrict", + domain=[("channel_type", "=", "indirect")], ) # Compute and Search methods @@ -71,10 +71,10 @@ class ResPartner(models.Model): ) return res - @api.constrains("is_agency","sale_channel_id") + @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")) + raise models.ValidationError(_("Sale Channel must be entered")) if not record.is_agency and record.sale_channel_id: - record.sale_channel_id=None + record.sale_channel_id = None diff --git a/pms/tests/test_pms_sale_channel.py b/pms/tests/test_pms_sale_channel.py index a955ce117..f482b773c 100644 --- a/pms/tests/test_pms_sale_channel.py +++ b/pms/tests/test_pms_sale_channel.py @@ -1,124 +1,103 @@ -from .common import TestHotel -from freezegun import freeze_time -from odoo.exceptions import ValidationError import datetime -from odoo import fields + +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 + # ARRANGE PmsReservation = self.env["pms.reservation"] - partner = customer = self.env.ref("base.res_partner_12") - #ACT & ASSERT + 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), - "channel_type_id":partner.id, + "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 + # ARRANGE PmsReservation = self.env["pms.reservation"] PmsSaleChannel = self.env["pms.sale.channel"] - #ACT - saleChannel = PmsSaleChannel.create( - { - "channel_type":"direct" - } - ) + # 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 + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3), + "channel_type_id": saleChannel.id, } ) - #ASSERT + # ASSERT self.assertEqual( self.browse_ref(reservation.channel_type_id).channel_type, "direct", - "Sale channel is not direct" + "Sale channel is not direct", ) def test_agency_id_is_agency(self): - #ARRANGE + # ARRANGE PmsReservation = self.env["pms.reservation"] - #ACT - agency = self.env["res.partner"].create( - { - "name":"partner1", - "is_agency":True - } - ) + # 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 + "checkin": datetime.datetime.now(), + "checkout": datetime.datetime.now() + datetime.datetimedelta(days=3), + "agency_id": agency.id, } ) - #ASSERT + # ASSERT self.assertEqual( self.browse_ref(reservation.agency_id).is_agency, True, - "Agency_id doesn't correspond to an agency" + "Agency_id doesn't correspond to an agency", ) def test_sale_channel_id_only_indirect(self): - #ARRANGE + # ARRANGE PmsSaleChannel = self.env["pms.sale.channel"] - #ACT - saleChannel = PmsSaleChannel.create( - { - "channel_type":"indirect" - } - ) + # ACT + saleChannel = PmsSaleChannel.create({"channel_type": "indirect"}) agency = self.env["res.partner"].create( - { - "name":"example", - "is_agency":True, - "sale_channel_id":saleChannel.id - } + {"name": "example", "is_agency": True, "sale_channel_id": saleChannel.id} ) - #ASSERT + # ASSERT self.assertEqual( self.browse_ref(agency.sale_channel_id).channel_type, "indirect", - "An agency should be a indirect channel" + "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 - } - ) + # 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 a60137167..3e912eb46 100644 --- a/pms/views/pms_folio_views.xml +++ b/pms/views/pms_folio_views.xml @@ -106,8 +106,8 @@ name="agency_id" options="{'no_create': True,'no_open': True}" />--> - - + + diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 056cf9601..bcdde8c26 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -289,8 +289,8 @@ name="agency_id" options="{'no_create': True,'no_open': True}" />--> - - + + --> - + Date: Mon, 23 Nov 2020 14:07:24 +0100 Subject: [PATCH 6/9] [IMP] add sale channel views --- pms/__manifest__.py | 1 + pms/data/pms_data.xml | 17 +++++++++++++ pms/models/__init__.py | 1 + pms/models/pms_reservation.py | 14 ++++++----- pms/security/ir.model.access.csv | 2 ++ pms/views/pms_reservation_views.xml | 4 +-- pms/views/pms_sale_channel.xml | 39 +++++++++++++++++++++++++++++ pms/views/res_partner_views.xml | 16 +++++------- 8 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 pms/views/pms_sale_channel.xml 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..3152d83f7 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_reservation.py b/pms/models/pms_reservation.py index ac5077310..9c71e2f00 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -824,19 +824,21 @@ 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 and "channel_type_id" not 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 + channel_type_id = ( + vals["channel_type_id"] + if "channel_type_id" in vals + else folio.channel_type_id ) 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}) + vals.update({"channel_type_id": channel_type_id, "partner_id": partner_id}) elif "partner_id" in vals: folio_vals = { "partner_id": int(vals.get("partner_id")), - "channel_type": vals.get("channel_type"), + "channel_type_id": vals.get("channel_type_id"), } # Create the folio in case of need # (To allow to create reservations direct) @@ -845,7 +847,7 @@ class PmsReservation(models.Model): { "folio_id": folio.id, "reservation_type": vals.get("reservation_type"), - "channel_type": vals.get("channel_type"), + "channel_type_id": vals.get("channel_type_id"), } ) record = super(PmsReservation, self).create(vals) diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index 02f4a382b..2c6a46253 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/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index bcdde8c26..a49fc2c60 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -289,8 +289,6 @@ name="agency_id" options="{'no_create': True,'no_open': 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 dd8ce2f40..f7d35348d 100644 --- a/pms/views/res_partner_views.xml +++ b/pms/views/res_partner_views.xml @@ -39,19 +39,15 @@ - + - - Date: Mon, 30 Nov 2020 10:08:32 +0100 Subject: [PATCH 7/9] [IMP] add commissions on reservation and folio --- pms/data/pms_data.xml | 8 ++-- pms/models/pms_folio.py | 45 +++++++++++++++++----- pms/models/pms_reservation.py | 48 +++++++++++++++++------- pms/models/res_partner.py | 3 ++ pms/tests/__init__.py | 1 + pms/tests/test_pms_folio.py | 58 +++++++++++++++++++++++++++++ pms/views/pms_reservation_views.xml | 24 ++++++------ pms/views/res_partner_views.xml | 27 +++++++++++--- 8 files changed, 170 insertions(+), 44 deletions(-) create mode 100644 pms/tests/test_pms_folio.py diff --git a/pms/data/pms_data.xml b/pms/data/pms_data.xml index 3152d83f7..7eafee0f0 100644 --- a/pms/data/pms_data.xml +++ b/pms/data/pms_data.xml @@ -37,19 +37,19 @@ Door - Direct + direct Phone - Direct + direct Mail - Direct + direct Agency - Indirect + indirect diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 6ee800782..a760de0a0 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", @@ -282,17 +291,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): @@ -311,11 +329,20 @@ class PmsFolio(models.Model): 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" ) diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 9c71e2f00..22cbff138 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -192,6 +192,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", "reservation_id") segmentation_ids = fields.Many2many( @@ -438,7 +449,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": @@ -447,6 +458,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): @@ -525,6 +538,26 @@ class PmsReservation(models.Model): # TODO: Warning change de pricelist? reservation.pricelist_id = pricelist_id + @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): @@ -824,21 +857,11 @@ class PmsReservation(models.Model): @api.model def create(self, vals): - if "folio_id" in vals and "channel_type_id" not in vals: + if "folio_id" in vals: folio = self.env["pms.folio"].browse(vals["folio_id"]) - channel_type_id = ( - vals["channel_type_id"] - if "channel_type_id" in vals - else folio.channel_type_id - ) - partner_id = ( - vals["partner_id"] if "partner_id" in vals else folio.partner_id.id - ) - vals.update({"channel_type_id": channel_type_id, "partner_id": partner_id}) elif "partner_id" in vals: folio_vals = { "partner_id": int(vals.get("partner_id")), - "channel_type_id": vals.get("channel_type_id"), } # Create the folio in case of need # (To allow to create reservations direct) @@ -847,7 +870,6 @@ class PmsReservation(models.Model): { "folio_id": folio.id, "reservation_type": vals.get("reservation_type"), - "channel_type_id": vals.get("channel_type_id"), } ) record = super(PmsReservation, self).create(vals) diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py index 33461f377..d13b1f299 100644 --- a/pms/models/res_partner.py +++ b/pms/models/res_partner.py @@ -27,6 +27,9 @@ class ResPartner(models.Model): 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): diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index 77e4bb457..682b252be 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -22,3 +22,4 @@ from . import test_pms_reservation from . import test_pms_pricelist 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/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index a49fc2c60..612bf4ff3 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -243,19 +243,6 @@ 'readonly': [('partner_id', '!=', False), ('mobile','!=', False)]}" /> - + + + + + - + - + + + > + + + + + + + + From 5a5b2a8b5587c9ed7f45a2fe2b5f164d242d9b45 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Nov 2020 13:32:26 +0100 Subject: [PATCH 8/9] [FIX] Check partner_id in reservations and folios count --- pms/models/res_partner.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pms/models/res_partner.py b/pms/models/res_partner.py index d13b1f299..d715688e0 100644 --- a/pms/models/res_partner.py +++ b/pms/models/res_partner.py @@ -36,14 +36,26 @@ class ResPartner(models.Model): 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 From 23fa6d9621cd6245422b327301f928da35152687 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Nov 2020 13:52:51 +0100 Subject: [PATCH 9/9] [IMP] Checking only one selected sales channel --- pms/models/pms_folio.py | 7 +++++++ pms/views/res_partner_views.xml | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index a760de0a0..00a197b5c 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -674,3 +674,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/views/res_partner_views.xml b/pms/views/res_partner_views.xml index 27010e070..76827ff04 100644 --- a/pms/views/res_partner_views.xml +++ b/pms/views/res_partner_views.xml @@ -40,7 +40,9 @@ - + + +