mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
Merge branch 'hotel_node_master' into 11.0
This commit is contained in:
@@ -255,7 +255,7 @@ class MassiveChangesWizard(models.TransientModel):
|
||||
diff_days = abs((date_end_dt - date_start_dt).days) + 1
|
||||
wedays = (record.dmo, record.dtu, record.dwe, record.dth,
|
||||
record.dfr, record.dsa, record.dsu)
|
||||
room_types = record.room_type_id if record.applied_on == '1' \
|
||||
room_types = record.room_type_ids if record.applied_on == '1' \
|
||||
else hotel_room_type_obj.search([])
|
||||
|
||||
for i in range(0, diff_days):
|
||||
|
||||
@@ -1 +1 @@
|
||||
from . import wizard_reservation
|
||||
from . import wizard_reservation
|
||||
|
||||
22
hotel_node_helper/README.rst
Normal file
22
hotel_node_helper/README.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
=================
|
||||
Hotel Node Helper
|
||||
=================
|
||||
|
||||
This module is for providing helper functions to the hotel node master module.
|
||||
|
||||
**Try me on Runbot**
|
||||
|
||||
**Known issues / Roadmap**
|
||||
|
||||
...
|
||||
|
||||
**Bug Tracker**
|
||||
|
||||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here.
|
||||
|
||||
Credits
|
||||
|
||||
Contributors
|
||||
|
||||
Maintainer
|
||||
|
||||
3
hotel_node_helper/__init__.py
Normal file
3
hotel_node_helper/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
21
hotel_node_helper/__manifest__.py
Normal file
21
hotel_node_helper/__manifest__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
'name': 'Hotel Node Helper',
|
||||
'summary': """Provides helper functions to the hotel node master module""",
|
||||
'version': '0.1.0',
|
||||
'author': 'Pablo Q. Barriuso, \
|
||||
Darío Lodeiros, \
|
||||
Alexandre Díaz, \
|
||||
Odoo Community Association (OCA)',
|
||||
'category': 'Generic Modules/Hotel Management',
|
||||
'depends': [
|
||||
'hotel'
|
||||
],
|
||||
'license': "AGPL-3",
|
||||
'data': [
|
||||
'security/hotel_node_security.xml',
|
||||
'security/ir.model.access.csv'
|
||||
],
|
||||
'demo': [],
|
||||
'auto_install': False,
|
||||
'installable': True
|
||||
}
|
||||
3
hotel_node_helper/models/__init__.py
Normal file
3
hotel_node_helper/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import inherited_hotel_room_type
|
||||
41
hotel_node_helper/models/inherited_hotel_room_type.py
Normal file
41
hotel_node_helper/models/inherited_hotel_room_type.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import wdb
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class HotelRoomType(models.Model):
|
||||
|
||||
_inherit = 'hotel.room.type'
|
||||
|
||||
@api.model
|
||||
def check_availability_room_ids(self, dfrom, dto,
|
||||
room_type_id=False, notthis=[]):
|
||||
"""
|
||||
Check availability for all or specific room types between dates
|
||||
@return: A list of `ids` with free rooms
|
||||
"""
|
||||
free_rooms = super().check_availability_room(dfrom, dto, room_type_id, notthis)
|
||||
return free_rooms.ids
|
||||
|
||||
@api.model
|
||||
def get_room_type_availability(self, dfrom, dto, room_type_id):
|
||||
free_rooms = self.check_availability_room(dfrom, dto)
|
||||
availability_real = self.env['hotel.room'].search_count([
|
||||
('id', 'in', free_rooms.ids),
|
||||
('room_type_id', '=', room_type_id),
|
||||
])
|
||||
availability_plan = self.env['hotel.room.type.availability'].search_read([
|
||||
('date', '>=', dfrom),
|
||||
('date', '<', dto),
|
||||
('room_type_id', '=', room_type_id),
|
||||
|
||||
], ['avail']) or float('inf')
|
||||
|
||||
if isinstance(availability_plan, list):
|
||||
availability_plan = min([r['avail'] for r in availability_plan])
|
||||
|
||||
return min(availability_real, availability_plan)
|
||||
3
hotel_node_helper/security/hotel_node_security.xml
Normal file
3
hotel_node_helper/security/hotel_node_security.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="0">
|
||||
</odoo>
|
||||
1
hotel_node_helper/security/ir.model.access.csv
Normal file
1
hotel_node_helper/security/ir.model.access.csv
Normal file
@@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
BIN
hotel_node_helper/static/description/icon.png
Normal file
BIN
hotel_node_helper/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
43
hotel_node_master/README.rst
Normal file
43
hotel_node_master/README.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
=================
|
||||
Hotel Node Master
|
||||
=================
|
||||
|
||||
This module is for providing centralized hotel management features for hootel.
|
||||
|
||||
You can manage:
|
||||
|
||||
- Node connection data
|
||||
- Remote users and access groups
|
||||
- Hotel reservations
|
||||
|
||||
**Installation**
|
||||
|
||||
To install this module, you need to:
|
||||
|
||||
**External dependencies**
|
||||
- OdooRPC, a Python package providing an easy way to pilot your Odoo servers through RPC
|
||||
|
||||
**Configuration**
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
**Usage**
|
||||
|
||||
To use this module, you need to:
|
||||
|
||||
**Try me on Runbot**
|
||||
|
||||
**Known issues / Roadmap**
|
||||
|
||||
...
|
||||
|
||||
**Bug Tracker**
|
||||
|
||||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback here.
|
||||
|
||||
Credits
|
||||
|
||||
Contributors
|
||||
|
||||
Maintainer
|
||||
|
||||
4
hotel_node_master/__init__.py
Normal file
4
hotel_node_master/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import models
|
||||
from . import wizards
|
||||
29
hotel_node_master/__manifest__.py
Normal file
29
hotel_node_master/__manifest__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
'name': 'Hotel Node Master',
|
||||
'summary': """Provides centralized hotel management features""",
|
||||
'version': '0.1.0',
|
||||
'author': 'Pablo Q. Barriuso, \
|
||||
Darío Lodeiros, \
|
||||
Alexandre Díaz, \
|
||||
Odoo Community Association (OCA)',
|
||||
'category': 'Generic Modules/Hotel Management',
|
||||
'depends': [
|
||||
'project'
|
||||
],
|
||||
'external_dependencies':
|
||||
{'python' : ['odoorpc']},
|
||||
'license': "AGPL-3",
|
||||
'data': [
|
||||
'views/hotel_node.xml',
|
||||
'views/hotel_node_user.xml',
|
||||
'views/hotel_node_group.xml',
|
||||
'views/hotel_node_room_type.xml',
|
||||
'views/inherited_res_partner_views.xml',
|
||||
'wizards/wizard_hotel_node_reservation.xml',
|
||||
'security/hotel_node_security.xml',
|
||||
'security/ir.model.access.csv'
|
||||
],
|
||||
'demo': [],
|
||||
'auto_install': False,
|
||||
'installable': True
|
||||
}
|
||||
8
hotel_node_master/models/__init__.py
Normal file
8
hotel_node_master/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import hotel_node
|
||||
from . import hotel_node_user
|
||||
from . import hotel_node_group
|
||||
from . import hotel_node_room
|
||||
from . import hotel_node_room_type
|
||||
from . import inherited_res_partner
|
||||
325
hotel_node_master/models/hotel_node.py
Normal file
325
hotel_node_master/models/hotel_node.py
Normal file
@@ -0,0 +1,325 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import wdb
|
||||
import logging
|
||||
import urllib.error
|
||||
import odoorpc.odoo
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNode(models.Model):
|
||||
|
||||
_inherit = ['project.project']
|
||||
|
||||
_description = 'Centralized hotel management features'
|
||||
|
||||
active = fields.Boolean('Active', default=True,
|
||||
help='The active field allows you to hide the \
|
||||
node without removing it.')
|
||||
sequence = fields.Integer('Sequence', default=0,
|
||||
help='Gives the sequence order when displaying the list of Nodes.')
|
||||
|
||||
odoo_version = fields.Char()
|
||||
odoo_host = fields.Char('Host', required=True,
|
||||
help='Full URL to the host.')
|
||||
odoo_db = fields.Char('Database Name',
|
||||
help='Odoo database name.')
|
||||
odoo_user = fields.Char('Username',
|
||||
help='Odoo administration user.')
|
||||
odoo_password = fields.Char('Password',
|
||||
help='Odoo password.')
|
||||
odoo_port = fields.Integer(string='TCP Port', default=443,
|
||||
help='Specify the TCP port for the XML-RPC protocol.')
|
||||
odoo_protocol = fields.Selection([('jsonrpc', 'jsonrpc'), ('jsonrpc+ssl', 'jsonrpc+ssl')],
|
||||
'Protocol', required=True, default='jsonrpc+ssl')
|
||||
|
||||
user_ids = fields.One2many('hotel.node.user', 'node_id',
|
||||
'Users with access to this hotel')
|
||||
|
||||
group_ids = fields.Many2many('hotel.node.group', 'hotel_node_group_rel', 'node_id', 'group_id',
|
||||
string='Access Groups')
|
||||
|
||||
room_type_ids = fields.One2many('hotel.node.room.type', 'node_id',
|
||||
'Rooms Type in this hotel')
|
||||
room_ids = fields.One2many('hotel.node.room', 'node_id',
|
||||
'Rooms in this hotel')
|
||||
|
||||
@api.constrains('group_ids')
|
||||
def _check_group_version(self):
|
||||
"""
|
||||
:raise: ValidationError
|
||||
"""
|
||||
for node in self:
|
||||
domain = [('id', 'in', node.group_ids.ids), ('odoo_version', '!=', node.odoo_version)]
|
||||
invalid_groups = self.env["hotel.node.group"].search_count(domain)
|
||||
if invalid_groups > 0:
|
||||
msg = _("At least one group is not within the node version.") + " " + \
|
||||
_("Odoo version of the node: %s") % node.odoo_version
|
||||
_logger.warning(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
_sql_constraints = [
|
||||
('db_node_id_uniq', 'unique (odoo_db, id)',
|
||||
'The database name of the hotel must be unique within the Master Node!'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
:param dict vals: the model's fields as a dictionary
|
||||
:return: new hotel node record created.
|
||||
:raise: ValidationError
|
||||
"""
|
||||
try:
|
||||
noderpc = odoorpc.ODOO(vals['odoo_host'], vals['odoo_protocol'], vals['odoo_port'])
|
||||
noderpc.login(vals['odoo_db'], vals['odoo_user'], vals['odoo_password'])
|
||||
|
||||
vals.update({'odoo_version': noderpc.version})
|
||||
|
||||
# TODO Check if hotel_node_helper module is installed / available in the node.
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
else:
|
||||
node_id = super().create(vals)
|
||||
noderpc.logout()
|
||||
return node_id
|
||||
|
||||
@api.multi
|
||||
def action_sync_from_node(self):
|
||||
self.ensure_one()
|
||||
try:
|
||||
noderpc = odoorpc.ODOO(self.odoo_host, self.odoo_protocol, self.odoo_port)
|
||||
noderpc.login(self.odoo_db, self.odoo_user, self.odoo_password)
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
# TODO synchronize only if write_date in remote node is newer ¿?
|
||||
try:
|
||||
vals = {}
|
||||
# import remote groups
|
||||
remote_groups = noderpc.env['ir.model.data'].search_read(
|
||||
[('model', '=', 'res.groups')],
|
||||
['complete_name', 'display_name'])
|
||||
|
||||
master_groups = self.env["hotel.node.group"].search_read(
|
||||
[('odoo_version', '=', self.odoo_version)], ['xml_id'])
|
||||
|
||||
gui_ids = [r['id'] for r in master_groups]
|
||||
xml_ids = [r['xml_id'] for r in master_groups]
|
||||
|
||||
group_ids = []
|
||||
for group in remote_groups:
|
||||
if group['complete_name'] in xml_ids:
|
||||
idx = xml_ids.index(group['complete_name'])
|
||||
group_ids.append((4, gui_ids[idx], 0))
|
||||
else:
|
||||
group_ids.append((0, 0, {
|
||||
'name': group['display_name'],
|
||||
'xml_id': group['complete_name'],
|
||||
'odoo_version': self.odoo_version,
|
||||
}))
|
||||
vals.update({'group_ids': group_ids})
|
||||
|
||||
self.write(vals)
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
try:
|
||||
vals = {}
|
||||
# import remote users
|
||||
remote_users = noderpc.env['res.users'].search_read(
|
||||
[('login', '!=', 'admin')],
|
||||
['name', 'login', 'email', 'is_company', 'partner_id', 'groups_id', 'active'])
|
||||
|
||||
master_users = self.env["hotel.node.user"].search_read(
|
||||
[('node_id', '=', self.id)], ['remote_user_id'])
|
||||
|
||||
master_ids = [r['id'] for r in master_users]
|
||||
remote_ids = [r['remote_user_id'] for r in master_users]
|
||||
|
||||
user_ids = []
|
||||
for user in remote_users:
|
||||
group_ids = []
|
||||
# retrieve the remote external ID(s) of group records
|
||||
remote_xml_ids = noderpc.env['res.groups'].browse(user['groups_id']).get_external_id()
|
||||
for key, value in remote_xml_ids.items():
|
||||
group_ids.append(gui_ids[xml_ids.index(value)])
|
||||
|
||||
if user['id'] in remote_ids:
|
||||
idx = remote_ids.index(user['id'])
|
||||
user_ids.append((1, master_ids[idx], {
|
||||
'name': user['name'],
|
||||
'login': user['login'],
|
||||
'email': user['email'],
|
||||
'active': user['active'],
|
||||
'remote_user_id': user['id'],
|
||||
'group_ids': [[
|
||||
6,
|
||||
False,
|
||||
group_ids
|
||||
]]
|
||||
}))
|
||||
else:
|
||||
partner = self.env['res.partner'].search([('email', '=', user['email'])])
|
||||
if not partner:
|
||||
partner = self.env['res.partner'].create({
|
||||
'name': user['name'],
|
||||
'is_company': False,
|
||||
'email': user['email'],
|
||||
})
|
||||
user_ids.append((0, 0, {
|
||||
'name': user['name'],
|
||||
'login': user['login'],
|
||||
'email': user['email'],
|
||||
'active': user['active'],
|
||||
'remote_user_id': user['id'],
|
||||
'partner_id': partner.id,
|
||||
'group_ids': [[
|
||||
6,
|
||||
False,
|
||||
group_ids
|
||||
]]
|
||||
}))
|
||||
vals.update({'user_ids': user_ids})
|
||||
|
||||
self.with_context({
|
||||
'is_synchronizing': True,
|
||||
}).write(vals)
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
try:
|
||||
# import remote partners
|
||||
node_partners = noderpc.env['res.partner'].search_read(
|
||||
[('email', '!=', '')], # TODO import remote partners (exclude unconfirmed using DNI)
|
||||
['name', 'email', 'is_company', 'website', 'type', 'active'])
|
||||
master_partners = self.env['res.partner'].search([('email', 'in', [r['email'] for r in node_partners])])
|
||||
|
||||
master_partner_emails = [r['email'] for r in master_partners]
|
||||
master_partner_ids = master_partners.ids
|
||||
for partner in node_partners:
|
||||
if partner['email'] not in master_partner_emails:
|
||||
new_partner = self.env['res.partner'].create({
|
||||
'name': partner['name'],
|
||||
'email': partner['email'],
|
||||
'is_company': partner['is_company'],
|
||||
'website': partner['website'],
|
||||
'type': partner['type'],
|
||||
'active': partner['active'],
|
||||
})
|
||||
_logger.info('User #%s created res.partner with ID: [%s]',
|
||||
self._context.get('uid'), new_partner.id)
|
||||
else:
|
||||
partner_id = master_partner_ids[master_partner_emails.index(partner['email'])]
|
||||
self.env['res.partner'].browse(partner_id).write({
|
||||
'name': partner['name'],
|
||||
'is_company': partner['is_company'],
|
||||
'website': partner['website'],
|
||||
'type': partner['type'],
|
||||
'active': partner['active'],
|
||||
# Partners in different Nodes may have different parent_id
|
||||
# TODO How to manage parent_id for related company ¿?
|
||||
})
|
||||
_logger.info('User #%s update res.partner with ID: [%s]',
|
||||
self._context.get('uid'), partner_id)
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
try:
|
||||
vals = {}
|
||||
# import remote room types
|
||||
remote_room_types = noderpc.env['hotel.room.type'].search_read(
|
||||
[], ['name', 'active', 'sequence', 'room_ids'])
|
||||
|
||||
master_room_types = self.env["hotel.node.room.type"].search_read(
|
||||
[('node_id', '=', self.id)], ['remote_room_type_id'])
|
||||
|
||||
master_ids = [r['id'] for r in master_room_types]
|
||||
remote_ids = [r['remote_room_type_id'] for r in master_room_types]
|
||||
|
||||
room_type_ids = []
|
||||
for room_type in remote_room_types:
|
||||
if room_type['id'] in remote_ids:
|
||||
idx = remote_ids.index(room_type['id'])
|
||||
room_type_ids.append((1, master_ids[idx], {
|
||||
'name': room_type['name'],
|
||||
'active': room_type['active'],
|
||||
'sequence': room_type['sequence'],
|
||||
'remote_room_type_id': room_type['id'],
|
||||
}))
|
||||
else:
|
||||
room_type_ids.append((0, 0, {
|
||||
'name': room_type['name'],
|
||||
'active': room_type['active'],
|
||||
'sequence': room_type['sequence'],
|
||||
'remote_room_type_id': room_type['id'],
|
||||
}))
|
||||
vals.update({'room_type_ids': room_type_ids})
|
||||
|
||||
self.write(vals)
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
try:
|
||||
vals = {}
|
||||
# import remote rooms
|
||||
remote_rooms = noderpc.env['hotel.room'].search_read(
|
||||
[],
|
||||
['name', 'active', 'sequence', 'capacity', 'room_type_id'])
|
||||
|
||||
master_rooms = self.env["hotel.node.room"].search_read(
|
||||
[('node_id', '=', self.id)], ['remote_room_id'])
|
||||
|
||||
master_ids = [r['id'] for r in master_rooms]
|
||||
remote_ids = [r['remote_room_id'] for r in master_rooms]
|
||||
|
||||
room_ids = []
|
||||
for room in remote_rooms:
|
||||
room_type_id = self.env["hotel.node.room.type"].search(
|
||||
[('node_id', '=', self.id),
|
||||
('remote_room_type_id', '=', room['room_type_id'][0])]) or None
|
||||
|
||||
if room_type_id is None:
|
||||
msg = _("Something was completely wrong for Remote Room ID: [%s]") % room['id']
|
||||
_logger.critical(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
if room['id'] in remote_ids:
|
||||
idx = remote_ids.index(room['id'])
|
||||
room_ids.append((1, master_ids[idx], {
|
||||
'name': room['name'],
|
||||
'active': room['active'],
|
||||
'sequence': room['sequence'],
|
||||
'capacity': room['capacity'],
|
||||
'room_type_id': room_type_id.id,
|
||||
'remote_room_id': room['id'],
|
||||
}))
|
||||
else:
|
||||
room_ids.append((0, 0, {
|
||||
'name': room['name'],
|
||||
'active': room['active'],
|
||||
'sequence': room['sequence'],
|
||||
'capacity': room['capacity'],
|
||||
'room_type_id': room_type_id.id,
|
||||
'remote_room_id': room['id'],
|
||||
}))
|
||||
vals.update({'room_ids': room_ids})
|
||||
|
||||
self.write(vals)
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
noderpc.logout()
|
||||
return True
|
||||
36
hotel_node_master/models/hotel_node_group.py
Normal file
36
hotel_node_master/models/hotel_node_group.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNodeGroup(models.Model):
|
||||
_name = "hotel.node.group"
|
||||
_description = "Hotel Access Groups"
|
||||
|
||||
active = fields.Boolean(default=True,
|
||||
help="The active field allows you to hide the \
|
||||
group without removing it.")
|
||||
sequence = fields.Integer(default=0,
|
||||
help="Gives the sequence order when displaying the list of Groups.")
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
node_ids = fields.Many2many('project.project', 'hotel_node_group_rel', 'group_id', 'node_id',
|
||||
string='Hotels')
|
||||
user_ids = fields.Many2many('hotel.node.user', 'hotel_node_user_group_rel', 'group_id', 'user_id',
|
||||
string='Users')
|
||||
# xml_id represents the complete module.name, xml_id = ("%s.%s" % (data['module'], data['name']))
|
||||
xml_id = fields.Char(string='External Identifier', required=True,
|
||||
help="External Key/Identifier that can be used for "
|
||||
"data integration with third-party systems")
|
||||
odoo_version = fields.Char('Odoo Version')
|
||||
|
||||
_sql_constraints = [
|
||||
('xml_id_uniq', 'unique (odoo_version, xml_id)',
|
||||
'_(The external identifier of the group must be unique within an Odoo version!')
|
||||
]
|
||||
35
hotel_node_master/models/hotel_node_room.py
Normal file
35
hotel_node_master/models/hotel_node_room.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNodeRoom(models.Model):
|
||||
_name = "hotel.node.room"
|
||||
_description = "Rooms"
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
|
||||
remote_room_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True,
|
||||
help="ID of the target record in the remote database")
|
||||
|
||||
room_type_id = fields.Many2one('hotel.node.room.type', 'Hotel Room Type', required=True)
|
||||
|
||||
node_id = fields.Many2one('project.project', 'Hotel', required=True, readonly=True)
|
||||
|
||||
capacity = fields.Integer('Capacity')
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(default=0)
|
||||
|
||||
_sql_constraints = [
|
||||
('db_remote_room_id_uniq', 'unique (remote_room_id, node_id)',
|
||||
'The Room must be unique within the Node!'),
|
||||
]
|
||||
|
||||
# TODO Changing a room between nodes. Do not allow change the node at create / write
|
||||
73
hotel_node_master/models/hotel_node_room_type.py
Normal file
73
hotel_node_master/models/hotel_node_room_type.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import wdb
|
||||
import logging
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNodeRoomType(models.Model):
|
||||
_name = "hotel.node.room.type"
|
||||
_description = "Room Type"
|
||||
|
||||
active = fields.Boolean(default=True)
|
||||
sequence = fields.Integer(default=0)
|
||||
|
||||
name = fields.Char(required=True, translate=True)
|
||||
|
||||
remote_room_type_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True,
|
||||
help="ID of the target record in the remote database")
|
||||
|
||||
room_ids = fields.One2many('hotel.node.room', 'room_type_id', 'Rooms')
|
||||
|
||||
node_id = fields.Many2one('project.project', 'Hotel', required=True)
|
||||
|
||||
_sql_constraints = [
|
||||
('db_remote_room_type_id_uniq', 'unique (remote_room_type_id, node_id)',
|
||||
'The Room Type must be unique within the Node!'),
|
||||
]
|
||||
|
||||
@api.onchange('node_id')
|
||||
def _onchange_node_id(self):
|
||||
if self.node_id:
|
||||
return {'domain': {'room_ids': [('room_ids', 'in', self.room_ids.ids)]}}
|
||||
|
||||
return {'domain': {'room_ids': []}}
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
:param dict vals: the model's fields as a dictionary
|
||||
:return: new hotel room type record created.
|
||||
:raise: ValidationError
|
||||
"""
|
||||
_logger.warning("This fuction is not yet implemented for remote create.")
|
||||
return super().create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
"""
|
||||
:param dict vals: a dictionary of fields to update and the value to set on them.
|
||||
:raise: ValidationError
|
||||
"""
|
||||
for rec in self:
|
||||
if 'node_id' in vals and vals['node_id'] != rec.node_id.id:
|
||||
msg = _("Changing a room type between nodes is not allowed. Please create a new room type instead.")
|
||||
_logger.error(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
_logger.warning("This fuction is not yet implemented for remote update.")
|
||||
return super().write(vals)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
"""
|
||||
:raise: ValidationError
|
||||
"""
|
||||
_logger.warning("This fuction is not yet implemented for remote delete.")
|
||||
return super().unlink()
|
||||
203
hotel_node_master/models/hotel_node_user.py
Normal file
203
hotel_node_master/models/hotel_node_user.py
Normal file
@@ -0,0 +1,203 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import wdb
|
||||
import logging
|
||||
import urllib.error
|
||||
import odoorpc.odoo
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNodeUser(models.Model):
|
||||
_name = "hotel.node.user"
|
||||
_description = "Users with access to a hotel"
|
||||
|
||||
def _default_groups(self):
|
||||
pass
|
||||
|
||||
active = fields.Boolean(default=True,
|
||||
help="The active field allows you to hide the \
|
||||
user without removing it.")
|
||||
sequence = fields.Integer(default=0,
|
||||
help="Gives the sequence order when displaying the list of Users.")
|
||||
|
||||
node_id = fields.Many2one('project.project', 'Hotel', required=True)
|
||||
# remote users are managed as partners into the central node
|
||||
partner_id = fields.Many2one('res.partner', required=True)
|
||||
name = fields.Char(related='partner_id.name')
|
||||
email = fields.Char(related='partner_id.email', readonly=True)
|
||||
|
||||
login = fields.Char(related='partner_id.email', require=True,
|
||||
help="Used to log into the hotel")
|
||||
password = fields.Char(default='', invisible=True, copy=False,
|
||||
help="Keep empty if you don't want the user to be able to connect on the hotel.")
|
||||
remote_user_id = fields.Integer(require=True, invisible=True, copy=False, readonly=True,
|
||||
help="ID of the target record in the remote database")
|
||||
|
||||
group_ids = fields.Many2many('hotel.node.group', 'hotel_node_user_group_rel', 'user_id', 'group_id',
|
||||
string='Groups', default=_default_groups, require=True,
|
||||
help="Access rights for this user in this hotel.")
|
||||
|
||||
# Constraints and onchanges
|
||||
@api.constrains('group_ids')
|
||||
def _check_group_ids(self):
|
||||
# TODO ensure all group_ids are within the node version
|
||||
domain = [('id', 'in', self.group_ids.ids), ('odoo_version', '!=', self.node_id.odoo_version)]
|
||||
invalid_groups = self.env["hotel.node.group"].search_count(domain)
|
||||
if invalid_groups > 0:
|
||||
msg = _("At least one group is not within the node version.") + " " + \
|
||||
_("Odoo version of the node: %s") % self.node_id.odoo_version
|
||||
_logger.warning(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
@api.onchange('node_id')
|
||||
def _onchange_node_id(self):
|
||||
if self.node_id:
|
||||
# TODO clean group_ids
|
||||
# self.group_ids = []
|
||||
return {'domain': {'group_ids': [('odoo_version', '=', self.node_id.odoo_version)]}}
|
||||
|
||||
return {'domain': {'group_ids': []}}
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
"""
|
||||
:param dict vals: the model's fields as a dictionary
|
||||
:return: new hotel user record created.
|
||||
:raise: ValidationError
|
||||
"""
|
||||
node = self.env["project.project"].browse(vals['node_id'])
|
||||
|
||||
if 'group_ids' in vals:
|
||||
domain = [('id', 'in', vals['group_ids'][0][2]), ('odoo_version', '!=', node.odoo_version)]
|
||||
invalid_groups = self.env["hotel.node.group"].search_count(domain)
|
||||
if invalid_groups > 0:
|
||||
msg = _("At least one group is not within the node version.") + " " + \
|
||||
_("Odoo version in node: %s") % node.odoo_version
|
||||
_logger.error(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
try:
|
||||
if 'is_synchronizing' not in self._context:
|
||||
noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port)
|
||||
noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password)
|
||||
|
||||
partner = self.env["res.partner"].browse(vals['partner_id'])
|
||||
remote_vals = {
|
||||
'name': partner.name,
|
||||
'login': vals['login'],
|
||||
}
|
||||
|
||||
if 'group_ids' in vals:
|
||||
groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2])
|
||||
# TODO Improve one rpc call per remote group for better performance
|
||||
remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups]
|
||||
remote_vals.update({'groups_id': [[6, False, remote_groups]]})
|
||||
|
||||
# create user and delegate in remote node the default values for the user
|
||||
remote_user_id = noderpc.env['res.users'].create(remote_vals)
|
||||
_logger.info('User #%s created remote res.users with ID: [%s]',
|
||||
self._context.get('uid'), remote_user_id)
|
||||
vals.update({'remote_user_id': remote_user_id})
|
||||
|
||||
noderpc.logout()
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
_logger.error(err)
|
||||
raise ValidationError(err)
|
||||
else:
|
||||
return super().create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
"""
|
||||
:param dict vals: a dictionary of fields to update and the value to set on them.
|
||||
:raise: ValidationError
|
||||
"""
|
||||
|
||||
for rec in self:
|
||||
if 'node_id' in vals and vals['node_id'] != rec.node_id.id:
|
||||
msg = _("Changing a user between nodes is not allowed. Please create a new user instead.")
|
||||
_logger.error(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
node = rec.node_id
|
||||
|
||||
if 'group_ids' in vals:
|
||||
domain = [('id', 'in', vals['group_ids'][0][2]), ('odoo_version', '!=', node.odoo_version)]
|
||||
invalid_groups = self.env["hotel.node.group"].search_count(domain)
|
||||
if invalid_groups > 0:
|
||||
msg = _("At least one group is not within the node version.") + " " + \
|
||||
_("Odoo version in node: %s") % node.odoo_version
|
||||
_logger.error(msg)
|
||||
raise ValidationError(msg)
|
||||
|
||||
try:
|
||||
if 'is_synchronizing' not in self._context:
|
||||
noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port)
|
||||
noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password)
|
||||
|
||||
remote_vals = {}
|
||||
|
||||
if 'login' in vals:
|
||||
remote_vals.update({'login': vals['login']})
|
||||
|
||||
if 'active' in vals:
|
||||
remote_vals.update({'active': vals['active']})
|
||||
|
||||
if 'password' in vals:
|
||||
remote_vals.update({'password': vals['password']})
|
||||
|
||||
if 'partner_id' in vals:
|
||||
partner = self.env["res.partner"].browse(vals['partner_id'])
|
||||
remote_vals.update({'name': partner.name})
|
||||
|
||||
if 'group_ids' in vals:
|
||||
groups = self.env["hotel.node.group"].browse(vals['group_ids'][0][2])
|
||||
# TODO Improve one rpc call per remote group for better performance
|
||||
remote_groups = [noderpc.env.ref(r.xml_id).id for r in groups]
|
||||
remote_vals.update({'groups_id': [[6, False, remote_groups]]})
|
||||
|
||||
noderpc.env['res.users'].write([rec.remote_user_id], remote_vals)
|
||||
_logger.info('User #%s updated remote res.users with ID: [%s]',
|
||||
self._context.get('uid'), rec.remote_user_id)
|
||||
|
||||
noderpc.logout()
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
_logger.error(err)
|
||||
raise ValidationError(err)
|
||||
|
||||
# TODO update record in central node only if the corresponding remote call was successfully
|
||||
return super().write(vals)
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
"""
|
||||
:raise: ValidationError
|
||||
"""
|
||||
for rec in self:
|
||||
try:
|
||||
node = rec.node_id
|
||||
|
||||
noderpc = odoorpc.ODOO(node.odoo_host, node.odoo_protocol, node.odoo_port)
|
||||
noderpc.login(node.odoo_db, node.odoo_user, node.odoo_password)
|
||||
# TODO In production users are archived instead of removed
|
||||
# noderpc.env['res.users'].unlink([rec.remote_user_id])
|
||||
noderpc.env['res.users'].write([rec.remote_user_id], {'active': False})
|
||||
_logger.info('User #%s deleted remote res.users with ID: [%s]',
|
||||
self._context.get('uid'), rec.remote_user_id)
|
||||
noderpc.logout()
|
||||
|
||||
# TODO How to manage the relationship with the partner? Also deleted?
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
_logger.error(err)
|
||||
raise ValidationError(err)
|
||||
|
||||
return super().unlink()
|
||||
16
hotel_node_master/models/inherited_res_partner.py
Normal file
16
hotel_node_master/models/inherited_res_partner.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
|
||||
_inherit = 'res.partner'
|
||||
# As res.partner has already a `user_ids` field, you can not use that name in this inheritance
|
||||
node_user_ids = fields.One2many('hotel.node.user', 'partner_id',
|
||||
'Users associated to this partner')
|
||||
|
||||
# TODO Override write for updating in remote nodes
|
||||
3
hotel_node_master/security/hotel_node_security.xml
Normal file
3
hotel_node_master/security/hotel_node_security.xml
Normal file
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="0">
|
||||
</odoo>
|
||||
1
hotel_node_master/security/ir.model.access.csv
Normal file
1
hotel_node_master/security/ir.model.access.csv
Normal file
@@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
BIN
hotel_node_master/static/description/icon.png
Normal file
BIN
hotel_node_master/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
194
hotel_node_master/views/hotel_node.xml
Normal file
194
hotel_node_master/views/hotel_node.xml
Normal file
@@ -0,0 +1,194 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hotel_node_view_form" model="ir.ui.view">
|
||||
<field name="name">hotel.node.view.form</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Hotel Node">
|
||||
<sheet string="Hotel Node">
|
||||
<div class="oe_button_box" name="button_box" groups="base.group_user">
|
||||
<button name="%(hotel_node_reservation_action)d" type="action"
|
||||
string="Reserve" help="Make a reservation in this hotel"
|
||||
class="oe_stat_button" icon="fa-suitcase"
|
||||
context="{'node_id': id}">
|
||||
</button>
|
||||
<button class="oe_stat_button" type="action"
|
||||
name="" icon="fa-tasks">
|
||||
<field string="Tasks" name="task_count" widget="statinfo" options="{'label_field': 'label_tasks'}"/>
|
||||
</button>
|
||||
<button name="action_sync_from_node" type="object" string="Synchronize"
|
||||
attrs="{'invisible':[('id','=',False)]}"
|
||||
confirm="Synchronizing a hotel node automatically updates Groups, Users, Room Types and Rooms in the Central Node. Do you want to proceed?"
|
||||
class="oe_stat_button" icon="fa-cloud-download">
|
||||
</button>
|
||||
<button name="toggle_active" type="object"
|
||||
confirm="(Un)archiving a hotel node automatically (un)archives its issues. Do you want to proceed?"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Hotel Name"/>
|
||||
</h1>
|
||||
<div name="options_active" class="oe_edit_only">
|
||||
<div>
|
||||
<label for="label_tasks" class="oe_inline" string="Name of the tasks :"/>
|
||||
<field name="label_tasks" class="oe_inline oe_input_align"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<notebook>
|
||||
<page name="remote_access" string="Remote Access">
|
||||
<group name="remote_access" string="Hotel Node">
|
||||
<group>
|
||||
<field name="odoo_host" />
|
||||
<field name="odoo_protocol" />
|
||||
<field name="odoo_port" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="odoo_db" />
|
||||
<field name="odoo_user" />
|
||||
<field name="odoo_password" password="True" />
|
||||
</group>
|
||||
</group>
|
||||
</page>
|
||||
<page name="groups" string="Groups" attrs="{'invisible':[('id','=',False)]}">
|
||||
<group>
|
||||
<field name="odoo_version" readonly="1"/>
|
||||
<!-- TODO Make group_ids readonly in hotel node form -->
|
||||
<field name="group_ids" domain="[('odoo_version','=',odoo_version)]"/>
|
||||
</group>
|
||||
</page>
|
||||
<page name="users" string="Users" attrs="{'invisible':[('id','=',False)]}">
|
||||
<group>
|
||||
<field name="user_ids" context="{'default_node_id': active_id}" />
|
||||
</group>
|
||||
</page>
|
||||
<page name="room_type" string="Room Type" attrs="{'invisible':[('id','=',False)]}">
|
||||
<group>
|
||||
<field name="room_type_ids" context="{'default_node_id': active_id}" />
|
||||
</group>
|
||||
</page>
|
||||
<page name="settings" string="Settings">
|
||||
<group>
|
||||
<field name="user_id" string="Node Manager"
|
||||
attrs="{'readonly':[('active','=',False)]}"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"
|
||||
help="Follow this node to automatically track the events associated to tasks and issues of this node."
|
||||
groups="base.group_user"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hotel_node_view_tree" model="ir.ui.view">
|
||||
<field name="name">hotel.node.view.tree</field>
|
||||
<field name="model">project.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-bf="message_needaction==True" decoration-muted="active == False" string="Hotels">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="message_needaction" invisible="1"/>
|
||||
<field name="active" invisible="1"/>
|
||||
<field name="name" string="Hotel Name"/>
|
||||
<field name="odoo_host"/>
|
||||
<field name="odoo_db"/>
|
||||
<field name="odoo_version"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hotel_node_action_open_dashboard" model="ir.actions.act_window">
|
||||
<field name="name">Hotels</field>
|
||||
<field name="res_model">project.project</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="domain">[]</field>
|
||||
<field name="view_mode">kanban,form</field>
|
||||
<field name="target">main</field>
|
||||
</record>
|
||||
|
||||
<!-- Action to open Hotel Nodes List -->
|
||||
<act_window id="hotel_node_action"
|
||||
name="List of Hotels"
|
||||
res_model="project.project"
|
||||
view_mode="tree,form"
|
||||
/>
|
||||
<!-- Action to open Hotel Room Types List -->
|
||||
<act_window id="hotel_node_room_type_action"
|
||||
name="List of Room Types in Hotels"
|
||||
res_model="hotel.node.room.type"
|
||||
view_mode="tree,form"
|
||||
/>
|
||||
<!-- Action to open Hotel Users List -->
|
||||
<act_window id="hotel_node_user_action"
|
||||
name="List of Users in Hotels"
|
||||
res_model="hotel.node.user"
|
||||
view_mode="tree,form"
|
||||
/>
|
||||
<!-- Action to open Hotel Groups List -->
|
||||
<act_window id="hotel_node_group_action"
|
||||
name="List of Access Groups in Hotels"
|
||||
res_model="hotel.node.group"
|
||||
view_mode="tree,form"
|
||||
/>
|
||||
<!-- Action to open Hotel Node Reservation List -->
|
||||
<act_window id="hotel_node_reservation_action"
|
||||
name="Hotel Reservation Wizard"
|
||||
res_model="hotel.node.reservation.wizard"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
/>
|
||||
|
||||
<!-- Menu default to open Hotel Nodes Dashboard -->
|
||||
<menuitem id="hotel_node_dashboard_menu"
|
||||
name="Hotel Central Dashboard"
|
||||
action="hotel_node_action_open_dashboard"
|
||||
/>
|
||||
<!-- Menu Management Section -->
|
||||
<menuitem id="dashboard_menu"
|
||||
name="Management"
|
||||
parent="hotel_node_dashboard_menu"
|
||||
/>
|
||||
<!-- Menu item to open Hotel Nodes List -->
|
||||
<menuitem id="hotel_node_menu"
|
||||
name="Hotels"
|
||||
action="hotel_node_action"
|
||||
parent="dashboard_menu"
|
||||
sequence="1"
|
||||
/>
|
||||
<!-- Menu item to open Hotel Users List -->
|
||||
<menuitem id="hotel_node_user_menu"
|
||||
name="Users"
|
||||
action="hotel_node_user_action"
|
||||
parent="dashboard_menu"
|
||||
sequence="2"
|
||||
/>
|
||||
<!-- Menu item to open Hotel Users List -->
|
||||
<menuitem id="hotel_node_group_menu"
|
||||
name="Access Groups"
|
||||
action="hotel_node_group_action"
|
||||
parent="dashboard_menu"
|
||||
sequence="3"
|
||||
/>
|
||||
<!-- Menu item to open Hotel Nodes List -->
|
||||
<menuitem id="hotel_node_room_type_menu"
|
||||
name="Room Types"
|
||||
action="hotel_node_room_type_action"
|
||||
parent="dashboard_menu"
|
||||
sequence="1"
|
||||
/>
|
||||
<!-- Menu item to open Hotel Node Reservation List -->
|
||||
<!--<menuitem id="hotel_node_reservation_menu"-->
|
||||
<!--name="Reservations Wizard"-->
|
||||
<!--action="hotel_node_reservation_action"-->
|
||||
<!--parent="dashboard_menu"-->
|
||||
<!--sequence="4"-->
|
||||
<!--/>-->
|
||||
</odoo>
|
||||
14
hotel_node_master/views/hotel_node_group.xml
Normal file
14
hotel_node_master/views/hotel_node_group.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hotel_node_group_view_tree" model="ir.ui.view">
|
||||
<field name="name">hotel.node.group.tree</field>
|
||||
<field name="model">hotel.node.group</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="odoo_version"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
57
hotel_node_master/views/hotel_node_room_type.xml
Normal file
57
hotel_node_master/views/hotel_node_room_type.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hotel_node_room_type_view_form" model="ir.ui.view">
|
||||
<field name="name">hotel.node.room.type.view.form</field>
|
||||
<field name="model">hotel.node.room.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Hotel Room Type">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="node_id" placeholder="Hotel" required="1"
|
||||
attrs="{'readonly': [('node_id', '!=', False)]}"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group name="room_type" attrs="{'invisible':[('node_id','=',False)]}">
|
||||
<group>
|
||||
<field name="name" placeholder="Room Type Name" required="1"/>
|
||||
</group>
|
||||
<group>
|
||||
<div name="options_active">
|
||||
<label for="remote_room_type_id" class="oe_inline" string="Remote Room Type :"/>
|
||||
<field name="remote_room_type_id" readonly="1" attrs="{'invisible': [('id', '=', False)]}" />
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="Rooms" string="Rooms" attrs="{'invisible':[('id','=',False)]}">
|
||||
<group>
|
||||
<field name="room_ids" domain="[('room_type_id','=',active_id)]"
|
||||
context="{'default_node_id': node_id, 'default_room_type_id': active_id}"/>
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hotel_node_room_type_view_tree" model="ir.ui.view">
|
||||
<field name="name">hotel.node.room.type.tree</field>
|
||||
<field name="model">hotel.node.room.type</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="node_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
54
hotel_node_master/views/hotel_node_user.xml
Normal file
54
hotel_node_master/views/hotel_node_user.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hotel_node_user_view_form" model="ir.ui.view">
|
||||
<field name="name">hotel.node.user.view.form</field>
|
||||
<field name="model">hotel.node.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Users">
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="toggle_active" type="object"
|
||||
class="oe_stat_button" icon="fa-archive">
|
||||
<field name="active" widget="boolean_button"
|
||||
options='{"terminology": "archive"}'/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="node_id" placeholder="Hotel" required="1"
|
||||
attrs="{'readonly': [('node_id', '!=', False)]}"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group attrs="{'invisible': [('node_id', '=', False)]}">
|
||||
<group name="nodes">
|
||||
<field name="partner_id" placeholder="Name" required="1"/>
|
||||
<field name="remote_user_id" readonly="1" attrs="{'invisible': [('id', '=', False)]}" />
|
||||
</group>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('partner_id', '=', False)]}">
|
||||
<group name="login">
|
||||
<field name="login"/>
|
||||
<field name="password" password="True"/>
|
||||
</group>
|
||||
</group>
|
||||
<group name="groups" attrs="{'invisible': [('node_id', '=', False)]}" collapse="2">
|
||||
<field name="group_ids"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="hotel_node_user_view_tree" model="ir.ui.view">
|
||||
<field name="name">hotel.node.user.tree</field>
|
||||
<field name="model">hotel.node.user</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="login"/>
|
||||
<field name="node_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
16
hotel_node_master/views/inherited_res_partner_views.xml
Normal file
16
hotel_node_master/views/inherited_res_partner_views.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_form" model="ir.ui.view">
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//page[@name='sales_purchases']" position='after'>
|
||||
<page string='Hotels & Users' autofocus="autofocus">
|
||||
<field name="node_user_ids" />
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
3
hotel_node_master/wizards/__init__.py
Normal file
3
hotel_node_master/wizards/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import wizard_hotel_node_reservation
|
||||
227
hotel_node_master/wizards/wizard_hotel_node_reservation.py
Normal file
227
hotel_node_master/wizards/wizard_hotel_node_reservation.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# Copyright 2018 Pablo Q. Barriuso
|
||||
# Copyright 2018 Alexandre Díaz
|
||||
# Copyright 2018 Dario Lodeiros
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
from builtins import list
|
||||
|
||||
import wdb
|
||||
import logging
|
||||
import urllib.error
|
||||
import odoorpc.odoo
|
||||
from datetime import timedelta
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import (
|
||||
DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelNodeReservationWizard(models.TransientModel):
|
||||
_name = "hotel.node.reservation.wizard"
|
||||
_description = "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_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")
|
||||
price_total = fields.Float(string='Total Price', compute='_compute_price_total')
|
||||
|
||||
@api.depends('room_type_wizard_ids.price_total')
|
||||
def _compute_price_total(self):
|
||||
_logger.info('_compute_price_total for wizard %s', self.id)
|
||||
price_total = 0.0
|
||||
for record in self.room_type_wizard_ids:
|
||||
price_total += record.price_total
|
||||
self.price_total = price_total
|
||||
|
||||
@api.onchange('node_id')
|
||||
def _onchange_node_id(self):
|
||||
self.ensure_one()
|
||||
if self.node_id:
|
||||
_logger.info('_onchange_node_id(self): %s', self)
|
||||
# Save your credentials (session)
|
||||
|
||||
@api.onchange('checkin', 'checkout')
|
||||
def _onchange_dates(self):
|
||||
self.ensure_one()
|
||||
_logger.info('_onchange_dates(self): %s', self)
|
||||
# TODO check hotel timezone
|
||||
self.checkin = self._get_default_checkin() if not self.checkin \
|
||||
else fields.Date.from_string(self.checkin)
|
||||
self.checkout = self._get_default_checkout() if not self.checkout \
|
||||
else fields.Date.from_string(self.checkout)
|
||||
if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout):
|
||||
self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime(
|
||||
DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
try:
|
||||
noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port)
|
||||
noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password)
|
||||
|
||||
# free_room_ids = noderpc.env['hotel.room.type'].check_availability_room_ids(self.checkin, self.checkout)
|
||||
room_type_availability = {}
|
||||
# room_type_price_unit = {}
|
||||
for room_type in self.node_id.room_type_ids:
|
||||
room_type_availability[room_type.id] = \
|
||||
noderpc.env['hotel.room.type'].get_room_type_availability(
|
||||
self.checkin, self.checkout, room_type.remote_room_type_id)
|
||||
# availability_real = noderpc.env['hotel.room'].search_count([
|
||||
# ('id', 'in', free_room_ids),
|
||||
# ('room_type_id', '=', room_type.remote_room_type_id),
|
||||
# ])
|
||||
# availability_plan = noderpc.env['hotel.room.type.availability'].search_read([
|
||||
# ('date', '>=', self.checkin),
|
||||
# ('date', '<', self.checkout),
|
||||
# ('room_type_id', '=', room_type.remote_room_type_id),
|
||||
#
|
||||
# ], ['avail']) or float('inf')
|
||||
#
|
||||
# if isinstance(availability_plan, list):
|
||||
# availability_plan = min([r['avail'] for r in availability_plan])
|
||||
#
|
||||
# room_type_availability[room_type.id] = min(
|
||||
# availability_real, availability_plan)
|
||||
|
||||
# room_type_price_unit[room_type.id] = noderpc.env['hotel.room.type'].search_read([
|
||||
# ('id', '=', room_type.remote_room_type_id),
|
||||
# ], ['list_price'])[0]['list_price']
|
||||
|
||||
nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days
|
||||
|
||||
cmds = self.node_id.room_type_ids.mapped(lambda room_type_id: (0, False, {
|
||||
'room_type_id': room_type_id.id,
|
||||
'checkin': self.checkin,
|
||||
'checkout': self.checkout,
|
||||
'nights': nights,
|
||||
'room_type_availability': room_type_availability[room_type_id.id],
|
||||
# 'price_unit': room_type_price_unit[room_type_id.id],
|
||||
'node_reservation_wizard_id': self.id,
|
||||
}))
|
||||
self.room_type_wizard_ids = cmds
|
||||
|
||||
noderpc.logout()
|
||||
|
||||
except (odoorpc.error.RPCError, odoorpc.error.InternalError, urllib.error.URLError) as err:
|
||||
raise ValidationError(err)
|
||||
|
||||
@api.multi
|
||||
def create_node_reservation(self):
|
||||
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 room_type in self.room_type_wizard_ids:
|
||||
for x in range(room_type.room_qty):
|
||||
vals_reservation_lines = {
|
||||
'partner_id': remote_partner_id,
|
||||
'room_type_id': room_type.room_type_id.remote_room_type_id,
|
||||
}
|
||||
# add discount
|
||||
reservation_line_ids = noderpc.env['hotel.reservation'].prepare_reservation_lines(
|
||||
room_type.checkin,
|
||||
(fields.Date.from_string(room_type.checkout) - fields.Date.from_string(room_type.checkin)).days,
|
||||
vals_reservation_lines
|
||||
) # [[5, 0, 0], ¿?
|
||||
|
||||
room_lines.append((0, False, {
|
||||
'room_type_id': room_type.room_type_id.remote_room_type_id,
|
||||
'checkin': room_type.checkin,
|
||||
'checkout': room_type.checkout,
|
||||
'reservation_line_ids': reservation_line_ids['reservation_line_ids'],
|
||||
}))
|
||||
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)
|
||||
|
||||
|
||||
class NodeRoomTypeWizard(models.TransientModel):
|
||||
_name = "node.room.type.wizard"
|
||||
_description = "Node Room Type Wizard"
|
||||
|
||||
node_reservation_wizard_id = fields.Many2one('hotel.node.reservation.wizard')
|
||||
node_id = fields.Many2one(related='node_reservation_wizard_id.node_id')
|
||||
|
||||
room_type_id = fields.Many2one('hotel.node.room.type', 'Rooms Type')
|
||||
room_type_name = fields.Char('Name', related='room_type_id.name')
|
||||
room_type_availability = fields.Integer('Availability', readonly=True) #, compute="_compute_room_type_availability")
|
||||
room_qty = fields.Integer('Quantity', default=0)
|
||||
|
||||
checkin = fields.Date('Check In', required=True)
|
||||
checkout = fields.Date('Check Out', required=True)
|
||||
nights = fields.Integer('Nights', readonly=True)
|
||||
min_stay = fields.Integer('Min. Days', compute="_compute_restrictions", readonly=True)
|
||||
|
||||
price_unit = fields.Float(string='Room Price', required=True, default=0.0, readonly=True)
|
||||
discount = fields.Float(string='Discount (%)', default=0.0)
|
||||
price_total = fields.Float(string='Total Price', compute='_compute_price_total')
|
||||
|
||||
@api.depends('room_qty', 'price_unit', 'discount')
|
||||
def _compute_price_total(self):
|
||||
for room_type in self:
|
||||
_logger.info('_compute_price_total for room type %s', room_type.room_type_id)
|
||||
# noderpc = odoorpc.ODOO(self.node_id.odoo_host, self.node_id.odoo_protocol, self.node_id.odoo_port)
|
||||
# noderpc.login(self.node_id.odoo_db, self.node_id.odoo_user, self.node_id.odoo_password)
|
||||
# self.price_unit = noderpc.env['hotel.room.type'].search_read([
|
||||
# ('id', '=', self.room_type_id.remote_room_type_id),
|
||||
# ], ['list_price'])[0]['list_price']
|
||||
# noderpc.logout()
|
||||
|
||||
room_type.price_total = (room_type.room_qty * room_type.price_unit * room_type.nights) * (1.0 - room_type.discount * 0.01)
|
||||
# Unidades x precio unidad (el precio de unidad ya incluye el conjunto de días)
|
||||
|
||||
@api.depends('checkin', 'checkout')
|
||||
def _compute_restrictions(self):
|
||||
for room_type in self:
|
||||
_logger.info('_compute_restrictions for room type %s', room_type.room_type_id)
|
||||
|
||||
@api.onchange('checkin', 'checkout')
|
||||
def _onchange_dates(self):
|
||||
_logger.info('_onchange_dates for room type %s', self.room_type_id)
|
||||
# recompute price unit
|
||||
self.checkin = self._default_checkin() \
|
||||
if not self.checkin else fields.Date.from_string(self.checkin)
|
||||
self.checkout = self._default_checkout() \
|
||||
if not self.checkout else fields.Date.from_string(self.checkout)
|
||||
if fields.Date.from_string(self.checkin) >= fields.Date.from_string(self.checkout):
|
||||
self.checkout = (fields.Date.from_string(self.checkin) + timedelta(days=1)).strftime(
|
||||
DEFAULT_SERVER_DATE_FORMAT)
|
||||
|
||||
self.nights = (fields.Date.from_string(self.checkout) - fields.Date.from_string(self.checkin)).days
|
||||
|
||||
# Conectar con nodo para traer dispo(availability) y precio por habitación(price_unit)
|
||||
# availability: search de hotel.room.type.availability filtrando por room_type y date y escogiendo el min avail en el rango
|
||||
# preci_unit y json_days: usando prepare_reservation_lines
|
||||
|
||||
60
hotel_node_master/wizards/wizard_hotel_node_reservation.xml
Normal file
60
hotel_node_master/wizards/wizard_hotel_node_reservation.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="hotel_node_reservation_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">hotel.node.reservation.wizard</field>
|
||||
<field name="model">hotel.node.reservation.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Reservation Wizard" >
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="node_id" placeholder="Hotel" required="1" force_save="1"
|
||||
attrs="{'readonly': [('node_id', '!=', False)]}"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group attrs="{'invisible':[('node_id','=',False)]}">
|
||||
<group>
|
||||
<field name="checkin" required="1" widget="date" />
|
||||
<field name="checkout" required="1" widget="date" />
|
||||
</group>
|
||||
<group>
|
||||
<field name="partner_id"/>
|
||||
</group>
|
||||
<group colspan="2">
|
||||
<field name="room_type_wizard_ids" nolabel="1">
|
||||
<tree editable="bottom" create="false" delete="false"
|
||||
decoration-muted="room_type_availability == 0">
|
||||
<field name="room_type_id" string="Room Type" readonly="1" force_save="1"/>
|
||||
<field name="room_type_availability" readonly="1" force_save="1"/>
|
||||
<field name="room_qty"/>
|
||||
<field name="checkin" widget="date" />
|
||||
<field name="checkout" widget="date" />
|
||||
<field name="nights"/>
|
||||
<field name="min_stay" />
|
||||
<field name="price_unit" widget="monetary" />
|
||||
<!--<field name="currency_id" invisible="1"/>-->
|
||||
<field name="discount"/>
|
||||
<field name="price_total" widget="monetary" readonly="1" force_save="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<group>
|
||||
<field name="price_total" widget="monetary" readonly="1" force_save="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<!--<field name="confirm" invisible="1"/>-->
|
||||
<!--<group colspan="2" class="oe_subtotal_footer">-->
|
||||
<!--<field name="total"/>-->
|
||||
<!--</group>-->
|
||||
<footer attrs="{'invisible':[('node_id','=',False)]}">
|
||||
<button name="create_node_reservation" string="Create Reservations" type="object"
|
||||
class="oe_highlight" />
|
||||
<button name="cancel" string="Cancel" special="cancel"
|
||||
class="oe_link" />
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user