Finalize Hotel Channel Connector (#64)

* [FIX] Export pricelist

* [IMP] Import Reservations

* [IMP] Better exception handle

* [IMP] Notifiactions

* [FIX] Issues [IMP] Notifications

* [FIX] Cron Jobs
This commit is contained in:
QS5ELkMu
2018-11-14 23:32:58 +01:00
committed by GitHub
parent 8622ff1948
commit 77cddb44f1
47 changed files with 1178 additions and 1863 deletions

View File

@@ -11,6 +11,7 @@
'description': "Hotel Channel Connector Base", 'description': "Hotel Channel Connector Base",
'depends': [ 'depends': [
'connector', 'connector',
'web_notify',
'hotel', 'hotel',
], ],
'external_dependencies': { 'external_dependencies': {
@@ -18,7 +19,6 @@
}, },
'data': [ 'data': [
'data/cron_jobs.xml', 'data/cron_jobs.xml',
'views/general.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',
@@ -29,7 +29,6 @@
'views/inherited_hotel_room_type_restriction_views.xml', 'views/inherited_hotel_room_type_restriction_views.xml',
'views/inherited_hotel_room_type_restriction_item_views.xml', 'views/inherited_hotel_room_type_restriction_item_views.xml',
'views/inherited_res_partner_views.xml', 'views/inherited_res_partner_views.xml',
'views/channel_ota_info_views.xml',
'views/channel_hotel_reservation_views.xml', 'views/channel_hotel_reservation_views.xml',
'views/channel_hotel_room_type_views.xml', 'views/channel_hotel_room_type_views.xml',
'views/channel_hotel_room_type_availability_views.xml', 'views/channel_hotel_room_type_availability_views.xml',
@@ -38,6 +37,7 @@
'views/channel_product_pricelist_views.xml', 'views/channel_product_pricelist_views.xml',
'views/channel_product_pricelist_item_views.xml', 'views/channel_product_pricelist_item_views.xml',
'views/channel_connector_backend_views.xml', 'views/channel_connector_backend_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', 'data/sequences.xml',

View File

@@ -6,4 +6,5 @@ from . import backend_adapter
from . import binder from . import binder
from . import importer from . import importer
from . import exporter from . import exporter
from . import deleter
from . import mapper from . import mapper

View File

@@ -361,7 +361,7 @@ class WuBookAdapter(AbstractComponent):
return results return results
def fetch_booking(self, channel_reservation_id): def fetch_booking(self, channel_reservation_id):
rcode, results = self.backend_adapter.fetch_booking( rcode, results = self._server.fetch_booking(
self._session_info[0], self._session_info[0],
self._session_info[1], self._session_info[1],
channel_reservation_id) channel_reservation_id)
@@ -372,10 +372,6 @@ class WuBookAdapter(AbstractComponent):
return results return results
def mark_bookings(self, channel_reservation_ids): def mark_bookings(self, channel_reservation_ids):
init_connection = self._context.get('init_connection', True)
if init_connection:
if not self.init_connection():
return False
rcode, results = self._server.mark_bookings( rcode, results = self._server.mark_bookings(
self._session_info[0], self._session_info[0],
self._session_info[1], self._session_info[1],

View File

@@ -9,6 +9,18 @@ class BaseHotelChannelConnectorComponent(AbstractComponent):
_inherit = 'base.connector' _inherit = 'base.connector'
_collection = 'channel.backend' _collection = 'channel.backend'
@api.model
def create_issue(self, **kwargs):
self.env['hotel.channel.connector.issue'].sudo().create({
'backend_id': kwargs.get('backend', self.backend_record.id),
'section': kwargs.get('section', False),
'internal_message': kwargs.get('internal_message', False),
'channel_object_id': kwargs.get('channel_object_id', False),
'channel_message': kwargs.get('channel_message', False),
'date_start': kwargs.get('dfrom', False),
'date_end': kwargs.get('dto', False),
})
class ChannelConnectorError(Exception): class ChannelConnectorError(Exception):
def __init__(self, message, data): def __init__(self, message, data):
super().__init__(message) super().__init__(message)

View File

@@ -0,0 +1,9 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.addons.component.core import AbstractComponent
class HotelChannelConnectorDeleter(AbstractComponent):
_name = 'hotel.channel.deleter'
_inherit = ['base.deleter', 'base.hotel.channel.connector']
_usage = 'channel.deleter'

View File

@@ -1,784 +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 AbstractComponent
import pytz
import json
from datetime import timedelta
from odoo.exceptions import ValidationError
from odoo.addons.component.core import AbstractComponent, Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import _
from odoo.tools import (
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_DATETIME_FORMAT)
from .backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT,
DEFAULT_WUBOOK_DATETIME_FORMAT,
WUBOOK_STATUS_BAD)
from odoo import api
_logger = logging.getLogger(__name__)
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']
_usage = 'channel.importer' _usage = 'channel.importer'
@api.model
def _get_room_values_availability(self, room_type_id, date_str, day_vals, set_max_avail):
room_type_avail_obj = self.env['hotel.room.type.availability']
room_type_avail = room_type_avail_obj.search([
('room_type_id', '=', room_type_id),
('date', '=', date_str)
], limit=1)
vals = {
'no_ota': day_vals.get('no_ota'),
'booked': day_vals.get('booked'),
'avail': day_vals.get('avail', 0),
'wpushed': True,
}
if set_max_avail:
vals.update({'max_avail': day_vals.get('avail', 0)})
if room_type_avail:
room_type_avail.with_context({
'wubook_action': False,
}).write(vals)
else:
vals.update({
'room_type_id': room_type_id,
'date': date_str,
})
room_type_avail_obj.with_context({
'wubook_action': False,
'mail_create_nosubscribe': True,
}).create(vals)
@api.model
def _get_room_values_restrictions(self, restriction_plan_id, room_type_id, date_str, day_vals):
room_type_restr_item_obj = self.env['hotel.room.type.restriction.item']
room_type_restr = room_type_restr_item_obj.search([
('room_type_id', '=', room_type_id),
('applied_on', '=', '0_room_type'),
('date_start', '=', date_str),
('date_end', '=', date_str),
('restriction_id', '=', restriction_plan_id),
])
vals = {
'min_stay': int(day_vals.get('min_stay', 0)),
'min_stay_arrival': int(day_vals.get(
'min_stay_arrival',
0)),
'max_stay': int(day_vals.get('max_stay', 0)),
'max_stay_arrival': int(day_vals.get(
'max_stay_arrival',
0)),
'closed': int(day_vals.get('closed', False)),
'closed_departure': int(day_vals.get(
'closed_departure',
False)),
'closed_arrival': int(day_vals.get(
'closed_arrival',
False)),
'wpushed': True,
}
if room_type_restr:
room_type_restr.with_context({
'wubook_action': False,
}).write(vals)
else:
vals.update({
'restriction_id': restriction_plan_id,
'room_type_id': room_type_id,
'date_start': date_str,
'date_end': date_str,
'applied_on': '0_room_type',
})
room_type_restr_item_obj.with_context({
'wubook_action': False,
}).create(vals)
@api.model
def _generate_room_values(self, dfrom, dto, values, set_max_avail=False):
room_type_restr_obj = self.env['hotel.room.type.restriction']
hotel_room_type_obj = self.env['hotel.room.type']
def_restr_plan = room_type_restr_obj.search([('wpid', '=', '0')])
_logger.info("==== ROOM VALUES (%s -- %s)", dfrom, dto)
_logger.info(values)
for k_rid, v_rid in values.iteritems():
room_type = hotel_room_type_obj.search([
('wrid', '=', k_rid)
], limit=1)
if room_type:
date_dt = fields.Date.from_string(dfrom)
for day_vals in v_rid:
date_str = date_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
self._get_room_values_availability(
room_type.id,
date_str,
day_vals,
set_max_avail)
if def_restr_plan:
self._get_room_values_restrictions(
def_restr_plan.id,
room_type.id,
date_str,
day_vals)
date_dt = date_dt + timedelta(days=1)
return True
@api.model
def _generate_booking_vals(self, broom, checkin_str, checkout_str,
is_cancellation, channel_info, wstatus, crcode,
rcode, room_type, split_booking, dates_checkin,
dates_checkout, book):
# Generate Reservation Day Lines
reservation_line_ids = []
tprice = 0.0
for brday in broom['roomdays']:
wndate = fields.Date.from_string(
brday['day']
)
if wndate >= dates_checkin[0] and wndate <= (dates_checkout[0] - timedelta(days=1)):
reservation_line_ids.append((0, False, {
'date': wndate.strftime(
DEFAULT_SERVER_DATE_FORMAT),
'price': brday['price']
}))
tprice += brday['price']
persons = room_type.wcapacity
if 'ancillary' in broom and 'guests' in broom['ancillary']:
persons = broom['ancillary']['guests']
vals = {
'channel_reservation_id': rcode,
'ota_id': channel_info and channel_info.id,
'ota_reservation_id': crcode,
'channel_raw_data': json.dumps(book),
'wstatus': wstatus,
'wmodified': book['was_modified'],
'odoo_id': [0, False, {
'checkin': checkin_str,
'checkout': checkout_str,
'adults': persons,
'children': book['children'],
'reservation_line_ids': reservation_line_ids,
'price_unit': tprice,
'to_assign': True,
'to_read': True,
'state': is_cancellation and 'cancelled' or 'draft',
'room_type_id': room_type.id,
'splitted': split_booking,
}],
}
_logger.info("===== CONTRUCT RESERV")
_logger.info(vals)
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,
}
# FIXME: Super big method!!! O_o
@api.model
def _generate_reservations(self, bookings):
_logger.info("=== BOOKINGS FROM WUBOOK")
_logger.info(bookings)
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 user timezone
tz_hotel = self.env['ir.default'].sudo().get(
'res.config.settings', 'tz_hotel')
res_partner_obj = self.env['res.partner']
channel_reserv_obj = self.env['channel.hotel.reservation']
hotel_reserv_obj = self.env['hotel.reservation']
hotel_folio_obj = self.env['hotel.folio']
channel_room_type_obj = self.env['channel.hotel.room.type']
hotel_room_type_obj = self.env['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 = {}
is_cancellation = book['status'] in WUBOOK_STATUS_BAD
bstatus = str(book['status'])
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
# Get dates for the reservation (GMT->UTC)
arr_hour = default_arrival_hour if book['arrival_hour'] == "--" \
else book['arrival_hour']
checkin = "%s %s" % (book['date_arrival'], arr_hour)
checkin_dt = fields.Date.from_string(checkin)
checkin_utc_dt = date_utils.dt_as_timezone(checkin_dt, 'UTC')
checkin = checkin_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
checkout = "%s %s" % (book['date_departure'],
default_departure_hour)
checkout_dt = fields.Date.from_string(checkout)
checkout_utc_dt = date_utils.dt_as_timezone(checkout_dt, 'UTC')
checkout = checkout_utc_dt.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
# Search Folio. If exists.
folio_id = False
if crcode != 'undefined':
reserv_folio = channel_reserv_obj.search([
('ota_reservation_id', '=', crcode)
], limit=1)
if reserv_folio:
folio_id = reserv_folio.odoo_id.folio_id
else:
reserv_folio = channel_reserv_obj.search([
('channel_reservation_id', '=', rcode)
], limit=1)
if reserv_folio:
folio_id = reserv_folio.odoo_id.folio_id
# Need update reservations?
sreservs = channel_reserv_obj.search([('channel_reservation_id', '=', rcode)])
reservs = folio_id.room_lines if folio_id else sreservs.mapped(lambda x: x.odoo_id)
reservs_processed = False
if any(reservs):
folio_id = reservs[0].folio_id
for reserv in reservs:
if reserv.channel_reservation_id == rcode:
binding_id = reserv.channel_bind_ids[0]
binding_id.write({
'channel_raw_data': json.dumps(book),
'wstatus': str(book['status']),
'wstatus_reason': book.get('status_reason', ''),
})
reserv.with_context({'wubook_action': False}).write({
'to_read': True,
'to_assign': True,
'price_unit': book['amount'],
'customer_notes': book['customer_notes'],
})
if reserv.partner_id.unconfirmed:
reserv.partner_id.write(
self._generate_partner_vals(book)
)
reservs_processed = True
if is_cancellation:
reserv.with_context({
'wubook_action': False}).action_cancel()
elif reserv.state == 'cancelled':
reserv.with_context({
'wubook_action': False,
}).write({
'discount': 0.0,
'state': 'confirm',
})
# Do Nothing if already processed 'wrid'
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))
# Search Wubook Channel Info
channel_info = self.env['hotel.channel.connector.ota.info'].search(
[('ota_id', '=', str(book['id_channel']))], limit=1)
reservations = []
used_rooms = []
# Iterate booked rooms
for broom in book['booked_rooms']:
room_type = channel_room_type_obj.search([
('channel_room_id', '=', broom['room_id'])
], limit=1)
if not room_type:
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
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]:
checkin_str = dates_checkin[0].strftime(
DEFAULT_SERVER_DATETIME_FORMAT)
checkout_str = dates_checkout[0].strftime(
DEFAULT_SERVER_DATETIME_FORMAT)
vals = self._generate_booking_vals(
broom,
checkin_str,
checkout_str,
is_cancellation,
channel_info,
bstatus,
crcode,
rcode,
room_type.odoo_id,
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 != %.2f" % (vals['price_unit'], book['amount']),
channel_object_id=book['reservation_code'])
free_rooms = room_type.odoo_id.check_availability_room_type(
checkin_str,
checkout_str,
room_type_id=room_type.odoo_id.id,
notthis=used_rooms)
if any(free_rooms):
vals.update({
'product_id': free_rooms[0].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
vals = self._generate_booking_vals(
broom,
checkin_utc_dt,
checkout_utc_dt,
is_cancellation,
channel_info,
bstatus,
crcode,
rcode,
room_type.odoo_id,
False,
(checkin_utc_dt, False),
(checkout_utc_dt, False),
book,
)
vals.update({
'product_id':
room_type.odoo_id.room_ids[0].product_id.id,
'name': room_type.odoo_id.name,
'overbooking': True,
})
reservations.append((0, False, vals))
self.create_issue(
section='reservation',
internal_message="Reservation imported with overbooking state",
channel_object_id=rcode)
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
]
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):
try:
# TODO: Improve 'addons_list' & discounts
addons = str(book['addons_list']) if any(book['addons_list']) else ''
discounts = book.get('discount', '')
vals = {
'room_lines': reservations,
'wcustomer_notes': "%s\nADDONS:\n%s\nDISCOUNT:\n%s" % (
book['customer_notes'], addons, discounts),
'channel_type': 'web',
}
_logger.info("=== FOLIO CREATE")
_logger.info(reservations)
if folio_id:
folio_id.with_context({
'wubook_action': False}).write(vals)
else:
vals.update({
'partner_id': partner_id.id,
'wseed': book['sessionSeed']
})
folio_id = hotel_folio_obj.with_context({
'wubook_action': False}).create(vals)
# Update Reservation Spitted Parents
sorted_rlines = folio_id.room_lines.sorted(key='id')
for k_pid, v_pid in splitted_map.iteritems():
preserv = sorted_rlines[k_pid-1]
for pid in v_pid:
creserv = sorted_rlines[pid-1]
creserv.parent_reservation = preserv.id
processed_rids.append(rcode)
except ChannelConnectorError as err:
self.create_issue(
section='reservation',
internal_message=err.data['message'],
channel_object_id=rcode)
failed_reservations.append(crcode)
return (processed_rids, any(failed_reservations),
checkin_utc_dt, checkout_utc_dt)
@api.model
def _generate_pricelists(self, price_plans):
product_listprice_obj = self.env['product.pricelist']
count = 0
for plan in price_plans:
if 'vpid' in plan:
continue # Ignore Virtual Plans
vals = {
'name': plan['name'],
'is_daily_plan': plan['daily'] == 1,
}
plan_id = product_listprice_obj.search([
('wpid', '=', str(plan['id']))
], limit=1)
if not plan_id:
vals.update({
'wpid': str(plan['id']),
})
product_listprice_obj.with_context({
'wubook_action': False}).create(vals)
else:
plan_id.with_context({'wubook_action': False}).write(vals)
count = count + 1
return count
@api.model
def _generate_pricelist_items(self, channel_plan_id, date_from, date_to, plan_prices):
hotel_room_type_obj = self.env['hotel.room.type']
pricelist = self.env['product.pricelist'].search([
('wpid', '=', channel_plan_id)
], limit=1)
if pricelist:
pricelist_item_obj = self.env['product.pricelist.item']
dfrom_dt = fields.Date.from_string(date_from)
dto_dt = fields.Date.from_string(date_to)
days_diff = (dfrom_dt - dto_dt) + 1
for i in range(0, days_diff):
ndate_dt = dfrom_dt + timedelta(days=i)
for k_rid, v_rid in plan_prices.iteritems():
room_type = hotel_room_type_obj.search([
('wrid', '=', k_rid)
], limit=1)
if room_type:
pricelist_item = pricelist_item_obj.search([
('pricelist_id', '=', pricelist.id),
('date_start', '=', ndate_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT)),
('date_end', '=', ndate_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT)),
('compute_price', '=', 'fixed'),
('applied_on', '=', '1_product'),
('product_tmpl_id', '=', room_type.product_id.product_tmpl_id.id)
], limit=1)
vals = {
'fixed_price': plan_prices[k_rid][i],
'wpushed': True,
}
if pricelist_item:
pricelist_item.with_context({
'wubook_action': False}).write(vals)
else:
vals.update({
'pricelist_id': pricelist.id,
'date_start': ndate_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT),
'date_end': ndate_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT),
'compute_price': 'fixed',
'applied_on': '1_product',
'product_tmpl_id': room_type.product_id.product_tmpl_id.id
})
pricelist_item_obj.with_context({
'wubook_action': False}).create(vals)
return True
@api.model
def _generate_restrictions(self, restriction_plans):
restriction_obj = self.env['hotel.room.type.restriction']
count = 0
for plan in restriction_plans:
vals = {
'name': plan['name'],
}
plan_id = restriction_obj.search([
('wpid', '=', str(plan['id']))
], limit=1)
if not plan_id:
vals.update({
'wpid': str(plan['id']),
})
restriction_obj.with_context({
'wubook_action': False,
'rules': plan.get('rules'),
}).create(vals)
else:
plan_id.with_context({'wubook_action': False}).write(vals)
count = count + 1
return count
@api.model
def _generate_restriction_items(self, plan_restrictions):
hotel_room_type_obj = self.env['hotel.room.type']
reserv_restriction_obj = self.env['hotel.room.type.restriction']
restriction_item_obj = self.env['hotel.room.type.restriction.item']
_logger.info("===== RESTRICTIONS")
_logger.info(plan_restrictions)
for k_rpid, v_rpid in plan_restrictions.iteritems():
restriction_id = reserv_restriction_obj.search([
('wpid', '=', k_rpid)
], limit=1)
if restriction_id:
for k_rid, v_rid in v_rpid.iteritems():
room_type = hotel_room_type_obj.search([
('wrid', '=', k_rid)
], limit=1)
if room_type:
for item in v_rid:
date_dt = fields.Date.from_string(item['date'])
restriction_item = restriction_item_obj.search([
('restriction_id', '=', restriction_id.id),
('date_start', '=', date_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT)),
('date_end', '=', date_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT)),
('applied_on', '=', '0_room_type'),
('room_type_id', '=', room_type.id)
], limit=1)
vals = {
'closed_arrival': item['closed_arrival'],
'closed': item['closed'],
'min_stay': item['min_stay'],
'closed_departure': item['closed_departure'],
'max_stay': item['max_stay'],
'max_stay_arrival': item['max_stay_arrival'],
'min_stay_arrival': item['min_stay_arrival'],
'wpushed': True,
}
if restriction_item:
restriction_item.with_context({
'wubook_action': False}).write(vals)
else:
vals.update({
'restriction_id': restriction_id.id,
'date_start': date_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT),
'date_end': date_dt.strftime(
DEFAULT_SERVER_DATE_FORMAT),
'applied_on': '0_room_type',
'room_type_id': room_type.id
})
restriction_item_obj.with_context({
'wubook_action': False}).create(vals)
return True
@api.model
def fetch_booking(self, channel_reservation_id):
try:
results = self.backend_adapter.fetch_booking(channel_reservation_id)
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)
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))
except ChannelConnectorError as err:
self.create_issue(
'reservation',
_("Can't process reservations from wubook"),
err.data['message'], channel_object_id=channel_reservation_id)
return False
return True
@api.model
def import_pricing_plans(self):
try:
results = self.backend_adapter.get_pricing_plans()
count = self._generate_pricelists(results)
except ChannelConnectorError as err:
self.create_issue(
section='plan',
internal_message=_("Can't get pricing plans from wubook"),
channel_message=err.data['message'])
return 0
return count
@api.model
def fetch_plan_prices(self, channel_plan_id, date_from, date_to, rooms=None):
try:
results = self.backend_adapter.fetch_plan_prices(
channel_plan_id,
date_from,
date_to,
rooms)
self._generate_pricelist_items(channel_plan_id, date_from, date_to, results)
except ChannelConnectorError as err:
self.create_issue(
section='plan',
internal_message=_("Can't fetch plan prices from wubook"),
channel_message=err.data['message'])
return False
return True
@api.model
def fetch_all_plan_prices(self, date_from, date_to, rooms=None):
no_errors = True
channel_plan_ids = self.env['product.pricelist'].search([
('wpid', '!=', False), ('wpid', '!=', '')
]).mapped('wpid')
if any(channel_plan_ids):
try:
for channel_plan_id in channel_plan_ids:
results = self.backend_adapter.fetch_plan_prices(
channel_plan_id,
date_from,
date_to,
rooms)
self._generate_pricelist_items(channel_plan_id, date_from, date_to, results)
except ChannelConnectorError as err:
self.create_issue(
section='plan',
internal_message="Can't fetch all plan prices from wubook!",
channel_message=err.data['message'],
channel_object_id=channel_plan_id, dfrom=date_from, dto=date_to)
return False
return no_errors
@api.model
def import_restriction_plans(self):
try:
results = self.backend_adapter.rplan_rplans()
count = self._generate_restrictions(results)
except ChannelConnectorError as err:
self.create_issue(
section='rplan',
internal_message=_("Can't fetch restriction plans from wubook"),
channel_message=err.data['message'])
return 0
return count
@api.model
def fetch_rplan_restrictions(self, date_from, date_to, channel_restriction_plan_id=False):
try:
results = self.backend_adapter.wired_rplan_get_rplan_values(
date_from,
date_to,
int(channel_restriction_plan_id))
if any(results):
self._generate_restriction_items(results)
except ChannelConnectorError as err:
self.create_issue(
section='rplan',
internal_message=_("Can't fetch plan restrictions from wubook"),
channel_message=err.data['message'],
channel_object_id=channel_restriction_plan_id,
dfrom=date_from, dto=date_to)
return False
return True
class BatchImporter(AbstractComponent):
""" The role of a BatchImporter is to search for a list of
items to import, then it can either import them directly or delay
the import of each item separately.
"""
_name = 'channel.batch.importer'
_inherit = ['base.importer', 'base.hotel.channel.connector']
_usage = 'batch.importer'
def _import_record(self, external_id):
""" Import a record directly or delay the import of the record.
Method to implement in sub-classes.
"""
raise NotImplementedError
class DirectBatchImporter(AbstractComponent):
""" Import the records directly, without delaying the jobs. """
_name = 'channel.direct.batch.importer'
_inherit = 'channel.batch.importer'
def _import_record(self, external_id):
""" Import the record directly """
self.model.import_record(self.backend_record, external_id)

