diff --git a/hotel_calendar/models/bus_hotel_calendar.py b/hotel_calendar/models/bus_hotel_calendar.py index 2cef55759..9314f95ed 100644 --- a/hotel_calendar/models/bus_hotel_calendar.py +++ b/hotel_calendar/models/bus_hotel_calendar.py @@ -110,7 +110,7 @@ class BusHotelCalendar(models.TransientModel): return { 'type': 'availability', 'availability': { - vals['room_id']: { + vals['room_type_id']: { date_dt.strftime("%d/%m/%Y"): [ vals['avail'], vals['no_ota'], diff --git a/hotel_calendar/models/inherited_hotel_room_type_availability.py b/hotel_calendar/models/inherited_hotel_room_type_availability.py index fa81bf137..4a001c4a9 100644 --- a/hotel_calendar/models/inherited_hotel_room_type_availability.py +++ b/hotel_calendar/models/inherited_hotel_room_type_availability.py @@ -8,7 +8,7 @@ class HotelRoomTypeAvailability(models.Model): @api.model def create(self, vals): - res = super(HotelVirtualRoomAvailability, self).create(vals) + res = super(HotelRoomTypeAvailability, self).create(vals) self.env['bus.hotel.calendar'].send_availability_notification({ 'date': res.date, 'avail': res.avail, @@ -20,7 +20,7 @@ class HotelRoomTypeAvailability(models.Model): @api.multi def write(self, vals): - ret_vals = super(HotelVirtualRoomAvailability, self).write(vals) + ret_vals = super(HotelRoomTypeAvailability, self).write(vals) bus_hotel_calendar_obj = self.env['bus.hotel.calendar'] for record in self: bus_hotel_calendar_obj.send_availability_notification({ @@ -44,7 +44,7 @@ class HotelRoomTypeAvailability(models.Model): 'no_ota': False, 'id': record.id, }) - res = super(HotelVirtualRoomAvailability, self).unlink() + res = super(HotelRoomTypeAvailability, self).unlink() bus_hotel_calendar_obj = self.env['bus.hotel.calendar'] for uval in unlink_vals: bus_hotel_calendar_obj.send_availability_notification(uval) diff --git a/hotel_calendar/models/inherited_ir_default.py b/hotel_calendar/models/inherited_ir_default.py index 60bc254a0..ce5cf8f98 100644 --- a/hotel_calendar/models/inherited_ir_default.py +++ b/hotel_calendar/models/inherited_ir_default.py @@ -25,10 +25,10 @@ class IrDefault(models.Model): fixed_price = pitem.fixed_price room_type = room_type_obj.search([ ('product_id.product_tmpl_id', '=', product_tmpl_id), - ('date_start', '>=', fields.Date.today()) ], limit=1) - room_pr_cached_obj.create({ - 'room_type_id': room_type.id, - 'date': date_start, - 'price': fixed_price, - }) + if room_type: + room_pr_cached_obj.create({ + 'room_id': room_type.id, + 'date': date_start, + 'price': fixed_price, + }) diff --git a/hotel_calendar/views/res_config_views.xml b/hotel_calendar/views/res_config_views.xml index a3549f08b..0047838ad 100644 --- a/hotel_calendar/views/res_config_views.xml +++ b/hotel_calendar/views/res_config_views.xml @@ -8,24 +8,22 @@ - -
-

Calendar colors

-
-
- - - - - -
-
- - - - - -
+ +

Calendar colors

+
+
+ + + + + +
+
+ + + + +
diff --git a/hotel_channel_connector/__manifest__.py b/hotel_channel_connector/__manifest__.py index 8bb150279..b478791b1 100644 --- a/hotel_channel_connector/__manifest__.py +++ b/hotel_channel_connector/__manifest__.py @@ -22,6 +22,7 @@ 'wizard/wubook_import_plan_restrictions.xml', 'wizard/wubook_import_availability.xml', 'views/general.xml', + 'views/hotel_channel_connector_issue_views.xml', 'views/inherited_hotel_reservation_views.xml', 'views/inherited_hotel_room_type_views.xml', 'views/inherited_hotel_room_type_availability_views.xml', @@ -29,14 +30,16 @@ 'views/inherited_product_pricelist_views.xml', 'views/inherited_product_pricelist_item_views.xml', '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/hotel_channel_connector_issue_views.xml', 'views/channel_hotel_reservation_views.xml', 'views/channel_hotel_room_type_views.xml', 'views/channel_hotel_room_type_availability_views.xml', 'views/channel_hotel_room_type_restriction_views.xml', + 'views/channel_hotel_room_type_restriction_item_views.xml', 'views/channel_product_pricelist_views.xml', + 'views/channel_product_pricelist_item_views.xml', 'views/channel_connector_backend_views.xml', 'data/menus.xml', 'data/sequences.xml', diff --git a/hotel_channel_connector/components/backend_adapter.py b/hotel_channel_connector/components/backend_adapter.py index e5b64afd7..07d2058ba 100644 --- a/hotel_channel_connector/components/backend_adapter.py +++ b/hotel_channel_connector/components/backend_adapter.py @@ -9,7 +9,6 @@ from odoo.tools import ( DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT) from odoo.addons.payment.models.payment_acquirer import _partner_split_name -from odoo.addons.hotel import date_utils from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError from odoo import fields, _ _logger = logging.getLogger(__name__) @@ -276,8 +275,8 @@ class WuBookAdapter(AbstractComponent): rcode, results = self._server.fetch_rooms_values( self._session_info[0], self._session_info[1], - date_utils.get_datetime(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), - date_utils.get_datetime(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), rooms) if rcode != 0: raise ChannelConnectorError("Can't fetch rooms values from WuBook", { @@ -317,14 +316,14 @@ class WuBookAdapter(AbstractComponent): 'phone': phone, 'street': address, 'country': country_code, - 'arrival_hour': date_utils.get_datetime(checkin).strftime("%H:%M"), + 'arrival_hour': fields.Datetime.from_string(checkin).strftime("%H:%M"), 'notes': notes } rcode, results = self._server.new_reservation( self._session_info[0], self._session_info[1], - date_utils.get_datetime(checkin).strftime(DEFAULT_WUBOOK_DATE_FORMAT), - date_utils.get_datetime(checkout).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(checkin).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(checkout).strftime(DEFAULT_WUBOOK_DATE_FORMAT), {channel_room_id: [adults+children, 'nb']}, customer, adults+children) @@ -431,7 +430,7 @@ class WuBookAdapter(AbstractComponent): self._session_info[0], self._session_info[1], channel_plan_id, - date_utils.get_datetime(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), prices) if rcode != 0: raise ChannelConnectorError("Can't update pricing plan in wubook", { @@ -442,7 +441,7 @@ class WuBookAdapter(AbstractComponent): return results def update_plan_periods(self, channel_plan_id, periods): - rcode, results = self.SERVER.update_plan_periods( + rcode, results = self._server.update_plan_periods( self._session_info[0], self._session_info[1], channel_plan_id, @@ -455,7 +454,7 @@ class WuBookAdapter(AbstractComponent): return results def get_pricing_plans(self): - rcode, results = self.SERVER.get_pricing_plans( + rcode, results = self._server.get_pricing_plans( self._session_info[0], self._session_info[1]) if rcode != 0: @@ -469,8 +468,8 @@ class WuBookAdapter(AbstractComponent): self._session_info[0], self._session_info[1], channel_plan_id, - date_utils(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), - date_utils(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), rooms or []) if rcode != 0: raise ChannelConnectorError("Can't get pricing plans from wubook", { @@ -496,8 +495,8 @@ class WuBookAdapter(AbstractComponent): rcode, results = self._server.wired_rplan_get_rplan_values( self._session_info[0], self._session_info[1], - date_utils(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), - date_utils(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_to).strftime(DEFAULT_WUBOOK_DATE_FORMAT), channel_restriction_plan_id) if rcode != 0: raise ChannelConnectorError("Can't fetch restriction plans from wubook", { @@ -513,7 +512,7 @@ class WuBookAdapter(AbstractComponent): self._session_info[0], self._session_info[1], channel_restriction_plan_id, - date_utils(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), + fields.Date.from_string(date_from).strftime(DEFAULT_WUBOOK_DATE_FORMAT), values) if rcode != 0: raise ChannelConnectorError("Can't update plan restrictions on wubook", { diff --git a/hotel_channel_connector/components/binder.py b/hotel_channel_connector/components/binder.py index 3276998d5..3ed074208 100644 --- a/hotel_channel_connector/components/binder.py +++ b/hotel_channel_connector/components/binder.py @@ -11,6 +11,8 @@ class HotelConnectorModelBinder(Component): 'channel.hotel.room.type', 'channel.hotel.room.type.availability', 'channel.hotel.room.type.restriction', + 'channel.hotel.room.type.restriction.item', 'channel.product.pricelist', + 'channel.product.pricelist.item', 'channel.ota.info', ] diff --git a/hotel_channel_connector/components/core.py b/hotel_channel_connector/components/core.py index 951c40996..0400e7fba 100644 --- a/hotel_channel_connector/components/core.py +++ b/hotel_channel_connector/components/core.py @@ -9,18 +9,6 @@ class BaseHotelChannelConnectorComponent(AbstractComponent): _inherit = 'base.connector' _collection = 'channel.backend' - @api.model - def create_issue(self, section, message, channel_message, channel_object_id=False, - dfrom=False, dto=False): - self.env['hotel.channel.connector.issue'].sudo().create({ - 'section': section, - 'internal_message': message, - 'channel_object_id': channel_object_id, - 'channel_message': channel_message, - 'date_start': dfrom, - 'date_end': dto, - }) - class ChannelConnectorError(Exception): def __init__(self, message, data): super().__init__(message) diff --git a/hotel_channel_connector/components/exporter.py b/hotel_channel_connector/components/exporter.py index fb42e8e61..bc1545ede 100644 --- a/hotel_channel_connector/components/exporter.py +++ b/hotel_channel_connector/components/exporter.py @@ -12,7 +12,7 @@ from odoo.tools import ( DEFAULT_SERVER_DATETIME_FORMAT) from .backend_adapter import DEFAULT_WUBOOK_DATE_FORMAT from odoo.addons.hotel import date_utils -from odoo import api +from odoo import api, fields _logger = logging.getLogger(__name__) class HotelChannelConnectorExporter(AbstractComponent): @@ -25,42 +25,6 @@ class HotelChannelConnectorExporter(AbstractComponent): return self.push_availability() and self.push_priceplans() and \ self.push_restrictions() - @api.model - def push_availability(self): - room_type_avail_ids = self.env['hotel.room.type.availability'].search([ - ('wpushed', '=', False), - ('date', '>=', date_utils.now(hours=False).strftime( - DEFAULT_SERVER_DATE_FORMAT)) - ]) - - room_types = room_type_avail_ids.mapped('room_type_id') - avails = [] - for room_type in room_types: - room_type_avails = room_type_avail_ids.filtered( - lambda x: x.room_type_id.id == room_type.id) - days = [] - for room_type_avail in room_type_avails: - room_type_avail.with_context({ - 'wubook_action': False}).write({'wpushed': True}) - wavail = room_type_avail.avail - if wavail > room_type_avail.wmax_avail: - wavail = room_type_avail.wmax_avail - date_dt = date_utils.get_datetime( - room_type_avail.date, - dtformat=DEFAULT_SERVER_DATE_FORMAT) - days.append({ - 'date': date_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT), - 'avail': wavail, - 'no_ota': room_type_avail.no_ota and 1 or 0, - # 'booked': room_type_avail.booked and 1 or 0, - }) - avails.append({'id': room_type.wrid, 'days': days}) - _logger.info("UPDATING AVAILABILITY IN WUBOOK...") - _logger.info(avails) - if any(avails): - self.backend_adapter.update_availability(avails) - return True - @api.model def push_priceplans(self): unpushed = self.env['product.pricelist.item'].search([ diff --git a/hotel_channel_connector/components/importer.py b/hotel_channel_connector/components/importer.py index dc94680dd..093a7b7f2 100644 --- a/hotel_channel_connector/components/importer.py +++ b/hotel_channel_connector/components/importer.py @@ -238,9 +238,9 @@ class HotelChannelConnectorImporter(AbstractComponent): # the same transaction an cancellation) if crcode in failed_reservations: self.create_issue( - 'reservation', - "Can't process a reservation that previusly failed!", - '', channel_object_id=book['reservation_code']) + 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) @@ -342,10 +342,10 @@ class HotelChannelConnectorImporter(AbstractComponent): ], limit=1) if not room_type: self.create_issue( - 'reservation', - "Can't found any room type associated to '%s' \ - in this hotel" % book['rooms'], - '', channel_object_id=book['reservation_code']) + 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 @@ -376,9 +376,9 @@ class HotelChannelConnectorImporter(AbstractComponent): ) if vals['price_unit'] != book['amount']: self.create_issue( - 'reservation', - "Invalid reservation total price! %.2f != %.2f" % (vals['price_unit'], book['amount']), - '', channel_object_id=book['reservation_code']) + 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( checkin_str, @@ -439,9 +439,9 @@ class HotelChannelConnectorImporter(AbstractComponent): }) reservations.append((0, False, vals)) self.create_issue( - 'reservation', - "Reservation imported with overbooking state", - '', channel_object_id=rcode) + section='reservation', + internal_message="Reservation imported with overbooking state", + channel_object_id=rcode) dates_checkin = [False, False] dates_checkout = [False, False] split_booking = False @@ -458,9 +458,9 @@ class HotelChannelConnectorImporter(AbstractComponent): if split_booking: self.create_issue( - 'reservation', - "Reservation Splitted", - '', channel_object_id=rcode) + section='reservation', + internal_message="Reservation Splitted", + channel_object_id=rcode) # Create Folio if not any(failed_reservations) and any(reservations): @@ -498,9 +498,9 @@ class HotelChannelConnectorImporter(AbstractComponent): processed_rids.append(rcode) except ChannelConnectorError as err: self.create_issue( - 'reservation', - err.data['message'], - '', channel_object_id=rcode) + 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) @@ -692,9 +692,9 @@ class HotelChannelConnectorImporter(AbstractComponent): count = self._generate_pricelists(results) except ChannelConnectorError as err: self.create_issue( - 'plan', - _("Can't get pricing plans from wubook"), - err.data['message']) + section='plan', + internal_message=_("Can't get pricing plans from wubook"), + channel_message=err.data['message']) return 0 return count @@ -709,9 +709,9 @@ class HotelChannelConnectorImporter(AbstractComponent): self._generate_pricelist_items(channel_plan_id, date_from, date_to, results) except ChannelConnectorError as err: self.create_issue( - 'plan', - _("Can't fetch plan prices from wubook"), - err.data['message']) + section='plan', + internal_message=_("Can't fetch plan prices from wubook"), + channel_message=err.data['message']) return False return True @@ -732,9 +732,9 @@ class HotelChannelConnectorImporter(AbstractComponent): self._generate_pricelist_items(channel_plan_id, date_from, date_to, results) except ChannelConnectorError as err: self.create_issue( - 'plan', - "Can't fetch all plan prices from wubook!", - err.data['message'], + 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 @@ -746,9 +746,9 @@ class HotelChannelConnectorImporter(AbstractComponent): count = self._generate_restrictions(results) except ChannelConnectorError as err: self.create_issue( - 'rplan', - _("Can't fetch restriction plans from wubook"), - err.data['message']) + section='rplan', + internal_message=_("Can't fetch restriction plans from wubook"), + channel_message=err.data['message']) return 0 return count @@ -763,9 +763,9 @@ class HotelChannelConnectorImporter(AbstractComponent): self._generate_restriction_items(results) except ChannelConnectorError as err: self.create_issue( - 'rplan', - _("Can't fetch plan restrictions from wubook"), - err.data['message'], + 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 diff --git a/hotel_channel_connector/data/menus.xml b/hotel_channel_connector/data/menus.xml index 06b13e837..3b6264619 100644 --- a/hotel_channel_connector/data/menus.xml +++ b/hotel_channel_connector/data/menus.xml @@ -13,12 +13,6 @@ parent="menu_channel_connector_root" action="action_channel_backend"/> - - dto_dt: dfrom_dt, dto_dt = dto_dt, dfrom_dt - try: - 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) - except ChannelConnectorError as err: - self.create_issue('room', _("Can't fetch rooms values from WuBook"), - err.data['message'], dfrom=dfrom, dto=dto) - return False - return True + 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): @@ -158,7 +146,7 @@ class HotelRoomTypeImportMapper(Component): _apply_on = 'channel.hotel.room.type' direct = [ - ('id', 'channel_room_id'), + ('id', 'external_id'), ('shortname', 'channel_short_code'), ('occupancy', 'ota_capacity'), ('price', 'list_price'), diff --git a/hotel_channel_connector/models/hotel_room_type_availability/__init__.py b/hotel_channel_connector/models/hotel_room_type_availability/__init__.py index 06e54858b..fe02f8e98 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/__init__.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/__init__.py @@ -3,3 +3,4 @@ from . import common from . import importer +from . import exporter 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 1235db837..ccd521a36 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/common.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/common.py @@ -2,12 +2,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from datetime import timedelta -from odoo import api, models, fields +from odoo import api, models, fields, _ from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo.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) @@ -15,7 +16,7 @@ class ChannelHotelRoomTypeAvailability(models.Model): _name = 'channel.hotel.room.type.availability' _inherit = 'channel.binding' _inherits = {'hotel.room.type.availability': 'odoo_id'} - _description = 'Channel Product Pricelist' + _description = 'Channel Availability' @api.model def _default_channel_max_avail(self): @@ -27,6 +28,8 @@ class ChannelHotelRoomTypeAvailability(models.Model): string='Pricelist', required=True, ondelete='cascade') + no_ota = fields.Boolean('No OTA', default=False) + booked = fields.Boolean('Booked', default=False, readonly=True) channel_max_avail = fields.Integer("Max. Channel Avail", default=_default_channel_max_avail, old_name='wmax_avail') @@ -44,19 +47,45 @@ class ChannelHotelRoomTypeAvailability(models.Model): @job(default_channel='root.channel') @related_action(action='related_action_unwrap_binding') @api.multi - def update_availability(self): - self.ensure_one() - if self._context.get('channel_action', True): - with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') - date_dt = fields.Date.from_string(self.date) - adapter.update_availability([{ - 'id': self.odoo_id.room_type_id.channel_room_id, - 'days': [{ - 'date': date_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT), - 'avail': self.odoo_id.avail, - }], - }]) + 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=_("Can't update availability in WuBook"), + channel_message=err.data['message']) + + @job(default_channel='root.channel') + @api.model + def import_availability(self, backend): + with backend.work_on(self._name) as work: + importer = work.component(usage='hotel.room.type.availability.importer') + try: + return importer.get_availability(backend.avail_from, backend.avail_to) + except ChannelConnectorError as err: + self.create_issue( + backend=backend.id, + section='avail', + internal_message=_("Can't import availability from WuBook"), + channel_message=err.data['message']) + + @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=_("Can't update availability in WuBook"), + channel_message=err.data['message']) class HotelRoomTypeAvailability(models.Model): _inherit = 'hotel.room.type.availability' @@ -79,16 +108,15 @@ class HotelRoomTypeAvailability(models.Model): if record.avail > max_avail: issue_obj.sudo().create({ 'section': 'avail', - 'message': _(r"The new availability can't be greater than \ - the actual availability \ - \n[%s]\nInput: %d\Limit: %d") % (record.room_type_id.name, - record.avail, - record), - 'channel_id': record.room_type_id.channel_bind_ids[0].channel_plan_id, + 'internal_message': _(r"The new availability can't be greater than \ + the max. availability \ + (%s) [Input: %d\Max: %d]") % (record.room_type_id.name, + record.avail, + max_avail), 'date_start': record.date, 'date_end': record.date, }) - # Auto-Fix wubook availability + # Auto-Fix channel availability self._event('on_fix_channel_availability').notify(record) return super(HotelRoomTypeAvailability, self)._check_avail() @@ -97,12 +125,6 @@ class HotelRoomTypeAvailability(models.Model): if self.room_type_id: self.channel_max_avail = self.room_type_id.total_rooms_count - @api.multi - def write(self, vals): - if self._context.get('channel_action', True): - vals.update({'channel_pushed': False}) - return super(HotelRoomTypeAvailability, self).write(vals) - @api.model def refresh_availability(self, checkin, checkout, product_id): date_start = fields.Date.from_string(checkin) @@ -143,11 +165,41 @@ class HotelRoomTypeAvailability(models.Model): 'avail': avail, }) +class HotelRoomTypeAvailabilityAdapter(Component): + _name = 'channel.hotel.room.type.availability.adapter' + _inherit = 'wubook.adapter' + _apply_on = 'channel.hotel.room.type.availability' + + def fetch_rooms_values(self, date_from, date_to, rooms=False): + return super(HotelRoomTypeAvailabilityAdapter, self).fetch_rooms_values( + date_from, + date_to, + rooms) + + def update_availability(self, rooms_avail): + return super(HotelRoomTypeAvailabilityAdapter, self).update_availability( + rooms_avail) + +class BindingHotelRoomTypeAvailabilityListener(Component): + _name = 'binding.hotel.room.type.listener' + _inherit = 'base.connector.listener' + _apply_on = ['hotel.room.type.availability'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + if 'avail' in fields: + record.channel_bind_ids.write({'channel_pushed': False}) + class ChannelBindingHotelRoomTypeAvailabilityListener(Component): _name = 'channel.binding.hotel.room.type.availability.listener' _inherit = 'base.connector.listener' _apply_on = ['channel.hotel.room.type.availability'] + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + if 'avail' in fields: + record.channel_pushed = False + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_fix_channel_availability(self, record, fields=None): - record.with_delay(priority=20).update_availability() + record.update_availability() diff --git a/hotel_channel_connector/models/hotel_room_type_availability/exporter.py b/hotel_channel_connector/models/hotel_room_type_availability/exporter.py new file mode 100644 index 000000000..3c5cfae81 --- /dev/null +++ b/hotel_channel_connector/models/hotel_room_type_availability/exporter.py @@ -0,0 +1,62 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import Component +from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError +from odoo import api, fields, _ +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +_logger = logging.getLogger(__name__) + +class HotelRoomTypeAvailabilityExporter(Component): + _name = 'channel.hotel.room.type.availability.exporter' + _inherit = 'hotel.channel.exporter' + _apply_on = ['channel.hotel.room.type.availability'] + _usage = 'hotel.room.type.availability.exporter' + + @api.model + def update_availability(self, binding): + if any(binding.room_type_id.channel_bind_ids): + sday_dt = fields.Date.from_string(binding.date) + # FIXME: Supossed that only exists one channel connector per record + binding.channel_pushed = True + return self.backend_adapter.update_availability({ + 'id': binding.room_type_id.channel_bind_ids[0].channel_room_id, + 'days': [{ + 'date': sday_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT), + 'avail': binding.avail, + 'no_ota': binding.no_ota, + }], + }) + + def push_availability(self): + channel_room_type_avail_ids = self.env['channel.hotel.room.type.availability'].search([ + ('channel_pushed', '=', False), + ('date', '>=', fields.Date.today()) + ]) + room_types = channel_room_type_avail_ids.mapped('room_type_id') + avails = [] + for room_type in room_types: + if any(room_type.channel_bind_ids): + channel_room_type_avails = channel_room_type_avail_ids.filtered( + lambda x: x.room_type_id.id == room_type.id) + days = [] + for channel_room_type_avail in channel_room_type_avails: + 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: + cavail = channel_room_type_avail.channel_max_avail + date_dt = fields.Date.from_string(channel_room_type_avail.date) + days.append({ + 'date': date_dt.strftime(DEFAULT_WUBOOK_DATE_FORMAT), + 'avail': cavail, + 'no_ota': channel_room_type_avail.no_ota and 1 or 0, + # 'booked': room_type_avail.booked and 1 or 0, + }) + avails.append({'id': room_type.channel_bind_ids[0].channel_room_id, 'days': days}) + _logger.info("UPDATING AVAILABILITY IN WUBOOK...") + _logger.info(avails) + if any(avails): + self.backend_adapter.update_availability(avails) 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 4970a308b..a10301fd5 100644 --- a/hotel_channel_connector/models/hotel_room_type_availability/importer.py +++ b/hotel_channel_connector/models/hotel_room_type_availability/importer.py @@ -2,11 +2,11 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from datetime import timedelta +from datetime import date, timedelta from odoo.exceptions import ValidationError from odoo.addons.component.core import Component from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError -from odoo.addons.connector.components.mapper import mapping +from odoo.addons.connector.components.mapper import mapping, only_create from odoo.addons.hotel import date_utils from odoo import fields, api, _ _logger = logging.getLogger(__name__) @@ -18,6 +18,53 @@ class HotelRoomTypeAvailabilityImporter(Component): _apply_on = ['channel.hotel.room.type.availability'] _usage = 'hotel.room.type.availability.importer' + @api.model + def get_availability(self, date_from, date_to): + now_dt = date.today() + dfrom_dt = fields.Date.from_string(date_from) + dto_dt = fields.Date.from_string(date_to) + if dfrom_dt < now_dt: + dfrom_dt = now_dt + if dfrom_dt > dto_dt: + dfrom_dt, dto_dt = dto_dt, dfrom_dt + if dto_dt < now_dt: + return True + + results = self.backend_adapter.fetch_rooms_values(date_from, date_to) + + 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({ + 'wubook_action': False + }).write(map_record.values()) + else: + room_type_avail_bind = channel_room_type_avail_obj.with_context({ + 'wubook_action': False + }).create(map_record.values(for_create=True)) + iter_day += timedelta(days=1) + return count + class HotelRoomTypeAvailabilityImportMapper(Component): _name = 'channel.hotel.room.type.availability.import.mapper' @@ -28,10 +75,18 @@ class HotelRoomTypeAvailabilityImportMapper(Component): ('no_ota', 'no_ota'), ('booked', 'booked'), ('avail', 'avail'), - ('room_type_id', 'room_type_id'), ('date', 'date'), ] + @only_create + @mapping + def channel_pushed(self, record): + return {'channel_pushed': True} + @mapping def backend_id(self, record): return {'backend_id': self.backend_record.id} + + @mapping + def room_type_id(self, record): + return {'room_type_id': record['room_type_id']} 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 06e54858b..fe02f8e98 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/__init__.py @@ -3,3 +3,4 @@ from . import common from . import importer +from . import exporter 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 189523a1d..fef19def4 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/common.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/common.py @@ -1,11 +1,14 @@ # Copyright 2018 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +import logging from odoo import api, models, fields 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): _name = 'channel.hotel.room.type.restriction' @@ -17,57 +20,71 @@ class ChannelHotelRoomTypeRestriction(models.Model): string='Hotel Virtual Room Restriction', required=True, ondelete='cascade') - channel_plan_id = fields.Char("Channel Plan ID", readonly=True, old_name='wpid') - 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 self._context.get('channel_action', True): + if not self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='hotel.room.type.restriction.exporter') try: - channel_plan_id = adapter.create_rplan(self.name) - if channel_plan_id: - self.channel_plan_id = channel_plan_id - except ValidationError as e: - self.create_issue('room', "Can't create restriction plan on channel", "sss") + exporter.create_rplan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't create restriction plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') @related_action(action='related_action_unwrap_binding') @api.multi def update_plan_name(self): self.ensure_one() - if self._context.get('channel_action', True): + if self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='hotel.room.type.restriction.exporter') try: - adapter.rename_rplan(self.channel_plan_id, self.name) - except ValidationError as e: - self.create_issue('room', "Can't update restriction plan name on channel", "sss") + exporter.rename_rplan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't modify restriction plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') @related_action(action='related_action_unwrap_binding') @api.multi def delete_plan(self): self.ensure_one() - if self._context.get('channel_action', True) and self.channel_room_id: + if self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='hotel.room.type.restriction.exporter') try: - adapter.delete_rplan(self.channel_plan_id) - except ValidationError as e: - self.create_issue('room', "Can't delete restriction plan on channel", "sss") + exporter.delete_rplan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't delete restriction plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') - @api.multi - def import_restriction_plans(self): - if self._context.get('channel_action', True): - with self.backend_id.work_on(self._name) as work: - importer = work.component(usage='channel.importer') + @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=_("Can't fetch restriction plans from wubook"), + channel_message=err.data['message']) class HotelRoomTypeRestriction(models.Model): _inherit = 'hotel.room.type.restriction' @@ -85,12 +102,44 @@ class HotelRoomTypeRestriction(models.Model): names = [] for name in org_names: restriction_id = room_type_restriction_obj.browse(name[0]) - if restriction_id.channel_bind_ids.channel_plan_id: - names.append((name[0], '%s (WuBook)' % name[1])) + if any(restriction_id.channel_bind_ids) and \ + restriction_id.channel_bind_ids[0].external_id: + names.append(( + name[0], + '%s (%s Backend)' % (name[1], + restriction_id.channel_bind_ids[0].backend_id.name), + )) else: names.append((name[0], name[1])) return names +class HotelRoomTypeRestrictionAdapter(Component): + _name = 'channel.hotel.room.type.restriction.adapter' + _inherit = 'wubook.adapter' + _apply_on = 'channel.hotel.room.type.restriction' + + def rplan_rplans(self): + return super(HotelRoomTypeRestrictionAdapter, self).rplan_rplans() + + def create_rplan(self, name): + return super(HotelRoomTypeRestrictionAdapter, self).create_rplan(name) + + def delete_rplan(self, external_id): + return super(HotelRoomTypeRestrictionAdapter, self).delete_rplan(external_id) + + def rename_rplan(self, external_id, new_name): + return super(HotelRoomTypeRestrictionAdapter, self).rename_rplan(external_id, new_name) + +class BindingHotelRoomTypeListener(Component): + _name = 'binding.hotel.room.type.restriction.listener' + _inherit = 'base.connector.listener' + _apply_on = ['hotel.room.type.restriction'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + if any(record.channel_bind_ids) and 'name' in fields: + record.channel_bind_ids[0].update_plan_name() + class ChannelBindingHotelRoomTypeRestrictionListener(Component): _name = 'channel.binding.hotel.room.type.restriction.listener' _inherit = 'base.connector.listener' @@ -98,13 +147,13 @@ class ChannelBindingHotelRoomTypeRestrictionListener(Component): @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_create(self, record, fields=None): - record.with_delay(priority=20).create_plan() + record.create_plan() @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_unlink(self, record, fields=None): - record.with_delay(priority=20).delete_plan() + record.delete_plan() @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_write(self, record, fields=None): if 'name' in fields: - record.with_delay(priority=20).update_plan_name() + record.update_plan_name() diff --git a/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py b/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py new file mode 100644 index 000000000..9348347d3 --- /dev/null +++ b/hotel_channel_connector/models/hotel_room_type_restriction/exporter.py @@ -0,0 +1,29 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import Component +from odoo import api, _ +_logger = logging.getLogger(__name__) + +class HotelRoomTypeRestrictionExporter(Component): + _name = 'channel.hotel.room.type.restriction.exporter' + _inherit = 'hotel.channel.exporter' + _apply_on = ['channel.hotel.room.type.restriction'] + _usage = 'hotel.room.type.restriction.exporter' + + @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) + + @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) 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 33b15b3a4..c20db4ce0 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction/importer.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction/importer.py @@ -5,7 +5,6 @@ import logging from datetime import timedelta from odoo.exceptions import ValidationError from odoo.addons.component.core import Component -from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError from odoo.addons.connector.components.mapper import mapping from odoo.addons.hotel import date_utils from odoo import fields, api, _ @@ -18,6 +17,28 @@ class HotelRoomTypeRestrictionImporter(Component): _apply_on = ['channel.hotel.room.type.restriction'] _usage = 'hotel.room.type.restriction.importer' + @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({ + 'wubook_action': False, + 'rules': plan.get('rules'), + }).create(plan_record.values(for_create=True)) + else: + plan_bind.with_context({'wubook_action': False}).write( + plan_record.values()) + count = count + 1 + return count + class HotelRoomTypeRestrictionImportMapper(Component): _name = 'channel.hotel.room.type.restriction.import.mapper' @@ -25,11 +46,8 @@ class HotelRoomTypeRestrictionImportMapper(Component): _apply_on = 'channel.hotel.room.type.restriction' direct = [ - ('no_ota', 'no_ota'), - ('booked', 'booked'), - ('avail', 'avail'), - ('room_type_id', 'room_type_id'), - ('date', 'date') + ('name', 'name'), + ('id', 'external_id'), ] @mapping diff --git a/hotel_channel_connector/models/hotel_room_type_restriction_item/__init__.py b/hotel_channel_connector/models/hotel_room_type_restriction_item/__init__.py index 06e54858b..fe02f8e98 100644 --- a/hotel_channel_connector/models/hotel_room_type_restriction_item/__init__.py +++ b/hotel_channel_connector/models/hotel_room_type_restriction_item/__init__.py @@ -3,3 +3,4 @@ from . import common from . import importer +from . import exporter 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 2f79bfc38..c57c8db29 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,6 +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 ChannelHotelRoomTypeRestrictionItem(models.Model): _name = 'channel.hotel.room.type.restriction.item' @@ -21,10 +22,37 @@ class ChannelHotelRoomTypeRestrictionItem(models.Model): old_name='wpushed') @job(default_channel='root.channel') - @api.multi - def update_channel_pushed(self, status): - self.ensure_one() - self.channel_pushed = status + @api.model + def import_restriction_values(self, backend): + 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=_("Can't fetch plan restrictions from wubook"), + channel_message=err.data['message'], + channel_object_id=backend.restriction_id, + dfrom=backend.restriction_from, dto=backend.restriction_to) + + @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=_("Can't update restrictions in WuBook"), + channel_message=err.data['message']) class HotelRoomTypeRestrictionItem(models.Model): _inherit = 'hotel.room.type.restriction.item' @@ -34,15 +62,41 @@ class HotelRoomTypeRestrictionItem(models.Model): inverse_name='odoo_id', string='Hotel Channel Connector Bindings') -class ChannelBindingHotelRoomTypeRestrictionItemListener(Component): - _name = 'channel.binding.hotel.room.type.restriction.listener' - _inherit = 'base.connector.listener' - _apply_on = ['channel.hotel.room.type.restriction'] +class HotelRoomTypeRestrictionItemAdapter(Component): + _name = 'channel.hotel.room.type.restriction.item.adapter' + _inherit = 'wubook.adapter' + _apply_on = 'channel.hotel.room.type.restriction.item' - @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) - def on_record_create(self, record, fields=None): - record.update_channel_pushed(False) + def wired_rplan_get_rplan_values(self, date_from, date_to, channel_restriction_plan_id): + return super(HotelRoomTypeRestrictionItemAdapter, self).wired_rplan_get_rplan_values( + date_from, + date_to, + channel_restriction_plan_id) + +class BindingHotelRoomTypeRestrictionItemListener(Component): + _name = 'binding.hotel.room.type.restriction.item.listener' + _inherit = 'base.connector.listener' + _apply_on = ['hotel.room.type.restriction.item'] @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_write(self, record, fields=None): - record.update_channel_pushed(False) + fields_to_check = ('min_stay', 'min_stay_arrival', 'max_stay', 'max_stay_arrival', + 'max_stay_arrival', 'closed', 'closed_departure', 'closed_arrival', + 'date') + fields_checked = [elm for elm in fields_to_check if elm in fields] + if any(fields_checked): + record.channel_bind_ids.write({'channel_pushed': False}) + +class ChannelBindingHotelRoomTypeRestrictionItemListener(Component): + _name = 'channel.binding.hotel.room.type.restriction.item.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.hotel.room.type.restriction.item'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + fields_to_check = ('min_stay', 'min_stay_arrival', 'max_stay', 'max_stay_arrival', + 'max_stay_arrival', 'closed', 'closed_departure', 'closed_arrival', + 'date') + fields_checked = [elm for elm in fields_to_check if elm in fields] + if any(fields_checked): + record.channel_pushed = False 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 new file mode 100644 index 000000000..ac44d4c58 --- /dev/null +++ b/hotel_channel_connector/models/hotel_room_type_restriction_item/exporter.py @@ -0,0 +1,93 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta +from odoo.addons.component.core import Component +from odoo.addons.hotel_channel_connector.components.core import ChannelConnectorError +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo import fields, api, _ +_logger = logging.getLogger(__name__) + +class HotelRoomTypeRestrictionItemExporter(Component): + _name = 'channel.hotel.room.type.restriction.item.exporter' + _inherit = 'hotel.channel.exporter' + _apply_on = ['channel.hotel.room.type.restriction.item'] + _usage = 'hotel.room.type.restriction.item.exporter' + + @api.model + def update_restriction(self, binding): + if any(binding.restriction_id.channel_bind_ids): + # FIXME: Supossed that only exists one channel connector per record + binding.channel_pushed = True + return self.backend_adapter.update_rplan_values( + binding.restriction_id.channel_bind_ids[0].external_id, + binding.date, + { + 'min_stay': binding.min_stay or 0, + 'min_stay_arrival': binding.min_stay_arrival or 0, + 'max_stay': binding.max_stay or 0, + 'max_stay_arrival': binding.max_stay_arrival or 0, + 'closed': binding.closed and 1 or 0, + 'closed_arrival': binding.closed_arrival and 1 or 0, + 'closed_departure': binding.closed_departure and 1 or 0, + }) + + @api.model + def push_restriction(self): + channel_room_type_rest_obj = self.env['channel.hotel.room.type.restriction'] + channel_rest_item_obj = self.env['channel.hotel.room.type.restriction.item'] + unpushed = channel_rest_item_obj.search([ + ('channel_pushed', '=', False), + ('date', '>=', fields.Date.today()) + ], order="date ASC") + if any(unpushed): + date_start = fields.Date.from_string(unpushed[0].date) + date_end = fields.Date.from_string(unpushed[-1].date) + days_diff = (date_end-date_start).days + 1 + restrictions = {} + channel_restr_plan_ids = channel_room_type_rest_obj.search([]) + for rp in channel_restr_plan_ids: + restrictions.update({rp.external_id: {}}) + unpushed_rp = channel_rest_item_obj.search([ + ('channel_pushed', '=', False), + ('restriction_id', '=', rp.odoo_id.id) + ]) + room_type_ids = unpushed_rp.mapped('room_type_id') + for room_type in room_type_ids: + if any(room_type.channel_bind_ids): + # FIXME: Supossed that only exists one channel connector per record + room_type_external_id = room_type.channel_bind_ids[0].external_id + restrictions[rp.external_id].update({ + room_type_external_id: [], + }) + for i in range(0, days_diff): + ndate_dt = date_start + timedelta(days=i) + restr = room_type.get_restrictions( + ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), + rp.odoo_id.id) + if restr: + restrictions[rp.external_id][room_type_external_id].append({ + 'min_stay': restr.min_stay or 0, + 'min_stay_arrival': restr.min_stay_arrival or 0, + 'max_stay': restr.max_stay or 0, + 'max_stay_arrival': restr.max_stay_arrival or 0, + 'closed': restr.closed and 1 or 0, + 'closed_arrival': restr.closed_arrival and 1 or 0, + 'closed_departure': restr.closed_departure and 1 or 0, + }) + else: + restrictions[rp.external_id][room_type_external_id].append({}) + _logger.info("==[ODOO->CHANNEL]==== UPDATING 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}) + 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 a50a8868d..4c6a925b8 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 @@ -2,11 +2,13 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import logging -from datetime import timedelta +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.connector.components.mapper import mapping, only_create -from odoo.addons.hotel import date_utils +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT from odoo import fields, api, _ _logger = logging.getLogger(__name__) @@ -17,6 +19,60 @@ class HotelRoomTypeRestrictionImporter(Component): _apply_on = ['channel.hotel.room.type.restriction.item'] _usage = 'hotel.room.type.restriction.item.importer' + # FIXME: Reduce Nested Loops!! + @api.model + def _generate_restriction_items(self, plan_restrictions): + channel_hotel_room_type_obj = self.env['channel.hotel.room.type'] + channel_reserv_restriction_obj = self.env['channel.hotel.room.type.restriction'] + channel_restriction_item_obj = self.env['channel.hotel.room.type.restriction.item'] + restriction_item_mapper = self.component( + usage='import.mapper', + model_name='channel.hotel.room.type.restriction.item') + _logger.info("==[CHANNEL->ODOO]==== RESTRICTIONS ==") + _logger.info(plan_restrictions) + for k_rpid, v_rpid in plan_restrictions.items(): + channel_restriction_id = channel_reserv_restriction_obj.search([ + ('external_id', '=', k_rpid) + ], limit=1) + if channel_restriction_id: + for k_rid, v_rid in v_rpid.items(): + channel_room_type = channel_hotel_room_type_obj.search([ + ('external_id', '=', k_rid) + ], limit=1) + if channel_room_type: + for item in v_rid: + map_record = restriction_item_mapper.map_record(item) + date_dt = datetime.strptime(item['date'], DEFAULT_WUBOOK_DATE_FORMAT) + date_str = date_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) + channel_restriction_item = channel_restriction_item_obj.search([ + ('restriction_id', '=', channel_restriction_id.odoo_id.id), + ('date', '=', date_str), + ('applied_on', '=', '0_room_type'), + ('room_type_id', '=', channel_room_type.odoo_id.id) + ], limit=1) + item.update({ + 'date': date_str, + 'room_type_id': channel_room_type.odoo_id.id, + 'restriction_id': channel_restriction_id.odoo_id.id, + }) + if channel_restriction_item: + channel_restriction_item.with_context({ + 'wubook_action': False}).write(map_record.values()) + else: + channel_restriction_item_obj.with_context({ + 'wubook_action': False + }).create(map_record.values(for_create=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) + class HotelRoomTypeRestrictionItemImportMapper(Component): _name = 'channel.hotel.room.type.restriction.item.import.mapper' _inherit = 'channel.import.mapper' @@ -30,7 +86,6 @@ class HotelRoomTypeRestrictionItemImportMapper(Component): ('closed', 'closed'), ('closed_departure', 'closed_departure'), ('closed_arrival', 'closed_arrival'), - ('room_type_id', 'room_type_id'), ('date', 'date'), ] @@ -39,6 +94,20 @@ class HotelRoomTypeRestrictionItemImportMapper(Component): def applied_on(self, record): return {'applied_on': '0_room_type'} + @only_create + @mapping + def channel_pushed(self, record): + return {'channel_pushed': True} + + + @mapping + def room_type_id(self, record): + return {'room_type_id': record['room_type_id']} + + @mapping + def restriction_id(self, record): + return {'restriction_id': record['restriction_id']} + @mapping def backend_id(self, record): return {'backend_id': self.backend_record.id} diff --git a/hotel_channel_connector/models/inherited_product_pricelist_item.py b/hotel_channel_connector/models/inherited_product_pricelist_item.py deleted file mode 100644 index 49934f850..000000000 --- a/hotel_channel_connector/models/inherited_product_pricelist_item.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright 2018 Alexandre Díaz -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from openerp import models, fields, api, _ -from openerp.exceptions import ValidationError - - -class ProductPricelistItem(models.Model): - _inherit = 'product.pricelist.item' - - is_channel_pushed = fields.Boolean("WuBook Pushed", default=True, readonly=True, - old_name='wpushed') - is_daily_plan = fields.Boolean(related='pricelist_id.channel_bind_ids.is_daily_plan', readonly=True, - old_name='wdaily') - - @api.constrains('fixed_price') - def _check_fixed_price(self): - room_type_obj = self.env['hotel.room.type'] - for record in self: - room_type = room_type_obj.search([ - ('product_id.product_tmpl_id', '=', record.product_tmpl_id.id) - ], limit=1) - if room_type and room_type.channel_room_id and record.compute_price == 'fixed' \ - and record.fixed_price <= 0.0: - raise ValidationError(_("Price need be greater than zero")) - - @api.model - def create(self, vals): - if self._context.get('channel_action', True): - pricelist_id = self.env['product.pricelist'].browse( - vals.get('pricelist_id')) - room_type = self.env['hotel.room.type'].search([ - ('product_id.product_tmpl_id', '=', - vals.get('product_tmpl_id')), - ('channel_room_id', '!=', False) - ]) - if room_type and pricelist_id.channel_plan_id: - vals.update({'is_channel_pushed': False}) - return super(ProductPricelistItem, self).create(vals) - - @api.multi - def write(self, vals): - if self._context.get('channel_action', True): - prices_obj = self.env['product.pricelist'] - for record in self: - pricelist_id = prices_obj.browse(vals.get('pricelist_id')) if \ - vals.get('pricelist_id') else record.pricelist_id - product_tmpl_id = vals.get('product_tmpl_id') or \ - record.product_tmpl_id.id - room_type = self.env['hotel.room.type'].search([ - ('product_id.product_tmpl_id', '=', product_tmpl_id), - ('channel_room_id', '!=', False), - ]) - if room_type and pricelist_id.channel_plan_id: - vals.update({'is_channel_pushed': False}) - return super(ProductPricelistItem, self).write(vals) diff --git a/hotel_channel_connector/models/product_pricelist/__init__.py b/hotel_channel_connector/models/product_pricelist/__init__.py index 257ab04fc..fe02f8e98 100644 --- a/hotel_channel_connector/models/product_pricelist/__init__.py +++ b/hotel_channel_connector/models/product_pricelist/__init__.py @@ -2,3 +2,5 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import common +from . import importer +from . import exporter diff --git a/hotel_channel_connector/models/product_pricelist/common.py b/hotel_channel_connector/models/product_pricelist/common.py index 976e0b338..9f45630da 100644 --- a/hotel_channel_connector/models/product_pricelist/common.py +++ b/hotel_channel_connector/models/product_pricelist/common.py @@ -6,6 +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' @@ -17,7 +18,6 @@ class ChannelProductPricelist(models.Model): string='Pricelist', required=True, ondelete='cascade') - channel_plan_id = fields.Char("Channel Plan ID", readonly=True, old_name='wpid') is_daily_plan = fields.Boolean("Channel Daily Plan", default=True, old_name='wdaily_plan') @job(default_channel='root.channel') @@ -25,52 +25,65 @@ class ChannelProductPricelist(models.Model): @api.multi def create_plan(self): self.ensure_one() - if self._context.get('channel_action', True): + if not self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='product.pricelist.exporter') try: - channel_plan_id = adapter.create_plan(self.name, - self.is_daily_plan and 1 or 0) - if channel_plan_id: - self.channel_plan_id = channel_plan_id - except ValidationError as e: - self.create_issue('room', "Can't create plan on channel", "sss") + exporter.create_plan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't create pricelist plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') @related_action(action='related_action_unwrap_binding') @api.multi def update_plan_name(self): self.ensure_one() - if self._context.get('channel_action', True): + if self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='product.pricelist.exporter') try: - adapter.update_plan_name( - self.channel_plan_id, - self.name) - except ValidationError as e: - self.create_issue('room', "Can't update plan name on channel", "sss") + exporter.rename_plan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't modify pricelist plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') @related_action(action='related_action_unwrap_binding') @api.multi def delete_plan(self): self.ensure_one() - if self._context.get('channel_action', True) and self.channel_room_id: + if self.external_id: with self.backend_id.work_on(self._name) as work: - adapter = work.component(usage='backend.adapter') + exporter = work.component(usage='product.pricelist.exporter') try: - adapter.delete_plan(self.channel_plan_id) - except ValidationError as e: - self.create_issue('room', "Can't delete plan on channel", "sss") + exporter.delete_plan(self) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_id.id, + section='restriction', + internal_message=_("Can't delete pricelist plan in WuBook"), + channel_message=err.data['message']) @job(default_channel='root.channel') - @api.multi - def import_price_plans(self): - if self._context.get('channel_action', True): - with self.backend_id.work_on(self._name) as work: - importer = work.component(usage='channel.importer') + @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=_("Can't get pricing plans from wubook"), + channel_message=err.data['message']) class ProductPricelist(models.Model): _inherit = 'product.pricelist' @@ -83,19 +96,47 @@ class ProductPricelist(models.Model): @api.multi @api.depends('name') def name_get(self): - self.ensure_one() pricelist_obj = self.env['product.pricelist'] org_names = super(ProductPricelist, self).name_get() names = [] for name in org_names: priclist_id = pricelist_obj.browse(name[0]) if any(priclist_id.channel_bind_ids) and \ - priclist_id.channel_bind_ids[0].channel_plan_id: - names.append((name[0], '%s (Channel)' % name[1])) + priclist_id.channel_bind_ids[0].external_id: + names.append((name[0], '%s (%s Backend)' % ( + name[1], + priclist_id.channel_bind_ids[0].backend_id.name))) else: names.append((name[0], name[1])) return names +class ProductPricelistAdapter(Component): + _name = 'channel.product.pricelist.adapter' + _inherit = 'wubook.adapter' + _apply_on = 'channel.product.pricelist' + + def get_pricing_plans(self): + return super(ProductPricelistAdapter, self).get_pricing_plans() + + def create_plan(self, name): + return super(ProductPricelistAdapter, self).create_plan(name) + + def delete_plan(self, external_id): + return super(ProductPricelistAdapter, self).delete_plan(external_id) + + def rename_plan(self, external_id, new_name): + return super(ProductPricelistAdapter, self).rename_plan(external_id, new_name) + +class BindingProductPricelistListener(Component): + _name = 'binding.product.pricelist.listener' + _inherit = 'base.connector.listener' + _apply_on = ['product.pricelist'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + if any(record.channel_bind_ids) and 'name' in fields: + record.channel_bind_ids[0].update_plan_name() + class ChannelBindingProductPricelistListener(Component): _name = 'channel.binding.product.pricelist.listener' _inherit = 'base.connector.listener' @@ -103,13 +144,13 @@ class ChannelBindingProductPricelistListener(Component): @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_create(self, record, fields=None): - record.with_delay(priority=20).create_plan() + record.create_plan() @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_unlink(self, record, fields=None): - record.with_delay(priority=20).delete_plan() + record.delete_plan() @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) def on_record_write(self, record, fields=None): if 'name' in fields: - record.with_delay(priority=20).update_plan_name() + record.update_plan_name() diff --git a/hotel_channel_connector/models/product_pricelist/exporter.py b/hotel_channel_connector/models/product_pricelist/exporter.py new file mode 100644 index 000000000..fb990cfb2 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist/exporter.py @@ -0,0 +1,28 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from odoo.addons.component.core import Component +from odoo import api, _ +_logger = logging.getLogger(__name__) + +class ProductPricelistExporter(Component): + _name = 'channel.product.pricelist.exporter' + _inherit = 'hotel.channel.exporter' + _apply_on = ['channel.product.pricelist'] + _usage = 'product.pricelist.exporter' + + @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) + + @api.model + def create_plan(self, binding): + external_id = self.backend_adapter.create_plan(binding.name) + binding.external_id = external_id diff --git a/hotel_channel_connector/models/product_pricelist/importer.py b/hotel_channel_connector/models/product_pricelist/importer.py new file mode 100644 index 000000000..c99add698 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist/importer.py @@ -0,0 +1,57 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import datetime, timedelta +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo import fields, api, _ +_logger = logging.getLogger(__name__) + + +class ProductPricelistImporter(Component): + _name = 'channel.product.pricelist.importer' + _inherit = 'hotel.channel.importer' + _apply_on = ['channel.product.pricelist'] + _usage = 'product.pricelist.importer' + + @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({ + 'wubook_action': False}).create(plan_record.values(for_create=True)) + else: + channel_product_listprice_obj.write(plan_record.values()) + count = count + 1 + return count + + +class ProductPricelistImportMapper(Component): + _name = 'channel.product.pricelist.import.mapper' + _inherit = 'channel.import.mapper' + _apply_on = 'channel.product.pricelist' + + direct = [ + ('id', 'external_id'), + ('name', 'name'), + ('daily', 'is_daily_plan'), + ] + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} diff --git a/hotel_channel_connector/models/product_pricelist_item/__init__.py b/hotel_channel_connector/models/product_pricelist_item/__init__.py new file mode 100644 index 000000000..fe02f8e98 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist_item/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import common +from . import importer +from . import exporter diff --git a/hotel_channel_connector/models/product_pricelist_item/common.py b/hotel_channel_connector/models/product_pricelist_item/common.py new file mode 100644 index 000000000..03457d231 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist_item/common.py @@ -0,0 +1,98 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models, fields +from odoo.exceptions import ValidationError +from odoo.addons.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' + _inherit = 'channel.binding' + _inherits = {'product.pricelist.item': 'odoo_id'} + _description = 'Channel Product Pricelist Item' + + odoo_id = fields.Many2one(comodel_name='product.pricelist.item', + string='Hotel Product Pricelist Item', + required=True, + ondelete='cascade') + channel_pushed = fields.Boolean("Channel Pushed", readonly=True, default=False, + old_name='wpushed') + + @job(default_channel='root.channel') + @api.model + def import_pricelist_values(self, backend): + 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, + backend.pricelist_from, + backend.pricelist_to) + except ChannelConnectorError as err: + self.create_issue( + backend=backend.id, + section='pricelist', + internal_message="Can't fetch plan prices from wubook!", + channel_message=err.data['message'], + channel_object_id=backend.pricelist_id.external_id, + dfrom=backend.pricelist_from, + dto=backend.pricelist_to) + return False + + @job(default_channel='root.channel') + @api.model + def push_pricelist(self, backend): + with backend.work_on(self._name) as work: + exporter = work.component(usage='product.pricelist.item.exporter') + return exporter.push_pricelist() + +class ProductPricelistItem(models.Model): + _inherit = 'product.pricelist.item' + + channel_bind_ids = fields.One2many( + comodel_name='channel.product.pricelist.item', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') + +class ProducrPricelistItemAdapter(Component): + _name = 'channel.product.pricelist.item.adapter' + _inherit = 'wubook.adapter' + _apply_on = 'channel.product.pricelist.item' + + def fetch_plan_prices(self, external_id, date_from, date_to, rooms): + return super(ProducrPricelistItemAdapter, self).fetch_plan_prices( + external_id, + date_from, + date_to, + rooms) + +class BindingProductPricelistItemListener(Component): + _name = 'binding.product.pricelist.item.listener' + _inherit = 'base.connector.listener' + _apply_on = ['product.pricelist.item'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + fields_to_check = ('date_start', 'date_end', 'fixed_price', 'product_tmpl_id') + fields_checked = [elm for elm in fields_to_check if elm in fields] + if any(fields_checked): + record.channel_bind_ids.write({'channel_pushed': False}) + +class ChannelBindingProductPricelistItemListener(Component): + _name = 'channel.binding.product.pricelist.item.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.product.pricelist.item'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + fields_to_check = ('date_start', 'date_end', 'fixed_price', 'product_tmpl_id') + fields_checked = [elm for elm in fields_to_check if elm in fields] + if any(fields_checked): + record.channel_pushed = False diff --git a/hotel_channel_connector/models/product_pricelist_item/exporter.py b/hotel_channel_connector/models/product_pricelist_item/exporter.py new file mode 100644 index 000000000..1498a354b --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist_item/exporter.py @@ -0,0 +1,106 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta +from odoo.addons.component.core import Component +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo import fields, api, _ +_logger = logging.getLogger(__name__) + +class ProductPricelistItemExporter(Component): + _name = 'channel.product.pricelist.item.exporter' + _inherit = 'hotel.channel.exporter' + _apply_on = ['channel.product.pricelist.item'] + _usage = 'product.pricelist.item.exporter' + + @api.model + def update_restriction(self, binding): + if any(binding.restriction_id.channel_bind_ids): + try: + # FIXME: Supossed that only exists one channel connector per record + binding.channel_pushed = True + return self.backend_adapter.update_rplan_values( + binding.restriction_id.channel_bind_ids[0].external_id, + binding.date, + { + 'min_stay': binding.min_stay or 0, + 'min_stay_arrival': binding.min_stay_arrival or 0, + 'max_stay': binding.max_stay or 0, + 'max_stay_arrival': binding.max_stay_arrival or 0, + 'closed': binding.closed and 1 or 0, + 'closed_arrival': binding.closed_arrival and 1 or 0, + 'closed_departure': binding.closed_departure and 1 or 0, + }) + except ChannelConnectorError as err: + self.create_issue( + backend=self.backend_adapter.id, + section='restriction', + internal_message=_("Can't update restriction in WuBook"), + channel_message=err.data['message']) + + @api.model + def push_restriction(self): + channel_room_type_rest_obj = self.env['channel.hotel.room.type.restriction'] + channel_rest_item_obj = self.env['channel.hotel.room.type.restriction.item'] + unpushed = channel_rest_item_obj.search([ + ('channel_pushed', '=', False), + ('date', '>=', fields.Date.today()) + ], order="date ASC") + if any(unpushed): + date_start = fields.Date.from_string(unpushed[0].date) + date_end = fields.Date.from_string(unpushed[-1].date) + days_diff = (date_end-date_start).days + 1 + restrictions = {} + channel_restr_plan_ids = channel_room_type_rest_obj.search([]) + for rp in channel_restr_plan_ids: + restrictions.update({rp.external_id: {}}) + unpushed_rp = channel_rest_item_obj.search([ + ('channel_pushed', '=', False), + ('restriction_id', '=', rp.odoo_id.id) + ]) + room_type_ids = unpushed_rp.mapped('room_type_id') + for room_type in room_type_ids: + if any(room_type.channel_bind_ids): + # FIXME: Supossed that only exists one channel connector per record + room_type_external_id = room_type.channel_bind_ids[0].external_id + restrictions[rp.external_id].update({ + room_type_external_id: [], + }) + for i in range(0, days_diff): + ndate_dt = date_start + timedelta(days=i) + restr = room_type.get_restrictions( + ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), + rp.odoo_id.id) + if restr: + restrictions[rp.external_id][room_type_external_id].append({ + 'min_stay': restr.min_stay or 0, + 'min_stay_arrival': restr.min_stay_arrival or 0, + 'max_stay': restr.max_stay or 0, + 'max_stay_arrival': restr.max_stay_arrival or 0, + 'closed': restr.closed and 1 or 0, + 'closed_arrival': restr.closed_arrival and 1 or 0, + 'closed_departure': restr.closed_departure and 1 or 0, + }) + else: + restrictions[rp.external_id][room_type_external_id].append({}) + _logger.info("==[ODOO->CHANNEL]==== UPDATING RESTRICTIONS ==") + _logger.info(restrictions) + for k_res, v_res in restrictions.items(): + if any(v_res): + try: + 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( + backend=self.backend_adapter.id, + section='restriction', + internal_message=_("Can't update restrictions in WuBook"), + channel_message=err.data['message']) + unpushed.with_context({ + 'wubook_action': False}).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 new file mode 100644 index 000000000..bb7bb30b7 --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist_item/importer.py @@ -0,0 +1,120 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging +from datetime import timedelta +from odoo.addons.component.core import Component +from odoo.addons.connector.components.mapper import mapping, only_create +from odoo.addons.hotel_channel_connector.components.backend_adapter import ( + DEFAULT_WUBOOK_DATE_FORMAT) +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT +from odoo import fields, api, _ +_logger = logging.getLogger(__name__) + + +class ProductPricelistItemImporter(Component): + _name = 'channel.product.pricelist.item.importer' + _inherit = 'hotel.channel.importer' + _apply_on = ['channel.product.pricelist.item'] + _usage = 'product.pricelist.item.importer' + + @api.model + def _generate_pricelist_items(self, channel_plan_id, date_from, date_to, plan_prices): + channel_hotel_room_type_obj = self.env['channel.hotel.room.type'] + pricelist_bind = self.env['channel.product.pricelist'].search([ + ('external_id', '=', channel_plan_id) + ], limit=1) + pricelist_item_mapper = self.component( + usage='import.mapper', + model_name='channel.product.pricelist.item') + if pricelist_bind: + channel_pricelist_item_obj = self.env['channel.product.pricelist.item'] + dfrom_dt = fields.Date.from_string(date_from) + dto_dt = fields.Date.from_string(date_to) + days_diff = (dto_dt-dfrom_dt).days + 1 + for i in range(0, days_diff): + ndate_dt = dfrom_dt + timedelta(days=i) + for k_rid, v_rid in plan_prices.items(): + channel_room_type = channel_hotel_room_type_obj.search([ + ('external_id', '=', k_rid) + ], limit=1) + if channel_room_type: + ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT) + item = { + 'price': plan_prices[k_rid][i], + 'channel_room_type': channel_room_type, + 'pricelist_id': pricelist_bind.odoo_id.id, + 'date': ndate_str, + } + map_record = pricelist_item_mapper.map_record(item) + pricelist_item = channel_pricelist_item_obj.search([ + ('pricelist_id', '=', pricelist_bind.odoo_id.id), + ('date_start', '=', ndate_str), + ('date_end', '=', ndate_str), + ('compute_price', '=', 'fixed'), + ('applied_on', '=', '1_product'), + ('product_tmpl_id', '=', + channel_room_type.product_id.product_tmpl_id.id) + ], limit=1) + if pricelist_item: + pricelist_item.with_context({ + 'wubook_action': False}).write(map_record.values()) + else: + channel_pricelist_item_obj.with_context({ + 'wubook_action': False}).create(map_record.values(for_create=True)) + return True + + @api.model + def import_all_pricelist_values(self, date_from, date_to, rooms=None): + external_ids = self.env['channel.product.pricelist'].search([]).mapped('external_id') + for external_id in external_ids: + if external_id: + self.import_pricelist_values(external_id, date_from, date_to, rooms=rooms) + return True + + @api.model + 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) + +class ProductPricelistItemImportMapper(Component): + _name = 'channel.product.pricelist.item.import.mapper' + _inherit = 'channel.import.mapper' + _apply_on = 'channel.product.pricelist.item' + + direct = [ + ('price', 'fixed_price'), + ('date', 'date_start'), + ('date', 'date_end'), + ] + + @only_create + @mapping + def compute_price(self, record): + return {'compute_price': 'fixed'} + + @only_create + @mapping + def channel_pushed(self, record): + return {'channel_pushed': True} + + @only_create + @mapping + def applied_on(self, record): + return {'applied_on': '1_product'} + + @mapping + def product_tmpl_id(self, record): + return {'product_tmpl_id': record['channel_room_type'].product_id.product_tmpl_id.id} + + @mapping + def pricelist_id(self, record): + return {'pricelist_id': record['pricelist_id']} + + @mapping + def backend_id(self, record): + return {'backend_id': self.backend_record.id} diff --git a/hotel_channel_connector/models/res_config.py b/hotel_channel_connector/models/res_config.py deleted file mode 100644 index 25de1266d..000000000 --- a/hotel_channel_connector/models/res_config.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2018 Alexandre Díaz -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import models, fields, api - - -class HotelConfiguration(models.TransientModel): - _inherit = 'res.config.settings' - - default_channel_connector = fields.Many2one( - 'channel.backend', - 'Default Channel Connector Backend') - - @api.multi - def set_values(self): - super(HotelConfiguration, self).set_values() - - self.env['ir.default'].sudo().set( - 'res.config.settings', 'default_channel_connector', - self.default_channel_connector.id) - - @api.model - def get_values(self): - res = super(HotelConfiguration, self).get_values() - - # ONLY FOR v11. DO NOT FORWARD-PORT - default_channel_connector = self.env['ir.default'].sudo().get( - 'res.config.settings', 'default_channel_connector') - - res.update( - default_channel_connector=default_channel_connector, - ) - return res diff --git a/hotel_channel_connector/views/channel_connector_backend_views.xml b/hotel_channel_connector/views/channel_connector_backend_views.xml index 0728b2bf1..76f7bd117 100644 --- a/hotel_channel_connector/views/channel_connector_backend_views.xml +++ b/hotel_channel_connector/views/channel_connector_backend_views.xml @@ -28,6 +28,8 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hotel_channel_connector/views/channel_hotel_reservation_views.xml b/hotel_channel_connector/views/channel_hotel_reservation_views.xml index f75d53daa..bf6057427 100644 --- a/hotel_channel_connector/views/channel_hotel_reservation_views.xml +++ b/hotel_channel_connector/views/channel_hotel_reservation_views.xml @@ -7,7 +7,7 @@
- + diff --git a/hotel_channel_connector/views/channel_hotel_room_type_availability_views.xml b/hotel_channel_connector/views/channel_hotel_room_type_availability_views.xml index d55fa121f..c8e34c01c 100644 --- a/hotel_channel_connector/views/channel_hotel_room_type_availability_views.xml +++ b/hotel_channel_connector/views/channel_hotel_room_type_availability_views.xml @@ -6,6 +6,10 @@ channel.hotel.room.type.availability + + + + diff --git a/hotel_channel_connector/views/channel_hotel_room_type_restriction_item_views.xml b/hotel_channel_connector/views/channel_hotel_room_type_restriction_item_views.xml new file mode 100644 index 000000000..da7933657 --- /dev/null +++ b/hotel_channel_connector/views/channel_hotel_room_type_restriction_item_views.xml @@ -0,0 +1,30 @@ + + + + + channel.hotel.room.type.restriction.item.form + channel.hotel.room.type.restriction.item + + + + + + + + + + + + + + + channel.hotel.room.type.restriction.item.tree + channel.hotel.room.type.restriction.item + + + + + + + + diff --git a/hotel_channel_connector/views/channel_hotel_room_type_restriction_views.xml b/hotel_channel_connector/views/channel_hotel_room_type_restriction_views.xml index 662cc317c..d8956faad 100644 --- a/hotel_channel_connector/views/channel_hotel_room_type_restriction_views.xml +++ b/hotel_channel_connector/views/channel_hotel_room_type_restriction_views.xml @@ -7,8 +7,11 @@
- - + + + + +
diff --git a/hotel_channel_connector/views/channel_hotel_room_type_views.xml b/hotel_channel_connector/views/channel_hotel_room_type_views.xml index 846964b59..5cc063650 100644 --- a/hotel_channel_connector/views/channel_hotel_room_type_views.xml +++ b/hotel_channel_connector/views/channel_hotel_room_type_views.xml @@ -7,10 +7,11 @@
+ - + diff --git a/hotel_channel_connector/views/channel_ota_info_views.xml b/hotel_channel_connector/views/channel_ota_info_views.xml index 8dcc4dff8..d6a152922 100644 --- a/hotel_channel_connector/views/channel_ota_info_views.xml +++ b/hotel_channel_connector/views/channel_ota_info_views.xml @@ -8,6 +8,10 @@ + + + + @@ -30,11 +34,4 @@ - - Hotel Channel Connector OTA's Info - channel.ota.info - form - tree,form - - diff --git a/hotel_channel_connector/views/channel_product_pricelist_item_views.xml b/hotel_channel_connector/views/channel_product_pricelist_item_views.xml new file mode 100644 index 000000000..b8cb4554d --- /dev/null +++ b/hotel_channel_connector/views/channel_product_pricelist_item_views.xml @@ -0,0 +1,27 @@ + + + + + channel.product.pricelist.item.form + channel.product.pricelist.item + + + + + + + + + + + + channel.hotel.product.pricelist.item.tree + channel.product.pricelist.item + + + + + + + + diff --git a/hotel_channel_connector/views/channel_product_pricelist_views.xml b/hotel_channel_connector/views/channel_product_pricelist_views.xml index 1e64681d2..5f23c0c21 100644 --- a/hotel_channel_connector/views/channel_product_pricelist_views.xml +++ b/hotel_channel_connector/views/channel_product_pricelist_views.xml @@ -7,14 +7,18 @@
- + + + + +
- + channel.hotel.product.pricelist.tree channel.product.pricelist diff --git a/hotel_channel_connector/views/hotel_channel_connector_issue_views.xml b/hotel_channel_connector/views/hotel_channel_connector_issue_views.xml index 5732014df..10e854da5 100644 --- a/hotel_channel_connector/views/hotel_channel_connector_issue_views.xml +++ b/hotel_channel_connector/views/hotel_channel_connector_issue_views.xml @@ -18,6 +18,9 @@ class="oe_stat_button" icon="fa-warning" attrs="{'invisible':['|', ['section', '!=', 'reservation'], ['channel_object_id', '=', False]]}"/>
+ + + @@ -42,6 +45,7 @@ tree + @@ -58,6 +62,7 @@ hotel.channel.connector.issue + diff --git a/hotel_channel_connector/views/inherited_hotel_reservation_views.xml b/hotel_channel_connector/views/inherited_hotel_reservation_views.xml index 969ceb366..6b78a11c2 100644 --- a/hotel_channel_connector/views/inherited_hotel_reservation_views.xml +++ b/hotel_channel_connector/views/inherited_hotel_reservation_views.xml @@ -23,7 +23,7 @@
- + diff --git a/hotel_channel_connector/views/inherited_hotel_room_type_availability_views.xml b/hotel_channel_connector/views/inherited_hotel_room_type_availability_views.xml index c93b12a18..d7f17c58e 100644 --- a/hotel_channel_connector/views/inherited_hotel_room_type_availability_views.xml +++ b/hotel_channel_connector/views/inherited_hotel_room_type_availability_views.xml @@ -8,7 +8,7 @@ - + diff --git a/hotel_channel_connector/views/inherited_hotel_room_type_restriction_item_views.xml b/hotel_channel_connector/views/inherited_hotel_room_type_restriction_item_views.xml new file mode 100644 index 000000000..f28fe7e7f --- /dev/null +++ b/hotel_channel_connector/views/inherited_hotel_room_type_restriction_item_views.xml @@ -0,0 +1,24 @@ + + + + + hotel.room.type.restriction.item + + + + + + + + + + + + + + + + + + + diff --git a/hotel_channel_connector/views/inherited_hotel_room_type_restriction_views.xml b/hotel_channel_connector/views/inherited_hotel_room_type_restriction_views.xml index 86ef97d11..d640d760f 100644 --- a/hotel_channel_connector/views/inherited_hotel_room_type_restriction_views.xml +++ b/hotel_channel_connector/views/inherited_hotel_room_type_restriction_views.xml @@ -5,11 +5,19 @@ hotel.room.type.restriction - -
-
-
+ + + + + + + + + + + + +
diff --git a/hotel_channel_connector/views/inherited_hotel_room_type_views.xml b/hotel_channel_connector/views/inherited_hotel_room_type_views.xml index e75a7a7c0..70251b4b6 100644 --- a/hotel_channel_connector/views/inherited_hotel_room_type_views.xml +++ b/hotel_channel_connector/views/inherited_hotel_room_type_views.xml @@ -7,7 +7,7 @@ - + diff --git a/hotel_channel_connector/views/inherited_product_pricelist_item_views.xml b/hotel_channel_connector/views/inherited_product_pricelist_item_views.xml index 696e56773..9f93bdc2b 100644 --- a/hotel_channel_connector/views/inherited_product_pricelist_item_views.xml +++ b/hotel_channel_connector/views/inherited_product_pricelist_item_views.xml @@ -5,12 +5,19 @@ product.pricelist.item - - - - - - {'readonly': [('is_daily_plan', '=', True)]} + + + + + + + + + + + + + diff --git a/hotel_channel_connector/views/inherited_product_pricelist_views.xml b/hotel_channel_connector/views/inherited_product_pricelist_views.xml index ab4882128..0f1018f0b 100644 --- a/hotel_channel_connector/views/inherited_product_pricelist_views.xml +++ b/hotel_channel_connector/views/inherited_product_pricelist_views.xml @@ -5,14 +5,19 @@ product.pricelist - -
-
-
- - - + + + + + + + + + + + + +