From a5ad202432c11031fc48e9d37a13aa17e53f4291 Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Thu, 20 Sep 2018 10:24:45 +0200 Subject: [PATCH 01/33] [WIP] Manage Nodes and Groups by Odoo version --- hotel_node_master/README.rst | 42 ++++ hotel_node_master/__init__.py | 3 + hotel_node_master/__manifest__.py | 26 +++ hotel_node_master/models/__init__.py | 5 + hotel_node_master/models/hotel_node.py | 110 +++++++++ hotel_node_master/models/hotel_node_group.py | 36 +++ hotel_node_master/models/hotel_node_user.py | 211 ++++++++++++++++++ .../security/hotel_node_security.xml | 3 + .../security/ir.model.access.csv | 1 + hotel_node_master/static/description/icon.png | Bin 0 -> 22165 bytes hotel_node_master/views/hotel_node.xml | 151 +++++++++++++ hotel_node_master/views/hotel_node_group.xml | 14 ++ hotel_node_master/views/hotel_node_user.xml | 53 +++++ 13 files changed, 655 insertions(+) create mode 100644 hotel_node_master/README.rst create mode 100644 hotel_node_master/__init__.py create mode 100644 hotel_node_master/__manifest__.py create mode 100644 hotel_node_master/models/__init__.py create mode 100644 hotel_node_master/models/hotel_node.py create mode 100644 hotel_node_master/models/hotel_node_group.py create mode 100644 hotel_node_master/models/hotel_node_user.py create mode 100644 hotel_node_master/security/hotel_node_security.xml create mode 100644 hotel_node_master/security/ir.model.access.csv create mode 100644 hotel_node_master/static/description/icon.png create mode 100644 hotel_node_master/views/hotel_node.xml create mode 100644 hotel_node_master/views/hotel_node_group.xml create mode 100644 hotel_node_master/views/hotel_node_user.xml diff --git a/hotel_node_master/README.rst b/hotel_node_master/README.rst new file mode 100644 index 000000000..a8612512e --- /dev/null +++ b/hotel_node_master/README.rst @@ -0,0 +1,42 @@ +================= +Hotel Master Node +================= + +This module is for providing centralized hotel management features for hootel. + +You can manage: + +- Node connection data +- Remote users and access groups + +**Installation** + +To install this module, you need to: + +**External dependencies** + - OdooRPC, a Python package providing an easy way to pilot your Odoo servers through RPC + +**Configuration** + +To configure this module, you need to: + +**Usage** + +To use this module, you need to: + +**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_master/__init__.py b/hotel_node_master/__init__.py new file mode 100644 index 000000000..69f7babdf --- /dev/null +++ b/hotel_node_master/__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_master/__manifest__.py b/hotel_node_master/__manifest__.py new file mode 100644 index 000000000..48bde0927 --- /dev/null +++ b/hotel_node_master/__manifest__.py @@ -0,0 +1,26 @@ +{ + 'name': 'Hotel Master Node', + 'summary': """Provides centralized hotel management features""", + 'version': '0.1.0', + 'author': 'Pablo Q. Barriuso, \ + Darío Lodeiros, \ + Alexandre Díaz, \ + Odoo Community Association (OCA)', + 'category': 'Generic Modules/Hotel Management', + 'depends': [ + 'project' + ], + 'external_dependencies': + {'python' : ['odoorpc']}, + 'license': "AGPL-3", + 'data': [ + 'views/hotel_node.xml', + 'views/hotel_node_user.xml', + 'views/hotel_node_group.xml', + 'security/hotel_node_security.xml', + 'security/ir.model.access.csv' + ], + 'demo': [], + 'auto_install': False, + 'installable': True +} diff --git a/hotel_node_master/models/__init__.py b/hotel_node_master/models/__init__.py new file mode 100644 index 000000000..58c8f42c1 --- /dev/null +++ b/hotel_node_master/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import hotel_node +from . import hotel_node_user +from . import hotel_node_group diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py new file mode 100644 index 000000000..f4410364e --- /dev/null +++ b/hotel_node_master/models/hotel_node.py @@ -0,0 +1,110 @@ +# 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 +import logging +import urllib.error +import odoorpc.odoo +from odoo.exceptions import ValidationError +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class HotelNode(models.Model): + + _inherit = ['project.project'] + + _description = 'Centralized hotel management features' + + active = fields.Boolean('Active', default=True, + help='The active field allows you to hide the \ + node without removing it.') + sequence = fields.Integer('Sequence', default=0, + help='Gives the sequence order when displaying the list of Nodes.') + + odoo_version = fields.Char() + odoo_host = fields.Char('Host', required=True, + help='Full URL to the host.') + odoo_db = fields.Char('Database Name', + help='Odoo database name.') + odoo_user = fields.Char('Username', + help='Odoo administration user.') + odoo_password = fields.Char('Password', + help='Odoo password.') + odoo_port = fields.Integer(string='TCP Port', default=443, + help='Specify the TCP port for the XML-RPC protocol.') + odoo_protocol = fields.Selection([('jsonrpc', 'jsonrpc'), ('jsonrpc+ssl', 'jsonrpc+ssl')], + 'Protocol', required=True, default='jsonrpc+ssl') + + user_ids = fields.One2many('hotel.node.user', 'node_id', + 'Users with access to this hotel') + + group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id', + string='Access Groups') + + @api.constrains('group_ids') + def _check_group_version(self): + """ + :raise: ValidationError + """ + for node in self: + domain = [('id', 'in', node.group_ids.ids), ('odoo_version', '!=', node.odoo_version)] + invalid_groups = self.env["hotel.node.group"].search(domain) + if len(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) + raise ValidationError(msg) + + _sql_constraints = [ + ('db_node_id_uniq', 'unique (odoo_db, id)', + 'The database name of the hotel must be unique within the Master Node!'), + ] + + @api.model + def create(self, vals): + """ + :param dict vals: the model's fields as a dictionary + :return: new hotel node record created. + :raise: ValidationError + """ + try: + noderpc = odoorpc.ODOO(vals['odoo_host'], vals['odoo_protocol'], vals['odoo_port']) + noderpc.login(vals['odoo_db'], vals['odoo_user'], vals['odoo_password']) + + vals.update({'odoo_version': noderpc.version}) + + remote_domain = [('model', '=', 'res.groups')] + remote_fields = ['complete_name', 'display_name'] + remote_groups = noderpc.env['ir.model.data'].search_read(remote_domain, remote_fields) + + noderpc.logout() + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + else: + master_groups = self.env["hotel.node.group"].search_read( + [('odoo_version', '=', vals['odoo_version'])], ['xml_id']) + + gui_ids = [r['id'] for r in master_groups] + xml_ids = [r['xml_id'] for r in master_groups] + + group_ids = [] + for group in remote_groups: + if group['complete_name'] in xml_ids: + idx = xml_ids.index(group['complete_name']) + group_ids.append((4, gui_ids[idx], 0)) + else: + group_ids.append((0, 0, { + 'name': group['display_name'], + 'xml_id': group['complete_name'], + 'odoo_version': vals['odoo_version'], + })) + vals.update({'group_ids': group_ids}) + + node_id = super().create(vals) + + return node_id diff --git a/hotel_node_master/models/hotel_node_group.py b/hotel_node_master/models/hotel_node_group.py new file mode 100644 index 000000000..9fbb313f2 --- /dev/null +++ b/hotel_node_master/models/hotel_node_group.py @@ -0,0 +1,36 @@ +# 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 logging +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class HotelNodeGroup(models.Model): + _name = "hotel.node.group" + _description = "Hotel Access Groups" + + active = fields.Boolean(default=True, + help="The active field allows you to hide the \ + group without removing it.") + sequence = fields.Integer(default=0, + help="Gives the sequence order when displaying the list of Groups.") + + name = fields.Char(required=True, translate=True) + node_ids = fields.Many2many('project.project', 'hotel_node_group_rel', 'group_id', 'node_id', + string='Hotels') + user_ids = fields.Many2many('hotel.node.user', 'hotel_node_user_group_rel', 'group_id', 'user_id', + string='Users') + # xml_id represents the complete module.name, xml_id = ("%s.%s" % (data['module'], data['name'])) + xml_id = fields.Char(string='External Identifier', required=True, + help="External Key/Identifier that can be used for " + "data integration with third-party systems") + odoo_version = fields.Char('Odoo Version') + + _sql_constraints = [ + ('xml_id_uniq', 'unique (odoo_version, xml_id)', + '_(The external identifier of the group must be unique within an Odoo version!') + ] diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py new file mode 100644 index 000000000..59beaf3a4 --- /dev/null +++ b/hotel_node_master/models/hotel_node_user.py @@ -0,0 +1,211 @@ +# 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 +import logging +import urllib.error +import odoorpc.odoo +from odoo.exceptions import ValidationError +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class HotelNodeUser(models.Model): + _name = "hotel.node.user" + _description = "Users with access to a hotel" + + def _default_groups(self): + pass + + active = fields.Boolean(default=True, + help="The active field allows you to hide the \ + user without removing it.") + sequence = fields.Integer(default=0, + help="Gives the sequence order when displaying the list of Users.") + + 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) + # Remote login for the hotels + login = fields.Char(require=True, + help="Used to log into the hotel") + # Password for login into the remote hotels + 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.") + # Remote user id for client-server understanding + remote_user_id = fields.Integer(require=True, invisible=True, copy=False, + help="ID of the target record in the remote database") + + # The same user can not be assigned to the same hotel + # _sql_constraints = [ + # ('user_id_node_id_key', 'UNIQUE (user_id, node_id)', + # 'You can not have two users with the same login in the same hotel!') + # ] + + # Users access control ... + group_ids = fields.Many2many('hotel.node.group', 'hotel_node_user_group_rel', 'user_id', 'group_id', + string='Groups', default=_default_groups, require=True, + help="Access rights for this user in this hotel.") + + # @api.constrains('user_id', 'node_id') + # def _check_user_node_unicity(self): + # if self.search_count([ + # ('user_id', '=', self.user_id.id), + # ('node_id', '=', self.node_id.id), + # ]) > 1: + # raise ValidationError(_("You can not have two users with the same login in the same hotel!")) + + # Constraints and onchanges + @api.constrains('group_ids') + 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)] + invalid_groups = self.env["hotel.node.group"].search(domain) + if len(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) + raise ValidationError(msg) + + @api.onchange('node_id') + def _onchange_node_id(self): + if self.node_id: + # TODO clean group_ids + # self.group_ids = [] + node = self.env["project.project"].search([('id', '=', self.node_id.id)]) + return {'domain': {'group_ids': [('odoo_version', '=', node.odoo_version)]}} + + return {'domain': {'group_ids': []}} + + @api.model + def create(self, vals): + """ + :param dict vals: the model's fields as a dictionary + :return: new hotel user record created. + :raise: ValidationError + """ + wdb.set_trace() + node = self.env["project.project"].browse(vals['node_id']) + + 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) + if len(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: + 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'], + } + + 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}) + + noderpc.logout() + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + _logger.error(err) + raise ValidationError(err) + else: + return super().create(vals) + + @api.multi + def write(self, vals): + """ + :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 node user is not allowed. Please create a new user instead.") + _logger.error(msg) + raise ValidationError(msg) + + node = rec.node_id + + 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) + if len(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: + 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 = {} + + if 'active' in vals: + remote_vals.update({'active': vals['active']}) + + 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 '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.logout() + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + _logger.error(err) + raise ValidationError(err) + + # TODO update record in central node only if the corresponding remote call was successfully + return super().write(vals) + + @api.multi + def unlink(self): + """ + :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]) + _logger.info('User #%s deleted remote res.users with ID: [%s]', + self._context.get('uid'), rec.remote_user_id) + noderpc.logout() + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + _logger.error(err) + raise ValidationError(err) + + return super().unlink() diff --git a/hotel_node_master/security/hotel_node_security.xml b/hotel_node_master/security/hotel_node_security.xml new file mode 100644 index 000000000..74979936c --- /dev/null +++ b/hotel_node_master/security/hotel_node_security.xml @@ -0,0 +1,3 @@ + + + diff --git a/hotel_node_master/security/ir.model.access.csv b/hotel_node_master/security/ir.model.access.csv new file mode 100644 index 000000000..97dd8b917 --- /dev/null +++ b/hotel_node_master/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_master/static/description/icon.png b/hotel_node_master/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0ea70ef8a4e53f1d5dd9072e3bcb0cdeaeeb0d61 GIT binary patch literal 22165 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

