mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
Merge pull request #55 from hootel/hotel_node_master
Refactoring for Odoo-Connector
This commit is contained in:
@@ -47,7 +47,7 @@ class HotelRoomType(models.Model):
|
|||||||
)
|
)
|
||||||
reservation_line_ids = reservation_line_ids['reservation_line_ids']
|
reservation_line_ids = reservation_line_ids['reservation_line_ids']
|
||||||
# QUESTION Why add [[5, 0, 0], ¿?
|
# QUESTION Why add [[5, 0, 0], ¿?
|
||||||
del reservation_line_ids[0]
|
# del reservation_line_ids[0]
|
||||||
|
|
||||||
return reservation_line_ids
|
return reservation_line_ids
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
from . import components
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizards
|
from . import wizards
|
||||||
|
|||||||
@@ -8,20 +8,24 @@
|
|||||||
Odoo Community Association (OCA)',
|
Odoo Community Association (OCA)',
|
||||||
'category': 'Generic Modules/Hotel Management',
|
'category': 'Generic Modules/Hotel Management',
|
||||||
'depends': [
|
'depends': [
|
||||||
'project'
|
'project',
|
||||||
|
'connector'
|
||||||
],
|
],
|
||||||
'external_dependencies':
|
'external_dependencies':
|
||||||
{'python' : ['odoorpc']},
|
{'python' : ['odoorpc']},
|
||||||
'license': "AGPL-3",
|
'license': "AGPL-3",
|
||||||
'data': [
|
'data': [
|
||||||
'wizards/wizard_hotel_node_reservation.xml',
|
'wizards/wizard_hotel_node_reservation.xml',
|
||||||
|
'views/node_backend_views.xml',
|
||||||
'views/hotel_node.xml',
|
'views/hotel_node.xml',
|
||||||
'views/hotel_node_user.xml',
|
'views/hotel_node_user.xml',
|
||||||
'views/hotel_node_group.xml',
|
'views/hotel_node_group.xml',
|
||||||
|
'views/hotel_node_group_remote.xml',
|
||||||
'views/hotel_node_room_type.xml',
|
'views/hotel_node_room_type.xml',
|
||||||
'views/inherited_res_partner_views.xml',
|
'views/inherited_res_partner_views.xml',
|
||||||
'security/hotel_node_security.xml',
|
'security/hotel_node_security.xml',
|
||||||
'security/ir.model.access.csv'
|
'security/ir.model.access.csv',
|
||||||
|
'data/menus.xml',
|
||||||
],
|
],
|
||||||
'demo': [],
|
'demo': [],
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
|
|||||||
9
hotel_node_master/components/__init__.py
Normal file
9
hotel_node_master/components/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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
|
||||||
107
hotel_node_master/components/backend_adapter.py
Normal file
107
hotel_node_master/components/backend_adapter.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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']
|
||||||
|
)
|
||||||
11
hotel_node_master/components/binder.py
Normal file
11
hotel_node_master/components/binder.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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',
|
||||||
|
]
|
||||||
9
hotel_node_master/components/core.py
Normal file
9
hotel_node_master/components/core.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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'
|
||||||
12
hotel_node_master/components/exporter.py
Normal file
12
hotel_node_master/components/exporter.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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'
|
||||||
11
hotel_node_master/components/importer.py
Normal file
11
hotel_node_master/components/importer.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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'
|
||||||
16
hotel_node_master/components/mapper.py
Normal file
16
hotel_node_master/components/mapper.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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'
|
||||||
21
hotel_node_master/data/menus.xml
Normal file
21
hotel_node_master/data/menus.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<menuitem id="menu_node_connector_root"
|
||||||
|
parent="connector.menu_connector_root"
|
||||||
|
name="Hotel Node"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_node_backend"
|
||||||
|
name="Backends"
|
||||||
|
sequence="1"
|
||||||
|
parent="menu_node_connector_root"
|
||||||
|
action="action_node_backend"/>
|
||||||
|
|
||||||
|
<menuitem id="menu_node_room_type"
|
||||||
|
name="Room Types"
|
||||||
|
sequence="1"
|
||||||
|
parent="menu_node_connector_root"
|
||||||
|
action="action_node_room_type"/>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# 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
|
||||||
from . import hotel_node_user
|
from . import hotel_node_user
|
||||||
from . import hotel_node_group
|
from . import hotel_node_group
|
||||||
|
from . import hotel_node_group_remote
|
||||||
from . import hotel_node_room
|
from . import hotel_node_room
|
||||||
from . import hotel_node_room_type
|
from . import hotel_node_room_type
|
||||||
from . import inherited_res_partner
|
from . import inherited_res_partner
|
||||||
|
from . import hotel_room_type
|
||||||
|
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ class HotelNode(models.Model):
|
|||||||
user_ids = fields.One2many('hotel.node.user', 'node_id',
|
user_ids = fields.One2many('hotel.node.user', 'node_id',
|
||||||
'Users with access to this hotel')
|
'Users with access to this hotel')
|
||||||
|
|
||||||
group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id',
|
# group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id',
|
||||||
string='Access Groups')
|
# 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',
|
room_type_ids = fields.One2many('hotel.node.room.type', 'node_id',
|
||||||
'Rooms Type in this hotel')
|
'Rooms Type in this hotel')
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ class HotelNodeGroup(models.Model):
|
|||||||
help="Gives the sequence order when displaying the list of Groups.")
|
help="Gives the sequence order when displaying the list of Groups.")
|
||||||
|
|
||||||
name = fields.Char(required=True, translate=True)
|
name = fields.Char(required=True, translate=True)
|
||||||
node_ids = fields.Many2many('project.project', 'hotel_node_group_rel', 'group_id', 'node_id',
|
# node_ids = fields.Many2many('project.project', 'hotel_node_group_rel', 'group_id', 'node_id',
|
||||||
string='Hotels')
|
# 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',
|
user_ids = fields.Many2many('hotel.node.user', 'hotel_node_user_group_rel', 'group_id', 'user_id',
|
||||||
string='Users')
|
string='Users')
|
||||||
# xml_id represents the complete module.name, xml_id = ("%s.%s" % (data['module'], data['name']))
|
# 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 = [
|
_sql_constraints = [
|
||||||
('xml_id_uniq', 'unique (odoo_version, xml_id)',
|
('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!')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
25
hotel_node_master/models/hotel_node_group_remote.py
Normal file
25
hotel_node_master/models/hotel_node_group_remote.py
Normal file
@@ -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!')
|
||||||
|
]
|
||||||
6
hotel_node_master/models/hotel_room_type/__init__.py
Normal file
6
hotel_node_master/models/hotel_room_type/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import common
|
||||||
|
from . import exporter
|
||||||
|
from . import importer
|
||||||
83
hotel_node_master/models/hotel_room_type/common.py
Normal file
83
hotel_node_master/models/hotel_room_type/common.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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()
|
||||||
33
hotel_node_master/models/hotel_room_type/exporter.py
Normal file
33
hotel_node_master/models/hotel_room_type/exporter.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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)
|
||||||
47
hotel_node_master/models/hotel_room_type/importer.py
Normal file
47
hotel_node_master/models/hotel_room_type/importer.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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}
|
||||||
4
hotel_node_master/models/node_backend/__init__.py
Normal file
4
hotel_node_master/models/node_backend/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import common
|
||||||
52
hotel_node_master/models/node_backend/common.py
Normal file
52
hotel_node_master/models/node_backend/common.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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)
|
||||||
4
hotel_node_master/models/node_binding/__init__.py
Normal file
4
hotel_node_master/models/node_binding/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import common
|
||||||
22
hotel_node_master/models/node_binding/common.py
Normal file
22
hotel_node_master/models/node_binding/common.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# 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.'),
|
||||||
|
]
|
||||||
23
hotel_node_master/tests/__init__.py
Normal file
23
hotel_node_master/tests/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2017 Solucións Aloxa S.L. <info@aloxa.eu>
|
||||||
|
# Alexandre Díaz <dev@redneboa.es>
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from . import test_hotel_node_master
|
||||||
38
hotel_node_master/tests/common.py
Normal file
38
hotel_node_master/tests/common.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2017 Solucións Aloxa S.L. <info@aloxa.eu>
|
||||||
|
# Alexandre Díaz <dev@redneboa.es>
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
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()
|
||||||
30
hotel_node_master/tests/test_hotel_node_master.py
Normal file
30
hotel_node_master/tests/test_hotel_node_master.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# OpenERP, Open Source Management Solution
|
||||||
|
# Copyright (C) 2017 Solucións Aloxa S.L. <info@aloxa.eu>
|
||||||
|
# Alexandre Díaz <dev@redneboa.es>
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
from .common import TestHotel
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
class TestHotelNodeMaster(TestHotel):
|
||||||
|
|
||||||
|
def test_wizard_hotel_node_reservation(self):
|
||||||
|
pass
|
||||||
14
hotel_node_master/views/hotel_node_group_remote.xml
Normal file
14
hotel_node_master/views/hotel_node_group_remote.xml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hotel_node_group_remote_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">hotel.node.group.remote.tree</field>
|
||||||
|
<field name="model">hotel.node.group.remote</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="node_id"/>
|
||||||
|
<field name="group_id"/>
|
||||||
|
<field name="remote_group_id"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
81
hotel_node_master/views/node_backend_views.xml
Normal file
81
hotel_node_master/views/node_backend_views.xml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="node_backend_views_form" model="ir.ui.view">
|
||||||
|
<field name="name">node.backend.form</field>
|
||||||
|
<field name="model">node.backend</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Hotel Node Backend">
|
||||||
|
<header>
|
||||||
|
<button name="test_connection"
|
||||||
|
type="object"
|
||||||
|
class="oe_highlight"
|
||||||
|
string="Test Connection"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<label for="name" class="oe_edit_only"/>
|
||||||
|
<h1>
|
||||||
|
<field name="name" class="oe_inline" />
|
||||||
|
</h1>
|
||||||
|
<group name="channel" string="Node Configuration">
|
||||||
|
<notebook>
|
||||||
|
<page string="API" name="api">
|
||||||
|
<group colspan="4" col="4">
|
||||||
|
<field name="address" colspan="2"/>
|
||||||
|
<field name="db" colspan="2"/>
|
||||||
|
<field name="user" colspan="2"/>
|
||||||
|
<field name="passwd" password="1" colspan="2"/>
|
||||||
|
<field name="port" colspan="2"/>
|
||||||
|
<field name="protocol" colspan="2"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page name="import" string="Imports">
|
||||||
|
<group>
|
||||||
|
<label string="Import Room Types" class="oe_inline"/>
|
||||||
|
<div>
|
||||||
|
<button name="import_room_types"
|
||||||
|
type="object"
|
||||||
|
class="oe_highlight"
|
||||||
|
string="Import in background"
|
||||||
|
confirm="Synchronizing a node automatically updates Room Types in the Central Node. Do you want to proceed?"/>
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="node_backend_views_tree" model="ir.ui.view">
|
||||||
|
<field name="name">node.backend.tree</field>
|
||||||
|
<field name="model">node.backend</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Node Backend">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="address"/>
|
||||||
|
<field name="db"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_node_backend" model="ir.actions.act_window">
|
||||||
|
<field name="name">Hotel Node Backends</field>
|
||||||
|
<field name="res_model">node.backend</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="view_id" ref="node_backend_views_tree"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_node_room_type" model="ir.actions.act_window">
|
||||||
|
<field name="name">Hotel Node Room Types</field>
|
||||||
|
<field name="res_model">node.room.type</field>
|
||||||
|
<field name="view_type">form</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<!--<field name="view_id" ref="node_backend_views_tree"/>-->
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
@@ -33,122 +33,152 @@ class HotelNodeReservationWizard(models.TransientModel):
|
|||||||
today = fields.Date.context_today(self.with_context())
|
today = fields.Date.context_today(self.with_context())
|
||||||
return (fields.Date.from_string(today) + timedelta(days=1)).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
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)
|
node_id = fields.Many2one('project.project', 'Hotel', required=True, default=_default_node_id)
|
||||||
partner_id = fields.Many2one('res.partner', string="Customer", required=True)
|
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',
|
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)
|
price_total = fields.Float(string='Total Price', compute='_compute_price_total', store=True)
|
||||||
|
|
||||||
@api.constrains('room_type_wizard_ids.room_qty')
|
# FIXED @constrains parameter 'room_type_wizard_ids.room_qty' is not a field name
|
||||||
def _check_room_type_wizard_ids(self):
|
# @api.constrains('room_type_wizard_ids')
|
||||||
"""
|
# def _check_room_type_wizard_total_qty(self):
|
||||||
:raise: ValidationError
|
# for rec in self:
|
||||||
"""
|
# total_qty = 0
|
||||||
total_qty = 0
|
# for rec_room_type in rec.room_type_wizard_ids:
|
||||||
for rec in self.room_type_wizard_ids:
|
# total_qty += rec_room_type.room_qty
|
||||||
total_qty += rec.room_qty
|
#
|
||||||
|
# if total_qty == 0:
|
||||||
if total_qty == 0:
|
# msg = _("It is not possible to create the reservation.") + " " + \
|
||||||
msg = _("It is not possible to create the reservation.") + " " + \
|
# _("Maybe you forgot adding the quantity to at least one type of room?.")
|
||||||
_("Maybe you forgot adding the quantity to at least one type of room?.")
|
# raise ValidationError(msg)
|
||||||
raise ValidationError(msg)
|
|
||||||
|
|
||||||
@api.depends('room_type_wizard_ids.price_total')
|
@api.depends('room_type_wizard_ids.price_total')
|
||||||
def _compute_price_total(self):
|
def _compute_price_total(self):
|
||||||
_logger.info('_compute_price_total for wizard %s', self.id)
|
for rec in self:
|
||||||
self.price_total = 0.0
|
_logger.info('_compute_price_total for wizard %s', rec.id)
|
||||||
for rec in self.room_type_wizard_ids:
|
rec.price_total = 0.0
|
||||||
self.price_total += rec.price_total
|
for rec_room_type in rec.room_type_wizard_ids:
|
||||||
|
rec.price_total += rec_room_type.price_total
|
||||||
|
|
||||||
@api.onchange('node_id')
|
@api.onchange('node_id')
|
||||||
def _onchange_node_id(self):
|
def _onchange_node_id(self):
|
||||||
self.ensure_one()
|
|
||||||
if self.node_id:
|
if self.node_id:
|
||||||
_logger.info('_onchange_node_id(self): %s', self)
|
_logger.info('_onchange_node_id(self): %s', self)
|
||||||
# TODO Save your credentials (session)
|
# 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')
|
@api.model
|
||||||
def _onchange_dates(self):
|
def create(self, vals):
|
||||||
self.ensure_one()
|
# TODO review node.room.type.wizard @api.constrains('room_qty')
|
||||||
_logger.info('_onchange_dates(self): %s', self)
|
from pprint import pprint
|
||||||
|
|
||||||
# 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()
|
|
||||||
try:
|
try:
|
||||||
noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port)
|
node = self.env["project.project"].browse(vals['node_id'])
|
||||||
noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password)
|
|
||||||
|
|
||||||
# prepare required fields for hotel folio
|
noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port)
|
||||||
remote_partner_id = noderpc.env['res.partner'].search([('email', '=', self.partner_id.email)]).pop()
|
noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password)
|
||||||
vals = {
|
|
||||||
|
# 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,
|
'partner_id': remote_partner_id,
|
||||||
}
|
})
|
||||||
# prepare hotel folio room_lines
|
|
||||||
|
# prepare hotel.folio.room_lines
|
||||||
room_lines = []
|
room_lines = []
|
||||||
for rec in self.room_type_wizard_ids:
|
for cmds in vals['room_type_wizard_ids']:
|
||||||
for x in range(rec.room_qty):
|
# cmds is a list of triples: [0, 'virtual_1008', {'checkin': '2018-11-05', ...
|
||||||
# vals_reservation_lines = {
|
room_type_wizard_values = cmds[2]
|
||||||
# 'partner_id': remote_partner_id,
|
remote_room_type_id = self.env['hotel.node.room.type'].search([
|
||||||
# 'room_type_id': rec.room_type_id.remote_room_type_id,
|
('id', '=', room_type_wizard_values['room_type_id'])
|
||||||
# }
|
]).remote_room_type_id
|
||||||
# add discount
|
# prepare room_lines a number of times `room_qty` times
|
||||||
# reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines(
|
for room in range(room_type_wizard_values['room_qty']):
|
||||||
# rec.checkin,
|
# prepare hotel.reservation.reservation_line_ids
|
||||||
# (fields.Date.from_string(rec.checkout) - fields.Date.from_string(rec.checkin)).days,
|
reservation_line_cmds = []
|
||||||
# vals_reservation_lines
|
for room_type_line_cmds in room_type_wizard_values['room_type_line_ids']:
|
||||||
# ) # [[5, 0, 0], ¿?
|
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_lines.append((0, False, {
|
||||||
'room_type_id': rec.room_type_id.remote_room_type_id,
|
'room_type_id': remote_room_type_id,
|
||||||
'checkin': rec.checkin,
|
'checkin': room_type_wizard_values['checkin'],
|
||||||
'checkout': rec.checkout,
|
'checkout': room_type_wizard_values['checkout'],
|
||||||
# 'reservation_line_ids': reservation_line_ids['reservation_line_ids'],
|
'reservation_line_ids': reservation_line_cmds,
|
||||||
}))
|
}))
|
||||||
vals.update({'room_lines': room_lines})
|
remote_vals.update({'room_lines': room_lines})
|
||||||
|
|
||||||
from pprint import pprint
|
pprint(remote_vals)
|
||||||
pprint(vals)
|
# if total_qty == 0:
|
||||||
|
# msg = _("It is not possible to create the reservation.") + " " + \
|
||||||
folio_id = noderpc.env['hotel.folio'].create(vals)
|
# _("Maybe you forgot adding the quantity to at least one type of room?.")
|
||||||
_logger.info('User #%s created a hotel.folio with ID: [%s]',
|
# 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)
|
self._context.get('uid'), folio_id)
|
||||||
|
|
||||||
noderpc.logout()
|
noderpc.logout()
|
||||||
|
|
||||||
# return self._open_wizard_action_search()
|
|
||||||
|
|
||||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||||
|
_logger.error(err)
|
||||||
raise ValidationError(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
|
@api.multi
|
||||||
def _open_wizard_action_search(self):
|
def _open_wizard_action_search(self):
|
||||||
@@ -167,37 +197,48 @@ class NodeRoomTypeWizard(models.TransientModel):
|
|||||||
_name = "node.room.type.wizard"
|
_name = "node.room.type.wizard"
|
||||||
_description = "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_id = fields.Many2one('hotel.node.room.type', 'Rooms Type')
|
||||||
room_type_availability = fields.Integer('Availability', compute="_compute_restrictions", readonly=True, store=True)
|
room_type_availability = fields.Integer('Availability', compute="_compute_restrictions", readonly=True, store=True)
|
||||||
room_qty = fields.Integer('Quantity', default=0)
|
room_qty = fields.Integer('Quantity', default=0)
|
||||||
room_type_line_ids = fields.One2many('node.room.type.line.wizard', 'node_room_type_line_wizard_id',
|
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)
|
checkin = fields.Date('Check In', default=_default_checkin, required=True)
|
||||||
checkout = fields.Date('Check Out', required=True)
|
checkout = fields.Date('Check Out', default=_default_checkout, required=True)
|
||||||
nights = fields.Integer('Nights', compute="_compute_nights", readonly=True, store=True)
|
nights = fields.Integer('Nights', compute='_compute_nights', readonly=True)
|
||||||
|
|
||||||
min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True, store=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", readonly=True, store=True)
|
||||||
price_unit = fields.Float(string='Room Price', compute="_compute_restrictions", store=True)
|
|
||||||
discount = fields.Float(string='Discount (%)', default=0.0)
|
discount = fields.Float(string='Discount (%)', default=0.0)
|
||||||
price_total = fields.Float(string='Total Price', compute='_compute_price_total', readonly=True, store=True)
|
price_total = fields.Float(string='Total Price', compute='_compute_price_total', readonly=True, store=True)
|
||||||
|
|
||||||
@api.constrains('room_qty')
|
@api.constrains('room_qty')
|
||||||
def _check_room_qty(self):
|
def _check_room_qty(self):
|
||||||
"""
|
# At least one model cache has been invalidated, signaling through the database.
|
||||||
:raise: ValidationError
|
|
||||||
"""
|
|
||||||
total_qty = 0
|
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if (rec.room_type_availability < rec.room_qty) or (rec.room_qty > 0 and rec.nights < rec.min_stay):
|
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.") + " " + \
|
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)
|
_("Please, review room type %s between %s and %s.") % (rec.room_type_id.name, rec.checkin, rec.checkout)
|
||||||
_logger.warning(msg)
|
_logger.warning(msg)
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
total_qty += rec.room_qty
|
|
||||||
|
|
||||||
@api.depends('room_qty', 'price_unit', 'discount')
|
@api.depends('room_qty', 'price_unit', 'discount')
|
||||||
def _compute_price_total(self):
|
def _compute_price_total(self):
|
||||||
@@ -215,38 +256,38 @@ class NodeRoomTypeWizard(models.TransientModel):
|
|||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.checkin and rec.checkout:
|
if rec.checkin and rec.checkout:
|
||||||
try:
|
try:
|
||||||
node_id = rec.node_reservation_wizard_id.node_id
|
|
||||||
# TODO Load your credentials (session) ... should be faster?
|
# TODO Load your credentials (session) ... should be faster?
|
||||||
noderpc = odoorpc.ODOO(node_id.odoo_host, node_id.odoo_protocol, node_id.odoo_port)
|
noderpc = odoorpc.ODOO(rec.node_id.odoo_host, rec.node_id.odoo_protocol, rec.node_id.odoo_port)
|
||||||
noderpc.login(node_id.odoo_db, node_id.odoo_user, node_id.odoo_password)
|
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.room_type_availability = noderpc.env['hotel.room.type'].get_room_type_availability(
|
||||||
rec.checkin,
|
rec.checkin,
|
||||||
rec.checkout,
|
rec.checkout,
|
||||||
rec.room_type_id.remote_room_type_id)
|
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(
|
_logger.info('_compute_restrictions [price_unit] for room type %s', rec.room_type_id)
|
||||||
# rec.checkin,
|
rec.room_type_line_ids = noderpc.env['hotel.room.type'].get_room_type_price_unit(
|
||||||
# rec.checkout,
|
rec.checkin,
|
||||||
# rec.room_type_id.remote_room_type_id)
|
rec.checkout,
|
||||||
cmds = []
|
rec.room_type_id.remote_room_type_id)
|
||||||
for x in range(rec.nights):
|
# cmds = []
|
||||||
cmds.append((0, False, {
|
# for x in range(rec.nights):
|
||||||
'node_room_type_line_wizard_id': rec.id,
|
# cmds.append((0, False, {
|
||||||
'date': (fields.Date.from_string(rec.checkin) + timedelta(days=x)).strftime(
|
# 'date': (fields.Date.from_string(rec.checkin) + timedelta(days=x)).strftime(
|
||||||
DEFAULT_SERVER_DATE_FORMAT),
|
# DEFAULT_SERVER_DATE_FORMAT),
|
||||||
'price': 0.0,
|
# 'price': 11.50,
|
||||||
}))
|
# }))
|
||||||
rec.room_type_line_ids = cmds
|
# from pprint import pprint
|
||||||
|
# pprint(cmds)
|
||||||
|
# rec.room_type_line_ids = cmds
|
||||||
rec.price_unit = sum(rec.room_type_line_ids.mapped('price'))
|
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.min_stay = noderpc.env['hotel.room.type'].get_room_type_restrictions(
|
||||||
rec.checkin,
|
rec.checkin,
|
||||||
rec.checkout,
|
rec.checkout,
|
||||||
rec.room_type_id.remote_room_type_id)
|
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()
|
noderpc.logout()
|
||||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||||
@@ -265,12 +306,11 @@ class NodeRoomTypeWizard(models.TransientModel):
|
|||||||
|
|
||||||
@api.onchange('checkin', 'checkout')
|
@api.onchange('checkin', 'checkout')
|
||||||
def _onchange_dates(self):
|
def _onchange_dates(self):
|
||||||
_logger.info('+++ _onchange_dates for room type %s +++', self.room_type_id)
|
_logger.info('_onchange_dates for room type: %s', self.room_type_id)
|
||||||
|
if not self.checkin:
|
||||||
self.checkin = self._default_checkin() \
|
self.checkin = self._default_checkin()
|
||||||
if not self.checkin else fields.Date.from_string(self.checkin)
|
if not self.checkout:
|
||||||
self.checkout = self._default_checkout() \
|
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):
|
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(
|
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
|
return self._context.get('node_id') or None
|
||||||
|
|
||||||
node_id = fields.Many2one('project.project', 'Hotel', default=_default_node_id)
|
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')
|
folio = fields.Char('Folio Number')
|
||||||
partner_id = fields.Many2one('res.partner', string="Customer")
|
partner_id = fields.Many2one('res.partner', string="Customer")
|
||||||
email = fields.Char('E-mail', related='partner_id.email')
|
email = fields.Char('E-mail', related='partner_id.email')
|
||||||
@@ -317,18 +358,34 @@ class NodeSearchWizard(models.TransientModel):
|
|||||||
if self.checkin:
|
if self.checkin:
|
||||||
domain.append(('checkin', '=', 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:
|
if not folio_ids:
|
||||||
raise UserError(_("No reservations found."))
|
raise UserError(_("No reservations found for [%s].") % domain)
|
||||||
|
|
||||||
noderpc.logout()
|
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:
|
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||||
raise ValidationError(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
|
@api.multi
|
||||||
def _open_wizard_action_edit(self, folio_id):
|
def _open_wizard_action_edit(self, folio_id):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|||||||
@@ -13,22 +13,29 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<group attrs="{'invisible':[('node_id','=',False)]}">
|
<group attrs="{'invisible':[('node_id','=',False)]}">
|
||||||
<group name="dates">
|
<!--<group name="dates">-->
|
||||||
<field name="checkin" required="1" widget="date" />
|
<!--<field name="checkin" required="1" widget="date" />-->
|
||||||
<field name="checkout" required="1" widget="date" />
|
<!--<field name="checkout" required="1" widget="date" />-->
|
||||||
</group>
|
<!--</group>-->
|
||||||
<group>
|
<group>
|
||||||
<field name="partner_id"/>
|
<field name="partner_id"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="room_type_wizard_ids" colspan="2">
|
<group name="room_type_wizard_ids" colspan="2">
|
||||||
<field name="room_type_wizard_ids" nolabel="1">
|
<field name="room_type_wizard_ids" nolabel="1"
|
||||||
|
context="{'default_node_reservation_wizard_id': active_id}">
|
||||||
<tree editable="bottom" create="false" delete="false"
|
<tree editable="bottom" create="false" delete="false"
|
||||||
decoration-muted="room_type_availability == 0">
|
decoration-muted="room_type_availability == 0">
|
||||||
<field name="node_reservation_wizard_id" invisible="1"/>
|
<field name="node_reservation_wizard_id" invisible="1"/>
|
||||||
|
<field name="node_id" invisible="1"/>
|
||||||
<field name="room_type_id" string="Room Type" readonly="1" force_save="1"/>
|
<field name="room_type_id" string="Room Type" readonly="1" force_save="1"/>
|
||||||
<field name="room_type_availability" readonly="1"/>
|
<field name="room_type_availability" readonly="1"/>
|
||||||
<field name="room_qty"/>
|
<field name="room_qty"/>
|
||||||
<field name="room_type_line_ids" invisible="1"/>
|
<field name="room_type_line_ids" widget="one2many_list" invisible="1">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="date"/>
|
||||||
|
<field name="price"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
<field name="checkin" widget="date"/>
|
<field name="checkin" widget="date"/>
|
||||||
<field name="checkout" widget="date"/>
|
<field name="checkout" widget="date"/>
|
||||||
<field name="nights"/>
|
<field name="nights"/>
|
||||||
@@ -91,11 +98,9 @@
|
|||||||
<field name="model">node.folio.wizard</field>
|
<field name="model">node.folio.wizard</field>
|
||||||
<field name="inherit_id" ref="hotel_node_reservation_wizard_view_form" />
|
<field name="inherit_id" ref="hotel_node_reservation_wizard_view_form" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//group[@name='dates']" position="replace">
|
<field name="partner_id" position="after">
|
||||||
<field name="folio_id" invisible="1"/>
|
<field name="folio_id" invisible="1"/>
|
||||||
<field name="folio_name"/>
|
<field name="folio_name"/>
|
||||||
</xpath>
|
|
||||||
<field name="partner_id" position="after">
|
|
||||||
<field name="email"/>
|
<field name="email"/>
|
||||||
<field name="internal_comment"/>
|
<field name="internal_comment"/>
|
||||||
</field>
|
</field>
|
||||||
|
|||||||
Reference in New Issue
Block a user