View File

@@ -39,9 +39,7 @@ class website_wubook(http.Controller):
if not backend: if not backend:
raise ValidationError(_("Can't found a backend!")) raise ValidationError(_("Can't found a backend!"))
_logger.info(_("[WUBOOK->ODOO] Importing Booking...")) request.env['channel.hotel.reservation'].import_reservation(rcode)
# Create Reservation
request.env['wubook'].sudo().fetch_booking(lcode, rcode)
return request.make_response('200 OK', [('Content-Type', 'text/plain')]) return request.make_response('200 OK', [('Content-Type', 'text/plain')])
@@ -72,18 +70,12 @@ class website_wubook(http.Controller):
odoo_dto = datetime.strptime( odoo_dto = datetime.strptime(
dto, dto,
DEFAULT_WUBOOK_DATE_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
backend.write({
'avail_from': odoo_dfrom, request.env['channel.hotel.room.type.availability'].import_availability(
'avail_to': odoo_dto, backend, odoo_dfrom, odoo_dto)
'restriction_id': False, request.env['channel.hotel.room.type.restriction.item'].import_restriction_values(
'restriction_from': odoo_dfrom, backend, odoo_dfrom, odoo_dto, False)
'restriction_to': odoo_dto, request.env['channel.product.pricelist.item'].import_pricelist_values(
'pricelist_id': False, backend, odoo_dfrom, odoo_dto, False)
'pricelist_from': odoo_dfrom,
'pricelist_to': odoo_dto,
})
backend.import_availability()
backend.import_restriction()
backend.import_pricelist()
return request.make_response('200 OK', [('Content-Type', 'text/plain')]) return request.make_response('200 OK', [('Content-Type', 'text/plain')])

View File

@@ -1,26 +1,29 @@
<odoo noupdate="1"> <?xml version="1.0" encoding="utf-8"?>
<!--record model="ir.cron" id="wubook_push_avail"> <odoo>
<field name="name">WuBook Push Availability</field> <data noupdate="1">
<record model="ir.cron" id="channel_connector_push_changes">
<field name="name">Channel Connector Push Changes</field>
<field name="interval_number">5</field> <field name="interval_number">5</field>
<field name="interval_type">minutes</field> <field name="interval_type">minutes</field>
<field name="numbercall">-1</field> <field name="numbercall">-1</field>
<field name="state">code</field>
<field name="doall" eval="False" /> <field name="doall" eval="False" />
<field name="model_id" ref="model_wubook" /> <field name="model_id" ref="model_channel_backend" />
<field name="code">model.push_changes()</field> <field name="code">model.cron_push_changes()</field>
</record--> </record>
<!-- <record id="wubook_corporate_fetch" model="ir.cron"> <record model="ir.cron" id="channel_connector_fetch_new_bookings">
<field name="name">WuBook Corporate Fetchable Properties</field> <field name="name">Channel Connector Fetch New Bookings</field>
<field name="active" eval="True" /> <field name="user_id" ref="base.user_root" />
<field name="user_id" ref="base.user_root" /> <field name="interval_number">1</field>
<field name="interval_number">4</field> <field name="interval_type">minutes</field>
<field name="interval_type">hours</field> <field name="numbercall">-1</field>
<field name="numbercall">-1</field> <field name="state">code</field>
<field name="doal">1</field> <field name="doall" eval="False" />
<field name="nextcall" >2016-12-31 23:59:59</field> <field name="model_id" ref="model_channel_backend" />
<field name="model" eval="'wubook'" /> <field name="code">model.cron_import_reservations()</field>
<field name="function" eval="'corporate_fetch'" /> </record>
<field name="args" eval="" />
<field name="priority" eval="5" /> </data>
</record> -->
</odoo> </odoo>

View File

@@ -1,11 +1,13 @@
# 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 contextlib import contextmanager
from odoo import models, api, fields from odoo import models, api, fields
from ...components.backend_adapter import WuBookLogin, WuBookServer 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'
@@ -31,18 +33,22 @@ class ChannelBackend(models.Model):
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')
avail_from = fields.Date('Availability From') reservation_id_str = fields.Char('Channel Reservation ID', store=False)
avail_to = fields.Date('Availability To')
restriction_from = fields.Date('Restriction From') avail_from = fields.Date('Availability From', store=False)
restriction_to = fields.Date('Restriction To') avail_to = fields.Date('Availability To', store=False)
restriction_from = fields.Date('Restriction From', store=False)
restriction_to = fields.Date('Restriction To', store=False)
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') pricelist_from = fields.Date('Pricelist From', store=False)
pricelist_to = fields.Date('Pricelist To') pricelist_to = fields.Date('Pricelist To', store=False)
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,84 +66,159 @@ class ChannelBackend(models.Model):
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:
channel_hotel_reservation_obj.import_reservations(backend) count = channel_hotel_reservation_obj.import_reservations(backend)
if count == 0:
self.env.user.notify_info("No reservations to import. All done :)",
title="Import Reservations")
else:
self.env.user.notify_info("%d reservations successfully imported" % count,
title="Import Reservations")
return True
@api.multi
def import_reservation(self):
channel_hotel_reservation_obj = self.env['channel.hotel.reservation']
for backend in self:
res = channel_hotel_reservation_obj.import_reservation(
backend,
backend.reservation_id_str)
if not res:
self.env.user.notify_warning(
"Can't import '%s' reservation" % backend.reservation_id_str,
title="Import Reservations")
return True return True
@api.multi @api.multi
def import_rooms(self): def import_rooms(self):
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:
channel_hotel_room_type_obj.import_rooms(backend) count = channel_hotel_room_type_obj.import_rooms(backend)
if count == 0:
self.env.user.notify_info("No rooms to import. All done :)",
title="Import Rooms")
else:
self.env.user.notify_info("%d rooms successfully imported" % count,
title="Import Rooms")
return True return True
@api.multi @api.multi
def import_otas_info(self): def import_otas_info(self):
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:
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,
title="Import OTA's")
return True return True
@api.multi @api.multi
def import_availability(self): def import_availability(self):
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:
channel_hotel_room_type_avail_obj.import_availability(backend) res = channel_hotel_room_type_avail_obj.import_availability(
backend,
backend.avail_from,
backend.avail_to)
if not res:
self.env.user.notify_warning("Error importing availability",
title="Import Availability")
return True return True
@api.multi @api.multi
def push_availability(self): def push_availability(self):
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:
channel_hotel_room_type_avail_obj.push_availability(backend) res = channel_hotel_room_type_avail_obj.push_availability(backend)
if not res:
self.env.user.notify_warning("Error pushing availability",
title="Export Availability")
return True return True
@api.multi @api.multi
def import_restriction_plans(self): def import_restriction_plans(self):
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:
channel_hotel_room_type_restr_obj.import_restriction_plans(backend) count = channel_hotel_room_type_restr_obj.import_restriction_plans(backend)
if count == 0:
self.env.user.notify_info("No restiction plans to import. All done :)",
title="Import Restrictions")
else:
self.env.user.notify_info("%d restriction plans successfully imported" % count,
title="Import Restrictions")
return True return True
@api.multi @api.multi
def import_restriction_values(self): def import_restriction_values(self):
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:
channel_hotel_restr_item_obj.import_restriction_values(backend) res = channel_hotel_restr_item_obj.import_restriction_values(
backend,
backend.restriction_from,
backend.restriction_to,
backend.restriction_id and backend.restriction_id.external_id or False)
if not res:
self.env.user.notify_warning("Error importing restrictions",
title="Import Restrictions")
return True return True
@api.multi @api.multi
def push_restriction(self): def push_restriction(self):
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:
channel_hotel_restr_item_obj.push_restriction(backend) res = channel_hotel_restr_item_obj.push_restriction(backend)
if not res:
self.env.user.notify_warning("Error pushing restrictions",
title="Export Restrictions")
return True return True
@api.multi @api.multi
def import_pricelist_plans(self): def import_pricelist_plans(self):
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:
channel_product_pricelist_obj.import_price_plans(backend) count = channel_product_pricelist_obj.import_price_plans(backend)
if count == 0:
self.env.user.notify_info("No pricelist plans to import. All done :)",
title="Import Pricelists")
else:
self.env.user.notify_info("%d pricelist plans successfully imported" % count,
title="Import Pricelists")
return True return True
@api.multi @api.multi
def import_pricelist_values(self): def import_pricelist_values(self):
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:
channel_product_pricelist_item_obj.import_pricelist_values(backend) res = channel_product_pricelist_item_obj.import_pricelist_values(
backend,
backend.pricelist_from,
backend.pricelist_to,
backend.pricelist_id and backend.pricelist_id.external_id or False)
if not res:
self.env.user.notify_warning("Error importing pricelists",
title="Import Pricelists")
return True return True
@api.multi @api.multi
def push_pricelist(self): def push_pricelist(self):
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:
channel_product_pricelist_item_obj.push_pricelist(backend) res = channel_product_pricelist_item_obj.push_pricelist(backend)
if not res:
self.env.user.notify_warning("Error pushing pricelists",
title="Export Pricelists")
return True return True
@api.multi @api.model
def push_changes(self): def cron_push_changes(self):
self.push_availability() _logger.info("======== PASA POR AKI!! AAAAA")
self.push_restriction() backends = self.env[self._name].search([])
self.push_pricelist() backends.push_availability()
backends.push_restriction()
backends.push_pricelist()
@api.model
def cron_import_reservations(self):
_logger.info("======== PASA POR AKI!! BBBBBB")
self.env[self._name].search([]).import_reservations()
@contextmanager @contextmanager
@api.multi @api.multi
@@ -153,163 +234,3 @@ class ChannelBackend(models.Model):
_super = super(ChannelBackend, self) _super = super(ChannelBackend, self)
with _super.work_on(model_name, channel_api=channel_api, **kwargs) as work: with _super.work_on(model_name, channel_api=channel_api, **kwargs) as work:
yield work yield work
# Dangerus method: Usefull for cloned instances with new wubook account
@api.multi
def resync(self):
self.ensure_one()
now_utc_dt = fields.Date.now()
now_utc_str = now_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
# Reset Issues
issue_ids = self.env['wubook.issue'].search([])
issue_ids.write({
'to_read': False
})
# Push Virtual Rooms
wubook_obj = self.env['wubook'].with_context({
'init_connection': False
})
if wubook_obj.init_connection():
ir_seq_obj = self.env['ir.sequence']
room_types = self.env['hotel.room.type'].search([])
for room_type in room_types:
shortcode = ir_seq_obj.next_by_code('hotel.room.type')[:4]
channel_room_id = wubook_obj.create_room(
shortcode,
room_type.name,
room_type.wcapacity,
room_type.list_price,
room_type.total_rooms_count
)
if channel_room_id:
room_type.with_context(wubook_action=False).write({
'channel_room_id': channel_room_id,
'wscode': shortcode,
})
else:
room_type.with_context(wubook_action=False).write({
'channel_room_id': '',
'wscode': '',
})
# Create Restrictions
room_type_rest_obj = self.env['hotel.room.type.restriction']
restriction_ids = room_type_rest_obj.search([])
for restriction in restriction_ids:
if restriction.wpid != '0':
channel_plan_id = wubook_obj.create_rplan(restriction.name)
restriction.write({
'channel_plan_id': channel_plan_id or ''
})
# Create Pricelist
pricelist_ids = self.env['product.pricelist'].search([])
for pricelist in pricelist_ids:
channel_plan_id = wubook_obj.create_plan(pricelist.name, pricelist.is_daily_plan)
pricelist.write({
'channel_plan_id': channel_plan_id or ''
})
wubook_obj.close_connection()
# Reset Folios
folio_ids = self.env['hotel.folio'].search([])
folio_ids.with_context(wubook_action=False).write({
'wseed': '',
})
# Reset Reservations
reservation_ids = self.env['hotel.reservation'].search([
('channel_reservation_id', '!=', ''),
('channel_reservation_id', '!=', False)
])
reservation_ids.with_context(wubook_action=False).write({
'channel_reservation_id': '',
'ota_id': False,
'ota_reservation_id': '',
'is_from_ota': False,
'wstatus': 0
})
# Get Default Models
pricelist_id = int(self.env['ir.default'].sudo().get(
'res.config.settings', 'default_pricelist_id'))
restriction_id = int(self.env['ir.default'].sudo().get(
'res.config.settings', 'default_restriction_id'))
room_type_restr_it_obj = self.env['hotel.room.type.restriction.item']
# Secure Wubook Input
restriction_item_ids = room_type_restr_it_obj.search([
('applied_on', '=', '0_room_type'),
('date_start', '<', now_utc_str),
])
if any(restriction_item_ids):
restriction_item_ids.with_context(wubook_action=False).write({
'wpushed': True
})
# Put to push restrictions
restriction_item_ids = room_type_restr_it_obj.search([
('restriction_id', '=', restriction_id),
('applied_on', '=', '0_room_type'),
('wpushed', '=', True),
('date_start', '>=', now_utc_str),
])
if any(restriction_item_ids):
restriction_item_ids.with_context(wubook_action=False).write({
'wpushed': False
})
# Secure Wubook Input
pricelist_item_ids = self.env['product.pricelist.item'].search([
('applied_on', '=', '1_product'),
('compute_price', '=', 'fixed'),
('date_start', '<', now_utc_str),
])
if any(pricelist_item_ids):
pricelist_item_ids.with_context(wubook_action=False).write({
'wpushed': True
})
# Put to push pricelists
pricelist_item_ids = self.env['product.pricelist.item'].search([
('pricelist_id', '=', pricelist_id),
('applied_on', '=', '1_product'),
('compute_price', '=', 'fixed'),
('wpushed', '=', True),
('date_start', '>=', now_utc_str),
])
if any(pricelist_item_ids):
pricelist_item_ids.with_context(wubook_action=False).write({
'wpushed': False
})
# Secure Wubook Input
availabity_ids = self.env['hotel.room.type.availability'].search([
('date', '<', now_utc_str),
])
if any(availabity_ids):
availabity_ids.with_context(wubook_action=False).write({
'wpushed': True
})
# Put to push availability
availabity_ids = self.env['hotel.room.type.availability'].search([
('wpushed', '=', True),
('date', '>=', now_utc_str),
])
if any(availabity_ids):
availabity_ids.with_context(wubook_action=False).write({
'wpushed': False
})
# Generate Security Token
self.env['ir.default'].sudo().set(
'wubook.config.settings',
'wubook_push_security_token',
binascii.hexlify(os.urandom(16)).decode())
self.env.cr.commit() # FIXME: Need do this
# Push Changes
if wubook_obj.init_connection():
wubook_obj.push_activation()
wubook_obj.import_channels_info()
wubook_obj.push_changes()
wubook_obj.close_connection()

View File

@@ -22,15 +22,3 @@ class ChannelBinding(models.AbstractModel):
('channel_uniq', 'unique(backend_id, external_id)', ('channel_uniq', 'unique(backend_id, external_id)',
'A binding already exists with the same Channel ID.'), 'A binding already exists with the same Channel ID.'),
] ]
@api.model
def create_issue(self, **kwargs):
self.env['hotel.channel.connector.issue'].sudo().create({
'backend_id': kwargs.get('backend', self.backend_id.id),
'section': kwargs.get('section', False),
'internal_message': kwargs.get('internal_message', False),
'channel_object_id': kwargs.get('channel_object_id', False),
'channel_message': kwargs.get('channel_message', False),
'date_start': kwargs.get('dfrom', False),
'date_end': kwargs.get('dto', False),
})