FASuT& 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 SqigB_0000 + + + + hotel.node.view.form + project.project + +

+ +
+ + +
+
+

+ +

+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + hotel.node.view.tree + project.project + + + + + + + + + + + + + + + Hotels + project.project + form + [] + kanban,form + main + + + + + + + + + + + + + + + + + + + + diff --git a/hotel_node_master/views/hotel_node_group.xml b/hotel_node_master/views/hotel_node_group.xml new file mode 100644 index 000000000..03362e259 --- /dev/null +++ b/hotel_node_master/views/hotel_node_group.xml @@ -0,0 +1,14 @@ + + + + hotel.node.group.tree + hotel.node.group + + + + + + + + + diff --git a/hotel_node_master/views/hotel_node_user.xml b/hotel_node_master/views/hotel_node_user.xml new file mode 100644 index 000000000..32c42a56f --- /dev/null +++ b/hotel_node_master/views/hotel_node_user.xml @@ -0,0 +1,53 @@ + + + + hotel.node.user.view.form + hotel.node.user + +
+ +
+ +
+
+
+ + + + + + + + + + + + + +
+
+
+
+ + + hotel.node.user.tree + hotel.node.user + + + + + + + + + + +
From c777ee97e9339a7a3d1df04c3d4e668687a6096b Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Fri, 21 Sep 2018 13:53:19 +0200 Subject: [PATCH 02/33] [WIP] Added Room Type and Rooms Master Node keeps minimal information of Hotel rooms for managing reservation through the reservation wizard. --- hotel_node_master/__manifest__.py | 1 + hotel_node_master/models/__init__.py | 2 + hotel_node_master/models/hotel_node.py | 22 +++++++ hotel_node_master/models/hotel_node_room.py | 26 +++++++++ .../models/hotel_node_room_type.py | 26 +++++++++ hotel_node_master/models/hotel_node_user.py | 13 ++--- hotel_node_master/views/hotel_node.xml | 21 ++++++- .../views/hotel_node_room_type.xml | 57 +++++++++++++++++++ hotel_node_master/views/hotel_node_user.xml | 19 ++++--- 9 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 hotel_node_master/models/hotel_node_room.py create mode 100644 hotel_node_master/models/hotel_node_room_type.py create mode 100644 hotel_node_master/views/hotel_node_room_type.xml diff --git a/hotel_node_master/__manifest__.py b/hotel_node_master/__manifest__.py index 48bde0927..59ea26b09 100644 --- a/hotel_node_master/__manifest__.py +++ b/hotel_node_master/__manifest__.py @@ -17,6 +17,7 @@ 'views/hotel_node.xml', 'views/hotel_node_user.xml', 'views/hotel_node_group.xml', + 'views/hotel_node_room_type.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 58c8f42c1..2555cdee7 100644 --- a/hotel_node_master/models/__init__.py +++ b/hotel_node_master/models/__init__.py @@ -3,3 +3,5 @@ from . import hotel_node from . import hotel_node_user from . import hotel_node_group +from . import hotel_node_room +from . import hotel_node_room_type diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index f4410364e..0c6a943aa 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -45,6 +45,9 @@ class HotelNode(models.Model): group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id', string='Access Groups') + room_type_ids = fields.One2many('hotel.node.room.type', 'node_id', + 'Rooms Type in this hotel') + @api.constrains('group_ids') def _check_group_version(self): """ @@ -77,15 +80,23 @@ class HotelNode(models.Model): vals.update({'odoo_version': noderpc.version}) + # Read remote Groups remote_domain = [('model', '=', 'res.groups')] remote_fields = ['complete_name', 'display_name'] remote_groups = noderpc.env['ir.model.data'].search_read(remote_domain, remote_fields) + # Read remote Room Type + remote_fields = ['name', 'active', 'sequence'] + remote_room_types = noderpc.env['hotel.room.type'].search_read([], remote_fields) + + wdb.set_trace() + noderpc.logout() except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) else: + # Process Groups master_groups = self.env["hotel.node.group"].search_read( [('odoo_version', '=', vals['odoo_version'])], ['xml_id']) @@ -105,6 +116,17 @@ class HotelNode(models.Model): })) vals.update({'group_ids': group_ids}) + # Process Room Type + room_type_ids = [] + for room_type in remote_room_types: + room_type_ids.append((0, 0, { + 'name': room_type['name'], + 'active': room_type['active'], + 'sequence': room_type['sequence'], + 'remote_room_type_id': room_type['id'], + })) + vals.update({'room_type_ids': room_type_ids}) + node_id = super().create(vals) return node_id diff --git a/hotel_node_master/models/hotel_node_room.py b/hotel_node_master/models/hotel_node_room.py new file mode 100644 index 000000000..5ed56424b --- /dev/null +++ b/hotel_node_master/models/hotel_node_room.py @@ -0,0 +1,26 @@ +# 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 logging +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class HotelNodeRoom(models.Model): + _name = "hotel.node.room" + _description = "Rooms" + + active = fields.Boolean(default=True) + sequence = fields.Integer(default=0) + + name = fields.Char(required=True, translate=True) + + remote_room_id = fields.Integer(require=True, invisible=True, copy=False, + help="ID of the target record in the remote database") + + room_type_id = fields.Many2one('hotel.node.room.type', 'Hotel Room Type') + + node_id = fields.Many2one('project.project', 'Hotel', required=True) diff --git a/hotel_node_master/models/hotel_node_room_type.py b/hotel_node_master/models/hotel_node_room_type.py new file mode 100644 index 000000000..0d07204cd --- /dev/null +++ b/hotel_node_master/models/hotel_node_room_type.py @@ -0,0 +1,26 @@ +# 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 logging +from odoo import models, fields, api, _ + +_logger = logging.getLogger(__name__) + + +class HotelNodeRoomType(models.Model): + _name = "hotel.node.room.type" + _description = "Room Type" + + active = fields.Boolean(default=True) + sequence = fields.Integer(default=0) + + name = fields.Char(required=True, translate=True) + + remote_room_type_id = fields.Integer(require=True, invisible=True, copy=False, + help="ID of the target record in the remote database") + + room_ids = fields.One2many('hotel.node.room', 'room_type_id', 'Rooms') + + node_id = fields.Many2one('project.project', 'Hotel', required=True) diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index 59beaf3a4..64d7b9c9c 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -75,8 +75,7 @@ class HotelNodeUser(models.Model): if self.node_id: # TODO clean group_ids # self.group_ids = [] - node = self.env["project.project"].search([('id', '=', self.node_id.id)]) - return {'domain': {'group_ids': [('odoo_version', '=', node.odoo_version)]}} + return {'domain': {'group_ids': [('odoo_version', '=', self.node_id.odoo_version)]}} return {'domain': {'group_ids': []}} @@ -87,7 +86,6 @@ class HotelNodeUser(models.Model): :return: new hotel user record created. :raise: ValidationError """ - wdb.set_trace() node = self.env["project.project"].browse(vals['node_id']) if 'group_ids' in vals: @@ -109,10 +107,11 @@ class HotelNodeUser(models.Model): 'login': vals['login'], } - 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) diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index 78f9b14fe..590d465db 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -48,12 +48,18 @@ + - + + + + + + @@ -104,6 +110,12 @@ res_model="project.project" view_mode="tree,form" /> + + + + + + + hotel.node.room.type.view.form + hotel.node.room.type + +
+ +
+ +
+
+

