mirror of
https://github.com/OCA/pms.git
synced 2025-01-29 00:17:45 +02:00
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:
@@ -11,6 +11,7 @@
|
||||
'description': "Hotel Channel Connector Base",
|
||||
'depends': [
|
||||
'connector',
|
||||
'web_notify',
|
||||
'hotel',
|
||||
],
|
||||
'external_dependencies': {
|
||||
@@ -18,7 +19,6 @@
|
||||
},
|
||||
'data': [
|
||||
'data/cron_jobs.xml',
|
||||
'views/general.xml',
|
||||
'views/hotel_channel_connector_issue_views.xml',
|
||||
'views/inherited_hotel_reservation_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_item_views.xml',
|
||||
'views/inherited_res_partner_views.xml',
|
||||
'views/channel_ota_info_views.xml',
|
||||
'views/channel_hotel_reservation_views.xml',
|
||||
'views/channel_hotel_room_type_views.xml',
|
||||
'views/channel_hotel_room_type_availability_views.xml',
|
||||
@@ -38,6 +37,7 @@
|
||||
'views/channel_product_pricelist_views.xml',
|
||||
'views/channel_product_pricelist_item_views.xml',
|
||||
'views/channel_connector_backend_views.xml',
|
||||
'views/channel_ota_info_views.xml',
|
||||
'wizard/inherited_massive_changes.xml',
|
||||
'data/menus.xml',
|
||||
'data/sequences.xml',
|
||||
|
||||
@@ -6,4 +6,5 @@ from . import backend_adapter
|
||||
from . import binder
|
||||
from . import importer
|
||||
from . import exporter
|
||||
from . import deleter
|
||||
from . import mapper
|
||||
|
||||
@@ -361,7 +361,7 @@ class WuBookAdapter(AbstractComponent):
|
||||
return results
|
||||
|
||||
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[1],
|
||||
channel_reservation_id)
|
||||
@@ -372,10 +372,6 @@ class WuBookAdapter(AbstractComponent):
|
||||
return results
|
||||
|
||||
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(
|
||||
self._session_info[0],
|
||||
self._session_info[1],
|
||||
|
||||
@@ -9,6 +9,18 @@ class BaseHotelChannelConnectorComponent(AbstractComponent):
|
||||
_inherit = 'base.connector'
|
||||
_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):
|
||||
def __init__(self, message, data):
|
||||
super().__init__(message)
|
||||
|
||||
9
hotel_channel_connector/components/deleter.py
Normal file
9
hotel_channel_connector/components/deleter.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.addons.component.core import AbstractComponent
|
||||
|
||||
class HotelChannelConnectorDeleter(AbstractComponent):
|
||||
_name = 'hotel.channel.deleter'
|
||||
_inherit = ['base.deleter', 'base.hotel.channel.connector']
|
||||
_usage = 'channel.deleter'
|
||||
@@ -1,784 +1,9 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
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__)
|
||||
from odoo.addons.component.core import AbstractComponent
|
||||
|
||||
class HotelChannelConnectorImporter(AbstractComponent):
|
||||
_name = 'hotel.channel.importer'
|
||||
_inherit = ['base.importer', 'base.hotel.channel.connector']
|
||||
_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)
|
||||
|
||||
@@ -39,9 +39,7 @@ class website_wubook(http.Controller):
|
||||
if not backend:
|
||||
raise ValidationError(_("Can't found a backend!"))
|
||||
|
||||
_logger.info(_("[WUBOOK->ODOO] Importing Booking..."))
|
||||
# Create Reservation
|
||||
request.env['wubook'].sudo().fetch_booking(lcode, rcode)
|
||||
request.env['channel.hotel.reservation'].import_reservation(rcode)
|
||||
|
||||
return request.make_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
|
||||
@@ -72,18 +70,12 @@ class website_wubook(http.Controller):
|
||||
odoo_dto = datetime.strptime(
|
||||
dto,
|
||||
DEFAULT_WUBOOK_DATE_FORMAT).strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
backend.write({
|
||||
'avail_from': odoo_dfrom,
|
||||
'avail_to': odoo_dto,
|
||||
'restriction_id': False,
|
||||
'restriction_from': odoo_dfrom,
|
||||
'restriction_to': odoo_dto,
|
||||
'pricelist_id': False,
|
||||
'pricelist_from': odoo_dfrom,
|
||||
'pricelist_to': odoo_dto,
|
||||
})
|
||||
backend.import_availability()
|
||||
backend.import_restriction()
|
||||
backend.import_pricelist()
|
||||
|
||||
request.env['channel.hotel.room.type.availability'].import_availability(
|
||||
backend, odoo_dfrom, odoo_dto)
|
||||
request.env['channel.hotel.room.type.restriction.item'].import_restriction_values(
|
||||
backend, odoo_dfrom, odoo_dto, False)
|
||||
request.env['channel.product.pricelist.item'].import_pricelist_values(
|
||||
backend, odoo_dfrom, odoo_dto, False)
|
||||
|
||||
return request.make_response('200 OK', [('Content-Type', 'text/plain')])
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
<odoo noupdate="1">
|
||||
<!--record model="ir.cron" id="wubook_push_avail">
|
||||
<field name="name">WuBook Push Availability</field>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<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_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="state">code</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_wubook" />
|
||||
<field name="code">model.push_changes()</field>
|
||||
</record-->
|
||||
<field name="model_id" ref="model_channel_backend" />
|
||||
<field name="code">model.cron_push_changes()</field>
|
||||
</record>
|
||||
|
||||
<!-- <record id="wubook_corporate_fetch" model="ir.cron">
|
||||
<field name="name">WuBook Corporate Fetchable Properties</field>
|
||||
<field name="active" eval="True" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">4</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doal">1</field>
|
||||
<field name="nextcall" >2016-12-31 23:59:59</field>
|
||||
<field name="model" eval="'wubook'" />
|
||||
<field name="function" eval="'corporate_fetch'" />
|
||||
<field name="args" eval="" />
|
||||
<field name="priority" eval="5" />
|
||||
</record> -->
|
||||
<record model="ir.cron" id="channel_connector_fetch_new_bookings">
|
||||
<field name="name">Channel Connector Fetch New Bookings</field>
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="state">code</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model_id" ref="model_channel_backend" />
|
||||
<field name="code">model.cron_import_reservations()</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
import os
|
||||
import binascii
|
||||
from contextlib import contextmanager
|
||||
from odoo import models, api, fields
|
||||
from ...components.backend_adapter import WuBookLogin, WuBookServer
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ChannelBackend(models.Model):
|
||||
_name = 'channel.backend'
|
||||
@@ -31,18 +33,22 @@ class ChannelBackend(models.Model):
|
||||
pkey = fields.Char('Channel Service PKey')
|
||||
security_token = fields.Char('Channel Service Security Token')
|
||||
|
||||
avail_from = fields.Date('Availability From')
|
||||
avail_to = fields.Date('Availability To')
|
||||
reservation_id_str = fields.Char('Channel Reservation ID', store=False)
|
||||
|
||||
restriction_from = fields.Date('Restriction From')
|
||||
restriction_to = fields.Date('Restriction To')
|
||||
avail_from = fields.Date('Availability From', store=False)
|
||||
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',
|
||||
'Channel Restriction')
|
||||
'Channel Restriction',
|
||||
store=False)
|
||||
|
||||
pricelist_from = fields.Date('Pricelist From')
|
||||
pricelist_to = fields.Date('Pricelist To')
|
||||
pricelist_from = fields.Date('Pricelist From', store=False)
|
||||
pricelist_to = fields.Date('Pricelist To', store=False)
|
||||
pricelist_id = fields.Many2one('channel.product.pricelist',
|
||||
'Channel Product Pricelist')
|
||||
'Channel Product Pricelist',
|
||||
store=False)
|
||||
|
||||
issue_ids = fields.One2many('hotel.channel.connector.issue',
|
||||
'backend_id',
|
||||
@@ -60,84 +66,159 @@ class ChannelBackend(models.Model):
|
||||
def import_reservations(self):
|
||||
channel_hotel_reservation_obj = self.env['channel.hotel.reservation']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_rooms(self):
|
||||
channel_hotel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_otas_info(self):
|
||||
channel_ota_info_obj = self.env['channel.ota.info']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_availability(self):
|
||||
channel_hotel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def push_availability(self):
|
||||
channel_hotel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_restriction_plans(self):
|
||||
channel_hotel_room_type_restr_obj = self.env['channel.hotel.room.type.restriction']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_restriction_values(self):
|
||||
channel_hotel_restr_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def push_restriction(self):
|
||||
channel_hotel_restr_item_obj = self.env['channel.hotel.room.type.restriction.item']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_pricelist_plans(self):
|
||||
channel_product_pricelist_obj = self.env['channel.product.pricelist']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def import_pricelist_values(self):
|
||||
channel_product_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def push_pricelist(self):
|
||||
channel_product_pricelist_item_obj = self.env['channel.product.pricelist.item']
|
||||
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
|
||||
|
||||
@api.multi
|
||||
def push_changes(self):
|
||||
self.push_availability()
|
||||
self.push_restriction()
|
||||
self.push_pricelist()
|
||||
@api.model
|
||||
def cron_push_changes(self):
|
||||
_logger.info("======== PASA POR AKI!! AAAAA")
|
||||
backends = self.env[self._name].search([])
|
||||
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
|
||||
@api.multi
|
||||
@@ -153,163 +234,3 @@ class ChannelBackend(models.Model):
|
||||
_super = super(ChannelBackend, self)
|
||||
with _super.work_on(model_name, channel_api=channel_api, **kwargs) as 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()
|
||||
|
||||
@@ -22,15 +22,3 @@ class ChannelBinding(models.AbstractModel):
|
||||
('channel_uniq', 'unique(backend_id, external_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),
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from odoo import api, models, fields
|
||||
from odoo.addons.queue_job.job import job
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
|
||||
|
||||
class ChannelOtaInfo(models.Model):
|
||||
_name = 'channel.ota.info'
|
||||
@@ -20,14 +20,7 @@ class ChannelOtaInfo(models.Model):
|
||||
def import_otas_info(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
importer = work.component(usage='ota.info.importer')
|
||||
try:
|
||||
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'])
|
||||
return importer.import_otas_info()
|
||||
|
||||
class HotelRoomTypeAdapter(Component):
|
||||
_name = 'channel.ota.info.adapter'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.connector.components.mapper import mapping
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import fields, api, _
|
||||
from odoo.tools import (
|
||||
DEFAULT_SERVER_DATE_FORMAT,
|
||||
@@ -18,31 +19,37 @@ class ChannelOtaInfoImporter(Component):
|
||||
|
||||
@api.model
|
||||
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
|
||||
for ota_id in results.keys():
|
||||
vals = {
|
||||
'id': ota_id,
|
||||
'name': results[ota_id]['name'],
|
||||
'ical': results[ota_id]['ical'] == 1,
|
||||
}
|
||||
map_record = ota_info_mapper.map_record(vals)
|
||||
ota_info_bind = channel_ota_info_obj.search([
|
||||
('ota_id', '=', ota_id)
|
||||
], limit=1)
|
||||
if ota_info_bind:
|
||||
ota_info_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
ota_info_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).create(map_record.values(for_create=True))
|
||||
count = count + 1
|
||||
try:
|
||||
results = self.backend_adapter.get_channels_info()
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='room',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
channel_ota_info_obj = self.env['channel.ota.info']
|
||||
ota_info_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.ota.info')
|
||||
for ota_id in results.keys():
|
||||
vals = {
|
||||
'id': ota_id,
|
||||
'name': results[ota_id]['name'],
|
||||
'ical': results[ota_id]['ical'] == 1,
|
||||
}
|
||||
map_record = ota_info_mapper.map_record(vals)
|
||||
ota_info_bind = channel_ota_info_obj.search([
|
||||
('ota_id', '=', ota_id)
|
||||
], limit=1)
|
||||
if ota_info_bind:
|
||||
ota_info_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
ota_info_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).create(map_record.values(for_create=True))
|
||||
count = count + 1
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class HotelChannelConnectorIssue(models.Model):
|
||||
_old_name = 'wubook.issue'
|
||||
|
||||
backend_id = fields.Many2one('channel.backend',
|
||||
'Restriction Plan',
|
||||
'Backend',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True)
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
from . import common
|
||||
from . import importer
|
||||
from . import exporter
|
||||
|
||||
@@ -34,19 +34,37 @@ class ChannelHotelReservation(models.Model):
|
||||
old_name='wchannel_reservation_code')
|
||||
channel_raw_data = fields.Text(readonly=True, old_name='wbook_json')
|
||||
|
||||
wstatus = fields.Selection([
|
||||
channel_status = fields.Selection([
|
||||
('0', 'No Channel'),
|
||||
(str(WUBOOK_STATUS_CONFIRMED), 'Confirmed'),
|
||||
(str(WUBOOK_STATUS_WAITING), 'Waiting'),
|
||||
(str(WUBOOK_STATUS_REFUSED), 'Refused'),
|
||||
(str(WUBOOK_STATUS_ACCEPTED), 'Accepted'),
|
||||
(str(WUBOOK_STATUS_CANCELLED), 'Cancelled'),
|
||||
(str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty')],
|
||||
string='WuBook Status',
|
||||
default='0',
|
||||
readonly=True)
|
||||
wstatus_reason = fields.Char("WuBook Status Reason", readonly=True)
|
||||
wmodified = fields.Boolean("WuBook Modified", readonly=True, default=False)
|
||||
(str(WUBOOK_STATUS_CANCELLED_PENALTY), 'Cancelled with penalty'),
|
||||
], string='Channel Status', default='0', readonly=True, old_name='wstatus')
|
||||
channel_status_reason = fields.Char("Channel Status Reason", readonly=True,
|
||||
old_name='wstatus_reason')
|
||||
channel_modified = fields.Boolean("Channel Modified", readonly=True,
|
||||
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')
|
||||
@api.model
|
||||
@@ -55,36 +73,19 @@ class ChannelHotelReservation(models.Model):
|
||||
importer = work.component(usage='hotel.reservation.importer')
|
||||
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')
|
||||
@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
|
||||
def cancel_reservation(self):
|
||||
self.ensure_one()
|
||||
if self._context.get('channel_action', True):
|
||||
user = self.env['res.user'].browse(self.env.uid)
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
adapter = work.component(usage='backend.adapter')
|
||||
wres = adapter.cancel_reservation(
|
||||
self.channel_reservation_id,
|
||||
_('Cancelled by %s') % user.partner_id.name)
|
||||
if not wres:
|
||||
raise ValidationError(_("Can't cancel reservation on WuBook"))
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.reservation.exporter')
|
||||
return exporter.cancel_reservation(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.multi
|
||||
def mark_booking(self):
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.reservation.exporter')
|
||||
return exporter.mark_booking(self)
|
||||
|
||||
class HotelReservation(models.Model):
|
||||
_inherit = 'hotel.reservation'
|
||||
@@ -100,11 +101,14 @@ class HotelReservation(models.Model):
|
||||
for record in self:
|
||||
if not record.channel_type:
|
||||
record.channel_type = 'door'
|
||||
record.origin_sale = dict(
|
||||
self.fields_get(
|
||||
allfields=['channel_type'])['channel_type']['selection'])[record.channel_type] \
|
||||
if record.channel_type != 'web' or not record.channel_bind_ids[0].ota_id \
|
||||
else record.channel_bind_ids[0].ota_id.name
|
||||
|
||||
if record.channel_type == 'web' and any(record.channel_bind_ids) and \
|
||||
record.channel_bind_ids[0].ota_id:
|
||||
record.origin_sale = 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(
|
||||
comodel_name='channel.hotel.reservation',
|
||||
@@ -124,17 +128,12 @@ class HotelReservation(models.Model):
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
if vals.get('channel_reservation_id') != None:
|
||||
if vals.get('external_id') is not None:
|
||||
vals.update({'preconfirm': False})
|
||||
user = self.env['res.users'].browse(self.env.uid)
|
||||
if user.has_group('hotel.group_hotel_call'):
|
||||
vals.update({'to_read': True})
|
||||
res = super(HotelReservation, self).create(vals)
|
||||
self.env['hotel.room.type.availability'].refresh_availability(
|
||||
vals['checkin'],
|
||||
vals['checkout'],
|
||||
vals['product_id'])
|
||||
return res
|
||||
return super(HotelReservation, self).create(vals)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
@@ -198,8 +197,8 @@ class HotelReservation(models.Model):
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
waction = self._context.get('wubook_action', True)
|
||||
if waction:
|
||||
no_export = self._context.get('connector_no_export', True)
|
||||
if no_export:
|
||||
for record in self:
|
||||
# Can't cancel in Odoo
|
||||
if record.is_from_ota:
|
||||
@@ -209,12 +208,13 @@ class HotelReservation(models.Model):
|
||||
self.write({'to_read': True, 'to_assign': True})
|
||||
|
||||
res = super(HotelReservation, self).action_cancel()
|
||||
if waction:
|
||||
if no_export:
|
||||
for record in self:
|
||||
# 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 \
|
||||
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)
|
||||
return res
|
||||
|
||||
@@ -222,7 +222,8 @@ class HotelReservation(models.Model):
|
||||
def confirm(self):
|
||||
can_confirm = True
|
||||
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
|
||||
break
|
||||
if not can_confirm:
|
||||
@@ -276,22 +277,39 @@ class HotelReservationAdapter(Component):
|
||||
_inherit = 'wubook.adapter'
|
||||
_apply_on = 'channel.hotel.reservation'
|
||||
|
||||
def mark_bookings(self, channel_reservation_ids):
|
||||
return super(HotelReservationAdapter, self).mark_bookings(
|
||||
channel_reservation_ids)
|
||||
|
||||
def fetch_new_bookings(self):
|
||||
return super(HotelReservationAdapter, self).fetch_new_bookings()
|
||||
|
||||
def fetch_booking(self, channel_reservation_id):
|
||||
return super(HotelReservationAdapter, self).fetch_booking(
|
||||
channel_reservation_id)
|
||||
|
||||
def cancel_reservation(self, channel_reservation_id, message):
|
||||
return super(HotelReservationAdapter, self).cancel_reservation(
|
||||
channel_reservation_id, message)
|
||||
|
||||
class ChannelBindingHotelReservationListener(Component):
|
||||
_name = 'channel.binding.hotel.reservation.listener'
|
||||
_inherit = 'base.connector.listener'
|
||||
_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))
|
||||
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))
|
||||
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))
|
||||
def on_record_cancel(self, record, fields=None):
|
||||
record.with_delay(priority=20).cancel_reservation()
|
||||
record.cancel_reservation()
|
||||
|
||||
48
hotel_channel_connector/models/hotel_reservation/exporter.py
Normal file
48
hotel_channel_connector/models/hotel_reservation/exporter.py
Normal 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'])
|
||||
@@ -1,13 +1,22 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from dateutil import tz
|
||||
from odoo.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.tools import (
|
||||
DEFAULT_SERVER_DATE_FORMAT,
|
||||
DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
||||
DEFAULT_WUBOOK_DATE_FORMAT,
|
||||
DEFAULT_WUBOOK_DATETIME_FORMAT,
|
||||
WUBOOK_STATUS_BAD)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HotelReservationImporter(Component):
|
||||
@@ -16,30 +25,424 @@ class HotelReservationImporter(Component):
|
||||
_apply_on = ['channel.hotel.reservation']
|
||||
_usage = 'hotel.reservation.importer'
|
||||
|
||||
def fetch_new_bookings(self):
|
||||
@api.model
|
||||
def fetch_booking(self, channel_reservation_id):
|
||||
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 = \
|
||||
self._generate_reservations(results)
|
||||
if any(processed_rids):
|
||||
uniq_rids = 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))
|
||||
self.backend_adapter.mark_bookings(list(set(processed_rids)))
|
||||
# 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:
|
||||
self.backend_adapter.fetch_rooms_values(
|
||||
checkin_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
checkout_utc_dt.strftime(DEFAULT_SERVER_DATE_FORMAT))
|
||||
return True
|
||||
|
||||
def fetch_new_bookings(self):
|
||||
count = 0
|
||||
try:
|
||||
results = self.backend_adapter.fetch_new_bookings()
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
backend=self.backend_adapter.id,
|
||||
section='reservation',
|
||||
internal_message=_("Can't process reservations from wubook"),
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
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)
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import common
|
||||
from . import importer
|
||||
from . import exporter
|
||||
from . import deleter
|
||||
|
||||
@@ -7,7 +7,6 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ChannelHotelRoomType(models.Model):
|
||||
@@ -37,14 +36,7 @@ class ChannelHotelRoomType(models.Model):
|
||||
def import_rooms(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
importer = work.component(usage='hotel.room.type.importer')
|
||||
try:
|
||||
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'])
|
||||
return importer.get_rooms()
|
||||
|
||||
@api.constrains('ota_capacity')
|
||||
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"))
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def create_room(self):
|
||||
self.ensure_one()
|
||||
if not self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.create_room(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def modify_room(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.modify_room(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def delete_room(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.exporter')
|
||||
try:
|
||||
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'])
|
||||
deleter = work.component(usage='hotel.room.type.deleter')
|
||||
deleter.delete_room(self)
|
||||
|
||||
class HotelRoomType(models.Model):
|
||||
_inherit = 'hotel.room.type'
|
||||
@@ -158,9 +126,20 @@ class HotelRoomTypeAdapter(Component):
|
||||
_inherit = 'wubook.adapter'
|
||||
_apply_on = 'channel.hotel.room.type'
|
||||
|
||||
def create_room(self, shortcode, name, capacity, price, availability):
|
||||
return super(HotelRoomTypeAdapter, self).create_room(
|
||||
shortcode, name, capacity, price, availability)
|
||||
|
||||
def fetch_rooms(self):
|
||||
return super(HotelRoomTypeAdapter, self).fetch_rooms()
|
||||
|
||||
def modify_room(self, channel_room_id, name, capacity, price, availability, scode):
|
||||
return super(HotelRoomTypeAdapter, self).modify_room(
|
||||
channel_room_id, name, capacity, price, availability, scode)
|
||||
|
||||
def delete_room(self, channel_room_id):
|
||||
return super(HotelRoomTypeAdapter, self).delete_room(channel_room_id)
|
||||
|
||||
class BindingHotelRoomTypeListener(Component):
|
||||
_name = 'binding.hotel.room.type.listener'
|
||||
_inherit = 'base.connector.listener'
|
||||
|
||||
22
hotel_channel_connector/models/hotel_room_type/deleter.py
Normal file
22
hotel_channel_connector/models/hotel_room_type/deleter.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.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'])
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import logging
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import api, _
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,31 +15,40 @@ class HotelRoomTypeExporter(Component):
|
||||
|
||||
@api.model
|
||||
def modify_room(self, binding):
|
||||
return self.backend_adapter.modify_room(
|
||||
binding.external_id,
|
||||
binding.name,
|
||||
binding.ota_capacity,
|
||||
binding.list_price,
|
||||
binding.total_rooms_count,
|
||||
binding.channel_short_code)
|
||||
|
||||
@api.model
|
||||
def delete_room(self, binding):
|
||||
return self.backend_adapter.delete_room(binding.external_id)
|
||||
try:
|
||||
return self.backend_adapter.modify_room(
|
||||
binding.external_id,
|
||||
binding.name,
|
||||
binding.ota_capacity,
|
||||
binding.list_price,
|
||||
binding.total_rooms_count,
|
||||
binding.channel_short_code)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='room',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
|
||||
@api.model
|
||||
def create_room(self, binding):
|
||||
seq_obj = self.env['ir.sequence']
|
||||
short_code = seq_obj.next_by_code('hotel.room.type')[:4]
|
||||
external_id = self.backend_adapter.create_room(
|
||||
short_code,
|
||||
binding.name,
|
||||
binding.ota_capacity,
|
||||
binding.list_price,
|
||||
binding.total_rooms_count
|
||||
)
|
||||
binding.write({
|
||||
'external_id': external_id,
|
||||
'channel_short_code': short_code,
|
||||
})
|
||||
self.binder.bind(external_id, binding)
|
||||
try:
|
||||
external_id = self.backend_adapter.create_room(
|
||||
short_code,
|
||||
binding.name,
|
||||
binding.ota_capacity,
|
||||
binding.list_price,
|
||||
binding.total_rooms_count
|
||||
)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='room',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
binding.write({
|
||||
'external_id': external_id,
|
||||
'channel_short_code': short_code,
|
||||
})
|
||||
self.binder.bind(external_id, binding)
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.connector.components.mapper import mapping
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import fields, api, _
|
||||
from odoo.tools import (
|
||||
DEFAULT_SERVER_DATE_FORMAT,
|
||||
@@ -22,123 +23,33 @@ class HotelRoomTypeImporter(Component):
|
||||
|
||||
@api.model
|
||||
def get_rooms(self):
|
||||
results = self.backend_adapter.fetch_rooms()
|
||||
|
||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
room_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.hotel.room.type')
|
||||
count = len(results)
|
||||
for room in results:
|
||||
map_record = room_mapper.map_record(room)
|
||||
room_bind = channel_room_type_obj.search([
|
||||
('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))
|
||||
count = 0
|
||||
try:
|
||||
results = self.backend_adapter.fetch_rooms()
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='room',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
room_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.hotel.room.type')
|
||||
for room in results:
|
||||
map_record = room_mapper.map_record(room)
|
||||
room_bind = channel_room_type_obj.search([
|
||||
('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
|
||||
|
||||
@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):
|
||||
_name = 'channel.hotel.room.type.import.mapper'
|
||||
_inherit = 'channel.import.mapper'
|
||||
|
||||
@@ -8,7 +8,6 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo.addons.hotel_channel_connector.components.backend_adapter import (
|
||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||
|
||||
@@ -42,49 +41,57 @@ class ChannelHotelRoomTypeAvailability(models.Model):
|
||||
than total rooms \
|
||||
count: %d") % record.odoo_id.room_type_id.total_rooms_count)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def update_availability(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.availability.exporter')
|
||||
try:
|
||||
return exporter.update_availability(self)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
backend=backend.id,
|
||||
section='avail',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
@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
|
||||
|
||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
channel_room_type_avail_obj = self.env['hotel.room.type.availability']
|
||||
|
||||
room_type_binds = channel_room_type_obj.search([('product_id', '=', product_id)])
|
||||
for room_type_bind in room_type_binds:
|
||||
if room_type_bind.external_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(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')
|
||||
@api.model
|
||||
def import_availability(self, backend):
|
||||
def import_availability(self, backend, dfrom, dto):
|
||||
with backend.work_on(self._name) as work:
|
||||
importer = work.component(usage='hotel.room.type.availability.importer')
|
||||
try:
|
||||
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'])
|
||||
return importer.import_availability_values(dfrom, dto)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.model
|
||||
def push_availability(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.availability.exporter')
|
||||
try:
|
||||
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'])
|
||||
return exporter.push_availability()
|
||||
|
||||
class HotelRoomTypeAvailability(models.Model):
|
||||
_inherit = 'hotel.room.type.availability'
|
||||
@@ -134,46 +141,6 @@ class HotelRoomTypeAvailability(models.Model):
|
||||
if self.room_type_id:
|
||||
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):
|
||||
_name = 'channel.hotel.room.type.availability.adapter'
|
||||
_inherit = 'wubook.adapter'
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import logging
|
||||
from odoo.addons.component.core import Component
|
||||
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 (
|
||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||
from odoo import api, fields, _
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class HotelRoomTypeAvailabilityExporter(Component):
|
||||
@@ -28,7 +28,6 @@ class HotelRoomTypeAvailabilityExporter(Component):
|
||||
lambda x: x.room_type_id.id == room_type.id)
|
||||
days = []
|
||||
for channel_room_type_avail in channel_room_type_avails:
|
||||
channel_room_type_avail.channel_pushed = True
|
||||
cavail = channel_room_type_avail.avail
|
||||
if channel_room_type_avail.channel_max_avail >= 0 and \
|
||||
cavail > channel_room_type_avail.channel_max_avail:
|
||||
@@ -44,4 +43,14 @@ class HotelRoomTypeAvailabilityExporter(Component):
|
||||
_logger.info("==[ODOO->CHANNEL]==== AVAILABILITY ==")
|
||||
_logger.info(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
|
||||
|
||||
@@ -29,42 +29,52 @@ class HotelRoomTypeAvailabilityImporter(Component):
|
||||
if dto_dt < now_dt:
|
||||
return True
|
||||
|
||||
results = self.backend_adapter.fetch_rooms_values(date_from, date_to)
|
||||
_logger.info("==[CHANNEL->ODOO]==== AVAILABILITY (%s - %s) ==",
|
||||
date_from, date_to)
|
||||
_logger.info(results)
|
||||
count = 0
|
||||
try:
|
||||
results = self.backend_adapter.fetch_rooms_values(date_from, date_to)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='avail',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'],
|
||||
dfrom=date_from, dto=date_to)
|
||||
else:
|
||||
_logger.info("==[CHANNEL->ODOO]==== AVAILABILITY (%s - %s) ==",
|
||||
date_from, date_to)
|
||||
_logger.info(results)
|
||||
|
||||
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
room_avail_mapper = self.component(
|
||||
usage='import.mapper',
|
||||
model_name='channel.hotel.room.type.availability')
|
||||
count = len(results)
|
||||
for room_k, room_v in results.items():
|
||||
iter_day = dfrom_dt
|
||||
channel_room_type = channel_room_type_obj.search([
|
||||
('channel_room_id', '=', room_k)
|
||||
], limit=1)
|
||||
if channel_room_type:
|
||||
for room in room_v:
|
||||
room.update({
|
||||
'room_type_id': channel_room_type.odoo_id.id,
|
||||
'date': fields.Date.to_string(iter_day),
|
||||
})
|
||||
map_record = room_avail_mapper.map_record(room)
|
||||
room_type_avail_bind = channel_room_type_avail_obj.search([
|
||||
('room_type_id', '=', room['room_type_id']),
|
||||
('date', '=', room['date'])
|
||||
], limit=1)
|
||||
if room_type_avail_bind:
|
||||
room_type_avail_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
room_type_avail_bind = channel_room_type_avail_obj.with_context({
|
||||
'connector_no_export': True,
|
||||
}).create(map_record.values(for_create=True))
|
||||
iter_day += timedelta(days=1)
|
||||
channel_room_type_avail_obj = self.env['channel.hotel.room.type.availability']
|
||||
channel_room_type_obj = self.env['channel.hotel.room.type']
|
||||
room_avail_mapper = self.component(
|
||||
usage='import.mapper',
|
||||
model_name='channel.hotel.room.type.availability')
|
||||
for room_k, room_v in results.items():
|
||||
iter_day = dfrom_dt
|
||||
channel_room_type = channel_room_type_obj.search([
|
||||
('channel_room_id', '=', room_k)
|
||||
], limit=1)
|
||||
if channel_room_type:
|
||||
for room in room_v:
|
||||
room.update({
|
||||
'room_type_id': channel_room_type.odoo_id.id,
|
||||
'date': fields.Date.to_string(iter_day),
|
||||
})
|
||||
map_record = room_avail_mapper.map_record(room)
|
||||
room_type_avail_bind = channel_room_type_avail_obj.search([
|
||||
('room_type_id', '=', room['room_type_id']),
|
||||
('date', '=', room['date'])
|
||||
], limit=1)
|
||||
if room_type_avail_bind:
|
||||
room_type_avail_bind.with_context({
|
||||
'connector_no_export': True,
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
room_type_avail_bind = channel_room_type_avail_obj.with_context({
|
||||
'connector_no_export': True,
|
||||
}).create(map_record.values(for_create=True))
|
||||
room_type_avail_bind.channel_pushed = True
|
||||
iter_day += timedelta(days=1)
|
||||
count = count + 1
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import common
|
||||
from . import importer
|
||||
from . import exporter
|
||||
from . import deleter
|
||||
|
||||
@@ -7,7 +7,6 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ChannelHotelRoomTypeRestriction(models.Model):
|
||||
@@ -22,69 +21,38 @@ class ChannelHotelRoomTypeRestriction(models.Model):
|
||||
ondelete='cascade')
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def create_plan(self):
|
||||
self.ensure_one()
|
||||
if not self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.restriction.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.create_rplan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def update_plan_name(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.restriction.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.rename_rplan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def delete_plan(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.restriction.exporter')
|
||||
try:
|
||||
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'])
|
||||
deleter = work.component(usage='hotel.room.type.restriction.deleter')
|
||||
deleter.delete_rplan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.model
|
||||
def import_restriction_plans(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
importer = work.component(usage='hotel.room.type.restriction.importer')
|
||||
try:
|
||||
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'])
|
||||
return importer.import_restriction_plans()
|
||||
|
||||
class HotelRoomTypeRestriction(models.Model):
|
||||
_inherit = 'hotel.room.type.restriction'
|
||||
|
||||
@@ -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'])
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import logging
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import api, _
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,16 +15,25 @@ class HotelRoomTypeRestrictionExporter(Component):
|
||||
|
||||
@api.model
|
||||
def rename_rplan(self, binding):
|
||||
return self.backend_adapter.rename_rplan(
|
||||
binding.external_id,
|
||||
binding.name)
|
||||
|
||||
@api.model
|
||||
def delete_rplan(self, binding):
|
||||
return self.backend_adapter.delete_rplan(binding.external_id)
|
||||
try:
|
||||
return self.backend_adapter.rename_rplan(
|
||||
binding.external_id,
|
||||
binding.name)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='restriction',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
|
||||
@api.model
|
||||
def create_rplan(self, binding):
|
||||
external_id = self.backend_adapter.create_rplan(binding.name)
|
||||
binding.external_id = external_id
|
||||
self.binder.bind(external_id, binding)
|
||||
try:
|
||||
external_id = self.backend_adapter.create_rplan(binding.name)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='restriction',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
binding.external_id = external_id
|
||||
self.binder.bind(external_id, binding)
|
||||
|
||||
@@ -6,6 +6,7 @@ from datetime import timedelta
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.connector.components.mapper import mapping
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import fields, api, _
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -18,24 +19,32 @@ class HotelRoomTypeRestrictionImporter(Component):
|
||||
|
||||
@api.model
|
||||
def import_restriction_plans(self):
|
||||
results = self.backend_adapter.rplan_rplans()
|
||||
channel_restriction_obj = self.env['channel.hotel.room.type.restriction']
|
||||
restriction_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.hotel.room.type.restriction')
|
||||
for plan in results:
|
||||
plan_record = restriction_mapper.map_record(plan)
|
||||
plan_bind = channel_restriction_obj.search([
|
||||
('external_id', '=', str(plan['id']))
|
||||
], limit=1)
|
||||
if not plan_bind:
|
||||
channel_restriction_obj.with_context({
|
||||
'connector_no_export': True,
|
||||
'rules': plan.get('rules'),
|
||||
}).create(plan_record.values(for_create=True))
|
||||
else:
|
||||
plan_bind.with_context({'connector_no_export':True}).write(
|
||||
plan_record.values())
|
||||
count = count + 1
|
||||
count = 0
|
||||
try:
|
||||
results = self.backend_adapter.rplan_rplans()
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='restriction',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
channel_restriction_obj = self.env['channel.hotel.room.type.restriction']
|
||||
restriction_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.hotel.room.type.restriction')
|
||||
for plan in results:
|
||||
plan_record = restriction_mapper.map_record(plan)
|
||||
plan_bind = channel_restriction_obj.search([
|
||||
('external_id', '=', str(plan['id']))
|
||||
], limit=1)
|
||||
if not plan_bind:
|
||||
channel_restriction_obj.with_context({
|
||||
'connector_no_export': True,
|
||||
'rules': plan.get('rules'),
|
||||
}).create(plan_record.values(for_create=True))
|
||||
else:
|
||||
plan_bind.with_context({'connector_no_export':True}).write(
|
||||
plan_record.values())
|
||||
count = count + 1
|
||||
return count
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
|
||||
class ChannelHotelRoomTypeRestrictionItem(models.Model):
|
||||
_name = 'channel.hotel.room.type.restriction.item'
|
||||
@@ -23,36 +22,20 @@ class ChannelHotelRoomTypeRestrictionItem(models.Model):
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@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:
|
||||
importer = work.component(usage='hotel.room.type.restriction.item.importer')
|
||||
try:
|
||||
return importer.import_restriction_values(
|
||||
backend.restriction_from,
|
||||
backend.restriction_to,
|
||||
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)
|
||||
return importer.import_restriction_values(
|
||||
dfrom,
|
||||
dto,
|
||||
channel_restr_id=external_id)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.model
|
||||
def push_restriction(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
exporter = work.component(usage='hotel.room.type.restriction.item.exporter')
|
||||
try:
|
||||
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'])
|
||||
return exporter.push_restriction()
|
||||
|
||||
class HotelRoomTypeRestrictionItem(models.Model):
|
||||
_inherit = 'hotel.room.type.restriction.item'
|
||||
|
||||
@@ -64,12 +64,18 @@ class HotelRoomTypeRestrictionItemExporter(Component):
|
||||
restrictions[rp.external_id][room_type_external_id].append({})
|
||||
_logger.info("==[ODOO->CHANNEL]==== RESTRICTIONS ==")
|
||||
_logger.info(restrictions)
|
||||
for k_res, v_res in restrictions.items():
|
||||
if any(v_res):
|
||||
self.backend_adapter.update_rplan_values(
|
||||
int(k_res),
|
||||
date_start.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
v_res)
|
||||
unpushed.with_context({
|
||||
'wubook_action': False}).write({'channel_pushed': True})
|
||||
try:
|
||||
for k_res, v_res in restrictions.items():
|
||||
if any(v_res):
|
||||
self.backend_adapter.update_rplan_values(
|
||||
int(k_res),
|
||||
date_start.strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
v_res)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='restriction',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
unpushed.write({'channel_pushed': True})
|
||||
return True
|
||||
|
||||
@@ -60,19 +60,29 @@ class HotelRoomTypeRestrictionImporter(Component):
|
||||
'connector_no_export': True
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
channel_restriction_item_obj.with_context({
|
||||
channel_restriction_item = channel_restriction_item_obj.with_context({
|
||||
'connector_no_export': True
|
||||
}).create(map_record.values(for_create=True))
|
||||
channel_restriction_item.channel_pushed = True
|
||||
|
||||
@api.model
|
||||
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
|
||||
results = self.backend_adapter.wired_rplan_get_rplan_values(
|
||||
date_from,
|
||||
date_to,
|
||||
int(channel_restr_plan_id))
|
||||
if any(results):
|
||||
self._generate_restriction_items(results)
|
||||
try:
|
||||
results = self.backend_adapter.wired_rplan_get_rplan_values(
|
||||
date_from,
|
||||
date_to,
|
||||
int(channel_restr_plan_id))
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='restriction',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'],
|
||||
channel_object_id=channel_restr_id,
|
||||
dfrom=date_from, dto=date_to)
|
||||
else:
|
||||
if any(results):
|
||||
self._generate_restriction_items(results)
|
||||
|
||||
class HotelRoomTypeRestrictionItemImportMapper(Component):
|
||||
_name = 'channel.hotel.room.type.restriction.item.import.mapper'
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
from . import common
|
||||
from . import importer
|
||||
from . import exporter
|
||||
from . import deleter
|
||||
|
||||
@@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
|
||||
|
||||
class ChannelProductPricelist(models.Model):
|
||||
_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')
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def create_plan(self):
|
||||
self.ensure_one()
|
||||
if not self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='product.pricelist.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.create_plan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def update_plan_name(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='product.pricelist.exporter')
|
||||
try:
|
||||
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'])
|
||||
exporter.rename_plan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@related_action(action='related_action_unwrap_binding')
|
||||
@api.multi
|
||||
def delete_plan(self):
|
||||
self.ensure_one()
|
||||
if self.external_id:
|
||||
with self.backend_id.work_on(self._name) as work:
|
||||
exporter = work.component(usage='product.pricelist.exporter')
|
||||
try:
|
||||
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'])
|
||||
deleter = work.component(usage='product.pricelist.deleter')
|
||||
deleter.delete_plan(self)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.model
|
||||
def import_price_plans(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
importer = work.component(usage='product.pricelist.importer')
|
||||
try:
|
||||
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'])
|
||||
return importer.import_pricing_plans()
|
||||
|
||||
class ProductPricelist(models.Model):
|
||||
_inherit = 'product.pricelist'
|
||||
|
||||
22
hotel_channel_connector/models/product_pricelist/deleter.py
Normal file
22
hotel_channel_connector/models/product_pricelist/deleter.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo.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'])
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import logging
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
from odoo import api, _
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -14,15 +15,25 @@ class ProductPricelistExporter(Component):
|
||||
|
||||
@api.model
|
||||
def rename_plan(self, binding):
|
||||
return self.backend_adapter.rename_plan(
|
||||
binding.external_id,
|
||||
binding.name)
|
||||
|
||||
@api.model
|
||||
def delete_plan(self, binding):
|
||||
return self.backend_adapter.delete_plan(binding.external_id)
|
||||
try:
|
||||
return self.backend_adapter.rename_plan(
|
||||
binding.external_id,
|
||||
binding.name)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='pricelist',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
|
||||
@api.model
|
||||
def create_plan(self, binding):
|
||||
external_id = self.backend_adapter.create_plan(binding.name)
|
||||
binding.external_id = external_id
|
||||
try:
|
||||
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)
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from datetime import datetime, timedelta
|
||||
from odoo.addons.component.core import Component
|
||||
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 (
|
||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
@@ -20,27 +21,34 @@ class ProductPricelistImporter(Component):
|
||||
|
||||
@api.model
|
||||
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
|
||||
for plan in results:
|
||||
if 'vpid' in plan:
|
||||
continue # FIXME: Ignore Virtual Plans
|
||||
plan_record = pricelist_mapper.map_record(plan)
|
||||
plan_bind = channel_product_listprice_obj.search([
|
||||
('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
|
||||
try:
|
||||
results = self.backend_adapter.get_pricing_plans()
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='pricelist',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
channel_product_listprice_obj = self.env['channel.product.pricelist']
|
||||
pricelist_mapper = self.component(usage='import.mapper',
|
||||
model_name='channel.product.pricelist')
|
||||
for plan in results:
|
||||
if 'vpid' in plan:
|
||||
continue # FIXME: Ignore Virtual Plans
|
||||
plan_record = pricelist_mapper.map_record(plan)
|
||||
plan_bind = channel_product_listprice_obj.search([
|
||||
('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
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from odoo.exceptions import ValidationError
|
||||
from odoo.addons.queue_job.job import job, related_action
|
||||
from odoo.addons.component.core import Component
|
||||
from odoo.addons.component_event import skip_if
|
||||
from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError
|
||||
|
||||
|
||||
class ChannelProductPricelistItem(models.Model):
|
||||
_name = 'channel.product.pricelist.item'
|
||||
@@ -23,42 +23,24 @@ class ChannelProductPricelistItem(models.Model):
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@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:
|
||||
importer = work.component(usage='product.pricelist.item.importer')
|
||||
try:
|
||||
if not backend.pricelist_id:
|
||||
return importer.import_all_pricelist_values(
|
||||
backend.pricelist_from,
|
||||
backend.pricelist_to)
|
||||
return importer.import_pricelist_values(
|
||||
backend.pricelist_id.external_id,
|
||||
if not backend.pricelist_id:
|
||||
return importer.import_all_pricelist_values(
|
||||
backend.pricelist_from,
|
||||
backend.pricelist_to)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
backend=backend.id,
|
||||
section='pricelist',
|
||||
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
|
||||
return importer.import_pricelist_values(
|
||||
backend.pricelist_id.external_id,
|
||||
backend.pricelist_from,
|
||||
backend.pricelist_to)
|
||||
|
||||
@job(default_channel='root.channel')
|
||||
@api.model
|
||||
def push_pricelist(self, backend):
|
||||
with backend.work_on(self._name) as work:
|
||||
exporter = work.component(usage='product.pricelist.item.exporter')
|
||||
try:
|
||||
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'])
|
||||
return exporter.push_pricelist()
|
||||
|
||||
class ProductPricelistItem(models.Model):
|
||||
_inherit = 'product.pricelist.item'
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
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)
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
@@ -26,10 +27,11 @@ class ProductPricelistItemExporter(Component):
|
||||
('date_start', '>=', datetime.now().strftime(
|
||||
DEFAULT_SERVER_DATE_FORMAT))
|
||||
], order="date_start ASC")
|
||||
|
||||
if any(channel_unpushed):
|
||||
date_start = fields.Date.from_string(channel_unpushed[0].date_start)
|
||||
date_end = fields.Date.from_string(channel_unpushed[-1].date_start)
|
||||
days_diff = (date_start - date_end).days + 1
|
||||
days_diff = (date_end - date_start).days + 1
|
||||
|
||||
prices = {}
|
||||
pricelist_ids = channel_product_pricelist_obj.search([
|
||||
@@ -39,7 +41,7 @@ class ProductPricelistItemExporter(Component):
|
||||
for pr in pricelist_ids:
|
||||
prices.update({pr.external_id: {}})
|
||||
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')
|
||||
for pt_id in product_tmpl_ids:
|
||||
channel_room_type = channel_room_type_obj.search([
|
||||
@@ -50,17 +52,23 @@ class ProductPricelistItemExporter(Component):
|
||||
for i in range(0, days_diff):
|
||||
prod = channel_room_type.product_id.with_context({
|
||||
'quantity': 1,
|
||||
'pricelist': pr.id,
|
||||
'pricelist': pr.odoo_id.id,
|
||||
'date': (date_start + timedelta(days=i)).
|
||||
strftime(DEFAULT_SERVER_DATE_FORMAT),
|
||||
})
|
||||
prices[pr.external_id][channel_room_type.external_id].append(prod.price)
|
||||
_logger.info("==[ODOO->CHANNEL]==== PRICELISTS ==")
|
||||
_logger.info(prices)
|
||||
for k_pk, v_pk in prices.items():
|
||||
if any(v_pk):
|
||||
self.backend_adapter.update_plan_prices(k_pk, date_start.strftime(
|
||||
DEFAULT_SERVER_DATE_FORMAT), v_pk)
|
||||
|
||||
channel_unpushed.write({'channel_pushed': True})
|
||||
try:
|
||||
for k_pk, v_pk in prices.items():
|
||||
if any(v_pk):
|
||||
self.backend_adapter.update_plan_prices(k_pk, date_start.strftime(
|
||||
DEFAULT_SERVER_DATE_FORMAT), v_pk)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='pricelist',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'])
|
||||
else:
|
||||
channel_unpushed.write({'channel_pushed': True})
|
||||
return True
|
||||
|
||||
@@ -5,6 +5,7 @@ import logging
|
||||
from datetime import timedelta
|
||||
from odoo.addons.component.core import Component
|
||||
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 (
|
||||
DEFAULT_WUBOOK_DATE_FORMAT)
|
||||
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
@@ -64,9 +65,10 @@ class ProductPricelistItemImporter(Component):
|
||||
'connector_no_export': True,
|
||||
}).write(map_record.values())
|
||||
else:
|
||||
channel_pricelist_item_obj.with_context({
|
||||
pricelist_item = channel_pricelist_item_obj.with_context({
|
||||
'connector_no_export': True,
|
||||
}).create(map_record.values(for_create=True))
|
||||
pricelist_item.channel_pushed = True
|
||||
return True
|
||||
|
||||
@api.model
|
||||
@@ -79,12 +81,22 @@ class ProductPricelistItemImporter(Component):
|
||||
|
||||
@api.model
|
||||
def import_pricelist_values(self, external_id, date_from, date_to, rooms=None):
|
||||
results = self.backend_adapter.fetch_plan_prices(
|
||||
external_id,
|
||||
date_from,
|
||||
date_to,
|
||||
rooms)
|
||||
self._generate_pricelist_items(external_id, date_from, date_to, results)
|
||||
try:
|
||||
results = self.backend_adapter.fetch_plan_prices(
|
||||
external_id,
|
||||
date_from,
|
||||
date_to,
|
||||
rooms)
|
||||
except ChannelConnectorError as err:
|
||||
self.create_issue(
|
||||
section='pricelist',
|
||||
internal_message=str(err),
|
||||
channel_message=err.data['message'],
|
||||
channel_object_id=external_id,
|
||||
dfrom=date_from,
|
||||
dto=date_to)
|
||||
else:
|
||||
self._generate_pricelist_items(external_id, date_from, date_to, results)
|
||||
|
||||
class ProductPricelistItemImportMapper(Component):
|
||||
_name = 'channel.product.pricelist.item.import.mapper'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
});
|
||||
@@ -50,6 +50,16 @@
|
||||
string="Import in background"/>
|
||||
</div>
|
||||
</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>
|
||||
<label string="Import Rooms" class="oe_inline"/>
|
||||
<div>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
<group>
|
||||
<field name="external_id" />
|
||||
<field name="ota_reservation_id" />
|
||||
<field name="wstatus" />
|
||||
<field name="wstatus_reason" />
|
||||
<field name="channel_status" />
|
||||
<field name="channel_status_reason" />
|
||||
<field name="to_read" />
|
||||
</group>
|
||||
</form>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user