View File

@@ -4,7 +4,7 @@
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 from odoo.addons.component.core import Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
class ChannelOtaInfo(models.Model): class ChannelOtaInfo(models.Model):
_name = 'channel.ota.info' _name = 'channel.ota.info'
@@ -20,14 +20,7 @@ class ChannelOtaInfo(models.Model):
def import_otas_info(self, backend): def import_otas_info(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='ota.info.importer') importer = work.component(usage='ota.info.importer')
try: return importer.import_otas_info()
return importer.import_otas_info()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='room',
internal_message=str(err),
channel_message=err.data['message'])
class HotelRoomTypeAdapter(Component): class HotelRoomTypeAdapter(Component):
_name = 'channel.ota.info.adapter' _name = 'channel.ota.info.adapter'

View File

@@ -4,6 +4,7 @@
from odoo.exceptions import ValidationError 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.addons.connector.components.mapper import mapping
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import fields, api, _ from odoo import fields, api, _
from odoo.tools import ( from odoo.tools import (
DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATE_FORMAT,
@@ -18,31 +19,37 @@ class ChannelOtaInfoImporter(Component):
@api.model @api.model
def import_otas_info(self): def import_otas_info(self):
results = self.backend_adapter.get_channels_info()
channel_ota_info_obj = self.env['channel.ota.info']
ota_info_mapper = self.component(usage='import.mapper',
model_name='channel.ota.info')
count = 0 count = 0
for ota_id in results.keys(): try:
vals = { results = self.backend_adapter.get_channels_info()
'id': ota_id, except ChannelConnectorError as err:
'name': results[ota_id]['name'], self.create_issue(
'ical': results[ota_id]['ical'] == 1, section='room',
} internal_message=str(err),
map_record = ota_info_mapper.map_record(vals) channel_message=err.data['message'])
ota_info_bind = channel_ota_info_obj.search([ else:
('ota_id', '=', ota_id) channel_ota_info_obj = self.env['channel.ota.info']
], limit=1) ota_info_mapper = self.component(usage='import.mapper',
if ota_info_bind: model_name='channel.ota.info')
ota_info_bind.with_context({ for ota_id in results.keys():
'connector_no_export': True, vals = {
}).write(map_record.values()) 'id': ota_id,
else: 'name': results[ota_id]['name'],
ota_info_bind.with_context({ 'ical': results[ota_id]['ical'] == 1,
'connector_no_export': True, }
}).create(map_record.values(for_create=True)) map_record = ota_info_mapper.map_record(vals)
count = count + 1 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 return count

View File

@@ -10,7 +10,7 @@ class HotelChannelConnectorIssue(models.Model):
_old_name = 'wubook.issue' _old_name = 'wubook.issue'
backend_id = fields.Many2one('channel.backend', backend_id = fields.Many2one('channel.backend',
'Restriction Plan', 'Backend',
required=True, required=True,
ondelete='cascade', ondelete='cascade',
index=True) index=True)

View File

@@ -3,3 +3,4 @@
from . import common from . import common
from . import importer from . import importer
from . import exporter

View File

@@ -34,19 +34,37 @@ class ChannelHotelReservation(models.Model):
old_name='wchannel_reservation_code') old_name='wchannel_reservation_code')
channel_raw_data = fields.Text(readonly=True, old_name='wbook_json') channel_raw_data = fields.Text(readonly=True, old_name='wbook_json')
wstatus = fields.Selection([ channel_status = fields.Selection([
('0', 'No Channel'), ('0', 'No Channel'),
(str(WUBOOK_STATUS_CONFIRMED), 'Confirmed'), (str(WUBOOK_STATUS_CONFIRMED), 'Confirmed'),
(str(WUBOOK_STATUS_WAITING), 'Waiting'), (str(WUBOOK_STATUS_WAITING), 'Waiting'),
(str(WUBOOK_STATUS_REFUSED), 'Refused'), (str(WUBOOK_STATUS_REFUSED), 'Refused'),
(str(WUBOOK_STATUS_ACCEPTED), 'Accepted'), (str(WUBOOK_STATUS_ACCEPTED), 'Accepted'),
(str(WUBOOK_STATUS_CANCELLED), 'Cancelled'), (str(WUBOOK_STATUS_CANCELLED), 'Cancelled'),
(str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty')], (str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty'),
string='WuBook Status', ], string='Channel Status', default='0', readonly=True, old_name='wstatus')
default='0', channel_status_reason = fields.Char("Channel Status Reason", readonly=True,
readonly=True) old_name='wstatus_reason')
wstatus_reason = fields.Char("WuBook Status Reason", readonly=True) channel_modified = fields.Boolean("Channel Modified", readonly=True,
wmodified = fields.Boolean("WuBook Modified", readonly=True, default=False) default=False, old_name='wmodified')
@api.depends('channel_reservation_id', 'ota_id')
def _is_from_ota(self):
for record in self:
record.is_from_ota = (record.external_id and record.ota_id)
@job(default_channel='root.channel')
@api.model
def refresh_availability(self, checkin, checkout, product_id):
self.env['channel.hotel.room.type.availability'].refresh_availability(
checkin, checkout, product_id)
@job(default_channel='root.channel')
@api.model
def import_reservation(self, backend, channel_reservation_id):
with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.reservation.importer')
return importer.fetch_booking(channel_reservation_id)
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
@@ -55,36 +73,19 @@ class ChannelHotelReservation(models.Model):
importer = work.component(usage='hotel.reservation.importer') importer = work.component(usage='hotel.reservation.importer')
return importer.fetch_new_bookings() return importer.fetch_new_bookings()
@api.depends('channel_reservation_id', 'ota_id')
def _is_from_ota(self):
for record in self:
record.odoo_id.is_from_ota = (record.channel_reservation_id and \
record.ota_id)
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi
def push_availability(self):
self.ensure_one()
if self._context.get('channel_action', True):
with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='channel.exporter')
exporter.push_availability()
@job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def cancel_reservation(self): def cancel_reservation(self):
self.ensure_one() with self.backend_id.work_on(self._name) as work:
if self._context.get('channel_action', True): exporter = work.component(usage='hotel.reservation.exporter')
user = self.env['res.user'].browse(self.env.uid) return exporter.cancel_reservation(self)
with self.backend_id.work_on(self._name) as work:
adapter = work.component(usage='backend.adapter') @job(default_channel='root.channel')
wres = adapter.cancel_reservation( @api.multi
self.channel_reservation_id, def mark_booking(self):
_('Cancelled by %s') % user.partner_id.name) with self.backend_id.work_on(self._name) as work:
if not wres: exporter = work.component(usage='hotel.reservation.exporter')
raise ValidationError(_("Can't cancel reservation on WuBook")) return exporter.mark_booking(self)
class HotelReservation(models.Model): class HotelReservation(models.Model):
_inherit = 'hotel.reservation' _inherit = 'hotel.reservation'
@@ -100,11 +101,14 @@ class HotelReservation(models.Model):
for record in self: for record in self:
if not record.channel_type: if not record.channel_type:
record.channel_type = 'door' record.channel_type = 'door'
record.origin_sale = dict(
self.fields_get( if record.channel_type == 'web' and any(record.channel_bind_ids) and \
allfields=['channel_type'])['channel_type']['selection'])[record.channel_type] \ record.channel_bind_ids[0].ota_id:
if record.channel_type != 'web' or not record.channel_bind_ids[0].ota_id \ record.origin_sale = record.channel_bind_ids[0].ota_id.name
else record.channel_bind_ids[0].ota_id.name else:
record.origin_sale = dict(
self.fields_get(allfields=['channel_type'])['channel_type']['selection']
)[record.channel_type]
channel_bind_ids = fields.One2many( channel_bind_ids = fields.One2many(
comodel_name='channel.hotel.reservation', comodel_name='channel.hotel.reservation',
@@ -124,17 +128,12 @@ class HotelReservation(models.Model):
@api.model @api.model
def create(self, vals): def create(self, vals):
if vals.get('channel_reservation_id') != None: if vals.get('external_id') is not None:
vals.update({'preconfirm': False}) vals.update({'preconfirm': False})
user = self.env['res.users'].browse(self.env.uid) user = self.env['res.users'].browse(self.env.uid)
if user.has_group('hotel.group_hotel_call'): if user.has_group('hotel.group_hotel_call'):
vals.update({'to_read': True}) vals.update({'to_read': True})
res = super(HotelReservation, self).create(vals) return super(HotelReservation, self).create(vals)
self.env['hotel.room.type.availability'].refresh_availability(
vals['checkin'],
vals['checkout'],
vals['product_id'])
return res
@api.multi @api.multi
def write(self, vals): def write(self, vals):
@@ -198,8 +197,8 @@ class HotelReservation(models.Model):
@api.multi @api.multi
def action_cancel(self): def action_cancel(self):
waction = self._context.get('wubook_action', True) no_export = self._context.get('connector_no_export', True)
if waction: if no_export:
for record in self: for record in self:
# Can't cancel in Odoo # Can't cancel in Odoo
if record.is_from_ota: if record.is_from_ota:
@@ -209,12 +208,13 @@ class HotelReservation(models.Model):
self.write({'to_read': True, 'to_assign': True}) self.write({'to_read': True, 'to_assign': True})
res = super(HotelReservation, self).action_cancel() res = super(HotelReservation, self).action_cancel()
if waction: if no_export:
for record in self: for record in self:
# Only can cancel reservations created directly in wubook # Only can cancel reservations created directly in wubook
if record.channel_bind_ids[0].channel_reservation_id and \ if any(record.channel_bind_ids) and \
record.channel_bind_ids[0].external_id and \
not record.channel_bind_ids[0].ota_id and \ not record.channel_bind_ids[0].ota_id and \
record.channel_bind_ids[0].wstatus in ['1', '2', '4']: record.channel_bind_ids[0].channel_status in ['1', '2', '4']:
self._event('on_record_cancel').notify(record) self._event('on_record_cancel').notify(record)
return res return res
@@ -222,7 +222,8 @@ class HotelReservation(models.Model):
def confirm(self): def confirm(self):
can_confirm = True can_confirm = True
for record in self: for record in self:
if record.is_from_ota and int(record.wstatus) in WUBOOK_STATUS_BAD: 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 can_confirm = False
break break
if not can_confirm: if not can_confirm:
@@ -276,22 +277,39 @@ class HotelReservationAdapter(Component):
_inherit = 'wubook.adapter' _inherit = 'wubook.adapter'
_apply_on = 'channel.hotel.reservation' _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): def fetch_new_bookings(self):
return super(HotelReservationAdapter, self).fetch_new_bookings() 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'
_inherit = 'base.connector.listener' _inherit = 'base.connector.listener'
_apply_on = ['channel.hotel.reservation'] _apply_on = ['channel.hotel.reservation']
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_create(self, record, fields=None):
record.refresh_availability()
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_write(self, record, fields=None): def on_record_write(self, record, fields=None):
record.with_delay(priority=20).push_availability() record.push_availability()
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_unlink(self, record, fields=None): def on_record_unlink(self, record, fields=None):
record.with_delay(priority=20).push_availability() record.push_availability()
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) @skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_cancel(self, record, fields=None): def on_record_cancel(self, record, fields=None):
record.with_delay(priority=20).cancel_reservation() record.cancel_reservation()

