diff --git a/hotel_node_helper/models/inherited_hotel_room_type.py b/hotel_node_helper/models/inherited_hotel_room_type.py index 1d82b85de..3247a2035 100644 --- a/hotel_node_helper/models/inherited_hotel_room_type.py +++ b/hotel_node_helper/models/inherited_hotel_room_type.py @@ -47,7 +47,7 @@ class HotelRoomType(models.Model): ) reservation_line_ids = reservation_line_ids['reservation_line_ids'] # QUESTION Why add [[5, 0, 0], ¿? - del reservation_line_ids[0] + # del reservation_line_ids[0] return reservation_line_ids 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 ab8d8f1b7..20aec9808 100644 --- a/hotel_node_master/__manifest__.py +++ b/hotel_node_master/__manifest__.py @@ -8,20 +8,24 @@ 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', + 'views/hotel_node_group_remote.xml', '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..7fbb884f1 --- /dev/null +++ b/hotel_node_master/components/backend_adapter.py @@ -0,0 +1,107 @@ +# 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 node!") + 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): + _logger.warning("_delete_room_type(%s, room_type_id) is not yet implemented.", self) + return True + # 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..b3c34937f --- /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 BaseNodeConnectorComponent(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..c8a55a4f0 --- /dev/null +++ b/hotel_node_master/data/menus.xml @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/hotel_node_master/models/__init__.py b/hotel_node_master/models/__init__.py index 9d741ca83..f1f67dff9 100644 --- a/hotel_node_master/models/__init__.py +++ b/hotel_node_master/models/__init__.py @@ -1,8 +1,13 @@ # 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 +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_node.py b/hotel_node_master/models/hotel_node.py index b3bed344c..5da56c387 100644 --- a/hotel_node_master/models/hotel_node.py +++ b/hotel_node_master/models/hotel_node.py @@ -41,8 +41,10 @@ class HotelNode(models.Model): 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') + # group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id', + # string='Access Groups') + group_ids = fields.One2many('hotel.node.group.remote', 'node_id', + 'Access Groups') room_type_ids = fields.One2many('hotel.node.room.type', 'node_id', 'Rooms Type in this hotel') diff --git a/hotel_node_master/models/hotel_node_group.py b/hotel_node_master/models/hotel_node_group.py index 9fbb313f2..a1100b0d2 100644 --- a/hotel_node_master/models/hotel_node_group.py +++ b/hotel_node_master/models/hotel_node_group.py @@ -20,8 +20,10 @@ class HotelNodeGroup(models.Model): 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') + # node_ids = fields.Many2many('project.project', 'hotel_node_group_rel', 'group_id', 'node_id', + # string='Hotels') + remote_group_ids = fields.One2many('hotel.node.group.remote', 'group_id', + 'Access Groups') 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'])) @@ -32,5 +34,6 @@ class HotelNodeGroup(models.Model): _sql_constraints = [ ('xml_id_uniq', 'unique (odoo_version, xml_id)', - '_(The external identifier of the group must be unique within an Odoo version!') + 'The external identifier of the group must be unique within an Odoo version!') ] + diff --git a/hotel_node_master/models/hotel_node_group_remote.py b/hotel_node_master/models/hotel_node_group_remote.py new file mode 100644 index 000000000..768b2b1f9 --- /dev/null +++ b/hotel_node_master/models/hotel_node_group_remote.py @@ -0,0 +1,25 @@ +# 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 HotelNodeGroupRemote(models.Model): + _name = "hotel.node.group.remote" + _description = "Remote Access Groups IDs" + + node_id = fields.Many2one('project.project', 'Hotel', required=True) + group_id = fields.Many2one('hotel.node.group', 'Group', require=True) + name = fields.Char(related='group_id.name') + remote_group_id = fields.Integer(require=True, copy=False, readonly=True, + help="ID of the target record in the remote database") + + _sql_constraints = [ + ('node_remote_group_id_uniq', 'unique (node_id, remote_group_id)', + 'The remote identifier of the group must be unique within a Node!') + ] 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..d1c8f0774 --- /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 NodeBindingRoomTypeListener(Component): + _name = 'node.binding.room.type.listener' + _inherit = 'base.connector.listener' + _apply_on = ['node.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..0a506fa6f --- /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_type(self, binding): + return self.backend_adapter.modify_room_type( + binding.external_id, + binding.name, + binding.room_ids + ) + + @api.model + def delete_room_type(self, binding): + return self.backend_adapter.delete_room_type(binding.external_id) + + @api.model + def create_room_type(self, binding): + external_id = self.backend_adapter.create_room_type( + 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..2013622ae --- /dev/null +++ b/hotel_node_master/models/hotel_room_type/importer.py @@ -0,0 +1,47 @@ +# 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_type = node_room_type_obj.search([('external_id', '=', rec['id'])], + limit=1) + # NEED REVIEW Import a record triggers a room_type.write / room_type.create back to the node + if room_type: + room_type.write(map_record.values()) + else: + room_type.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..b44de8c37 --- /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.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/tests/__init__.py b/hotel_node_master/tests/__init__.py new file mode 100644 index 000000000..71f06506f --- /dev/null +++ b/hotel_node_master/tests/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Solucións Aloxa S.L. +# Alexandre Díaz +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## +from . import test_hotel_node_master diff --git a/hotel_node_master/tests/common.py b/hotel_node_master/tests/common.py new file mode 100644 index 000000000..285651553 --- /dev/null +++ b/hotel_node_master/tests/common.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Solucións Aloxa S.L. +# Alexandre Díaz +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## +from odoo.tests import common +import logging +_logger = logging.getLogger(__name__) + + +class TestHotel(common.SavepointCase): + + @classmethod + def _init_mock_hotel(cls): + return True + + @classmethod + def setUpClass(cls): + super(TestHotel, cls).setUpClass() + + cls._init_mock_hotel() diff --git a/hotel_node_master/tests/test_hotel_node_master.py b/hotel_node_master/tests/test_hotel_node_master.py new file mode 100644 index 000000000..2b63fd301 --- /dev/null +++ b/hotel_node_master/tests/test_hotel_node_master.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2017 Solucións Aloxa S.L. +# Alexandre Díaz +# +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +############################################################################## +from .common import TestHotel +from odoo.exceptions import ValidationError + + +class TestHotelNodeMaster(TestHotel): + + def test_wizard_hotel_node_reservation(self): + pass diff --git a/hotel_node_master/views/hotel_node_group_remote.xml b/hotel_node_master/views/hotel_node_group_remote.xml new file mode 100644 index 000000000..c8c56c1d6 --- /dev/null +++ b/hotel_node_master/views/hotel_node_group_remote.xml @@ -0,0 +1,14 @@ + + + + hotel.node.group.remote.tree + hotel.node.group.remote + + + + + + + + + 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..44feb57cb --- /dev/null +++ b/hotel_node_master/views/node_backend_views.xml @@ -0,0 +1,81 @@ + + + + + node.backend.form + node.backend + +
+
+
+ + +
+
+
+ + + node.backend.tree + node.backend + + + + + + + + + + + Hotel Node Backends + node.backend + form + tree,form + + + + + Hotel Node Room Types + node.room.type + form + tree,form + + + +
diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.py b/hotel_node_master/wizards/wizard_hotel_node_reservation.py index a736c9e1c..5b492412b 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.py +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.py @@ -33,122 +33,152 @@ class HotelNodeReservationWizard(models.TransientModel): today = fields.Date.context_today(self.with_context()) return (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT) - @api.model - def _default_room_type_wizard_ids(self): - node_id = self.env['project.project'].browse(self._context.get('node_id')) - checkin = self._default_checkin() - checkout = self._default_checkout() - room_type_wizard_ids = node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { - 'room_type_id': room_type_id.id, - 'room_type_availability': 0, - 'checkin': checkin, - 'checkout': checkout, - })) - return room_type_wizard_ids - 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=_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", default=_default_room_type_wizard_ids) + string="Room Types") price_total = fields.Float(string='Total Price', compute='_compute_price_total', store=True) - @api.constrains('room_type_wizard_ids.room_qty') - 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) + # FIXED @constrains parameter 'room_type_wizard_ids.room_qty' is not a field name + # @api.constrains('room_type_wizard_ids') + # def _check_room_type_wizard_total_qty(self): + # for rec in self: + # total_qty = 0 + # for rec_room_type in rec.room_type_wizard_ids: + # total_qty += rec_room_type.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) - self.price_total = 0.0 - for rec in self.room_type_wizard_ids: - self.price_total += rec.price_total + for rec in self: + _logger.info('_compute_price_total for wizard %s', rec.id) + rec.price_total = 0.0 + for rec_room_type in rec.room_type_wizard_ids: + rec.price_total += rec_room_type.price_total @api.onchange('node_id') def _onchange_node_id(self): - self.ensure_one() if self.node_id: _logger.info('_onchange_node_id(self): %s', self) # TODO Save your credentials (session) + _logger.info('_compute_room_types for node %s', self.node_id) + cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, { + 'node_id': self.node_id.id, + 'room_type_id': room_type_id.id, + 'checkin': self._default_checkin(), + 'checkout': self._default_checkout(), + })) + self.room_type_wizard_ids = cmds - @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._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) - - # update room_type_wizard_ids - for rec in self.room_type_wizard_ids: - if self.checkin != rec.checkin: - _logger.warning('_onchange_dates need new data for room_type: %s', rec.room_type_id) - - @api.multi - def create_node_reservation(self): - self.ensure_one() + @api.model + def create(self, vals): + # TODO review node.room.type.wizard @api.constrains('room_qty') + from pprint import pprint 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) + node = self.env["project.project"].browse(vals['node_id']) - # prepare required fields for hotel folio - remote_partner_id = noderpc.env['res.partner'].search([('email', '=', self.partner_id.email)]).pop() - vals = { + noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port) + noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password) + + # prepare required fields for hotel.folio + remote_vals = {} + partner = self.env["res.partner"].browse(vals['partner_id']) + remote_partner_id = noderpc.env['res.partner'].search([('email', '=', partner.email)]).pop() + # TODO create partner if does not exist in remote node + remote_vals.update({ 'partner_id': remote_partner_id, - } - # prepare hotel folio room_lines + }) + + # prepare hotel.folio.room_lines room_lines = [] - 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': rec.room_type_id.remote_room_type_id, - # } - # add discount - # reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines( - # rec.checkin, - # (fields.Date.from_string(rec.checkout) - fields.Date.from_string(rec.checkin)).days, - # vals_reservation_lines - # ) # [[5, 0, 0], ¿? + for cmds in vals['room_type_wizard_ids']: + # cmds is a list of triples: [0, 'virtual_1008', {'checkin': '2018-11-05', ... + room_type_wizard_values = cmds[2] + remote_room_type_id = self.env['hotel.node.room.type'].search([ + ('id', '=', room_type_wizard_values['room_type_id']) + ]).remote_room_type_id + # prepare room_lines a number of times `room_qty` times + for room in range(room_type_wizard_values['room_qty']): + # prepare hotel.reservation.reservation_line_ids + reservation_line_cmds = [] + for room_type_line_cmds in room_type_wizard_values['room_type_line_ids']: + reservation_line = room_type_line_cmds[2] + reservation_line_cmds.append((0, False, { + 'date': reservation_line['date'], + 'price': reservation_line['price'], + })) + # add discount ¿? room_lines.append((0, False, { - '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'], + 'room_type_id': remote_room_type_id, + 'checkin': room_type_wizard_values['checkin'], + 'checkout': room_type_wizard_values['checkout'], + 'reservation_line_ids': reservation_line_cmds, })) - vals.update({'room_lines': room_lines}) + remote_vals.update({'room_lines': room_lines}) - from pprint import pprint - pprint(vals) - - folio_id = noderpc.env['hotel.folio'].create(vals) - _logger.info('User #%s created a hotel.folio with ID: [%s]', + pprint(remote_vals) + # 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) + folio_id = noderpc.env['hotel.folio'].create(remote_vals) + _logger.info('User #%s created a remote hotel.folio with ID: [%s]', self._context.get('uid'), folio_id) noderpc.logout() - # return self._open_wizard_action_search() - 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 create_node_reservation(self): + _logger.info('# TODO: return a wizard and preview the reservation') + # self.ensure_one() + # # NOTE This function is executed __after__ create(self, vals) where _compute_restrictions are executed again + # 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) + # + # # prepare required fields for hotel folio + # 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 rec in self.room_type_wizard_ids: + # for x in range(rec.room_qty): + # # prepare hotel reservation lines with details by day + # reservation_line_cmds = rec.room_type_line_ids.mapped(lambda reservation_line: (0, False, { + # 'date': reservation_line.date, + # 'price': reservation_line.price, + # })) + # # add discount + # room_lines.append((0, False, { + # 'room_type_id': rec.room_type_id.remote_room_type_id, + # 'checkin': rec.checkin, + # 'checkout': rec.checkout, + # 'reservation_line_ids': reservation_line_cmds, + # })) + # vals.update({'room_lines': room_lines}) + # + # from pprint import pprint + # pprint(vals) + # + # folio_id = noderpc.env['hotel.folio'].create(vals) + # _logger.info('User #%s created a hotel.folio with ID: [%s]', + # self._context.get('uid'), folio_id) + # + # noderpc.logout() + # except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: + # raise ValidationError(err) @api.multi def _open_wizard_action_search(self): @@ -167,37 +197,48 @@ class NodeRoomTypeWizard(models.TransientModel): _name = "node.room.type.wizard" _description = "Node Room Type Wizard" - node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard') + @api.model + def _default_node_id(self): + return self._context.get('node_id') or None + + @api.model + 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 _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_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard', + ondelete = 'cascade', required = True) + node_id = fields.Many2one('project.project', 'Hotel', default=_default_node_id, required=True) room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type') room_type_availability = fields.Integer('Availability', compute="_compute_restrictions", readonly=True, store=True) room_qty = fields.Integer('Quantity', default=0) room_type_line_ids = fields.One2many('node.room.type.line.wizard', 'node_room_type_line_wizard_id', - compute="_compute_restrictions", string="Room type detail per day.") + string="Room type detail per day") - checkin = fields.Date('Check In', required=True) - checkout = fields.Date('Check Out', required=True) - nights = fields.Integer('Nights', compute="_compute_nights", readonly=True, store=True) + checkin = fields.Date('Check In', default=_default_checkin, required=True) + checkout = fields.Date('Check Out', default=_default_checkout, required=True) + nights = fields.Integer('Nights', compute='_compute_nights', readonly=True) min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True, store=True) - # price_unit indicates Room Price x Nights - price_unit = fields.Float(string='Room Price', compute="_compute_restrictions", store=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', readonly=True, store=True) @api.constrains('room_qty') def _check_room_qty(self): - """ - :raise: ValidationError - """ - total_qty = 0 + # At least one model cache has been invalidated, signaling through the database. 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_id.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): @@ -215,38 +256,38 @@ class NodeRoomTypeWizard(models.TransientModel): for rec in self: if rec.checkin and rec.checkout: try: - node_id = rec.node_reservation_wizard_id.node_id # TODO Load your credentials (session) ... should be faster? - noderpc = odoorpc.ODOO(node_id.odoo_host, node_id.odoo_protocol, node_id.odoo_port) - noderpc.login(node_id.odoo_db, node_id.odoo_user, node_id.odoo_password) + 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.warning('_compute_restrictions [availability: %s] for room type %s', rec.room_type_availability, rec.room_type_id) - # rec.room_type_line_ids = noderpc.env['hotel.room.type'].get_room_type_price_unit( - # rec.checkin, - # rec.checkout, - # rec.room_type_id.remote_room_type_id) - cmds = [] - for x in range(rec.nights): - cmds.append((0, False, { - 'node_room_type_line_wizard_id': rec.id, - 'date': (fields.Date.from_string(rec.checkin) + timedelta(days=x)).strftime( - DEFAULT_SERVER_DATE_FORMAT), - 'price': 0.0, - })) - rec.room_type_line_ids = cmds + _logger.info('_compute_restrictions [price_unit] for room type %s', rec.room_type_id) + rec.room_type_line_ids = noderpc.env['hotel.room.type'].get_room_type_price_unit( + rec.checkin, + rec.checkout, + rec.room_type_id.remote_room_type_id) + # cmds = [] + # for x in range(rec.nights): + # cmds.append((0, False, { + # 'date': (fields.Date.from_string(rec.checkin) + timedelta(days=x)).strftime( + # DEFAULT_SERVER_DATE_FORMAT), + # 'price': 11.50, + # })) + # from pprint import pprint + # pprint(cmds) + # rec.room_type_line_ids = cmds rec.price_unit = sum(rec.room_type_line_ids.mapped('price')) - _logger.warning('_compute_restrictions [price_unit: %s] for room type %s', rec.price_unit, rec.room_type_id) + _logger.info('_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) - _logger.warning('_compute_restrictions [min days: %s] for room type %s', rec.min_stay, rec.room_type_id) noderpc.logout() except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: @@ -265,12 +306,11 @@ class NodeRoomTypeWizard(models.TransientModel): @api.onchange('checkin', 'checkout') def _onchange_dates(self): - _logger.info('+++ _onchange_dates for room type %s +++', self.room_type_id) - - 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) + _logger.info('_onchange_dates for room type: %s', self.room_type_id) + if not self.checkin: + self.checkin = self._default_checkin() + if not self.checkout: + self.checkout = self._default_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( @@ -296,6 +336,7 @@ class NodeSearchWizard(models.TransientModel): return self._context.get('node_id') or None node_id = fields.Many2one('project.project', 'Hotel', default=_default_node_id) + node_folio_wizard_id = fields.Many2one('node.folio.wizard') folio = fields.Char('Folio Number') partner_id = fields.Many2one('res.partner', string="Customer") email = fields.Char('E-mail', related='partner_id.email') @@ -317,18 +358,34 @@ class NodeSearchWizard(models.TransientModel): if self.checkin: domain.append(('checkin', '=', self.checkin)) - folio_id = noderpc.env['hotel.folio'].search(domain) + folio_ids = noderpc.env['hotel.folio'].search(domain) - if not folio_id: - raise UserError(_("No reservations found.")) + if not folio_ids: + raise UserError(_("No reservations found for [%s].") % domain) noderpc.logout() - # TODO Need to manage more than one folio - return self._open_wizard_action_edit(folio_id.pop()) + + if len(folio_ids) > 1: + # TODO Need to manage more than one folio + return self._open_wizard_action_select(folio_ids) + + return self._open_wizard_action_edit(folio_ids.pop()) except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err: raise ValidationError(err) + @api.multi + def _open_wizard_action_select(self, folio_ids): + self.ensure_one() + return { + 'name': _('Hotel Reservation Wizard Select'), + 'type': 'ir.actions.act_window', + 'res_model': 'node.folio.wizard', + 'view_id': self.env.ref('hotel_node_master.hotel_node_reservation_wizard_view_tree', False).id, + 'view_type': 'tree', + 'view_mode': 'tree', + } + @api.multi def _open_wizard_action_edit(self, folio_id): self.ensure_one() diff --git a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml index 828811d85..094127437 100644 --- a/hotel_node_master/wizards/wizard_hotel_node_reservation.xml +++ b/hotel_node_master/wizards/wizard_hotel_node_reservation.xml @@ -13,22 +13,29 @@ - - - - + + + + - + + - + + + + + + @@ -91,11 +98,9 @@ node.folio.wizard - + - -