From df08533c6c49d47bdf231ea8b20722e7c866c626 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Tue, 3 Nov 2020 12:51:39 +0100 Subject: [PATCH 01/37] [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 02/37] [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 03/37] [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 04/37] [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, 3 Nov 2020 13:34:02 +0100 Subject: [PATCH 05/37] [WIP]pms: Add first TestsCase for checkin flow --- pms/tests/__init__.py | 1 + pms/tests/test_pms_checkin_partner.py | 48 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 pms/tests/test_pms_checkin_partner.py diff --git a/pms/tests/__init__.py b/pms/tests/__init__.py index cfbd2f1f4..99f80c5c0 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_checkin_partner diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py new file mode 100644 index 000000000..68abc79a7 --- /dev/null +++ b/pms/tests/test_pms_checkin_partner.py @@ -0,0 +1,48 @@ +from freezegun import freeze_time + +from .common import TestHotel + + +@freeze_time("2012-01-14") +class TestPmsCheckinPartner(TestHotel): + @classmethod + def setUpClass(cls): + super(TestHotel, cls).setUpClass() + + def test_create_checkin_partner(self): + + # ARRANGE + host1 = self.env["res.partner"].create( + { + "name": "Miguel", + "phone": "654667733", + "email": "miguel@example.com", + } + ) + reservation_vals = { + "checkin": "2012-01-14", + "checkout": "2012-01-17", + "room_type_id": self.env.ref("pms.pms_room_type_3").id, + "partner_id": host1.id, + "pms_property_id": self.env.ref("pms.main_pms_property").id, + } + demo_user = self.env.ref("base.user_demo") + + # ACT + reservation_1 = ( + self.env["pms.reservation"].with_user(demo_user).create(reservation_vals) + ) + checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": host1.id, + "reservation_id": reservation_1.id, + } + ) + checkin1.onboard() + + # ASSERT + self.assertEqual( + checkin1.state, + "onboard", + "the checkin was not successful", + ) From ccf4245aebc6f625b802635b9791e6d3fe1bf566 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Wed, 4 Nov 2020 08:57:09 +0100 Subject: [PATCH 06/37] [WIP]pms: change checkin name field arriva-departure --- pms/demo/pms_folio.xml | 6 +- pms/i18n/es.po | 8 +- pms/models/pms_checkin_partner.py | 174 ++++-------------------- pms/models/pms_reservation.py | 2 +- pms/tests/test_pms_checkin_partner.py | 142 ++++++++++++++++--- pms/views/pms_checkin_partner_views.xml | 34 +++-- 6 files changed, 175 insertions(+), 191 deletions(-) diff --git a/pms/demo/pms_folio.xml b/pms/demo/pms_folio.xml index be9214c38..a5b48079e 100644 --- a/pms/demo/pms_folio.xml +++ b/pms/demo/pms_folio.xml @@ -34,7 +34,7 @@ /> - + - + normal @@ -381,7 +381,7 @@ /> - + normal diff --git a/pms/i18n/es.po b/pms/i18n/es.po index 0eca6fec0..7b22c075c 100644 --- a/pms/i18n/es.po +++ b/pms/i18n/es.po @@ -5700,12 +5700,12 @@ msgid "End Date" msgstr "Fecha de finalización" #. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_enter_date +#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_arrival msgid "Enter Date" msgstr "Fecha de entrada" #. module: hotel -#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_exit_date +#: model:ir.model.fields,field_description:hotel.field_hotel_checkin_partner_departure msgid "Exit Date" msgstr "Fecha salida" @@ -6671,7 +6671,7 @@ msgstr "Mail" #: model:ir.model.fields,help:hotel.field_hotel_room_type_property_valuation msgid "" "Manual: The accounting entries to value the inventory are not posted automatically.\n" -" Automated: An accounting entry is automatically created to value the inventory when a product enters or leaves the company." +" Automated: An accounting arrival is automatically created to value the inventory when a product enters or leaves the company." msgstr "" "Manual: Los registros contables de valoración del inventario no se publican automáticamente.\n" " Automatizado: Se crea automáticamente un registro contable para evaluar el inventario cuando un producto entra o sale de la empresa." @@ -7215,7 +7215,7 @@ msgstr "Pagos" #. module: hotel #: selection:hotel.checkin.partner,state:0 selection:hotel.reservation,state:0 -msgid "Pending Entry" +msgid "Pending arrival" msgstr "Por entrar" #. module: hotel diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 4acc25465..728319274 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -1,103 +1,44 @@ # Copyright 2017 Dario Lodeiros # Copyright 2018 Alexandre Diaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import datetime from odoo import _, api, fields, models from odoo.exceptions import ValidationError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT class PmsCheckinPartner(models.Model): _name = "pms.checkin.partner" _description = "Partner Checkins" - # Default Methods ang Gets - def _default_reservation_id(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation - return False - - def _default_partner_id(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - partner_ids = [] - if reservation.folio_id: - for room in reservation.folio_id.reservation_ids: - partner_ids.append(room.mapped("checkin_partner_ids.partner_id.id")) - if "checkin_partner_ids" in self.env.context: - for checkin in self.env.context["checkin_partner_ids"]: - if checkin[0] == 0: - partner_ids.append(checkin[2].get("partner_id")) - if ( - self._context.get("include_customer") - and reservation.partner_id.id not in partner_ids - and not reservation.partner_id.is_company - ): - return reservation.partner_id - return False - - def _default_folio_id(self): - if "folio_id" in self.env.context: - folio = self.env["pms.folio"].browse([self.env.context["folio_id"]]) - return folio - if "reservation_id" in self.env.context: - folio = ( - self.env["pms.reservation"] - .browse([self.env.context["reservation_id"]]) - .folio_id - ) - return folio - return False - - def _default_enter_date(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation.checkin - return False - - def _default_exit_date(self): - if "reservation_id" in self.env.context: - reservation = self.env["pms.reservation"].browse( - [self.env.context["reservation_id"]] - ) - return reservation.checkout - return False - @api.model def _get_default_pms_property(self): + # TODO: Change by property env variable (like company) return self.env.user.pms_property_id # Fields declaration partner_id = fields.Many2one( - "res.partner", default=_default_partner_id, required=True + "res.partner", + required=True, + domain="[('is_company', '=', False)]", ) - reservation_id = fields.Many2one("pms.reservation", default=_default_reservation_id) + reservation_id = fields.Many2one("pms.reservation") folio_id = fields.Many2one( - "pms.folio", default=_default_folio_id, readonly=True, required=True + "pms.folio", + compute="_compute_folio_id", + store=True, + readonly=False, ) pms_property_id = fields.Many2one( "pms.property", default=_get_default_pms_property, required=True ) email = fields.Char("E-mail", related="partner_id.email") mobile = fields.Char("Mobile", related="partner_id.mobile") - enter_date = fields.Date(default=_default_enter_date, required=True) - exit_date = fields.Date(default=_default_exit_date, required=True) - arrival_hour = fields.Char("Arrival Hour", help="Default Arrival Hour (HH:MM)") - departure_hour = fields.Char( - "Departure Hour", help="Default Departure Hour (HH:MM)" - ) + arrival = fields.Datetime("Enter") + departure = fields.Datetime("Exit") auto_booking = fields.Boolean("Get in Now", default=False) state = fields.Selection( selection=[ - ("draft", "Pending Entry"), + ("draft", "Pending arrival"), ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), @@ -105,50 +46,36 @@ class PmsCheckinPartner(models.Model): string="State", readonly=True, default=lambda *a: "draft", - tracking=True, ) + # Compute + + @api.depends("reservation_id", "reservation_id.folio_id") + def _compute_folio_id(self): + for record in self: + record.folio_id = record.reservation_id.folio_id + # Constraints and onchanges - @api.constrains("exit_date", "enter_date") - def _check_exit_date(self): + @api.constrains("departure", "arrival") + def _check_departure(self): for record in self: - date_in = fields.Date.from_string(record.enter_date) - date_out = fields.Date.from_string(record.exit_date) - if date_out < date_in: - raise models.ValidationError( + if record.departure and record.arrival < record.departure: + raise ValidationError( _("Departure date (%s) is prior to arrival on %s") - % (date_out, date_in) + % (record.departure, record.arrival) ) - @api.onchange("enter_date", "exit_date") - def _onchange_enter_date(self): - date_in = fields.Date.from_string(self.enter_date) - date_out = fields.Date.from_string(self.exit_date) - if date_out <= date_in: - date_out = date_in + datetime.timedelta(days=1) - self.update({"exit_date": date_out}) - raise ValidationError( - _("Departure date, is prior to arrival. Check it now. %s") % date_out - ) - - @api.onchange("partner_id") + @api.constrains("partner_id") def _check_partner_id(self): for record in self: if record.partner_id: - if record.partner_id.is_company: - raise models.ValidationError( - _( - "A Checkin Guest is configured like a company, \ - modify it in contact form if its a mistake" - ) - ) indoor_partner_ids = record.reservation_id.checkin_partner_ids.filtered( lambda r: r.id != record.id ).mapped("partner_id.id") if indoor_partner_ids.count(record.partner_id.id) > 1: record.partner_id = None - raise models.ValidationError( + raise ValidationError( _("This guest is already registered in the room") ) @@ -157,18 +84,16 @@ class PmsCheckinPartner(models.Model): def action_on_board(self): for record in self: if record.reservation_id.checkin > fields.Date.today(): - raise models.ValidationError(_("It is not yet checkin day!")) - hour = record._get_arrival_hour() + raise ValidationError(_("It is not yet checkin day!")) + if record.reservation_id.checkout <= fields.Date.today(): + raise ValidationError(_("Its too late to checkin")) vals = { "state": "onboard", - "arrival_hour": hour, + "arrival": fields.Datetime.now(), } record.update(vals) if record.reservation_id.state == "confirm": record.reservation_id.state = "onboard" - return { - "type": "ir.actions.do_nothing", - } def action_done(self): for record in self: @@ -188,42 +113,3 @@ class PmsCheckinPartner(models.Model): if vals.get("auto_booking", False): record.action_on_board() return record - - # Business methods - def _get_arrival_hour(self): - self.ensure_one() - tz_property = self.env.user.pms_property_id.tz - today = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT), - ) - default_arrival_hour = self.env.user.pms_property_id.default_arrival_hour - if self.reservation_id.checkin < today.strftime(DEFAULT_SERVER_DATE_FORMAT): - return default_arrival_hour - now = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime( - fields.Datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT - ), - ) - arrival_hour = now.strftime("%H:%M") - return arrival_hour - - def _get_departure_hour(self): - self.ensure_one() - tz_property = self.env.user.pms_property_id.tz - today = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime(fields.Date.today(), DEFAULT_SERVER_DATE_FORMAT), - ) - default_departure_hour = self.env.user.pms_property_id.default_departure_hour - if self.reservation_id.checkout < today.strftime(DEFAULT_SERVER_DATE_FORMAT): - return default_departure_hour - now = fields.Datetime.context_timestamp( - self.with_context(tz=tz_property), - datetime.datetime.strptime( - fields.Datetime.now(), DEFAULT_SERVER_DATETIME_FORMAT - ), - ) - departure_hour = now.strftime("%H:%M") - return departure_hour diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index cedccf2bd..25c872f72 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -239,7 +239,7 @@ class PmsReservation(models.Model): state = fields.Selection( [ ("draft", "Pre-reservation"), - ("confirm", "Pending Entry"), + ("confirm", "Pending arrival"), ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py index 68abc79a7..9049cd3ac 100644 --- a/pms/tests/test_pms_checkin_partner.py +++ b/pms/tests/test_pms_checkin_partner.py @@ -1,18 +1,21 @@ +import logging + from freezegun import freeze_time +from odoo import fields +from odoo.exceptions import ValidationError + from .common import TestHotel +_logger = logging.getLogger(__name__) + @freeze_time("2012-01-14") class TestPmsCheckinPartner(TestHotel): @classmethod - def setUpClass(cls): - super(TestHotel, cls).setUpClass() - - def test_create_checkin_partner(self): - - # ARRANGE - host1 = self.env["res.partner"].create( + def arrange_single_checkin(cls): + # Arrange for one checkin on one reservation + cls.host1 = cls.env["res.partner"].create( { "name": "Miguel", "phone": "654667733", @@ -22,27 +25,124 @@ class TestPmsCheckinPartner(TestHotel): reservation_vals = { "checkin": "2012-01-14", "checkout": "2012-01-17", - "room_type_id": self.env.ref("pms.pms_room_type_3").id, - "partner_id": host1.id, - "pms_property_id": self.env.ref("pms.main_pms_property").id, + "room_type_id": cls.env.ref("pms.pms_room_type_3").id, + "partner_id": cls.host1.id, + "pms_property_id": cls.env.ref("pms.main_pms_property").id, } - demo_user = self.env.ref("base.user_demo") - - # ACT - reservation_1 = ( - self.env["pms.reservation"].with_user(demo_user).create(reservation_vals) + demo_user = cls.env.ref("base.user_demo") + cls.reservation_1 = ( + cls.env["pms.reservation"].with_user(demo_user).create(reservation_vals) ) - checkin1 = self.env["pms.checkin.partner"].create( + cls.checkin1 = cls.env["pms.checkin.partner"].create( { - "partner_id": host1.id, - "reservation_id": reservation_1.id, + "partner_id": cls.host1.id, + "reservation_id": cls.reservation_1.id, } ) - checkin1.onboard() + + def test_onboard_checkin(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACT + self.checkin1.action_on_board() # ASSERT self.assertEqual( - checkin1.state, + self.checkin1.state, "onboard", - "the checkin was not successful", + "the partner checkin was not successful", ) + + def test_onboard_reservation(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.reservation_1.state, + "onboard", + "the reservation checkin was not successful", + ) + + def test_premature_checkin(self): + # ARRANGE + self.arrange_single_checkin() + self.reservation_1.write( + { + "checkin": "2012-01-15", + } + ) + + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.checkin1.action_on_board() + + def test_late_checkin(self): + # ARRANGE + self.arrange_single_checkin() + self.reservation_1.write( + { + "checkin": "2012-01-13", + } + ) + + # ACT + self.checkin1.action_on_board() + + # ASSERT + self.assertEqual( + self.checkin1.arrival, + fields.datetime.now(), + "the late checkin has problems", + ) + + def test_too_many_people_checkin(self): + # ARRANGE + self.arrange_single_checkin() + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "phone": "654667733", + "email": "carlos@example.com", + } + ) + host3 = self.env["res.partner"].create( + { + "name": "Enmanuel", + "phone": "654667733", + "email": "enmanuel@example.com", + } + ) + host4 = self.env["res.partner"].create( + { + "name": "Enrique", + "phone": "654667733", + "email": "enrique@example.com", + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host2.id, + "reservation_id": self.reservation_1.id, + } + ) + self.env["pms.checkin.partner"].create( + { + "partner_id": host3.id, + "reservation_id": self.reservation_1.id, + } + ) + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.env["pms.checkin.partner"].create( + { + "partner_id": host4.id, + "reservation_id": self.reservation_1.id, + } + ) diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index f17d2deed..0f2392df3 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -27,10 +27,8 @@ domain="[('is_company','=', False)]" /> - - - - + + @@ -56,7 +54,7 @@ Date: Mon, 9 Nov 2020 14:43:14 +0100 Subject: [PATCH 09/37] [WIP] Checkin status indicators --- pms/models/pms_checkin_partner.py | 16 ++++++ pms/models/pms_reservation.py | 79 ++++++++++++++++++++++++++- pms/tests/test_pms_checkin_partner.py | 33 +++++++++++ pms/views/pms_reservation_views.xml | 39 ++++++++----- 4 files changed, 151 insertions(+), 16 deletions(-) diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 63c008757..e404cdcac 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -31,10 +31,12 @@ class PmsCheckinPartner(models.Model): pms_property_id = fields.Many2one( "pms.property", default=_get_default_pms_property, required=True ) + name = fields.Char("E-mail", related="partner_id.name") email = fields.Char("E-mail", related="partner_id.email") mobile = fields.Char("Mobile", related="partner_id.mobile") arrival = fields.Datetime("Enter") departure = fields.Datetime("Exit") + completed_data = fields.Boolean(compute="_compute_completed_data", store=True) state = fields.Selection( selection=[ ("draft", "Pending arrival"), @@ -54,6 +56,20 @@ class PmsCheckinPartner(models.Model): for record in self: record.folio_id = record.reservation_id.folio_id + @api.depends(lambda self: self._checkin_mandatory_fields(), "state") + def _compute_completed_data(self): + self.completed_data = False + for record in self: + if any( + not getattr(self, field) for field in record._checkin_mandatory_fields() + ): + record.completed_data = False + break + record.completed_data = True + + def _checkin_mandatory_fields(self): + return ["name"] + # Constraints and onchanges @api.constrains("departure", "arrival") diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 3ee218c74..a580c369f 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -5,7 +5,7 @@ import logging from datetime import timedelta from odoo import _, api, fields, models -from odoo.exceptions import ValidationError +from odoo.exceptions import UserError, ValidationError from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, float_compare, float_is_zero _logger = logging.getLogger(__name__) @@ -188,7 +188,7 @@ class PmsReservation(models.Model): # TODO: Warning Mens to update pricelist checkin_partner_ids = fields.One2many("pms.checkin.partner", "reservation_id") count_pending_arrival = fields.Integer( - "Reservation Description", + "Pending Arrival", compute="_compute_count_pending_arrival", store=True, ) @@ -196,6 +196,21 @@ class PmsReservation(models.Model): string="Pending Arrival Ratio", compute="_compute_checkins_ratio", ) + pending_checkin_data = fields.Integer( + "Pending Checkin Data", + compute="_compute_pending_checkin_data", + store=True, + ) + arrival_today = fields.Boolean( + compute="_compute_arrival_today", search="_search_arrival_today" + ) + departure_today = fields.Boolean( + compute="_compute_departure_today", search="_search_departure_today" + ) + ratio_checkin_data = fields.Integer( + string="Pending Checkin Data Ratio", + compute="_compute_ratio_checkin_data", + ) segmentation_ids = fields.Many2many( "res.partner.category", string="Segmentation", @@ -562,6 +577,66 @@ class PmsReservation(models.Model): / reservation.adults ) + @api.depends("checkin_partner_ids", "checkin_partner_ids.completed_data") + def _compute_pending_checkin_data(self): + for reservation in self: + reservation.pending_checkin_data = reservation.adults - len( + reservation.checkin_partner_ids.filtered( + lambda c: c.state != "cancelled" and c.completed_data + ) + ) + + @api.depends("pending_checkin_data") + def _compute_ratio_checkin_data(self): + self.ratio_checkin_data = 0 + for reservation in self.filtered(lambda r: r.adults > 0): + reservation.ratio_checkin_data = ( + (reservation.adults - reservation.pending_checkin_data) + * 100 + / reservation.adults + ) + + def _compute_arrival_today(self): + for record in self: + record.arrival_today = ( + True if record.checkin == fields.Date.today() else False + ) + # REVIEW: Late checkin?? (next day) + + def _search_arrival_today(self, operator, value): + if operator not in ("=", "!="): + raise UserError(_("Invalid domain operator %s", operator)) + + if value not in (False, True): + raise UserError(_("Invalid domain right operand %s", value)) + + searching_for_true = (operator == "=" and value) or ( + operator == "!=" and not value + ) + today = fields.Date.context_today(self) + + return [("checkin", searching_for_true, today)] + + def _compute_departure_today(self): + for record in self: + record.departure_today = ( + True if record.checkout == fields.Date.today() else False + ) + + def _search_departure_today(self, operator, value): + if operator not in ("=", "!="): + raise UserError(_("Invalid domain operator %s", operator)) + + if value not in (False, True): + raise UserError(_("Invalid domain right operand %s", value)) + + searching_for_true = (operator == "=" and value) or ( + operator == "!=" and not value + ) + today = fields.Date.context_today(self) + + return [("checkout", searching_for_true, today)] + # REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿? @api.depends("preferred_room_id") def _compute_adults(self): diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py index 89e63430b..e8346fdd5 100644 --- a/pms/tests/test_pms_checkin_partner.py +++ b/pms/tests/test_pms_checkin_partner.py @@ -267,3 +267,36 @@ class TestPmsCheckinPartner(TestHotel): int(2 * 100 / 3), "Fail the checkins ratio on reservation", ) + + def test_complete_checkin_data(self): + + # ARRANGE + self.arrange_folio_reservations() + + # ACT + self.checkin1 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host1.id, + "reservation_id": self.reservation_1.id, + } + ) + self.checkin2 = self.env["pms.checkin.partner"].create( + { + "partner_id": self.host2.id, + "reservation_id": self.reservation_1.id, + } + ) + pending_checkin_data = self.reservation_1.pending_checkin_data + ratio_checkin_data = self.reservation_1.ratio_checkin_data + + # ASSERT + self.assertEqual( + pending_checkin_data, + 1, + "Fail the count pending checkin data on reservation", + ) + self.assertEqual( + ratio_checkin_data, + int(2 * 100 / 3), + "Fail the checkins data ratio on reservation", + ) diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 79a94840a..18a8cb251 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -14,6 +14,7 @@
+ - + From 1e711e842b4a14be15bd9b147b3969f2d8ecf555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Sun, 15 Nov 2020 16:31:44 +0100 Subject: [PATCH 10/37] [WIP] Checkin Flow Assigned-Unassigned --- pms/models/pms_checkin_partner.py | 57 ++++++++++++++------ pms/models/pms_reservation.py | 69 ++++++++++++++++++++----- pms/models/pms_reservation_line.py | 4 -- pms/views/pms_checkin_partner_views.xml | 9 +++- pms/views/pms_reservation_views.xml | 4 +- 5 files changed, 106 insertions(+), 37 deletions(-) diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index e404cdcac..7a762d746 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -16,9 +16,14 @@ class PmsCheckinPartner(models.Model): return self.env.user.pms_property_id # Fields declaration + identifier = fields.Char( + "Identifier", + compute="_compute_identifier", + readonly=False, + store=True, + ) partner_id = fields.Many2one( "res.partner", - required=True, domain="[('is_company', '=', False)]", ) reservation_id = fields.Many2one("pms.reservation") @@ -26,49 +31,67 @@ class PmsCheckinPartner(models.Model): "pms.folio", compute="_compute_folio_id", store=True, - readonly=False, ) pms_property_id = fields.Many2one( "pms.property", default=_get_default_pms_property, required=True ) - name = fields.Char("E-mail", related="partner_id.name") + name = fields.Char("Name", related="partner_id.name") email = fields.Char("E-mail", related="partner_id.email") mobile = fields.Char("Mobile", related="partner_id.mobile") arrival = fields.Datetime("Enter") departure = fields.Datetime("Exit") - completed_data = fields.Boolean(compute="_compute_completed_data", store=True) state = fields.Selection( selection=[ - ("draft", "Pending arrival"), + ("draft", "Unkown Guest"), + ("precheckin", "Pending arrival"), ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), ], string="State", + compute="_compute_state", + store=True, readonly=True, - default=lambda *a: "draft", ) # Compute + @api.depends("reservation_id", "folio_id", "reservation_id.room_id") + def _compute_identifier(self): + for record in self: + # TODO: Identifier + if record.reservation_id.filtered("room_id"): + checkins = record.reservation_id.checkin_partner_ids + record.identifier = ( + record.reservation_id.room_id.name + "-" + str(len(checkins) - 1) + ) + elif record.folio_id: + record.identifier = record.folio_id.name + "-" + str(len(checkins) - 1) + else: + record.identifier = False @api.depends("reservation_id", "reservation_id.folio_id") def _compute_folio_id(self): - for record in self: + for record in self.filtered("reservation_id"): record.folio_id = record.reservation_id.folio_id - @api.depends(lambda self: self._checkin_mandatory_fields(), "state") - def _compute_completed_data(self): - self.completed_data = False + @api.depends(lambda self: self._checkin_mandatory_fields(), "reservation_id.state") + def _compute_state(self): for record in self: - if any( - not getattr(self, field) for field in record._checkin_mandatory_fields() - ): - record.completed_data = False - break - record.completed_data = True + if record.reservation_id.state == "cancelled": + record.state = "cancelled" + elif record.state in ("draft", "cancelled"): + if any( + not getattr(self, field) + for field in record._checkin_mandatory_fields() + ): + record.state = "draft" + else: + record.state = "precheckin" + elif not record.state: + record.state = "draft" def _checkin_mandatory_fields(self): - return ["name"] + return ["name", "email"] # Constraints and onchanges diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index a580c369f..a410d8bc3 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -186,7 +186,14 @@ class PmsReservation(models.Model): readonly=False, ) # TODO: Warning Mens to update pricelist - checkin_partner_ids = fields.One2many("pms.checkin.partner", "reservation_id") + checkin_partner_ids = fields.One2many( + "pms.checkin.partner", + "reservation_id", + compute="_compute_checkin_partner_ids", + store=True, + readonly=False, + ondelete="restrict", + ) count_pending_arrival = fields.Integer( "Pending Arrival", compute="_compute_count_pending_arrival", @@ -558,12 +565,48 @@ class PmsReservation(models.Model): # TODO: Warning change de pricelist? reservation.pricelist_id = pricelist_id + @api.depends("adults") + def _compute_checkin_partner_ids(self): + for reservation in self: + assigned_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state in ("precheckin", "onboard", "done") + ) + unassigned_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state not in ("draft") + ) + leftover_unassigneds_count = ( + len(assigned_checkins) + len(unassigned_checkins) - reservation.adults + ) + if len(assigned_checkins) > reservation.adults: + raise UserError( + _("Remove some of the leftover assigned checkins first") + ) + elif leftover_unassigneds_count > 0: + for i in range(0, len(unassigned_checkins)): + unassigned_checkins[i].unlink() + elif reservation.adults > len(reservation.checkin_partner_ids): + checkins_lst = [] + count_new_checkins = reservation.adults - len( + reservation.checkin_partner_ids + ) + for _i in range(0, count_new_checkins): + checkins_lst.append( + ( + 0, + False, + { + "reservation_id": reservation.id, + }, + ) + ) + reservation.checkin_partner_ids = checkins_lst + @api.depends("checkin_partner_ids", "checkin_partner_ids.state") def _compute_count_pending_arrival(self): for reservation in self: - reservation.count_pending_arrival = reservation.adults - len( + reservation.count_pending_arrival = len( reservation.checkin_partner_ids.filtered( - lambda c: c.state in ("onboard", "done") + lambda c: c.state in ("draft", "precheckin") ) ) @@ -577,13 +620,11 @@ class PmsReservation(models.Model): / reservation.adults ) - @api.depends("checkin_partner_ids", "checkin_partner_ids.completed_data") + @api.depends("checkin_partner_ids", "checkin_partner_ids.state") def _compute_pending_checkin_data(self): for reservation in self: - reservation.pending_checkin_data = reservation.adults - len( - reservation.checkin_partner_ids.filtered( - lambda c: c.state != "cancelled" and c.completed_data - ) + reservation.pending_checkin_data = len( + reservation.checkin_partner_ids.filtered(lambda c: c.state in ("draft")) ) @api.depends("pending_checkin_data") @@ -810,11 +851,13 @@ class PmsReservation(models.Model): ) ) - @api.constrains("checkin_partner_ids", "adults") - def _max_checkin_partner_ids(self): - for record in self: - if len(record.checkin_partner_ids) > record.adults + record.children: - raise models.ValidationError(_("The room already is completed")) + # @api.constrains("checkin_partner_ids", "adults") + # def _max_checkin_partner_ids(self): + # for record in self: + # if len(record.checkin_partner_ids) != record.adults: + # raise models.ValidationError( + # _("Reservation Adults and Checkins does not match") + # ) # @api.constrains("reservation_type", "partner_id") # def _check_partner_reservation(self): diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index e61cab552..e9252e7db 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -250,10 +250,6 @@ class PmsReservationLine(models.Model): line.reservation_id.tax_ids, line.reservation_id.company_id, ) -<<<<<<< HEAD - # _logger.info(line.price) -======= ->>>>>>> [DEL] delete auto_booking field # TODO: Out of service 0 amount else: line.price = line._origin.price diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index 0e4c28b5d..d8b4ea75c 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -32,6 +32,9 @@ + + + @@ -57,8 +60,9 @@ icon="fa-2x fa-check-circle" name="action_on_board" help="Get in" - attrs="{'invisible': [('state','!=','draft')]}" + attrs="{'invisible': [('state','!=','preconfirm')]}" /> + @@ -86,9 +90,10 @@ class="oe_stat_button" icon="fa fa-2x fa-check-circle" name="action_on_board" - attrs="{'invisible':[('state','not in', ['draft'])]}" + attrs="{'invisible':[('state','not in', ['preconfirm'])]}" help="Get in" /> + From 8fa5c5660bd8e812f6838014e6eac5e957dfeb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Mon, 16 Nov 2020 16:49:45 +0100 Subject: [PATCH 11/37] [ADD] Auto create checkin records from reservation adults --- pms/models/pms_checkin_partner.py | 30 +++++++++++++---- pms/models/pms_reservation.py | 42 ++++++++++++++---------- pms/models/pms_reservation_line.py | 2 +- pms/security/ir.model.access.csv | 4 +-- pms/tests/test_pms_checkin_partner.py | 46 ++++++++++++++++++++++++++- pms/views/pms_reservation_views.xml | 1 + 6 files changed, 98 insertions(+), 27 deletions(-) diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 7a762d746..4e411646b 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -55,14 +55,16 @@ class PmsCheckinPartner(models.Model): ) # Compute - @api.depends("reservation_id", "folio_id", "reservation_id.room_id") + @api.depends("reservation_id", "folio_id", "reservation_id.preferred_room_id") def _compute_identifier(self): for record in self: # TODO: Identifier - if record.reservation_id.filtered("room_id"): + if record.reservation_id.filtered("preferred_room_id"): checkins = record.reservation_id.checkin_partner_ids record.identifier = ( - record.reservation_id.room_id.name + "-" + str(len(checkins) - 1) + record.reservation_id.preferred_room_id.name + + "-" + + str(len(checkins) - 1) ) elif record.folio_id: record.identifier = record.folio_id.name + "-" + str(len(checkins) - 1) @@ -77,18 +79,18 @@ class PmsCheckinPartner(models.Model): @api.depends(lambda self: self._checkin_mandatory_fields(), "reservation_id.state") def _compute_state(self): for record in self: + if not record.state: + record.state = "draft" if record.reservation_id.state == "cancelled": record.state = "cancelled" elif record.state in ("draft", "cancelled"): if any( - not getattr(self, field) + not getattr(record, field) for field in record._checkin_mandatory_fields() ): record.state = "draft" else: record.state = "precheckin" - elif not record.state: - record.state = "draft" def _checkin_mandatory_fields(self): return ["name", "email"] @@ -117,6 +119,22 @@ class PmsCheckinPartner(models.Model): _("This guest is already registered in the room") ) + # CRUD + @api.model + def create(self, vals): + # The checkin records are created automatically from adult depends + # if you try to create one manually, we update one unassigned checkin + if not self._context.get("auto_create_checkin"): + reservation_id = vals.get("reservation_id") + if reservation_id: + reservation = self.env["pms.reservation"].browse(reservation_id) + draft_checkins = reservation.checkin_partner_ids.filtered( + lambda c: c.state in ("draft") + ) + if len(draft_checkins) > 0 and vals.get("partner_id"): + draft_checkins[0].sudo().unlink() + return super(PmsCheckinPartner, self).create(vals) + # Action methods def action_on_board(self): diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index a410d8bc3..c5ec14cea 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -572,7 +572,7 @@ class PmsReservation(models.Model): lambda c: c.state in ("precheckin", "onboard", "done") ) unassigned_checkins = reservation.checkin_partner_ids.filtered( - lambda c: c.state not in ("draft") + lambda c: c.state in ("draft") ) leftover_unassigneds_count = ( len(assigned_checkins) + len(unassigned_checkins) - reservation.adults @@ -582,8 +582,8 @@ class PmsReservation(models.Model): _("Remove some of the leftover assigned checkins first") ) elif leftover_unassigneds_count > 0: - for i in range(0, len(unassigned_checkins)): - unassigned_checkins[i].unlink() + for i in range(0, leftover_unassigneds_count): + unassigned_checkins[i].sudo().unlink() elif reservation.adults > len(reservation.checkin_partner_ids): checkins_lst = [] count_new_checkins = reservation.adults - len( @@ -599,7 +599,9 @@ class PmsReservation(models.Model): }, ) ) - reservation.checkin_partner_ids = checkins_lst + reservation.with_context( + {"auto_create_checkin": True} + ).checkin_partner_ids = checkins_lst @api.depends("checkin_partner_ids", "checkin_partner_ids.state") def _compute_count_pending_arrival(self): @@ -851,13 +853,25 @@ class PmsReservation(models.Model): ) ) - # @api.constrains("checkin_partner_ids", "adults") - # def _max_checkin_partner_ids(self): - # for record in self: - # if len(record.checkin_partner_ids) != record.adults: - # raise models.ValidationError( - # _("Reservation Adults and Checkins does not match") - # ) + @api.constrains("checkin_partner_ids", "adults") + def _max_checkin_partner_ids(self): + for record in self: + if len(record.checkin_partner_ids) > record.adults: + raise models.ValidationError(_("The room already is completed")) + + @api.constrains("adults") + def _check_adults(self): + for record in self: + extra_bed = record.service_ids.filtered( + lambda r: r.product_id.is_extra_bed is True + ) + for room in record.reservation_line_ids.room_id: + if record.adults + record.children_occupying > room.get_capacity( + len(extra_bed) + ): + raise ValidationError( + _("Persons can't be higher than room capacity") + ) # @api.constrains("reservation_type", "partner_id") # def _check_partner_reservation(self): @@ -880,12 +894,6 @@ class PmsReservation(models.Model): # _("Only the out reservations can has a clousure reason") # ) - # @api.onchange("checkin_partner_ids") - # def onchange_checkin_partner_ids(self): - # for record in self: - # if len(record.checkin_partner_ids) > record.adults + record.children: - # raise models.ValidationError(_("The room already is completed")) - # self._compute_tax_ids() TODO: refact # Action methods diff --git a/pms/models/pms_reservation_line.py b/pms/models/pms_reservation_line.py index e9252e7db..3415178ff 100644 --- a/pms/models/pms_reservation_line.py +++ b/pms/models/pms_reservation_line.py @@ -394,7 +394,7 @@ class PmsReservationLine(models.Model): # negative discounts (= surcharge) are included in the display price return max(base_price, final_price) - @api.constrains("reservation_id.adults", "room_id") + @api.constrains("room_id") def _check_adults(self): for record in self.filtered("room_id"): extra_bed = record.reservation_id.service_ids.filtered( diff --git a/pms/security/ir.model.access.csv b/pms/security/ir.model.access.csv index 02f4a382b..aeb970f96 100644 --- a/pms/security/ir.model.access.csv +++ b/pms/security/ir.model.access.csv @@ -8,7 +8,7 @@ user_access_pms_reservation_line,user_access_pms_reservation_line,model_pms_rese user_access_room_closure_reason,user_access_room_closure_reason,model_room_closure_reason,pms.group_pms_user,1,0,0,0 user_access_pms_service_line,user_access_pms_service_line,model_pms_service_line,pms.group_pms_user,1,1,1,1 user_access_pms_board_service,user_access_pms_board_service,model_pms_board_service,pms.group_pms_user,1,0,0,0 -user_access_pms_checkin_partner,user_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_user,1,1,1,1 +user_access_pms_checkin_partner,user_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_user,1,1,1,0 user_access_pms_room_type_class,user_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_user,1,0,0,0 user_access_pms_room,user_access_pms_room,model_pms_room,pms.group_pms_user,1,0,0,0 user_access_shared_pms_room,user_access_pms_shared_room,model_pms_shared_room,pms.group_pms_user,1,0,0,0 @@ -33,7 +33,7 @@ manager_access_pms_reservation_line,manager_access_pms_reservation_line,model_pm manager_access_room_closure_reason,manager_access_room_closure_reason,model_room_closure_reason,pms.group_pms_manager,1,1,1,1 manager_access_pms_service_line,manager_access_pms_service_line,model_pms_service_line,pms.group_pms_manager,1,1,1,1 manager_access_pms_board_service,manager_access_pms_board_service,model_pms_board_service,pms.group_pms_manager,1,1,1,1 -manager_access_pms_checkin_partner,manager_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_manager,1,1,1,1 +manager_access_pms_checkin_partner,manager_access_pms_checkin_partner,model_pms_checkin_partner,pms.group_pms_manager,1,1,1,0 manager_access_pms_room_type_class,manager_access_pms_room_type_class,model_pms_room_type_class,pms.group_pms_manager,1,1,1,1 manager_access_pms_room,manager_access_pms_room,model_pms_room,pms.group_pms_manager,1,1,1,1 manager_access_pms_shared_room,manager_access_pms_shared_room,model_pms_shared_room,pms.group_pms_manager,1,1,1,1 diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py index e8346fdd5..7b53f97c2 100644 --- a/pms/tests/test_pms_checkin_partner.py +++ b/pms/tests/test_pms_checkin_partner.py @@ -41,6 +41,51 @@ class TestPmsCheckinPartner(TestHotel): } ) + def test_auto_create_checkins(self): + + # ACTION + self.arrange_single_checkin() + checkins_count = len(self.reservation_1.checkin_partner_ids) + + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not created successful", + ) + + def test_auto_unlink_checkins(self): + + # ARRANGE + self.arrange_single_checkin() + + # ACTION + host2 = self.env["res.partner"].create( + { + "name": "Carlos", + "phone": "654667733", + "email": "carlos@example.com", + } + ) + self.reservation_1.checkin_partner_ids = [ + ( + 0, + False, + { + "partner_id": host2.id, + }, + ) + ] + + checkins_count = len(self.reservation_1.checkin_partner_ids) + + # ASSERT + self.assertEqual( + checkins_count, + 3, + "the automatic partner checkin was not updated successful", + ) + def test_onboard_checkin(self): # ARRANGE @@ -288,7 +333,6 @@ class TestPmsCheckinPartner(TestHotel): ) pending_checkin_data = self.reservation_1.pending_checkin_data ratio_checkin_data = self.reservation_1.ratio_checkin_data - # ASSERT self.assertEqual( pending_checkin_data, diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index f8a43f589..f7f7236f4 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -495,6 +495,7 @@ --> - - + + 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: Thu, 19 Nov 2020 09:36:40 +0100 Subject: [PATCH 13/37] [IMP] Check persons Onboard reservation --- pms/demo/pms_reservation.xml | 2 +- pms/models/pms_checkin_partner.py | 2 +- pms/models/pms_reservation.py | 30 ++++++++++++++++++++------- pms/models/pms_service.py | 2 +- pms/tests/test_pms_checkin_partner.py | 12 +++-------- 5 files changed, 29 insertions(+), 19 deletions(-) diff --git a/pms/demo/pms_reservation.xml b/pms/demo/pms_reservation.xml index 8f201a7d7..57c858299 100644 --- a/pms/demo/pms_reservation.xml +++ b/pms/demo/pms_reservation.xml @@ -244,7 +244,7 @@ 2 1 - onboard + confirm diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 4e411646b..def65db15 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -129,7 +129,7 @@ class PmsCheckinPartner(models.Model): if reservation_id: reservation = self.env["pms.reservation"].browse(reservation_id) draft_checkins = reservation.checkin_partner_ids.filtered( - lambda c: c.state in ("draft") + lambda c: c.state == "draft" ) if len(draft_checkins) > 0 and vals.get("partner_id"): draft_checkins[0].sudo().unlink() diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index c5ec14cea..c07fae506 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -572,7 +572,7 @@ class PmsReservation(models.Model): lambda c: c.state in ("precheckin", "onboard", "done") ) unassigned_checkins = reservation.checkin_partner_ids.filtered( - lambda c: c.state in ("draft") + lambda c: c.state == "draft" ) leftover_unassigneds_count = ( len(assigned_checkins) + len(unassigned_checkins) - reservation.adults @@ -626,7 +626,7 @@ class PmsReservation(models.Model): def _compute_pending_checkin_data(self): for reservation in self: reservation.pending_checkin_data = len( - reservation.checkin_partner_ids.filtered(lambda c: c.state in ("draft")) + reservation.checkin_partner_ids.filtered(lambda c: c.state == "draft") ) @api.depends("pending_checkin_data") @@ -718,7 +718,7 @@ class PmsReservation(models.Model): "Product Unit of Measure" ) for line in self: - if line.state in ("draft"): + if line.state == "draft": line.invoice_status = "no" elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = "to invoice" @@ -857,7 +857,9 @@ class PmsReservation(models.Model): def _max_checkin_partner_ids(self): for record in self: if len(record.checkin_partner_ids) > record.adults: - raise models.ValidationError(_("The room already is completed")) + raise models.ValidationError( + _("The room already is completed (%s)", record.name) + ) @api.constrains("adults") def _check_adults(self): @@ -870,9 +872,23 @@ class PmsReservation(models.Model): len(extra_bed) ): raise ValidationError( - _("Persons can't be higher than room capacity") + _( + "Persons can't be higher than room capacity (%s)", + record.name, + ) ) + @api.constrains("state") + def _check_onboard_reservation(self): + for record in self: + if ( + not record.checkin_partner_ids.filtered(lambda c: c.state == "onboard") + and record.state == "onboard" + ): + raise ValidationError( + _("No person from reserve %s has arrived", record.name) + ) + # @api.constrains("reservation_type", "partner_id") # def _check_partner_reservation(self): # for reservation in self: @@ -1050,7 +1066,7 @@ class PmsReservation(models.Model): def autocheckout(self): reservations = self.env["pms.reservation"].search( [ - ("state", "not in", ("done", "cancelled")), + ("state", "not in", ["done", "cancelled"]), ("checkout", "<", fields.Date.today()), ] ) @@ -1089,7 +1105,7 @@ class PmsReservation(models.Model): def confirm(self): for record in self: vals = {} - if record.checkin_partner_ids: + if record.checkin_partner_ids.filtered(lambda c: c.state == "onboard"): vals.update({"state": "onboard"}) else: vals.update({"state": "confirm"}) diff --git a/pms/models/pms_service.py b/pms/models/pms_service.py index 5a36ecc30..6ff3facee 100644 --- a/pms/models/pms_service.py +++ b/pms/models/pms_service.py @@ -459,7 +459,7 @@ class PmsService(models.Model): ) for line in self: state = line.folio_id.state or "draft" - if state in ("draft"): + if state == "draft": line.invoice_status = "no" elif not float_is_zero(line.qty_to_invoice, precision_digits=precision): line.invoice_status = "to invoice" diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py index 7b53f97c2..9485e6dd1 100644 --- a/pms/tests/test_pms_checkin_partner.py +++ b/pms/tests/test_pms_checkin_partner.py @@ -91,15 +91,9 @@ class TestPmsCheckinPartner(TestHotel): # ARRANGE self.arrange_single_checkin() - # ACT - self.checkin1.action_on_board() - - # ASSERT - self.assertEqual( - self.checkin1.state, - "onboard", - "the partner checkin was not successful", - ) + # ACT & ASSERT + with self.assertRaises(ValidationError), self.cr.savepoint(): + self.reservation_1.state = "onboard" def test_onboard_reservation(self): From 95d2f9fc8ff22e34bfdba3bd960debb4c02396eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Lodeiros?= Date: Fri, 20 Nov 2020 10:10:02 +0100 Subject: [PATCH 14/37] [IMP] Check persons Onboard reservation --- pms/demo/pms_reservation.xml | 82 +++++++++++- pms/models/pms_checkin_partner.py | 8 +- pms/models/pms_folio.py | 21 ++- pms/static/description/avatar.png | Bin 0 -> 14873 bytes pms/views/pms_checkin_partner_views.xml | 162 +++++++++++++++++++++++- pms/views/pms_folio_views.xml | 24 ---- pms/views/pms_reservation_views.xml | 6 +- 7 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 pms/static/description/avatar.png diff --git a/pms/demo/pms_reservation.xml b/pms/demo/pms_reservation.xml index 57c858299..fb6c62c86 100644 --- a/pms/demo/pms_reservation.xml +++ b/pms/demo/pms_reservation.xml @@ -24,6 +24,20 @@ 2 + onboard + @@ -58,6 +72,15 @@ 1 + @@ -67,6 +90,14 @@ 1 + @@ -129,6 +160,13 @@ 2 + @@ -164,6 +202,13 @@ 2 + @@ -217,6 +262,14 @@ 3 + @@ -225,6 +278,12 @@ 1 + 2 @@ -235,6 +294,14 @@ 3 + @@ -244,7 +311,20 @@ 2 1 - confirm + onboard + diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index def65db15..f67124df4 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -38,6 +38,11 @@ class PmsCheckinPartner(models.Model): name = fields.Char("Name", related="partner_id.name") email = fields.Char("E-mail", related="partner_id.email") mobile = fields.Char("Mobile", related="partner_id.mobile") + image_128 = fields.Image(related="partner_id.image_128") + segmentation_ids = fields.Many2many( + related="reservation_id.segmentation_ids", + readonly=True, + ) arrival = fields.Datetime("Enter") departure = fields.Datetime("Exit") state = fields.Selection( @@ -59,6 +64,7 @@ class PmsCheckinPartner(models.Model): def _compute_identifier(self): for record in self: # TODO: Identifier + checkins = [] if record.reservation_id.filtered("preferred_room_id"): checkins = record.reservation_id.checkin_partner_ids record.identifier = ( @@ -93,7 +99,7 @@ class PmsCheckinPartner(models.Model): record.state = "precheckin" def _checkin_mandatory_fields(self): - return ["name", "email"] + return ["name"] # Constraints and onchanges diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index 94149e51e..d63b01c46 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -244,14 +244,11 @@ class PmsFolio(models.Model): tracking=True, ) # Checkin Fields----------------------------------------------------- - booking_pending = fields.Integer( - "Booking pending", compute="_compute_checkin_partner_count" + reservation_pending_arrival_ids = fields.One2many( + string="Pending Arrival Rooms", compute="_compute_reservations_pending_arrival" ) - checkin_partner_count = fields.Integer( - "Checkin counter", compute="_compute_checkin_partner_count" - ) - checkin_partner_pending_count = fields.Integer( - "Checkin Pending", compute="_compute_checkin_partner_count" + reservations_pending_count = fields.Integer( + compute="_compute_reservations_pending_arrival" ) # Invoice Fields----------------------------------------------------- invoice_status = fields.Selection( @@ -444,6 +441,16 @@ class PmsFolio(models.Model): } ) + @api.depends("reservation_ids", "reservation_ids.state") + def _compute_reservations_pending_arrival(self): + for record in self: + record.reservation_pending_arrival_ids = record.reservation_ids.filtered( + lambda r: r.state in ("draft", "precheckin") + ) + record.reservations_pending_count = len( + record.reservations_pending_arrival_ids + ) + # TODO: Add return_ids to depends @api.depends("amount_total", "payment_ids", "reservation_type", "state") def _compute_amount(self): diff --git a/pms/static/description/avatar.png b/pms/static/description/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..74861de3f0fc8a476ec3d295c7d842e32396cf34 GIT binary patch literal 14873 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 LNkvXXu0mjfq^Y + + pms.checkin.partner.kanban + pms.checkin.partner + + + + + + + + + + + + + + + + + +
+
+ Contact image + + + Draft + Cancelled + +
+
+
+
+ +
+ + + +
+
+ + + +
+
+ + + +
+ + +
+ + + + + + +
+
+ +
    +
  • + +
  • +
  • + +
  • +
+ +
+
+
+
+
+
+
pms.checkin.partner.search pms.checkin.partner diff --git a/pms/views/pms_folio_views.xml b/pms/views/pms_folio_views.xml index c4b310a55..ca2e6fb57 100644 --- a/pms/views/pms_folio_views.xml +++ b/pms/views/pms_folio_views.xml @@ -278,9 +278,6 @@ - - - @@ -295,27 +292,6 @@ -
    -
  • - -
  • - - - - - - - - -
diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index f7f7236f4..8c3399719 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -495,12 +495,10 @@
From 2e8e82401c17c45e6ee92dcb803d17c0fbccf19c Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 23 Nov 2020 14:07:24 +0100 Subject: [PATCH 15/37] [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: Thu, 26 Nov 2020 12:19:35 +0100 Subject: [PATCH 16/37] [ADD] Cron no_show and no_checkout --- pms/data/cron_jobs.xml | 32 +++++++ pms/models/pms_checkin_partner.py | 24 +++++- pms/models/pms_folio.py | 4 +- pms/models/pms_reservation.py | 70 +++++++++++++--- pms/tests/test_pms_checkin_partner.py | 45 ++++++++++ pms/views/pms_checkin_partner_views.xml | 12 ++- pms/views/pms_reservation_views.xml | 106 +----------------------- 7 files changed, 170 insertions(+), 123 deletions(-) diff --git a/pms/data/cron_jobs.xml b/pms/data/cron_jobs.xml index 7adf1a6a5..06d4bce45 100644 --- a/pms/data/cron_jobs.xml +++ b/pms/data/cron_jobs.xml @@ -1,6 +1,38 @@ + + + Automatic No Show Reservation + 1 + + days + -1 + + code + + + model.auto_no_show() + + + + Automatic No Checkout Reservations + 1 + + days + -1 + + code + + + model.auto_no_checkout() + Automatic Checkout on past reservations diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index f67124df4..e4c1d5649 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -82,7 +82,7 @@ class PmsCheckinPartner(models.Model): for record in self.filtered("reservation_id"): record.folio_id = record.reservation_id.folio_id - @api.depends(lambda self: self._checkin_mandatory_fields(), "reservation_id.state") + @api.depends(lambda self: self._checkin_mandatory_fields(depends=True)) def _compute_state(self): for record in self: if not record.state: @@ -98,7 +98,11 @@ class PmsCheckinPartner(models.Model): else: record.state = "precheckin" - def _checkin_mandatory_fields(self): + @api.model + def _checkin_mandatory_fields(self, depends=False): + # api.depends need "reservation_id.state" in de lambda function + if depends: + return ["reservation_id.state", "name"] return ["name"] # Constraints and onchanges @@ -156,6 +160,22 @@ class PmsCheckinPartner(models.Model): record.update(vals) if record.reservation_id.state == "confirm": record.reservation_id.state = "onboard" + if self._context.get("popup"): + self.ensure_one() + kanban_id = self.env.ref("pms.pms_checkin_partner_kanban_view").id + return { + "name": _("Register Checkins"), + "views": [[kanban_id, "tree"]], + "res_model": "pms.checkin.partner", + "type": "ir.actions.act_window", + "context": { + "create": False, + "edit": True, + "popup": True, + }, + "domain": [("reservation_id", "=", self.reservation_id.id)], + "target": "new", + } def action_done(self): for record in self: diff --git a/pms/models/pms_folio.py b/pms/models/pms_folio.py index d63b01c46..d48de2ba4 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -245,7 +245,9 @@ class PmsFolio(models.Model): ) # Checkin Fields----------------------------------------------------- reservation_pending_arrival_ids = fields.One2many( - string="Pending Arrival Rooms", compute="_compute_reservations_pending_arrival" + comodel_name="pms.checkin.partner", + string="Pending Arrival Rooms", + compute="_compute_reservations_pending_arrival", ) reservations_pending_count = fields.Integer( compute="_compute_reservations_pending_arrival" diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index c07fae506..830993f5b 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -2,7 +2,7 @@ # Copyright 2017 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from datetime import timedelta +from datetime import datetime, time, timedelta from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError @@ -274,6 +274,8 @@ class PmsReservation(models.Model): ("onboard", "On Board"), ("done", "Out"), ("cancelled", "Cancelled"), + ("no_show", "No Show"), + ("no_checkout", "No Checkout"), ], string="Status", default=lambda *a: "draft", @@ -653,12 +655,9 @@ class PmsReservation(models.Model): if value not in (False, True): raise UserError(_("Invalid domain right operand %s", value)) - searching_for_true = (operator == "=" and value) or ( - operator == "!=" and not value - ) today = fields.Date.context_today(self) - return [("checkin", searching_for_true, today)] + return [("checkin", operator, today)] def _compute_departure_today(self): for record in self: @@ -1224,13 +1223,60 @@ class PmsReservation(models.Model): def action_checks(self): self.ensure_one() - action = self.env.ref("pms.open_pms_reservation_form_tree_all").read()[0] - action["views"] = [ - (self.env.ref("pms.pms_reservation_checkin_view_form").id, "form") - ] - action["res_id"] = self.id - action["target"] = "new" - return action + tree_id = self.env.ref("pms.pms_checkin_partner_reservation_view_tree").id + return { + "name": _("Register Partners"), + "views": [[tree_id, "tree"]], + "res_model": "pms.checkin.partner", + "type": "ir.actions.act_window", + "context": { + "create": False, + "edit": True, + "popup": True, + }, + "domain": [("reservation_id", "=", self.id), ("state", "=", "draft")], + "target": "new", + } + + def action_onboard(self): + self.ensure_one() + kanban_id = self.env.ref("pms.pms_checkin_partner_kanban_view").id + return { + "name": _("Register Checkins"), + "views": [[kanban_id, "kanban"]], + "res_model": "pms.checkin.partner", + "type": "ir.actions.act_window", + "context": { + "create": False, + "edit": True, + "popup": True, + }, + "domain": [("reservation_id", "=", self.id), ("state", "=", "precheckin")], + "target": "new", + } + + @api.model + def auto_no_show(self): + no_show_reservations = self.env["pms.reservation"].search( + [ + ("state", "in", ("draft", "confirm")), + ("checkin", "<", fields.Date.today()), + ] + ) + no_show_reservations.state = "no_show" + + @api.model + def auto_no_checkout(self): + reservations = self.env["pms.reservation"].search( + [("state", "in", ("onboard",))] + ) + for reservation in reservations: + checkout_hour = int(reservation.departure_hour[0:2]) + checkout_minut = int(reservation.departure_hour[3:5]) + checkout_time = time(checkout_hour, checkout_minut, 00) + checkout_datetime = datetime.combine(reservation.checkout, checkout_time) + if checkout_datetime <= fields.Datetime.now(): + reservation.state = "no_checkout" def unify(self): # TODO diff --git a/pms/tests/test_pms_checkin_partner.py b/pms/tests/test_pms_checkin_partner.py index 9485e6dd1..2cf23f05d 100644 --- a/pms/tests/test_pms_checkin_partner.py +++ b/pms/tests/test_pms_checkin_partner.py @@ -338,3 +338,48 @@ class TestPmsCheckinPartner(TestHotel): int(2 * 100 / 3), "Fail the checkins data ratio on reservation", ) + + def test_auto_no_show(self): + + # ARRANGE + self.arrange_folio_reservations() + PmsReservation = self.env["pms.reservation"] + + # ACTION + freezer = freeze_time("2012-01-15 10:00:00") + freezer.start() + PmsReservation.auto_no_show() + + no_show_reservations = PmsReservation.search([("state", "=", "no_show")]) + + # ASSERT + self.assertEqual( + len(no_show_reservations), + 3, + "Reservations not set like No Show", + ) + freezer.stop() + + def test_auto_no_checkout(self): + + # ARRANGE + self.arrange_single_checkin() + PmsReservation = self.env["pms.reservation"] + self.checkin1.action_on_board() + + # ACTION + freezer = freeze_time("2012-01-17 12:00:00") + freezer.start() + PmsReservation.auto_no_checkout() + + no_checkout_reservations = PmsReservation.search( + [("state", "=", "no_checkout")] + ) + freezer.stop() + + # ASSERT + self.assertEqual( + len(no_checkout_reservations), + 1, + "Reservations not set like No checkout", + ) diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index 8fb5af300..b9fbf1eb8 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -49,7 +49,6 @@ @@ -71,7 +70,7 @@ - + @@ -114,7 +113,12 @@ pms.checkin.partner.kanban pms.checkin.partner - + diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 8c3399719..3ce4b6eeb 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -120,7 +120,7 @@ - + - + + + > + + + + + + + + From 5a5b2a8b5587c9ed7f45a2fe2b5f164d242d9b45 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Nov 2020 13:32:26 +0100 Subject: [PATCH 24/37] [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 25/37] [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 @@ - + + + Date: Mon, 30 Nov 2020 19:48:27 +0100 Subject: [PATCH 26/37] [IMP] Checkin Flouw status --- pms/models/pms_checkin_partner.py | 4 +- pms/models/pms_reservation.py | 74 +++++++++++++++++++------ pms/views/pms_checkin_partner_views.xml | 6 -- pms/views/pms_reservation_views.xml | 39 +++++++------ 4 files changed, 79 insertions(+), 44 deletions(-) diff --git a/pms/models/pms_checkin_partner.py b/pms/models/pms_checkin_partner.py index 37092785b..c364c8e2e 100644 --- a/pms/models/pms_checkin_partner.py +++ b/pms/models/pms_checkin_partner.py @@ -110,7 +110,7 @@ class PmsCheckinPartner(models.Model): @api.constrains("departure", "arrival") def _check_departure(self): for record in self: - if record.departure and record.arrival < record.departure: + if record.departure and record.arrival > record.departure: raise ValidationError( _("Departure date (%s) is prior to arrival on %s") % (record.departure, record.arrival) @@ -158,7 +158,7 @@ class PmsCheckinPartner(models.Model): "arrival": fields.Datetime.now(), } record.update(vals) - if record.reservation_id.state == "confirm": + if record.reservation_id.left_for_checkin: record.reservation_id.state = "onboard" def action_done(self): diff --git a/pms/models/pms_reservation.py b/pms/models/pms_reservation.py index 40c261508..52454cd31 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -205,20 +205,24 @@ class PmsReservation(models.Model): compute="_compute_checkins_ratio", ) pending_checkin_data = fields.Integer( - "Pending Checkin Data", + "Checkin Data", compute="_compute_pending_checkin_data", store=True, ) - arrival_today = fields.Boolean( - compute="_compute_arrival_today", search="_search_arrival_today" + ratio_checkin_data = fields.Integer( + string="Pending Checkin Data", + compute="_compute_ratio_checkin_data", + ) + ready_for_checkin = fields.Boolean(compute="_compute_ready_for_checkin") + left_for_checkin = fields.Boolean( + compute="_compute_left_for_checkin", search="_search_left_for_checkin" + ) + checkin_today = fields.Boolean( + compute="_compute_checkin_today", search="_search_checkin_today" ) departure_today = fields.Boolean( compute="_compute_departure_today", search="_search_departure_today" ) - ratio_checkin_data = fields.Integer( - string="Pending Checkin Data Ratio", - compute="_compute_ratio_checkin_data", - ) segmentation_ids = fields.Many2many( "res.partner.category", string="Segmentation", @@ -676,14 +680,56 @@ class PmsReservation(models.Model): / reservation.adults ) - def _compute_arrival_today(self): + def _compute_left_for_checkin(self): + # Reservations still pending entry today for record in self: - record.arrival_today = ( + record.left_for_checkin = ( + True + if ( + record.state in ["draft", "confirm", "no_show"] + and record.checkin <= fields.Date.today() + ) + else False + ) + + def _search_left_for_checkin(self, operator, value): + if operator not in ("=",): + raise UserError( + _("Invalid domain operator %s for left of checkin", operator) + ) + + if value not in (True,): + raise UserError( + _("Invalid domain right operand %s for left of checkin", value) + ) + + today = fields.Date.context_today(self) + return [ + ("state", "in", ("draft", "confirm", "no_show")), + ("checkin", "<=", today), + ] + + def _compute_ready_for_checkin(self): + # Reservations with hosts data enought to checkin + for record in self: + record.ready_for_checkin = ( + record.left_for_checkin + and len( + record.checkin_partner_ids.filtered( + lambda c: c.state == "precheckin" + ) + ) + >= 1 + ) + + def _compute_checkin_today(self): + for record in self: + record.checkin_today = ( True if record.checkin == fields.Date.today() else False ) # REVIEW: Late checkin?? (next day) - def _search_arrival_today(self, operator, value): + def _search_checkin_today(self, operator, value): if operator not in ("=", "!="): raise UserError(_("Invalid domain operator %s", operator)) @@ -1171,14 +1217,6 @@ class PmsReservation(models.Model): record.folio_id.action_confirm() return True - def button_done(self): - """ - @param self: object pointer - """ - for record in self: - record.action_reservation_checkout() - return True - def action_cancel(self): for record in self: cancel_reason = ( diff --git a/pms/views/pms_checkin_partner_views.xml b/pms/views/pms_checkin_partner_views.xml index 99e3b1dec..6e985c9ae 100644 --- a/pms/views/pms_checkin_partner_views.xml +++ b/pms/views/pms_checkin_partner_views.xml @@ -230,12 +230,6 @@ -
- -
diff --git a/pms/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index c4e28441d..c1bc8593c 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -14,7 +14,7 @@
- + - + + + Date: Tue, 17 Nov 2020 09:21:02 +0100 Subject: [PATCH 33/37] [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 4c2e4e45a..cbe83746c 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 9e9765711..0846b325f 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -150,11 +150,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 31a68bcc2..aed069ffc 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 ae296b13b..a68711b31 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -301,8 +301,8 @@ name="agency_id" options="{'no_create': True,'no_open': True}" />--> - - + + --> - + Date: Mon, 23 Nov 2020 14:07:24 +0100 Subject: [PATCH 34/37] [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 0846b325f..d461c58e0 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -1096,19 +1096,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) @@ -1117,7 +1119,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 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/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index a68711b31..3a7376286 100644 --- a/pms/views/pms_reservation_views.xml +++ b/pms/views/pms_reservation_views.xml @@ -301,8 +301,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 35/37] [IMP] add commissions on reservation and folio --- pms/data/pms_data.xml | 8 ++-- pms/models/pms_folio.py | 45 +++++++++++++++++----- pms/models/pms_reservation.py | 47 ++++++++++++++++------- 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, 169 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 cbe83746c..981823a94 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", @@ -281,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): @@ -310,11 +328,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 d461c58e0..b77313b4b 100644 --- a/pms/models/pms_reservation.py +++ b/pms/models/pms_reservation.py @@ -193,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", @@ -510,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": @@ -519,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): @@ -750,6 +763,25 @@ class PmsReservation(models.Model): today = fields.Date.context_today(self) return [("checkout", searching_for_true, today)] + @api.depends("agency_id") + def _compute_commission_percent(self): + for reservation in self: + if reservation.agency_id: + reservation.commission_percent = ( + reservation.agency_id.default_commission + ) + else: + reservation.commission_percent = 0 + + @api.depends("commission_percent", "price_total") + def _compute_commission_amount(self): + for reservation in self: + if reservation.commission_percent > 0: + reservation.commission_amount = ( + reservation.price_total * reservation.commission_percent + ) + else: + reservation.commission_amount = 0 # REVIEW: Dont run with set room_type_id -> room_id(compute)-> No set adults¿? @api.depends("preferred_room_id") @@ -1096,21 +1128,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) @@ -1119,7 +1141,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 8fd1589ba..6a10f9f21 100644 --- a/pms/tests/__init__.py +++ b/pms/tests/__init__.py @@ -23,3 +23,4 @@ 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/views/pms_reservation_views.xml b/pms/views/pms_reservation_views.xml index 3a7376286..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)]}" /> - + + + + + - + - + + + > + + + + + + + + From 69b9b8a06c09090c5272c4cd961a9fb7baaf4ac3 Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Nov 2020 13:32:26 +0100 Subject: [PATCH 36/37] [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 3d49e1377b9b982bd7241b14d450cced0c9ca78a Mon Sep 17 00:00:00 2001 From: Sara Lago Date: Mon, 30 Nov 2020 13:52:51 +0100 Subject: [PATCH 37/37] [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 981823a94..01460eb2b 100644 --- a/pms/models/pms_folio.py +++ b/pms/models/pms_folio.py @@ -683,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/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 @@ - + + +