View File

@@ -0,0 +1,48 @@
# 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):
_name = 'channel.hotel.reservation.exporter'
_inherit = 'hotel.channel.exporter'
_apply_on = ['channel.hotel.reservation']
_usage = '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'])

View File

@@ -1,13 +1,22 @@
# 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 json
from datetime import datetime, timedelta
from dateutil import tz
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
from odoo.addons.component.core import Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import fields, api, _ from odoo import fields, api, _
from odoo.tools import ( from odoo.tools import (
DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_DATETIME_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.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):
@@ -16,30 +25,424 @@ class HotelReservationImporter(Component):
_apply_on = ['channel.hotel.reservation'] _apply_on = ['channel.hotel.reservation']
_usage = 'hotel.reservation.importer' _usage = 'hotel.reservation.importer'
def fetch_new_bookings(self): @api.model
def fetch_booking(self, channel_reservation_id):
try: try:
results = self.backend_adapter.fetch_new_bookings() 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:
processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \ processed_rids, errors, checkin_utc_dt, checkout_utc_dt = \
self._generate_reservations(results) self._generate_reservations(results)
if any(processed_rids): if any(processed_rids):
uniq_rids = list(set(processed_rids)) self.backend_adapter.mark_bookings(list(set(processed_rids)))
rcodeb, resultsb = self.backend_adapter.mark_bookings(uniq_rids)
if rcodeb != 0:
self.create_issue(
backend=self.backend_adapter.id,
section='wubook',
internal_message=_("Problem trying mark bookings (%s)") % str(processed_rids))
# Update Odoo availability (don't wait for wubook) # Update Odoo availability (don't wait for wubook)
# This cause abuse service in first import!! # FIXME: This cause abuse service in first import!!
if checkin_utc_dt and checkout_utc_dt: if checkin_utc_dt and checkout_utc_dt:
self.backend_adapter.fetch_rooms_values( self.backend_adapter.fetch_rooms_values(
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
checkout_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: except ChannelConnectorError as err:
self.create_issue( self.create_issue(
backend=self.backend_adapter.id,
section='reservation', section='reservation',
internal_message=_("Can't process reservations from wubook"), internal_message=str(err),
channel_message=err.data['message']) channel_message=err.data['message'])
return False else:
return True 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([
('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_folio = channel_reserv_obj.search([
('ota_reservation_id', '=', crcode)
], limit=1)
if reserv_folio:
folio_id = reserv_folio.odoo_id.folio_id
else:
reserv_folio = channel_reserv_obj.search([
('external_id', '=', rcode)
], limit=1)
if reserv_folio:
folio_id = reserv_folio.odoo_id.folio_id
# Need update reservations?
reservs_processed = False
reservs_binds = channel_reserv_obj.search([('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([
('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)

View File

@@ -4,3 +4,4 @@
from . import common from . import common
from . import importer from . import importer
from . import exporter from . import exporter
from . import deleter

View File

@@ -7,7 +7,6 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class ChannelHotelRoomType(models.Model): class ChannelHotelRoomType(models.Model):
@@ -37,14 +36,7 @@ class ChannelHotelRoomType(models.Model):
def import_rooms(self, backend): def import_rooms(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.room.type.importer') importer = work.component(usage='hotel.room.type.importer')
try: return importer.get_rooms()
return importer.get_rooms()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='room',
internal_message=_("Can't import rooms from WuBook"),
channel_message=err.data['message'])
@api.constrains('ota_capacity') @api.constrains('ota_capacity')
def _check_ota_capacity(self): def _check_ota_capacity(self):
@@ -60,55 +52,31 @@ class ChannelHotelRoomType(models.Model):
raise ValidationError(_("Chanel short code can't be longer than 4 characters")) raise ValidationError(_("Chanel short code can't be longer than 4 characters"))
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def create_room(self): def create_room(self):
self.ensure_one() self.ensure_one()
if not self.external_id: if not self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.exporter') exporter = work.component(usage='hotel.room.type.exporter')
try: exporter.create_room(self)
exporter.create_room(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='room',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def modify_room(self): def modify_room(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.exporter') exporter = work.component(usage='hotel.room.type.exporter')
try: exporter.modify_room(self)
exporter.modify_room(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='room',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def delete_room(self): def delete_room(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.exporter') deleter = work.component(usage='hotel.room.type.deleter')
try: deleter.delete_room(self)
exporter.delete_room(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='room',
internal_message=str(err),
channel_message=err.data['message'])
class HotelRoomType(models.Model): class HotelRoomType(models.Model):
_inherit = 'hotel.room.type' _inherit = 'hotel.room.type'
@@ -158,9 +126,20 @@ class HotelRoomTypeAdapter(Component):
_inherit = 'wubook.adapter' _inherit = 'wubook.adapter'
_apply_on = 'channel.hotel.room.type' _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): def fetch_rooms(self):
return super(HotelRoomTypeAdapter, self).fetch_rooms() 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'

View File

@@ -0,0 +1,22 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.addons.component.core import Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import api
class HotelRoomTypeDeleter(Component):
_name = 'channel.hotel.room.type.deleter'
_inherit = 'hotel.channel.deleter'
_apply_on = ['channel.hotel.room.type']
_usage = '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'])

View File

@@ -3,6 +3,7 @@
import logging 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__) _logger = logging.getLogger(__name__)
@@ -14,31 +15,40 @@ class HotelRoomTypeExporter(Component):
@api.model @api.model
def modify_room(self, binding): def modify_room(self, binding):
return self.backend_adapter.modify_room( try:
binding.external_id, return self.backend_adapter.modify_room(
binding.name, binding.external_id,
binding.ota_capacity, binding.name,
binding.list_price, binding.ota_capacity,
binding.total_rooms_count, binding.list_price,
binding.channel_short_code) binding.total_rooms_count,
binding.channel_short_code)
@api.model except ChannelConnectorError as err:
def delete_room(self, binding): self.create_issue(
return self.backend_adapter.delete_room(binding.external_id) 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'] seq_obj = self.env['ir.sequence']
short_code = seq_obj.next_by_code('hotel.room.type')[:4] short_code = seq_obj.next_by_code('hotel.room.type')[:4]
external_id = self.backend_adapter.create_room( try:
short_code, external_id = self.backend_adapter.create_room(
binding.name, short_code,
binding.ota_capacity, binding.name,
binding.list_price, binding.ota_capacity,
binding.total_rooms_count binding.list_price,
) binding.total_rooms_count
binding.write({ )
'external_id': external_id, except ChannelConnectorError as err:
'channel_short_code': short_code, self.create_issue(
}) section='room',
self.binder.bind(external_id, binding) 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)

View File

@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
from odoo.exceptions import ValidationError 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.addons.connector.components.mapper import mapping
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import fields, api, _ from odoo import fields, api, _
from odoo.tools import ( from odoo.tools import (
DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATE_FORMAT,
@@ -22,123 +23,33 @@ class HotelRoomTypeImporter(Component):
@api.model @api.model
def get_rooms(self): def get_rooms(self):
results = self.backend_adapter.fetch_rooms() count = 0
try:
channel_room_type_obj = self.env['channel.hotel.room.type'] results = self.backend_adapter.fetch_rooms()
room_mapper = self.component(usage='import.mapper', except ChannelConnectorError as err:
model_name='channel.hotel.room.type') self.create_issue(
count = len(results) section='room',
for room in results: internal_message=str(err),
map_record = room_mapper.map_record(room) channel_message=err.data['message'])
room_bind = channel_room_type_obj.search([ else:
('external_id', '=', room['id']) channel_room_type_obj = self.env['channel.hotel.room.type']
], limit=1) room_mapper = self.component(usage='import.mapper',
if room_bind: model_name='channel.hotel.room.type')
room_bind.with_context({'connector_no_export':True}).write(map_record.values()) for room in results:
else: map_record = room_mapper.map_record(room)
room_bind = channel_room_type_obj.with_context({'connector_no_export':True}).create( room_bind = channel_room_type_obj.search([
map_record.values(for_create=True)) ('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 return count
@api.model
def fetch_rooms_values(self, dfrom, dto, rooms=False,
set_max_avail=False):
# Sanitize Dates
now_dt = datetime.now()
dfrom_dt = fields.Date.from_string(dfrom)
dto_dt = fields.Date.from_string(dto)
if dto_dt < now_dt:
return True
if dfrom_dt < now_dt:
dfrom_dt = now_dt
if dfrom_dt > dto_dt:
dfrom_dt, dto_dt = dto_dt, dfrom_dt
results = self.backend_adapter.fetch_rooms_values(
dfrom_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT),
dto_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT),
rooms)
self._generate_room_values(dfrom, dto, results,
set_max_avail=set_max_avail)
@api.model
def _map_room_values_availability(self, day_vals, set_max_avail):
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
room_avail_mapper = self.component(usage='import.mapper',
model_name='channel.hotel.room.type.availability')
map_record = room_avail_mapper.map_record(day_vals)
map_record.update(channel_pushed=True)
if set_max_avail:
map_record.update(max_avail=day_vals.get('avail', 0))
channel_room_type_avail = channel_room_type_avail_obj.search([
('room_type_id', '=', day_vals['room_type_id']),
('date', '=', day_vals['date'])
], limit=1)
if channel_room_type_avail:
channel_room_type_avail.with_context({
'connector_no_export': True,
}).write(map_record.values())
else:
channel_room_type_avail_obj.with_context({
'connector_no_export': True,
'mail_create_nosubscribe': True,
}).create(map_record.values(for_create=True))
@api.model
def _map_room_values_restrictions(self, day_vals):
channel_room_type_restr_item_obj = self.env['channel.hotel.room.type.restriction.item']
room_restriction_mapper = self.component(
usage='import.mapper',
model_name='channel.hotel.room.type.restriction.item')
map_record = room_restriction_mapper.map_record(day_vals)
map_record.update(channel_pushed=True)
room_type_restr = channel_room_type_restr_item_obj.search([
('room_type_id', '=', day_vals['room_type_id']),
('applied_on', '=', '0_room_type'),
('date', '=', day_vals['date']),
('restriction_id', '=', day_vals['restriction_plan_id']),
])
if room_type_restr:
room_type_restr.with_context({
'connector_no_export': True,
}).write(map_record.values())
else:
channel_room_type_restr_item_obj.with_context({
'connector_no_export': True,
}).create(map_record.values(for_create=True))
@api.model
def _generate_room_values(self, dfrom, dto, values, set_max_avail=False):
channel_room_type_restr_obj = self.env['channel.hotel.room.type.restriction']
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
def_restr_plan = channel_room_type_restr_obj.search([('channel_plan_id', '=', '0')])
_logger.info("==== ROOM VALUES (%s -- %s)", dfrom, dto)
_logger.info(values)
for k_rid, v_rid in values.iteritems():
room_type = channel_hotel_room_type_obj.search([
('channel_plan_id', '=', k_rid)
], limit=1)
if room_type:
date_dt = fields.Date.from_string(
dfrom,
dtformat=DEFAULT_WUBOOK_DATE_FORMAT)
for day_vals in v_rid:
date_str = date_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
day_vals.update({
'room_type_id': room_type.odoo_id.id,
'date': date_str,
})
self._map_room_values_availability(day_vals, set_max_avail)
if def_restr_plan:
day_vals.update({
'restriction_plan_id': def_restr_plan.odoo_id.id
})
self._map_room_values_restrictions(day_vals)
date_dt = date_dt + timedelta(days=1)
return True
class HotelRoomTypeImportMapper(Component): class HotelRoomTypeImportMapper(Component):
_name = 'channel.hotel.room.type.import.mapper' _name = 'channel.hotel.room.type.import.mapper'
_inherit = 'channel.import.mapper' _inherit = 'channel.import.mapper'

View File

@@ -8,7 +8,6 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo.addons.hotel_channel_connector.components.backend_adapter import ( from odoo.addons.hotel_channel_connector.components.backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT)
@@ -42,49 +41,57 @@ class ChannelHotelRoomTypeAvailability(models.Model):
than total rooms \ than total rooms \
count: %d") % record.odoo_id.room_type_id.total_rooms_count) count: %d") % record.odoo_id.room_type_id.total_rooms_count)
@job(default_channel='root.channel') @api.model
@related_action(action='related_action_unwrap_binding') def refresh_availability(self, checkin, checkout, product_id):
@api.multi date_start = fields.Date.from_string(checkin)
def update_availability(self, backend): date_end = fields.Date.from_string(checkout)
with backend.work_on(self._name) as work: # Not count end day of the reservation
exporter = work.component(usage='hotel.room.type.availability.exporter') date_diff = (date_end - date_start).days
try:
return exporter.update_availability(self) channel_room_type_obj = self.env['channel.hotel.room.type']
except ChannelConnectorError as err: channel_room_type_avail_obj = self.env['hotel.room.type.availability']
self.create_issue(
backend=backend.id, room_type_binds = channel_room_type_obj.search([('product_id', '=', product_id)])
section='avail', for room_type_bind in room_type_binds:
internal_message=str(err), if room_type_bind.external_id:
channel_message=err.data['message']) for i in range(0, date_diff):
ndate_dt = date_start + timedelta(days=i)
ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
avail = len(channel_room_type_obj.odoo_id.check_availability_room_type(
ndate_str,
ndate_str,
room_type_id=room_type_bind.odoo.id))
max_avail = room_type_bind.total_rooms_count
room_type_avail_id = channel_room_type_avail_obj.search([
('room_type_id', '=', room_type_bind.odoo.id),
('date', '=', ndate_str)], limit=1)
if room_type_avail_id and room_type_avail_id.channel_max_avail >= 0:
max_avail = room_type_avail_id.channel_max_avail
avail = max(
min(avail, room_type_bind.total_rooms_count, max_avail), 0)
if room_type_avail_id:
room_type_avail_id.write({'avail': avail})
else:
channel_room_type_avail_obj.create({
'room_type_id': room_type_bind.odoo.id,
'date': ndate_str,
'avail': avail,
})
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def import_availability(self, backend): def import_availability(self, backend, dfrom, dto):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.room.type.availability.importer') importer = work.component(usage='hotel.room.type.availability.importer')
try: return importer.import_availability_values(dfrom, dto)
return importer.import_availability_values(backend.avail_from,
backend.avail_to)
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='avail',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def push_availability(self, backend): def push_availability(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.availability.exporter') exporter = work.component(usage='hotel.room.type.availability.exporter')
try: return exporter.push_availability()
return exporter.push_availability()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='avail',
internal_message=str(err),
channel_message=err.data['message'])
class HotelRoomTypeAvailability(models.Model): class HotelRoomTypeAvailability(models.Model):
_inherit = 'hotel.room.type.availability' _inherit = 'hotel.room.type.availability'
@@ -134,46 +141,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
@api.model
def refresh_availability(self, checkin, checkout, product_id):
date_start = fields.Date.from_string(checkin)
date_end = fields.Date.from_string(checkout)
# Not count end day of the reservation
date_diff = (date_end - date_start).days
room_type_obj = self.env['hotel.room.type']
room_type_avail_obj = self.env['hotel.room.type.availability']
room_types = room_type_obj.search([
('room_ids.product_id', '=', product_id)
])
for room_type in room_types:
if room_type.channel_room_id:
for i in range(0, date_diff):
ndate_dt = date_start + timedelta(days=i)
ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
avail = len(room_type_obj.check_availability_room_type(
ndate_str,
ndate_str,
room_type_id=room_type.id))
max_avail = room_type.total_rooms_count
room_type_avail_id = room_type_avail_obj.search([
('room_type_id', '=', room_type.id),
('date', '=', ndate_str)], limit=1)
if room_type_avail_id and room_type_avail_id.channel_max_avail >= 0:
max_avail = room_type_avail_id.channel_max_avail
avail = max(
min(avail, room_type.total_rooms_count, max_avail), 0)
if room_type_avail_id:
room_type_avail_id.write({'avail': avail})
else:
room_type_avail_obj.create({
'room_type_id': room_type.id,
'date': ndate_str,
'avail': avail,
})
class HotelRoomTypeAvailabilityAdapter(Component): class HotelRoomTypeAvailabilityAdapter(Component):
_name = 'channel.hotel.room.type.availability.adapter' _name = 'channel.hotel.room.type.availability.adapter'
_inherit = 'wubook.adapter' _inherit = 'wubook.adapter'

View File

@@ -4,9 +4,9 @@
import logging 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.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import api, fields, _
from odoo.addons.hotel_channel_connector.components.backend_adapter import ( from odoo.addons.hotel_channel_connector.components.backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT)
from odoo import api, fields, _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class HotelRoomTypeAvailabilityExporter(Component): class HotelRoomTypeAvailabilityExporter(Component):
@@ -28,7 +28,6 @@ class HotelRoomTypeAvailabilityExporter(Component):
lambda x: x.room_type_id.id == room_type.id) lambda x: x.room_type_id.id == room_type.id)
days = [] days = []
for channel_room_type_avail in channel_room_type_avails: for channel_room_type_avail in channel_room_type_avails:
channel_room_type_avail.channel_pushed = True
cavail = channel_room_type_avail.avail cavail = channel_room_type_avail.avail
if channel_room_type_avail.channel_max_avail >= 0 and \ 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:
@@ -44,4 +43,14 @@ class HotelRoomTypeAvailabilityExporter(Component):
_logger.info("==[ODOO->CHANNEL]==== AVAILABILITY ==") _logger.info("==[ODOO->CHANNEL]==== AVAILABILITY ==")
_logger.info(avails) _logger.info(avails)
if any(avails): if any(avails):
self.backend_adapter.update_availability(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

View File

@@ -29,42 +29,52 @@ class HotelRoomTypeAvailabilityImporter(Component):
if dto_dt < now_dt: if dto_dt < now_dt:
return True return True
results = self.backend_adapter.fetch_rooms_values(date_from, date_to) count = 0
_logger.info("==[CHANNEL->ODOO]==== AVAILABILITY (%s - %s) ==", try:
date_from, date_to) results = self.backend_adapter.fetch_rooms_values(date_from, date_to)
_logger.info(results) 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_avail_obj = self.env['channel.hotel.room.type.availability']
channel_room_type_obj = self.env['channel.hotel.room.type'] channel_room_type_obj = self.env['channel.hotel.room.type']
room_avail_mapper = self.component( room_avail_mapper = self.component(
usage='import.mapper', usage='import.mapper',
model_name='channel.hotel.room.type.availability') model_name='channel.hotel.room.type.availability')
count = len(results) for room_k, room_v in results.items():
for room_k, room_v in results.items(): iter_day = dfrom_dt
iter_day = dfrom_dt channel_room_type = channel_room_type_obj.search([
channel_room_type = channel_room_type_obj.search([ ('channel_room_id', '=', room_k)
('channel_room_id', '=', room_k) ], limit=1)
], limit=1) if channel_room_type:
if channel_room_type: for room in room_v:
for room in room_v: room.update({
room.update({ 'room_type_id': channel_room_type.odoo_id.id,
'room_type_id': channel_room_type.odoo_id.id, 'date': fields.Date.to_string(iter_day),
'date': fields.Date.to_string(iter_day), })
}) map_record = room_avail_mapper.map_record(room)
map_record = room_avail_mapper.map_record(room) room_type_avail_bind = channel_room_type_avail_obj.search([
room_type_avail_bind = channel_room_type_avail_obj.search([ ('room_type_id', '=', room['room_type_id']),
('room_type_id', '=', room['room_type_id']), ('date', '=', room['date'])
('date', '=', room['date']) ], limit=1)
], limit=1) if room_type_avail_bind:
if room_type_avail_bind: room_type_avail_bind.with_context({
room_type_avail_bind.with_context({ 'connector_no_export': True,
'connector_no_export': True, }).write(map_record.values())
}).write(map_record.values()) else:
else: room_type_avail_bind = channel_room_type_avail_obj.with_context({
room_type_avail_bind = channel_room_type_avail_obj.with_context({ 'connector_no_export': True,
'connector_no_export': True, }).create(map_record.values(for_create=True))
}).create(map_record.values(for_create=True)) room_type_avail_bind.channel_pushed = True
iter_day += timedelta(days=1) iter_day += timedelta(days=1)
count = count + 1
return count return count

View File

@@ -4,3 +4,4 @@
from . import common from . import common
from . import importer from . import importer
from . import exporter from . import exporter
from . import deleter

View File

@@ -7,7 +7,6 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class ChannelHotelRoomTypeRestriction(models.Model): class ChannelHotelRoomTypeRestriction(models.Model):
@@ -22,69 +21,38 @@ class ChannelHotelRoomTypeRestriction(models.Model):
ondelete='cascade') ondelete='cascade')
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def create_plan(self): def create_plan(self):
self.ensure_one() self.ensure_one()
if not self.external_id: if not self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.restriction.exporter') exporter = work.component(usage='hotel.room.type.restriction.exporter')
try: exporter.create_rplan(self)
exporter.create_rplan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def update_plan_name(self): def update_plan_name(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.restriction.exporter') exporter = work.component(usage='hotel.room.type.restriction.exporter')
try: exporter.rename_rplan(self)
exporter.rename_rplan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def delete_plan(self): def delete_plan(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.restriction.exporter') deleter = work.component(usage='hotel.room.type.restriction.deleter')
try: deleter.delete_rplan(self)
exporter.delete_rplan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def import_restriction_plans(self, backend): def import_restriction_plans(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.room.type.restriction.importer') importer = work.component(usage='hotel.room.type.restriction.importer')
try: return importer.import_restriction_plans()
return importer.import_restriction_plans()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
class HotelRoomTypeRestriction(models.Model): class HotelRoomTypeRestriction(models.Model):
_inherit = 'hotel.room.type.restriction' _inherit = 'hotel.room.type.restriction'

View File

@@ -0,0 +1,22 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.addons.component.core import Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import api
class HotelRoomTypeRestrictionDeleter(Component):
_name = 'channel.hotel.room.type.restriction.deleter'
_inherit = 'hotel.channel.deleter'
_apply_on = ['channel.hotel.room.type.restriction']
_usage = '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'])

View File

@@ -3,6 +3,7 @@
import logging 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__) _logger = logging.getLogger(__name__)
@@ -14,16 +15,25 @@ class HotelRoomTypeRestrictionExporter(Component):
@api.model @api.model
def rename_rplan(self, binding): def rename_rplan(self, binding):
return self.backend_adapter.rename_rplan( try:
binding.external_id, return self.backend_adapter.rename_rplan(
binding.name) binding.external_id,
binding.name)
@api.model except ChannelConnectorError as err:
def delete_rplan(self, binding): self.create_issue(
return self.backend_adapter.delete_rplan(binding.external_id) 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):
external_id = self.backend_adapter.create_rplan(binding.name) try:
binding.external_id = external_id external_id = self.backend_adapter.create_rplan(binding.name)
self.binder.bind(external_id, binding) 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)

View File

@@ -6,6 +6,7 @@ from datetime import timedelta
from odoo.exceptions import ValidationError 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.addons.connector.components.mapper import mapping
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import fields, api, _ from odoo import fields, api, _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -18,24 +19,32 @@ class HotelRoomTypeRestrictionImporter(Component):
@api.model @api.model
def import_restriction_plans(self): def import_restriction_plans(self):
results = self.backend_adapter.rplan_rplans() count = 0
channel_restriction_obj = self.env['channel.hotel.room.type.restriction'] try:
restriction_mapper = self.component(usage='import.mapper', results = self.backend_adapter.rplan_rplans()
model_name='channel.hotel.room.type.restriction') except ChannelConnectorError as err:
for plan in results: self.create_issue(
plan_record = restriction_mapper.map_record(plan) section='restriction',
plan_bind = channel_restriction_obj.search([ internal_message=str(err),
('external_id', '=', str(plan['id'])) channel_message=err.data['message'])
], limit=1) else:
if not plan_bind: channel_restriction_obj = self.env['channel.hotel.room.type.restriction']
channel_restriction_obj.with_context({ restriction_mapper = self.component(usage='import.mapper',
'connector_no_export': True, model_name='channel.hotel.room.type.restriction')
'rules': plan.get('rules'), for plan in results:
}).create(plan_record.values(for_create=True)) plan_record = restriction_mapper.map_record(plan)
else: plan_bind = channel_restriction_obj.search([
plan_bind.with_context({'connector_no_export':True}).write( ('external_id', '=', str(plan['id']))
plan_record.values()) ], limit=1)
count = count + 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 return count

View File

@@ -6,7 +6,6 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
class ChannelHotelRoomTypeRestrictionItem(models.Model): class ChannelHotelRoomTypeRestrictionItem(models.Model):
_name = 'channel.hotel.room.type.restriction.item' _name = 'channel.hotel.room.type.restriction.item'
@@ -23,36 +22,20 @@ class ChannelHotelRoomTypeRestrictionItem(models.Model):
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def import_restriction_values(self, backend): def import_restriction_values(self, backend, dfrom, dto, external_id):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.room.type.restriction.item.importer') importer = work.component(usage='hotel.room.type.restriction.item.importer')
try: return importer.import_restriction_values(
return importer.import_restriction_values( dfrom,
backend.restriction_from, dto,
backend.restriction_to, channel_restr_id=external_id)
channel_restr_id=backend.restriction_id)
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'],
channel_object_id=backend.restriction_id,
dfrom=backend.restriction_from, dto=backend.restriction_to)
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def push_restriction(self, backend): def push_restriction(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.restriction.item.exporter') exporter = work.component(usage='hotel.room.type.restriction.item.exporter')
try: return exporter.push_restriction()
return exporter.push_restriction()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
class HotelRoomTypeRestrictionItem(models.Model): class HotelRoomTypeRestrictionItem(models.Model):
_inherit = 'hotel.room.type.restriction.item' _inherit = 'hotel.room.type.restriction.item'

View File

@@ -64,12 +64,18 @@ class HotelRoomTypeRestrictionItemExporter(Component):
restrictions[rp.external_id][room_type_external_id].append({}) restrictions[rp.external_id][room_type_external_id].append({})
_logger.info("==[ODOO->CHANNEL]==== RESTRICTIONS ==") _logger.info("==[ODOO->CHANNEL]==== RESTRICTIONS ==")
_logger.info(restrictions) _logger.info(restrictions)
for k_res, v_res in restrictions.items(): try:
if any(v_res): for k_res, v_res in restrictions.items():
self.backend_adapter.update_rplan_values( if any(v_res):
int(k_res), self.backend_adapter.update_rplan_values(
date_start.strftime(DEFAULT_SERVER_DATE_FORMAT), int(k_res),
v_res) date_start.strftime(DEFAULT_SERVER_DATE_FORMAT),
unpushed.with_context({ v_res)
'wubook_action': False}).write({'channel_pushed': True}) 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 return True

View File

@@ -60,19 +60,29 @@ class HotelRoomTypeRestrictionImporter(Component):
'connector_no_export': True 'connector_no_export': True
}).write(map_record.values()) }).write(map_record.values())
else: else:
channel_restriction_item_obj.with_context({ channel_restriction_item = channel_restriction_item_obj.with_context({
'connector_no_export': True 'connector_no_export': True
}).create(map_record.values(for_create=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 channel_restr_plan_id = channel_restr_id.external_id if channel_restr_id else False
results = self.backend_adapter.wired_rplan_get_rplan_values( try:
date_from, results = self.backend_adapter.wired_rplan_get_rplan_values(
date_to, date_from,
int(channel_restr_plan_id)) date_to,
if any(results): int(channel_restr_plan_id))
self._generate_restriction_items(results) 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): class HotelRoomTypeRestrictionItemImportMapper(Component):
_name = 'channel.hotel.room.type.restriction.item.import.mapper' _name = 'channel.hotel.room.type.restriction.item.import.mapper'

View File

@@ -4,3 +4,4 @@
from . import common from . import common
from . import importer from . import importer
from . import exporter from . import exporter
from . import deleter

View File

@@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
class ChannelProductPricelist(models.Model): class ChannelProductPricelist(models.Model):
_name = 'channel.product.pricelist' _name = 'channel.product.pricelist'
@@ -21,69 +21,38 @@ class ChannelProductPricelist(models.Model):
is_daily_plan = fields.Boolean("Channel Daily Plan", default=True, old_name='wdaily_plan') is_daily_plan = fields.Boolean("Channel Daily Plan", default=True, old_name='wdaily_plan')
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def create_plan(self): def create_plan(self):
self.ensure_one() self.ensure_one()
if not self.external_id: if not self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='product.pricelist.exporter') exporter = work.component(usage='product.pricelist.exporter')
try: exporter.create_plan(self)
exporter.create_plan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def update_plan_name(self): def update_plan_name(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='product.pricelist.exporter') exporter = work.component(usage='product.pricelist.exporter')
try: exporter.rename_plan(self)
exporter.rename_plan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@related_action(action='related_action_unwrap_binding')
@api.multi @api.multi
def delete_plan(self): def delete_plan(self):
self.ensure_one() self.ensure_one()
if self.external_id: if self.external_id:
with self.backend_id.work_on(self._name) as work: with self.backend_id.work_on(self._name) as work:
exporter = work.component(usage='product.pricelist.exporter') deleter = work.component(usage='product.pricelist.deleter')
try: deleter.delete_plan(self)
exporter.delete_plan(self)
except ChannelConnectorError as err:
self.create_issue(
backend=self.backend_id.id,
section='restriction',
internal_message=str(err),
channel_message=err.data['message'])
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def import_price_plans(self, backend): def import_price_plans(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='product.pricelist.importer') importer = work.component(usage='product.pricelist.importer')
try: return importer.import_pricing_plans()
return importer.import_pricing_plans()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='pricelist',
internal_message=str(err),
channel_message=err.data['message'])
class ProductPricelist(models.Model): class ProductPricelist(models.Model):
_inherit = 'product.pricelist' _inherit = 'product.pricelist'

View File

@@ -0,0 +1,22 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo.addons.component.core import Component
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo import api
class ProductPricelistDeleter(Component):
_name = 'channel.product.pricelist.deleter'
_inherit = 'hotel.channel.deleter'
_apply_on = ['channel.product.pricelist']
_usage = 'product.pricelist.deleter'
@api.model
def delete_plan(self, binding):
try:
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'])

View File

@@ -3,6 +3,7 @@
import logging 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__) _logger = logging.getLogger(__name__)
@@ -14,15 +15,25 @@ class ProductPricelistExporter(Component):
@api.model @api.model
def rename_plan(self, binding): def rename_plan(self, binding):
return self.backend_adapter.rename_plan( try:
binding.external_id, return self.backend_adapter.rename_plan(
binding.name) binding.external_id,
binding.name)
@api.model except ChannelConnectorError as err:
def delete_plan(self, binding): self.create_issue(
return self.backend_adapter.delete_plan(binding.external_id) 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):
external_id = self.backend_adapter.create_plan(binding.name) try:
binding.external_id = external_id 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)

View File

@@ -5,6 +5,7 @@ import logging
from datetime import datetime, timedelta 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.addons.connector.components.mapper import mapping, only_create
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo.addons.hotel_channel_connector.components.backend_adapter import ( from odoo.addons.hotel_channel_connector.components.backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT)
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
@@ -20,27 +21,34 @@ class ProductPricelistImporter(Component):
@api.model @api.model
def import_pricing_plans(self): def import_pricing_plans(self):
channel_product_listprice_obj = self.env['channel.product.pricelist']
pricelist_mapper = self.component(usage='import.mapper',
model_name='channel.product.pricelist')
results = self.backend_adapter.get_pricing_plans()
count = 0 count = 0
for plan in results: try:
if 'vpid' in plan: results = self.backend_adapter.get_pricing_plans()
continue # FIXME: Ignore Virtual Plans except ChannelConnectorError as err:
plan_record = pricelist_mapper.map_record(plan) self.create_issue(
plan_bind = channel_product_listprice_obj.search([ section='pricelist',
('external_id', '=', str(plan['id'])) internal_message=str(err),
], limit=1) channel_message=err.data['message'])
if not plan_bind: else:
channel_product_listprice_obj.with_context({ channel_product_listprice_obj = self.env['channel.product.pricelist']
'connector_no_export': True, pricelist_mapper = self.component(usage='import.mapper',
}).create(plan_record.values(for_create=True)) model_name='channel.product.pricelist')
else: for plan in results:
channel_product_listprice_obj.with_context({ if 'vpid' in plan:
'connector_no_export': True, continue # FIXME: Ignore Virtual Plans
}).write(plan_record.values()) plan_record = pricelist_mapper.map_record(plan)
count = count + 1 plan_bind = channel_product_listprice_obj.search([
('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 return count

View File

@@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError
from odoo.addons.queue_job.job import job, related_action 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
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
class ChannelProductPricelistItem(models.Model): class ChannelProductPricelistItem(models.Model):
_name = 'channel.product.pricelist.item' _name = 'channel.product.pricelist.item'
@@ -23,42 +23,24 @@ class ChannelProductPricelistItem(models.Model):
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def import_pricelist_values(self, backend): def import_pricelist_values(self, backend, dfrom, dto, external_id):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
importer = work.component(usage='product.pricelist.item.importer') importer = work.component(usage='product.pricelist.item.importer')
try: 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,
backend.pricelist_to)
return importer.import_pricelist_values(
backend.pricelist_id.external_id,
backend.pricelist_from, backend.pricelist_from,
backend.pricelist_to) backend.pricelist_to)
except ChannelConnectorError as err: return importer.import_pricelist_values(
self.create_issue( backend.pricelist_id.external_id,
backend=backend.id, backend.pricelist_from,
section='pricelist', backend.pricelist_to)
internal_message=str(err),
channel_message=err.data['message'],
channel_object_id=backend.pricelist_id.external_id,
dfrom=backend.pricelist_from,
dto=backend.pricelist_to)
return False
@job(default_channel='root.channel') @job(default_channel='root.channel')
@api.model @api.model
def push_pricelist(self, backend): def push_pricelist(self, backend):
with backend.work_on(self._name) as work: with backend.work_on(self._name) as work:
exporter = work.component(usage='product.pricelist.item.exporter') exporter = work.component(usage='product.pricelist.item.exporter')
try: return exporter.push_pricelist()
return exporter.push_pricelist()
except ChannelConnectorError as err:
self.create_issue(
backend=backend.id,
section='pricelist',
internal_message=str(err),
channel_message=err.data['message'])
class ProductPricelistItem(models.Model): class ProductPricelistItem(models.Model):
_inherit = 'product.pricelist.item' _inherit = 'product.pricelist.item'

View File

@@ -4,6 +4,7 @@
import logging import logging
from datetime import datetime, timedelta 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.addons.hotel_channel_connector.components.backend_adapter import ( from odoo.addons.hotel_channel_connector.components.backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT)
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
@@ -26,10 +27,11 @@ class ProductPricelistItemExporter(Component):
('date_start', '>=', datetime.now().strftime( ('date_start', '>=', datetime.now().strftime(
DEFAULT_SERVER_DATE_FORMAT)) DEFAULT_SERVER_DATE_FORMAT))
], order="date_start ASC") ], order="date_start ASC")
if any(channel_unpushed): if any(channel_unpushed):
date_start = fields.Date.from_string(channel_unpushed[0].date_start) date_start = fields.Date.from_string(channel_unpushed[0].date_start)
date_end = fields.Date.from_string(channel_unpushed[-1].date_start) date_end = fields.Date.from_string(channel_unpushed[-1].date_start)
days_diff = (date_start - date_end).days + 1 days_diff = (date_end - date_start).days + 1
prices = {} prices = {}
pricelist_ids = channel_product_pricelist_obj.search([ pricelist_ids = channel_product_pricelist_obj.search([
@@ -39,7 +41,7 @@ class ProductPricelistItemExporter(Component):
for pr in pricelist_ids: for pr in pricelist_ids:
prices.update({pr.external_id: {}}) prices.update({pr.external_id: {}})
unpushed_pl = channel_product_pricelist_item_obj.search( unpushed_pl = channel_product_pricelist_item_obj.search(
[('channel_pushed', '=', False), ('pricelist_id', '=', pr.id)]) [('channel_pushed', '=', False), ('pricelist_id', '=', pr.odoo_id.id)])
product_tmpl_ids = unpushed_pl.mapped('product_tmpl_id') product_tmpl_ids = unpushed_pl.mapped('product_tmpl_id')
for pt_id in product_tmpl_ids: for pt_id in product_tmpl_ids:
channel_room_type = channel_room_type_obj.search([ channel_room_type = channel_room_type_obj.search([
@@ -50,17 +52,23 @@ class ProductPricelistItemExporter(Component):
for i in range(0, days_diff): for i in range(0, days_diff):
prod = channel_room_type.product_id.with_context({ prod = channel_room_type.product_id.with_context({
'quantity': 1, 'quantity': 1,
'pricelist': pr.id, 'pricelist': pr.odoo_id.id,
'date': (date_start + timedelta(days=i)). 'date': (date_start + timedelta(days=i)).
strftime(DEFAULT_SERVER_DATE_FORMAT), strftime(DEFAULT_SERVER_DATE_FORMAT),
}) })
prices[pr.external_id][channel_room_type.external_id].append(prod.price) prices[pr.external_id][channel_room_type.external_id].append(prod.price)
_logger.info("==[ODOO->CHANNEL]==== PRICELISTS ==") _logger.info("==[ODOO->CHANNEL]==== PRICELISTS ==")
_logger.info(prices) _logger.info(prices)
for k_pk, v_pk in prices.items(): try:
if any(v_pk): for k_pk, v_pk in prices.items():
self.backend_adapter.update_plan_prices(k_pk, date_start.strftime( if any(v_pk):
DEFAULT_SERVER_DATE_FORMAT), v_pk) self.backend_adapter.update_plan_prices(k_pk, date_start.strftime(
DEFAULT_SERVER_DATE_FORMAT), v_pk)
channel_unpushed.write({'channel_pushed': True}) 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 return True

View File

@@ -5,6 +5,7 @@ import logging
from datetime import timedelta 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.addons.connector.components.mapper import mapping, only_create
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
from odoo.addons.hotel_channel_connector.components.backend_adapter import ( from odoo.addons.hotel_channel_connector.components.backend_adapter import (
DEFAULT_WUBOOK_DATE_FORMAT) DEFAULT_WUBOOK_DATE_FORMAT)
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
@@ -64,9 +65,10 @@ class ProductPricelistItemImporter(Component):
'connector_no_export': True, 'connector_no_export': True,
}).write(map_record.values()) }).write(map_record.values())
else: else:
channel_pricelist_item_obj.with_context({ pricelist_item = channel_pricelist_item_obj.with_context({
'connector_no_export': True, 'connector_no_export': True,
}).create(map_record.values(for_create=True)) }).create(map_record.values(for_create=True))
pricelist_item.channel_pushed = True
return True return True
@api.model @api.model
@@ -79,12 +81,22 @@ class ProductPricelistItemImporter(Component):
@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):
results = self.backend_adapter.fetch_plan_prices( try:
external_id, results = self.backend_adapter.fetch_plan_prices(
date_from, external_id,
date_to, date_from,
rooms) date_to,
self._generate_pricelist_items(external_id, date_from, date_to, results) 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): class ProductPricelistItemImportMapper(Component):
_name = 'channel.product.pricelist.item.import.mapper' _name = 'channel.product.pricelist.item.import.mapper'

View File

@@ -1,187 +0,0 @@
odoo.define('hotel_channel_connector.ListController', function(require) {
'use strict';
/*
* Hotel Channel Connector
* GNU Public License
* Alexandre Díaz <dev@redneboa.es>
*/
var ListController = require('web.ListController');
var Core = require('web.core');
var _t = Core._t;
ListController.include({
renderButtons: function () {
this._super.apply(this, arguments); // Sets this.$buttons
if (this.modelName === 'hotel.room.type') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_rooms' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_rooms').on('click', this._importRooms.bind(this));
} else if (this.modelName === 'hotel.folio') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_reservations' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_reservations').on('click', this._importReservations.bind(this));
} else if (this.modelName === 'product.pricelist') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_price_plans' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_price_plans').on('click', this._importPricePlans.bind(this));
this.$buttons.append("<button class='btn btm-sm btn-danger o_channel_connector_push_price_plans' type='button'>"+_t('Push to Channel')+"</button>");
this.$buttons.find('.o_channel_connector_push_price_plans').on('click', this._pushPricePlans.bind(this));
} else if (this.modelName === 'wubook.channel.info') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_channels_info' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_channels_info').on('click', this._importChannelsInfo.bind(this));
} else if (this.modelName === 'hotel.room.type.restriction') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_restriction_plans' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_restriction_plans').on('click', this._importRestrictionPlans.bind(this));
this.$buttons.append("<button class='btn btm-sm btn-danger o_channel_connector_push_restriction_plans' type='button'>"+_t('Push to Channel')+"</button>");
this.$buttons.find('.o_channel_connector_push_restriction_plans').on('click', this._pushRestrictionPlans.bind(this));
} else if (this.modelName === 'hotel.room.type.availability') {
this.$buttons.append("<button class='btn btm-sm o_channel_connector_import_availability' type='button'>"+_t('Fetch from Channel')+"</button>");
this.$buttons.find('.o_channel_connector_import_availability').on('click', this._importAvailability.bind(this));
this.$buttons.append("<button class='btn btm-sm btn-danger o_channel_connector_push_availability' type='button'>"+_t('Push to Channel')+"</button>");
this.$buttons.find('.o_channel_connector_push_availability').on('click', this._pushAvailability.bind(this));
}
},
_importRooms: function () {
var self = this;
this.dataset._model.call('import_rooms', [false]).then(function(results){
if (!results[0]) {
self.do_warn(_t('Operation Errors'), _t('Errors while importing rooms. See issues registry.'), true);
}
if (results[0] || results[1] > 0) {
if (results[1] > 0) {
self.do_notify(_t('Operation Success'), `<b>${results[1]}</b>` + ' ' + _t('Rooms successfully imported'), false);
} else {
self.do_notify(_t('Operation Success'), _t('No new rooms found. Everything is done.'), false);
}
var active_view = self.ViewManager.active_view;
active_view.controller.reload(); // list view only has reload
}
});
return false;
},
_importReservations: function () {
var self = this;
console.log(this);
this.model.import_reservations().then(function(results){
console.log(results);
if (!results[0]) {
self.do_warn(_t('Operation Errors'), _t('Errors while importing reservations. See issues registry.'), true);
}
if (results[0] || results[1] > 0) {
if (results[1] > 0) {
self.do_notify(_t('Operation Success'), `<b>${results[1]}</b>` + ' ' + _t('Reservations successfully imported'), false);
} else {
self.do_notify(_t('Operation Success'), _t('No new reservations found. Everything is done.'), false);
}
var active_view = self.ViewManager.active_view;
active_view.controller.reload(); // list view only has reload
}
});
return false;
},
_importPricePlans: function () {
var self = this;
this.dataset._model.call('import_price_plans', [false]).then(function(results){
if (!results[0]) {
self.do_warn(_t('Operation Errors'), _t('Errors while importing price plans from WuBook. See issues log.'), true);
}
if (results[0] || results[1] > 0) {
if (results[1] > 0) {
self.do_notify(_t('Operation Success'), `<b>${results[1]}</b>` + ' ' + _t('Price Plans successfully imported'), false);
} else {
self.do_notify(_t('Operation Success'), _t('No new price plans found. Everything is done.'), false);
}
var active_view = self.ViewManager.active_view;
active_view.controller.reload(); // list view only has reload
}
});
return false;
},
_pushPricePlans: function () {
var self = this;
new Model('wubook').call('push_priceplans', [false]).then(function(results){
self.do_notify(_t('Operation Success'), _t('Price Plans successfully pushed'), false);
}).fail(function(){
self.do_warn(_t('Operation Errors'), _t('Errors while pushing price plans to WuBook. See issues log.'), true);
});
return false;
},
_importChannelsInfo: function () {
var self = this;
this.dataset._model.call('import_channels_info', [false]).then(function(results){
if (!results[0]) {
self.do_warn(_t('Operation Errors'), _t('Errors while importing channels info from WuBook. See issues log.'), true);
}
if (results[0] || results[1] > 0) {
if (results[1] > 0) {
self.do_notify(_t('Operation Success'), `<b>${results[1]}</b>` + ' ' + _t('Channels Info successfully imported'), false);
} else {
self.do_notify(_t('Operation Success'), _t('No new channels info found. Everything is done.'), false);
}
var active_view = self.ViewManager.active_view;
active_view.controller.reload(); // list view only has reload
}
});
return false;
},
_importRestrictionPlans: function () {
var self = this;
this.dataset._model.call('import_restriction_plans', [false]).then(function(results){
if (!results[0]) {
self.do_warn(_t('Operation Errors'), _t('Errors while importing restriction plans from WuBook. See issues log.'), true);
}
if (results[0] || results[1] > 0) {
if (results[1] > 0) {
self.do_notify(_t('Operation Success'), `<b>${results[1]}</b>` + ' ' + _t('Restriction Plans successfully imported'), false);
} else {
self.do_notify(_t('Operation Success'), _t('No new restriction plans found. Everything is done.'), false);
}
var active_view = self.ViewManager.active_view;
active_view.controller.reload(); // list view only has reload
}
});
return false;
},
_pushRestrictionPlans: function () {
var self = this;
new Model('wubook').call('push_restrictions', [false]).then(function(results){
self.do_notify(_t('Operation Success'), _t('Restrictions successfully pushed'), false);
}).fail(function(){
self.do_warn(_t('Operation Errors'), _t('Errors while pushing restrictions to WuBook. See issues log.'), true);
});
return false;
},
_importAvailability: function () {
this.do_action('hotel_wubook_proto.action_wubook_import_availability');
return false;
},
_pushAvailability: function () {
var self = this;
new Model('wubook').call('push_availability', [false]).then(function(results){
self.do_notify(_t('Operation Success'), _t('Availability successfully pushed'), false);
}).fail(function(){
self.do_warn(_t('Operation Errors'), _t('Errors while pushing availability to Channel. See issues log.'), true);
});
return false;
}
});
});

