From 9aa3d50cc2474b7b5f4c2800574224bb9fa3b5de Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Wed, 3 Oct 2018 19:25:20 +0200 Subject: [PATCH 01/46] [WIP] Synchronize manually from a remote node --- hotel_node_master/models/hotel_node.py | 54 ++++++++++++ hotel_node_master/models/hotel_node_user.py | 95 ++++++++++++--------- 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index 2cb8adb3c..dfeaa0fad 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -131,9 +131,62 @@ class HotelNode(models.Model): raise ValidationError(err) # TODO logout from node in any case. Take into account each try / except block + try: + vals = {} + # import remote users + # TODO Restrict users to hootel users + domain = [('login', '!=', 'admin')] + fields = ['name', 'login', 'email', 'is_company', 'partner_id', 'groups_id', 'active'] + remote_users = noderpc.env['res.users'].search_read(domain, fields) + + master_users = self.env["hotel.node.user"].search_read( + [('node_id', '=', self.id)], ['remote_user_id']) + + master_ids = [r['id'] for r in master_users] + remote_ids = [r['remote_user_id'] for r in master_users] + + user_ids = [] + for user in remote_users: + if user['id'] in remote_ids: + idx = remote_ids.index(user['id']) + user_ids.append((1, master_ids[idx], { + 'name': user['name'], + 'login': user['login'], + 'email': user['email'], + 'active': user['active'], + 'remote_user_id': user['id'], + 'is_synchronizing': True, + })) + else: + user_ids.append((0, 0, { + 'name': user['name'], + 'login': user['login'], + 'email': user['email'], + 'active': user['active'], + 'remote_user_id': user['id'], + 'is_synchronizing': True, + })) + vals.update({'user_ids': user_ids}) + + wdb.set_trace() + + self.write(vals) + + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + + try: + vals = {} + # import remote partners (exclude unconfirmed using DNI) + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + try: vals = {} # import remote room types + # TODO Actually only work for hootel v2 fields = ['name', 'active', 'sequence', 'room_ids'] remote_room_types = noderpc.env['hotel.room.type'].search_read([], fields) @@ -170,6 +223,7 @@ class HotelNode(models.Model): try: vals = {} # import remote rooms + # TODO Actually only work for hootel v2 fields = ['name', 'active', 'sequence', 'capacity', 'room_type_id'] remote_rooms = noderpc.env['hotel.room'].search_read([], fields) diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index 7da26092a..d78a76a0e 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -29,6 +29,9 @@ class HotelNodeUser(models.Model): node_id = fields.Many2one('project.project', 'Hotel', required=True) # remote users are managed as partners into the central node partner_id = fields.Many2one('res.partner', required=True) + name = fields.Char(related='partner_id.name', readonly=True) + email = fields.Char(related='partner_id.email', readonly=True) + login = fields.Char(require=True, help="Used to log into the hotel") password = fields.Char(default='', invisible=True, copy=False, @@ -82,28 +85,39 @@ class HotelNodeUser(models.Model): raise ValidationError(msg) try: - noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) - noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) + if 'is_synchronizing' in vals: + partner = self.env["res.partner"].search([('email', '=', vals['login'])]) + if partner.id: + vals['partner_id'] = partner.id + else: + partner = partner.create({ + 'name': vals['name'], + 'email': vals['login'], + }) + vals['partner_id'] = partner.id + else: + noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) + noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) - partner = self.env["res.partner"].browse(vals['partner_id']) - remote_vals = { - 'name': partner.name, - 'login': vals['login'], - } + partner = self.env["res.partner"].browse(vals['partner_id']) + remote_vals = { + 'name': partner.name, + 'login': vals['login'], + } - if 'group_ids' in vals: - groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2]) - # TODO Improve one rpc call per remote group for better performance - remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups] - remote_vals.update({'groups_id': [[6, False, remote_groups]]}) + if 'group_ids' in vals: + groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2]) + # TODO Improve one rpc call per remote group for better performance + remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups] + remote_vals.update({'groups_id': [[6, False, remote_groups]]}) - # create user and delegate in remote node the default values for the user - remote_user_id = noderpc.env['res.users'].create(remote_vals) - _logger.info('User #%s created remote res.users with ID: [%s]', - self._context.get('uid'), remote_user_id) - vals.update({'remote_user_id': remote_user_id}) + # create user and delegate in remote node the default values for the user + remote_user_id = noderpc.env['res.users'].create(remote_vals) + _logger.info('User #%s created remote res.users with ID: [%s]', + self._context.get('uid'), remote_user_id) + vals.update({'remote_user_id': remote_user_id}) - noderpc.logout() + noderpc.logout() except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: _logger.error(err) @@ -136,32 +150,33 @@ class HotelNodeUser(models.Model): raise ValidationError(msg) try: - noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) - noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) + if 'is_synchronizing' not in vals: + noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) + noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) - remote_vals = {} + remote_vals = {} - if 'active' in vals: - remote_vals.update({'active': vals['active']}) + if 'active' in vals: + remote_vals.update({'active': vals['active']}) - if 'password' in vals: - remote_vals.update({'password': vals['password']}) + if 'password' in vals: + remote_vals.update({'password': vals['password']}) - if 'partner_id' in vals: - partner = self.env["res.partner"].browse(vals['partner_id']) - remote_vals.update({'name': partner.name}) + if 'partner_id' in vals: + partner = self.env["res.partner"].browse(vals['partner_id']) + remote_vals.update({'name': partner.name}) - if 'group_ids' in vals: - groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2]) - # TODO Improve one rpc call per remote group for better performance - remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups] - remote_vals.update({'groups_id': [[6, False, remote_groups]]}) + if 'group_ids' in vals: + groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2]) + # TODO Improve one rpc call per remote group for better performance + remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups] + remote_vals.update({'groups_id': [[6, False, remote_groups]]}) - noderpc.env['res.users'].write([rec.remote_user_id], remote_vals) - _logger.info('User #%s updated remote res.users with ID: [%s]', - self._context.get('uid'), rec.remote_user_id) + noderpc.env['res.users'].write([rec.remote_user_id], remote_vals) + _logger.info('User #%s updated remote res.users with ID: [%s]', + self._context.get('uid'), rec.remote_user_id) - noderpc.logout() + noderpc.logout() except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: _logger.error(err) @@ -175,15 +190,15 @@ class HotelNodeUser(models.Model): """ :raise: ValidationError """ - # TODO In production users are archived instead of removed for rec in self: try: node = rec.node_id noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) - - noderpc.env['res.users'].unlink([rec.remote_user_id]) + # TODO In production users are archived instead of removed + # noderpc.env['res.users'].unlink([rec.remote_user_id]) + noderpc.env['res.users'].write([rec.remote_user_id], {'active': False}) _logger.info('User #%s deleted remote res.users with ID: [%s]', self._context.get('uid'), rec.remote_user_id) noderpc.logout() From afb92403dad5eff6319bfa6b7c15623c5f53bc90 Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Wed, 3 Oct 2018 19:43:42 +0200 Subject: [PATCH 02/46] [WIP] Using with_context for synchronizing --- hotel_node_master/models/hotel_node.py | 8 +++----- hotel_node_master/models/hotel_node_user.py | 6 +++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index dfeaa0fad..c059c4ae0 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -155,7 +155,6 @@ class HotelNode(models.Model): 'email': user['email'], 'active': user['active'], 'remote_user_id': user['id'], - 'is_synchronizing': True, })) else: user_ids.append((0, 0, { @@ -164,13 +163,12 @@ class HotelNode(models.Model): 'email': user['email'], 'active': user['active'], 'remote_user_id': user['id'], - 'is_synchronizing': True, })) vals.update({'user_ids': user_ids}) - wdb.set_trace() - - self.write(vals) + self.with_context({ + 'is_synchronizing': True, + }).write(vals) except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index d78a76a0e..bd513c7d5 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -85,7 +85,7 @@ class HotelNodeUser(models.Model): raise ValidationError(msg) try: - if 'is_synchronizing' in vals: + if 'is_synchronizing' not in self._context: partner = self.env["res.partner"].search([('email', '=', vals['login'])]) if partner.id: vals['partner_id'] = partner.id @@ -148,9 +148,9 @@ class HotelNodeUser(models.Model): _("Odoo version in node: %s") % node.odoo_version _logger.error(msg) raise ValidationError(msg) - + try: - if 'is_synchronizing' not in vals: + if 'is_synchronizing' not in self._context: noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) From 7f5bedfb08a9717c35edd82c60331d68cd263963 Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 8 Oct 2018 18:20:01 +0200 Subject: [PATCH 03/46] [WIP] Synchronize manually from a remote node --- hotel_node_master/models/hotel_node.py | 39 ++++++++++++--------- hotel_node_master/models/hotel_node_user.py | 33 ++++++----------- 2 files changed, 34 insertions(+), 38 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index c059c4ae0..c19757d10 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -57,9 +57,8 @@ class HotelNode(models.Model): """ for node in self: domain = [('id', 'in', node.group_ids.ids), ('odoo_version', '!=', node.odoo_version)] - # TODO Use search_count - invalid_groups = self.env["hotel.node.group"].search(domain) - if len(invalid_groups) > 0: + invalid_groups = self.env["hotel.node.group"].search_count(domain) + if invalid_groups > 0: msg = _("At least one group is not within the node version.") + " " + \ _("Odoo version of the node: %s") % node.odoo_version _logger.warning(msg) @@ -98,13 +97,14 @@ class HotelNode(models.Model): noderpc.login(self.odoo_db, self.odoo_user, self.odoo_password) except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) + # TODO synchronize only if write_date in remote node is newer ¿? try: vals = {} # import remote groups - domain = [('model', '=', 'res.groups')] - fields = ['complete_name', 'display_name'] - remote_groups = noderpc.env['ir.model.data'].search_read(domain, fields) + remote_groups = noderpc.env['ir.model.data'].search_read( + [('model', '=', 'res.groups')], + ['complete_name', 'display_name']) master_groups = self.env["hotel.node.group"].search_read( [('odoo_version', '=', self.odoo_version)], ['xml_id']) @@ -129,15 +129,14 @@ class HotelNode(models.Model): except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) - # TODO logout from node in any case. Take into account each try / except block try: vals = {} # import remote users # TODO Restrict users to hootel users - domain = [('login', '!=', 'admin')] - fields = ['name', 'login', 'email', 'is_company', 'partner_id', 'groups_id', 'active'] - remote_users = noderpc.env['res.users'].search_read(domain, fields) + remote_users = noderpc.env['res.users'].search_read( + [('login', '!=', 'admin')], + ['name', 'login', 'email', 'is_company', 'partner_id', 'groups_id', 'active']) master_users = self.env["hotel.node.user"].search_read( [('node_id', '=', self.id)], ['remote_user_id']) @@ -157,12 +156,21 @@ class HotelNode(models.Model): 'remote_user_id': user['id'], })) else: + partner = self.env['res.partner'].search([('email', '=', user['email'])]) + if not partner: + partner = self.env['res.partner'].create({ + 'name': user['name'], + 'is_company': False, + 'email': user['email'], + }) + user_ids.append((0, 0, { 'name': user['name'], 'login': user['login'], 'email': user['email'], 'active': user['active'], 'remote_user_id': user['id'], + 'partner_id': partner.id, })) vals.update({'user_ids': user_ids}) @@ -170,7 +178,6 @@ class HotelNode(models.Model): 'is_synchronizing': True, }).write(vals) - except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) @@ -185,8 +192,8 @@ class HotelNode(models.Model): vals = {} # import remote room types # TODO Actually only work for hootel v2 - fields = ['name', 'active', 'sequence', 'room_ids'] - remote_room_types = noderpc.env['hotel.room.type'].search_read([], fields) + remote_room_types = noderpc.env['hotel.room.type'].search_read( + [], ['name', 'active', 'sequence', 'room_ids']) master_room_types = self.env["hotel.node.room.type"].search_read( [('node_id', '=', self.id)], ['remote_room_type_id']) @@ -222,8 +229,9 @@ class HotelNode(models.Model): vals = {} # import remote rooms # TODO Actually only work for hootel v2 - fields = ['name', 'active', 'sequence', 'capacity', 'room_type_id'] - remote_rooms = noderpc.env['hotel.room'].search_read([], fields) + remote_rooms = noderpc.env['hotel.room'].search_read( + [], + ['name', 'active', 'sequence', 'capacity', 'room_type_id']) master_rooms = self.env["hotel.node.room"].search_read( [('node_id', '=', self.id)], ['remote_room_id']) @@ -268,6 +276,5 @@ class HotelNode(models.Model): except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) - noderpc.logout() return True diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index bd513c7d5..a3a042a63 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -29,10 +29,10 @@ class HotelNodeUser(models.Model): node_id = fields.Many2one('project.project', 'Hotel', required=True) # remote users are managed as partners into the central node partner_id = fields.Many2one('res.partner', required=True) - name = fields.Char(related='partner_id.name', readonly=True) + name = fields.Char(related='partner_id.name') email = fields.Char(related='partner_id.email', readonly=True) - login = fields.Char(require=True, + login = fields.Char(related='partner_id.email', require=True, help="Used to log into the hotel") password = fields.Char(default='', invisible=True, copy=False, help="Keep empty if you don't want the user to be able to connect on the hotel.") @@ -48,9 +48,8 @@ class HotelNodeUser(models.Model): def _check_group_ids(self): # TODO ensure all group_ids are within the node version domain = [('id', 'in', self.group_ids.ids), ('odoo_version', '!=', self.node_id.odoo_version)] - # TODO Use search_count - invalid_groups = self.env["hotel.node.group"].search(domain) - if len(invalid_groups) > 0: + invalid_groups = self.env["hotel.node.group"].search_count(domain) + if invalid_groups > 0: msg = _("At least one group is not within the node version.") + " " + \ _("Odoo version of the node: %s") % self.node_id.odoo_version _logger.warning(msg) @@ -76,9 +75,8 @@ class HotelNodeUser(models.Model): if 'group_ids' in vals: domain = [('id', 'in', vals['group_ids'][0][2]), ('odoo_version', '!=', node.odoo_version)] - invalid_groups = self.env["hotel.node.group"].search(domain) - # TODO Use search_count - if len(invalid_groups) > 0: + invalid_groups = self.env["hotel.node.group"].search_count(domain) + if invalid_groups > 0: msg = _("At least one group is not within the node version.") + " " + \ _("Odoo version in node: %s") % node.odoo_version _logger.error(msg) @@ -86,16 +84,6 @@ class HotelNodeUser(models.Model): try: if 'is_synchronizing' not in self._context: - partner = self.env["res.partner"].search([('email', '=', vals['login'])]) - if partner.id: - vals['partner_id'] = partner.id - else: - partner = partner.create({ - 'name': vals['name'], - 'email': vals['login'], - }) - vals['partner_id'] = partner.id - else: noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) @@ -141,14 +129,13 @@ class HotelNodeUser(models.Model): if 'group_ids' in vals: domain = [('id', 'in', vals['group_ids'][0][2]), ('odoo_version', '!=', node.odoo_version)] - invalid_groups = self.env["hotel.node.group"].search(domain) - # TODO Use search_count - if len(invalid_groups) > 0: + invalid_groups = self.env["hotel.node.group"].search_count(domain) + if invalid_groups > 0: msg = _("At least one group is not within the node version.") + " " + \ _("Odoo version in node: %s") % node.odoo_version _logger.error(msg) raise ValidationError(msg) - + try: if 'is_synchronizing' not in self._context: noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) @@ -203,6 +190,8 @@ class HotelNodeUser(models.Model): self._context.get('uid'), rec.remote_user_id) noderpc.logout() + # TODO How to manage the relationship with the partner? Also deleted? + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: _logger.error(err) raise ValidationError(err) From 924bbcc38b943770da1ef37fcffe1b25e558c6ad Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 9 Oct 2018 11:38:05 +0200 Subject: [PATCH 04/46] [WIP] res.partner inheritance --- hotel_node_master/__manifest__.py | 1 + hotel_node_master/models/__init__.py | 1 + .../models/inherited_res_partner.py | 14 ++++++++++++++ .../views/inherited_res_partner_views.xml | 16 ++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 hotel_node_master/models/inherited_res_partner.py create mode 100644 hotel_node_master/views/inherited_res_partner_views.xml diff --git a/hotel_node_master/__manifest__.py b/hotel_node_master/__manifest__.py index 88ed89063..c2e1c2222 100644 --- a/hotel_node_master/__manifest__.py +++ b/hotel_node_master/__manifest__.py @@ -18,6 +18,7 @@ 'views/hotel_node_user.xml', 'views/hotel_node_group.xml', 'views/hotel_node_room_type.xml', + 'views/inherited_res_partner_views.xml', 'wizards/wizard_hotel_node_reservation.xml', 'security/hotel_node_security.xml', 'security/ir.model.access.csv' diff --git a/hotel_node_master/models/__init__.py b/hotel_node_master/models/__init__.py index 2555cdee7..9d741ca83 100644 --- a/hotel_node_master/models/__init__.py +++ b/hotel_node_master/models/__init__.py @@ -5,3 +5,4 @@ from . import hotel_node_user from . import hotel_node_group from . import hotel_node_room from . import hotel_node_room_type +from . import inherited_res_partner diff --git a/hotel_node_master/models/inherited_res_partner.py b/hotel_node_master/models/inherited_res_partner.py new file mode 100644 index 000000000..d69da26b7 --- /dev/null +++ b/hotel_node_master/models/inherited_res_partner.py @@ -0,0 +1,14 @@ +# Copyright 2018 Pablo Q. Barriuso +# Copyright 2018 Alexandre Díaz +# Copyright 2018 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class ResPartner(models.Model): + + _inherit = 'res.partner' + # As res.partner has already a `user_ids` field, you can not use that name in this inheritance + node_user_ids = fields.One2many('hotel.node.user', 'partner_id', + 'Users associated to this partner') diff --git a/hotel_node_master/views/inherited_res_partner_views.xml b/hotel_node_master/views/inherited_res_partner_views.xml new file mode 100644 index 000000000..5e2c1085c --- /dev/null +++ b/hotel_node_master/views/inherited_res_partner_views.xml @@ -0,0 +1,16 @@ + + + + + res.partner + + + + + + + + + + + From ed43be57b01fdb48e453019ae448009a33c011b5 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 11 Oct 2018 19:12:33 +0200 Subject: [PATCH 05/46] [WIP] Synchronize manually from a remote node --- hotel_node_master/models/hotel_node.py | 17 ++++++++++++++++- hotel_node_master/models/hotel_node_user.py | 4 ++++ .../models/inherited_res_partner.py | 2 ++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index c19757d10..65cfa5180 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -146,6 +146,12 @@ class HotelNode(models.Model): user_ids = [] for user in remote_users: + group_ids = [] + # retrieve the remote external ID(s) of group records + remote_xml_ids = noderpc.env['res.groups'].browse(user['groups_id']).get_external_id() + for key, value in remote_xml_ids.items(): + group_ids.append(gui_ids[xml_ids.index(value)]) + if user['id'] in remote_ids: idx = remote_ids.index(user['id']) user_ids.append((1, master_ids[idx], { @@ -154,6 +160,11 @@ class HotelNode(models.Model): 'email': user['email'], 'active': user['active'], 'remote_user_id': user['id'], + 'group_ids': [[ + 6, + False, + group_ids + ]] })) else: partner = self.env['res.partner'].search([('email', '=', user['email'])]) @@ -163,7 +174,6 @@ class HotelNode(models.Model): 'is_company': False, 'email': user['email'], }) - user_ids.append((0, 0, { 'name': user['name'], 'login': user['login'], @@ -171,6 +181,11 @@ class HotelNode(models.Model): 'active': user['active'], 'remote_user_id': user['id'], 'partner_id': partner.id, + 'group_ids': [[ + 6, + False, + group_ids + ]] })) vals.update({'user_ids': user_ids}) diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index a3a042a63..d6d0455dd 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -119,6 +119,7 @@ class HotelNodeUser(models.Model): :param dict vals: a dictionary of fields to update and the value to set on them. :raise: ValidationError """ + for rec in self: if 'node_id' in vals and vals['node_id'] != rec.node_id.id: msg = _("Changing a user between nodes is not allowed. Please create a new user instead.") @@ -143,6 +144,9 @@ class HotelNodeUser(models.Model): remote_vals = {} + if 'login' in vals: + remote_vals.update({'login': vals['login']}) + if 'active' in vals: remote_vals.update({'active': vals['active']}) diff --git a/hotel_node_master/models/inherited_res_partner.py b/hotel_node_master/models/inherited_res_partner.py index d69da26b7..89167ee77 100644 --- a/hotel_node_master/models/inherited_res_partner.py +++ b/hotel_node_master/models/inherited_res_partner.py @@ -12,3 +12,5 @@ class ResPartner(models.Model): # As res.partner has already a `user_ids` field, you can not use that name in this inheritance node_user_ids = fields.One2many('hotel.node.user', 'partner_id', 'Users associated to this partner') + + # TODO Override write for updating in remote nodes From b05bbc7ccf277e0ff628229ef14b0918d563102f Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 12 Oct 2018 20:56:58 +0200 Subject: [PATCH 06/46] [FIX] xpath is not working with string --- hotel_node_master/views/inherited_res_partner_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hotel_node_master/views/inherited_res_partner_views.xml b/hotel_node_master/views/inherited_res_partner_views.xml index 5e2c1085c..18af833e6 100644 --- a/hotel_node_master/views/inherited_res_partner_views.xml +++ b/hotel_node_master/views/inherited_res_partner_views.xml @@ -5,7 +5,7 @@ res.partner - + From 5c97e2370ae41b29dff3facf85387d6bc05d7f93 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 12 Oct 2018 20:58:26 +0200 Subject: [PATCH 07/46] [PEP8] No newline at end of file --- hotel_calendar/wizard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hotel_calendar/wizard/__init__.py b/hotel_calendar/wizard/__init__.py index cc636fed6..a75922538 100644 --- a/hotel_calendar/wizard/__init__.py +++ b/hotel_calendar/wizard/__init__.py @@ -1 +1 @@ -from . import wizard_reservation \ No newline at end of file +from . import wizard_reservation From 3d7af43c94178b7bd34aaadabf02e1f4b42efbb9 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 12 Oct 2018 20:59:37 +0200 Subject: [PATCH 08/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 85 ++++++++++++++++++- .../wizards/wizard_hotel_node_reservation.xml | 51 +++++++---- 2 files changed, 118 insertions(+), 18 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index e59e5fa4f..2ead5e23a 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -3,8 +3,14 @@ # Copyright 2018 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import wdb import logging +from datetime import timedelta from odoo import models, fields, api, _ +from odoo.tools import ( + misc, + DEFAULT_SERVER_DATE_FORMAT, + DEFAULT_SERVER_DATETIME_FORMAT) _logger = logging.getLogger(__name__) @@ -13,10 +19,85 @@ class HotelNodeReservationWizard(models.TransientModel): _name = "hotel.node.reservation.wizard" _description = "Hotel Node Reservation Wizard" - node_id = fields.Many2one('project.project', 'Hotel') + @api.model + def _get_default_checkin(self): + pass + + @api.model + def _get_default_checkout(self): + pass + + node_id = fields.Many2one('project.project', 'Hotel', required=True) + + partner_id = fields.Many2one('res.partner', string="Customer") + + checkin = fields.Date('Check In', required=True, + default=_get_default_checkin) + checkout = fields.Date('Check Out', required=True, + default=_get_default_checkout) + + room_type_wizard_ids = fields.Many2many('node.room.type.wizard', + string="Room Types") + + @api.onchange('node_id') + def onchange_node_id(self): + self.ensure_one() + if self.node_id: + today = fields.Date.context_today(self.with_context()) + + # TODO check hotel timezone + checkin = fields.Date.from_string(today).strftime( + DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkin) + + checkout = (fields.Date.from_string(today) + timedelta(days=1)).strftime( + DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkout) + + if checkin >= checkout: + checkout = checkin + timedelta(days=1) + + room_type_ids = self.env['hotel.node.room.type'].search([('node_id','=',self.node_id.id)]) + cmds = room_type_ids.mapped(lambda x: (0, False, { + 'room_type_id': x.id, + 'checkin': checkin, + 'checkout': checkout, + })) + self.update({ + 'checkin': checkin, + 'checkout': checkout, + 'room_type_wizard_ids': cmds, + }) + + +class NodeRoomTypeWizard(models.TransientModel): + _name = "node.room.type.wizard" + _description = "Node Room Type Wizard" + + @api.model + def _get_default_checkin(self): + pass + + @api.model + def _get_default_checkout(self): + pass + + node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') + room_type_name = fields.Char('Name', related='room_type_id.name') + room_type_availability = fields.Integer('Availability', compute="_compute_room_type_availability") - room_id = fields.Many2one('hotel.node.room', 'Rooms') + + rooms_qty = fields.Integer('Number of Rooms', default=0) + + checkin = fields.Date('Check In', required=True, + default=_get_default_checkin) + checkout = fields.Date('Check Out', required=True, + default=_get_default_checkout) + + # compute and search fields, in the same order that fields declaration + @api.multi + def _compute_room_type_availability(self): + for record in self: + record.room_type_availability = 42; diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 84382a9f9..af68e7c44 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -5,30 +5,49 @@ hotel.node.reservation.wizard hotel.node.reservation.wizard -
- + +

- - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + -
-
-
- + + + + + + + + +
+
From 525497fa06eeb117e20ae317458e141eb61118f6 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sat, 13 Oct 2018 19:08:49 +0200 Subject: [PATCH 09/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index 2ead5e23a..77f5b0638 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -5,12 +5,13 @@ import wdb import logging +import urllib.error +import odoorpc.odoo from datetime import timedelta from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError from odoo.tools import ( - misc, - DEFAULT_SERVER_DATE_FORMAT, - DEFAULT_SERVER_DATETIME_FORMAT) + DEFAULT_SERVER_DATE_FORMAT) _logger = logging.getLogger(__name__) @@ -42,7 +43,14 @@ class HotelNodeReservationWizard(models.TransientModel): @api.onchange('node_id') def onchange_node_id(self): self.ensure_one() + _logger.info('onchange_node_id(self): %s', self) if self.node_id: + try: + noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) + noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + today = fields.Date.context_today(self.with_context()) # TODO check hotel timezone @@ -55,7 +63,13 @@ class HotelNodeReservationWizard(models.TransientModel): if checkin >= checkout: checkout = checkin + timedelta(days=1) - room_type_ids = self.env['hotel.node.room.type'].search([('node_id','=',self.node_id.id)]) + wdb.set_trace() + + rooms_availability = noderpc.env['hotel.room.type'].check_availability_room(checkin, checkout) + node.env['hotel.room.type'].search([('id', '=', room_type.id)]) + + room_type_ids = self.env['hotel.node.room.type'].search([('node_id', '=', self.node_id.id)]) + cmds = room_type_ids.mapped(lambda x: (0, False, { 'room_type_id': x.id, 'checkin': checkin, @@ -66,6 +80,12 @@ class HotelNodeReservationWizard(models.TransientModel): 'checkout': checkout, 'room_type_wizard_ids': cmds, }) +# +# def create_folio_node(self): +# # Mediante un botón en el nodo creamos el folio indicando {partner_id, room_lines} pasandole con el +# # formato o2m todas las reservas -room_lines- indicando {room_type_id, checkin, checkout, reservation_lines} +# # reservation_lines en formato o2m usando el campo json_days de node.room.type.wizard. +# class NodeRoomTypeWizard(models.TransientModel): @@ -82,11 +102,13 @@ class NodeRoomTypeWizard(models.TransientModel): node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') + node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') + room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') - room_type_availability = fields.Integer('Availability', compute="_compute_room_type_availability") + room_type_availability = fields.Integer('Availability', compute="_compute_room_type_availability") rooms_qty = fields.Integer('Number of Rooms', default=0) @@ -95,9 +117,25 @@ class NodeRoomTypeWizard(models.TransientModel): checkout = fields.Date('Check Out', required=True, default=_get_default_checkout) - # compute and search fields, in the same order that fields declaration - @api.multi - def _compute_room_type_availability(self): - for record in self: - record.room_type_availability = 42; +# price_unit #compute +# price_total #compute +# json_days #enchufar como texto literal la cadena devuelta por el método prepare_reservation_lines del hotel.reservation del nodo.(para que funcione +# #es necesario que Darío modifique el método en el modulo Hotel haciendolo independiente del self. + # compute and search fields, in the same order that fields declaration + @api.depends('node_id') + def _compute_room_type_availability(self): + pass + # for record in self: + # record.room_type_availability = 42 +# +# @api.onchange('checkin','checkout') +# def onchange_dates(self): +# # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) +# # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango +# # preci_unit y json_days: usando prepare_reservation_lines +# +# @api.onchange('rooms_qty') +# def _compute_price_total(self): +# # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) +# From 921daa44560e957ebb1e030b5f43db2f2d1d99a7 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sat, 13 Oct 2018 19:12:05 +0200 Subject: [PATCH 10/46] [FIX] Use room_type_id --- hotel/models/hotel_room_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hotel/models/hotel_room_type.py b/hotel/models/hotel_room_type.py index 8ce8d5f3d..0c055ea6b 100644 --- a/hotel/models/hotel_room_type.py +++ b/hotel/models/hotel_room_type.py @@ -81,7 +81,7 @@ class HotelRoomType(models.Model): ('id', '=', room_type_id) ]) # QUESTION What linked represent? Rooms in this type ? - rooms_linked = self.room_ids + rooms_linked = room_type_id.room_ids free_rooms = free_rooms & rooms_linked return free_rooms.sorted(key=lambda r: r.sequence) From 6637a58f28e307b06888aed2200775106887e74e Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 15 Oct 2018 09:17:20 +0200 Subject: [PATCH 11/46] [FIX] Use room_type_id --- hotel/models/hotel_room_type.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hotel/models/hotel_room_type.py b/hotel/models/hotel_room_type.py index 0c055ea6b..7dcd4abe0 100644 --- a/hotel/models/hotel_room_type.py +++ b/hotel/models/hotel_room_type.py @@ -74,14 +74,12 @@ class HotelRoomType(models.Model): reservations_rooms = reservations.mapped('room_id.id') free_rooms = self.env['hotel.room'].search([ ('id', 'not in', reservations_rooms), - ('id', 'not in', notthis) + ('room_type_id.id', 'not in', notthis) ]) if room_type_id: - room_type_id = self.env['hotel.room.type'].search([ + rooms_linked = self.env['hotel.room.type'].search([ ('id', '=', room_type_id) - ]) - # QUESTION What linked represent? Rooms in this type ? - rooms_linked = room_type_id.room_ids + ]).room_ids free_rooms = free_rooms & rooms_linked return free_rooms.sorted(key=lambda r: r.sequence) From 4f64ccae0d1e6a0de6ac500c700f6c03db9f80b2 Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Tue, 16 Oct 2018 16:21:56 +0200 Subject: [PATCH 12/46] [ADD] autoassign base method to create reservation without room_id --- hotel/models/hotel_reservation.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 56a00dd74..ecb4d3726 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -271,6 +271,8 @@ class HotelReservation(models.Model): @api.model def create(self, vals): vals.update(self._prepare_add_missing_fields(vals)) + if 'room_id' not in vals: + vals.update(self._autoassign(vals)) if 'folio_id' in vals: folio = self.env["hotel.folio"].browse(vals['folio_id']) vals.update({'channel_type': folio.channel_type}) @@ -342,6 +344,19 @@ class HotelReservation(models.Model): res[field] = line._fields[field].convert_to_write(line[field], line) return res + @api.model + def _autoassign(self, values): + res = {} + checkin = values.get('checkin') + checkout = values.get('checkout') + room_type = values.get('room_type_id') + if checkin and checkout and room_type: + room_chosen = self.env['hotel.room.type'].check_availability_room(checkin, checkout, room_type)[0] + res.update({ + 'room_id': room_chosen + }) + return res + @api.multi def notify_update(self, vals): if 'checkin' in vals or \ From a63665209a7f42fb8996465db77fac185ae848ee Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 16 Oct 2018 16:35:06 +0200 Subject: [PATCH 13/46] [WIP] Reassignment of menu --- hotel_node_master/views/hotel_node.xml | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index a0a923591..a09388bcd 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -135,9 +135,9 @@ /> @@ -157,13 +157,6 @@ parent="dashboard_menu" sequence="1" /> - - - + + Date: Tue, 16 Oct 2018 16:35:15 +0200 Subject: [PATCH 14/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 45 ++++++++++++------- .../wizards/wizard_hotel_node_reservation.xml | 9 ++-- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index 77f5b0638..a505c465a 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -30,7 +30,7 @@ class HotelNodeReservationWizard(models.TransientModel): node_id = fields.Many2one('project.project', 'Hotel', required=True) - partner_id = fields.Many2one('res.partner', string="Customer") + partner_id = fields.Many2one('res.partner', string="Customer", required=True) checkin = fields.Date('Check In', required=True, default=_get_default_checkin) @@ -43,8 +43,8 @@ class HotelNodeReservationWizard(models.TransientModel): @api.onchange('node_id') def onchange_node_id(self): self.ensure_one() - _logger.info('onchange_node_id(self): %s', self) if self.node_id: + _logger.info('onchange_node_id(self): %s', self) try: noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) @@ -55,7 +55,7 @@ class HotelNodeReservationWizard(models.TransientModel): # TODO check hotel timezone checkin = fields.Date.from_string(today).strftime( - DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkin) + DEFAULT_SERVER_DATE_FORMAT) if not self.checkin else fields.Date.from_string(self.checkin) checkout = (fields.Date.from_string(today) + timedelta(days=1)).strftime( DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkout) @@ -63,23 +63,39 @@ class HotelNodeReservationWizard(models.TransientModel): if checkin >= checkout: checkout = checkin + timedelta(days=1) - wdb.set_trace() + # rooms_availability = noderpc.env['hotel.room.type'].check_availability_room(checkin, checkout) # return str - rooms_availability = noderpc.env['hotel.room.type'].check_availability_room(checkin, checkout) - node.env['hotel.room.type'].search([('id', '=', room_type.id)]) + reservation_ids = noderpc.env['hotel.reservation'].search([ + ('reservation_line_ids.date', '>=', checkin), + ('reservation_line_ids.date', '<', checkout), + ('state', '!=', 'cancelled'), + ('overbooking', '=', False) + ]) + reservation_room_ids = noderpc.env['hotel.reservation'].browse(reservation_ids).mapped('room_id.id') - room_type_ids = self.env['hotel.node.room.type'].search([('node_id', '=', self.node_id.id)]) + room_type_availability = {} + for room_type in self.node_id.room_type_ids: + room_type_availability[room_type.id] = noderpc.env['hotel.room'].search_count([ + ('id', 'not in', reservation_room_ids), + ('room_type_id', '=', room_type.remote_room_type_id) + ]) - cmds = room_type_ids.mapped(lambda x: (0, False, { + cmds = self.node_id.room_type_ids.mapped(lambda x: (0, False, { 'room_type_id': x.id, 'checkin': checkin, 'checkout': checkout, + 'room_type_availability': room_type_availability[x.id], })) self.update({ 'checkin': checkin, 'checkout': checkout, 'room_type_wizard_ids': cmds, }) + + @api.model + def create_node_reservation(self): + _logger.info('*** create_node_reservation(self) ***: %s', self) + # # def create_folio_node(self): # # Mediante un botón en el nodo creamos el folio indicando {partner_id, room_lines} pasandole con el @@ -100,16 +116,13 @@ class NodeRoomTypeWizard(models.TransientModel): def _get_default_checkout(self): pass + # node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') + node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') - node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') - room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') - room_type_name = fields.Char('Name', related='room_type_id.name') - room_type_availability = fields.Integer('Availability', compute="_compute_room_type_availability") - rooms_qty = fields.Integer('Number of Rooms', default=0) checkin = fields.Date('Check In', required=True, @@ -123,9 +136,9 @@ class NodeRoomTypeWizard(models.TransientModel): # #es necesario que Darío modifique el método en el modulo Hotel haciendolo independiente del self. # compute and search fields, in the same order that fields declaration - @api.depends('node_id') - def _compute_room_type_availability(self): - pass + # @api.depends('node_id') + # def _compute_room_type_availability(self): + # pass # for record in self: # record.room_type_availability = 42 # diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index af68e7c44..cb2e76db8 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -42,13 +42,12 @@ - - - - +
+
- From 634eba5cefdeb4cd1d4856e2e4c562d2bc0ab064 Mon Sep 17 00:00:00 2001 From: Pablo Date: Wed, 17 Oct 2018 19:02:45 +0200 Subject: [PATCH 15/46] [FIX] Open Wizard in a Window --- hotel_node_master/views/hotel_node.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index a09388bcd..8bc080464 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -138,6 +138,7 @@ name="Hotel Reservation Wizard" res_model="hotel.node.reservation.wizard" view_mode="form" + target="new" /> @@ -180,7 +181,7 @@ /> Date: Wed, 17 Oct 2018 19:03:39 +0200 Subject: [PATCH 16/46] [WIP] Wizard Node Reservation Using prepare_reservation_lines from node --- .../wizards/wizard_hotel_node_reservation.py | 97 +++++++++++++------ .../wizards/wizard_hotel_node_reservation.xml | 6 +- 2 files changed, 72 insertions(+), 31 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index a505c465a..ffab690f8 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -37,8 +37,8 @@ class HotelNodeReservationWizard(models.TransientModel): checkout = fields.Date('Check Out', required=True, default=_get_default_checkout) - room_type_wizard_ids = fields.Many2many('node.room.type.wizard', - string="Room Types") + room_type_wizard_ids = fields.One2many('node.room.type.wizard', 'node_reservation_wizard_id', + string="Room Types") @api.onchange('node_id') def onchange_node_id(self): @@ -64,6 +64,7 @@ class HotelNodeReservationWizard(models.TransientModel): checkout = checkin + timedelta(days=1) # rooms_availability = noderpc.env['hotel.room.type'].check_availability_room(checkin, checkout) # return str + # TODO add check_availability_room in a hotel slave node module reservation_ids = noderpc.env['hotel.reservation'].search([ ('reservation_line_ids.date', '>=', checkin), @@ -71,7 +72,11 @@ class HotelNodeReservationWizard(models.TransientModel): ('state', '!=', 'cancelled'), ('overbooking', '=', False) ]) - reservation_room_ids = noderpc.env['hotel.reservation'].browse(reservation_ids).mapped('room_id.id') + + reservation_room_ids = [] + # do not trust even your father + if reservation_ids: + reservation_room_ids = noderpc.env['hotel.reservation'].browse(reservation_ids).mapped('room_id.id') room_type_availability = {} for room_type in self.node_id.room_type_ids: @@ -85,52 +90,86 @@ class HotelNodeReservationWizard(models.TransientModel): 'checkin': checkin, 'checkout': checkout, 'room_type_availability': room_type_availability[x.id], + 'node_reservation_wizard_id': self.id, + })) self.update({ 'checkin': checkin, 'checkout': checkout, 'room_type_wizard_ids': cmds, }) + noderpc.logout() - @api.model + @api.multi def create_node_reservation(self): - _logger.info('*** create_node_reservation(self) ***: %s', self) + try: + noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) + noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) -# -# def create_folio_node(self): -# # Mediante un botón en el nodo creamos el folio indicando {partner_id, room_lines} pasandole con el -# # formato o2m todas las reservas -room_lines- indicando {room_type_id, checkin, checkout, reservation_lines} -# # reservation_lines en formato o2m usando el campo json_days de node.room.type.wizard. -# + # prepare required fields for hotel folio + vals = { + 'partner_id': self.partner_id.id, + 'checkin': self.checkin, + 'checkout': self.checkout, + } + # prepare hotel folio room_lines + room_lines = [] + for line in self.room_type_wizard_ids: + if line.rooms_qty > 0: + vals_reservation_lines = { + 'partner_id': self.partner_id.id, + 'room_type_id': line.room_type_id.remote_room_type_id, + } + reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines( + line.checkin, + (fields.Date.from_string(line.checkout) - fields.Date.from_string(line.checkin)).days, + vals_reservation_lines + ) + room_lines.append((0, False, { + 'room_type_id': line.room_type_id.remote_room_type_id, + 'checkin': line.checkin, + 'checkout': line.checkout, + 'reservation_line_ids': reservation_line_ids['reservation_line_ids'], + })) + vals.update({'room_lines': room_lines}) + + x = noderpc.env['hotel.reservation'].create(vals) + + noderpc.logout() class NodeRoomTypeWizard(models.TransientModel): _name = "node.room.type.wizard" _description = "Node Room Type Wizard" - @api.model - def _get_default_checkin(self): - pass + def _default_checkin(self): + today = fields.Date.context_today(self.with_context()) + return self.node_reservation_wizard_id.checkin or \ + fields.Date.from_string(today).strftime(DEFAULT_SERVER_DATE_FORMAT) - @api.model - def _get_default_checkout(self): - pass - - # node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') + def _default_checkout(self): + today = fields.Date.context_today(self.with_context()) + return self.node_reservation_wizard_id.checkin or \ + (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') + node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') - room_type_availability = fields.Integer('Availability', compute="_compute_room_type_availability") + room_type_availability = fields.Integer('Availability') #, compute="_compute_room_type_availability") rooms_qty = fields.Integer('Number of Rooms', default=0) checkin = fields.Date('Check In', required=True, - default=_get_default_checkin) + default=_default_checkin) checkout = fields.Date('Check Out', required=True, - default=_get_default_checkout) + default=_default_checkout) -# price_unit #compute + # price_unit = fields.Float('Unit Price', required=True, + # digits=dp.get_precision('Product Price'), + # default=0.0) # price_total #compute # json_days #enchufar como texto literal la cadena devuelta por el método prepare_reservation_lines del hotel.reservation del nodo.(para que funcione # #es necesario que Darío modifique el método en el modulo Hotel haciendolo independiente del self. @@ -142,11 +181,13 @@ class NodeRoomTypeWizard(models.TransientModel): # for record in self: # record.room_type_availability = 42 # -# @api.onchange('checkin','checkout') -# def onchange_dates(self): -# # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) -# # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango -# # preci_unit y json_days: usando prepare_reservation_lines + @api.onchange('checkin','checkout') + def onchange_dates(self): + if self.checkin and self.checkout: + _logger.info('%s', self.room_type_id) + # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) + # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango + # preci_unit y json_days: usando prepare_reservation_lines # # @api.onchange('rooms_qty') # def _compute_price_total(self): diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index cb2e76db8..202bc45cd 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -9,7 +9,7 @@

