mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
Finalize Calendar & Connector integration (#69)
* [WIP] Refactor calendar_channel_connector * [FIX] Hotel Calendar + Hotel Connector * [ADD] Push URLs * [IMP] Connector * [IMP] Notifications * [ADD] hotel_channel_connector_wubook * [FIX] missing imports * [IMP] Clean code
This commit is contained in:
@@ -31,7 +31,7 @@ class HotelRoomTypeRestrictionItem(models.Model):
|
|||||||
'max_stay_arrival')
|
'max_stay_arrival')
|
||||||
def _check_min_stay(self):
|
def _check_min_stay(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
if record.self.min_stay < 0:
|
if record.min_stay < 0:
|
||||||
raise ValidationError(_("Min. Stay can't be less than zero"))
|
raise ValidationError(_("Min. Stay can't be less than zero"))
|
||||||
elif record.min_stay_arrival < 0:
|
elif record.min_stay_arrival < 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
@@ -40,5 +40,4 @@ class HotelRoomTypeRestrictionItem(models.Model):
|
|||||||
raise ValidationError(_("Max. Stay can't be less than zero"))
|
raise ValidationError(_("Max. Stay can't be less than zero"))
|
||||||
elif record.max_stay_arrival < 0:
|
elif record.max_stay_arrival < 0:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
("Max. Stay Arrival can't be less than zero"))
|
_("Max. Stay Arrival can't be less than zero"))
|
||||||
|
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ class FolioWizard(models.TransientModel):
|
|||||||
'checkout': line.checkout,
|
'checkout': line.checkout,
|
||||||
'discount': line.discount,
|
'discount': line.discount,
|
||||||
'room_type_id': line.room_type_id.id,
|
'room_type_id': line.room_type_id.id,
|
||||||
'to_read': line.to_read, #REFACT: wubook module
|
'to_read': line.to_read,
|
||||||
'to_assign': line.to_assign,
|
'to_assign': line.to_assign,
|
||||||
}))
|
}))
|
||||||
for line in self.service_wizard_ids:
|
for line in self.service_wizard_ids:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class BusHotelCalendar(models.TransientModel):
|
|||||||
'username': user_id.partner_id.name,
|
'username': user_id.partner_id.name,
|
||||||
'userid': user_id.id,
|
'userid': user_id.id,
|
||||||
'reservation': {
|
'reservation': {
|
||||||
'id': vals['id'],
|
'room_id': vals['room_id'],
|
||||||
'reserv_id': vals['reserv_id'],
|
'reserv_id': vals['reserv_id'],
|
||||||
'partner_name': vals['partner_name'],
|
'partner_name': vals['partner_name'],
|
||||||
'adults': vals['adults'],
|
'adults': vals['adults'],
|
||||||
@@ -89,7 +89,7 @@ class BusHotelCalendar(models.TransientModel):
|
|||||||
return {
|
return {
|
||||||
'type': 'restriction',
|
'type': 'restriction',
|
||||||
'restriction': {
|
'restriction': {
|
||||||
vals['room_id']: {
|
vals['room_type_id']: {
|
||||||
date_dt.strftime("%d/%m/%Y"): [
|
date_dt.strftime("%d/%m/%Y"): [
|
||||||
vals['min_stay'],
|
vals['min_stay'],
|
||||||
vals['min_stay_arrival'],
|
vals['min_stay_arrival'],
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class HotelCalendarManagement(models.TransientModel):
|
|||||||
res_id = room_type_rest_item_obj.search([
|
res_id = room_type_rest_item_obj.search([
|
||||||
('date', '=', restriction['date']),
|
('date', '=', restriction['date']),
|
||||||
('restriction_id', '=', int(restriction_id)),
|
('restriction_id', '=', int(restriction_id)),
|
||||||
('applied_on', '=', '0_room_type'),
|
|
||||||
('room_type_id', '=', int(k_res)),
|
('room_type_id', '=', int(k_res)),
|
||||||
], limit=1)
|
], limit=1)
|
||||||
vals = self._get_restrictions_values(restriction)
|
vals = self._get_restrictions_values(restriction)
|
||||||
@@ -93,7 +92,6 @@ class HotelCalendarManagement(models.TransientModel):
|
|||||||
vals.update({
|
vals.update({
|
||||||
'date': restriction['date'],
|
'date': restriction['date'],
|
||||||
'restriction_id': int(restriction_id),
|
'restriction_id': int(restriction_id),
|
||||||
'applied_on': '0_room_type',
|
|
||||||
'room_type_id': int(k_res),
|
'room_type_id': int(k_res),
|
||||||
})
|
})
|
||||||
res_id = room_type_rest_item_obj.create(vals)
|
res_id = room_type_rest_item_obj.create(vals)
|
||||||
@@ -277,7 +275,6 @@ class HotelCalendarManagement(models.TransientModel):
|
|||||||
restriction_item_ids = room_type_rest_it_obj.search([
|
restriction_item_ids = room_type_rest_it_obj.search([
|
||||||
('date', '>=', dfrom), ('date', '<=', dto),
|
('date', '>=', dfrom), ('date', '<=', dto),
|
||||||
('restriction_id', '=', restriction_id),
|
('restriction_id', '=', restriction_id),
|
||||||
('applied_on', '=', '0_room_type'),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
pricelist_item_ids = self.env['product.pricelist.item'].search([
|
pricelist_item_ids = self.env['product.pricelist.item'].search([
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class HotelReservation(models.Model):
|
|||||||
ndate_str = ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
ndate_str = ndate.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||||
rest_id = room_type_rest_obj.search([
|
rest_id = room_type_rest_obj.search([
|
||||||
('room_type_id', '=', room_type.id),
|
('room_type_id', '=', room_type.id),
|
||||||
('date', '>=', ndate_str),
|
('date', '=', ndate_str),
|
||||||
('restriction_id', '=', restriction_id)
|
('restriction_id', '=', restriction_id)
|
||||||
], limit=1)
|
], limit=1)
|
||||||
if rest_id and (rest_id.min_stay or rest_id.min_stay_arrival or
|
if rest_id and (rest_id.min_stay or rest_id.min_stay_arrival or
|
||||||
@@ -267,7 +267,7 @@ class HotelReservation(models.Model):
|
|||||||
'action': naction,
|
'action': naction,
|
||||||
'type': ntype,
|
'type': ntype,
|
||||||
'title': ntitle,
|
'title': ntitle,
|
||||||
'id': record.room_id.id,
|
'room_id': record.room_id.id,
|
||||||
'reserv_id': record.id,
|
'reserv_id': record.id,
|
||||||
'partner_name': record.partner_id.name,
|
'partner_name': record.partner_id.name,
|
||||||
'adults': record.adults,
|
'adults': record.adults,
|
||||||
@@ -280,7 +280,7 @@ class HotelReservation(models.Model):
|
|||||||
'splitted': record.splitted,
|
'splitted': record.splitted,
|
||||||
'parent_reservation': record.parent_reservation and
|
'parent_reservation': record.parent_reservation and
|
||||||
record.parent_reservation.id or 0,
|
record.parent_reservation.id or 0,
|
||||||
'room_name': record.name,
|
'room_name': record.room_id.name,
|
||||||
'partner_phone': record.partner_id.mobile
|
'partner_phone': record.partner_id.mobile
|
||||||
or record.partner_id.phone or _('Undefined'),
|
or record.partner_id.phone or _('Undefined'),
|
||||||
'state': record.state,
|
'state': record.state,
|
||||||
|
|||||||
@@ -15,21 +15,19 @@ class HotelRoomTypeResrtrictionItem(models.Model):
|
|||||||
'res.config.settings', 'default_restriction_id')
|
'res.config.settings', 'default_restriction_id')
|
||||||
if restrictions_default_id:
|
if restrictions_default_id:
|
||||||
restrictions_default_id = int(restrictions_default_id)
|
restrictions_default_id = int(restrictions_default_id)
|
||||||
restriction_id = res.restriction_id.id
|
if res.restriction_id.id == restrictions_default_id:
|
||||||
if restriction_id == restrictions_default_id and \
|
|
||||||
self.applied_on == '0_room_type':
|
|
||||||
self.env['bus.hotel.calendar'].send_restriction_notification({
|
self.env['bus.hotel.calendar'].send_restriction_notification({
|
||||||
'restriction_id': self.restriction_id.id,
|
'restriction_id': res.restriction_id.id,
|
||||||
'date': self.date,
|
'date': res.date,
|
||||||
'min_stay': self.min_stay,
|
'min_stay': res.min_stay,
|
||||||
'min_stay_arrival': self.min_stay_arrival,
|
'min_stay_arrival': res.min_stay_arrival,
|
||||||
'max_stay': self.max_stay,
|
'max_stay': res.max_stay,
|
||||||
'max_stay_arrival': self.max_stay_arrival,
|
'max_stay_arrival': res.max_stay_arrival,
|
||||||
'closed': self.closed,
|
'closed': res.closed,
|
||||||
'closed_departure': self.closed_departure,
|
'closed_departure': res.closed_departure,
|
||||||
'closed_arrival': self.closed_arrival,
|
'closed_arrival': res.closed_arrival,
|
||||||
'room_type_id': self.room_type_id.id,
|
'room_type_id': res.room_type_id.id,
|
||||||
'id': self.id,
|
'id': res.id,
|
||||||
})
|
})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|||||||
@@ -281,10 +281,10 @@ var PMSCalendarController = AbstractController.extend({
|
|||||||
nreservs = _.reject(nreservs, function(item){ return item.id == reserv['reserv_id']; });
|
nreservs = _.reject(nreservs, function(item){ return item.id == reserv['reserv_id']; });
|
||||||
} else {
|
} else {
|
||||||
nreservs = _.reject(nreservs, {'id': reserv['reserv_id']}); // Only like last changes
|
nreservs = _.reject(nreservs, {'id': reserv['reserv_id']}); // Only like last changes
|
||||||
var room = this.renderer._hcalendar.getRoom(reserv['id'], reserv['overbooking'], reserv['reserv_id']);
|
var room = this.renderer._hcalendar.getRoom(reserv['room_id'], reserv['overbooking'], reserv['reserv_id']);
|
||||||
// need create a overbooking row?
|
// need create a overbooking row?
|
||||||
if (!room && reserv['overbooking']) {
|
if (!room && reserv['overbooking']) {
|
||||||
room = this.renderer._hcalendar.createOBRoom(this.renderer._hcalendar.getRoom(reserv['id']), reserv['reserv_id']);
|
room = this.renderer._hcalendar.createOBRoom(this.renderer._hcalendar.getRoom(reserv['room_id']), reserv['reserv_id']);
|
||||||
this.renderer._hcalendar.createOBRoomRow(room);
|
this.renderer._hcalendar.createOBRoomRow(room);
|
||||||
}
|
}
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ return AbstractModel.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
save_changes: function(params) {
|
save_changes: function(params) {
|
||||||
|
params.splice(0, 0, false); // FIXME: ID=False because first parameter its an integer
|
||||||
return this._rpc({
|
return this._rpc({
|
||||||
model: 'hotel.calendar.management',
|
model: 'hotel.calendar.management',
|
||||||
method: 'save_changes',
|
method: 'save_changes',
|
||||||
|
|||||||
@@ -115,7 +115,6 @@ var MPMSCalendarController = AbstractController.extend({
|
|||||||
_onLoadCalendarSettings: function (ev) {
|
_onLoadCalendarSettings: function (ev) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.model.get_hcalendar_settings().then(function(results){
|
this.model.get_hcalendar_settings().then(function(results){
|
||||||
console.log(results);
|
|
||||||
self.renderer.setHCalendarSettings(results);
|
self.renderer.setHCalendarSettings(results);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ return AbstractModel.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
save_changes: function (params) {
|
save_changes: function (params) {
|
||||||
|
params.splice(0, 0, false); // FIXME: ID=False because first parameter its an integer
|
||||||
return this._rpc({
|
return this._rpc({
|
||||||
model: this.modelName,
|
model: this.modelName,
|
||||||
method: 'save_changes',
|
method: 'save_changes',
|
||||||
|
|||||||
@@ -81,11 +81,7 @@ var HotelCalendarManagementView = AbstractRenderer.extend({
|
|||||||
var availability = this._hcalendar.getAvailability(true);
|
var availability = this._hcalendar.getAvailability(true);
|
||||||
|
|
||||||
var params = this.generate_params();
|
var params = this.generate_params();
|
||||||
console.log("---- PARAMS");
|
|
||||||
console.log(params);
|
|
||||||
var oparams = [params['prices'], params['restrictions'], pricelist, restrictions, availability];
|
var oparams = [params['prices'], params['restrictions'], pricelist, restrictions, availability];
|
||||||
console.log("---- OPARAMS");
|
|
||||||
console.log(oparams);
|
|
||||||
this.trigger_up('onSaveChanges', oparams);
|
this.trigger_up('onSaveChanges', oparams);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -240,7 +236,6 @@ var HotelCalendarManagementView = AbstractRenderer.extend({
|
|||||||
|
|
||||||
/** VIEW CONTROLS INITIALIZATION **/
|
/** VIEW CONTROLS INITIALIZATION **/
|
||||||
// DATE TIME PICKERS
|
// DATE TIME PICKERS
|
||||||
console.log("asdads");
|
|
||||||
var DTPickerOptions = {
|
var DTPickerOptions = {
|
||||||
viewMode: 'months',
|
viewMode: 'months',
|
||||||
icons : {
|
icons : {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
HOTEL CALENDAR WUBOOK
|
HOTEL CALENDAR CHANNEL CONNECTOR
|
||||||
=============
|
===================================
|
||||||
- Show button for see reservations from wubook in hotel calendar view
|
Unify 'hotel_calendar' and 'hotel_channel_connector'
|
||||||
- Mark as read-only reservations from wubook
|
|
||||||
|
|
||||||
** UNDER DEVELOPMENT: NOT USE IN PRODUCTION **
|
|
||||||
|
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
@@ -12,4 +9,4 @@ Credits
|
|||||||
Creator
|
Creator
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* Alexandre Díaz <alex@aloxa.eu>
|
* Alexandre Díaz <alex@aloxa.eu>
|
||||||
|
|||||||
@@ -1,31 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
##############################################################################
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
#
|
|
||||||
# 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/>.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Hotel Calendar Channel Connector',
|
'name': 'Hotel Calendar Channel Connector',
|
||||||
'version': '1.0',
|
'version': '11.0.2.0',
|
||||||
'author': "Alexandre Díaz <dev@redneboa.es>",
|
'author': "Alexandre Díaz <dev@redneboa.es>",
|
||||||
'website': 'https://github.com/hootel/hootel',
|
'website': 'https://github.com/hootel/hootel',
|
||||||
'category': 'eiqui/hotel',
|
'category': 'hotel/addon',
|
||||||
'summary': "Hotel Calendar Channel Connector",
|
'summary': "Hotel Calendar Channel Connector",
|
||||||
'description': "Unify 'hotel_calendar' and 'hotel_channel_connector'",
|
'description': "Unify 'hotel_calendar' and 'hotel_channel_connector'",
|
||||||
'depends': [
|
'depends': [
|
||||||
@@ -46,7 +27,7 @@
|
|||||||
'test': [
|
'test': [
|
||||||
],
|
],
|
||||||
|
|
||||||
'installable': False,
|
'installable': True,
|
||||||
'auto_install': True,
|
'auto_install': True,
|
||||||
'application': False,
|
'application': False,
|
||||||
'license': 'AGPL-3',
|
'license': 'AGPL-3',
|
||||||
|
|||||||
@@ -2,5 +2,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 inherited_hotel_reservation
|
from . import inherited_hotel_reservation
|
||||||
from . import inherited_bus_hotel_calendar
|
from . import inherited_bus_hotel_calendar
|
||||||
from . import inherited_hotel_channel_connector_issue
|
|
||||||
from . import inherited_hotel_calendar_management
|
from . import inherited_hotel_calendar_management
|
||||||
|
|||||||
@@ -34,5 +34,5 @@ class HotelCalendarManagement(models.TransientModel):
|
|||||||
pricelist,
|
pricelist,
|
||||||
restrictions,
|
restrictions,
|
||||||
availability)
|
availability)
|
||||||
self.env['wubook'].push_changes()
|
self.env['channel.backend'].cron_push_changes()
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# 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 HotelChannelConnectorIssue(models.Model):
|
|
||||||
_inherit = 'hotel.channel.connector.issue'
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def create(self, vals):
|
|
||||||
issue_id = super(HotelChannelConnectorIssue, self).create(vals)
|
|
||||||
self.env['bus.hotel.calendar'].send_issue_notification(
|
|
||||||
'warn',
|
|
||||||
_("Oops! Issue Reported!!"),
|
|
||||||
issue_id.id,
|
|
||||||
issue_id.section,
|
|
||||||
issue_id.message)
|
|
||||||
return issue_id
|
|
||||||
@@ -43,7 +43,7 @@ class HotelReservation(models.Model):
|
|||||||
for v_rval in vals[0]:
|
for v_rval in vals[0]:
|
||||||
reserv = hotel_reservation_obj.browse(v_rval[1])
|
reserv = hotel_reservation_obj.browse(v_rval[1])
|
||||||
json_reservations.append((
|
json_reservations.append((
|
||||||
reserv.product_id.id,
|
reserv.room_id.id,
|
||||||
reserv.id,
|
reserv.id,
|
||||||
reserv.folio_id.partner_id.name,
|
reserv.folio_id.partner_id.name,
|
||||||
reserv.adults,
|
reserv.adults,
|
||||||
@@ -78,7 +78,7 @@ class HotelReservation(models.Model):
|
|||||||
'action': naction,
|
'action': naction,
|
||||||
'type': ntype,
|
'type': ntype,
|
||||||
'title': ntitle,
|
'title': ntitle,
|
||||||
'product_id': record.product_id.id,
|
'room_id': record.room_id.id,
|
||||||
'reserv_id': record.id,
|
'reserv_id': record.id,
|
||||||
'partner_name': record.partner_id.name,
|
'partner_name': record.partner_id.name,
|
||||||
'adults': record.adults,
|
'adults': record.adults,
|
||||||
@@ -91,7 +91,7 @@ class HotelReservation(models.Model):
|
|||||||
'splitted': record.splitted,
|
'splitted': record.splitted,
|
||||||
'parent_reservation': record.parent_reservation and
|
'parent_reservation': record.parent_reservation and
|
||||||
record.parent_reservation.id or 0,
|
record.parent_reservation.id or 0,
|
||||||
'room_name': record.product_id.name,
|
'room_name': record.room_id.name,
|
||||||
'partner_phone': record.partner_id.mobile or
|
'partner_phone': record.partner_id.mobile or
|
||||||
record.partner_id.phone or _('Undefined'),
|
record.partner_id.phone or _('Undefined'),
|
||||||
'state': record.state,
|
'state': record.state,
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ var PMSHotelCalendarControllerChannelConnector = PMSCalendarController.include({
|
|||||||
}
|
}
|
||||||
else if (notif[1]['type'] === 'reservation') {
|
else if (notif[1]['type'] === 'reservation') {
|
||||||
var reserv = notif[1]['reservation'];
|
var reserv = notif[1]['reservation'];
|
||||||
if (reserv['wrid']) {
|
if (reserv['external_id']) {
|
||||||
if (notif[1]['action'] === 'create') {
|
if (notif[1]['action'] === 'create') {
|
||||||
this._play_sound(this.SOUNDS.BOOK_NEW);
|
this._play_sound(this.SOUNDS.BOOK_NEW);
|
||||||
} else if (notif[1]['action'] !== 'unlink' && reserv['state'] === 'cancelled') {
|
} else if (notif[1]['action'] !== 'unlink' && reserv['state'] === 'cancelled') {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarRendererChannelCon
|
|||||||
var self = this;
|
var self = this;
|
||||||
return this._super().then(function () {
|
return this._super().then(function () {
|
||||||
self.$el.find('#btn_channel_manager_request').on('click', function (ev) {
|
self.$el.find('#btn_channel_manager_request').on('click', function (ev) {
|
||||||
self.do_action("hotel_calendar_wubook.hotel_reservation_action_manager_request");
|
self.do_action("hotel_calendar_channel_connector.hotel_reservation_action_manager_request");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarRendererChannelCon
|
|||||||
_generate_bookings_domain: function(tsearch) {
|
_generate_bookings_domain: function(tsearch) {
|
||||||
var domain = this._super(tsearch);
|
var domain = this._super(tsearch);
|
||||||
domain.splice(0, 0, '|');
|
domain.splice(0, 0, '|');
|
||||||
domain.push(['wrid', 'ilike', tsearch]);
|
domain.push(['external_id', 'ilike', tsearch]);
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,8 +20,8 @@
|
|||||||
|
|
||||||
<t t-extend="HotelCalendar.Notification">
|
<t t-extend="HotelCalendar.Notification">
|
||||||
<t t-jquery="ul > li:first-child" t-operation="before">
|
<t t-jquery="ul > li:first-child" t-operation="before">
|
||||||
<t t-if="wchannel">
|
<t t-if="channel_name">
|
||||||
<li><b>Channel:</b> <t t-esc="wchannel"/></li>
|
<li><b>Channel:</b> <t t-esc="channel_name"/></li>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
<data>
|
<data>
|
||||||
|
|
||||||
<record id="hotel_reservation_action_manager_request" model="ir.actions.act_window">
|
<record id="hotel_reservation_action_manager_request" model="ir.actions.act_window">
|
||||||
<field name="name">Reservations to Assign from WuBook</field>
|
<field name="name">Reservations to Assign from Channel</field>
|
||||||
<field name="res_model">hotel.reservation</field>
|
<field name="res_model">hotel.reservation</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">tree,form</field>
|
||||||
<field name="view_id" ref="view_hotel_toassign_reservation_tree" />
|
<field name="view_id" ref="hotel_toassign_reservation_view_tree" />
|
||||||
<field name="domain">[('to_assign','=',True),('to_read','=',True)]</field>
|
<field name="domain">[('to_assign','=',True),('to_read','=',True)]</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
decoration-muted="state == 'cancelled' and not overbooking"
|
decoration-muted="state == 'cancelled' and not overbooking"
|
||||||
decoration-warning="overbooking"
|
decoration-warning="overbooking"
|
||||||
>
|
>
|
||||||
<button icon="fa fa-2x fa-thumb-tack"
|
<!--button icon="fa fa-2x fa-thumb-tack"
|
||||||
type="object"
|
type="object"
|
||||||
class="oe_stat_button"
|
class="oe_stat_button"
|
||||||
id="mark_readed"
|
id="mark_readed"
|
||||||
@@ -25,20 +25,20 @@
|
|||||||
id="splitted"
|
id="splitted"
|
||||||
name="open_master"
|
name="open_master"
|
||||||
attrs="{'invisible':[('splitted','=', False)]}"
|
attrs="{'invisible':[('splitted','=', False)]}"
|
||||||
/>
|
/-->
|
||||||
<field name="state" />
|
<field name="state" />
|
||||||
<button type="object" class="oe_stat_button"
|
<!--button type="object" class="oe_stat_button"
|
||||||
id="go_folio" icon="fa fa-2x fa-file"
|
id="go_folio" icon="fa fa-2x fa-file"
|
||||||
name="open_folio"
|
name="open_folio"
|
||||||
/>
|
/-->
|
||||||
<field name="folio_id"/>
|
<!--field name="folio_id"/>
|
||||||
<field name="product_id" string="Room"/>
|
<field name="room_id" string="Room"/-->
|
||||||
<button type="action" class="oe_stat_button"
|
<!--button type="action" class="oe_stat_button"
|
||||||
id="reservations_partner" icon="fa fa-2x fa-list-ul"
|
id="reservations_partner" icon="fa fa-2x fa-list-ul"
|
||||||
name="%(hotel.open_hotel_reservation_form_tree_all)d"
|
name="%(hotel.open_hotel_reservation_form_tree_all)d"
|
||||||
context="{'search_default_partner_id': partner_id}"
|
context="{'search_default_partner_id': partner_id}"
|
||||||
/>
|
/-->
|
||||||
<field name="partner_id"/>
|
<!--field name="partner_id"/>
|
||||||
<field name="splitted" invisible="1" />
|
<field name="splitted" invisible="1" />
|
||||||
<field name="parent_reservation" invisible="1" />
|
<field name="parent_reservation" invisible="1" />
|
||||||
<field name="room_type_id" string="Reserved Room Type"/>
|
<field name="room_type_id" string="Reserved Room Type"/>
|
||||||
@@ -49,19 +49,17 @@
|
|||||||
<field name="checkout" widget="date"/>
|
<field name="checkout" widget="date"/>
|
||||||
<field name="create_date"/>
|
<field name="create_date"/>
|
||||||
<field name="overbooking" invisible="1" />
|
<field name="overbooking" invisible="1" />
|
||||||
<field name="last_updated_res" string="Updated on"/>
|
<field name="last_updated_res" string="Updated on"/-->
|
||||||
<field name="origin_sale"/>
|
|
||||||
<field name="overbooking" invisible="1" />
|
<field name="overbooking" invisible="1" />
|
||||||
<field name="to_assign" invisible="1"/>
|
<!--field name="to_assign" invisible="1"/>
|
||||||
<field name="price_unit" string="Reservation Price"/>
|
|
||||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||||
<field name="price_total" string="Final Price"/>
|
<field name="price_total" string="Final Price"/>
|
||||||
<field name="folio_pending_amount" string="Folio Pending Amount"/>
|
<field name="folio_pending_amount" string="Folio Pending Amount"/-->
|
||||||
<button type="object" class="oe_stat_button"
|
<!--button type="object" class="oe_stat_button"
|
||||||
id="checkin_partner_smart_button" icon="fa fa-3x fa-money"
|
id="checkin_partner_smart_button" icon="fa fa-3x fa-money"
|
||||||
name="action_pay_folio"
|
name="action_pay_folio"
|
||||||
attrs="{'invisible':[('folio_pending_amount','==',0)]}"
|
attrs="{'invisible':[('folio_pending_amount','==',0)]}"
|
||||||
/>
|
/-->
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
@@ -71,6 +69,7 @@
|
|||||||
<field name="inherit_id" ref="hotel.hotel_reservation_view_form" />
|
<field name="inherit_id" ref="hotel.hotel_reservation_view_form" />
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//button[@name='unify']" position="after">
|
<xpath expr="//button[@name='unify']" position="after">
|
||||||
|
<field name="to_read" invisible="1"/>
|
||||||
<button name="mark_as_readed" string="Mark as Read"
|
<button name="mark_as_readed" string="Mark as Read"
|
||||||
type="object" class="oe_highlight"
|
type="object" class="oe_highlight"
|
||||||
icon="fa-1x fa-thumb-tack"
|
icon="fa-1x fa-thumb-tack"
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
HOTEL WUBOOK PROTOTYPE (NOT USING ODOO CONNECTOR)
|
HOTEL CHANNEL CONNECTOR
|
||||||
=============
|
=============
|
||||||
- Experimental Connector for WuBook
|
Base implementation for hotel channels
|
||||||
- Active push notifications
|
|
||||||
- Create reservations
|
|
||||||
- Update availability
|
|
||||||
- Handle rooms
|
|
||||||
|
|
||||||
** UNDER DEVELOPMENT: NOT USE IN PRODUCTION **
|
** UNDER DEVELOPMENT: NOT USE IN PRODUCTION **
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,5 @@
|
|||||||
# 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 components
|
||||||
from . import controllers
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import wizard
|
from . import wizard
|
||||||
|
|||||||
@@ -18,7 +18,6 @@
|
|||||||
'python': ['xmlrpc']
|
'python': ['xmlrpc']
|
||||||
},
|
},
|
||||||
'data': [
|
'data': [
|
||||||
'data/cron_jobs.xml',
|
|
||||||
'views/hotel_channel_connector_issue_views.xml',
|
'views/hotel_channel_connector_issue_views.xml',
|
||||||
'views/inherited_hotel_reservation_views.xml',
|
'views/inherited_hotel_reservation_views.xml',
|
||||||
'views/inherited_hotel_room_type_views.xml',
|
'views/inherited_hotel_room_type_views.xml',
|
||||||
@@ -40,7 +39,6 @@
|
|||||||
'views/channel_ota_info_views.xml',
|
'views/channel_ota_info_views.xml',
|
||||||
'wizard/inherited_massive_changes.xml',
|
'wizard/inherited_massive_changes.xml',
|
||||||
'data/menus.xml',
|
'data/menus.xml',
|
||||||
'data/sequences.xml',
|
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
#'security/wubook_security.xml',
|
#'security/wubook_security.xml',
|
||||||
# 'views/res_config.xml'
|
# 'views/res_config.xml'
|
||||||
|
|||||||
@@ -1,104 +1,17 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import xmlrpc.client
|
|
||||||
import logging
|
|
||||||
from odoo.addons.component.core import AbstractComponent
|
from odoo.addons.component.core import AbstractComponent
|
||||||
from odoo.addons.queue_job.exception import RetryableJobError
|
|
||||||
from odoo.tools import (
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
|
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import fields, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# GLOBAL VARS
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT = "%d/%m/%Y"
|
|
||||||
DEFAULT_WUBOOK_TIME_FORMAT = "%H:%M"
|
|
||||||
DEFAULT_WUBOOK_DATETIME_FORMAT = "%s %s" % (DEFAULT_WUBOOK_DATE_FORMAT,
|
|
||||||
DEFAULT_WUBOOK_TIME_FORMAT)
|
|
||||||
WUBOOK_STATUS_CONFIRMED = 1
|
|
||||||
WUBOOK_STATUS_WAITING = 2
|
|
||||||
WUBOOK_STATUS_REFUSED = 3
|
|
||||||
WUBOOK_STATUS_ACCEPTED = 4
|
|
||||||
WUBOOK_STATUS_CANCELLED = 5
|
|
||||||
WUBOOK_STATUS_CANCELLED_PENALTY = 6
|
|
||||||
|
|
||||||
WUBOOK_STATUS_GOOD = (
|
|
||||||
WUBOOK_STATUS_CONFIRMED,
|
|
||||||
WUBOOK_STATUS_WAITING,
|
|
||||||
WUBOOK_STATUS_ACCEPTED,
|
|
||||||
)
|
|
||||||
WUBOOK_STATUS_BAD = (
|
|
||||||
WUBOOK_STATUS_REFUSED,
|
|
||||||
WUBOOK_STATUS_CANCELLED,
|
|
||||||
WUBOOK_STATUS_CANCELLED_PENALTY,
|
|
||||||
)
|
|
||||||
|
|
||||||
class WuBookLogin(object):
|
|
||||||
def __init__(self, address, user, passwd, lcode, pkey):
|
|
||||||
self.address = address
|
|
||||||
self.user = user
|
|
||||||
self.passwd = passwd
|
|
||||||
self.lcode = lcode
|
|
||||||
self.pkey = pkey
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
return self.address and self.user and self.passwd and self.lcode and self.pkey
|
|
||||||
|
|
||||||
class WuBookServer(object):
|
|
||||||
def __init__(self, login_data):
|
|
||||||
self._server = None
|
|
||||||
self._token = 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 not self._login_data.is_valid():
|
|
||||||
raise ChannelConnectorError("Invalid Channel Parameters!")
|
|
||||||
if self._server is None:
|
|
||||||
try:
|
|
||||||
self._server = xmlrpc.client.ServerProxy(self._login_data.address)
|
|
||||||
res, tok = self._server.acquire_token(
|
|
||||||
self._login_data.user,
|
|
||||||
self._login_data.passwd,
|
|
||||||
self._login_data.pkey)
|
|
||||||
if res == 0:
|
|
||||||
self._token = tok
|
|
||||||
else:
|
|
||||||
self._server = None
|
|
||||||
except Exception:
|
|
||||||
self._server = None
|
|
||||||
raise RetryableJobError("Can't connect with channel!")
|
|
||||||
return self._server
|
|
||||||
|
|
||||||
@property
|
|
||||||
def session_token(self):
|
|
||||||
return self._token
|
|
||||||
|
|
||||||
@property
|
|
||||||
def lcode(self):
|
|
||||||
return self._login_data.lcode
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self._server.release_token(self._token)
|
|
||||||
self._token = None
|
|
||||||
self._server = None
|
|
||||||
|
|
||||||
class HotelChannelInterfaceAdapter(AbstractComponent):
|
class HotelChannelInterfaceAdapter(AbstractComponent):
|
||||||
_name = 'hotel.channel.adapter'
|
_name = 'hotel.channel.adapter'
|
||||||
_inherit = ['base.backend.adapter', 'base.hotel.channel.connector']
|
_inherit = ['base.backend.adapter', 'base.hotel.channel.connector']
|
||||||
_usage = 'backend.adapter'
|
_usage = 'backend.adapter'
|
||||||
|
|
||||||
|
def push_activation(self, base_url, security_token):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
def create_room(self, shortcode, name, capacity, price, availability):
|
def create_room(self, shortcode, name, capacity, price, availability):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -186,7 +99,7 @@ class HotelChannelInterfaceAdapter(AbstractComponent):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'You must provide a channel_api attribute with a '
|
'You must provide a channel_api attribute with a '
|
||||||
'WuBookServer instance to be able to use the '
|
'ChannelServer instance to be able to use the '
|
||||||
'Backend Adapter.'
|
'Backend Adapter.'
|
||||||
)
|
)
|
||||||
return channel_server.server
|
return channel_server.server
|
||||||
@@ -198,367 +111,7 @@ class HotelChannelInterfaceAdapter(AbstractComponent):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'You must provide a channel_api attribute with a '
|
'You must provide a channel_api attribute with a '
|
||||||
'WuBookServer instance to be able to use the '
|
'ChannelServer instance to be able to use the '
|
||||||
'Backend Adapter.'
|
'Backend Adapter.'
|
||||||
)
|
)
|
||||||
return (channel_server.session_token, channel_server._login_data.lcode)
|
return (channel_server.session_token, channel_server._login_data.lcode)
|
||||||
|
|
||||||
class WuBookAdapter(AbstractComponent):
|
|
||||||
_name = 'wubook.adapter'
|
|
||||||
_inherit = 'hotel.channel.adapter'
|
|
||||||
|
|
||||||
# === ROOMS
|
|
||||||
def create_room(self, shortcode, name, capacity, price, availability):
|
|
||||||
rcode, results = self._server.new_room(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
0,
|
|
||||||
name,
|
|
||||||
capacity,
|
|
||||||
price,
|
|
||||||
availability,
|
|
||||||
shortcode[:4],
|
|
||||||
'nb' # TODO: Complete this part
|
|
||||||
# rtype=('name' in vals and vals['name'] and 3) or 1
|
|
||||||
)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't create room in WuBook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def modify_room(self, channel_room_id, name, capacity, price, availability, scode):
|
|
||||||
rcode, results = self._server.mod_room(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_room_id,
|
|
||||||
name,
|
|
||||||
capacity,
|
|
||||||
price,
|
|
||||||
availability,
|
|
||||||
scode,
|
|
||||||
'nb'
|
|
||||||
# rtype=('name' in vals and vals['name'] and 3) or 1
|
|
||||||
)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't modify room in WuBook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_id': channel_room_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def delete_room(self, channel_room_id):
|
|
||||||
rcode, results = self._server.del_room(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_room_id)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't delete room in WuBook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_id': channel_room_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_rooms(self, channel_room_id=0):
|
|
||||||
rcode, results = self._server.fetch_rooms(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_room_id)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't fetch room values from WuBook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_id': channel_room_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_rooms_values(self, date_from, date_to, rooms=False):
|
|
||||||
rcode, results = self._server.fetch_rooms_values(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
rooms)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't fetch rooms values from WuBook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_availability(self, rooms_avail):
|
|
||||||
rcode, results = self._server.update_sparse_avail(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
rooms_avail)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't update rooms availability in WuBook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def corporate_fetch(self):
|
|
||||||
rcode, results = self._server.corporate_fetchable_properties(self.TOKEN)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't call 'corporate_fetch' from WuBook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
# === RESERVATIONS
|
|
||||||
def create_reservation(self, channel_room_id, customer_name, email, city,
|
|
||||||
phone, address, country_code, checkin, checkout,
|
|
||||||
adults, children, notes=''):
|
|
||||||
customer_name = _partner_split_name(customer_name)
|
|
||||||
customer = {
|
|
||||||
'lname': customer_name[0],
|
|
||||||
'fname': customer_name[1],
|
|
||||||
'email': email,
|
|
||||||
'city': city,
|
|
||||||
'phone': phone,
|
|
||||||
'street': address,
|
|
||||||
'country': country_code,
|
|
||||||
'arrival_hour': fields.Datetime.from_string(checkin).strftime("%H:%M"),
|
|
||||||
'notes': notes
|
|
||||||
}
|
|
||||||
rcode, results = self._server.new_reservation(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
fields.Date.from_string(checkin).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
fields.Date.from_string(checkout).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
{channel_room_id: [adults+children, 'nb']},
|
|
||||||
customer,
|
|
||||||
adults+children)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't create reservations in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'date_from': checkin,
|
|
||||||
'date_to': checkout,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def cancel_reservation(self, channel_reservation_id, reason=""):
|
|
||||||
rcode, results = self._server.cancel_reservation(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_reservation_id,
|
|
||||||
reason)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't cancel reservation in WuBook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_reservation_id': channel_reservation_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_new_bookings(self):
|
|
||||||
rcode, results = self._server.fetch_new_bookings(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
1,
|
|
||||||
0)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't process reservations from wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_booking(self, channel_reservation_id):
|
|
||||||
rcode, results = self._server.fetch_booking(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_reservation_id)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't process reservation from wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def mark_bookings(self, channel_reservation_ids):
|
|
||||||
rcode, results = self._server.mark_bookings(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_reservation_ids)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't mark as readed a reservation in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_reservation_ids': str(channel_reservation_ids),
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
# === PRICE PLANS
|
|
||||||
def create_plan(self, name, daily=1):
|
|
||||||
rcode, results = self._server.add_pricing_plan(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
name,
|
|
||||||
daily)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't add pricing plan to wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def delete_plan(self, channel_plan_id):
|
|
||||||
rcode, results = self._server.del_plan(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_plan_id)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't delete pricing plan from wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_plan_id': channel_plan_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_plan_name(self, channel_plan_id, new_name):
|
|
||||||
rcode, results = self._server.update_plan_name(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_plan_id,
|
|
||||||
new_name)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't update pricing plan name in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_plan_id': channel_plan_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_plan_prices(self, channel_plan_id, date_from, prices):
|
|
||||||
rcode, results = self._server.update_plan_prices(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_plan_id,
|
|
||||||
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
prices)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't update pricing plan in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_plan_id': channel_plan_id,
|
|
||||||
'date_from': date_from,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_plan_periods(self, channel_plan_id, periods):
|
|
||||||
rcode, results = self._server.update_plan_periods(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_plan_id,
|
|
||||||
periods)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't update pricing plan period in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_plan_id': channel_plan_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_pricing_plans(self):
|
|
||||||
rcode, results = self._server.get_pricing_plans(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1])
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't get pricing plans from wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def fetch_plan_prices(self, channel_plan_id, date_from, date_to, rooms):
|
|
||||||
rcode, results = self._server.fetch_plan_prices(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_plan_id,
|
|
||||||
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
rooms or [])
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't get pricing plans from wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_plan_id': channel_plan_id,
|
|
||||||
'date_from': date_from,
|
|
||||||
'date_to': date_to
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
# === RESTRICTIONS
|
|
||||||
def rplan_rplans(self):
|
|
||||||
rcode, results = self._server.rplan_rplans(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1])
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't fetch restriction plans from wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def wired_rplan_get_rplan_values(self, date_from, date_to, channel_restriction_plan_id):
|
|
||||||
rcode, results = self._server.wired_rplan_get_rplan_values(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
int(channel_restriction_plan_id))
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't fetch restriction plans from wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_restriction_plan_id': channel_restriction_plan_id,
|
|
||||||
'date_from': date_from,
|
|
||||||
'date_to': date_to,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def update_rplan_values(self, channel_restriction_plan_id, date_from, values):
|
|
||||||
rcode, results = self._server.rplan_update_rplan_values(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_restriction_plan_id,
|
|
||||||
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
values)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't update plan restrictions on wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_restriction_plan_id': channel_restriction_plan_id,
|
|
||||||
'date_from': date_from,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def create_rplan(self, name, compact=False):
|
|
||||||
rcode, results = self._server.rplan_add_rplan(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
name,
|
|
||||||
compact and 1 or 0)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't create plan restriction in wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def rename_rplan(self, channel_restriction_plan_id, new_name):
|
|
||||||
rcode, results = self._server.rplan_rename_rplan(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_restriction_plan_id,
|
|
||||||
new_name)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't rename plan restriction in wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_restriction_plan_id': channel_restriction_plan_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def delete_rplan(self, channel_restriction_plan_id):
|
|
||||||
rcode, results = self._server.rplan_del_rplan(
|
|
||||||
self._session_info[0],
|
|
||||||
self._session_info[1],
|
|
||||||
channel_restriction_plan_id)
|
|
||||||
if rcode != 0:
|
|
||||||
raise ChannelConnectorError("Can't delete plan restriction on wubook", {
|
|
||||||
'message': results,
|
|
||||||
'channel_restriction_plan_id': channel_restriction_plan_id,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def get_channels_info(self):
|
|
||||||
results = self._server.get_channels_info(self._session_info[0])
|
|
||||||
if not any(results):
|
|
||||||
raise ChannelConnectorError("Can't import channels info from wubook", {
|
|
||||||
'message': results,
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
|
|
||||||
|
|
||||||
class HotelConnectorModelBinder(Component):
|
class HotelConnectorModelBinder(Component):
|
||||||
_name = 'hotel.channel.connector.binder'
|
_name = 'hotel.channel.connector.binder'
|
||||||
_inherit = ['base.binder', 'base.hotel.channel.connector']
|
_inherit = ['base.binder', 'base.hotel.channel.connector']
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
from odoo.addons.component.core import AbstractComponent
|
from odoo.addons.component.core import AbstractComponent
|
||||||
from odoo import api
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
class BaseHotelChannelConnectorComponent(AbstractComponent):
|
class BaseHotelChannelConnectorComponent(AbstractComponent):
|
||||||
_name = 'base.hotel.channel.connector'
|
_name = 'base.hotel.channel.connector'
|
||||||
_inherit = 'base.connector'
|
_inherit = 'base.connector'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from odoo.addons.component.core import AbstractComponent
|
from odoo.addons.component.core import AbstractComponent
|
||||||
|
|
||||||
|
|
||||||
class HotelChannelConnectorDeleter(AbstractComponent):
|
class HotelChannelConnectorDeleter(AbstractComponent):
|
||||||
_name = 'hotel.channel.deleter'
|
_name = 'hotel.channel.deleter'
|
||||||
_inherit = ['base.deleter', 'base.hotel.channel.connector']
|
_inherit = ['base.deleter', 'base.hotel.channel.connector']
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
from odoo.addons.component.core import AbstractComponent
|
from odoo.addons.component.core import AbstractComponent
|
||||||
|
|
||||||
|
|
||||||
class HotelChannelConnectorImporter(AbstractComponent):
|
class HotelChannelConnectorImporter(AbstractComponent):
|
||||||
_name = 'hotel.channel.importer'
|
_name = 'hotel.channel.importer'
|
||||||
_inherit = ['base.importer', 'base.hotel.channel.connector']
|
_inherit = ['base.importer', 'base.hotel.channel.connector']
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
import os
|
||||||
import binascii
|
import binascii
|
||||||
from contextlib import contextmanager
|
|
||||||
from odoo import models, api, fields
|
from odoo import models, api, fields
|
||||||
from ...components.backend_adapter import WuBookLogin, WuBookServer
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ChannelBackend(models.Model):
|
class ChannelBackend(models.Model):
|
||||||
_name = 'channel.backend'
|
_name = 'channel.backend'
|
||||||
@@ -21,7 +18,10 @@ class ChannelBackend(models.Model):
|
|||||||
to add a version from an ``_inherit`` does not constrain
|
to add a version from an ``_inherit`` does not constrain
|
||||||
to redefine the ``version`` field in the ``_inherit`` model.
|
to redefine the ``version`` field in the ``_inherit`` model.
|
||||||
"""
|
"""
|
||||||
return [('1.2', '1.2+')]
|
return []
|
||||||
|
|
||||||
|
def _get_default_server(self):
|
||||||
|
return ''
|
||||||
|
|
||||||
name = fields.Char('Name')
|
name = fields.Char('Name')
|
||||||
version = fields.Selection(selection='select_versions', required=True)
|
version = fields.Selection(selection='select_versions', required=True)
|
||||||
@@ -29,26 +29,24 @@ class ChannelBackend(models.Model):
|
|||||||
passwd = fields.Char('Channel Service Password')
|
passwd = fields.Char('Channel Service Password')
|
||||||
lcode = fields.Char('Channel Service lcode')
|
lcode = fields.Char('Channel Service lcode')
|
||||||
server = fields.Char('Channel Service Server',
|
server = fields.Char('Channel Service Server',
|
||||||
default='https://wired.wubook.net/xrws/')
|
default=_get_default_server)
|
||||||
pkey = fields.Char('Channel Service PKey')
|
pkey = fields.Char('Channel Service PKey')
|
||||||
security_token = fields.Char('Channel Service Security Token')
|
security_token = fields.Char('Channel Service Security Token')
|
||||||
|
|
||||||
reservation_id_str = fields.Char('Channel Reservation ID', store=False)
|
reservation_id_str = fields.Char('Channel Reservation ID')
|
||||||
|
|
||||||
avail_from = fields.Date('Availability From', store=False)
|
avail_from = fields.Date('Availability From')
|
||||||
avail_to = fields.Date('Availability To', store=False)
|
avail_to = fields.Date('Availability To')
|
||||||
|
|
||||||
restriction_from = fields.Date('Restriction From', store=False)
|
restriction_from = fields.Date('Restriction From')
|
||||||
restriction_to = fields.Date('Restriction To', store=False)
|
restriction_to = fields.Date('Restriction To')
|
||||||
restriction_id = fields.Many2one('channel.hotel.room.type.restriction',
|
restriction_id = fields.Many2one('channel.hotel.room.type.restriction',
|
||||||
'Channel Restriction',
|
'Channel Restriction')
|
||||||
store=False)
|
|
||||||
|
|
||||||
pricelist_from = fields.Date('Pricelist From', store=False)
|
pricelist_from = fields.Date('Pricelist From')
|
||||||
pricelist_to = fields.Date('Pricelist To', store=False)
|
pricelist_to = fields.Date('Pricelist To')
|
||||||
pricelist_id = fields.Many2one('channel.product.pricelist',
|
pricelist_id = fields.Many2one('channel.product.pricelist',
|
||||||
'Channel Product Pricelist',
|
'Channel Product Pricelist')
|
||||||
store=False)
|
|
||||||
|
|
||||||
issue_ids = fields.One2many('hotel.channel.connector.issue',
|
issue_ids = fields.One2many('hotel.channel.connector.issue',
|
||||||
'backend_id',
|
'backend_id',
|
||||||
@@ -60,19 +58,28 @@ class ChannelBackend(models.Model):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def generate_key(self):
|
def generate_key(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.security_token = binascii.hexlify(os.urandom(32)).decode()
|
record.security_token = binascii.hexlify(os.urandom(16)).decode()
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def synchronize_push_urls(self):
|
||||||
|
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
|
||||||
|
base_url = base_url.replace("http://", "https://")
|
||||||
|
channel_ota_info_obj = self.env['channel.ota.info']
|
||||||
|
for record in self:
|
||||||
|
channel_ota_info_obj.push_activation(record, base_url)
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def import_reservations(self):
|
def import_reservations(self):
|
||||||
channel_hotel_reservation_obj = self.env['channel.hotel.reservation']
|
channel_hotel_reservation_obj = self.env['channel.hotel.reservation']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
count = channel_hotel_reservation_obj.import_reservations(backend)
|
count = channel_hotel_reservation_obj.import_reservations(backend)
|
||||||
if count == 0:
|
if self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_info("No reservations to import. All done :)",
|
if count == 0:
|
||||||
title="Import Reservations")
|
self.env.user.notify_info("No reservations to import. All done :)",
|
||||||
else:
|
title="Import Reservations")
|
||||||
self.env.user.notify_info("%d reservations successfully imported" % count,
|
else:
|
||||||
title="Import Reservations")
|
self.env.user.notify_info("%d reservations successfully imported" % count,
|
||||||
|
title="Import Reservations")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -82,7 +89,7 @@ class ChannelBackend(models.Model):
|
|||||||
res = channel_hotel_reservation_obj.import_reservation(
|
res = channel_hotel_reservation_obj.import_reservation(
|
||||||
backend,
|
backend,
|
||||||
backend.reservation_id_str)
|
backend.reservation_id_str)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning(
|
self.env.user.notify_warning(
|
||||||
"Can't import '%s' reservation" % backend.reservation_id_str,
|
"Can't import '%s' reservation" % backend.reservation_id_str,
|
||||||
title="Import Reservations")
|
title="Import Reservations")
|
||||||
@@ -93,12 +100,13 @@ class ChannelBackend(models.Model):
|
|||||||
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
|
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
count = channel_hotel_room_type_obj.import_rooms(backend)
|
count = channel_hotel_room_type_obj.import_rooms(backend)
|
||||||
if count == 0:
|
if self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_info("No rooms to import. All done :)",
|
if count == 0:
|
||||||
title="Import Rooms")
|
self.env.user.notify_info("No rooms to import. All done :)",
|
||||||
else:
|
title="Import Rooms")
|
||||||
self.env.user.notify_info("%d rooms successfully imported" % count,
|
else:
|
||||||
title="Import Rooms")
|
self.env.user.notify_info("%d rooms successfully imported" % count,
|
||||||
|
title="Import Rooms")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -106,8 +114,9 @@ class ChannelBackend(models.Model):
|
|||||||
channel_ota_info_obj = self.env['channel.ota.info']
|
channel_ota_info_obj = self.env['channel.ota.info']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
count = channel_ota_info_obj.import_otas_info(backend)
|
count = channel_ota_info_obj.import_otas_info(backend)
|
||||||
self.env.user.notify_info("%d ota's successfully imported" % count,
|
if self.env.context.get('show_notify', True):
|
||||||
title="Import OTA's")
|
self.env.user.notify_info("%d ota's successfully imported" % count,
|
||||||
|
title="Import OTA's")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -118,7 +127,7 @@ class ChannelBackend(models.Model):
|
|||||||
backend,
|
backend,
|
||||||
backend.avail_from,
|
backend.avail_from,
|
||||||
backend.avail_to)
|
backend.avail_to)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error importing availability",
|
self.env.user.notify_warning("Error importing availability",
|
||||||
title="Import Availability")
|
title="Import Availability")
|
||||||
return True
|
return True
|
||||||
@@ -128,7 +137,7 @@ class ChannelBackend(models.Model):
|
|||||||
channel_hotel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
channel_hotel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
res = channel_hotel_room_type_avail_obj.push_availability(backend)
|
res = channel_hotel_room_type_avail_obj.push_availability(backend)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error pushing availability",
|
self.env.user.notify_warning("Error pushing availability",
|
||||||
title="Export Availability")
|
title="Export Availability")
|
||||||
return True
|
return True
|
||||||
@@ -138,12 +147,13 @@ class ChannelBackend(models.Model):
|
|||||||
channel_hotel_room_type_restr_obj = self.env['channel.hotel.room.type.restriction']
|
channel_hotel_room_type_restr_obj = self.env['channel.hotel.room.type.restriction']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
count = channel_hotel_room_type_restr_obj.import_restriction_plans(backend)
|
count = channel_hotel_room_type_restr_obj.import_restriction_plans(backend)
|
||||||
if count == 0:
|
if self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_info("No restiction plans to import. All done :)",
|
if count == 0:
|
||||||
title="Import Restrictions")
|
self.env.user.notify_info("No restiction plans to import. All done :)",
|
||||||
else:
|
title="Import Restrictions")
|
||||||
self.env.user.notify_info("%d restriction plans successfully imported" % count,
|
else:
|
||||||
title="Import Restrictions")
|
self.env.user.notify_info("%d restriction plans successfully imported" % count,
|
||||||
|
title="Import Restrictions")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -155,7 +165,7 @@ class ChannelBackend(models.Model):
|
|||||||
backend.restriction_from,
|
backend.restriction_from,
|
||||||
backend.restriction_to,
|
backend.restriction_to,
|
||||||
backend.restriction_id and backend.restriction_id.external_id or False)
|
backend.restriction_id and backend.restriction_id.external_id or False)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error importing restrictions",
|
self.env.user.notify_warning("Error importing restrictions",
|
||||||
title="Import Restrictions")
|
title="Import Restrictions")
|
||||||
return True
|
return True
|
||||||
@@ -165,7 +175,7 @@ class ChannelBackend(models.Model):
|
|||||||
channel_hotel_restr_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
channel_hotel_restr_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
res = channel_hotel_restr_item_obj.push_restriction(backend)
|
res = channel_hotel_restr_item_obj.push_restriction(backend)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error pushing restrictions",
|
self.env.user.notify_warning("Error pushing restrictions",
|
||||||
title="Export Restrictions")
|
title="Export Restrictions")
|
||||||
return True
|
return True
|
||||||
@@ -175,12 +185,13 @@ class ChannelBackend(models.Model):
|
|||||||
channel_product_pricelist_obj = self.env['channel.product.pricelist']
|
channel_product_pricelist_obj = self.env['channel.product.pricelist']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
count = channel_product_pricelist_obj.import_price_plans(backend)
|
count = channel_product_pricelist_obj.import_price_plans(backend)
|
||||||
if count == 0:
|
if self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_info("No pricelist plans to import. All done :)",
|
if count == 0:
|
||||||
title="Import Pricelists")
|
self.env.user.notify_info("No pricelist plans to import. All done :)",
|
||||||
else:
|
title="Import Pricelists")
|
||||||
self.env.user.notify_info("%d pricelist plans successfully imported" % count,
|
else:
|
||||||
title="Import Pricelists")
|
self.env.user.notify_info("%d pricelist plans successfully imported" % count,
|
||||||
|
title="Import Pricelists")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
@@ -192,7 +203,7 @@ class ChannelBackend(models.Model):
|
|||||||
backend.pricelist_from,
|
backend.pricelist_from,
|
||||||
backend.pricelist_to,
|
backend.pricelist_to,
|
||||||
backend.pricelist_id and backend.pricelist_id.external_id or False)
|
backend.pricelist_id and backend.pricelist_id.external_id or False)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error importing pricelists",
|
self.env.user.notify_warning("Error importing pricelists",
|
||||||
title="Import Pricelists")
|
title="Import Pricelists")
|
||||||
return True
|
return True
|
||||||
@@ -202,7 +213,7 @@ class ChannelBackend(models.Model):
|
|||||||
channel_product_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
channel_product_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
||||||
for backend in self:
|
for backend in self:
|
||||||
res = channel_product_pricelist_item_obj.push_pricelist(backend)
|
res = channel_product_pricelist_item_obj.push_pricelist(backend)
|
||||||
if not res:
|
if not res and self.env.context.get('show_notify', True):
|
||||||
self.env.user.notify_warning("Error pushing pricelists",
|
self.env.user.notify_warning("Error pushing pricelists",
|
||||||
title="Export Pricelists")
|
title="Export Pricelists")
|
||||||
return True
|
return True
|
||||||
@@ -217,18 +228,3 @@ class ChannelBackend(models.Model):
|
|||||||
@api.model
|
@api.model
|
||||||
def cron_import_reservations(self):
|
def cron_import_reservations(self):
|
||||||
self.env[self._name].search([]).import_reservations()
|
self.env[self._name].search([]).import_reservations()
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
@api.multi
|
|
||||||
def work_on(self, model_name, **kwargs):
|
|
||||||
self.ensure_one()
|
|
||||||
wubook_login = WuBookLogin(
|
|
||||||
self.server,
|
|
||||||
self.username,
|
|
||||||
self.passwd,
|
|
||||||
self.lcode,
|
|
||||||
self.pkey)
|
|
||||||
with WuBookServer(wubook_login) as channel_api:
|
|
||||||
_super = super(ChannelBackend, self)
|
|
||||||
with _super.work_on(model_name, channel_api=channel_api, **kwargs) as work:
|
|
||||||
yield work
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# 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 odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
from odoo.addons.queue_job.job import job
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelBinding(models.AbstractModel):
|
class ChannelBinding(models.AbstractModel):
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
from odoo import api, models, fields
|
from odoo import api, models, fields
|
||||||
from odoo.addons.queue_job.job import job
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.component.core import Component
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelOtaInfo(models.Model):
|
class ChannelOtaInfo(models.Model):
|
||||||
@@ -13,7 +12,6 @@ class ChannelOtaInfo(models.Model):
|
|||||||
|
|
||||||
ota_id = fields.Char("Channel OTA ID", required=True)
|
ota_id = fields.Char("Channel OTA ID", required=True)
|
||||||
name = fields.Char("OTA Name", required=True)
|
name = fields.Char("OTA Name", required=True)
|
||||||
ical = fields.Boolean("ical", default=False)
|
|
||||||
|
|
||||||
@job(default_channel='root.channel')
|
@job(default_channel='root.channel')
|
||||||
@api.model
|
@api.model
|
||||||
@@ -22,10 +20,9 @@ class ChannelOtaInfo(models.Model):
|
|||||||
importer = work.component(usage='ota.info.importer')
|
importer = work.component(usage='ota.info.importer')
|
||||||
return importer.import_otas_info()
|
return importer.import_otas_info()
|
||||||
|
|
||||||
class HotelRoomTypeAdapter(Component):
|
@job(default_channel='root.channel')
|
||||||
_name = 'channel.ota.info.adapter'
|
@api.model
|
||||||
_inherit = 'wubook.adapter'
|
def push_activation(self, backend, base_url):
|
||||||
_apply_on = 'channel.ota.info'
|
with backend.work_on(self._name) as work:
|
||||||
|
importer = work.component(usage='ota.info.importer')
|
||||||
def fetch_rooms(self):
|
return importer.push_activation(base_url)
|
||||||
return super(HotelRoomTypeAdapter, self).fetch_rooms()
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# 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 odoo.exceptions import ValidationError
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.connector.components.mapper import mapping
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import fields, api, _
|
|
||||||
from odoo.tools import (
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelOtaInfoImporter(Component):
|
class ChannelOtaInfoImporter(Component):
|
||||||
@@ -19,51 +13,8 @@ class ChannelOtaInfoImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_otas_info(self):
|
def import_otas_info(self):
|
||||||
count = 0
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.get_channels_info()
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='room',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
channel_ota_info_obj = self.env['channel.ota.info']
|
|
||||||
ota_info_mapper = self.component(usage='import.mapper',
|
|
||||||
model_name='channel.ota.info')
|
|
||||||
for ota_id in results.keys():
|
|
||||||
vals = {
|
|
||||||
'id': ota_id,
|
|
||||||
'name': results[ota_id]['name'],
|
|
||||||
'ical': results[ota_id]['ical'] == 1,
|
|
||||||
}
|
|
||||||
map_record = ota_info_mapper.map_record(vals)
|
|
||||||
ota_info_bind = channel_ota_info_obj.search([
|
|
||||||
('ota_id', '=', ota_id)
|
|
||||||
], limit=1)
|
|
||||||
if ota_info_bind:
|
|
||||||
ota_info_bind.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).write(map_record.values())
|
|
||||||
else:
|
|
||||||
ota_info_bind.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).create(map_record.values(for_create=True))
|
|
||||||
count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
@api.model
|
||||||
class ChannelOtaInfoImportMapper(Component):
|
def push_activation(self, base_url):
|
||||||
_name = 'channel.ota.info.import.mapper'
|
raise NotImplementedError
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.ota.info'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('id', 'ota_id'),
|
|
||||||
('name', 'name'),
|
|
||||||
('ical', 'ical'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -29,6 +29,14 @@ class HotelChannelConnectorIssue(models.Model):
|
|||||||
channel_object_id = fields.Char("Channel Object ID", old_name='wid', readonly=True)
|
channel_object_id = fields.Char("Channel Object ID", old_name='wid', readonly=True)
|
||||||
channel_message = fields.Char("Channel Message", old_name='wmessage', readonly=True)
|
channel_message = fields.Char("Channel Message", old_name='wmessage', readonly=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
issue_id = super(HotelChannelConnectorIssue, self).create(vals)
|
||||||
|
self.env.user.notify_warning(
|
||||||
|
issue_id.internal_message or issue_id.channel_message,
|
||||||
|
title=_("Oops! %s Issue Reported!!") % issue_id.section)
|
||||||
|
return issue_id
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def mark_readed(self):
|
def mark_readed(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
|
|||||||
@@ -2,18 +2,11 @@
|
|||||||
# 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 odoo import api, models, fields, _
|
from odoo import api, models, fields, _
|
||||||
from odoo.exceptions import ValidationError, UserError
|
from odoo.exceptions import UserError
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
WUBOOK_STATUS_CONFIRMED,
|
|
||||||
WUBOOK_STATUS_WAITING,
|
|
||||||
WUBOOK_STATUS_REFUSED,
|
|
||||||
WUBOOK_STATUS_ACCEPTED,
|
|
||||||
WUBOOK_STATUS_CANCELLED,
|
|
||||||
WUBOOK_STATUS_CANCELLED_PENALTY,
|
|
||||||
WUBOOK_STATUS_BAD)
|
|
||||||
|
|
||||||
class ChannelHotelReservation(models.Model):
|
class ChannelHotelReservation(models.Model):
|
||||||
_name = 'channel.hotel.reservation'
|
_name = 'channel.hotel.reservation'
|
||||||
@@ -36,12 +29,6 @@ class ChannelHotelReservation(models.Model):
|
|||||||
|
|
||||||
channel_status = fields.Selection([
|
channel_status = fields.Selection([
|
||||||
('0', 'No Channel'),
|
('0', 'No Channel'),
|
||||||
(str(WUBOOK_STATUS_CONFIRMED), 'Confirmed'),
|
|
||||||
(str(WUBOOK_STATUS_WAITING), 'Waiting'),
|
|
||||||
(str(WUBOOK_STATUS_REFUSED), 'Refused'),
|
|
||||||
(str(WUBOOK_STATUS_ACCEPTED), 'Accepted'),
|
|
||||||
(str(WUBOOK_STATUS_CANCELLED), 'Cancelled'),
|
|
||||||
(str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty'),
|
|
||||||
], string='Channel Status', default='0', readonly=True, old_name='wstatus')
|
], string='Channel Status', default='0', readonly=True, old_name='wstatus')
|
||||||
channel_status_reason = fields.Char("Channel Status Reason", readonly=True,
|
channel_status_reason = fields.Char("Channel Status Reason", readonly=True,
|
||||||
old_name='wstatus_reason')
|
old_name='wstatus_reason')
|
||||||
@@ -55,9 +42,9 @@ class ChannelHotelReservation(models.Model):
|
|||||||
|
|
||||||
@job(default_channel='root.channel')
|
@job(default_channel='root.channel')
|
||||||
@api.model
|
@api.model
|
||||||
def refresh_availability(self, checkin, checkout, product_id):
|
def refresh_availability(self, checkin, checkout, room_id):
|
||||||
self.env['channel.hotel.room.type.availability'].refresh_availability(
|
self.env['channel.hotel.room.type.availability'].refresh_availability(
|
||||||
checkin, checkout, product_id)
|
checkin, checkout, room_id)
|
||||||
|
|
||||||
@job(default_channel='root.channel')
|
@job(default_channel='root.channel')
|
||||||
@api.model
|
@api.model
|
||||||
@@ -87,11 +74,68 @@ class ChannelHotelReservation(models.Model):
|
|||||||
exporter = work.component(usage='hotel.reservation.exporter')
|
exporter = work.component(usage='hotel.reservation.exporter')
|
||||||
return exporter.mark_booking(self)
|
return exporter.mark_booking(self)
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def write(self, vals):
|
||||||
|
if self._context.get('connector_no_export', True) and \
|
||||||
|
(vals.get('checkin') or vals.get('checkout') or
|
||||||
|
vals.get('room_id') or vals.get('state')):
|
||||||
|
older_vals = []
|
||||||
|
new_vals = []
|
||||||
|
for record in self:
|
||||||
|
older_vals.append({
|
||||||
|
'checkin': record.checkin,
|
||||||
|
'checkout': record.checkout,
|
||||||
|
'room_id': record.room_id,
|
||||||
|
})
|
||||||
|
new_vals.append({
|
||||||
|
'checkin': vals.get('checkin', record.checkin),
|
||||||
|
'checkout': vals.get('checkout', record.checkout),
|
||||||
|
'room_id': vals.get('room_id', record.room_id),
|
||||||
|
})
|
||||||
|
|
||||||
|
res = super(ChannelHotelReservation, self).write(vals)
|
||||||
|
|
||||||
|
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||||
|
for k_i, v_i in enumerate(older_vals):
|
||||||
|
channel_room_type_avail_obj.refresh_availability(
|
||||||
|
v_i['checkin'],
|
||||||
|
v_i['checkout'],
|
||||||
|
v_i['room_id'])
|
||||||
|
channel_room_type_avail_obj.refresh_availability(
|
||||||
|
new_vals[k_i]['checkin'],
|
||||||
|
new_vals[k_i]['checkout'],
|
||||||
|
new_vals[k_i]['room_id'])
|
||||||
|
else:
|
||||||
|
res = super(ChannelHotelReservation, self).write(vals)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def unlink(self):
|
||||||
|
vals = []
|
||||||
|
for record in self:
|
||||||
|
if any(record.channel_bind_ids) and record.channel_bind_ids[0].external_id \
|
||||||
|
and not record.parent_reservation:
|
||||||
|
raise UserError(_("You can't delete OTA's reservations"))
|
||||||
|
vals.append({
|
||||||
|
'checkin': record.checkin,
|
||||||
|
'checkout': record.checkout,
|
||||||
|
'room_id': record.room_id.id,
|
||||||
|
})
|
||||||
|
res = super(ChannelHotelReservation, self).unlink()
|
||||||
|
if self._context.get('connector_no_export', True):
|
||||||
|
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||||
|
for record in vals:
|
||||||
|
channel_room_type_avail_obj.refresh_availability(
|
||||||
|
record['checkin'],
|
||||||
|
record['checkout'],
|
||||||
|
record['room_id'])
|
||||||
|
return res
|
||||||
|
|
||||||
class HotelReservation(models.Model):
|
class HotelReservation(models.Model):
|
||||||
_inherit = 'hotel.reservation'
|
_inherit = 'hotel.reservation'
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def _set_access_for_wubook_fields(self):
|
def _set_access_for_channel_fields(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
user = self.env['res.users'].browse(self.env.uid)
|
user = self.env['res.users'].browse(self.env.uid)
|
||||||
record.able_to_modify_channel = user.has_group('base.group_system')
|
record.able_to_modify_channel = user.has_group('base.group_system')
|
||||||
@@ -119,8 +163,8 @@ class HotelReservation(models.Model):
|
|||||||
is_from_ota = fields.Boolean('Is From OTA',
|
is_from_ota = fields.Boolean('Is From OTA',
|
||||||
readonly=True,
|
readonly=True,
|
||||||
old_name='wis_from_channel')
|
old_name='wis_from_channel')
|
||||||
able_to_modify_channel = fields.Boolean(compute=_set_access_for_wubook_fields,
|
able_to_modify_channel = fields.Boolean(compute=_set_access_for_channel_fields,
|
||||||
string='Is user able to modify wubook fields?',
|
string='Is user able to modify channel fields?',
|
||||||
old_name='able_to_modify_wubook')
|
old_name='able_to_modify_wubook')
|
||||||
to_read = fields.Boolean('To Read', default=False)
|
to_read = fields.Boolean('To Read', default=False)
|
||||||
customer_notes = fields.Text(related='folio_id.customer_notes',
|
customer_notes = fields.Text(related='folio_id.customer_notes',
|
||||||
@@ -135,101 +179,6 @@ class HotelReservation(models.Model):
|
|||||||
vals.update({'to_read': True})
|
vals.update({'to_read': True})
|
||||||
return super(HotelReservation, self).create(vals)
|
return super(HotelReservation, self).create(vals)
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def write(self, vals):
|
|
||||||
if self._context.get('wubook_action', True) and \
|
|
||||||
(vals.get('checkin') or vals.get('checkout') or
|
|
||||||
vals.get('product_id') or vals.get('state')):
|
|
||||||
older_vals = []
|
|
||||||
new_vals = []
|
|
||||||
for record in self:
|
|
||||||
prod_id = False
|
|
||||||
if record.product_id:
|
|
||||||
prod_id = record.product_id.id
|
|
||||||
older_vals.append({
|
|
||||||
'checkin': record.checkin,
|
|
||||||
'checkout': record.checkout,
|
|
||||||
'product_id': prod_id,
|
|
||||||
})
|
|
||||||
new_vals.append({
|
|
||||||
'checkin': vals.get('checkin', record.checkin),
|
|
||||||
'checkout': vals.get('checkout', record.checkout),
|
|
||||||
'product_id': vals.get('product_id', prod_id),
|
|
||||||
})
|
|
||||||
|
|
||||||
res = super(HotelReservation, self).write(vals)
|
|
||||||
|
|
||||||
room_type_avail_obj = self.env['hotel.room.type.availability']
|
|
||||||
for i in range(0, len(older_vals)):
|
|
||||||
room_type_avail_obj.refresh_availability(
|
|
||||||
older_vals[i]['checkin'],
|
|
||||||
older_vals[i]['checkout'],
|
|
||||||
older_vals[i]['product_id'])
|
|
||||||
room_type_avail_obj.refresh_availability(
|
|
||||||
new_vals[i]['checkin'],
|
|
||||||
new_vals[i]['checkout'],
|
|
||||||
new_vals[i]['product_id'])
|
|
||||||
else:
|
|
||||||
res = super(HotelReservation, self).write(vals)
|
|
||||||
return res
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def unlink(self):
|
|
||||||
vals = []
|
|
||||||
for record in self:
|
|
||||||
if any(record.channel_bind_ids) and record.channel_bind_ids[0].external_id \
|
|
||||||
and not record.parent_reservation:
|
|
||||||
raise UserError(_("You can't delete OTA's reservations"))
|
|
||||||
vals.append({
|
|
||||||
'checkin': record.checkin,
|
|
||||||
'checkout': record.checkout,
|
|
||||||
'product_id': record.product_id.id,
|
|
||||||
})
|
|
||||||
res = super(HotelReservation, self).unlink()
|
|
||||||
if self._context.get('wubook_action', True):
|
|
||||||
room_type_avail_obj = self.env['hotel.room.type.availability']
|
|
||||||
for record in vals:
|
|
||||||
room_type_avail_obj.refresh_availability(
|
|
||||||
record['checkin'],
|
|
||||||
record['checkout'],
|
|
||||||
record['product_id'])
|
|
||||||
return res
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def action_cancel(self):
|
|
||||||
no_export = self._context.get('connector_no_export', True)
|
|
||||||
if no_export:
|
|
||||||
for record in self:
|
|
||||||
# Can't cancel in Odoo
|
|
||||||
if record.is_from_ota:
|
|
||||||
raise ValidationError(_("Can't cancel reservations from OTA's"))
|
|
||||||
user = self.env['res.users'].browse(self.env.uid)
|
|
||||||
if user.has_group('hotel.group_hotel_call'):
|
|
||||||
self.write({'to_read': True, 'to_assign': True})
|
|
||||||
|
|
||||||
res = super(HotelReservation, self).action_cancel()
|
|
||||||
if no_export:
|
|
||||||
for record in self:
|
|
||||||
# Only can cancel reservations created directly in wubook
|
|
||||||
if any(record.channel_bind_ids) and \
|
|
||||||
record.channel_bind_ids[0].external_id and \
|
|
||||||
not record.channel_bind_ids[0].ota_id and \
|
|
||||||
record.channel_bind_ids[0].channel_status in ['1', '2', '4']:
|
|
||||||
self._event('on_record_cancel').notify(record)
|
|
||||||
return res
|
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def confirm(self):
|
|
||||||
can_confirm = True
|
|
||||||
for record in self:
|
|
||||||
if record.is_from_ota and any(record.channel_bind_ids) and \
|
|
||||||
int(record.channel_bind_ids[0].channel_status) in WUBOOK_STATUS_BAD:
|
|
||||||
can_confirm = False
|
|
||||||
break
|
|
||||||
if not can_confirm:
|
|
||||||
raise ValidationError(_("Can't confirm OTA's cancelled reservations"))
|
|
||||||
return super(HotelReservation, self).confirm()
|
|
||||||
|
|
||||||
# @api.multi
|
# @api.multi
|
||||||
# def generate_copy_values(self, checkin=False, checkout=False):
|
# def generate_copy_values(self, checkin=False, checkout=False):
|
||||||
# self.ensure_one()
|
# self.ensure_one()
|
||||||
@@ -267,30 +216,10 @@ class HotelReservation(models.Model):
|
|||||||
def mark_as_readed(self):
|
def mark_as_readed(self):
|
||||||
self.write({'to_read': False, 'to_assign': False})
|
self.write({'to_read': False, 'to_assign': False})
|
||||||
|
|
||||||
@api.onchange('checkin', 'checkout', 'product_id')
|
@api.onchange('checkin', 'checkout')
|
||||||
def on_change_checkin_checkout_product_id(self):
|
def onchange_dates(self):
|
||||||
if not self.is_from_ota:
|
if not self.is_from_ota:
|
||||||
return super().on_change_checkin_checkout_product_id()
|
return super().onchange_dates()
|
||||||
|
|
||||||
class HotelReservationAdapter(Component):
|
|
||||||
_name = 'channel.hotel.reservation.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.hotel.reservation'
|
|
||||||
|
|
||||||
def mark_bookings(self, channel_reservation_ids):
|
|
||||||
return super(HotelReservationAdapter, self).mark_bookings(
|
|
||||||
channel_reservation_ids)
|
|
||||||
|
|
||||||
def fetch_new_bookings(self):
|
|
||||||
return super(HotelReservationAdapter, self).fetch_new_bookings()
|
|
||||||
|
|
||||||
def fetch_booking(self, channel_reservation_id):
|
|
||||||
return super(HotelReservationAdapter, self).fetch_booking(
|
|
||||||
channel_reservation_id)
|
|
||||||
|
|
||||||
def cancel_reservation(self, channel_reservation_id, message):
|
|
||||||
return super(HotelReservationAdapter, self).cancel_reservation(
|
|
||||||
channel_reservation_id, message)
|
|
||||||
|
|
||||||
class ChannelBindingHotelReservationListener(Component):
|
class ChannelBindingHotelReservationListener(Component):
|
||||||
_name = 'channel.binding.hotel.reservation.listener'
|
_name = 'channel.binding.hotel.reservation.listener'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# 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 odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo import api, _
|
|
||||||
|
|
||||||
class HotelReservationExporter(Component):
|
class HotelReservationExporter(Component):
|
||||||
_name = 'channel.hotel.reservation.exporter'
|
_name = 'channel.hotel.reservation.exporter'
|
||||||
@@ -13,36 +13,12 @@ class HotelReservationExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def cancel_reservation(self, binding):
|
def cancel_reservation(self, binding):
|
||||||
user = self.env['res.user'].browse(self.env.uid)
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
return self.backend_adapter.cancel_reservation(
|
|
||||||
binding.external_id,
|
|
||||||
_('Cancelled by %s') % user.partner_id.name)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_object_id=binding.external_id,
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def mark_booking(self, binding):
|
def mark_booking(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.mark_bookings([binding.external_id])
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_object_id=binding.external_id,
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def mark_bookings(self, external_ids):
|
def mark_bookings(self, external_ids):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.mark_bookings(external_ids)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_object_id=external_ids,
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|||||||
@@ -1,22 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
from odoo import api
|
||||||
import json
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from dateutil import tz
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo import fields, api, _
|
|
||||||
from odoo.tools import (
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT,
|
|
||||||
DEFAULT_WUBOOK_DATETIME_FORMAT,
|
|
||||||
WUBOOK_STATUS_BAD)
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HotelReservationImporter(Component):
|
class HotelReservationImporter(Component):
|
||||||
@@ -27,430 +13,7 @@ class HotelReservationImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def fetch_booking(self, channel_reservation_id):
|
def fetch_booking(self, channel_reservation_id):
|
||||||
try:
|
raise NotImplementedError
|
||||||
results = self.backend_adapter.fetch_booking(channel_reservation_id)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
if any(results):
|
|
||||||
processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \
|
|
||||||
self._generate_reservations(results)
|
|
||||||
if any(processed_rids):
|
|
||||||
self.backend_adapter.mark_bookings(list(set(processed_rids)))
|
|
||||||
# Update Odoo availability (don't wait for wubook)
|
|
||||||
# FIXME: This cause abuse service in first import!!
|
|
||||||
if checkin_utc_dt and checkout_utc_dt:
|
|
||||||
self.backend_adapter.fetch_rooms_values(
|
|
||||||
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
checkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT))
|
|
||||||
return True
|
|
||||||
|
|
||||||
def fetch_new_bookings(self):
|
def fetch_new_bookings(self):
|
||||||
count = 0
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.fetch_new_bookings()
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
if any(results):
|
|
||||||
processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \
|
|
||||||
self._generate_reservations(results)
|
|
||||||
if any(processed_rids):
|
|
||||||
uniq_rids = list(set(processed_rids))
|
|
||||||
self.backend_adapter.mark_bookings(uniq_rids)
|
|
||||||
count = len(uniq_rids)
|
|
||||||
# Update Odoo availability (don't wait for wubook)
|
|
||||||
# FIXME: This cause abuse service in first import!!
|
|
||||||
if checkin_utc_dt and checkout_utc_dt:
|
|
||||||
self.backend_adapter.fetch_rooms_values(
|
|
||||||
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
checkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT))
|
|
||||||
return count
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _generate_booking_vals(self, broom, crcode, rcode, room_type_bind,
|
|
||||||
split_booking, dates_checkin, dates_checkout, book):
|
|
||||||
is_cancellation = book['status'] in WUBOOK_STATUS_BAD
|
|
||||||
tax_inclusive = True
|
|
||||||
persons = room_type_bind.channel_capacity
|
|
||||||
# Dates
|
|
||||||
checkin_str = dates_checkin[0].strftime(
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
checkout_str = dates_checkout[0].strftime(
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
# Parse 'ancyllary' info
|
|
||||||
if 'ancillary' in broom:
|
|
||||||
if 'guests' in broom['ancillary']:
|
|
||||||
persons = broom['ancillary']['guests']
|
|
||||||
if 'tax_inclusive' in broom['ancillary'] and not broom['ancillary']['tax_inclusive']:
|
|
||||||
_logger.info("--- Incoming Reservation without taxes included!")
|
|
||||||
tax_inclusive = False
|
|
||||||
# Generate Reservation Day Lines
|
|
||||||
reservation_lines = []
|
|
||||||
tprice = 0.0
|
|
||||||
for brday in broom['roomdays']:
|
|
||||||
wndate = datetime.strptime(
|
|
||||||
brday['day'],
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT
|
|
||||||
).replace(tzinfo=tz.gettz('UTC'))
|
|
||||||
if dates_checkin[0] >= wndate <= (dates_checkout[0] - timedelta(days=1)):
|
|
||||||
# HOT-FIX: Hard-Coded Tax 10%
|
|
||||||
room_day_price = round(brday['price'] * 1.1, 2) if not tax_inclusive else brday['price']
|
|
||||||
reservation_lines.append((0, False, {
|
|
||||||
'date': wndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
'price': room_day_price,
|
|
||||||
}))
|
|
||||||
tprice += room_day_price
|
|
||||||
# Get OTA
|
|
||||||
ota_id = self.env['channel.ota.info'].search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('ota_id', '=', str(book['id_channel'])),
|
|
||||||
], limit=1)
|
|
||||||
|
|
||||||
vals = {
|
|
||||||
'backend_id': self.backend_record.id,
|
|
||||||
'checkin': checkin_str,
|
|
||||||
'checkout': checkout_str,
|
|
||||||
'adults': persons,
|
|
||||||
'children': book['children'],
|
|
||||||
'reservation_lines': reservation_lines,
|
|
||||||
'price_unit': tprice,
|
|
||||||
'to_assign': True,
|
|
||||||
'wrid': rcode,
|
|
||||||
'ota_id': ota_id and ota_id.id,
|
|
||||||
'wchannel_reservation_code': crcode,
|
|
||||||
'channel_status': str(book['status']),
|
|
||||||
'to_read': True,
|
|
||||||
'state': is_cancellation and 'cancelled' or 'draft',
|
|
||||||
'room_type_id': room_type_bind.odoo_id.id,
|
|
||||||
'splitted': split_booking,
|
|
||||||
'wbook_json': json.dumps(book),
|
|
||||||
'wmodified': book['was_modified'],
|
|
||||||
'product_id': room_type_bind and room_type_bind.product_id.id,
|
|
||||||
'name': room_type_bind and room_type_bind.name,
|
|
||||||
}
|
|
||||||
return vals
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _generate_partner_vals(self, book):
|
|
||||||
country_id = self.env['res.country'].search([
|
|
||||||
('code', '=', str(book['customer_country']))
|
|
||||||
], limit=1)
|
|
||||||
# lang = self.env['res.lang'].search([('code', '=', book['customer_language_iso'])], limit=1)
|
|
||||||
return {
|
|
||||||
'name': "%s, %s" % (book['customer_surname'], book['customer_name']),
|
|
||||||
'country_id': country_id and country_id.id,
|
|
||||||
'city': book['customer_city'],
|
|
||||||
'phone': book['customer_phone'],
|
|
||||||
'zip': book['customer_zip'],
|
|
||||||
'street': book['customer_address'],
|
|
||||||
'email': book['customer_mail'],
|
|
||||||
'unconfirmed': True,
|
|
||||||
# 'lang': lang and lang.id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _get_book_dates(self, book):
|
|
||||||
tz_hotel = self.env['ir.default'].sudo().get('res.config.settings', 'tz_hotel')
|
|
||||||
default_arrival_hour = self.env['ir.default'].sudo().get(
|
|
||||||
'res.config.settings', 'default_arrival_hour')
|
|
||||||
default_departure_hour = self.env['ir.default'].sudo().get(
|
|
||||||
'res.config.settings', 'default_departure_hour')
|
|
||||||
|
|
||||||
# Get dates for the reservation (GMT->UTC)
|
|
||||||
arr_hour = default_arrival_hour if book['arrival_hour'] == "--" \
|
|
||||||
else book['arrival_hour']
|
|
||||||
# HOT-FIX: Wubook 24:00 hour
|
|
||||||
arr_hour_s = arr_hour.split(':')
|
|
||||||
if arr_hour_s[0] == '24':
|
|
||||||
arr_hour_s[0] = '00'
|
|
||||||
arr_hour = ':'.join(arr_hour_s)
|
|
||||||
checkin = "%s %s" % (book['date_arrival'], arr_hour)
|
|
||||||
checkin_dt = datetime.strptime(checkin, DEFAULT_WUBOOK_DATETIME_FORMAT).replace(
|
|
||||||
tzinfo=tz.gettz(str(tz_hotel)))
|
|
||||||
checkin_utc_dt = checkin_dt.astimezone(tz.gettz('UTC'))
|
|
||||||
#checkin = checkin_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
|
|
||||||
checkout = "%s %s" % (book['date_departure'],
|
|
||||||
default_departure_hour)
|
|
||||||
checkout_dt = datetime.strptime(checkout, DEFAULT_WUBOOK_DATETIME_FORMAT).replace(
|
|
||||||
tzinfo=tz.gettz(str(tz_hotel)))
|
|
||||||
checkout_utc_dt = checkout_dt.astimezone(tz.gettz('UTC'))
|
|
||||||
#checkout = checkout_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
|
|
||||||
return (checkin_utc_dt, checkout_utc_dt)
|
|
||||||
|
|
||||||
def _update_reservation_binding(self, binding, book):
|
|
||||||
is_cancellation = book['status'] in WUBOOK_STATUS_BAD
|
|
||||||
binding.with_context({'connector_no_export': True}).write({
|
|
||||||
'channel_raw_data': json.dumps(book),
|
|
||||||
'channel_status': str(book['status']),
|
|
||||||
'channel_status_reason': book.get('status_reason', ''),
|
|
||||||
'to_read': True,
|
|
||||||
'to_assign': True,
|
|
||||||
'price_unit': book['amount'],
|
|
||||||
'customer_notes': book['customer_notes'],
|
|
||||||
})
|
|
||||||
if binding.partner_id.unconfirmed:
|
|
||||||
binding.partner_id.write(
|
|
||||||
self._generate_partner_vals(book)
|
|
||||||
)
|
|
||||||
if is_cancellation:
|
|
||||||
binding.with_context({
|
|
||||||
'connector_no_export': True}).action_cancel()
|
|
||||||
elif binding.state == 'cancelled':
|
|
||||||
binding.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).write({
|
|
||||||
'discount': 0.0,
|
|
||||||
'state': 'confirm',
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# FIXME: Super big method!!! O_o
|
|
||||||
@api.model
|
|
||||||
def _generate_reservations(self, bookings):
|
|
||||||
_logger.info("==[CHANNEL->ODOO]==== READING BOOKING ==")
|
|
||||||
_logger.info(bookings)
|
|
||||||
|
|
||||||
# Get user timezone
|
|
||||||
res_partner_obj = self.env['res.partner']
|
|
||||||
channel_reserv_obj = self.env['channel.hotel.reservation']
|
|
||||||
hotel_folio_obj = self.env['hotel.folio']
|
|
||||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
|
||||||
# Space for store some data for construct folios
|
|
||||||
processed_rids = []
|
|
||||||
failed_reservations = []
|
|
||||||
checkin_utc_dt = False
|
|
||||||
checkout_utc_dt = False
|
|
||||||
split_booking = False
|
|
||||||
for book in bookings: # This create a new folio
|
|
||||||
splitted_map = {}
|
|
||||||
rcode = str(book['reservation_code'])
|
|
||||||
crcode = str(book['channel_reservation_code']) \
|
|
||||||
if book['channel_reservation_code'] else 'undefined'
|
|
||||||
|
|
||||||
# Can't process failed reservations
|
|
||||||
# (for example set a invalid new reservation and receive in
|
|
||||||
# the same transaction an cancellation)
|
|
||||||
if crcode in failed_reservations:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_emssage="Can't process a reservation that previusly failed!",
|
|
||||||
channel_object_id=book['reservation_code'])
|
|
||||||
continue
|
|
||||||
|
|
||||||
checkin_utc_dt, checkout_utc_dt = self._get_book_dates(book)
|
|
||||||
|
|
||||||
# Search Folio. If exists.
|
|
||||||
folio_id = False
|
|
||||||
if crcode != 'undefined':
|
|
||||||
reserv_bind = channel_reserv_obj.search([
|
|
||||||
('ota_reservation_id', '=', crcode),
|
|
||||||
], limit=1)
|
|
||||||
if reserv_bind:
|
|
||||||
folio_id = reserv_bind.folio_id
|
|
||||||
else:
|
|
||||||
reserv_bind = channel_reserv_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', rcode),
|
|
||||||
], limit=1)
|
|
||||||
if reserv_bind:
|
|
||||||
folio_id = reserv_bind.folio_id
|
|
||||||
|
|
||||||
# Need update reservations?
|
|
||||||
reservs_processed = False
|
|
||||||
reservs_binds = channel_reserv_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', rcode),
|
|
||||||
])
|
|
||||||
for reserv_bind in reservs_binds:
|
|
||||||
self._update_reservation_binding(reserv_bind, book)
|
|
||||||
reservs_processed = True
|
|
||||||
# Do Nothing if already processed 'external_id'
|
|
||||||
if reservs_processed:
|
|
||||||
processed_rids.append(rcode)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Search Customer
|
|
||||||
customer_mail = book.get('customer_mail', False)
|
|
||||||
partner_id = False
|
|
||||||
if customer_mail:
|
|
||||||
partner_id = res_partner_obj.search([
|
|
||||||
('email', '=', customer_mail)
|
|
||||||
], limit=1)
|
|
||||||
if not partner_id:
|
|
||||||
partner_id = res_partner_obj.create(self._generate_partner_vals(book))
|
|
||||||
|
|
||||||
reservations = []
|
|
||||||
used_rooms = []
|
|
||||||
# Iterate booked rooms
|
|
||||||
for broom in book['booked_rooms']:
|
|
||||||
room_type_bind = channel_room_type_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', broom['room_id'])
|
|
||||||
], limit=1)
|
|
||||||
if not room_type_bind:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message="Can't found any room type associated to '%s' \
|
|
||||||
in this hotel" % book['rooms'],
|
|
||||||
channel_object_id=book['reservation_code'])
|
|
||||||
failed_reservations.append(crcode)
|
|
||||||
continue
|
|
||||||
if not any(room_type_bind.room_ids):
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message="Selected room type (%s) doesn't have any \
|
|
||||||
real room" % book['rooms'],
|
|
||||||
channel_object_id=book['reservation_code'])
|
|
||||||
failed_reservations.append(crcode)
|
|
||||||
continue
|
|
||||||
|
|
||||||
dates_checkin = [checkin_utc_dt, False]
|
|
||||||
dates_checkout = [checkout_utc_dt, False]
|
|
||||||
split_booking = False
|
|
||||||
split_booking_parent = False
|
|
||||||
# This perhaps create splitted reservation
|
|
||||||
while dates_checkin[0]:
|
|
||||||
vals = self._generate_booking_vals(
|
|
||||||
broom,
|
|
||||||
crcode,
|
|
||||||
rcode,
|
|
||||||
room_type_bind,
|
|
||||||
split_booking,
|
|
||||||
dates_checkin,
|
|
||||||
dates_checkout,
|
|
||||||
book,
|
|
||||||
)
|
|
||||||
if vals['price_unit'] != book['amount']:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message="Invalid reservation total price! %.2f (calculated) != %.2f (wubook)" % (vals['price_unit'], book['amount']),
|
|
||||||
channel_object_id=book['reservation_code'])
|
|
||||||
|
|
||||||
free_rooms = room_type_bind.odoo_id.check_availability_room_type(
|
|
||||||
vals['checkin'],
|
|
||||||
vals['checkout'],
|
|
||||||
room_type_id=room_type_bind.odoo_id.id,
|
|
||||||
notthis=used_rooms)
|
|
||||||
if any(free_rooms):
|
|
||||||
vals.update({
|
|
||||||
'product_id': room_type_bind.product_id.id,
|
|
||||||
'name': free_rooms[0].name,
|
|
||||||
})
|
|
||||||
reservations.append((0, False, vals))
|
|
||||||
used_rooms.append(free_rooms[0].id)
|
|
||||||
|
|
||||||
if split_booking:
|
|
||||||
if not split_booking_parent:
|
|
||||||
split_booking_parent = len(reservations)
|
|
||||||
else:
|
|
||||||
splitted_map.setdefault(
|
|
||||||
split_booking_parent,
|
|
||||||
[]).append(len(reservations))
|
|
||||||
dates_checkin = [dates_checkin[1], False]
|
|
||||||
dates_checkout = [dates_checkout[1], False]
|
|
||||||
else:
|
|
||||||
date_diff = (dates_checkout[0].replace(
|
|
||||||
hour=0, minute=0, second=0,
|
|
||||||
microsecond=0) -
|
|
||||||
dates_checkin[0].replace(
|
|
||||||
hour=0, minute=0, second=0,
|
|
||||||
microsecond=0)).days
|
|
||||||
if date_diff <= 0:
|
|
||||||
if split_booking:
|
|
||||||
if split_booking_parent:
|
|
||||||
del reservations[split_booking_parent-1:]
|
|
||||||
if split_booking_parent in splitted_map:
|
|
||||||
del splitted_map[split_booking_parent]
|
|
||||||
# Can't found space for reservation: Overbooking
|
|
||||||
vals = self._generate_booking_vals(
|
|
||||||
broom,
|
|
||||||
crcode,
|
|
||||||
rcode,
|
|
||||||
room_type_bind,
|
|
||||||
False,
|
|
||||||
(checkin_utc_dt, False),
|
|
||||||
(checkout_utc_dt, False),
|
|
||||||
book,
|
|
||||||
)
|
|
||||||
vals.update({
|
|
||||||
'product_id': room_type_bind.product_id.id,
|
|
||||||
'name': room_type_bind.name,
|
|
||||||
'overbooking': True,
|
|
||||||
})
|
|
||||||
reservations.append((0, False, vals))
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message="Reservation imported with overbooking state",
|
|
||||||
channel_object_id=rcode,
|
|
||||||
dfrom=vals['checkin'], dto=vals['checkout'])
|
|
||||||
dates_checkin = [False, False]
|
|
||||||
dates_checkout = [False, False]
|
|
||||||
split_booking = False
|
|
||||||
else:
|
|
||||||
split_booking = True
|
|
||||||
dates_checkin = [
|
|
||||||
dates_checkin[0],
|
|
||||||
dates_checkin[0] + timedelta(days=date_diff-1)
|
|
||||||
]
|
|
||||||
dates_checkout = [
|
|
||||||
dates_checkout[0] - timedelta(days=1),
|
|
||||||
checkout_utc_dt
|
|
||||||
]
|
|
||||||
|
|
||||||
# Create Splitted Issue Information
|
|
||||||
if split_booking:
|
|
||||||
self.create_issue(
|
|
||||||
section='reservation',
|
|
||||||
internal_message="Reservation Splitted",
|
|
||||||
channel_object_id=rcode)
|
|
||||||
|
|
||||||
# Create Folio
|
|
||||||
if not any(failed_reservations) and any(reservations):
|
|
||||||
# TODO: Improve 'addons_list' & discounts
|
|
||||||
addons = str(book['addons_list']) if any(book['addons_list']) else ''
|
|
||||||
discounts = book.get('discount', '')
|
|
||||||
vals = {
|
|
||||||
'room_lines': reservations,
|
|
||||||
'customer_notes': "%s\nADDONS:\n%s\nDISCOUNT:\n%s" % (
|
|
||||||
book['customer_notes'], addons, discounts),
|
|
||||||
'channel_type': 'web',
|
|
||||||
}
|
|
||||||
_logger.info("==[CHANNEL->ODOO]==== CREATING/UPDATING FOLIO ==")
|
|
||||||
_logger.info(reservations)
|
|
||||||
if folio_id:
|
|
||||||
folio_id.with_context({
|
|
||||||
'connector_no_export': True}).write(vals)
|
|
||||||
else:
|
|
||||||
vals.update({
|
|
||||||
'partner_id': partner_id.id,
|
|
||||||
'wseed': book['sessionSeed']
|
|
||||||
})
|
|
||||||
folio_id = hotel_folio_obj.with_context({
|
|
||||||
'connector_no_export': True}).create(vals)
|
|
||||||
|
|
||||||
# Update Reservation Spitted Parents
|
|
||||||
sorted_rlines = folio_id.room_lines.sorted(key='id')
|
|
||||||
for k_pid, v_pid in splitted_map.items():
|
|
||||||
preserv = sorted_rlines[k_pid-1]
|
|
||||||
for pid in v_pid:
|
|
||||||
creserv = sorted_rlines[pid-1]
|
|
||||||
creserv.parent_reservation = preserv.id
|
|
||||||
|
|
||||||
# Bind reservations
|
|
||||||
rlines = sorted_rlines = folio_id.room_lines
|
|
||||||
for rline in rlines:
|
|
||||||
for rline_bind in rline.channel_bind_ids:
|
|
||||||
self.binder(rline_bind.external_id, rline_bind)
|
|
||||||
|
|
||||||
processed_rids.append(rcode)
|
|
||||||
return (processed_rids, any(failed_reservations),
|
|
||||||
checkin_utc_dt, checkout_utc_dt)
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo import api, models, fields, _
|
from odoo import api, models, fields, _
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ChannelHotelRoomType(models.Model):
|
class ChannelHotelRoomType(models.Model):
|
||||||
_name = 'channel.hotel.room.type'
|
_name = 'channel.hotel.room.type'
|
||||||
@@ -107,39 +106,6 @@ class HotelRoomType(models.Model):
|
|||||||
], limit=1)
|
], limit=1)
|
||||||
return restriction
|
return restriction
|
||||||
|
|
||||||
@api.multi
|
|
||||||
def create_bindings(self):
|
|
||||||
backends = self.env['channel.backend'].search([])
|
|
||||||
binding_obj = self.env['channel.hotel.room.type']
|
|
||||||
for backend in backends:
|
|
||||||
binding = binding_obj.search([
|
|
||||||
('odoo_id', '=', self.id),
|
|
||||||
('backend_id', '=', backend.id)], limit=1)
|
|
||||||
if not binding:
|
|
||||||
binding_obj.sudo().create({
|
|
||||||
'odoo_id': self.id,
|
|
||||||
'backend_id': backend.id,
|
|
||||||
})
|
|
||||||
|
|
||||||
class HotelRoomTypeAdapter(Component):
|
|
||||||
_name = 'channel.hotel.room.type.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.hotel.room.type'
|
|
||||||
|
|
||||||
def create_room(self, shortcode, name, capacity, price, availability):
|
|
||||||
return super(HotelRoomTypeAdapter, self).create_room(
|
|
||||||
shortcode, name, capacity, price, availability)
|
|
||||||
|
|
||||||
def fetch_rooms(self):
|
|
||||||
return super(HotelRoomTypeAdapter, self).fetch_rooms()
|
|
||||||
|
|
||||||
def modify_room(self, channel_room_id, name, capacity, price, availability, scode):
|
|
||||||
return super(HotelRoomTypeAdapter, self).modify_room(
|
|
||||||
channel_room_id, name, capacity, price, availability, scode)
|
|
||||||
|
|
||||||
def delete_room(self, channel_room_id):
|
|
||||||
return super(HotelRoomTypeAdapter, self).delete_room(channel_room_id)
|
|
||||||
|
|
||||||
class BindingHotelRoomTypeListener(Component):
|
class BindingHotelRoomTypeListener(Component):
|
||||||
_name = 'binding.hotel.room.type.listener'
|
_name = 'binding.hotel.room.type.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# 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 odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import api
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeDeleter(Component):
|
class HotelRoomTypeDeleter(Component):
|
||||||
_name = 'channel.hotel.room.type.deleter'
|
_name = 'channel.hotel.room.type.deleter'
|
||||||
_inherit = 'hotel.channel.deleter'
|
_inherit = 'hotel.channel.deleter'
|
||||||
@@ -13,10 +13,4 @@ class HotelRoomTypeDeleter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def delete_room(self, binding):
|
def delete_room(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.delete_room(binding.external_id)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='room',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo import api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class HotelRoomTypeExporter(Component):
|
class HotelRoomTypeExporter(Component):
|
||||||
_name = 'channel.hotel.room.type.exporter'
|
_name = 'channel.hotel.room.type.exporter'
|
||||||
@@ -15,40 +13,8 @@ class HotelRoomTypeExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def modify_room(self, binding):
|
def modify_room(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.modify_room(
|
|
||||||
binding.external_id,
|
|
||||||
binding.name,
|
|
||||||
binding.ota_capacity,
|
|
||||||
binding.list_price,
|
|
||||||
binding.total_rooms_count,
|
|
||||||
binding.channel_short_code)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='room',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create_room(self, binding):
|
def create_room(self, binding):
|
||||||
seq_obj = self.env['ir.sequence']
|
raise NotImplementedError
|
||||||
short_code = seq_obj.next_by_code('hotel.room.type')[:4]
|
|
||||||
try:
|
|
||||||
external_id = self.backend_adapter.create_room(
|
|
||||||
short_code,
|
|
||||||
binding.name,
|
|
||||||
binding.ota_capacity,
|
|
||||||
binding.list_price,
|
|
||||||
binding.total_rooms_count
|
|
||||||
)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='room',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
binding.write({
|
|
||||||
'external_id': external_id,
|
|
||||||
'channel_short_code': short_code,
|
|
||||||
})
|
|
||||||
self.binder.bind(external_id, binding)
|
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.connector.components.mapper import mapping
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import fields, api, _
|
|
||||||
from odoo.tools import (
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import DEFAULT_WUBOOK_DATE_FORMAT
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeImporter(Component):
|
class HotelRoomTypeImporter(Component):
|
||||||
@@ -23,47 +13,4 @@ class HotelRoomTypeImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_rooms(self):
|
def get_rooms(self):
|
||||||
count = 0
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.fetch_rooms()
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='room',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
|
||||||
room_mapper = self.component(usage='import.mapper',
|
|
||||||
model_name='channel.hotel.room.type')
|
|
||||||
for room in results:
|
|
||||||
map_record = room_mapper.map_record(room)
|
|
||||||
room_bind = channel_room_type_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', room['id']),
|
|
||||||
], limit=1)
|
|
||||||
if room_bind:
|
|
||||||
room_bind.with_context({'connector_no_export':True}).write(map_record.values())
|
|
||||||
else:
|
|
||||||
room_bind = channel_room_type_obj.with_context({
|
|
||||||
'connector_no_export':True}).create(
|
|
||||||
map_record.values(for_create=True))
|
|
||||||
self.binder.bind(room['id'], room_bind)
|
|
||||||
count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
class HotelRoomTypeImportMapper(Component):
|
|
||||||
_name = 'channel.hotel.room.type.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.hotel.room.type'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('id', 'external_id'),
|
|
||||||
('shortname', 'channel_short_code'),
|
|
||||||
('occupancy', 'ota_capacity'),
|
|
||||||
('price', 'list_price'),
|
|
||||||
('name', 'name'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ from datetime import timedelta
|
|||||||
from odoo import api, models, fields, _
|
from odoo import api, models, fields, _
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
|
|
||||||
class ChannelHotelRoomTypeAvailability(models.Model):
|
class ChannelHotelRoomTypeAvailability(models.Model):
|
||||||
_name = 'channel.hotel.room.type.availability'
|
_name = 'channel.hotel.room.type.availability'
|
||||||
@@ -42,7 +41,7 @@ class ChannelHotelRoomTypeAvailability(models.Model):
|
|||||||
count: %d") % record.odoo_id.room_type_id.total_rooms_count)
|
count: %d") % record.odoo_id.room_type_id.total_rooms_count)
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def refresh_availability(self, checkin, checkout, product_id):
|
def refresh_availability(self, checkin, checkout, room_id):
|
||||||
date_start = fields.Date.from_string(checkin)
|
date_start = fields.Date.from_string(checkin)
|
||||||
date_end = fields.Date.from_string(checkout)
|
date_end = fields.Date.from_string(checkout)
|
||||||
# Not count end day of the reservation
|
# Not count end day of the reservation
|
||||||
@@ -51,7 +50,10 @@ class ChannelHotelRoomTypeAvailability(models.Model):
|
|||||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||||
channel_room_type_avail_obj = self.env['hotel.room.type.availability']
|
channel_room_type_avail_obj = self.env['hotel.room.type.availability']
|
||||||
|
|
||||||
room_type_binds = channel_room_type_obj.search([('product_id', '=', product_id)])
|
room_type_binds = channel_room_type_obj.search([
|
||||||
|
('backend_id', '=', self.backend_id.id),
|
||||||
|
('room_ids', '=', room_id),
|
||||||
|
])
|
||||||
for room_type_bind in room_type_binds:
|
for room_type_bind in room_type_binds:
|
||||||
if room_type_bind.external_id:
|
if room_type_bind.external_id:
|
||||||
for i in range(0, date_diff):
|
for i in range(0, date_diff):
|
||||||
@@ -105,7 +107,7 @@ class HotelRoomTypeAvailability(models.Model):
|
|||||||
booked = fields.Boolean('Booked', default=False, readonly=True)
|
booked = fields.Boolean('Booked', default=False, readonly=True)
|
||||||
|
|
||||||
def _prepare_notif_values(self, record):
|
def _prepare_notif_values(self, record):
|
||||||
vals = super(HotelRoomTypeAvailability, self)._prepare_notif_values()
|
vals = super(HotelRoomTypeAvailability, self)._prepare_notif_values(record)
|
||||||
vals.update({
|
vals.update({
|
||||||
'no_ota': record.no_ota,
|
'no_ota': record.no_ota,
|
||||||
})
|
})
|
||||||
@@ -141,21 +143,6 @@ class HotelRoomTypeAvailability(models.Model):
|
|||||||
if self.room_type_id:
|
if self.room_type_id:
|
||||||
self.channel_max_avail = self.room_type_id.total_rooms_count
|
self.channel_max_avail = self.room_type_id.total_rooms_count
|
||||||
|
|
||||||
class HotelRoomTypeAvailabilityAdapter(Component):
|
|
||||||
_name = 'channel.hotel.room.type.availability.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.hotel.room.type.availability'
|
|
||||||
|
|
||||||
def fetch_rooms_values(self, date_from, date_to, rooms=False):
|
|
||||||
return super(HotelRoomTypeAvailabilityAdapter, self).fetch_rooms_values(
|
|
||||||
date_from,
|
|
||||||
date_to,
|
|
||||||
rooms)
|
|
||||||
|
|
||||||
def update_availability(self, rooms_avail):
|
|
||||||
return super(HotelRoomTypeAvailabilityAdapter, self).update_availability(
|
|
||||||
rooms_avail)
|
|
||||||
|
|
||||||
class BindingHotelRoomTypeAvailabilityListener(Component):
|
class BindingHotelRoomTypeAvailabilityListener(Component):
|
||||||
_name = 'binding.hotel.room.type.listener'
|
_name = 'binding.hotel.room.type.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo import api, fields, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class HotelRoomTypeAvailabilityExporter(Component):
|
class HotelRoomTypeAvailabilityExporter(Component):
|
||||||
_name = 'channel.hotel.room.type.availability.exporter'
|
_name = 'channel.hotel.room.type.availability.exporter'
|
||||||
@@ -16,41 +12,4 @@ class HotelRoomTypeAvailabilityExporter(Component):
|
|||||||
_usage = 'hotel.room.type.availability.exporter'
|
_usage = 'hotel.room.type.availability.exporter'
|
||||||
|
|
||||||
def push_availability(self):
|
def push_availability(self):
|
||||||
channel_room_type_avail_ids = self.env['channel.hotel.room.type.availability'].search([
|
raise NotImplementedError
|
||||||
('channel_pushed', '=', False),
|
|
||||||
('date', '>=', fields.Date.today())
|
|
||||||
])
|
|
||||||
room_types = channel_room_type_avail_ids.mapped('room_type_id')
|
|
||||||
avails = []
|
|
||||||
for room_type in room_types:
|
|
||||||
if any(room_type.channel_bind_ids):
|
|
||||||
channel_room_type_avails = channel_room_type_avail_ids.filtered(
|
|
||||||
lambda x: x.room_type_id.id == room_type.id)
|
|
||||||
days = []
|
|
||||||
for channel_room_type_avail in channel_room_type_avails:
|
|
||||||
cavail = channel_room_type_avail.avail
|
|
||||||
if channel_room_type_avail.channel_max_avail >= 0 and \
|
|
||||||
cavail > channel_room_type_avail.channel_max_avail:
|
|
||||||
cavail = channel_room_type_avail.channel_max_avail
|
|
||||||
date_dt = fields.Date.from_string(channel_room_type_avail.date)
|
|
||||||
days.append({
|
|
||||||
'date': date_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
|
||||||
'avail': cavail,
|
|
||||||
'no_ota': channel_room_type_avail.no_ota and 1 or 0,
|
|
||||||
# 'booked': room_type_avail.booked and 1 or 0,
|
|
||||||
})
|
|
||||||
avails.append({'id': room_type.channel_bind_ids[0].channel_room_id, 'days': days})
|
|
||||||
_logger.info("==[ODOO->CHANNEL]==== AVAILABILITY ==")
|
|
||||||
_logger.info(avails)
|
|
||||||
if any(avails):
|
|
||||||
try:
|
|
||||||
self.backend_adapter.update_availability(avails)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='avail',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
channel_room_type_avails.write({'channel_pushed': True})
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import date, timedelta
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo.addons.connector.components.mapper import mapping, only_create
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeAvailabilityImporter(Component):
|
class HotelRoomTypeAvailabilityImporter(Component):
|
||||||
@@ -19,86 +13,4 @@ class HotelRoomTypeAvailabilityImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_availability_values(self, date_from, date_to):
|
def import_availability_values(self, date_from, date_to):
|
||||||
now_dt = date.today()
|
raise NotImplementedError
|
||||||
dfrom_dt = fields.Date.from_string(date_from)
|
|
||||||
dto_dt = fields.Date.from_string(date_to)
|
|
||||||
if dfrom_dt < now_dt:
|
|
||||||
dfrom_dt = now_dt
|
|
||||||
if dfrom_dt > dto_dt:
|
|
||||||
dfrom_dt, dto_dt = dto_dt, dfrom_dt
|
|
||||||
if dto_dt < now_dt:
|
|
||||||
return True
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
try:
|
|
||||||
results = self.backend_adapter.fetch_rooms_values(date_from, date_to)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='avail',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'],
|
|
||||||
dfrom=date_from, dto=date_to)
|
|
||||||
else:
|
|
||||||
_logger.info("==[CHANNEL->ODOO]==== AVAILABILITY (%s - %s) ==",
|
|
||||||
date_from, date_to)
|
|
||||||
_logger.info(results)
|
|
||||||
|
|
||||||
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
|
||||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
|
||||||
room_avail_mapper = self.component(
|
|
||||||
usage='import.mapper',
|
|
||||||
model_name='channel.hotel.room.type.availability')
|
|
||||||
for room_k, room_v in results.items():
|
|
||||||
iter_day = dfrom_dt
|
|
||||||
channel_room_type = channel_room_type_obj.search([
|
|
||||||
('channel_room_id', '=', room_k)
|
|
||||||
], limit=1)
|
|
||||||
if channel_room_type:
|
|
||||||
for room in room_v:
|
|
||||||
room.update({
|
|
||||||
'room_type_id': channel_room_type.odoo_id.id,
|
|
||||||
'date': fields.Date.to_string(iter_day),
|
|
||||||
})
|
|
||||||
map_record = room_avail_mapper.map_record(room)
|
|
||||||
room_type_avail_bind = channel_room_type_avail_obj.search([
|
|
||||||
('room_type_id', '=', room['room_type_id']),
|
|
||||||
('date', '=', room['date'])
|
|
||||||
], limit=1)
|
|
||||||
if room_type_avail_bind:
|
|
||||||
room_type_avail_bind.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).write(map_record.values())
|
|
||||||
else:
|
|
||||||
room_type_avail_bind = channel_room_type_avail_obj.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).create(map_record.values(for_create=True))
|
|
||||||
room_type_avail_bind.channel_pushed = True
|
|
||||||
iter_day += timedelta(days=1)
|
|
||||||
count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeAvailabilityImportMapper(Component):
|
|
||||||
_name = 'channel.hotel.room.type.availability.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.hotel.room.type.availability'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('no_ota', 'no_ota'),
|
|
||||||
('booked', 'booked'),
|
|
||||||
('avail', 'avail'),
|
|
||||||
('date', 'date'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def channel_pushed(self, record):
|
|
||||||
return {'channel_pushed': True}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def room_type_id(self, record):
|
|
||||||
return {'room_type_id': record['room_type_id']}
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo import api, models, fields
|
from odoo import api, models, fields
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ChannelHotelRoomTypeRestriction(models.Model):
|
class ChannelHotelRoomTypeRestriction(models.Model):
|
||||||
_name = 'channel.hotel.room.type.restriction'
|
_name = 'channel.hotel.room.type.restriction'
|
||||||
@@ -81,23 +79,6 @@ class HotelRoomTypeRestriction(models.Model):
|
|||||||
names.append((name[0], name[1]))
|
names.append((name[0], name[1]))
|
||||||
return names
|
return names
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionAdapter(Component):
|
|
||||||
_name = 'channel.hotel.room.type.restriction.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.hotel.room.type.restriction'
|
|
||||||
|
|
||||||
def rplan_rplans(self):
|
|
||||||
return super(HotelRoomTypeRestrictionAdapter, self).rplan_rplans()
|
|
||||||
|
|
||||||
def create_rplan(self, name):
|
|
||||||
return super(HotelRoomTypeRestrictionAdapter, self).create_rplan(name)
|
|
||||||
|
|
||||||
def delete_rplan(self, external_id):
|
|
||||||
return super(HotelRoomTypeRestrictionAdapter, self).delete_rplan(external_id)
|
|
||||||
|
|
||||||
def rename_rplan(self, external_id, new_name):
|
|
||||||
return super(HotelRoomTypeRestrictionAdapter, self).rename_rplan(external_id, new_name)
|
|
||||||
|
|
||||||
class BindingHotelRoomTypeListener(Component):
|
class BindingHotelRoomTypeListener(Component):
|
||||||
_name = 'binding.hotel.room.type.restriction.listener'
|
_name = 'binding.hotel.room.type.restriction.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# 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 odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import api
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionDeleter(Component):
|
class HotelRoomTypeRestrictionDeleter(Component):
|
||||||
_name = 'channel.hotel.room.type.restriction.deleter'
|
_name = 'channel.hotel.room.type.restriction.deleter'
|
||||||
_inherit = 'hotel.channel.deleter'
|
_inherit = 'hotel.channel.deleter'
|
||||||
@@ -13,10 +13,4 @@ class HotelRoomTypeRestrictionDeleter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def delete_rplan(self, binding):
|
def delete_rplan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.delete_rplan(binding.external_id)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo import api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionExporter(Component):
|
class HotelRoomTypeRestrictionExporter(Component):
|
||||||
_name = 'channel.hotel.room.type.restriction.exporter'
|
_name = 'channel.hotel.room.type.restriction.exporter'
|
||||||
@@ -15,25 +13,8 @@ class HotelRoomTypeRestrictionExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def rename_rplan(self, binding):
|
def rename_rplan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.rename_rplan(
|
|
||||||
binding.external_id,
|
|
||||||
binding.name)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create_rplan(self, binding):
|
def create_rplan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
external_id = self.backend_adapter.create_rplan(binding.name)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
binding.external_id = external_id
|
|
||||||
self.binder.bind(external_id, binding)
|
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
|
||||||
from odoo.exceptions import ValidationError
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.connector.components.mapper import mapping
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionImporter(Component):
|
class HotelRoomTypeRestrictionImporter(Component):
|
||||||
@@ -19,46 +13,4 @@ class HotelRoomTypeRestrictionImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_restriction_plans(self):
|
def import_restriction_plans(self):
|
||||||
count = 0
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.rplan_rplans()
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
channel_restriction_obj = self.env['channel.hotel.room.type.restriction']
|
|
||||||
restriction_mapper = self.component(usage='import.mapper',
|
|
||||||
model_name='channel.hotel.room.type.restriction')
|
|
||||||
for plan in results:
|
|
||||||
plan_record = restriction_mapper.map_record(plan)
|
|
||||||
plan_bind = channel_restriction_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', str(plan['id'])),
|
|
||||||
], limit=1)
|
|
||||||
if not plan_bind:
|
|
||||||
channel_restriction_obj.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
'rules': plan.get('rules'),
|
|
||||||
}).create(plan_record.values(for_create=True))
|
|
||||||
else:
|
|
||||||
plan_bind.with_context({'connector_no_export':True}).write(
|
|
||||||
plan_record.values())
|
|
||||||
count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionImportMapper(Component):
|
|
||||||
_name = 'channel.hotel.room.type.restriction.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.hotel.room.type.restriction'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('name', 'name'),
|
|
||||||
('id', 'external_id'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
# 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 odoo import api, models, fields
|
from odoo import api, models, fields
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
|
|
||||||
|
|
||||||
class ChannelHotelRoomTypeRestrictionItem(models.Model):
|
class ChannelHotelRoomTypeRestrictionItem(models.Model):
|
||||||
_name = 'channel.hotel.room.type.restriction.item'
|
_name = 'channel.hotel.room.type.restriction.item'
|
||||||
_inherit = 'channel.binding'
|
_inherit = 'channel.binding'
|
||||||
@@ -45,17 +45,6 @@ class HotelRoomTypeRestrictionItem(models.Model):
|
|||||||
inverse_name='odoo_id',
|
inverse_name='odoo_id',
|
||||||
string='Hotel Channel Connector Bindings')
|
string='Hotel Channel Connector Bindings')
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionItemAdapter(Component):
|
|
||||||
_name = 'channel.hotel.room.type.restriction.item.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.hotel.room.type.restriction.item'
|
|
||||||
|
|
||||||
def wired_rplan_get_rplan_values(self, date_from, date_to, channel_restriction_plan_id):
|
|
||||||
return super(HotelRoomTypeRestrictionItemAdapter, self).wired_rplan_get_rplan_values(
|
|
||||||
date_from,
|
|
||||||
date_to,
|
|
||||||
channel_restriction_plan_id)
|
|
||||||
|
|
||||||
class BindingHotelRoomTypeRestrictionItemListener(Component):
|
class BindingHotelRoomTypeRestrictionItemListener(Component):
|
||||||
_name = 'binding.hotel.room.type.restriction.item.listener'
|
_name = 'binding.hotel.room.type.restriction.item.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionItemExporter(Component):
|
class HotelRoomTypeRestrictionItemExporter(Component):
|
||||||
_name = 'channel.hotel.room.type.restriction.item.exporter'
|
_name = 'channel.hotel.room.type.restriction.item.exporter'
|
||||||
@@ -19,63 +13,4 @@ class HotelRoomTypeRestrictionItemExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def push_restriction(self):
|
def push_restriction(self):
|
||||||
channel_room_type_rest_obj = self.env['channel.hotel.room.type.restriction']
|
raise NotImplementedError
|
||||||
channel_rest_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
|
||||||
unpushed = channel_rest_item_obj.search([
|
|
||||||
('channel_pushed', '=', False),
|
|
||||||
('date', '>=', fields.Date.today())
|
|
||||||
], order="date ASC")
|
|
||||||
if any(unpushed):
|
|
||||||
date_start = fields.Date.from_string(unpushed[0].date)
|
|
||||||
date_end = fields.Date.from_string(unpushed[-1].date)
|
|
||||||
days_diff = (date_end-date_start).days + 1
|
|
||||||
restrictions = {}
|
|
||||||
channel_restr_plan_ids = channel_room_type_rest_obj.search([])
|
|
||||||
for rp in channel_restr_plan_ids:
|
|
||||||
restrictions.update({rp.external_id: {}})
|
|
||||||
unpushed_rp = channel_rest_item_obj.search([
|
|
||||||
('channel_pushed', '=', False),
|
|
||||||
('restriction_id', '=', rp.odoo_id.id)
|
|
||||||
])
|
|
||||||
room_type_ids = unpushed_rp.mapped('room_type_id')
|
|
||||||
for room_type in room_type_ids:
|
|
||||||
if any(room_type.channel_bind_ids):
|
|
||||||
# FIXME: Supossed that only exists one channel connector per record
|
|
||||||
room_type_external_id = room_type.channel_bind_ids[0].external_id
|
|
||||||
restrictions[rp.external_id].update({
|
|
||||||
room_type_external_id: [],
|
|
||||||
})
|
|
||||||
for i in range(0, days_diff):
|
|
||||||
ndate_dt = date_start + timedelta(days=i)
|
|
||||||
restr = room_type.get_restrictions(
|
|
||||||
ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
rp.odoo_id.id)
|
|
||||||
if restr:
|
|
||||||
restrictions[rp.external_id][room_type_external_id].append({
|
|
||||||
'min_stay': restr.min_stay or 0,
|
|
||||||
'min_stay_arrival': restr.min_stay_arrival or 0,
|
|
||||||
'max_stay': restr.max_stay or 0,
|
|
||||||
'max_stay_arrival': restr.max_stay_arrival or 0,
|
|
||||||
'closed': restr.closed and 1 or 0,
|
|
||||||
'closed_arrival': restr.closed_arrival and 1 or 0,
|
|
||||||
'closed_departure': restr.closed_departure and 1 or 0,
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
restrictions[rp.external_id][room_type_external_id].append({})
|
|
||||||
_logger.info("==[ODOO->CHANNEL]==== RESTRICTIONS ==")
|
|
||||||
_logger.info(restrictions)
|
|
||||||
try:
|
|
||||||
for k_res, v_res in restrictions.items():
|
|
||||||
if any(v_res):
|
|
||||||
self.backend_adapter.update_rplan_values(
|
|
||||||
int(k_res),
|
|
||||||
date_start.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
v_res)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
unpushed.write({'channel_pushed': True})
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo.addons.connector.components.mapper import mapping, only_create
|
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionImporter(Component):
|
class HotelRoomTypeRestrictionImporter(Component):
|
||||||
@@ -22,104 +14,8 @@ class HotelRoomTypeRestrictionImporter(Component):
|
|||||||
# FIXME: Reduce Nested Loops!!
|
# FIXME: Reduce Nested Loops!!
|
||||||
@api.model
|
@api.model
|
||||||
def _generate_restriction_items(self, plan_restrictions):
|
def _generate_restriction_items(self, plan_restrictions):
|
||||||
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
|
raise NotImplementedError
|
||||||
channel_reserv_restriction_obj = self.env['channel.hotel.room.type.restriction']
|
|
||||||
channel_restriction_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
|
||||||
restriction_item_mapper = self.component(
|
|
||||||
usage='import.mapper',
|
|
||||||
model_name='channel.hotel.room.type.restriction.item')
|
|
||||||
_logger.info("==[CHANNEL->ODOO]==== RESTRICTIONS ==")
|
|
||||||
_logger.info(plan_restrictions)
|
|
||||||
for k_rpid, v_rpid in plan_restrictions.items():
|
|
||||||
channel_restriction_id = channel_reserv_restriction_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', k_rpid),
|
|
||||||
], limit=1)
|
|
||||||
if channel_restriction_id:
|
|
||||||
for k_rid, v_rid in v_rpid.items():
|
|
||||||
channel_room_type = channel_hotel_room_type_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', k_rid),
|
|
||||||
], limit=1)
|
|
||||||
if channel_room_type:
|
|
||||||
for item in v_rid:
|
|
||||||
map_record = restriction_item_mapper.map_record(item)
|
|
||||||
date_dt = datetime.strptime(item['date'], DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
date_str = date_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
|
||||||
channel_restriction_item = channel_restriction_item_obj.search([
|
|
||||||
('restriction_id', '=', channel_restriction_id.odoo_id.id),
|
|
||||||
('date', '=', date_str),
|
|
||||||
('applied_on', '=', '0_room_type'),
|
|
||||||
('room_type_id', '=', channel_room_type.odoo_id.id)
|
|
||||||
], limit=1)
|
|
||||||
item.update({
|
|
||||||
'date': date_str,
|
|
||||||
'room_type_id': channel_room_type.odoo_id.id,
|
|
||||||
'restriction_id': channel_restriction_id.odoo_id.id,
|
|
||||||
})
|
|
||||||
if channel_restriction_item:
|
|
||||||
channel_restriction_item.with_context({
|
|
||||||
'connector_no_export': True
|
|
||||||
}).write(map_record.values())
|
|
||||||
else:
|
|
||||||
channel_restriction_item = channel_restriction_item_obj.with_context({
|
|
||||||
'connector_no_export': True
|
|
||||||
}).create(map_record.values(for_create=True))
|
|
||||||
channel_restriction_item.channel_pushed = True
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_restriction_values(self, date_from, date_to, channel_restr_id=False):
|
def import_restriction_values(self, date_from, date_to, channel_restr_id=False):
|
||||||
channel_restr_plan_id = channel_restr_id.external_id if channel_restr_id else False
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.wired_rplan_get_rplan_values(
|
|
||||||
date_from,
|
|
||||||
date_to,
|
|
||||||
int(channel_restr_plan_id))
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='restriction',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'],
|
|
||||||
channel_object_id=channel_restr_id,
|
|
||||||
dfrom=date_from, dto=date_to)
|
|
||||||
else:
|
|
||||||
if any(results):
|
|
||||||
self._generate_restriction_items(results)
|
|
||||||
|
|
||||||
class HotelRoomTypeRestrictionItemImportMapper(Component):
|
|
||||||
_name = 'channel.hotel.room.type.restriction.item.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.hotel.room.type.restriction.item'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('min_stay', 'min_stay'),
|
|
||||||
('min_stay_arrival', 'min_stay_arrival'),
|
|
||||||
('max_stay', 'max_stay'),
|
|
||||||
('max_stay_arrival', 'max_stay_arrival'),
|
|
||||||
('closed', 'closed'),
|
|
||||||
('closed_departure', 'closed_departure'),
|
|
||||||
('closed_arrival', 'closed_arrival'),
|
|
||||||
('date', 'date'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def applied_on(self, record):
|
|
||||||
return {'applied_on': '0_room_type'}
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def channel_pushed(self, record):
|
|
||||||
return {'channel_pushed': True}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def room_type_id(self, record):
|
|
||||||
return {'room_type_id': record['room_type_id']}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def restriction_id(self, record):
|
|
||||||
return {'restriction_id': record['restriction_id']}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class HotelFolio(models.Model):
|
|||||||
channel_reservations = record.room_lines.filtered(lambda x: x.room_id)
|
channel_reservations = record.room_lines.filtered(lambda x: x.room_id)
|
||||||
record.has_channel_reservations = any(channel_reservations)
|
record.has_channel_reservations = any(channel_reservations)
|
||||||
|
|
||||||
wseed = fields.Char("Wubook Session Seed", readonly=True)
|
|
||||||
customer_notes = fields.Text("Channel Customer Notes",
|
customer_notes = fields.Text("Channel Customer Notes",
|
||||||
readonly=True, old_name='wcustomer_notes')
|
readonly=True, old_name='wcustomer_notes')
|
||||||
has_channel_reservations = fields.Boolean(compute=_has_channel_reservations,
|
has_channel_reservations = fields.Boolean(compute=_has_channel_reservations,
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
# 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 odoo import api, models, fields
|
from odoo import api, models, fields
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
|
|
||||||
@@ -79,23 +78,6 @@ class ProductPricelist(models.Model):
|
|||||||
names.append((name[0], name[1]))
|
names.append((name[0], name[1]))
|
||||||
return names
|
return names
|
||||||
|
|
||||||
class ProductPricelistAdapter(Component):
|
|
||||||
_name = 'channel.product.pricelist.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.product.pricelist'
|
|
||||||
|
|
||||||
def get_pricing_plans(self):
|
|
||||||
return super(ProductPricelistAdapter, self).get_pricing_plans()
|
|
||||||
|
|
||||||
def create_plan(self, name):
|
|
||||||
return super(ProductPricelistAdapter, self).create_plan(name)
|
|
||||||
|
|
||||||
def delete_plan(self, external_id):
|
|
||||||
return super(ProductPricelistAdapter, self).delete_plan(external_id)
|
|
||||||
|
|
||||||
def rename_plan(self, external_id, new_name):
|
|
||||||
return super(ProductPricelistAdapter, self).rename_plan(external_id, new_name)
|
|
||||||
|
|
||||||
class BindingProductPricelistListener(Component):
|
class BindingProductPricelistListener(Component):
|
||||||
_name = 'binding.product.pricelist.listener'
|
_name = 'binding.product.pricelist.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
# 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 odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo import api
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
class ProductPricelistDeleter(Component):
|
class ProductPricelistDeleter(Component):
|
||||||
_name = 'channel.product.pricelist.deleter'
|
_name = 'channel.product.pricelist.deleter'
|
||||||
_inherit = 'hotel.channel.deleter'
|
_inherit = 'hotel.channel.deleter'
|
||||||
@@ -13,10 +13,4 @@ class ProductPricelistDeleter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def delete_plan(self, binding):
|
def delete_plan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.delete_plan(binding.external_id)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo import api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ProductPricelistExporter(Component):
|
class ProductPricelistExporter(Component):
|
||||||
_name = 'channel.product.pricelist.exporter'
|
_name = 'channel.product.pricelist.exporter'
|
||||||
@@ -15,25 +13,8 @@ class ProductPricelistExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def rename_plan(self, binding):
|
def rename_plan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
return self.backend_adapter.rename_plan(
|
|
||||||
binding.external_id,
|
|
||||||
binding.name)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def create_plan(self, binding):
|
def create_plan(self, binding):
|
||||||
try:
|
raise NotImplementedError
|
||||||
external_id = self.backend_adapter.create_plan(binding.name)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
binding.external_id = external_id
|
|
||||||
self.binder.bind(external_id, binding)
|
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.connector.components.mapper import mapping, only_create
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ProductPricelistImporter(Component):
|
class ProductPricelistImporter(Component):
|
||||||
@@ -21,49 +13,4 @@ class ProductPricelistImporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_pricing_plans(self):
|
def import_pricing_plans(self):
|
||||||
count = 0
|
raise NotImplementedError
|
||||||
try:
|
|
||||||
results = self.backend_adapter.get_pricing_plans()
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
channel_product_listprice_obj = self.env['channel.product.pricelist']
|
|
||||||
pricelist_mapper = self.component(usage='import.mapper',
|
|
||||||
model_name='channel.product.pricelist')
|
|
||||||
for plan in results:
|
|
||||||
if 'vpid' in plan:
|
|
||||||
continue # FIXME: Ignore Virtual Plans
|
|
||||||
plan_record = pricelist_mapper.map_record(plan)
|
|
||||||
plan_bind = channel_product_listprice_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', str(plan['id'])),
|
|
||||||
], limit=1)
|
|
||||||
if not plan_bind:
|
|
||||||
channel_product_listprice_obj.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).create(plan_record.values(for_create=True))
|
|
||||||
else:
|
|
||||||
channel_product_listprice_obj.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).write(plan_record.values())
|
|
||||||
count = count + 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
|
|
||||||
class ProductPricelistImportMapper(Component):
|
|
||||||
_name = 'channel.product.pricelist.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.product.pricelist'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('id', 'external_id'),
|
|
||||||
('name', 'name'),
|
|
||||||
('daily', 'is_daily_plan'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
# 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 odoo import api, models, fields
|
from odoo import api, models, fields
|
||||||
from odoo.exceptions import ValidationError
|
from odoo.addons.queue_job.job import job
|
||||||
from odoo.addons.queue_job.job import job, related_action
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.component_event import skip_if
|
from odoo.addons.component_event import skip_if
|
||||||
|
|
||||||
@@ -28,12 +27,12 @@ class ChannelProductPricelistItem(models.Model):
|
|||||||
importer = work.component(usage='product.pricelist.item.importer')
|
importer = work.component(usage='product.pricelist.item.importer')
|
||||||
if not backend.pricelist_id:
|
if not backend.pricelist_id:
|
||||||
return importer.import_all_pricelist_values(
|
return importer.import_all_pricelist_values(
|
||||||
backend.pricelist_from,
|
dfrom,
|
||||||
backend.pricelist_to)
|
dto)
|
||||||
return importer.import_pricelist_values(
|
return importer.import_pricelist_values(
|
||||||
backend.pricelist_id.external_id,
|
external_id,
|
||||||
backend.pricelist_from,
|
dfrom,
|
||||||
backend.pricelist_to)
|
dto)
|
||||||
|
|
||||||
@job(default_channel='root.channel')
|
@job(default_channel='root.channel')
|
||||||
@api.model
|
@api.model
|
||||||
@@ -50,18 +49,6 @@ class ProductPricelistItem(models.Model):
|
|||||||
inverse_name='odoo_id',
|
inverse_name='odoo_id',
|
||||||
string='Hotel Channel Connector Bindings')
|
string='Hotel Channel Connector Bindings')
|
||||||
|
|
||||||
class ProducrPricelistItemAdapter(Component):
|
|
||||||
_name = 'channel.product.pricelist.item.adapter'
|
|
||||||
_inherit = 'wubook.adapter'
|
|
||||||
_apply_on = 'channel.product.pricelist.item'
|
|
||||||
|
|
||||||
def fetch_plan_prices(self, external_id, date_from, date_to, rooms):
|
|
||||||
return super(ProducrPricelistItemAdapter, self).fetch_plan_prices(
|
|
||||||
external_id,
|
|
||||||
date_from,
|
|
||||||
date_to,
|
|
||||||
rooms)
|
|
||||||
|
|
||||||
class BindingProductPricelistItemListener(Component):
|
class BindingProductPricelistItemListener(Component):
|
||||||
_name = 'binding.product.pricelist.item.listener'
|
_name = 'binding.product.pricelist.item.listener'
|
||||||
_inherit = 'base.connector.listener'
|
_inherit = 'base.connector.listener'
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
class ProductPricelistItemExporter(Component):
|
class ProductPricelistItemExporter(Component):
|
||||||
_name = 'channel.product.pricelist.item.exporter'
|
_name = 'channel.product.pricelist.item.exporter'
|
||||||
@@ -19,56 +13,4 @@ class ProductPricelistItemExporter(Component):
|
|||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def push_pricelist(self):
|
def push_pricelist(self):
|
||||||
channel_product_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
raise NotImplementedError
|
||||||
channel_product_pricelist_obj = self.env['channel.product.pricelist']
|
|
||||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
|
||||||
channel_unpushed = channel_product_pricelist_item_obj.search([
|
|
||||||
('channel_pushed', '=', False),
|
|
||||||
('date_start', '>=', datetime.now().strftime(
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT))
|
|
||||||
], order="date_start ASC")
|
|
||||||
|
|
||||||
if any(channel_unpushed):
|
|
||||||
date_start = fields.Date.from_string(channel_unpushed[0].date_start)
|
|
||||||
date_end = fields.Date.from_string(channel_unpushed[-1].date_start)
|
|
||||||
days_diff = (date_end - date_start).days + 1
|
|
||||||
|
|
||||||
prices = {}
|
|
||||||
pricelist_ids = channel_product_pricelist_obj.search([
|
|
||||||
('external_id', '!=', False),
|
|
||||||
('active', '=', True)
|
|
||||||
])
|
|
||||||
for pr in pricelist_ids:
|
|
||||||
prices.update({pr.external_id: {}})
|
|
||||||
unpushed_pl = channel_product_pricelist_item_obj.search(
|
|
||||||
[('channel_pushed', '=', False), ('pricelist_id', '=', pr.odoo_id.id)])
|
|
||||||
product_tmpl_ids = unpushed_pl.mapped('product_tmpl_id')
|
|
||||||
for pt_id in product_tmpl_ids:
|
|
||||||
channel_room_type = channel_room_type_obj.search([
|
|
||||||
('product_id.product_tmpl_id', '=', pt_id.id)
|
|
||||||
], limit=1)
|
|
||||||
if channel_room_type:
|
|
||||||
prices[pr.external_id].update({channel_room_type.external_id: []})
|
|
||||||
for i in range(0, days_diff):
|
|
||||||
prod = channel_room_type.product_id.with_context({
|
|
||||||
'quantity': 1,
|
|
||||||
'pricelist': pr.odoo_id.id,
|
|
||||||
'date': (date_start + timedelta(days=i)).
|
|
||||||
strftime(DEFAULT_SERVER_DATE_FORMAT),
|
|
||||||
})
|
|
||||||
prices[pr.external_id][channel_room_type.external_id].append(prod.price)
|
|
||||||
_logger.info("==[ODOO->CHANNEL]==== PRICELISTS ==")
|
|
||||||
_logger.info(prices)
|
|
||||||
try:
|
|
||||||
for k_pk, v_pk in prices.items():
|
|
||||||
if any(v_pk):
|
|
||||||
self.backend_adapter.update_plan_prices(k_pk, date_start.strftime(
|
|
||||||
DEFAULT_SERVER_DATE_FORMAT), v_pk)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'])
|
|
||||||
else:
|
|
||||||
channel_unpushed.write({'channel_pushed': True})
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
|
||||||
from datetime import timedelta
|
|
||||||
from odoo.addons.component.core import Component
|
from odoo.addons.component.core import Component
|
||||||
from odoo.addons.connector.components.mapper import mapping, only_create
|
from odoo import api
|
||||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
|
||||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
|
||||||
from odoo import fields, api, _
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ProductPricelistItemImporter(Component):
|
class ProductPricelistItemImporter(Component):
|
||||||
@@ -19,121 +11,10 @@ class ProductPricelistItemImporter(Component):
|
|||||||
_apply_on = ['channel.product.pricelist.item']
|
_apply_on = ['channel.product.pricelist.item']
|
||||||
_usage = 'product.pricelist.item.importer'
|
_usage = 'product.pricelist.item.importer'
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _generate_pricelist_items(self, channel_plan_id, date_from, date_to, plan_prices):
|
|
||||||
_logger.info("==[CHANNEL->ODOO]==== PRICELISTS [%d] (%s - %s) ==",
|
|
||||||
int(channel_plan_id), date_from, date_to)
|
|
||||||
_logger.info(plan_prices)
|
|
||||||
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
|
|
||||||
pricelist_bind = self.env['channel.product.pricelist'].search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', channel_plan_id),
|
|
||||||
], limit=1)
|
|
||||||
pricelist_item_mapper = self.component(
|
|
||||||
usage='import.mapper',
|
|
||||||
model_name='channel.product.pricelist.item')
|
|
||||||
if pricelist_bind:
|
|
||||||
channel_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
|
||||||
dfrom_dt = fields.Date.from_string(date_from)
|
|
||||||
dto_dt = fields.Date.from_string(date_to)
|
|
||||||
days_diff = (dto_dt-dfrom_dt).days + 1
|
|
||||||
for i in range(0, days_diff):
|
|
||||||
ndate_dt = dfrom_dt + timedelta(days=i)
|
|
||||||
for k_rid, v_rid in plan_prices.items():
|
|
||||||
channel_room_type = channel_hotel_room_type_obj.search([
|
|
||||||
('backend_id', '=', self.backend_record.id),
|
|
||||||
('external_id', '=', k_rid),
|
|
||||||
], limit=1)
|
|
||||||
if channel_room_type:
|
|
||||||
ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
|
||||||
item = {
|
|
||||||
'price': plan_prices[k_rid][i],
|
|
||||||
'channel_room_type': channel_room_type,
|
|
||||||
'pricelist_id': pricelist_bind.odoo_id.id,
|
|
||||||
'date': ndate_str,
|
|
||||||
}
|
|
||||||
map_record = pricelist_item_mapper.map_record(item)
|
|
||||||
pricelist_item = channel_pricelist_item_obj.search([
|
|
||||||
('pricelist_id', '=', pricelist_bind.odoo_id.id),
|
|
||||||
('date_start', '=', ndate_str),
|
|
||||||
('date_end', '=', ndate_str),
|
|
||||||
('compute_price', '=', 'fixed'),
|
|
||||||
('applied_on', '=', '1_product'),
|
|
||||||
('product_tmpl_id', '=',
|
|
||||||
channel_room_type.product_id.product_tmpl_id.id)
|
|
||||||
], limit=1)
|
|
||||||
if pricelist_item:
|
|
||||||
pricelist_item.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).write(map_record.values())
|
|
||||||
else:
|
|
||||||
pricelist_item = channel_pricelist_item_obj.with_context({
|
|
||||||
'connector_no_export': True,
|
|
||||||
}).create(map_record.values(for_create=True))
|
|
||||||
pricelist_item.channel_pushed = True
|
|
||||||
return True
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_all_pricelist_values(self, date_from, date_to, rooms=None):
|
def import_all_pricelist_values(self, date_from, date_to, rooms=None):
|
||||||
external_ids = self.env['channel.product.pricelist'].search([]).mapped('external_id')
|
raise NotImplementedError
|
||||||
for external_id in external_ids:
|
|
||||||
if external_id:
|
|
||||||
self.import_pricelist_values(external_id, date_from, date_to, rooms=rooms)
|
|
||||||
return True
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def import_pricelist_values(self, external_id, date_from, date_to, rooms=None):
|
def import_pricelist_values(self, external_id, date_from, date_to, rooms=None):
|
||||||
try:
|
raise NotImplementedError
|
||||||
results = self.backend_adapter.fetch_plan_prices(
|
|
||||||
external_id,
|
|
||||||
date_from,
|
|
||||||
date_to,
|
|
||||||
rooms)
|
|
||||||
except ChannelConnectorError as err:
|
|
||||||
self.create_issue(
|
|
||||||
section='pricelist',
|
|
||||||
internal_message=str(err),
|
|
||||||
channel_message=err.data['message'],
|
|
||||||
channel_object_id=external_id,
|
|
||||||
dfrom=date_from,
|
|
||||||
dto=date_to)
|
|
||||||
else:
|
|
||||||
self._generate_pricelist_items(external_id, date_from, date_to, results)
|
|
||||||
|
|
||||||
class ProductPricelistItemImportMapper(Component):
|
|
||||||
_name = 'channel.product.pricelist.item.import.mapper'
|
|
||||||
_inherit = 'channel.import.mapper'
|
|
||||||
_apply_on = 'channel.product.pricelist.item'
|
|
||||||
|
|
||||||
direct = [
|
|
||||||
('price', 'fixed_price'),
|
|
||||||
('date', 'date_start'),
|
|
||||||
('date', 'date_end'),
|
|
||||||
]
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def compute_price(self, record):
|
|
||||||
return {'compute_price': 'fixed'}
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def channel_pushed(self, record):
|
|
||||||
return {'channel_pushed': True}
|
|
||||||
|
|
||||||
@only_create
|
|
||||||
@mapping
|
|
||||||
def applied_on(self, record):
|
|
||||||
return {'applied_on': '1_product'}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def product_tmpl_id(self, record):
|
|
||||||
return {'product_tmpl_id': record['channel_room_type'].product_id.product_tmpl_id.id}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def pricelist_id(self, record):
|
|
||||||
return {'pricelist_id': record['pricelist_id']}
|
|
||||||
|
|
||||||
@mapping
|
|
||||||
def backend_id(self, record):
|
|
||||||
return {'backend_id': self.backend_record.id}
|
|
||||||
|
|||||||
@@ -23,8 +23,6 @@
|
|||||||
<group colspan="4" col="4">
|
<group colspan="4" col="4">
|
||||||
<field name="version" colspan="2"/>
|
<field name="version" colspan="2"/>
|
||||||
<field name="server" colspan="2"/>
|
<field name="server" colspan="2"/>
|
||||||
<field name="lcode" colspan="2"/>
|
|
||||||
<field name="pkey" colspan="2"/>
|
|
||||||
<field name="username" colspan="2"/>
|
<field name="username" colspan="2"/>
|
||||||
<field name="passwd" password="1" colspan="2"/>
|
<field name="passwd" password="1" colspan="2"/>
|
||||||
</group>
|
</group>
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
<group>
|
<group>
|
||||||
<field name="ota_id" />
|
<field name="ota_id" />
|
||||||
<field name="name" />
|
<field name="name" />
|
||||||
<field name="ical" readonly="True" widget="checkbox" />
|
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ class MassiveChangesWizard(models.TransientModel):
|
|||||||
@api.multi
|
@api.multi
|
||||||
def massive_change(self):
|
def massive_change(self):
|
||||||
res = super(MassiveChangesWizard, self).massive_change()
|
res = super(MassiveChangesWizard, self).massive_change()
|
||||||
self.env['wubook'].push_changes()
|
self.env['channel.backend'].push_changes()
|
||||||
return res
|
return res
|
||||||
|
|||||||
13
hotel_channel_connector_wubook/README.rst
Normal file
13
hotel_channel_connector_wubook/README.rst
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
HOTEL CHANNEL CONNECTOR WUBOOK
|
||||||
|
=============
|
||||||
|
|
||||||
|
** UNDER DEVELOPMENT: NOT USE IN PRODUCTION **
|
||||||
|
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Creator
|
||||||
|
------------
|
||||||
|
|
||||||
|
* Alexandre Díaz <alex@aloxa.eu>
|
||||||
6
hotel_channel_connector_wubook/__init__.py
Normal file
6
hotel_channel_connector_wubook/__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 components
|
||||||
|
from . import controllers
|
||||||
|
from . import models
|
||||||
32
hotel_channel_connector_wubook/__manifest__.py
Normal file
32
hotel_channel_connector_wubook/__manifest__.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
'name': 'Hotel Channel Connector Wubook',
|
||||||
|
'version': '1.0',
|
||||||
|
'author': "Alexandre Díaz <dev@redneboa.es>",
|
||||||
|
'website': 'https://github.com/hootel/hootel',
|
||||||
|
'category': 'hotel/connector',
|
||||||
|
'summary': "Hotel Channel Connector Wubook",
|
||||||
|
'description': "Hotel Channel Connector Wubook",
|
||||||
|
'depends': [
|
||||||
|
'hotel_channel_connector',
|
||||||
|
],
|
||||||
|
'external_dependencies': {
|
||||||
|
'python': ['xmlrpc']
|
||||||
|
},
|
||||||
|
'data': [
|
||||||
|
'data/cron_jobs.xml',
|
||||||
|
'data/sequences.xml',
|
||||||
|
'views/inherited_channel_connector_backend_views.xml',
|
||||||
|
'views/inherited_channel_ota_info_views.xml',
|
||||||
|
#'security/wubook_security.xml',
|
||||||
|
# 'views/res_config.xml'
|
||||||
|
],
|
||||||
|
'test': [
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False,
|
||||||
|
'application': False,
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
}
|
||||||
4
hotel_channel_connector_wubook/components/__init__.py
Normal file
4
hotel_channel_connector_wubook/components/__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 backend_adapter
|
||||||
478
hotel_channel_connector_wubook/components/backend_adapter.py
Normal file
478
hotel_channel_connector_wubook/components/backend_adapter.py
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import xmlrpc.client
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from odoo.addons.component.core import AbstractComponent
|
||||||
|
from odoo.addons.queue_job.exception import RetryableJobError
|
||||||
|
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import fields, _
|
||||||
|
|
||||||
|
# GLOBAL VARS
|
||||||
|
DEFAULT_WUBOOK_DATE_FORMAT = "%d/%m/%Y"
|
||||||
|
DEFAULT_WUBOOK_TIME_FORMAT = "%H:%M"
|
||||||
|
DEFAULT_WUBOOK_DATETIME_FORMAT = "%s %s" % (DEFAULT_WUBOOK_DATE_FORMAT,
|
||||||
|
DEFAULT_WUBOOK_TIME_FORMAT)
|
||||||
|
WUBOOK_STATUS_CONFIRMED = 1
|
||||||
|
WUBOOK_STATUS_WAITING = 2
|
||||||
|
WUBOOK_STATUS_REFUSED = 3
|
||||||
|
WUBOOK_STATUS_ACCEPTED = 4
|
||||||
|
WUBOOK_STATUS_CANCELLED = 5
|
||||||
|
WUBOOK_STATUS_CANCELLED_PENALTY = 6
|
||||||
|
|
||||||
|
WUBOOK_STATUS_GOOD = (
|
||||||
|
WUBOOK_STATUS_CONFIRMED,
|
||||||
|
WUBOOK_STATUS_WAITING,
|
||||||
|
WUBOOK_STATUS_ACCEPTED,
|
||||||
|
)
|
||||||
|
WUBOOK_STATUS_BAD = (
|
||||||
|
WUBOOK_STATUS_REFUSED,
|
||||||
|
WUBOOK_STATUS_CANCELLED,
|
||||||
|
WUBOOK_STATUS_CANCELLED_PENALTY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WuBookLogin(object):
|
||||||
|
def __init__(self, address, user, passwd, lcode, pkey):
|
||||||
|
self.address = address
|
||||||
|
self.user = user
|
||||||
|
self.passwd = passwd
|
||||||
|
self.lcode = lcode
|
||||||
|
self.pkey = pkey
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return self.address and self.user and self.passwd and self.lcode and self.pkey
|
||||||
|
|
||||||
|
|
||||||
|
class WuBookServer(object):
|
||||||
|
def __init__(self, login_data):
|
||||||
|
self._server = None
|
||||||
|
self._token = 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 not self._login_data.is_valid():
|
||||||
|
raise ChannelConnectorError(_("Invalid Channel Parameters!"))
|
||||||
|
if self._server is None:
|
||||||
|
try:
|
||||||
|
self._server = xmlrpc.client.ServerProxy(self._login_data.address)
|
||||||
|
res, tok = self._server.acquire_token(
|
||||||
|
self._login_data.user,
|
||||||
|
self._login_data.passwd,
|
||||||
|
self._login_data.pkey)
|
||||||
|
if res == 0:
|
||||||
|
self._token = tok
|
||||||
|
else:
|
||||||
|
self._server = None
|
||||||
|
except Exception:
|
||||||
|
self._server = None
|
||||||
|
raise RetryableJobError(_("Can't connect with channel!"))
|
||||||
|
return self._server
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session_token(self):
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
@property
|
||||||
|
def lcode(self):
|
||||||
|
return self._login_data.lcode
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._server.release_token(self._token)
|
||||||
|
self._token = None
|
||||||
|
self._server = None
|
||||||
|
|
||||||
|
|
||||||
|
class WuBookAdapter(AbstractComponent):
|
||||||
|
_name = 'wubook.adapter'
|
||||||
|
_inherit = 'hotel.channel.adapter'
|
||||||
|
|
||||||
|
# === GENERAL
|
||||||
|
def push_activation(self, base_url, security_token):
|
||||||
|
rcode_a, results_a = self._server.push_activation(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
urljoin(base_url,
|
||||||
|
"/wubook/push/reservations/%s" % security_token),
|
||||||
|
1)
|
||||||
|
if rcode_a != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't activate push reservations"), {
|
||||||
|
'message': results_a,
|
||||||
|
})
|
||||||
|
|
||||||
|
rcode_b, results_b = self._server.push_update_activation(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
urljoin(base_url, "/wubook/push/rooms/%s" % security_token))
|
||||||
|
if rcode_b != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't activate push rooms"), {
|
||||||
|
'message': results_b,
|
||||||
|
})
|
||||||
|
|
||||||
|
return rcode_a == 0 and results_b == 0
|
||||||
|
|
||||||
|
# === ROOMS
|
||||||
|
def create_room(self, shortcode, name, capacity, price, availability):
|
||||||
|
rcode, results = self._server.new_room(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
0,
|
||||||
|
name,
|
||||||
|
capacity,
|
||||||
|
price,
|
||||||
|
availability,
|
||||||
|
shortcode[:4],
|
||||||
|
'nb' # TODO: Complete this part
|
||||||
|
# rtype=('name' in vals and vals['name'] and 3) or 1
|
||||||
|
)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't create room in WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def modify_room(self, channel_room_id, name, capacity, price, availability, scode):
|
||||||
|
rcode, results = self._server.mod_room(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_room_id,
|
||||||
|
name,
|
||||||
|
capacity,
|
||||||
|
price,
|
||||||
|
availability,
|
||||||
|
scode,
|
||||||
|
'nb'
|
||||||
|
# rtype=('name' in vals and vals['name'] and 3) or 1
|
||||||
|
)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't modify room in WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_id': channel_room_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def delete_room(self, channel_room_id):
|
||||||
|
rcode, results = self._server.del_room(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_room_id)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't delete room in WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_id': channel_room_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def fetch_rooms(self, channel_room_id=0):
|
||||||
|
rcode, results = self._server.fetch_rooms(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_room_id)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't fetch room values from WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_id': channel_room_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def fetch_rooms_values(self, date_from, date_to, rooms=False):
|
||||||
|
rcode, results = self._server.fetch_rooms_values(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
rooms)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't fetch rooms values from WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_availability(self, rooms_avail):
|
||||||
|
rcode, results = self._server.update_sparse_avail(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
rooms_avail)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't update rooms availability in WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def corporate_fetch(self):
|
||||||
|
rcode, results = self._server.corporate_fetchable_properties(self.TOKEN)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't call 'corporate_fetch' from WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
# === RESERVATIONS
|
||||||
|
def create_reservation(self, channel_room_id, customer_name, email, city,
|
||||||
|
phone, address, country_code, checkin, checkout,
|
||||||
|
adults, children, notes=''):
|
||||||
|
customer_name = _partner_split_name(customer_name)
|
||||||
|
customer = {
|
||||||
|
'lname': customer_name[0],
|
||||||
|
'fname': customer_name[1],
|
||||||
|
'email': email,
|
||||||
|
'city': city,
|
||||||
|
'phone': phone,
|
||||||
|
'street': address,
|
||||||
|
'country': country_code,
|
||||||
|
'arrival_hour': fields.Datetime.from_string(checkin).strftime("%H:%M"),
|
||||||
|
'notes': notes
|
||||||
|
}
|
||||||
|
rcode, results = self._server.new_reservation(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
fields.Date.from_string(checkin).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
fields.Date.from_string(checkout).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
{channel_room_id: [adults+children, 'nb']},
|
||||||
|
customer,
|
||||||
|
adults+children)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't create reservations in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'date_from': checkin,
|
||||||
|
'date_to': checkout,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def cancel_reservation(self, channel_reservation_id, reason=""):
|
||||||
|
rcode, results = self._server.cancel_reservation(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_reservation_id,
|
||||||
|
reason)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't cancel reservation in WuBook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_reservation_id': channel_reservation_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def fetch_new_bookings(self):
|
||||||
|
rcode, results = self._server.fetch_new_bookings(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
1,
|
||||||
|
0)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't process reservations from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def fetch_booking(self, channel_reservation_id):
|
||||||
|
rcode, results = self._server.fetch_booking(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_reservation_id)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't process reservation from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def mark_bookings(self, channel_reservation_ids):
|
||||||
|
rcode, results = self._server.mark_bookings(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_reservation_ids)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't mark as readed a reservation in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_reservation_ids': str(channel_reservation_ids),
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
# === PRICE PLANS
|
||||||
|
def create_plan(self, name, daily=1):
|
||||||
|
rcode, results = self._server.add_pricing_plan(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
name,
|
||||||
|
daily)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't add pricing plan to wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def delete_plan(self, channel_plan_id):
|
||||||
|
rcode, results = self._server.del_plan(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_plan_id)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't delete pricing plan from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_plan_id': channel_plan_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_plan_name(self, channel_plan_id, new_name):
|
||||||
|
rcode, results = self._server.update_plan_name(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_plan_id,
|
||||||
|
new_name)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't update pricing plan name in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_plan_id': channel_plan_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_plan_prices(self, channel_plan_id, date_from, prices):
|
||||||
|
rcode, results = self._server.update_plan_prices(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_plan_id,
|
||||||
|
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
prices)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't update pricing plan in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_plan_id': channel_plan_id,
|
||||||
|
'date_from': date_from,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_plan_periods(self, channel_plan_id, periods):
|
||||||
|
rcode, results = self._server.update_plan_periods(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_plan_id,
|
||||||
|
periods)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't update pricing plan period in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_plan_id': channel_plan_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_pricing_plans(self):
|
||||||
|
rcode, results = self._server.get_pricing_plans(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1])
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't get pricing plans from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def fetch_plan_prices(self, channel_plan_id, date_from, date_to, rooms):
|
||||||
|
rcode, results = self._server.fetch_plan_prices(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_plan_id,
|
||||||
|
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
rooms or [])
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't get pricing plans from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_plan_id': channel_plan_id,
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
# === RESTRICTIONS
|
||||||
|
def rplan_rplans(self):
|
||||||
|
rcode, results = self._server.rplan_rplans(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1])
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't fetch restriction plans from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def wired_rplan_get_rplan_values(self, date_from, date_to, channel_restriction_plan_id):
|
||||||
|
rcode, results = self._server.wired_rplan_get_rplan_values(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
int(channel_restriction_plan_id))
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't fetch restriction plans from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_restriction_plan_id': channel_restriction_plan_id,
|
||||||
|
'date_from': date_from,
|
||||||
|
'date_to': date_to,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def update_rplan_values(self, channel_restriction_plan_id, date_from, values):
|
||||||
|
rcode, results = self._server.rplan_update_rplan_values(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_restriction_plan_id,
|
||||||
|
fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
values)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't update plan restrictions on wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_restriction_plan_id': channel_restriction_plan_id,
|
||||||
|
'date_from': date_from,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def create_rplan(self, name, compact=False):
|
||||||
|
rcode, results = self._server.rplan_add_rplan(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
name,
|
||||||
|
compact and 1 or 0)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't create plan restriction in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def rename_rplan(self, channel_restriction_plan_id, new_name):
|
||||||
|
rcode, results = self._server.rplan_rename_rplan(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_restriction_plan_id,
|
||||||
|
new_name)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't rename plan restriction in wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_restriction_plan_id': channel_restriction_plan_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def delete_rplan(self, channel_restriction_plan_id):
|
||||||
|
rcode, results = self._server.rplan_del_rplan(
|
||||||
|
self._session_info[0],
|
||||||
|
self._session_info[1],
|
||||||
|
channel_restriction_plan_id)
|
||||||
|
if rcode != 0:
|
||||||
|
raise ChannelConnectorError(_("Can't delete plan restriction on wubook"), {
|
||||||
|
'message': results,
|
||||||
|
'channel_restriction_plan_id': channel_restriction_plan_id,
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
def get_channels_info(self):
|
||||||
|
results = self._server.get_channels_info(self._session_info[0])
|
||||||
|
if not any(results):
|
||||||
|
raise ChannelConnectorError(_("Can't import channels info from wubook"), {
|
||||||
|
'message': results,
|
||||||
|
})
|
||||||
|
return results
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
# 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 main
|
from . import main
|
||||||
@@ -3,28 +3,31 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from openerp import http, _
|
from odoo import http, _
|
||||||
from openerp.http import request
|
from odoo.http import request
|
||||||
from openerp.exceptions import ValidationError
|
from odoo.exceptions import ValidationError
|
||||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
from odoo.addons.hotel_channel_connector_wubook.components.backend_adapter import (
|
||||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||||
from odoo.tools import (
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||||
DEFAULT_SERVER_DATE_FORMAT,
|
|
||||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class website_wubook(http.Controller):
|
class WubookPushURL(http.Controller):
|
||||||
# Called when created a reservation in wubook
|
# Called when created a reservation in wubook
|
||||||
@http.route(['/wubook/push/reservations/<string:security_token>'],
|
@http.route(['/wubook/push/reservations/<string:security_token>'],
|
||||||
type='http', cors="*", auth="public", methods=['POST'],
|
type='http', cors="*", auth="public", methods=['POST'],
|
||||||
website=True, csrf=False)
|
website=True, csrf=True)
|
||||||
def wubook_push_reservations(self, security_token, **kwargs):
|
def wubook_push_reservations(self, security_token, **kwargs):
|
||||||
rcode = kwargs.get('rcode')
|
rcode = kwargs.get('rcode')
|
||||||
lcode = kwargs.get('lcode')
|
lcode = kwargs.get('lcode')
|
||||||
|
|
||||||
|
_logger.info("====== PUSH RESERVATION")
|
||||||
|
_logger.info(rcode)
|
||||||
|
_logger.info(lcode)
|
||||||
|
_logger.info(security_token)
|
||||||
|
|
||||||
# Correct Input?
|
# Correct Input?
|
||||||
if not lcode or not rcode:
|
if not lcode or not rcode or not security_token:
|
||||||
raise ValidationError(_('Invalid Input Parameters!'))
|
raise ValidationError(_('Invalid Input Parameters!'))
|
||||||
|
|
||||||
# WuBook Check
|
# WuBook Check
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<field name="state">code</field>
|
<field name="state">code</field>
|
||||||
<field name="doall" eval="False" />
|
<field name="doall" eval="False" />
|
||||||
<field name="model_id" ref="model_channel_backend" />
|
<field name="model_id" ref="model_channel_backend" />
|
||||||
<field name="code">model.cron_push_changes()</field>
|
<field name="code">model.with_context({'show_notify': False}).cron_push_changes()</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record model="ir.cron" id="channel_connector_fetch_new_bookings">
|
<record model="ir.cron" id="channel_connector_fetch_new_bookings">
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<field name="state">code</field>
|
<field name="state">code</field>
|
||||||
<field name="doall" eval="False" />
|
<field name="doall" eval="False" />
|
||||||
<field name="model_id" ref="model_channel_backend" />
|
<field name="model_id" ref="model_channel_backend" />
|
||||||
<field name="code">model.cron_import_reservations()</field>
|
<field name="code">model.with_context({'show_notify': False}).cron_import_reservations()</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
1121
hotel_channel_connector_wubook/i18n/es.po
Normal file
1121
hotel_channel_connector_wubook/i18n/es.po
Normal file
File diff suppressed because it is too large
Load Diff
13
hotel_channel_connector_wubook/models/__init__.py
Normal file
13
hotel_channel_connector_wubook/models/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from . import channel_backend
|
||||||
|
from . import hotel_room_type
|
||||||
|
from . import product_pricelist
|
||||||
|
from . import product_pricelist_item
|
||||||
|
from . import hotel_room_type_restriction
|
||||||
|
from . import hotel_room_type_restriction_item
|
||||||
|
from . import hotel_room_type_availability
|
||||||
|
from . import hotel_reservation
|
||||||
|
from . import inherited_hotel_folio
|
||||||
|
from . import channel_ota_info
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 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 WuBookLogin, WuBookServer
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelBackend(models.Model):
|
||||||
|
_inherit = 'channel.backend'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def select_versions(self):
|
||||||
|
""" Available versions in the backend.
|
||||||
|
Can be inherited to add custom versions. Using this method
|
||||||
|
to add a version from an ``_inherit`` does not constrain
|
||||||
|
to redefine the ``version`` field in the ``_inherit`` model.
|
||||||
|
"""
|
||||||
|
super(ChannelBackend, self).select_versions()
|
||||||
|
return [('1.2', '1.2+')]
|
||||||
|
|
||||||
|
def _get_default_server(self):
|
||||||
|
super(ChannelBackend, self)._get_default_server()
|
||||||
|
return 'https://wired.wubook.net/xrws/'
|
||||||
|
|
||||||
|
lcode = fields.Char('Channel Service lcode')
|
||||||
|
pkey = fields.Char('Channel Service PKey')
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
@api.multi
|
||||||
|
def work_on(self, model_name, **kwargs):
|
||||||
|
self.ensure_one()
|
||||||
|
wubook_login = WuBookLogin(
|
||||||
|
self.server,
|
||||||
|
self.username,
|
||||||
|
self.passwd,
|
||||||
|
self.lcode,
|
||||||
|
self.pkey)
|
||||||
|
with WuBookServer(wubook_login) as channel_api:
|
||||||
|
_super = super(ChannelBackend, self)
|
||||||
|
with _super.work_on(model_name, channel_api=channel_api, **kwargs) as work:
|
||||||
|
yield work
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# 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 importer
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.component.core import Component
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelOtaInfo(models.Model):
|
||||||
|
_inherit = 'channel.ota.info'
|
||||||
|
|
||||||
|
ical = fields.Boolean("ical", default=False)
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeAdapter(Component):
|
||||||
|
_name = 'channel.ota.info.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.ota.info'
|
||||||
|
|
||||||
|
def fetch_rooms(self):
|
||||||
|
return super(HotelRoomTypeAdapter, self).fetch_rooms()
|
||||||
|
|
||||||
|
def push_activation(self, base_url, security_token):
|
||||||
|
return super(HotelRoomTypeAdapter, self).push_activation(base_url, security_token)
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.connector.components.mapper import mapping
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
from odoo.tools import (
|
||||||
|
DEFAULT_SERVER_DATE_FORMAT,
|
||||||
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelOtaInfoImporter(Component):
|
||||||
|
_inherit = 'channel.ota.info.importer'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def import_otas_info(self):
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.get_channels_info()
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='room',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
channel_ota_info_obj = self.env['channel.ota.info']
|
||||||
|
ota_info_mapper = self.component(usage='import.mapper',
|
||||||
|
model_name='channel.ota.info')
|
||||||
|
for ota_id in results.keys():
|
||||||
|
vals = {
|
||||||
|
'id': ota_id,
|
||||||
|
'name': results[ota_id]['name'],
|
||||||
|
'ical': results[ota_id]['ical'] == 1,
|
||||||
|
}
|
||||||
|
map_record = ota_info_mapper.map_record(vals)
|
||||||
|
ota_info_bind = channel_ota_info_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('ota_id', '=', ota_id)
|
||||||
|
], limit=1)
|
||||||
|
if ota_info_bind:
|
||||||
|
ota_info_bind.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
}).write(map_record.values())
|
||||||
|
else:
|
||||||
|
ota_info_bind.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
}).create(map_record.values(for_create=True))
|
||||||
|
count = count + 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def push_activation(self, base_url):
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.push_activation(
|
||||||
|
base_url,
|
||||||
|
self.backend_record.security_token)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='channel',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
return False
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelOtaInfoImportMapper(Component):
|
||||||
|
_name = 'channel.ota.info.import.mapper'
|
||||||
|
_inherit = 'channel.import.mapper'
|
||||||
|
_apply_on = 'channel.ota.info'
|
||||||
|
|
||||||
|
direct = [
|
||||||
|
('id', 'ota_id'),
|
||||||
|
('name', 'name'),
|
||||||
|
('ical', 'ical'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@mapping
|
||||||
|
def backend_id(self, record):
|
||||||
|
return {'backend_id': self.backend_record.id}
|
||||||
@@ -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 importer
|
||||||
|
from . import exporter
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api, models, fields, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.addons.component.core import Component
|
||||||
|
from odoo.addons.hotel_channel_connector_wubook.components.backend_adapter import (
|
||||||
|
WUBOOK_STATUS_CONFIRMED,
|
||||||
|
WUBOOK_STATUS_WAITING,
|
||||||
|
WUBOOK_STATUS_REFUSED,
|
||||||
|
WUBOOK_STATUS_ACCEPTED,
|
||||||
|
WUBOOK_STATUS_CANCELLED,
|
||||||
|
WUBOOK_STATUS_CANCELLED_PENALTY,
|
||||||
|
WUBOOK_STATUS_BAD)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelHotelReservation(models.Model):
|
||||||
|
_inherit = 'channel.hotel.reservation'
|
||||||
|
|
||||||
|
channel_status = fields.Selection(selection_add=[
|
||||||
|
(str(WUBOOK_STATUS_CONFIRMED), 'Confirmed'),
|
||||||
|
(str(WUBOOK_STATUS_WAITING), 'Waiting'),
|
||||||
|
(str(WUBOOK_STATUS_REFUSED), 'Refused'),
|
||||||
|
(str(WUBOOK_STATUS_ACCEPTED), 'Accepted'),
|
||||||
|
(str(WUBOOK_STATUS_CANCELLED), 'Cancelled'),
|
||||||
|
(str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty'),
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
class HotelReservation(models.Model):
|
||||||
|
_inherit = 'hotel.reservation'
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def action_cancel(self):
|
||||||
|
no_export = self._context.get('connector_no_export', True)
|
||||||
|
if no_export:
|
||||||
|
for record in self:
|
||||||
|
# Can't cancel in Odoo
|
||||||
|
if record.is_from_ota:
|
||||||
|
raise ValidationError(_("Can't cancel reservations from OTA's"))
|
||||||
|
user = self.env['res.users'].browse(self.env.uid)
|
||||||
|
if user.has_group('hotel.group_hotel_call'):
|
||||||
|
self.write({'to_read': True, 'to_assign': True})
|
||||||
|
|
||||||
|
res = super(HotelReservation, self).action_cancel()
|
||||||
|
if no_export:
|
||||||
|
for record in self:
|
||||||
|
# Only can cancel reservations created directly in wubook
|
||||||
|
if any(record.channel_bind_ids) and \
|
||||||
|
record.channel_bind_ids[0].external_id and \
|
||||||
|
not record.channel_bind_ids[0].ota_id and \
|
||||||
|
record.channel_bind_ids[0].channel_status in ['1', '2', '4']:
|
||||||
|
self._event('on_record_cancel').notify(record)
|
||||||
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def confirm(self):
|
||||||
|
can_confirm = True
|
||||||
|
for record in self:
|
||||||
|
if record.is_from_ota and any(record.channel_bind_ids) and \
|
||||||
|
int(record.channel_bind_ids[0].channel_status) in WUBOOK_STATUS_BAD:
|
||||||
|
can_confirm = False
|
||||||
|
break
|
||||||
|
if not can_confirm:
|
||||||
|
raise ValidationError(_("Can't confirm OTA's cancelled reservations"))
|
||||||
|
return super(HotelReservation, self).confirm()
|
||||||
|
|
||||||
|
|
||||||
|
class HotelReservationAdapter(Component):
|
||||||
|
_name = 'channel.hotel.reservation.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.hotel.reservation'
|
||||||
|
|
||||||
|
def mark_bookings(self, channel_reservation_ids):
|
||||||
|
return super(HotelReservationAdapter, self).mark_bookings(
|
||||||
|
channel_reservation_ids)
|
||||||
|
|
||||||
|
def fetch_new_bookings(self):
|
||||||
|
return super(HotelReservationAdapter, self).fetch_new_bookings()
|
||||||
|
|
||||||
|
def fetch_booking(self, channel_reservation_id):
|
||||||
|
return super(HotelReservationAdapter, self).fetch_booking(
|
||||||
|
channel_reservation_id)
|
||||||
|
|
||||||
|
def cancel_reservation(self, channel_reservation_id, message):
|
||||||
|
return super(HotelReservationAdapter, self).cancel_reservation(
|
||||||
|
channel_reservation_id, message)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api, _
|
||||||
|
|
||||||
|
class HotelReservationExporter(Component):
|
||||||
|
_inherit = 'channel.hotel.reservation.exporter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def cancel_reservation(self, binding):
|
||||||
|
user = self.env['res.user'].browse(self.env.uid)
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.cancel_reservation(
|
||||||
|
binding.external_id,
|
||||||
|
_('Cancelled by %s') % user.partner_id.name)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_object_id=binding.external_id,
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def mark_booking(self, binding):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.mark_bookings([binding.external_id])
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_object_id=binding.external_id,
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def mark_bookings(self, external_ids):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.mark_bookings(external_ids)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_object_id=external_ids,
|
||||||
|
channel_message=err.data['message'])
|
||||||
@@ -0,0 +1,453 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from dateutil import tz
|
||||||
|
from odoo import fields, api
|
||||||
|
from odoo.tools import (
|
||||||
|
DEFAULT_SERVER_DATE_FORMAT,
|
||||||
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
from odoo.addons.component.core import Component
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo.addons.hotel_channel_connector_wubook.components.backend_adapter import (
|
||||||
|
DEFAULT_WUBOOK_DATE_FORMAT,
|
||||||
|
DEFAULT_WUBOOK_DATETIME_FORMAT,
|
||||||
|
WUBOOK_STATUS_BAD)
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HotelReservationImporter(Component):
|
||||||
|
_inherit = 'channel.hotel.reservation.importer'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def fetch_booking(self, channel_reservation_id):
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.fetch_booking(channel_reservation_id)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
if any(results):
|
||||||
|
processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \
|
||||||
|
self._generate_reservations(results)
|
||||||
|
if any(processed_rids):
|
||||||
|
self.backend_adapter.mark_bookings(list(set(processed_rids)))
|
||||||
|
# Update Odoo availability (don't wait for wubook)
|
||||||
|
# FIXME: This cause abuse service in first import!!
|
||||||
|
if checkin_utc_dt and checkout_utc_dt:
|
||||||
|
self.backend_adapter.fetch_rooms_values(
|
||||||
|
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
|
checkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def fetch_new_bookings(self):
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.fetch_new_bookings()
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
if any(results):
|
||||||
|
processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \
|
||||||
|
self._generate_reservations(results)
|
||||||
|
if any(processed_rids):
|
||||||
|
uniq_rids = list(set(processed_rids))
|
||||||
|
self.backend_adapter.mark_bookings(uniq_rids)
|
||||||
|
count = len(uniq_rids)
|
||||||
|
# Update Odoo availability (don't wait for wubook)
|
||||||
|
# FIXME: This cause abuse service in first import!!
|
||||||
|
if checkin_utc_dt and checkout_utc_dt:
|
||||||
|
self.backend_adapter.fetch_rooms_values(
|
||||||
|
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
|
checkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT))
|
||||||
|
return count
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _generate_booking_vals(self, broom, crcode, rcode, room_type_bind,
|
||||||
|
split_booking, dates_checkin, dates_checkout, book):
|
||||||
|
is_cancellation = book['status'] in WUBOOK_STATUS_BAD
|
||||||
|
tax_inclusive = True
|
||||||
|
persons = room_type_bind.channel_capacity
|
||||||
|
# Dates
|
||||||
|
checkin_str = dates_checkin[0].strftime(
|
||||||
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
checkout_str = dates_checkout[0].strftime(
|
||||||
|
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
# Parse 'ancyllary' info
|
||||||
|
if 'ancillary' in broom:
|
||||||
|
if 'guests' in broom['ancillary']:
|
||||||
|
persons = broom['ancillary']['guests']
|
||||||
|
if 'tax_inclusive' in broom['ancillary'] and not broom['ancillary']['tax_inclusive']:
|
||||||
|
_logger.info("--- Incoming Reservation without taxes included!")
|
||||||
|
tax_inclusive = False
|
||||||
|
# Generate Reservation Day Lines
|
||||||
|
reservation_lines = []
|
||||||
|
tprice = 0.0
|
||||||
|
for brday in broom['roomdays']:
|
||||||
|
wndate = datetime.strptime(
|
||||||
|
brday['day'],
|
||||||
|
DEFAULT_WUBOOK_DATE_FORMAT
|
||||||
|
).replace(tzinfo=tz.gettz('UTC'))
|
||||||
|
if dates_checkin[0] >= wndate <= (dates_checkout[0] - timedelta(days=1)):
|
||||||
|
# HOT-FIX: Hard-Coded Tax 10%
|
||||||
|
room_day_price = round(brday['price'] * 1.1, 2) if not tax_inclusive else brday['price']
|
||||||
|
reservation_lines.append((0, False, {
|
||||||
|
'date': wndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
|
'price': room_day_price,
|
||||||
|
}))
|
||||||
|
tprice += room_day_price
|
||||||
|
# Get OTA
|
||||||
|
ota_id = self.env['channel.ota.info'].search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('ota_id', '=', str(book['id_channel'])),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
vals = {
|
||||||
|
'backend_id': self.backend_record.id,
|
||||||
|
'checkin': checkin_str,
|
||||||
|
'checkout': checkout_str,
|
||||||
|
'adults': persons,
|
||||||
|
'children': book['children'],
|
||||||
|
'reservation_lines': reservation_lines,
|
||||||
|
'price_unit': tprice,
|
||||||
|
'to_assign': True,
|
||||||
|
'wrid': rcode,
|
||||||
|
'ota_id': ota_id and ota_id.id,
|
||||||
|
'wchannel_reservation_code': crcode,
|
||||||
|
'channel_status': str(book['status']),
|
||||||
|
'to_read': True,
|
||||||
|
'state': is_cancellation and 'cancelled' or 'draft',
|
||||||
|
'room_type_id': room_type_bind.odoo_id.id,
|
||||||
|
'splitted': split_booking,
|
||||||
|
'wbook_json': json.dumps(book),
|
||||||
|
'wmodified': book['was_modified'],
|
||||||
|
'product_id': room_type_bind and room_type_bind.product_id.id,
|
||||||
|
'name': room_type_bind and room_type_bind.name,
|
||||||
|
}
|
||||||
|
return vals
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _generate_partner_vals(self, book):
|
||||||
|
country_id = self.env['res.country'].search([
|
||||||
|
('code', '=', str(book['customer_country']))
|
||||||
|
], limit=1)
|
||||||
|
# lang = self.env['res.lang'].search([('code', '=', book['customer_language_iso'])], limit=1)
|
||||||
|
return {
|
||||||
|
'name': "%s, %s" % (book['customer_surname'], book['customer_name']),
|
||||||
|
'country_id': country_id and country_id.id,
|
||||||
|
'city': book['customer_city'],
|
||||||
|
'phone': book['customer_phone'],
|
||||||
|
'zip': book['customer_zip'],
|
||||||
|
'street': book['customer_address'],
|
||||||
|
'email': book['customer_mail'],
|
||||||
|
'unconfirmed': True,
|
||||||
|
# 'lang': lang and lang.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_book_dates(self, book):
|
||||||
|
tz_hotel = self.env['ir.default'].sudo().get('res.config.settings', 'tz_hotel')
|
||||||
|
default_arrival_hour = self.env['ir.default'].sudo().get(
|
||||||
|
'res.config.settings', 'default_arrival_hour')
|
||||||
|
default_departure_hour = self.env['ir.default'].sudo().get(
|
||||||
|
'res.config.settings', 'default_departure_hour')
|
||||||
|
|
||||||
|
# Get dates for the reservation (GMT->UTC)
|
||||||
|
arr_hour = default_arrival_hour if book['arrival_hour'] == "--" \
|
||||||
|
else book['arrival_hour']
|
||||||
|
# HOT-FIX: Wubook 24:00 hour
|
||||||
|
arr_hour_s = arr_hour.split(':')
|
||||||
|
if arr_hour_s[0] == '24':
|
||||||
|
arr_hour_s[0] = '00'
|
||||||
|
arr_hour = ':'.join(arr_hour_s)
|
||||||
|
checkin = "%s %s" % (book['date_arrival'], arr_hour)
|
||||||
|
checkin_dt = datetime.strptime(checkin, DEFAULT_WUBOOK_DATETIME_FORMAT).replace(
|
||||||
|
tzinfo=tz.gettz(str(tz_hotel)))
|
||||||
|
checkin_utc_dt = checkin_dt.astimezone(tz.gettz('UTC'))
|
||||||
|
#checkin = checkin_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
|
||||||
|
checkout = "%s %s" % (book['date_departure'],
|
||||||
|
default_departure_hour)
|
||||||
|
checkout_dt = datetime.strptime(checkout, DEFAULT_WUBOOK_DATETIME_FORMAT).replace(
|
||||||
|
tzinfo=tz.gettz(str(tz_hotel)))
|
||||||
|
checkout_utc_dt = checkout_dt.astimezone(tz.gettz('UTC'))
|
||||||
|
#checkout = checkout_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
||||||
|
|
||||||
|
return (checkin_utc_dt, checkout_utc_dt)
|
||||||
|
|
||||||
|
def _update_reservation_binding(self, binding, book):
|
||||||
|
is_cancellation = book['status'] in WUBOOK_STATUS_BAD
|
||||||
|
binding.with_context({'connector_no_export': True}).write({
|
||||||
|
'channel_raw_data': json.dumps(book),
|
||||||
|
'channel_status': str(book['status']),
|
||||||
|
'channel_status_reason': book.get('status_reason', ''),
|
||||||
|
'to_read': True,
|
||||||
|
'to_assign': True,
|
||||||
|
'price_unit': book['amount'],
|
||||||
|
'customer_notes': book['customer_notes'],
|
||||||
|
})
|
||||||
|
if binding.partner_id.unconfirmed:
|
||||||
|
binding.partner_id.write(
|
||||||
|
self._generate_partner_vals(book)
|
||||||
|
)
|
||||||
|
if is_cancellation:
|
||||||
|
binding.with_context({
|
||||||
|
'connector_no_export': True}).action_cancel()
|
||||||
|
elif binding.state == 'cancelled':
|
||||||
|
binding.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
}).write({
|
||||||
|
'discount': 0.0,
|
||||||
|
'state': 'confirm',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: Super big method!!! O_o
|
||||||
|
@api.model
|
||||||
|
def _generate_reservations(self, bookings):
|
||||||
|
_logger.info("==[CHANNEL->ODOO]==== READING BOOKING ==")
|
||||||
|
_logger.info(bookings)
|
||||||
|
|
||||||
|
# Get user timezone
|
||||||
|
res_partner_obj = self.env['res.partner']
|
||||||
|
channel_reserv_obj = self.env['channel.hotel.reservation']
|
||||||
|
hotel_folio_obj = self.env['hotel.folio']
|
||||||
|
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||||
|
# Space for store some data for construct folios
|
||||||
|
processed_rids = []
|
||||||
|
failed_reservations = []
|
||||||
|
checkin_utc_dt = False
|
||||||
|
checkout_utc_dt = False
|
||||||
|
split_booking = False
|
||||||
|
for book in bookings: # This create a new folio
|
||||||
|
splitted_map = {}
|
||||||
|
rcode = str(book['reservation_code'])
|
||||||
|
crcode = str(book['channel_reservation_code']) \
|
||||||
|
if book['channel_reservation_code'] else 'undefined'
|
||||||
|
|
||||||
|
# Can't process failed reservations
|
||||||
|
# (for example set a invalid new reservation and receive in
|
||||||
|
# the same transaction an cancellation)
|
||||||
|
if crcode in failed_reservations:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_emssage="Can't process a reservation that previusly failed!",
|
||||||
|
channel_object_id=book['reservation_code'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
checkin_utc_dt, checkout_utc_dt = self._get_book_dates(book)
|
||||||
|
|
||||||
|
# Search Folio. If exists.
|
||||||
|
folio_id = False
|
||||||
|
if crcode != 'undefined':
|
||||||
|
reserv_bind = channel_reserv_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('ota_reservation_id', '=', crcode),
|
||||||
|
], limit=1)
|
||||||
|
if reserv_bind:
|
||||||
|
folio_id = reserv_bind.folio_id
|
||||||
|
else:
|
||||||
|
reserv_bind = channel_reserv_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', rcode),
|
||||||
|
], limit=1)
|
||||||
|
if reserv_bind:
|
||||||
|
folio_id = reserv_bind.folio_id
|
||||||
|
|
||||||
|
# Need update reservations?
|
||||||
|
reservs_processed = False
|
||||||
|
reservs_binds = channel_reserv_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', rcode),
|
||||||
|
])
|
||||||
|
for reserv_bind in reservs_binds:
|
||||||
|
self._update_reservation_binding(reserv_bind, book)
|
||||||
|
reservs_processed = True
|
||||||
|
# Do Nothing if already processed 'external_id'
|
||||||
|
if reservs_processed:
|
||||||
|
processed_rids.append(rcode)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Search Customer
|
||||||
|
customer_mail = book.get('customer_mail', False)
|
||||||
|
partner_id = False
|
||||||
|
if customer_mail:
|
||||||
|
partner_id = res_partner_obj.search([
|
||||||
|
('email', '=', customer_mail)
|
||||||
|
], limit=1)
|
||||||
|
if not partner_id:
|
||||||
|
partner_id = res_partner_obj.create(self._generate_partner_vals(book))
|
||||||
|
|
||||||
|
reservations = []
|
||||||
|
used_rooms = []
|
||||||
|
# Iterate booked rooms
|
||||||
|
for broom in book['booked_rooms']:
|
||||||
|
room_type_bind = channel_room_type_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', broom['room_id'])
|
||||||
|
], limit=1)
|
||||||
|
if not room_type_bind:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message="Can't found any room type associated to '%s' \
|
||||||
|
in this hotel" % book['rooms'],
|
||||||
|
channel_object_id=book['reservation_code'])
|
||||||
|
failed_reservations.append(crcode)
|
||||||
|
continue
|
||||||
|
if not any(room_type_bind.room_ids):
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message="Selected room type (%s) doesn't have any \
|
||||||
|
real room" % book['rooms'],
|
||||||
|
channel_object_id=book['reservation_code'])
|
||||||
|
failed_reservations.append(crcode)
|
||||||
|
continue
|
||||||
|
|
||||||
|
dates_checkin = [checkin_utc_dt, False]
|
||||||
|
dates_checkout = [checkout_utc_dt, False]
|
||||||
|
split_booking = False
|
||||||
|
split_booking_parent = False
|
||||||
|
# This perhaps create splitted reservation
|
||||||
|
while dates_checkin[0]:
|
||||||
|
vals = self._generate_booking_vals(
|
||||||
|
broom,
|
||||||
|
crcode,
|
||||||
|
rcode,
|
||||||
|
room_type_bind,
|
||||||
|
split_booking,
|
||||||
|
dates_checkin,
|
||||||
|
dates_checkout,
|
||||||
|
book,
|
||||||
|
)
|
||||||
|
if vals['price_unit'] != book['amount']:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message="Invalid reservation total price! %.2f (calculated) != %.2f (wubook)" % (vals['price_unit'], book['amount']),
|
||||||
|
channel_object_id=book['reservation_code'])
|
||||||
|
|
||||||
|
free_rooms = room_type_bind.odoo_id.check_availability_room_type(
|
||||||
|
vals['checkin'],
|
||||||
|
vals['checkout'],
|
||||||
|
room_type_id=room_type_bind.odoo_id.id,
|
||||||
|
notthis=used_rooms)
|
||||||
|
if any(free_rooms):
|
||||||
|
vals.update({
|
||||||
|
'product_id': room_type_bind.product_id.id,
|
||||||
|
'name': free_rooms[0].name,
|
||||||
|
})
|
||||||
|
reservations.append((0, False, vals))
|
||||||
|
used_rooms.append(free_rooms[0].id)
|
||||||
|
|
||||||
|
if split_booking:
|
||||||
|
if not split_booking_parent:
|
||||||
|
split_booking_parent = len(reservations)
|
||||||
|
else:
|
||||||
|
splitted_map.setdefault(
|
||||||
|
split_booking_parent,
|
||||||
|
[]).append(len(reservations))
|
||||||
|
dates_checkin = [dates_checkin[1], False]
|
||||||
|
dates_checkout = [dates_checkout[1], False]
|
||||||
|
else:
|
||||||
|
date_diff = (dates_checkout[0].replace(
|
||||||
|
hour=0, minute=0, second=0,
|
||||||
|
microsecond=0) -
|
||||||
|
dates_checkin[0].replace(
|
||||||
|
hour=0, minute=0, second=0,
|
||||||
|
microsecond=0)).days
|
||||||
|
if date_diff <= 0:
|
||||||
|
if split_booking:
|
||||||
|
if split_booking_parent:
|
||||||
|
del reservations[split_booking_parent-1:]
|
||||||
|
if split_booking_parent in splitted_map:
|
||||||
|
del splitted_map[split_booking_parent]
|
||||||
|
# Can't found space for reservation: Overbooking
|
||||||
|
vals = self._generate_booking_vals(
|
||||||
|
broom,
|
||||||
|
crcode,
|
||||||
|
rcode,
|
||||||
|
room_type_bind,
|
||||||
|
False,
|
||||||
|
(checkin_utc_dt, False),
|
||||||
|
(checkout_utc_dt, False),
|
||||||
|
book,
|
||||||
|
)
|
||||||
|
vals.update({
|
||||||
|
'product_id': room_type_bind.product_id.id,
|
||||||
|
'name': room_type_bind.name,
|
||||||
|
'overbooking': True,
|
||||||
|
})
|
||||||
|
reservations.append((0, False, vals))
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message="Reservation imported with overbooking state",
|
||||||
|
channel_object_id=rcode,
|
||||||
|
dfrom=vals['checkin'], dto=vals['checkout'])
|
||||||
|
dates_checkin = [False, False]
|
||||||
|
dates_checkout = [False, False]
|
||||||
|
split_booking = False
|
||||||
|
else:
|
||||||
|
split_booking = True
|
||||||
|
dates_checkin = [
|
||||||
|
dates_checkin[0],
|
||||||
|
dates_checkin[0] + timedelta(days=date_diff-1)
|
||||||
|
]
|
||||||
|
dates_checkout = [
|
||||||
|
dates_checkout[0] - timedelta(days=1),
|
||||||
|
checkout_utc_dt
|
||||||
|
]
|
||||||
|
|
||||||
|
# Create Splitted Issue Information
|
||||||
|
if split_booking:
|
||||||
|
self.create_issue(
|
||||||
|
section='reservation',
|
||||||
|
internal_message="Reservation Splitted",
|
||||||
|
channel_object_id=rcode)
|
||||||
|
|
||||||
|
# Create Folio
|
||||||
|
if not any(failed_reservations) and any(reservations):
|
||||||
|
# TODO: Improve 'addons_list' & discounts
|
||||||
|
addons = str(book['addons_list']) if any(book['addons_list']) else ''
|
||||||
|
discounts = book.get('discount', '')
|
||||||
|
vals = {
|
||||||
|
'room_lines': reservations,
|
||||||
|
'customer_notes': "%s\nADDONS:\n%s\nDISCOUNT:\n%s" % (
|
||||||
|
book['customer_notes'], addons, discounts),
|
||||||
|
'channel_type': 'web',
|
||||||
|
}
|
||||||
|
_logger.info("==[CHANNEL->ODOO]==== CREATING/UPDATING FOLIO ==")
|
||||||
|
_logger.info(reservations)
|
||||||
|
if folio_id:
|
||||||
|
folio_id.with_context({
|
||||||
|
'connector_no_export': True}).write(vals)
|
||||||
|
else:
|
||||||
|
vals.update({
|
||||||
|
'partner_id': partner_id.id,
|
||||||
|
'wseed': book['sessionSeed']
|
||||||
|
})
|
||||||
|
folio_id = hotel_folio_obj.with_context({
|
||||||
|
'connector_no_export': True}).create(vals)
|
||||||
|
|
||||||
|
# Update Reservation Spitted Parents
|
||||||
|
sorted_rlines = folio_id.room_lines.sorted(key='id')
|
||||||
|
for k_pid, v_pid in splitted_map.items():
|
||||||
|
preserv = sorted_rlines[k_pid-1]
|
||||||
|
for pid in v_pid:
|
||||||
|
creserv = sorted_rlines[pid-1]
|
||||||
|
creserv.parent_reservation = preserv.id
|
||||||
|
|
||||||
|
# Bind reservations
|
||||||
|
rlines = sorted_rlines = folio_id.room_lines
|
||||||
|
for rline in rlines:
|
||||||
|
for rline_bind in rline.channel_bind_ids:
|
||||||
|
self.binder(rline_bind.external_id, rline_bind)
|
||||||
|
|
||||||
|
processed_rids.append(rcode)
|
||||||
|
return (processed_rids, any(failed_reservations),
|
||||||
|
checkin_utc_dt, checkout_utc_dt)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 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 importer
|
||||||
|
from . import exporter
|
||||||
|
from . import deleter
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# 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 HotelRoomTypeAdapter(Component):
|
||||||
|
_name = 'channel.hotel.room.type.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.hotel.room.type'
|
||||||
|
|
||||||
|
def create_room(self, shortcode, name, capacity, price, availability):
|
||||||
|
return super(HotelRoomTypeAdapter, self).create_room(
|
||||||
|
shortcode, name, capacity, price, availability)
|
||||||
|
|
||||||
|
def fetch_rooms(self):
|
||||||
|
return super(HotelRoomTypeAdapter, self).fetch_rooms()
|
||||||
|
|
||||||
|
def modify_room(self, channel_room_id, name, capacity, price, availability, scode):
|
||||||
|
return super(HotelRoomTypeAdapter, self).modify_room(
|
||||||
|
channel_room_id, name, capacity, price, availability, scode)
|
||||||
|
|
||||||
|
def delete_room(self, channel_room_id):
|
||||||
|
return super(HotelRoomTypeAdapter, self).delete_room(channel_room_id)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeDeleter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.deleter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def delete_room(self, binding):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.delete_room(binding.external_id)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='room',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeExporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.exporter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def modify_room(self, binding):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.modify_room(
|
||||||
|
binding.external_id,
|
||||||
|
binding.name,
|
||||||
|
binding.ota_capacity,
|
||||||
|
binding.list_price,
|
||||||
|
binding.total_rooms_count,
|
||||||
|
binding.channel_short_code)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='room',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create_room(self, binding):
|
||||||
|
seq_obj = self.env['ir.sequence']
|
||||||
|
short_code = seq_obj.next_by_code('hotel.room.type')[:4]
|
||||||
|
try:
|
||||||
|
external_id = self.backend_adapter.create_room(
|
||||||
|
short_code,
|
||||||
|
binding.name,
|
||||||
|
binding.ota_capacity,
|
||||||
|
binding.list_price,
|
||||||
|
binding.total_rooms_count
|
||||||
|
)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='room',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
binding.write({
|
||||||
|
'external_id': external_id,
|
||||||
|
'channel_short_code': short_code,
|
||||||
|
})
|
||||||
|
self.binder.bind(external_id, binding)
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.connector.components.mapper import mapping
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeImporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.importer'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_rooms(self):
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.fetch_rooms()
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='room',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||||
|
room_mapper = self.component(usage='import.mapper',
|
||||||
|
model_name='channel.hotel.room.type')
|
||||||
|
for room in results:
|
||||||
|
map_record = room_mapper.map_record(room)
|
||||||
|
room_bind = channel_room_type_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', room['id']),
|
||||||
|
], limit=1)
|
||||||
|
if room_bind:
|
||||||
|
room_bind.with_context({'connector_no_export':True}).write(map_record.values())
|
||||||
|
else:
|
||||||
|
room_bind = channel_room_type_obj.with_context({
|
||||||
|
'connector_no_export':True}).create(
|
||||||
|
map_record.values(for_create=True))
|
||||||
|
self.binder.bind(room['id'], room_bind)
|
||||||
|
count = count + 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeImportMapper(Component):
|
||||||
|
_name = 'channel.hotel.room.type.import.mapper'
|
||||||
|
_inherit = 'channel.import.mapper'
|
||||||
|
_apply_on = 'channel.hotel.room.type'
|
||||||
|
|
||||||
|
direct = [
|
||||||
|
('id', 'external_id'),
|
||||||
|
('shortname', 'channel_short_code'),
|
||||||
|
('occupancy', 'ota_capacity'),
|
||||||
|
('price', 'list_price'),
|
||||||
|
('name', 'name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@mapping
|
||||||
|
def backend_id(self, record):
|
||||||
|
return {'backend_id': self.backend_record.id}
|
||||||
@@ -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 importer
|
||||||
|
from . import exporter
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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 HotelRoomTypeAvailabilityAdapter(Component):
|
||||||
|
_name = 'channel.hotel.room.type.availability.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.hotel.room.type.availability'
|
||||||
|
|
||||||
|
def fetch_rooms_values(self, date_from, date_to, rooms=False):
|
||||||
|
return super(HotelRoomTypeAvailabilityAdapter, self).fetch_rooms_values(
|
||||||
|
date_from,
|
||||||
|
date_to,
|
||||||
|
rooms)
|
||||||
|
|
||||||
|
def update_availability(self, rooms_avail):
|
||||||
|
return super(HotelRoomTypeAvailabilityAdapter, self).update_availability(
|
||||||
|
rooms_avail)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# 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.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo.addons.hotel_channel_connector_wubook.components.backend_adapter import (
|
||||||
|
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||||
|
from odoo import api, _, fields
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeAvailabilityExporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.availability.exporter'
|
||||||
|
|
||||||
|
def push_availability(self):
|
||||||
|
channel_room_type_avail_ids = self.env['channel.hotel.room.type.availability'].search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('channel_pushed', '=', False),
|
||||||
|
('date', '>=', fields.Date.today())
|
||||||
|
])
|
||||||
|
room_types = channel_room_type_avail_ids.mapped('room_type_id')
|
||||||
|
avails = []
|
||||||
|
for room_type in room_types:
|
||||||
|
if any(room_type.channel_bind_ids):
|
||||||
|
channel_room_type_avails = channel_room_type_avail_ids.filtered(
|
||||||
|
lambda x: x.room_type_id.id == room_type.id)
|
||||||
|
days = []
|
||||||
|
for channel_room_type_avail in channel_room_type_avails:
|
||||||
|
cavail = channel_room_type_avail.avail
|
||||||
|
if channel_room_type_avail.channel_max_avail >= 0 and \
|
||||||
|
cavail > channel_room_type_avail.channel_max_avail:
|
||||||
|
cavail = channel_room_type_avail.channel_max_avail
|
||||||
|
date_dt = fields.Date.from_string(channel_room_type_avail.date)
|
||||||
|
days.append({
|
||||||
|
'date': date_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT),
|
||||||
|
'avail': cavail,
|
||||||
|
'no_ota': channel_room_type_avail.no_ota and 1 or 0,
|
||||||
|
# 'booked': room_type_avail.booked and 1 or 0,
|
||||||
|
})
|
||||||
|
avails.append({'id': room_type.channel_bind_ids[0].channel_room_id, 'days': days})
|
||||||
|
_logger.info("==[ODOO->CHANNEL]==== AVAILABILITY ==")
|
||||||
|
_logger.info(avails)
|
||||||
|
if any(avails):
|
||||||
|
try:
|
||||||
|
self.backend_adapter.update_availability(avails)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='avail',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
channel_room_type_avails.write({'channel_pushed': True})
|
||||||
|
return True
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
from odoo.addons.component.core import Component
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo.addons.connector.components.mapper import mapping, only_create
|
||||||
|
from odoo import api, _, fields
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeAvailabilityImporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.availability.importer'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def import_availability_values(self, date_from, date_to):
|
||||||
|
now_dt = date.today()
|
||||||
|
dfrom_dt = fields.Date.from_string(date_from)
|
||||||
|
dto_dt = fields.Date.from_string(date_to)
|
||||||
|
if dfrom_dt < now_dt:
|
||||||
|
dfrom_dt = now_dt
|
||||||
|
if dfrom_dt > dto_dt:
|
||||||
|
dfrom_dt, dto_dt = dto_dt, dfrom_dt
|
||||||
|
if dto_dt < now_dt:
|
||||||
|
return True
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.fetch_rooms_values(date_from, date_to)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='avail',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'],
|
||||||
|
dfrom=date_from, dto=date_to)
|
||||||
|
else:
|
||||||
|
_logger.info("==[CHANNEL->ODOO]==== AVAILABILITY (%s - %s) ==",
|
||||||
|
date_from, date_to)
|
||||||
|
_logger.info(results)
|
||||||
|
|
||||||
|
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||||
|
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||||
|
room_avail_mapper = self.component(
|
||||||
|
usage='import.mapper',
|
||||||
|
model_name='channel.hotel.room.type.availability')
|
||||||
|
for room_k, room_v in results.items():
|
||||||
|
iter_day = dfrom_dt
|
||||||
|
channel_room_type = channel_room_type_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', room_k)
|
||||||
|
], limit=1)
|
||||||
|
if channel_room_type:
|
||||||
|
for room in room_v:
|
||||||
|
room.update({
|
||||||
|
'room_type_id': channel_room_type.odoo_id.id,
|
||||||
|
'date': fields.Date.to_string(iter_day),
|
||||||
|
})
|
||||||
|
map_record = room_avail_mapper.map_record(room)
|
||||||
|
room_type_avail_bind = channel_room_type_avail_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('room_type_id', '=', room['room_type_id']),
|
||||||
|
('date', '=', room['date'])
|
||||||
|
], limit=1)
|
||||||
|
if room_type_avail_bind:
|
||||||
|
room_type_avail_bind.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
}).write(map_record.values())
|
||||||
|
else:
|
||||||
|
room_type_avail_bind = channel_room_type_avail_obj.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
}).create(map_record.values(for_create=True))
|
||||||
|
room_type_avail_bind.channel_pushed = True
|
||||||
|
iter_day += timedelta(days=1)
|
||||||
|
count = count + 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeAvailabilityImportMapper(Component):
|
||||||
|
_name = 'channel.hotel.room.type.availability.import.mapper'
|
||||||
|
_inherit = 'channel.import.mapper'
|
||||||
|
_apply_on = 'channel.hotel.room.type.availability'
|
||||||
|
|
||||||
|
direct = [
|
||||||
|
('no_ota', 'no_ota'),
|
||||||
|
('booked', 'booked'),
|
||||||
|
('avail', 'avail'),
|
||||||
|
('date', 'date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@only_create
|
||||||
|
@mapping
|
||||||
|
def channel_pushed(self, record):
|
||||||
|
return {'channel_pushed': True}
|
||||||
|
|
||||||
|
@mapping
|
||||||
|
def backend_id(self, record):
|
||||||
|
return {'backend_id': self.backend_record.id}
|
||||||
|
|
||||||
|
@mapping
|
||||||
|
def room_type_id(self, record):
|
||||||
|
return {'room_type_id': record['room_type_id']}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# 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 importer
|
||||||
|
from . import exporter
|
||||||
|
from . import deleter
|
||||||
@@ -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.addons.component.core import Component
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionAdapter(Component):
|
||||||
|
_name = 'channel.hotel.room.type.restriction.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.hotel.room.type.restriction'
|
||||||
|
|
||||||
|
def rplan_rplans(self):
|
||||||
|
return super(HotelRoomTypeRestrictionAdapter, self).rplan_rplans()
|
||||||
|
|
||||||
|
def create_rplan(self, name):
|
||||||
|
return super(HotelRoomTypeRestrictionAdapter, self).create_rplan(name)
|
||||||
|
|
||||||
|
def delete_rplan(self, external_id):
|
||||||
|
return super(HotelRoomTypeRestrictionAdapter, self).delete_rplan(external_id)
|
||||||
|
|
||||||
|
def rename_rplan(self, external_id, new_name):
|
||||||
|
return super(HotelRoomTypeRestrictionAdapter, self).rename_rplan(external_id, new_name)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionDeleter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.restriction.deleter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def delete_rplan(self, binding):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.delete_rplan(binding.external_id)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='restriction',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionExporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.restriction.exporter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def rename_rplan(self, binding):
|
||||||
|
try:
|
||||||
|
return self.backend_adapter.rename_rplan(
|
||||||
|
binding.external_id,
|
||||||
|
binding.name)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='restriction',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create_rplan(self, binding):
|
||||||
|
try:
|
||||||
|
external_id = self.backend_adapter.create_rplan(binding.name)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='restriction',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
binding.external_id = external_id
|
||||||
|
self.binder.bind(external_id, binding)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# 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
|
||||||
|
from odoo.addons.connector.components.mapper import mapping
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo import api
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionImporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.restriction.importer'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def import_restriction_plans(self):
|
||||||
|
count = 0
|
||||||
|
try:
|
||||||
|
results = self.backend_adapter.rplan_rplans()
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='restriction',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
channel_restriction_obj = self.env['channel.hotel.room.type.restriction']
|
||||||
|
restriction_mapper = self.component(usage='import.mapper',
|
||||||
|
model_name='channel.hotel.room.type.restriction')
|
||||||
|
for plan in results:
|
||||||
|
plan_record = restriction_mapper.map_record(plan)
|
||||||
|
plan_bind = channel_restriction_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('external_id', '=', str(plan['id'])),
|
||||||
|
], limit=1)
|
||||||
|
if not plan_bind:
|
||||||
|
channel_restriction_obj.with_context({
|
||||||
|
'connector_no_export': True,
|
||||||
|
'rules': plan.get('rules'),
|
||||||
|
}).create(plan_record.values(for_create=True))
|
||||||
|
else:
|
||||||
|
plan_bind.with_context({'connector_no_export':True}).write(
|
||||||
|
plan_record.values())
|
||||||
|
count = count + 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionImportMapper(Component):
|
||||||
|
_name = 'channel.hotel.room.type.restriction.import.mapper'
|
||||||
|
_inherit = 'channel.import.mapper'
|
||||||
|
_apply_on = 'channel.hotel.room.type.restriction'
|
||||||
|
|
||||||
|
direct = [
|
||||||
|
('name', 'name'),
|
||||||
|
('id', 'external_id'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@mapping
|
||||||
|
def backend_id(self, record):
|
||||||
|
return {'backend_id': self.backend_record.id}
|
||||||
@@ -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 importer
|
||||||
|
from . import exporter
|
||||||
@@ -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 Component
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionItemAdapter(Component):
|
||||||
|
_name = 'channel.hotel.room.type.restriction.item.adapter'
|
||||||
|
_inherit = 'wubook.adapter'
|
||||||
|
_apply_on = 'channel.hotel.room.type.restriction.item'
|
||||||
|
|
||||||
|
def wired_rplan_get_rplan_values(self, date_from, date_to, channel_restriction_plan_id):
|
||||||
|
return super(HotelRoomTypeRestrictionItemAdapter, self).wired_rplan_get_rplan_values(
|
||||||
|
date_from,
|
||||||
|
date_to,
|
||||||
|
channel_restriction_plan_id)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from odoo.addons.component.core import Component
|
||||||
|
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||||
|
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||||
|
from odoo import api,fields
|
||||||
|
|
||||||
|
|
||||||
|
class HotelRoomTypeRestrictionItemExporter(Component):
|
||||||
|
_inherit = 'channel.hotel.room.type.restriction.item.exporter'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def push_restriction(self):
|
||||||
|
channel_room_type_rest_obj = self.env['channel.hotel.room.type.restriction']
|
||||||
|
channel_rest_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
||||||
|
unpushed = channel_rest_item_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('channel_pushed', '=', False),
|
||||||
|
('date', '>=', fields.Date.today())
|
||||||
|
], order="date ASC")
|
||||||
|
if any(unpushed):
|
||||||
|
date_start = fields.Date.from_string(unpushed[0].date)
|
||||||
|
date_end = fields.Date.from_string(unpushed[-1].date)
|
||||||
|
days_diff = (date_end-date_start).days + 1
|
||||||
|
restrictions = {}
|
||||||
|
channel_restr_plan_ids = channel_room_type_rest_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
])
|
||||||
|
for rp in channel_restr_plan_ids:
|
||||||
|
restrictions.update({rp.external_id: {}})
|
||||||
|
unpushed_rp = channel_rest_item_obj.search([
|
||||||
|
('backend_id', '=', self.backend_record.id),
|
||||||
|
('channel_pushed', '=', False),
|
||||||
|
('restriction_id', '=', rp.odoo_id.id)
|
||||||
|
])
|
||||||
|
room_type_ids = unpushed_rp.mapped('room_type_id')
|
||||||
|
for room_type in room_type_ids:
|
||||||
|
if any(room_type.channel_bind_ids):
|
||||||
|
# FIXME: Supossed that only exists one channel connector per record
|
||||||
|
room_type_external_id = room_type.channel_bind_ids[0].external_id
|
||||||
|
restrictions[rp.external_id].update({
|
||||||
|
room_type_external_id: [],
|
||||||
|
})
|
||||||
|
for i in range(0, days_diff):
|
||||||
|
ndate_dt = date_start + timedelta(days=i)
|
||||||
|
restr = room_type.get_restrictions(
|
||||||
|
ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
|
rp.odoo_id.id)
|
||||||
|
if restr:
|
||||||
|
restrictions[rp.external_id][room_type_external_id].append({
|
||||||
|
'min_stay': restr.min_stay or 0,
|
||||||
|
'min_stay_arrival': restr.min_stay_arrival or 0,
|
||||||
|
'max_stay': restr.max_stay or 0,
|
||||||
|
'max_stay_arrival': restr.max_stay_arrival or 0,
|
||||||
|
'closed': restr.closed and 1 or 0,
|
||||||
|
'closed_arrival': restr.closed_arrival and 1 or 0,
|
||||||
|
'closed_departure': restr.closed_departure and 1 or 0,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
restrictions[rp.external_id][room_type_external_id].append({})
|
||||||
|
_logger.info("==[ODOO->CHANNEL]==== RESTRICTIONS ==")
|
||||||
|
_logger.info(restrictions)
|
||||||
|
try:
|
||||||
|
for k_res, v_res in restrictions.items():
|
||||||
|
if any(v_res):
|
||||||
|
self.backend_adapter.update_rplan_values(
|
||||||
|
int(k_res),
|
||||||
|
date_start.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||||
|
v_res)
|
||||||
|
except ChannelConnectorError as err:
|
||||||
|
self.create_issue(
|
||||||
|
section='restriction',
|
||||||
|
internal_message=str(err),
|
||||||
|
channel_message=err.data['message'])
|
||||||
|
else:
|
||||||
|
unpushed.write({'channel_pushed': True})
|
||||||
|
return True
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user