+ +

+
+ + + + + +
+
+
+
+ + + + + + + +
+
+
+
+ + + hotel.node.room.type.tree + hotel.node.room.type + + + + + + + + +
diff --git a/hotel_node_master/views/hotel_node_user.xml b/hotel_node_master/views/hotel_node_user.xml index 32c42a56f..7824a1f33 100644 --- a/hotel_node_master/views/hotel_node_user.xml +++ b/hotel_node_master/views/hotel_node_user.xml @@ -14,23 +14,25 @@
-
+ + + + + + - - - - - - + + @@ -46,7 +48,6 @@ - From 88aa81080ecb37769be183d7dad92451898c001d Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Fri, 21 Sep 2018 22:21:18 +0200 Subject: [PATCH 03/33] [WIP] Room Type Management Added room type and room restriction for management in central node. --- hotel_node_master/models/hotel_node.py | 2 +- hotel_node_master/models/hotel_node_room.py | 11 +++-- .../models/hotel_node_room_type.py | 44 ++++++++++++++++++- hotel_node_master/models/hotel_node_user.py | 4 +- .../views/hotel_node_room_type.xml | 2 +- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index 0c6a943aa..0e9888315 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -85,7 +85,7 @@ class HotelNode(models.Model): remote_fields = ['complete_name', 'display_name'] remote_groups = noderpc.env['ir.model.data'].search_read(remote_domain, remote_fields) - # Read remote Room Type + # Read remote Room Types remote_fields = ['name', 'active', 'sequence'] remote_room_types = noderpc.env['hotel.room.type'].search_read([], remote_fields) diff --git a/hotel_node_master/models/hotel_node_room.py b/hotel_node_master/models/hotel_node_room.py index 5ed56424b..55dcb5a39 100644 --- a/hotel_node_master/models/hotel_node_room.py +++ b/hotel_node_master/models/hotel_node_room.py @@ -16,11 +16,16 @@ class HotelNodeRoom(models.Model): active = fields.Boolean(default=True) sequence = fields.Integer(default=0) + @api.model + def _get_default_node_id(self): + return self.room_type_id.node_id + name = fields.Char(required=True, translate=True) - remote_room_id = fields.Integer(require=True, invisible=True, copy=False, + remote_room_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True, help="ID of the target record in the remote database") - room_type_id = fields.Many2one('hotel.node.room.type', 'Hotel Room Type') + room_type_id = fields.Many2one('hotel.node.room.type', 'Hotel Room Type', required=True) - node_id = fields.Many2one('project.project', 'Hotel', required=True) + node_id = fields.Many2one('project.project', 'Hotel', required=True, readonly=True, + default=_get_default_node_id) diff --git a/hotel_node_master/models/hotel_node_room_type.py b/hotel_node_master/models/hotel_node_room_type.py index 0d07204cd..6f5e69d7b 100644 --- a/hotel_node_master/models/hotel_node_room_type.py +++ b/hotel_node_master/models/hotel_node_room_type.py @@ -3,8 +3,10 @@ # Copyright 2018 Dario Lodeiros # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import wdb import logging from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) @@ -18,9 +20,47 @@ class HotelNodeRoomType(models.Model): name = fields.Char(required=True, translate=True) - remote_room_type_id = fields.Integer(require=True, invisible=True, copy=False, - help="ID of the target record in the remote database") + remote_room_type_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True, + help="ID of the target record in the remote database") room_ids = fields.One2many('hotel.node.room', 'room_type_id', 'Rooms') node_id = fields.Many2one('project.project', 'Hotel', required=True) + + @api.onchange('node_id') + def _onchange_node_id(self): + if self.node_id: + return {'domain': {'room_ids': [('room_ids', 'in', self.room_ids.ids)]}} + + return {'domain': {'room_ids': []}} + + @api.model + def create(self, vals): + """ + :param dict vals: the model's fields as a dictionary + :return: new hotel room type record created. + :raise: ValidationError + """ + _logger.warning("This fuction is not yet implemented.") + wdb.set_trace() + + @api.multi + def write(self, vals): + """ + :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 room type between nodes is not allowed. Please create a new room type instead.") + _logger.error(msg) + raise ValidationError(msg) + + _logger.warning("This fuction is not yet implemented.") + + @api.multi + def unlink(self): + """ + :raise: ValidationError + """ + _logger.warning("This fuction is not yet implemented.") diff --git a/hotel_node_master/models/hotel_node_user.py b/hotel_node_master/models/hotel_node_user.py index 64d7b9c9c..8c4c20ad2 100644 --- a/hotel_node_master/models/hotel_node_user.py +++ b/hotel_node_master/models/hotel_node_user.py @@ -36,7 +36,7 @@ class HotelNodeUser(models.Model): 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.") # Remote user id for client-server understanding - remote_user_id = fields.Integer(require=True, invisible=True, copy=False, + remote_user_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True, help="ID of the target record in the remote database") # The same user can not be assigned to the same hotel @@ -135,7 +135,7 @@ class HotelNodeUser(models.Model): """ for rec in self: if 'node_id' in vals and vals['node_id'] != rec.node_id.id: - msg = _("Changing a node user is not allowed. Please create a new user instead.") + msg = _("Changing a user between nodes is not allowed. Please create a new user instead.") _logger.error(msg) raise ValidationError(msg) diff --git a/hotel_node_master/views/hotel_node_room_type.xml b/hotel_node_master/views/hotel_node_room_type.xml index 1f6a87cf9..eb4e376c4 100644 --- a/hotel_node_master/views/hotel_node_room_type.xml +++ b/hotel_node_master/views/hotel_node_room_type.xml @@ -33,7 +33,7 @@ - From 10bb51cc408a3a635c0e8a9e0070e75bd407ada3 Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Sat, 22 Sep 2018 00:40:41 +0200 Subject: [PATCH 04/33] [WIP] Add SQL constraint Room Type must be unique within the Node --- hotel_node_master/models/hotel_node_room_type.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/hotel_node_master/models/hotel_node_room_type.py b/hotel_node_master/models/hotel_node_room_type.py index 6f5e69d7b..1da0ed93b 100644 --- a/hotel_node_master/models/hotel_node_room_type.py +++ b/hotel_node_master/models/hotel_node_room_type.py @@ -27,6 +27,11 @@ class HotelNodeRoomType(models.Model): node_id = fields.Many2one('project.project', 'Hotel', required=True) + _sql_constraints = [ + ('db_remote_room_type_id_uniq', 'unique (remote_room_type_id, node_id)', + 'The Room Type must be unique within the Node!'), + ] + @api.onchange('node_id') def _onchange_node_id(self): if self.node_id: @@ -41,8 +46,8 @@ class HotelNodeRoomType(models.Model): :return: new hotel room type record created. :raise: ValidationError """ - _logger.warning("This fuction is not yet implemented.") - wdb.set_trace() + _logger.warning("This fuction is not yet implemented for remote create.") + return super().create(vals) @api.multi def write(self, vals): @@ -56,11 +61,13 @@ class HotelNodeRoomType(models.Model): _logger.error(msg) raise ValidationError(msg) - _logger.warning("This fuction is not yet implemented.") + _logger.warning("This fuction is not yet implemented for remote update.") + return super().write(vals) @api.multi def unlink(self): """ :raise: ValidationError """ - _logger.warning("This fuction is not yet implemented.") + _logger.warning("This fuction is not yet implemented for remote delete.") + return super().unlink() From 2632a85263b645f8684a80a643baee17638a82ef Mon Sep 17 00:00:00 2001 From: Pablo Quesada Barriuso Date: Sat, 22 Sep 2018 00:41:27 +0200 Subject: [PATCH 05/33] [WIP] Synchronize manually from a remote node --- hotel_node_master/models/hotel_node.py | 84 ++++++++++++++++++-------- hotel_node_master/views/hotel_node.xml | 5 ++ 2 files changed, 64 insertions(+), 25 deletions(-) diff --git a/hotel_node_master/models/hotel_node.py b/hotel_node_master/models/hotel_node.py index 0e9888315..284bac917 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -80,25 +80,31 @@ class HotelNode(models.Model): vals.update({'odoo_version': noderpc.version}) - # Read remote Groups - remote_domain = [('model', '=', 'res.groups')] - remote_fields = ['complete_name', 'display_name'] - remote_groups = noderpc.env['ir.model.data'].search_read(remote_domain, remote_fields) - - # Read remote Room Types - remote_fields = ['name', 'active', 'sequence'] - remote_room_types = noderpc.env['hotel.room.type'].search_read([], remote_fields) - - wdb.set_trace() - - noderpc.logout() - except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) else: - # Process Groups + node_id = super().create(vals) + noderpc.logout() + return node_id + + @api.multi + def action_sync_from_node(self): + self.ensure_one() + try: + noderpc = odoorpc.ODOO(self.odoo_host, self.odoo_protocol, self.odoo_port) + 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) + master_groups = self.env["hotel.node.group"].search_read( - [('odoo_version', '=', vals['odoo_version'])], ['xml_id']) + [('odoo_version', '=', self.odoo_version)], ['xml_id']) gui_ids = [r['id'] for r in master_groups] xml_ids = [r['xml_id'] for r in master_groups] @@ -112,21 +118,49 @@ class HotelNode(models.Model): group_ids.append((0, 0, { 'name': group['display_name'], 'xml_id': group['complete_name'], - 'odoo_version': vals['odoo_version'], + 'odoo_version': self.odoo_version, })) vals.update({'group_ids': group_ids}) - # Process Room Type + self.write(vals) + + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + + try: + vals = {} + # import remote room types + fields = ['name', 'active', 'sequence', 'room_ids'] + remote_room_types = noderpc.env['hotel.room.type'].search_read([], fields) + + master_room_types = self.env["hotel.node.room.type"].search_read( + [('node_id', '=', self.id)], ['remote_room_type_id']) + + master_ids = [r['id'] for r in master_room_types] + remote_ids = [r['remote_room_type_id'] for r in master_room_types] + room_type_ids = [] for room_type in remote_room_types: - room_type_ids.append((0, 0, { - 'name': room_type['name'], - 'active': room_type['active'], - 'sequence': room_type['sequence'], - 'remote_room_type_id': room_type['id'], - })) + if room_type['id'] in remote_ids: + idx = remote_ids.index(room_type['id']) + room_type_ids.append((1, master_ids[idx], { + 'name': room_type['name'], + 'active': room_type['active'], + 'sequence': room_type['sequence'], + 'remote_room_type_id': room_type['id'], + })) + else: + room_type_ids.append((0, 0, { + 'name': room_type['name'], + 'active': room_type['active'], + 'sequence': room_type['sequence'], + 'remote_room_type_id': room_type['id'], + })) vals.update({'room_type_ids': room_type_ids}) - node_id = super().create(vals) + self.write(vals) - return node_id + except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + raise ValidationError(err) + + noderpc.logout() diff --git a/hotel_node_master/views/hotel_node.xml b/hotel_node_master/views/hotel_node.xml index 590d465db..23eb01416 100644 --- a/hotel_node_master/views/hotel_node.xml +++ b/hotel_node_master/views/hotel_node.xml @@ -12,6 +12,11 @@ name="" icon="fa-tasks"> +