diff --git a/hotel_channel_connector/__manifest__.py b/hotel_channel_connector/__manifest__.py index bab0305e0..bfc0ea9f1 100644 --- a/hotel_channel_connector/__manifest__.py +++ b/hotel_channel_connector/__manifest__.py @@ -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', diff --git a/hotel_channel_connector/components/__init__.py b/hotel_channel_connector/components/__init__.py index fbeb3cccb..14d5a4652 100644 --- a/hotel_channel_connector/components/__init__.py +++ b/hotel_channel_connector/components/__init__.py @@ -6,4 +6,5 @@ from . import backend_adapter from . import binder from . import importer from . import exporter +from . import deleter from . import mapper diff --git a/hotel_channel_connector/components/backend_adapter.py b/hotel_channel_connector/components/backend_adapter.py index 4ce8e6758..fd187a748 100644 --- a/hotel_channel_connector/components/backend_adapter.py +++ b/hotel_channel_connector/components/backend_adapter.py @@ -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], diff --git a/hotel_channel_connector/components/core.py b/hotel_channel_connector/components/core.py index 0400e7fba..45d55bd97 100644 --- a/hotel_channel_connector/components/core.py +++ b/hotel_channel_connector/components/core.py @@ -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) diff --git a/hotel_channel_connector/components/deleter.py b/hotel_channel_connector/components/deleter.py new file mode 100644 index 000000000..b86c81259 --- /dev/null +++ b/hotel_channel_connector/components/deleter.py @@ -0,0 +1,9 @@ +# Copyright 2018 Alexandre Díaz +# 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' diff --git a/hotel_channel_connector/components/importer.py b/hotel_channel_connector/components/importer.py index 35608e458..4523311fd 100644 --- a/hotel_channel_connector/components/importer.py +++ b/hotel_channel_connector/components/importer.py @@ -1,784 +1,9 @@ # Copyright 2018 Alexandre Díaz # 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) diff --git a/hotel_channel_connector/controllers/main.py b/hotel_channel_connector/controllers/main.py index 840f20143..974396254 100644 --- a/hotel_channel_connector/controllers/main.py +++ b/hotel_channel_connector/controllers/main.py @@ -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')]) diff --git a/hotel_channel_connector/data/cron_jobs.xml b/hotel_channel_connector/data/cron_jobs.xml index 2810968b4..f7085e5de 100644 --- a/hotel_channel_connector/data/cron_jobs.xml +++ b/hotel_channel_connector/data/cron_jobs.xml @@ -1,26 +1,29 @@ - - + + model.cron_push_changes() + - + + Channel Connector Fetch New Bookings + + 1 + minutes + -1 + code + + + model.cron_import_reservations() + + + diff --git a/hotel_channel_connector/models/channel_backend/common.py b/hotel_channel_connector/models/channel_backend/common.py index 56771db3a..7c7527a99 100644 --- a/hotel_channel_connector/models/channel_backend/common.py +++ b/hotel_channel_connector/models/channel_backend/common.py @@ -1,11 +1,13 @@ # Copyright 2018 Alexandre Díaz # 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() diff --git a/hotel_channel_connector/models/channel_binding/common.py b/hotel_channel_connector/models/channel_binding/common.py index 0b3b72dd5..558404509 100644 --- a/hotel_channel_connector/models/channel_binding/common.py +++ b/hotel_channel_connector/models/channel_binding/common.py @@ -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), - }) diff --git a/hotel_channel_connector/models/channel_ota_info/common.py b/hotel_channel_connector/models/channel_ota_info/common.py index b66bafdd5..c69a414a4 100644 --- a/hotel_channel_connector/models/channel_ota_info/common.py +++ b/hotel_channel_connector/models/channel_ota_info/common.py @@ -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' diff --git a/hotel_channel_connector/models/channel_ota_info/importer.py b/hotel_channel_connector/models/channel_ota_info/importer.py index 8d16bc650..dafe8ccbd 100644 --- a/hotel_channel_connector/models/channel_ota_info/importer.py +++ b/hotel_channel_connector/models/channel_ota_info/importer.py @@ -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 diff --git a/hotel_channel_connector/models/hotel_channel_connector_issue.py b/hotel_channel_connector/models/hotel_channel_connector_issue.py index e9cba78b0..101853cc5 100644 --- a/hotel_channel_connector/models/hotel_channel_connector_issue.py +++ b/hotel_channel_connector/models/hotel_channel_connector_issue.py @@ -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) diff --git a/hotel_channel_connector/models/hotel_reservation/__init__.py b/hotel_channel_connector/models/hotel_reservation/__init__.py index 06e54858b..fe02f8e98 100644 --- a/hotel_channel_connector/models/hotel_reservation/__init__.py +++ b/hotel_channel_connector/models/hotel_reservation/__init__.py @@ -3,3 +3,4 @@ from . import common from . import importer +from . import exporter diff --git a/hotel_channel_connector/models/hotel_reservation/common.py b/hotel_channel_connector/models/hotel_reservation/common.py index 5475bb318..1dd111980 100644 --- a/hotel_channel_connector/models/hotel_reservation/common.py +++ b/hotel_channel_connector/models/hotel_reservation/common.py @@ -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() diff --git a/hotel_channel_connector/models/hotel_reservation/exporter.py b/hotel_channel_connector/models/hotel_reservation/exporter.py new file mode 100644 index 000000000..eab43a48d --- /dev/null +++ b/hotel_channel_connector/models/hotel_reservation/exporter.py @@ -0,0 +1,48 @@ +# Copyright 2018 Alexandre Díaz +# 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']) diff --git a/hotel_channel_connector/models/hotel_reservation/importer.py b/hotel_channel_connector/models/hotel_reservation/importer.py index 0b6f8b981..6c61f40d1 100644 --- a/hotel_channel_connector/models/hotel_reservation/importer.py +++ b/hotel_channel_connector/models/hotel_reservation/importer.py @@ -1,13 +1,22 @@ # Copyright 2018 Alexandre Díaz # 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) diff --git a/hotel_channel_connector/models/hotel_room_type/__init__.py b/hotel_channel_connector/models/hotel_room_type/__init__.py index fe02f8e98..7560603c9 100644 --- a/hotel_channel_connector/models/hotel_room_type/__init__.py +++ b/hotel_channel_connector/models/hotel_room_type/__init__.py @@ -4,3 +4,4 @@ from . import common from . import importer from . import exporter +from . import deleter diff --git a/hotel_channel_connector/models/hotel_room_type/common.py b/hotel_channel_connector/models/hotel_room_type/common.py index a145dd5b7..788476c1b 100644 --- a/hotel_channel_connector/models/hotel_room_type/common.py +++ b/hotel_channel_connector/models/hotel_room_type/common.py @@ -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' diff --git a/hotel_channel_connector/models/hotel_room_type/deleter.py b/hotel_channel_connector/models/hotel_room_type/deleter.py new file mode 100644 index 000000000..ca67c45d0 --- /dev/null +++ b/hotel_channel_connector/models/hotel_room_type/deleter.py @@ -0,0 +1,22 @@ +# Copyright 2018 Alexandre Díaz +# 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']) diff --git a/hotel_channel_connector/models/hotel_room_type/exporter.py b/hotel_channel_connector/models/hotel_room_type/exporter.py index d543ae544..fae272e19 100644 --- a/hotel_channel_connector/models/hotel_room_type/exporter.py +++ b/hotel_channel_connector/models/hotel_room_type/exporter.py @@ -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) diff --git a/hotel_channel_connector/models/hotel_room_type/importer.py b/hotel_channel_connector/models/hotel_room_type/importer.py index 5ef4d27dc..60d34713f 100644 --- a/hotel_channel_connector/models/hotel_room_type/importer.py +++ b/hotel_channel_connector/models/hotel_room_type/importer.py @@ -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' diff --git a/hotel_channel_connector/models/hotel_room_type_availability/common.py b/hotel_channel_connector/models/hotel_room_type_availability/common.py index 6f3a835bc..86a59388f 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/common.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/common.py @@ -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' diff --git a/hotel_channel_connector/models/hotel_room_type_availability/exporter.py b/hotel_channel_connector/models/hotel_room_type_availability/exporter.py index 511ef4af6..b78ed666d 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/exporter.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/exporter.py @@ -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 diff --git a/hotel_channel_connector/models/hotel_room_type_availability/importer.py b/hotel_channel_connector/models/hotel_room_type_availability/importer.py index d45195147..718cfda48 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/importer.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/importer.py @@ -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 diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py b/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py index fe02f8e98..7560603c9 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py @@ -4,3 +4,4 @@ from . import common from . import importer from . import exporter +from . import deleter diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/common.py b/hotel_channel_connector/models/hotel_room_type_restriction/common.py index 31e0bd708..81721e384 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/common.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/common.py @@ -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' diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/deleter.py b/hotel_channel_connector/models/hotel_room_type_restriction/deleter.py new file mode 100644 index 000000000..f7f655131 --- /dev/null +++ b/hotel_channel_connector/models/hotel_room_type_restriction/deleter.py @@ -0,0 +1,22 @@ +# Copyright 2018 Alexandre Díaz +# 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']) diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py b/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py index 9348347d3..934101985 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py @@ -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) diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/importer.py b/hotel_channel_connector/models/hotel_room_type_restriction/importer.py index aa044a101..f7bee2702 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/importer.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/importer.py @@ -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 diff --git a/hotel_channel_connector/models/hotel_room_type_restriction_item/common.py b/hotel_channel_connector/models/hotel_room_type_restriction_item/common.py index a6e0b1447..8af276fcf 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction_item/common.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction_item/common.py @@ -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' diff --git a/hotel_channel_connector/models/hotel_room_type_restriction_item/exporter.py b/hotel_channel_connector/models/hotel_room_type_restriction_item/exporter.py index 5d588f0ee..9895de4c9 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction_item/exporter.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction_item/exporter.py @@ -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 diff --git a/hotel_channel_connector/models/hotel_room_type_restriction_item/importer.py b/hotel_channel_connector/models/hotel_room_type_restriction_item/importer.py index 649ba7f06..f151042a2 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction_item/importer.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction_item/importer.py @@ -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' diff --git a/hotel_channel_connector/models/product_pricelist/__init__.py b/hotel_channel_connector/models/product_pricelist/__init__.py index fe02f8e98..7560603c9 100644 --- a/hotel_channel_connector/models/product_pricelist/__init__.py +++ b/hotel_channel_connector/models/product_pricelist/__init__.py @@ -4,3 +4,4 @@ from . import common from . import importer from . import exporter +from . import deleter diff --git a/hotel_channel_connector/models/product_pricelist/common.py b/hotel_channel_connector/models/product_pricelist/common.py index d207b0e5f..7a001db58 100644 --- a/hotel_channel_connector/models/product_pricelist/common.py +++ b/hotel_channel_connector/models/product_pricelist/common.py @@ -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' diff --git a/hotel_channel_connector/models/product_pricelist/deleter.py b/hotel_channel_connector/models/product_pricelist/deleter.py new file mode 100644 index 000000000..928004e49 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist/deleter.py @@ -0,0 +1,22 @@ +# Copyright 2018 Alexandre Díaz +# 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']) diff --git a/hotel_channel_connector/models/product_pricelist/exporter.py b/hotel_channel_connector/models/product_pricelist/exporter.py index fb990cfb2..47335258a 100644 --- a/hotel_channel_connector/models/product_pricelist/exporter.py +++ b/hotel_channel_connector/models/product_pricelist/exporter.py @@ -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) diff --git a/hotel_channel_connector/models/product_pricelist/importer.py b/hotel_channel_connector/models/product_pricelist/importer.py index dde6aaea0..54e5e7ff0 100644 --- a/hotel_channel_connector/models/product_pricelist/importer.py +++ b/hotel_channel_connector/models/product_pricelist/importer.py @@ -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 diff --git a/hotel_channel_connector/models/product_pricelist_item/common.py b/hotel_channel_connector/models/product_pricelist_item/common.py index cd58fd412..a5af8ef49 100644 --- a/hotel_channel_connector/models/product_pricelist_item/common.py +++ b/hotel_channel_connector/models/product_pricelist_item/common.py @@ -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' diff --git a/hotel_channel_connector/models/product_pricelist_item/exporter.py b/hotel_channel_connector/models/product_pricelist_item/exporter.py index c062acabd..b0df64456 100644 --- a/hotel_channel_connector/models/product_pricelist_item/exporter.py +++ b/hotel_channel_connector/models/product_pricelist_item/exporter.py @@ -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 diff --git a/hotel_channel_connector/models/product_pricelist_item/importer.py b/hotel_channel_connector/models/product_pricelist_item/importer.py index 491cebfb0..52b13d9a5 100644 --- a/hotel_channel_connector/models/product_pricelist_item/importer.py +++ b/hotel_channel_connector/models/product_pricelist_item/importer.py @@ -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' diff --git a/hotel_channel_connector/static/src/js/views/list/list_controller.js b/hotel_channel_connector/static/src/js/views/list/list_controller.js deleted file mode 100644 index 472c471f8..000000000 --- a/hotel_channel_connector/static/src/js/views/list/list_controller.js +++ /dev/null @@ -1,187 +0,0 @@ -odoo.define('hotel_channel_connector.ListController', function(require) { -'use strict'; -/* - * Hotel Channel Connector - * GNU Public License - * Alexandre Díaz - */ - -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(""); - this.$buttons.find('.o_channel_connector_import_rooms').on('click', this._importRooms.bind(this)); - } else if (this.modelName === 'hotel.folio') { - this.$buttons.append(""); - this.$buttons.find('.o_channel_connector_import_reservations').on('click', this._importReservations.bind(this)); - } else if (this.modelName === 'product.pricelist') { - this.$buttons.append(""); - this.$buttons.find('.o_channel_connector_import_price_plans').on('click', this._importPricePlans.bind(this)); - this.$buttons.append(""); - 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(""); - 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(""); - this.$buttons.find('.o_channel_connector_import_restriction_plans').on('click', this._importRestrictionPlans.bind(this)); - this.$buttons.append(""); - 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(""); - this.$buttons.find('.o_channel_connector_import_availability').on('click', this._importAvailability.bind(this)); - this.$buttons.append(""); - 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'), `${results[1]}` + ' ' + _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'), `${results[1]}` + ' ' + _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'), `${results[1]}` + ' ' + _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'), `${results[1]}` + ' ' + _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'), `${results[1]}` + ' ' + _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; - } -}); - -}); diff --git a/hotel_channel_connector/static/src/js/views/list/list_model.js b/hotel_channel_connector/static/src/js/views/list/list_model.js deleted file mode 100644 index 8f1cd70f7..000000000 --- a/hotel_channel_connector/static/src/js/views/list/list_model.js +++ /dev/null @@ -1,25 +0,0 @@ -odoo.define('hotel_channel_connector.ListModel', function(require) { -'use strict'; -/* - * Hotel Channel Connector - * GNU Public License - * Alexandre Díaz - */ - -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, - }); - }, - -}); - -}); diff --git a/hotel_channel_connector/static/src/js/views/list/list_view.js b/hotel_channel_connector/static/src/js/views/list/list_view.js deleted file mode 100644 index f3364bdcd..000000000 --- a/hotel_channel_connector/static/src/js/views/list/list_view.js +++ /dev/null @@ -1,18 +0,0 @@ -odoo.define('hotel_channel_connector.ListView', function(require) { -'use strict'; -/* - * Hotel Channel Connector - * GNU Public License - * Alexandre Díaz - */ - -var ListView = require('web.ListView'), - ListModel = require('hotel_channel_connector.ListModel'); - -ListView.include({ - config: _.extend({}, ListView.prototype.config, { - Model: ListModel, - }), -}); - -}); diff --git a/hotel_channel_connector/views/channel_connector_backend_views.xml b/hotel_channel_connector/views/channel_connector_backend_views.xml index 18ccbb9a5..8bb3f4f99 100644 --- a/hotel_channel_connector/views/channel_connector_backend_views.xml +++ b/hotel_channel_connector/views/channel_connector_backend_views.xml @@ -50,6 +50,16 @@ string="Import in background"/> + +