-

@@ -25,8 +25,8 @@ - - + + From 9401fbe918946878c4e804e196ced2ff701f675f Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Thu, 18 Oct 2018 13:20:34 +0200 Subject: [PATCH 17/46] [TMP][FIX] run create direct folio --- hotel/models/hotel_folio.py | 38 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 9e18492e6..9ef68c97f 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -324,13 +324,13 @@ class HotelFolio(models.Model): # Makes sure partner_invoice_id' and 'pricelist_id' are defined lfields = ('partner_invoice_id', 'partner_shipping_id', 'pricelist_id') - if any(f not in vals for f in lfields): - partner = self.env['res.partner'].browse(vals.get('partner_id')) - addr = partner.address_get(['delivery', 'invoice']) - vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) - vals['pricelist_id'] = vals.setdefault( - 'pricelist_id', - partner.property_product_pricelist and partner.property_product_pricelist.id) + #~ if any(f not in vals for f in lfields): + #~ partner = self.env['res.partner'].browse(vals.get('partner_id')) + #~ addr = partner.address_get(['delivery', 'invoice']) + #~ vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) + #~ vals['pricelist_id'] = vals.setdefault( + #~ 'pricelist_id', + #~ partner.property_product_pricelist and partner.property_product_pricelist.id) result = super(HotelFolio, self).create(vals) return result @@ -344,20 +344,22 @@ class HotelFolio(models.Model): - user_id """ if not self.partner_id: - self.update({ - 'partner_invoice_id': False, - 'payment_term_id': False, - 'fiscal_position_id': False, - }) + #~ self.update({ + #~ 'partner_invoice_id': False, + #~ 'payment_term_id': False, + #~ 'fiscal_position_id': False, + #~ }) return addr = self.partner_id.address_get(['invoice']) - values = { - 'pricelist_id': self.partner_id.property_product_pricelist and \ - self.partner_id.property_product_pricelist.id or False, - 'partner_invoice_id': addr['invoice'], - 'user_id': self.partner_id.user_id.id or self.env.uid - } + #TEMP: + values = { 'user_id': self.partner_id.user_id.id or self.env.uid } + #~ values = { + #~ 'pricelist_id': self.partner_id.property_product_pricelist and \ + #~ self.partner_id.property_product_pricelist.id or False, + #~ 'partner_invoice_id': addr['invoice'], + #~ 'user_id': self.partner_id.user_id.id or self.env.uid + #~ } if self.env['ir.config_parameter'].sudo().get_param('sale.use_sale_note') and \ self.env.user.company_id.sale_note: values['note'] = self.with_context( From 89fcc852a5963f1400d46b999209853a36689aac Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 18 Oct 2018 13:53:01 +0200 Subject: [PATCH 18/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 25 +++++++++++-------- .../wizards/wizard_hotel_node_reservation.xml | 3 +++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index ffab690f8..ebcbaf95a 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -9,6 +9,7 @@ import urllib.error import odoorpc.odoo from datetime import timedelta from odoo import models, fields, api, _ +from odoo.addons import decimal_precision as dp from odoo.exceptions import ValidationError from odoo.tools import ( DEFAULT_SERVER_DATE_FORMAT) @@ -41,7 +42,7 @@ class HotelNodeReservationWizard(models.TransientModel): string="Room Types") @api.onchange('node_id') - def onchange_node_id(self): + def _onchange_node_id(self): self.ensure_one() if self.node_id: _logger.info('onchange_node_id(self): %s', self) @@ -85,11 +86,11 @@ class HotelNodeReservationWizard(models.TransientModel): ('room_type_id', '=', room_type.remote_room_type_id) ]) - cmds = self.node_id.room_type_ids.mapped(lambda x: (0, False, { - 'room_type_id': x.id, + cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { + 'room_type_id': room_type_id.id, 'checkin': checkin, 'checkout': checkout, - 'room_type_availability': room_type_availability[x.id], + 'room_type_availability': room_type_availability[room_type_id.id], 'node_reservation_wizard_id': self.id, })) @@ -160,16 +161,18 @@ class NodeRoomTypeWizard(models.TransientModel): room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') room_type_availability = fields.Integer('Availability') #, compute="_compute_room_type_availability") - rooms_qty = fields.Integer('Number of Rooms', default=0) + rooms_qty = fields.Integer('Quantity', default=0) checkin = fields.Date('Check In', required=True, default=_default_checkin) checkout = fields.Date('Check Out', required=True, default=_default_checkout) - # price_unit = fields.Float('Unit Price', required=True, - # digits=dp.get_precision('Product Price'), - # default=0.0) + price_unit = fields.Float('Unit Price', required=True, + digits=dp.get_precision('Unit Price'), + default=0.0) + discount = fields.Float(string='Discount (%)', + digits=dp.get_precision('Discount'), default=0.0) # price_total #compute # json_days #enchufar como texto literal la cadena devuelta por el método prepare_reservation_lines del hotel.reservation del nodo.(para que funcione # #es necesario que Darío modifique el método en el modulo Hotel haciendolo independiente del self. @@ -181,10 +184,10 @@ class NodeRoomTypeWizard(models.TransientModel): # for record in self: # record.room_type_availability = 42 # - @api.onchange('checkin','checkout') - def onchange_dates(self): + @api.onchange('node_id','checkin','checkout') + def _onchange_dates(self): if self.checkin and self.checkout: - _logger.info('%s', self.room_type_id) + _logger.info('_onchange_dates for room type %s', self.room_type_id) # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango # preci_unit y json_days: usando prepare_reservation_lines diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 202bc45cd..43ce59df2 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -28,6 +28,9 @@ + + + From bb88d2b2859baf3e3f1e6f3e94c5d159dac3f807 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 18 Oct 2018 17:58:08 +0200 Subject: [PATCH 19/46] [ADD] Hotel Node Helper This module is for providing helper functions to the hotel node master module. --- hotel_node_helper/README.rst | 22 ++++++++++++++++++ hotel_node_helper/__init__.py | 3 +++ hotel_node_helper/__manifest__.py | 21 +++++++++++++++++ hotel_node_helper/models/__init__.py | 3 +++ .../models/inherited_hotel_room_type.py | 22 ++++++++++++++++++ .../security/hotel_node_security.xml | 3 +++ .../security/ir.model.access.csv | 1 + hotel_node_helper/static/description/icon.png | Bin 0 -> 29724 bytes 8 files changed, 75 insertions(+) create mode 100644 hotel_node_helper/README.rst create mode 100644 hotel_node_helper/__init__.py create mode 100644 hotel_node_helper/__manifest__.py create mode 100644 hotel_node_helper/models/__init__.py create mode 100644 hotel_node_helper/models/inherited_hotel_room_type.py create mode 100644 hotel_node_helper/security/hotel_node_security.xml create mode 100644 hotel_node_helper/security/ir.model.access.csv create mode 100644 hotel_node_helper/static/description/icon.png diff --git a/hotel_node_helper/README.rst b/hotel_node_helper/README.rst new file mode 100644 index 000000000..4a067e3c4 --- /dev/null +++ b/hotel_node_helper/README.rst @@ -0,0 +1,22 @@ +================= +Hotel Node Helper +================= + +This module is for providing helper functions to the hotel node master module. + +**Try me on Runbot** + +**Known issues / Roadmap** + +... + +**Bug Tracker** + +Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here. + +Credits + +Contributors + +Maintainer + diff --git a/hotel_node_helper/__init__.py b/hotel_node_helper/__init__.py new file mode 100644 index 000000000..69f7babdf --- /dev/null +++ b/hotel_node_helper/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/hotel_node_helper/__manifest__.py b/hotel_node_helper/__manifest__.py new file mode 100644 index 000000000..917ab41b4 --- /dev/null +++ b/hotel_node_helper/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Hotel Node Helper', + 'summary': """Provides helper functions to the hotel node master module""", + 'version': '0.1.0', + 'author': 'Pablo Q. Barriuso, \ + Darío Lodeiros, \ + Alexandre Díaz, \ + Odoo Community Association (OCA)', + 'category': 'Generic Modules/Hotel Management', + 'depends': [ + 'hotel' + ], + 'license': "AGPL-3", + 'data': [ + 'security/hotel_node_security.xml', + 'security/ir.model.access.csv' + ], + 'demo': [], + 'auto_install': False, + 'installable': True +} diff --git a/hotel_node_helper/models/__init__.py b/hotel_node_helper/models/__init__.py new file mode 100644 index 000000000..c21375169 --- /dev/null +++ b/hotel_node_helper/models/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import inherited_hotel_room_type diff --git a/hotel_node_helper/models/inherited_hotel_room_type.py b/hotel_node_helper/models/inherited_hotel_room_type.py new file mode 100644 index 000000000..ac03ae961 --- /dev/null +++ b/hotel_node_helper/models/inherited_hotel_room_type.py @@ -0,0 +1,22 @@ +# Copyright 2018 Pablo Q. Barriuso +# Copyright 2018 Alexandre Díaz +# Copyright 2018 Dario Lodeiros +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import wdb +from odoo import models, fields, api + + +class HotelRoomType(models.Model): + + _inherit = 'hotel.room.type' + + @api.model + def check_availability_room_ids(self, dfrom, dto, + room_type_id=False, notthis=[]): + """ + Check availability for all or specific room types between dates + @return: A list of `ids` with free rooms + """ + free_rooms = super().check_availability_room(dfrom, dto, room_type_id, notthis) + return free_rooms.ids diff --git a/hotel_node_helper/security/hotel_node_security.xml b/hotel_node_helper/security/hotel_node_security.xml new file mode 100644 index 000000000..74979936c --- /dev/null +++ b/hotel_node_helper/security/hotel_node_security.xml @@ -0,0 +1,3 @@ + + + diff --git a/hotel_node_helper/security/ir.model.access.csv b/hotel_node_helper/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/hotel_node_helper/security/ir.model.access.csv @@ -0,0 +1 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink diff --git a/hotel_node_helper/static/description/icon.png b/hotel_node_helper/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0b47e0cf928b031afe5420c03646e8405e11d096 GIT binary patch literal 29724 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 literal 0 HcmV?d00001 From 62af7b87bde343a54f64dc8b1d7bfadd3daff333 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 18 Oct 2018 17:58:48 +0200 Subject: [PATCH 20/46] [WIP] Use Hotel Node Helper for Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index ebcbaf95a..f2fd6628f 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -64,25 +64,12 @@ class HotelNodeReservationWizard(models.TransientModel): if checkin >= checkout: checkout = checkin + timedelta(days=1) - # rooms_availability = noderpc.env['hotel.room.type'].check_availability_room(checkin, checkout) # return str - # TODO add check_availability_room in a hotel slave node module - - reservation_ids = noderpc.env['hotel.reservation'].search([ - ('reservation_line_ids.date', '>=', checkin), - ('reservation_line_ids.date', '<', checkout), - ('state', '!=', 'cancelled'), - ('overbooking', '=', False) - ]) - - reservation_room_ids = [] - # do not trust even your father - if reservation_ids: - reservation_room_ids = noderpc.env['hotel.reservation'].browse(reservation_ids).mapped('room_id.id') + free_room_ids = noderpc.env['hotel.room.type'].check_availability_room_ids(checkin, checkout) room_type_availability = {} for room_type in self.node_id.room_type_ids: room_type_availability[room_type.id] = noderpc.env['hotel.room'].search_count([ - ('id', 'not in', reservation_room_ids), + ('id', 'in', free_room_ids), ('room_type_id', '=', room_type.remote_room_type_id) ]) @@ -137,6 +124,8 @@ class HotelNodeReservationWizard(models.TransientModel): })) vals.update({'room_lines': room_lines}) + wdb.set_trace() + x = noderpc.env['hotel.reservation'].create(vals) noderpc.logout() From e877c69ccb341dc434d6400b38a5420d30395da2 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 18 Oct 2018 18:03:42 +0200 Subject: [PATCH 21/46] [WIP] Minor information update --- hotel_node_master/README.rst | 3 ++- hotel_node_master/__manifest__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hotel_node_master/README.rst b/hotel_node_master/README.rst index a8612512e..0511c648d 100644 --- a/hotel_node_master/README.rst +++ b/hotel_node_master/README.rst @@ -1,5 +1,5 @@ ================= -Hotel Master Node +Hotel Node Master ================= This module is for providing centralized hotel management features for hootel. @@ -8,6 +8,7 @@ You can manage: - Node connection data - Remote users and access groups +- Hotel reservations **Installation** diff --git a/hotel_node_master/__manifest__.py b/hotel_node_master/__manifest__.py index c2e1c2222..fb227bb0c 100644 --- a/hotel_node_master/__manifest__.py +++ b/hotel_node_master/__manifest__.py @@ -1,5 +1,5 @@ { - 'name': 'Hotel Master Node', + 'name': 'Hotel Node Master', 'summary': """Provides centralized hotel management features""", 'version': '0.1.0', 'author': 'Pablo Q. Barriuso, \ From 976b3b6984be51477d8be1dda288b19786e8a724 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 19 Oct 2018 16:51:11 +0200 Subject: [PATCH 22/46] [WIP] Synchronize partners from a remote node --- hotel_node_master/models/hotel_node.py | 40 ++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index 65cfa5180..7f1575236 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -82,6 +82,8 @@ class HotelNode(models.Model): vals.update({'odoo_version': noderpc.version}) + # TODO Check if hotel_node_helper module is installed / available in the node. + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) else: @@ -133,7 +135,6 @@ class HotelNode(models.Model): try: vals = {} # import remote users - # TODO Restrict users to hootel users remote_users = noderpc.env['res.users'].search_read( [('login', '!=', 'admin')], ['name', 'login', 'email', 'is_company', 'partner_id', 'groups_id', 'active']) @@ -197,8 +198,39 @@ class HotelNode(models.Model): raise ValidationError(err) try: - vals = {} - # import remote partners (exclude unconfirmed using DNI) + # import remote partners + node_partners = noderpc.env['res.partner'].search_read( + [('email', '!=', '')], # TODO import remote partners (exclude unconfirmed using DNI) + ['name', 'email', 'is_company', 'website', 'type', 'active']) + master_partners = self.env['res.partner'].search([('email', 'in', [r['email'] for r in node_partners])]) + + master_partner_emails = [r['email'] for r in master_partners] + master_partner_ids = master_partners.ids + for partner in node_partners: + if partner['email'] not in master_partner_emails: + new_partner = self.env['res.partner'].create({ + 'name': partner['name'], + 'email': partner['email'], + 'is_company': partner['is_company'], + 'website': partner['website'], + 'type': partner['type'], + 'active': partner['active'], + }) + _logger.info('User #%s created res.partner with ID: [%s]', + self._context.get('uid'), new_partner.id) + else: + partner_id = master_partner_ids[master_partner_emails.index(partner['email'])] + self.env['res.partner'].browse(partner_id).write({ + 'name': partner['name'], + 'is_company': partner['is_company'], + 'website': partner['website'], + 'type': partner['type'], + 'active': partner['active'], + # Partners in different Nodes may have different parent_id + # TODO How to manage parent_id for related company ¿? + }) + _logger.info('User #%s update res.partner with ID: [%s]', + self._context.get('uid'), partner_id) except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) @@ -206,7 +238,6 @@ class HotelNode(models.Model): try: vals = {} # import remote room types - # TODO Actually only work for hootel v2 remote_room_types = noderpc.env['hotel.room.type'].search_read( [], ['name', 'active', 'sequence', 'room_ids']) @@ -243,7 +274,6 @@ class HotelNode(models.Model): try: vals = {} # import remote rooms - # TODO Actually only work for hootel v2 remote_rooms = noderpc.env['hotel.room'].search_read( [], ['name', 'active', 'sequence', 'capacity', 'room_type_id']) From 2b724418e72259f1550655e043253593f172c207 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 19 Oct 2018 18:32:22 +0200 Subject: [PATCH 23/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 27 ++++++------ .../wizards/wizard_hotel_node_reservation.xml | 42 ++++++++++--------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index f2fd6628f..e6349dc14 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -40,6 +40,7 @@ class HotelNodeReservationWizard(models.TransientModel): room_type_wizard_ids = fields.One2many('node.room.type.wizard', 'node_reservation_wizard_id', string="Room Types") + price_total = fields.Float(string='Total Price', default=250.0) @api.onchange('node_id') def _onchange_node_id(self): @@ -124,8 +125,6 @@ class HotelNodeReservationWizard(models.TransientModel): })) vals.update({'room_lines': room_lines}) - wdb.set_trace() - x = noderpc.env['hotel.reservation'].create(vals) noderpc.logout() @@ -150,18 +149,17 @@ class NodeRoomTypeWizard(models.TransientModel): room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') room_type_availability = fields.Integer('Availability') #, compute="_compute_room_type_availability") - rooms_qty = fields.Integer('Quantity', default=0) + room_qty = fields.Integer('Quantity', default=0) checkin = fields.Date('Check In', required=True, default=_default_checkin) checkout = fields.Date('Check Out', required=True, default=_default_checkout) - price_unit = fields.Float('Unit Price', required=True, - digits=dp.get_precision('Unit Price'), - default=0.0) - discount = fields.Float(string='Discount (%)', - digits=dp.get_precision('Discount'), default=0.0) + price_unit = fields.Float(string='Room Price', required=True, default=0.0) + discount = fields.Float(string='Discount (%)', default=0.0) + price_total = fields.Float(string='Total Price', compute="_compute_price_total") + # price_total #compute # json_days #enchufar como texto literal la cadena devuelta por el método prepare_reservation_lines del hotel.reservation del nodo.(para que funcione # #es necesario que Darío modifique el método en el modulo Hotel haciendolo independiente del self. @@ -180,8 +178,11 @@ class NodeRoomTypeWizard(models.TransientModel): # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango # preci_unit y json_days: usando prepare_reservation_lines -# -# @api.onchange('rooms_qty') -# def _compute_price_total(self): -# # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) -# + + @api.onchange('rooms_qty') + def _compute_price_total(self): + self.price_total + for record in self: + record.price_total = record.room_qty * (record.price_unit * record.discount * 0.01) + # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) + diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 43ce59df2..9a57be24f 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -13,7 +13,7 @@ attrs="{'readonly': [('node_id', '!=', False)]}"/> - + @@ -21,25 +21,27 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + From dca70847f0b40d0821c2d645887f1d0822e9ec5b Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Tue, 23 Oct 2018 13:48:23 +0200 Subject: [PATCH 24/46] [TMP][FIX] run create direct folio --- hotel/models/hotel_folio.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/hotel/models/hotel_folio.py b/hotel/models/hotel_folio.py index 9ef68c97f..85de9f48d 100644 --- a/hotel/models/hotel_folio.py +++ b/hotel/models/hotel_folio.py @@ -321,16 +321,17 @@ class HotelFolio(models.Model): ).next_by_code('sale.order') or _('New') else: vals['name'] = self.env['ir.sequence'].next_by_code('hotel.folio') or _('New') + # Makes sure partner_invoice_id' and 'pricelist_id' are defined lfields = ('partner_invoice_id', 'partner_shipping_id', 'pricelist_id') - #~ if any(f not in vals for f in lfields): - #~ partner = self.env['res.partner'].browse(vals.get('partner_id')) - #~ addr = partner.address_get(['delivery', 'invoice']) + if any(f not in vals for f in lfields): + partner = self.env['res.partner'].browse(vals.get('partner_id')) + addr = partner.address_get(['delivery', 'invoice']) #~ vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice']) - #~ vals['pricelist_id'] = vals.setdefault( - #~ 'pricelist_id', - #~ partner.property_product_pricelist and partner.property_product_pricelist.id) + vals['pricelist_id'] = vals.setdefault( + 'pricelist_id', + partner.property_product_pricelist and partner.property_product_pricelist.id) result = super(HotelFolio, self).create(vals) return result @@ -353,7 +354,10 @@ class HotelFolio(models.Model): addr = self.partner_id.address_get(['invoice']) #TEMP: - values = { 'user_id': self.partner_id.user_id.id or self.env.uid } + values = { 'user_id': self.partner_id.user_id.id or self.env.uid, + 'pricelist_id':self.partner_id.property_product_pricelist and \ + self.partner_id.property_product_pricelist.id or \ + self.env['ir.default'].sudo().get('res.config.settings', 'parity_pricelist_id')} #~ values = { #~ 'pricelist_id': self.partner_id.property_product_pricelist and \ #~ self.partner_id.property_product_pricelist.id or False, From 428d2e259c373fee50f8a0f8e7c8e2ac8b987c3d Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 23 Oct 2018 13:56:33 +0200 Subject: [PATCH 25/46] [WIP][MIG][11.0] hotel Fix refactoring using guidelines --- hotel/wizard/massive_changes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hotel/wizard/massive_changes.py b/hotel/wizard/massive_changes.py index d15c6339f..c71c0e3ef 100644 --- a/hotel/wizard/massive_changes.py +++ b/hotel/wizard/massive_changes.py @@ -255,7 +255,7 @@ class MassiveChangesWizard(models.TransientModel): diff_days = abs((date_end_dt - date_start_dt).days) + 1 wedays = (record.dmo, record.dtu, record.dwe, record.dth, record.dfr, record.dsa, record.dsu) - room_types = record.room_type_id if record.applied_on == '1' \ + room_types = record.room_type_ids if record.applied_on == '1' \ else hotel_room_type_obj.search([]) for i in range(0, diff_days): From 07e7ac78d7bb5e281ce6764a7178741bfe6abea1 Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 23 Oct 2018 13:56:49 +0200 Subject: [PATCH 26/46] [WIP] Wizard Node Reservation --- hotel_node_master/views/hotel_node.xml | 5 + .../wizards/wizard_hotel_node_reservation.py | 165 ++++++++++-------- .../wizards/wizard_hotel_node_reservation.xml | 16 +- 3 files changed, 107 insertions(+), 79 deletions(-) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index 8bc080464..30029783f 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -8,6 +8,11 @@
+
- + - - +
From b34e4124ea2ca8a9a9798af5adf7997fa73d4c8b Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 25 Oct 2018 12:06:21 +0200 Subject: [PATCH 31/46] [WIP] Wizard Node Reservation --- .../models/inherited_hotel_room_type.py | 19 ++ .../wizards/wizard_hotel_node_reservation.py | 217 +++++++++--------- .../wizards/wizard_hotel_node_reservation.xml | 2 + 3 files changed, 134 insertions(+), 104 deletions(-) diff --git a/hotel_node_helper/models/inherited_hotel_room_type.py b/hotel_node_helper/models/inherited_hotel_room_type.py index ac03ae961..f0e9bd96b 100644 --- a/hotel_node_helper/models/inherited_hotel_room_type.py +++ b/hotel_node_helper/models/inherited_hotel_room_type.py @@ -20,3 +20,22 @@ class HotelRoomType(models.Model): """ free_rooms = super().check_availability_room(dfrom, dto, room_type_id, notthis) return free_rooms.ids + + @api.model + def get_room_type_availability(self, dfrom, dto, room_type_id): + free_rooms = self.check_availability_room(dfrom, dto) + availability_real = self.env['hotel.room'].search_count([ + ('id', 'in', free_rooms.ids), + ('room_type_id', '=', room_type_id), + ]) + availability_plan = self.env['hotel.room.type.availability'].search_read([ + ('date', '>=', dfrom), + ('date', '<', dto), + ('room_type_id', '=', room_type_id), + + ], ['avail']) or float('inf') + + if isinstance(availability_plan, list): + availability_plan = min([r['avail'] for r in availability_plan]) + + return min(availability_real, availability_plan) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index a9adc6dae..8e672fbf9 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -22,107 +22,104 @@ class HotelNodeReservationWizard(models.TransientModel): _description = "Hotel Node Reservation Wizard" @api.model - def _get_default_node_id(self): + def _default_node_id(self): return self._context.get('node_id') or None @api.model - def _get_default_checkin(self): - pass + def _default_checkin(self): + today = fields.Date.context_today(self.with_context()) + return fields.Date.from_string(today).strftime(DEFAULT_SERVER_DATE_FORMAT) @api.model - def _get_default_checkout(self): - pass - - node_id = fields.Many2one('project.project', 'Hotel', required=True, - default=_get_default_node_id) + def _default_checkout(self): + today = fields.Date.context_today(self.with_context()) + return (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) + node_id = fields.Many2one('project.project', 'Hotel', required=True, default=_default_node_id) partner_id = fields.Many2one('res.partner', string="Customer", required=True) - - checkin = fields.Date('Check In', required=True, - default=_get_default_checkin) - checkout = fields.Date('Check Out', required=True, - default=_get_default_checkout) - + checkin = fields.Date('Check In', required=True, default=_default_checkin) + checkout = fields.Date('Check Out', required=True, default=_default_checkout) room_type_wizard_ids = fields.One2many('node.room.type.wizard', 'node_reservation_wizard_id', string="Room Types") - price_total = fields.Float(string='Total Price', default=250.0) + price_total = fields.Float(string='Total Price', compute='_compute_price_total') + + @api.depends('room_type_wizard_ids.price_total') + def _compute_price_total(self): + _logger.info('_compute_price_total for wizard %s', self.id) + price_total = 0.0 + for record in self.room_type_wizard_ids: + price_total += record.price_total + self.price_total = price_total @api.onchange('node_id') def _onchange_node_id(self): - # self.ensure_one() + self.ensure_one() if self.node_id: - _logger.info('onchange_node_id(self): %s', self) - try: - noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) - noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) - - today = fields.Date.context_today(self.with_context()) - - # TODO check hotel timezone - checkin = fields.Date.from_string(today).strftime( - DEFAULT_SERVER_DATE_FORMAT) if not self.checkin else fields.Date.from_string(self.checkin) - - checkout = (fields.Date.from_string(today) + timedelta(days=1)).strftime( - DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkout) - - free_room_ids = noderpc.env['hotel.room.type'].check_availability_room_ids(checkin, checkout) - - room_type_availability = {} - for room_type in self.node_id.room_type_ids: - availability_real = noderpc.env['hotel.room'].search_count([ - ('id', 'in', free_room_ids), - ('room_type_id', '=', room_type.remote_room_type_id), - ]) - availability_plan = noderpc.env['hotel.room.type.availability'].search_read([ - ('date', '>=', checkin), - ('date', '<', checkout), - ('room_type_id', '=', room_type.remote_room_type_id), - - ], ['avail']) or float('inf') - - if isinstance(availability_plan, list): - availability_plan = min([r['avail'] for r in availability_plan]) - - room_type_availability[room_type.id] = min( - availability_real, availability_plan) - - cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { - 'room_type_id': room_type_id.id, - 'checkin': checkin, - 'checkout': checkout, - 'room_type_availability': room_type_availability[room_type_id.id], - 'node_reservation_wizard_id': self.id, - - })) - self.update({ - 'checkin': checkin, - 'checkout': checkout, - 'room_type_wizard_ids': cmds, - }) - noderpc.logout() - except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: - raise ValidationError(err) + _logger.info('_onchange_node_id(self): %s', self) + # Save your credentials (session) @api.onchange('checkin', 'checkout') def _onchange_dates(self): + self.ensure_one() _logger.info('_onchange_dates(self): %s', self) - - today = fields.Date.context_today(self.with_context()) - # TODO check hotel timezone - self.checkin = fields.Date.from_string(today).strftime( - DEFAULT_SERVER_DATE_FORMAT) if not self.checkin else fields.Date.from_string(self.checkin) - - self.checkout = (fields.Date.from_string(today) + timedelta(days=1)).strftime( - DEFAULT_SERVER_DATE_FORMAT) if not self.checkout else fields.Date.from_string(self.checkout) - + self.checkin = self._get_default_checkin() if not self.checkin \ + else fields.Date.from_string(self.checkin) + self.checkout = self._get_default_checkout() if not self.checkout \ + else fields.Date.from_string(self.checkout) if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout): self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime( - DEFAULT_SERVER_DATE_FORMAT) + DEFAULT_SERVER_DATE_FORMAT) - for room_type in self.room_type_wizard_ids: - room_type.checkin = self.checkin - room_type.checkout = self.checkout + try: + noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) + noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) + + # free_room_ids = noderpc.env['hotel.room.type'].check_availability_room_ids(self.checkin, self.checkout) + room_type_availability = {} + # room_type_price_unit = {} + for room_type in self.node_id.room_type_ids: + room_type_availability[room_type.id] = \ + noderpc.env['hotel.room.type'].get_room_type_availability( + self.checkin, self.checkout, room_type.remote_room_type_id) + # availability_real = noderpc.env['hotel.room'].search_count([ + # ('id', 'in', free_room_ids), + # ('room_type_id', '=', room_type.remote_room_type_id), + # ]) + # availability_plan = noderpc.env['hotel.room.type.availability'].search_read([ + # ('date', '>=', self.checkin), + # ('date', '<', self.checkout), + # ('room_type_id', '=', room_type.remote_room_type_id), + # + # ], ['avail']) or float('inf') + # + # if isinstance(availability_plan, list): + # availability_plan = min([r['avail'] for r in availability_plan]) + # + # room_type_availability[room_type.id] = min( + # availability_real, availability_plan) + + # room_type_price_unit[room_type.id] = noderpc.env['hotel.room.type'].search_read([ + # ('id', '=', room_type.remote_room_type_id), + # ], ['list_price'])[0]['list_price'] + + nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days + + cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { + 'room_type_id': room_type_id.id, + 'checkin': self.checkin, + 'checkout': self.checkout, + 'nights': nights, + 'room_type_availability': room_type_availability[room_type_id.id], + # 'price_unit': room_type_price_unit[room_type_id.id], + 'node_reservation_wizard_id': self.id, + })) + self.room_type_wizard_ids = cmds + + noderpc.logout() + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) @api.multi def create_node_reservation(self): @@ -143,6 +140,7 @@ class HotelNodeReservationWizard(models.TransientModel): 'partner_id': remote_partner_id, 'room_type_id': line.room_type_id.remote_room_type_id, } + # añadir descuento reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines( line.checkin, (fields.Date.from_string(line.checkout) - fields.Date.from_string(line.checkin)).days, @@ -173,16 +171,6 @@ class NodeRoomTypeWizard(models.TransientModel): _name = "node.room.type.wizard" _description = "Node Room Type Wizard" - def _default_checkin(self): - today = fields.Date.context_today(self.with_context()) - return self.node_reservation_wizard_id.checkin or \ - fields.Date.from_string(today).strftime(DEFAULT_SERVER_DATE_FORMAT) - - def _default_checkout(self): - today = fields.Date.context_today(self.with_context()) - return self.node_reservation_wizard_id.checkin or \ - (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) - node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') node_id = fields.Many2one(related='node_reservation_wizard_id.node_id') @@ -191,28 +179,49 @@ class NodeRoomTypeWizard(models.TransientModel): room_type_availability = fields.Integer('Availability') #, compute="_compute_room_type_availability") room_qty = fields.Integer('Quantity', default=0) - checkin = fields.Date('Check In', required=True, default=_default_checkin) - checkout = fields.Date('Check Out', required=True, default=_default_checkout) + checkin = fields.Date('Check In', required=True) + checkout = fields.Date('Check Out', required=True) + nights = fields.Integer('Nights', readonly=True) + min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True) price_unit = fields.Float(string='Room Price', required=True, default=0.0) discount = fields.Float(string='Discount (%)', default=0.0) - price_total = fields.Float(string='Total Price', compute="_compute_price_total") + price_total = fields.Float(string='Total Price', compute='_compute_price_total') - # compute and search fields, in the same order that fields declaration - @api.onchange('checkin','checkout') + @api.depends('room_qty', 'price_unit', 'discount') + def _compute_price_total(self): + for room_type in self: + _logger.info('_compute_price_total for room type %s', room_type.room_type_id) + # noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) + # noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) + # self.price_unit = noderpc.env['hotel.room.type'].search_read([ + # ('id', '=', self.room_type_id.remote_room_type_id), + # ], ['list_price'])[0]['list_price'] + # noderpc.logout() + + room_type.price_total = (room_type.room_qty * room_type.price_unit * room_type.nights) * (1.0 - room_type.discount * 0.01) + # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) + + @api.depends('checkin', 'checkout') + def _compute_restrictions(self): + for room_type in self: + _logger.info('_compute_restrictions for room type %s', room_type.room_type_id) + + @api.onchange('checkin', 'checkout') def _onchange_dates(self): _logger.info('_onchange_dates for room type %s', self.room_type_id) - wdb.set_trace() - # if self.checkin and self.checkout: + # recompute price unit + self.checkin = self._default_checkin() \ + if not self.checkin else fields.Date.from_string(self.checkin) + self.checkout = self._default_checkout() \ + if not self.checkout else fields.Date.from_string(self.checkout) + if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout): + self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime( + DEFAULT_SERVER_DATE_FORMAT) + + self.nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango # preci_unit y json_days: usando prepare_reservation_lines - @api.depends('room_qty', 'price_unit', 'discount', 'checkin', 'checkout') - def _compute_price_total(self): - _logger.info('_compute_price_total') - for record in self: - record.price_total = (record.room_qty * record.price_unit) * (1.0 - record.discount * 0.01) - # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) - diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 1700fc32b..7355d63af 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -30,6 +30,8 @@ + + From fe09534b6df42a59e7ede9d5d80e03c620c19ae3 Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Thu, 25 Oct 2018 12:12:34 +0200 Subject: [PATCH 32/46] [IMP] get_domain function to reservations --- hotel/models/hotel_reservation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index f2a9eab6b..9593abf50 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -825,11 +825,16 @@ class HotelReservation(models.Model): @param dto: range date to @return: array with the reservations _confirmed_ between dfrom and dto """ + domain = self._get_domain_reservations_occupation(dfrom, dto) + return self.env['hotel.reservation'].search(domain) + + @api.model + def _get_domain_reservations_occupation(self, dfrom, dto): domain = [('reservation_line_ids.date', '>=', dfrom), ('reservation_line_ids.date', '<', dto), ('state', '!=', 'cancelled'), ('overbooking', '=', False)] - return self.env['hotel.reservation'].search(domain) + return domain @api.model def get_reservations_dates(self, dfrom, dto, room_type=False): From 2f7af67723635787ada1cedee9b163fc6996d52e Mon Sep 17 00:00:00 2001 From: Dario Lodeiros Date: Thu, 25 Oct 2018 12:45:53 +0200 Subject: [PATCH 33/46] [FIX] Prepare add missing fields reservation --- hotel/models/hotel_reservation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hotel/models/hotel_reservation.py b/hotel/models/hotel_reservation.py index 9593abf50..094fe6179 100644 --- a/hotel/models/hotel_reservation.py +++ b/hotel/models/hotel_reservation.py @@ -270,9 +270,9 @@ class HotelReservation(models.Model): @api.model def create(self, vals): - vals.update(self._prepare_add_missing_fields(vals)) if 'room_id' not in vals: vals.update(self._autoassign(vals)) + vals.update(self._prepare_add_missing_fields(vals)) if 'folio_id' in vals: folio = self.env["hotel.folio"].browse(vals['folio_id']) vals.update({'channel_type': folio.channel_type}) @@ -335,7 +335,7 @@ class HotelReservation(models.Model): """ Deduce missing required fields from the onchange """ res = {} onchange_fields = ['room_id', 'reservation_type', 'currency_id', 'name'] - if values.get('partner_id') and values.get('room_type_id'): + if values.get('room_type_id'): line = self.new(values) if any(f not in values for f in onchange_fields): line.onchange_room_id() From ea6ac31a48b5653262436e18a0dadad627018d40 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 25 Oct 2018 16:18:29 +0200 Subject: [PATCH 34/46] [WIP] Wizard Node Reservation --- hotel_node_master/views/hotel_node.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index 30029783f..f30470568 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -185,10 +185,10 @@ sequence="1" /> - + + + + + + From 8f72ea645d1ac22719da2fcaeb32761eba26b896 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 25 Oct 2018 16:20:21 +0200 Subject: [PATCH 35/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 22 +++++++++---------- .../wizards/wizard_hotel_node_reservation.xml | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index 8e672fbf9..411365908 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -134,23 +134,23 @@ class HotelNodeReservationWizard(models.TransientModel): } # prepare hotel folio room_lines room_lines = [] - for line in self.room_type_wizard_ids: - if line.room_qty > 0: + for room_type in self.room_type_wizard_ids: + for x in range(room_type.room_qty): vals_reservation_lines = { 'partner_id': remote_partner_id, - 'room_type_id': line.room_type_id.remote_room_type_id, + 'room_type_id': room_type.room_type_id.remote_room_type_id, } - # añadir descuento + # add discount reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines( - line.checkin, - (fields.Date.from_string(line.checkout) - fields.Date.from_string(line.checkin)).days, + room_type.checkin, + (fields.Date.from_string(room_type.checkout) - fields.Date.from_string(room_type.checkin)).days, vals_reservation_lines ) # [[5, 0, 0], ¿? room_lines.append((0, False, { - 'room_type_id': line.room_type_id.remote_room_type_id, - 'checkin': line.checkin, - 'checkout': line.checkout, + 'room_type_id': room_type.room_type_id.remote_room_type_id, + 'checkin': room_type.checkin, + 'checkout': room_type.checkout, 'reservation_line_ids': reservation_line_ids['reservation_line_ids'], })) vals.update({'room_lines': room_lines}) @@ -176,7 +176,7 @@ class NodeRoomTypeWizard(models.TransientModel): room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') - room_type_availability = fields.Integer('Availability') #, compute="_compute_room_type_availability") + room_type_availability = fields.Integer('Availability', readonly=True) #, compute="_compute_room_type_availability") room_qty = fields.Integer('Quantity', default=0) checkin = fields.Date('Check In', required=True) @@ -184,7 +184,7 @@ class NodeRoomTypeWizard(models.TransientModel): nights = fields.Integer('Nights', readonly=True) min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True) - price_unit = fields.Float(string='Room Price', required=True, default=0.0) + price_unit = fields.Float(string='Room Price', required=True, default=0.0, readonly=True) discount = fields.Float(string='Discount (%)', default=0.0) price_total = fields.Float(string='Total Price', compute='_compute_price_total') diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 7355d63af..0a34e5c1d 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -31,8 +31,8 @@ - - + + From 01535f71850e29213465357ec60ee638db14e4bc Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 25 Oct 2018 20:36:01 +0200 Subject: [PATCH 36/46] [WIP] Wizard Node Reservation Price unit is calculated in the node using hotel functions --- .../models/inherited_hotel_room_type.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/hotel_node_helper/models/inherited_hotel_room_type.py b/hotel_node_helper/models/inherited_hotel_room_type.py index f0e9bd96b..41d2c2b62 100644 --- a/hotel_node_helper/models/inherited_hotel_room_type.py +++ b/hotel_node_helper/models/inherited_hotel_room_type.py @@ -39,3 +39,20 @@ class HotelRoomType(models.Model): availability_plan = min([r['avail'] for r in availability_plan]) return min(availability_real, availability_plan) + + @api.model + def get_room_type_price_unit(self, dfrom, dto, room_type_id): + # TODO review how to get the prices + # price_unit = self.browse(room_type_id).list_price + reservation_line_ids = self.env['hotel.reservation'].prepare_reservation_lines( + dfrom, + (fields.Date.from_string(dto) - fields.Date.from_string(dfrom)).days, + {'room_type_id': room_type_id} + ) + reservation_line_ids = reservation_line_ids['reservation_line_ids'] + + price_unit = 0.0 + for x in range(1, len(reservation_line_ids)): + price_unit = price_unit + reservation_line_ids[x][2]['price'] + + return price_unit From 30e2ca99e04677f86c16ca95bfff29b63e8c1f87 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 25 Oct 2018 20:36:12 +0200 Subject: [PATCH 37/46] [WIP] Wizard Node Reservation --- .../wizards/wizard_hotel_node_reservation.py | 143 +++++++----------- 1 file changed, 57 insertions(+), 86 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index 411365908..053f081aa 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -2,8 +2,6 @@ # Copyright 2018 Alexandre Díaz # Copyright 2018 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from builtins import list - import wdb import logging import urllib.error @@ -47,8 +45,8 @@ class HotelNodeReservationWizard(models.TransientModel): def _compute_price_total(self): _logger.info('_compute_price_total for wizard %s', self.id) price_total = 0.0 - for record in self.room_type_wizard_ids: - price_total += record.price_total + for rec in self.room_type_wizard_ids: + price_total += rec.price_total self.price_total = price_total @api.onchange('node_id') @@ -56,70 +54,34 @@ class HotelNodeReservationWizard(models.TransientModel): self.ensure_one() if self.node_id: _logger.info('_onchange_node_id(self): %s', self) - # Save your credentials (session) + # TODO Save your credentials (session) @api.onchange('checkin', 'checkout') def _onchange_dates(self): self.ensure_one() _logger.info('_onchange_dates(self): %s', self) + # TODO check hotel timezone self.checkin = self._get_default_checkin() if not self.checkin \ else fields.Date.from_string(self.checkin) self.checkout = self._get_default_checkout() if not self.checkout \ else fields.Date.from_string(self.checkout) + if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout): self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime( DEFAULT_SERVER_DATE_FORMAT) - try: - noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) - noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) + cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { + 'room_type_id': room_type_id.id, + 'checkin': self.checkin, + 'checkout': self.checkout, + 'nights': (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days, + # 'room_type_availability': room_type_availability[room_type_id.id], + # 'price_unit': room_type_price_unit[room_type_id.id], + 'node_reservation_wizard_id': self.id, + })) - # free_room_ids = noderpc.env['hotel.room.type'].check_availability_room_ids(self.checkin, self.checkout) - room_type_availability = {} - # room_type_price_unit = {} - for room_type in self.node_id.room_type_ids: - room_type_availability[room_type.id] = \ - noderpc.env['hotel.room.type'].get_room_type_availability( - self.checkin, self.checkout, room_type.remote_room_type_id) - # availability_real = noderpc.env['hotel.room'].search_count([ - # ('id', 'in', free_room_ids), - # ('room_type_id', '=', room_type.remote_room_type_id), - # ]) - # availability_plan = noderpc.env['hotel.room.type.availability'].search_read([ - # ('date', '>=', self.checkin), - # ('date', '<', self.checkout), - # ('room_type_id', '=', room_type.remote_room_type_id), - # - # ], ['avail']) or float('inf') - # - # if isinstance(availability_plan, list): - # availability_plan = min([r['avail'] for r in availability_plan]) - # - # room_type_availability[room_type.id] = min( - # availability_real, availability_plan) - - # room_type_price_unit[room_type.id] = noderpc.env['hotel.room.type'].search_read([ - # ('id', '=', room_type.remote_room_type_id), - # ], ['list_price'])[0]['list_price'] - - nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days - - cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { - 'room_type_id': room_type_id.id, - 'checkin': self.checkin, - 'checkout': self.checkout, - 'nights': nights, - 'room_type_availability': room_type_availability[room_type_id.id], - # 'price_unit': room_type_price_unit[room_type_id.id], - 'node_reservation_wizard_id': self.id, - })) - self.room_type_wizard_ids = cmds - - noderpc.logout() - - except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: - raise ValidationError(err) + self.room_type_wizard_ids = cmds @api.multi def create_node_reservation(self): @@ -128,29 +90,29 @@ class HotelNodeReservationWizard(models.TransientModel): noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) # prepare required fields for hotel folio - remote_partner_id = noderpc.env['res.partner'].search([('email','=',self.partner_id.email)]).pop() + remote_partner_id = noderpc.env['res.partner'].search([('email', '=', self.partner_id.email)]).pop() vals = { 'partner_id': remote_partner_id, } # prepare hotel folio room_lines room_lines = [] - for room_type in self.room_type_wizard_ids: - for x in range(room_type.room_qty): + for rec in self.room_type_wizard_ids: + for x in range(rec.room_qty): vals_reservation_lines = { 'partner_id': remote_partner_id, - 'room_type_id': room_type.room_type_id.remote_room_type_id, + 'room_type_id': rec.room_type_id.remote_room_type_id, } # add discount reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines( - room_type.checkin, - (fields.Date.from_string(room_type.checkout) - fields.Date.from_string(room_type.checkin)).days, + rec.checkin, + (fields.Date.from_string(rec.checkout) - fields.Date.from_string(rec.checkin)).days, vals_reservation_lines - ) # [[5, 0, 0], ¿? + ) # [[5, 0, 0], ¿? room_lines.append((0, False, { - 'room_type_id': room_type.room_type_id.remote_room_type_id, - 'checkin': room_type.checkin, - 'checkout': room_type.checkout, + 'room_type_id': rec.room_type_id.remote_room_type_id, + 'checkin': rec.checkin, + 'checkout': rec.checkout, 'reservation_line_ids': reservation_line_ids['reservation_line_ids'], })) vals.update({'room_lines': room_lines}) @@ -176,52 +138,61 @@ class NodeRoomTypeWizard(models.TransientModel): room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_name = fields.Char('Name', related='room_type_id.name') - room_type_availability = fields.Integer('Availability', readonly=True) #, compute="_compute_room_type_availability") + room_type_availability = fields.Integer('Availability', compute="_compute_restrictions", readonly=True) room_qty = fields.Integer('Quantity', default=0) checkin = fields.Date('Check In', required=True) checkout = fields.Date('Check Out', required=True) nights = fields.Integer('Nights', readonly=True) min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True) - - price_unit = fields.Float(string='Room Price', required=True, default=0.0, readonly=True) + # price_unit indicates Room Price x Nights + price_unit = fields.Float(string='Room Price', compute="_compute_restrictions", readonly=True) discount = fields.Float(string='Discount (%)', default=0.0) price_total = fields.Float(string='Total Price', compute='_compute_price_total') - @api.depends('room_qty', 'price_unit', 'discount') + @api.depends('room_qty', 'price_unit', 'discount', 'nights') def _compute_price_total(self): - for room_type in self: - _logger.info('_compute_price_total for room type %s', room_type.room_type_id) - # noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) - # noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) - # self.price_unit = noderpc.env['hotel.room.type'].search_read([ - # ('id', '=', self.room_type_id.remote_room_type_id), - # ], ['list_price'])[0]['list_price'] - # noderpc.logout() - - room_type.price_total = (room_type.room_qty * room_type.price_unit * room_type.nights) * (1.0 - room_type.discount * 0.01) - # Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días) + for rec in self: + _logger.info('_compute_price_total for room type %s', rec.room_type_id) + rec.price_total = (rec.room_qty * rec.price_unit) * (1.0 - rec.discount * 0.01) @api.depends('checkin', 'checkout') def _compute_restrictions(self): - for room_type in self: - _logger.info('_compute_restrictions for room type %s', room_type.room_type_id) + for rec in self: + try: + # TODO Load your credentials (session) ... should be faster? + noderpc = odoorpc.ODOO(rec.node_id.odoo_host, rec.node_id.odoo_protocol, rec.node_id.odoo_port) + noderpc.login(rec.node_id.odoo_db, rec.node_id.odoo_user, rec.node_id.odoo_password) + + _logger.info('_compute_restrictions [availability] for room type %s', rec.room_type_id) + rec.room_type_availability = noderpc.env['hotel.room.type'].get_room_type_availability( + rec.checkin, + rec.checkout, + rec.room_type_id.remote_room_type_id) + + _logger.info('_compute_restrictions [price_unit] for room type %s', rec.room_type_id) + rec.price_unit = noderpc.env['hotel.room.type'].get_room_type_price_unit( + rec.checkin, + rec.checkout, + rec.room_type_id.remote_room_type_id) + + _logger.info('_compute_restrictions [min days] for room type %s', rec.room_type_id) + + noderpc.logout() + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) @api.onchange('checkin', 'checkout') def _onchange_dates(self): _logger.info('_onchange_dates for room type %s', self.room_type_id) - # recompute price unit + self.checkin = self._default_checkin() \ if not self.checkin else fields.Date.from_string(self.checkin) self.checkout = self._default_checkout() \ if not self.checkout else fields.Date.from_string(self.checkout) + if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout): self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime( DEFAULT_SERVER_DATE_FORMAT) self.nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days - - # Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit) - # availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango - # preci_unit y json_days: usando prepare_reservation_lines - From b127892a37851c1f2fa25782950b249238266c17 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 26 Oct 2018 16:15:51 +0200 Subject: [PATCH 38/46] [WIP] Simplify Code --- .../models/inherited_hotel_room_type.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hotel_node_helper/models/inherited_hotel_room_type.py b/hotel_node_helper/models/inherited_hotel_room_type.py index 41d2c2b62..b3a4c4213 100644 --- a/hotel_node_helper/models/inherited_hotel_room_type.py +++ b/hotel_node_helper/models/inherited_hotel_room_type.py @@ -32,11 +32,10 @@ class HotelRoomType(models.Model): ('date', '>=', dfrom), ('date', '<', dto), ('room_type_id', '=', room_type_id), + ], ['avail']) or [{'avail': availability_real}] - ], ['avail']) or float('inf') - - if isinstance(availability_plan, list): - availability_plan = min([r['avail'] for r in availability_plan]) + # if isinstance(availability_plan, list): + availability_plan = min([r['avail'] for r in availability_plan]) return min(availability_real, availability_plan) @@ -56,3 +55,15 @@ class HotelRoomType(models.Model): price_unit = price_unit + reservation_line_ids[x][2]['price'] return price_unit + + @api.model + def get_room_type_restrictions(self, dfrom, dto, room_type_id): + restrictions_plan = self.env['hotel.room.type.restriction.item'].search_read([ + ('date', '>=', dfrom), + ('date', '<', dto), + ('room_type_id', '=', room_type_id), + ], ['min_stay']) or [{'min_stay': 0}] + + min_stay = max([r['min_stay'] for r in restrictions_plan]) + + return min_stay From ca9caaae301644c12dc4a6d2ce31a434a4b170d9 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 26 Oct 2018 16:16:43 +0200 Subject: [PATCH 39/46] [WIP] Wizard Node Reservation Create reservation from central node --- .../wizards/wizard_hotel_node_reservation.py | 70 ++++++++++++++++--- .../wizards/wizard_hotel_node_reservation.xml | 8 +-- 2 files changed, 63 insertions(+), 15 deletions(-) diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index 053f081aa..9116d83e7 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -41,13 +41,26 @@ class HotelNodeReservationWizard(models.TransientModel): string="Room Types") price_total = fields.Float(string='Total Price', compute='_compute_price_total') + @api.constrains('room_type_wizard_ids') + def _check_room_type_wizard_ids(self): + """ + :raise: ValidationError + """ + total_qty = 0 + for rec in self.room_type_wizard_ids: + total_qty += rec.room_qty + + if total_qty == 0: + msg = _("It is not possible to create the reservation.") + " " + \ + _("Maybe you forgot adding the quantity to at least one type of room?.") + raise ValidationError(msg) + @api.depends('room_type_wizard_ids.price_total') def _compute_price_total(self): _logger.info('_compute_price_total for wizard %s', self.id) - price_total = 0.0 + self.price_total = 0.0 for rec in self.room_type_wizard_ids: - price_total += rec.price_total - self.price_total = price_total + self.price_total += rec.price_total @api.onchange('node_id') def _onchange_node_id(self): @@ -85,6 +98,7 @@ class HotelNodeReservationWizard(models.TransientModel): @api.multi def create_node_reservation(self): + self.ensure_one() try: noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port) noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password) @@ -143,18 +157,38 @@ class NodeRoomTypeWizard(models.TransientModel): checkin = fields.Date('Check In', required=True) checkout = fields.Date('Check Out', required=True) - nights = fields.Integer('Nights', readonly=True) + nights = fields.Integer('Nights', compute="_compute_nights", readonly=True) min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True) # price_unit indicates Room Price x Nights - price_unit = fields.Float(string='Room Price', compute="_compute_restrictions", readonly=True) + price_unit = fields.Float(string='Room Price', compute="_compute_restrictions", readonly=True, store=True) discount = fields.Float(string='Discount (%)', default=0.0) - price_total = fields.Float(string='Total Price', compute='_compute_price_total') + price_total = fields.Float(string='Total Price', compute='_compute_price_total', readonly=True, store=True) - @api.depends('room_qty', 'price_unit', 'discount', 'nights') + @api.constrains('room_qty') + def _check_room_qty(self): + """ + :raise: ValidationError + """ + total_qty = 0 + for rec in self: + if (rec.room_type_availability < rec.room_qty) or (rec.room_qty > 0 and rec.nights < rec.min_stay): + msg = _("At least one room type has not availability or does not meet restrictions.") + " " + \ + _("Please, review room type %s between %s and %s.") % (rec.room_type_name, rec.checkin, rec.checkout) + _logger.warning(msg) + raise ValidationError(msg) + total_qty += rec.room_qty + + @api.depends('room_qty', 'price_unit', 'discount') def _compute_price_total(self): for rec in self: _logger.info('_compute_price_total for room type %s', rec.room_type_id) rec.price_total = (rec.room_qty * rec.price_unit) * (1.0 - rec.discount * 0.01) + # TODO rec.price unit trigger _compute_restriction ¿? store = True? + + @api.depends('checkin', 'checkout') + def _compute_nights(self): + for rec in self: + rec.nights = (fields.Date.from_string(rec.checkout) - fields.Date.from_string(rec.checkin)).days @api.depends('checkin', 'checkout') def _compute_restrictions(self): @@ -164,24 +198,39 @@ class NodeRoomTypeWizard(models.TransientModel): noderpc = odoorpc.ODOO(rec.node_id.odoo_host, rec.node_id.odoo_protocol, rec.node_id.odoo_port) noderpc.login(rec.node_id.odoo_db, rec.node_id.odoo_user, rec.node_id.odoo_password) - _logger.info('_compute_restrictions [availability] for room type %s', rec.room_type_id) + _logger.warning('_compute_restrictions [availability] for room type %s', rec.room_type_id) rec.room_type_availability = noderpc.env['hotel.room.type'].get_room_type_availability( rec.checkin, rec.checkout, rec.room_type_id.remote_room_type_id) - _logger.info('_compute_restrictions [price_unit] for room type %s', rec.room_type_id) + _logger.warning('_compute_restrictions [price_unit] for room type %s', rec.room_type_id) rec.price_unit = noderpc.env['hotel.room.type'].get_room_type_price_unit( rec.checkin, rec.checkout, rec.room_type_id.remote_room_type_id) - _logger.info('_compute_restrictions [min days] for room type %s', rec.room_type_id) + _logger.warning('_compute_restrictions [min days] for room type %s', rec.room_type_id) + rec.min_stay = noderpc.env['hotel.room.type'].get_room_type_restrictions( + rec.checkin, + rec.checkout, + rec.room_type_id.remote_room_type_id) noderpc.logout() except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) + @api.onchange('room_qty') + def _onchange_room_qty(self): + if self.room_type_availability < self.room_qty: + msg = _("Please, review room type %s between %s and %s.") % (self.room_type_name, self.checkin, self.checkout) + return { + 'warning': { + 'title': 'Warning: Invalid room quantity', + 'message': msg, + } + } + @api.onchange('checkin', 'checkout') def _onchange_dates(self): _logger.info('_onchange_dates for room type %s', self.room_type_id) @@ -195,4 +244,3 @@ class NodeRoomTypeWizard(models.TransientModel): self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime( DEFAULT_SERVER_DATE_FORMAT) - self.nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 0a34e5c1d..82f73ae83 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -28,14 +28,14 @@ - - + + - + - +
From db1cc94147009a255873e345ce6cd8f04fcda994 Mon Sep 17 00:00:00 2001 From: Pablo Date: Fri, 26 Oct 2018 20:35:57 +0200 Subject: [PATCH 40/46] [WIP] Rearrange menu items and actions --- hotel_node_master/views/hotel_node.xml | 82 ++----------------- hotel_node_master/views/hotel_node_group.xml | 15 ++++ .../views/hotel_node_room_type.xml | 15 ++++ hotel_node_master/views/hotel_node_user.xml | 15 ++++ .../wizards/wizard_hotel_node_reservation.xml | 29 +++++-- 5 files changed, 75 insertions(+), 81 deletions(-) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index f30470568..bbc065ca1 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -8,7 +8,7 @@
- +
- + @@ -24,17 +24,18 @@ + - + + - - - + + - + @@ -54,15 +55,70 @@ - hotel.node.reservation.wizard.search - hotel.node.reservation.wizard + hotel.node.reservation.wizard.search + node.search.wizard + +
+ +
+

+ +

+
+ + + + + + + + + +
+
+
+
+
+
+ + + hotel.node.reservation.wizard.edit + node.folio.wizard + + + + + + + + - - + + + + + + + + + + + + + + +