diff --git a/hotel_node_master/__init__.py b/hotel_node_master/__init__.py index 7588e52c8..9144a9ab1 100644 --- a/hotel_node_master/__init__.py +++ b/hotel_node_master/__init__.py @@ -1,4 +1,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - +from . import components from . import models from . import wizards diff --git a/hotel_node_master/__manifest__.py b/hotel_node_master/__manifest__.py index a936b4ebe..20aec9808 100644 --- a/hotel_node_master/__manifest__.py +++ b/hotel_node_master/__manifest__.py @@ -8,13 +8,15 @@ Odoo Community Association (OCA)', 'category': 'Generic Modules/Hotel Management', 'depends': [ - 'project' + 'project', + 'connector' ], 'external_dependencies': {'python' : ['odoorpc']}, 'license': "AGPL-3", 'data': [ 'wizards/wizard_hotel_node_reservation.xml', + 'views/node_backend_views.xml', 'views/hotel_node.xml', 'views/hotel_node_user.xml', 'views/hotel_node_group.xml', @@ -22,7 +24,8 @@ 'views/hotel_node_room_type.xml', 'views/inherited_res_partner_views.xml', 'security/hotel_node_security.xml', - 'security/ir.model.access.csv' + 'security/ir.model.access.csv', + 'data/menus.xml', ], 'demo': [], 'auto_install': False, diff --git a/hotel_node_master/components/__init__.py b/hotel_node_master/components/__init__.py new file mode 100644 index 000000000..fbeb3cccb --- /dev/null +++ b/hotel_node_master/components/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import core +from . import backend_adapter +from . import binder +from . import importer +from . import exporter +from . import mapper diff --git a/hotel_node_master/components/backend_adapter.py b/hotel_node_master/components/backend_adapter.py new file mode 100644 index 000000000..2195bbd7f --- /dev/null +++ b/hotel_node_master/components/backend_adapter.py @@ -0,0 +1,105 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import odoorpc +import logging +from odoo.addons.component.core import AbstractComponent +from odoo.addons.queue_job.exception import RetryableJobError +_logger = logging.getLogger(__name__) + + +class NodeLogin(object): + def __init__(self, address, protocol, port, db, user, passwd): + self.address = address + self.protocol = protocol + self.port = port + self.db = db + self.user = user + self.passwd = passwd + +class NodeServer(object): + def __init__(self, login_data): + self._server = None + self._login_data = login_data + + def __enter__(self): + # we do nothing, api is lazy + return self + + def __exit__(self, type, value, traceback): + if self._server is not None: + self.close() + + @property + def server(self): + if self._server is None: + try: + self._server = odoorpc.ODOO(self._login_data.address, + self._login_data.protocol, + self._login_data.port) + self._server.login(self._login_data.db, + self._login_data.user, + self._login_data.passwd) + except Exception: + self._server = None + raise RetryableJobError("Can't connect with channel!") + return self._server + + def close(self): + self._server.logout() + self._server = None + +class HotelNodeInterfaceAdapter(AbstractComponent): + _name = 'hotel.node.interface.adapter' + _inherit = ['base.backend.adapter', 'base.node.connector'] + _usage = 'backend.adapter' + + def create_room_type(self, name, room_ids): + raise NotImplementedError + + def modify_room_type(self, room_type_id, name, room_ids): + raise NotImplementedError + + def delete_room_type(self, room_type_id): + raise NotImplementedError + + def fetch_room_types(self): + raise NotImplementedError + + @property + def _server(self): + try: + node_server = getattr(self.work, 'node_api') + except AttributeError: + raise AttributeError( + 'You must provide a node_api attribute with a ' + 'WuBookServer instance to be able to use the ' + 'Backend Adapter.' + ) + return node_server.server + +class HotelNodeAdapter(AbstractComponent): + _name = 'hotel.node.adapter' + _inherit = 'hotel.node.interface.adapter' + + # === ROOMS + def create_room_type(self, name, room_ids): + return self._server.env['hotel.room.type'].create({ + 'name': name + }) + + def modify_room_type(self, room_type_id, name, rooms_id): + return self._server.env['hotel.room.type'].write( + room_type_id, + { + 'name': name + }) + + def delete_room_type(self, room_type_id): + return self._server.env['hotel.room.type'].unlink(room_type_id) + + def fetch_room_types(self): + return self._server.env['hotel.room.type'].search_read( + [], + ['name'] + ) diff --git a/hotel_node_master/components/binder.py b/hotel_node_master/components/binder.py new file mode 100644 index 000000000..3a2e68676 --- /dev/null +++ b/hotel_node_master/components/binder.py @@ -0,0 +1,11 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import Component + +class NodeConnectorModelBinder(Component): + _name = 'node.connector.binder' + _inherit = ['base.binder', 'base.node.connector'] + _apply_on = [ + 'node.room.type', + ] diff --git a/hotel_node_master/components/core.py b/hotel_node_master/components/core.py new file mode 100644 index 000000000..54c4ca360 --- /dev/null +++ b/hotel_node_master/components/core.py @@ -0,0 +1,9 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + +class BaseHotelChannelConnectorComponent(AbstractComponent): + _name = 'base.node.connector' + _inherit = 'base.connector' + _collection = 'node.backend' diff --git a/hotel_node_master/components/exporter.py b/hotel_node_master/components/exporter.py new file mode 100644 index 000000000..401f761a0 --- /dev/null +++ b/hotel_node_master/components/exporter.py @@ -0,0 +1,12 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import AbstractComponent + +_logger = logging.getLogger(__name__) + +class NodeExporter(AbstractComponent): + _name = 'node.exporter' + _inherit = ['base.exporter', 'base.node.connector'] + _usage = 'node.exporter' diff --git a/hotel_node_master/components/importer.py b/hotel_node_master/components/importer.py new file mode 100644 index 000000000..73f80a4e3 --- /dev/null +++ b/hotel_node_master/components/importer.py @@ -0,0 +1,11 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import AbstractComponent, Component +_logger = logging.getLogger(__name__) + +class NodeImporter(AbstractComponent): + _name = 'node.importer' + _inherit = ['base.importer', 'base.node.connector'] + _usage = 'node.importer' diff --git a/hotel_node_master/components/mapper.py b/hotel_node_master/components/mapper.py new file mode 100644 index 000000000..7759923ca --- /dev/null +++ b/hotel_node_master/components/mapper.py @@ -0,0 +1,16 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.addons.component.core import AbstractComponent + + +class NodeImportMapper(AbstractComponent): + _name = 'node.import.mapper' + _inherit = ['base.node.connector', 'base.import.mapper'] + _usage = 'import.mapper' + + +class NodeExportMapper(AbstractComponent): + _name = 'node.export.mapper' + _inherit = ['base.node.connector', 'base.export.mapper'] + _usage = 'export.mapper' diff --git a/hotel_node_master/data/menus.xml b/hotel_node_master/data/menus.xml new file mode 100644 index 000000000..7263501b4 --- /dev/null +++ b/hotel_node_master/data/menus.xml @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/hotel_node_master/models/__init__.py b/hotel_node_master/models/__init__.py index e7d98eb6e..f1f67dff9 100644 --- a/hotel_node_master/models/__init__.py +++ b/hotel_node_master/models/__init__.py @@ -1,5 +1,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import node_backend +from . import node_binding from . import hotel_node from . import hotel_node_user from . import hotel_node_group @@ -7,3 +9,5 @@ from . import hotel_node_group_remote from . import hotel_node_room from . import hotel_node_room_type from . import inherited_res_partner +from . import hotel_room_type + diff --git a/hotel_node_master/models/hotel_room_type/__init__.py b/hotel_node_master/models/hotel_room_type/__init__.py new file mode 100644 index 000000000..c7a2c2b51 --- /dev/null +++ b/hotel_node_master/models/hotel_room_type/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common +from . import exporter +from . import importer diff --git a/hotel_node_master/models/hotel_room_type/common.py b/hotel_node_master/models/hotel_room_type/common.py new file mode 100644 index 000000000..465f4e488 --- /dev/null +++ b/hotel_node_master/models/hotel_room_type/common.py @@ -0,0 +1,83 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo import api, models, fields, _ +from odoo.addons.queue_job.job import job, related_action +from odoo.addons.component.core import Component +from odoo.addons.component_event import skip_if +_logger = logging.getLogger(__name__) + +class NodeRoomType(models.Model): + _name = 'node.room.type' + _inherit = 'node.binding' + _description = 'Node Hotel Room Type' + + name = fields.Char(required=True, translate=True) + room_ids = fields.Integer() + # fields.One2many('node.room', 'room_type_id', 'Rooms') + active = fields.Boolean(default=True) + sequence = fields.Integer(default=0) + + @job(default_channel='root.channel') + @api.model + def create_room_type(self): + with self.backend_id.work_on(self._name) as work: + exporter = work.component(usage='node.room.type.exporter') + return exporter.create_room_type(self) + + @job(default_channel='root.channel') + @api.model + def modify_room_type(self): + with self.backend_id.work_on(self._name) as work: + exporter = work.component(usage='node.room.type.exporter') + return exporter.modify_room_type(self) + + @job(default_channel='root.channel') + @api.model + def delete_room_type(self): + with self.backend_id.work_on(self._name) as work: + exporter = work.component(usage='node.room.type.exporter') + return exporter.delete_room_type(self) + + @job(default_channel='root.channel') + @api.model + def fetch_room_types(self, backend): + with backend.work_on(self._name) as work: + importer = work.component(usage='node.room.type.importer') + return importer.fetch_room_types() + +class NodeRoomTypeAdapter(Component): + _name = 'node.room.type.adapter' + _inherit = 'hotel.node.adapter' + _apply_on = 'node.room.type' + + def create_room_type(self, name, room_ids): + return super().create_room_type(name, room_ids) + + def modify_room_type(self, room_type_id, name, room_ids): + return super().modify_room_type(room_type_id, name, room_ids) + + def delete_room_type(self, room_type_id): + return super().delete_room_type(room_type_id) + + def fetch_room_types(self): + return super().fetch_room_types() + + +class ChannelBindingRoomTypeListener(Component): + _name = 'node.binding.room.type.listener' + _inherit = 'base.connector.listener' + _apply_on = ['node.hotel.room.type'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_create(self, record, fields=None): + record.create_room_type() + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_unlink(self, record, fields=None): + record.delete_room_type() + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.modify_room_type() diff --git a/hotel_node_master/models/hotel_room_type/exporter.py b/hotel_node_master/models/hotel_room_type/exporter.py new file mode 100644 index 000000000..77e1ad25a --- /dev/null +++ b/hotel_node_master/models/hotel_room_type/exporter.py @@ -0,0 +1,33 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import Component +from odoo import api, _ +_logger = logging.getLogger(__name__) + +class NodeRoomTypeExporter(Component): + _name = 'node.room.type.exporter' + _inherit = 'node.exporter' + _apply_on = ['node.room.type'] + _usage = 'node.room.type.exporter' + + @api.model + def modify_room(self, binding): + return self.backend_adapter.modify_room( + binding.room_type_id, + binding.name, + binding.room_ids + ) + + @api.model + def delete_room(self, binding): + return self.backend_adapter.delete_room(binding.room_type_id) + + @api.model + def create_room(self, binding): + external_id = self.backend_adapter.create_room( + binding.name, + binding.room_ids + ) + self.binder.bind(external_id, binding) diff --git a/hotel_node_master/models/hotel_room_type/importer.py b/hotel_node_master/models/hotel_room_type/importer.py new file mode 100644 index 000000000..c6e7d9a00 --- /dev/null +++ b/hotel_node_master/models/hotel_room_type/importer.py @@ -0,0 +1,46 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping +from odoo import fields, api, _ +_logger = logging.getLogger(__name__) + + +class HotelRoomTypeImporter(Component): + _name = 'node.room.type.importer' + _inherit = 'node.importer' + _apply_on = ['node.room.type'] + _usage = 'node.room.type.importer' + + @api.model + def fetch_room_types(self): + results = self.backend_adapter.fetch_room_types() + room_type_mapper = self.component(usage='import.mapper', + model_name='node.room.type') + + node_room_type_obj = self.env['node.room.type'] + for rec in results: + map_record = room_type_mapper.map_record(rec) + room_bind = node_room_type_obj.search([('external_id', '=', rec['id'])], + limit=1) + if room_bind: + room_bind.write(map_record.values()) + else: + room_bind.create(map_record.values(for_create=True)) + + +class NodeRoomTypeImportMapper(Component): + _name = 'node.room.type.import.mapper' + _inherit = 'node.import.mapper' + _apply_on = 'node.room.type' + + direct = [ + ('id', 'external_id'), + ('name', 'name'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} diff --git a/hotel_node_master/models/node_backend/__init__.py b/hotel_node_master/models/node_backend/__init__.py new file mode 100644 index 000000000..257ab04fc --- /dev/null +++ b/hotel_node_master/models/node_backend/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common diff --git a/hotel_node_master/models/node_backend/common.py b/hotel_node_master/models/node_backend/common.py new file mode 100644 index 000000000..df47ba4c4 --- /dev/null +++ b/hotel_node_master/models/node_backend/common.py @@ -0,0 +1,52 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from contextlib import contextmanager +from odoo import models, api, fields +from ...components.backend_adapter import NodeLogin, NodeServer + +class NodeBackend(models.Model): + _name = 'node.backend' + _description = 'Hotel Node Backend' + _inherit = 'connector.backend' + + name = fields.Char('Name') + address = fields.Char('Host', required=True, + help='Full URL to the host.') + db = fields.Char('Database Name', + help='Odoo database name.') + user = fields.Char('Username', + help='Odoo administration user.') + passwd = fields.Char('Password', + help='Odoo password.') + port = fields.Integer(string='TCP Port', default=443, + help='Specify the TCP port for the XML-RPC protocol.') + protocol = fields.Selection([('jsonrpc', 'jsonrpc'), ('jsonrpc+ssl', 'jsonrpc+ssl')], + 'Protocol', required=True, default='jsonrpc+ssl') + odoo_version = fields.Char() + + @contextmanager + @api.multi + def work_on(self, model_name, **kwargs): + self.ensure_one() + node_login = NodeLogin( + self.address, + self.protocol, + self.port, + self.db, + self.user, + self.passwd) + with NodeServer(node_login) as node_api: + _super = super(NodeBackend, self) + with _super.work_on(model_name, node_api=node_api, **kwargs) as work: + yield work + + @api.multi + def test_connection(self): + pass + + @api.multi + def import_room_types(self): + node_room_type_obj = self.env['node.room.type'] + for backend in self: + node_room_type_obj.with_delay().fetch_room_types(backend) diff --git a/hotel_node_master/models/node_binding/__init__.py b/hotel_node_master/models/node_binding/__init__.py new file mode 100644 index 000000000..257ab04fc --- /dev/null +++ b/hotel_node_master/models/node_binding/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common diff --git a/hotel_node_master/models/node_binding/common.py b/hotel_node_master/models/node_binding/common.py new file mode 100644 index 000000000..f2bf75638 --- /dev/null +++ b/hotel_node_master/models/node_binding/common.py @@ -0,0 +1,22 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api + + +class NodeBinding(models.AbstractModel): + _name = 'node.binding' + _inherit = 'external.binding' + _description = 'Hotel Node Connector Binding (abstract)' + + external_id = fields.Integer() + backend_id = fields.Many2one( + comodel_name='node.backend', + string='Hotel Node Connector Backend', + required=True, + ondelete='restrict') + + _sql_constraints = [ + ('backend_external_id_uniq', 'unique(backend_id, external_id)', + 'A binding already exists with the same Backend ID.'), + ] diff --git a/hotel_node_master/views/node_backend_views.xml b/hotel_node_master/views/node_backend_views.xml new file mode 100644 index 000000000..eb6057c99 --- /dev/null +++ b/hotel_node_master/views/node_backend_views.xml @@ -0,0 +1,72 @@ + + + + + node.backend.form + node.backend + +
+
+
+ + +
+
+
+ + + node.backend.tree + node.backend + + + + + + + + + + + Hotel Node Backends + node.backend + form + tree,form + + + +