View File

@@ -1,25 +0,0 @@
odoo.define('hotel_channel_connector.ListModel', function(require) {
'use strict';
/*
* Hotel Channel Connector
* GNU Public License
* Alexandre Díaz <dev@redneboa.es>
*/
var BasicModel = require('web.BasicModel'),
Session = require('web.session');
return BasicModel.extend({
import_reservations: function() {
return this._rpc({
model: 'hotel.folio',
method: 'import_reservations',
args: undefined,
context: Session.user_context,
});
},
});
});

View File

@@ -1,18 +0,0 @@
odoo.define('hotel_channel_connector.ListView', function(require) {
'use strict';
/*
* Hotel Channel Connector
* GNU Public License
* Alexandre Díaz <dev@redneboa.es>
*/
var ListView = require('web.ListView'),
ListModel = require('hotel_channel_connector.ListModel');
ListView.include({
config: _.extend({}, ListView.prototype.config, {
Model: ListModel,
}),
});
});

View File

@@ -50,6 +50,16 @@
string="Import in background"/> string="Import in background"/>
</div> </div>
</group> </group>
<group>
<label string="Import Reservation" class="oe_inline"/>
<div>
<field name="reservation_id_str" class="oe_inline" nolabel="1"/>
<button name="import_reservation"
type="object"
class="oe_highlight"
string="Import in background"/>
</div>
</group>
<group> <group>
<label string="Import Rooms" class="oe_inline"/> <label string="Import Rooms" class="oe_inline"/>
<div> <div>

View File

@@ -9,8 +9,8 @@
<group> <group>
<field name="external_id" /> <field name="external_id" />
<field name="ota_reservation_id" /> <field name="ota_reservation_id" />
<field name="wstatus" /> <field name="channel_status" />
<field name="wstatus_reason" /> <field name="channel_status_reason" />
<field name="to_read" /> <field name="to_read" />
</group> </group>
</form> </form>

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<!-- Backend stuff -->
<template id="assets_backend" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<script type="text/javascript" src="/hotel_channel_connector/static/src/js/views/list/list_model.js"></script>
<script type="text/javascript" src="/hotel_channel_connector/static/src/js/views/list/list_controller.js"></script>
<script type="text/javascript" src="/hotel_channel_connector/static/src/js/views/list/list_view.js"></script>
</xpath>
</template>
</odoo>