From 29eca3c98878618f729861baca6ac908f1551a61 Mon Sep 17 00:00:00 2001 From: QS5ELkMu Date: Sat, 18 Aug 2018 02:46:26 +0200 Subject: [PATCH] [WIP][MIG][11.0] Odoo Connector --- .../models/inherited_hotel_reservation.py | 2 +- hotel_channel_connector/components/core.py | 10 +- .../models/channel_backend/__init__.py | 3 + .../models/channel_backend/common.py | 37 +++ .../models/channel_binding/__init__.py | 3 + .../models/channel_binding/common.py | 17 ++ .../models/hotel_channel_connector_issue.py | 3 +- .../models/hotel_reservation/__init__.py | 3 + .../common.py} | 213 ++++++++++-------- .../models/hotel_virtual_room/__init__.py | 3 + .../models/hotel_virtual_room/common.py | 139 ++++++++++++ .../__init__.py | 3 + .../hotel_virtual_room_availability/common.py | 116 ++++++++++ .../__init__.py | 3 + .../hotel_virtual_room_restriction/common.py | 106 +++++++++ .../models/inherited_hotel_folio.py | 57 ++--- .../models/inherited_hotel_virtual_room.py | 111 --------- .../models/inherited_product_pricelist.py | 134 ----------- .../inherited_product_pricelist_item.py | 39 ++-- .../models/inherited_res_partner.py | 15 +- .../inherited_reservation_restriction.py | 125 ---------- .../inherited_reservation_restriction_item.py | 7 +- .../inherited_virtual_room_availability.py | 16 +- .../models/product_pricelist/__init__.py | 3 + .../models/product_pricelist/common.py | 107 +++++++++ hotel_channel_connector/models/res_config.py | 42 ++-- .../models/wubook_channel_info.py | 14 +- .../tests/test_hotel_folio_model.py | 4 +- .../tests/test_hotel_reservation_model.py | 6 +- .../wizard/inherited_duplicate_reservation.py | 4 +- ...nherited_massive_price_reservation_days.py | 2 +- 31 files changed, 776 insertions(+), 571 deletions(-) create mode 100644 hotel_channel_connector/models/channel_backend/__init__.py create mode 100644 hotel_channel_connector/models/channel_backend/common.py create mode 100644 hotel_channel_connector/models/channel_binding/__init__.py create mode 100644 hotel_channel_connector/models/channel_binding/common.py create mode 100644 hotel_channel_connector/models/hotel_reservation/__init__.py rename hotel_channel_connector/models/{inherited_hotel_reservation.py => hotel_reservation/common.py} (52%) create mode 100644 hotel_channel_connector/models/hotel_virtual_room/__init__.py create mode 100644 hotel_channel_connector/models/hotel_virtual_room/common.py create mode 100644 hotel_channel_connector/models/hotel_virtual_room_availability/__init__.py create mode 100644 hotel_channel_connector/models/hotel_virtual_room_availability/common.py create mode 100644 hotel_channel_connector/models/hotel_virtual_room_restriction/__init__.py create mode 100644 hotel_channel_connector/models/hotel_virtual_room_restriction/common.py delete mode 100644 hotel_channel_connector/models/inherited_hotel_virtual_room.py delete mode 100644 hotel_channel_connector/models/inherited_product_pricelist.py delete mode 100644 hotel_channel_connector/models/inherited_reservation_restriction.py create mode 100644 hotel_channel_connector/models/product_pricelist/__init__.py create mode 100644 hotel_channel_connector/models/product_pricelist/common.py diff --git a/hotel_calendar_channel_connector/models/inherited_hotel_reservation.py b/hotel_calendar_channel_connector/models/inherited_hotel_reservation.py index 94856adb4..fe396c641 100644 --- a/hotel_calendar_channel_connector/models/inherited_hotel_reservation.py +++ b/hotel_calendar_channel_connector/models/inherited_hotel_reservation.py @@ -89,7 +89,7 @@ class HotelReservation(models.Model): 'partner_phone': record.partner_id.mobile or record.partner_id.phone or _('Undefined'), 'state': record.state, - 'fix_days': record.splitted or record.wis_from_channel, + 'fix_days': record.splitted or record.is_from_ota, 'overbooking': record.overbooking, 'price': record.folio_id.amount_total, 'wrid': record.wrid, diff --git a/hotel_channel_connector/components/core.py b/hotel_channel_connector/components/core.py index 7cdea7605..5502f0b40 100644 --- a/hotel_channel_connector/components/core.py +++ b/hotel_channel_connector/components/core.py @@ -7,13 +7,13 @@ class BaseHotelChannelConnectorComponent(AbstractComponent): _collection = 'hotel.channel.backend' @api.model - def create_issue(self, section, message, wmessage, wid=False, + 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, 'message': message, - 'wid': wid, - 'wmessage': wmessage, - 'date_start': dfrom and dfrom.strftime(DEFAULT_SERVER_DATE_FORMAT), - 'date_end': dto and dto.strftime(DEFAULT_SERVER_DATE_FORMAT), + 'channel_object_id': channel_object_id, + 'channel_message': channel_message, + 'date_start': dfrom, + 'date_end': dto, }) diff --git a/hotel_channel_connector/models/channel_backend/__init__.py b/hotel_channel_connector/models/channel_backend/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/channel_backend/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/channel_backend/common.py b/hotel_channel_connector/models/channel_backend/common.py new file mode 100644 index 000000000..a8ebce94f --- /dev/null +++ b/hotel_channel_connector/models/channel_backend/common.py @@ -0,0 +1,37 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +class ChannelBackend(models.Model): + _name = 'channel.backend' + _description = 'Hotel Channel Backend' + _inherit = 'connector.backend' + + @contextmanager + @api.multi + def work_on(self, model_name, **kwargs): + self.ensure_one() + lang = self.default_lang_id + if lang.code != self.env.context.get('lang'): + self = self.with_context(lang=lang.code) + user = self.env['ir.default'].sudo().get( + 'res.config.settings', 'hotel_connector_user') + passwd = self.env['ir.default'].sudo().get( + 'res.config.settings', 'hotel_connector_passwd') + lcode = self.env['ir.default'].sudo().get( + 'res.config.settings', 'hotel_connector_lcode') + pkey = self.env['ir.default'].sudo().get( + 'res.config.settings', 'hotel_connector_pkey') + server_addr = self.env['ir.default'].sudo().get( + 'res.config.settings', 'hotel_connector_server') + wubook_login = WuBookLogin( + server_addr, + user, + passwd, + lcode, + pkey + ) + with WuBookAdapter(wubook_login) as channel_api: + _super = super(ChannelBackend, self) + # from the components we'll be able to do: self.work.magento_api + with _super.work_on(model_name, **kwargs) as work: + yield work diff --git a/hotel_channel_connector/models/channel_binding/__init__.py b/hotel_channel_connector/models/channel_binding/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/channel_binding/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/channel_binding/common.py b/hotel_channel_connector/models/channel_binding/common.py new file mode 100644 index 000000000..23a7015c8 --- /dev/null +++ b/hotel_channel_connector/models/channel_binding/common.py @@ -0,0 +1,17 @@ +# 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.addons.queue_job.job import job, related_action + + +class ChannelBinding(models.AbstractModel): + _name = 'channel.binding' + _inherit = 'external.binding' + _description = 'Hotel Channel Connector Binding (abstract)' + + backend_id = fields.Many2one( + comodel_name='channel.backend', + string='Hotel Channel Connector Backend', + required=True, + ondelete='restrict') diff --git a/hotel_channel_connector/models/hotel_channel_connector_issue.py b/hotel_channel_connector/models/hotel_channel_connector_issue.py index 2d68b4404..293bc7636 100644 --- a/hotel_channel_connector/models/hotel_channel_connector_issue.py +++ b/hotel_channel_connector/models/hotel_channel_connector_issue.py @@ -20,8 +20,7 @@ class HotelChannelConnectorIssue(models.Model): date_start = fields.Date("From", readonly=True) date_end = fields.Date("To", readonly=True) channel_object_id = fields.Char("Channel Object ID", old_name='wid', readonly=True) - channel_connector_message = fields.Char("Channel Connector Message", - old_name='wmessage', readonly=True) + channel_message = fields.Char("Channel Message", old_name='wmessage', readonly=True) @api.multi def mark_readed(self): diff --git a/hotel_channel_connector/models/hotel_reservation/__init__.py b/hotel_channel_connector/models/hotel_reservation/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/hotel_reservation/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/inherited_hotel_reservation.py b/hotel_channel_connector/models/hotel_reservation/common.py similarity index 52% rename from hotel_channel_connector/models/inherited_hotel_reservation.py rename to hotel_channel_connector/models/hotel_reservation/common.py index 1ded7f79d..ae8f05f68 100644 --- a/hotel_channel_connector/models/inherited_hotel_reservation.py +++ b/hotel_channel_connector/models/hotel_reservation/common.py @@ -1,56 +1,38 @@ # Copyright 2018 Alexandre Díaz # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import logging -from openerp import models, fields, api, _ -from openerp.exceptions import UserError, ValidationError -from ..wubook import ( - DEFAULT_WUBOOK_DATE_FORMAT, - WUBOOK_STATUS_CONFIRMED, - WUBOOK_STATUS_WAITING, - WUBOOK_STATUS_REFUSED, - WUBOOK_STATUS_ACCEPTED, - WUBOOK_STATUS_CANCELLED, - WUBOOK_STATUS_CANCELLED_PENALTY, - WUBOOK_STATUS_BAD) -from odoo.addons.hotel import date_utils -_logger = logging.getLogger(__name__) +class ChannelHotelReservation(models.Model): + _name = 'channel.hotel.reservation' + _inherit = 'channel.binding' + _inherits = {'hotel.reservation': 'odoo_id'} + _description = 'Channel Hotel Reservation' -class HotelReservation(models.Model): - _inherit = 'hotel.reservation' - - @api.multi - def set_access_for_wubook_fields(self): - for rec in self: - user = self.env['res.users'].browse(self.env.uid) - rec.able_to_modify_wubook = user.has_group('base.group_system') - - @api.depends('channel_type','wchannel_id') - def _get_origin_sale(self): + @api.depends('channel_reservation_id', 'ota_id') + def _is_from_ota(self): for record in self: - if not record.channel_type: - record.channel_type = 'door' - record.origin_sale = (record.channel_type != 'web' or not record.wchannel_id) and \ - dict(self.fields_get(allfields=['channel_type'])['channel_type']['selection'])[record.channel_type] \ - or record.wchannel_id.name + record.is_from_ota = (record.channel_reservation_id and record.ota_id) - @api.depends('wrid', 'wchannel_id') - def _is_from_channel(self): - for record in self: - record.wis_from_channel = (record.wrid and record.wchannel_id) - - wrid = fields.Char("WuBook Reservation ID", readonly=True) - wchannel_id = fields.Many2one('wubook.channel.info', - string='WuBook Channel ID', - readonly=True) - wchannel_reservation_code = fields.Char("WuBook Channel Reservation Code", - readonly=True) - wis_from_channel = fields.Boolean('WuBooK Is From Channel', - compute=_is_from_channel, store=False, - readonly=True) + odoo_id = fields.Many2one(comodel_names='hotel.reservation', + string='Reservation', + required=True, + ondelete='cascade') + channel_reservation_id = fields.Char("Channel Reservation ID", readonly=True, old_name='wrid') + ota_id = fields.Many2one('channel.ota.info', + string='Channel OTA ID', + readonly=True, + old_name='wchannel_id') + ota_reservation_id = fields.Char("Channel OTA Reservation Code", + readonly=True, + old_name='channel_reservation_code') + is_from_ota = fields.Boolean('Is From OTA', + compute=_is_from_ota, store=False, + readonly=True, + old_name='wis_from_channel') to_read = fields.Boolean('To Read', default=False) - able_to_modify_wubook = fields.Boolean(compute=set_access_for_wubook_fields, string='Is user able to modify wubook fields?') - wbook_json = fields.Text(readonly=True) + able_to_modify_channel = fields.Boolean(compute=set_access_for_wubook_fields, + string='Is user able to modify wubook fields?', + old_name='able_to_modify_wubook') + channel_raw_data = fields.Text(readonly=True, old_name='wbook_json') wstatus = fields.Selection([ ('0', 'No WuBook'), @@ -63,33 +45,75 @@ class HotelReservation(models.Model): string='WuBook Status', default='0', readonly=True) wstatus_reason = fields.Char("WuBook Status Reason", readonly=True) - wcustomer_notes = fields.Text(related='folio_id.wcustomer_notes') + customer_notes = fields.Text(related='folio_id.customer_notes', + old_name='wcustomer_notes') wmodified = fields.Boolean("WuBook Modified", readonly=True, default=False) + + @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): + 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') % partner_id.name) + if not wres: + raise ValidationError(_("Can't cancel reservation on WuBook")) + +class HotelReservation(models.Model): + _inherit = 'hotel.reservation' + + @api.multi + def set_access_for_wubook_fields(self): + for rec in self: + user = self.env['res.users'].browse(self.env.uid) + rec.able_to_modify_channel = user.has_group('base.group_system') + + @api.depends('channel_type', 'ota_id') + def _get_origin_sale(self): + 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.ota_id \ + else record.ota_id.name + + channel_bind_ids = fields.One2many( + comodel_name='channel.hotel.reservation', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') origin_sale = fields.Char('Origin', compute=_get_origin_sale, store=True) @api.model def create(self, vals): - if vals.get('wrid') != None: + if vals.get('channel_reservation_id') != 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) - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - self.env['hotel.virtual.room.availability'].refresh_availability( - vals['checkin'], - vals['checkout'], - vals['product_id']) - self.env['wubook'].push_availability() + self.env['hotel.virtual.room.availability'].refresh_availability( + vals['checkin'], + vals['checkout'], + vals['product_id']) return res -# @api.multi -# def read(self, fields=None, load='_classic_read'): -# self.to_read = False -# return super(HotelReservation, self).read(fields=fields, load=load) - @api.multi def write(self, vals): if self._context.get('wubook_action', True) and \ @@ -125,7 +149,6 @@ class HotelReservation(models.Model): new_vals[i]['checkin'], new_vals[i]['checkout'], new_vals[i]['product_id']) - self.env['wubook'].push_availability() else: res = super(HotelReservation, self).write(vals) return res @@ -135,7 +158,7 @@ class HotelReservation(models.Model): vals = [] for record in self: if record.wrid and not record.parent_reservation: - raise UserError(_("You can't delete wubook reservations")) + raise UserError(_("You can't delete OTA's reservations")) vals.append({ 'checkin': record.checkin, 'checkout': record.checkout, @@ -150,7 +173,6 @@ class HotelReservation(models.Model): record['checkin'], record['checkout'], record['product_id']) - self.env['wubook'].push_availability() return res @api.multi @@ -159,9 +181,8 @@ class HotelReservation(models.Model): if waction: for record in self: # Can't cancel in Odoo - if record.wis_from_channel: - raise ValidationError(_("Can't cancel reservations \ - from OTA's")) + if record.is_from_ota: + raise ValidationError(_("Can't cancel reservations from OTA's")) user = self.env['res.users'].browse(self.env.uid) if user.has_group('hotel.group_hotel_call'): self.write({'to_read': True, 'to_assign': True}) @@ -169,44 +190,37 @@ class HotelReservation(models.Model): res = super(HotelReservation, self).action_cancel() if waction and self.env['wubook'].is_valid_account(): partner_id = self.env['res.users'].browse(self.env.uid).partner_id - wubook_obj = self.env['wubook'] for record in self: # Only can cancel reservations created directly in wubook - if record.wrid and record.wrid != '' and \ - not record.wchannel_id and \ + if record.channel_reservation_id and not record.ota_id and \ record.wstatus in ['1', '2', '4']: - wres = wubook_obj.cancel_reservation( - record.wrid, - 'Cancelled by %s' % partner_id.name) - if not wres: - raise ValidationError(_("Can't cancel reservation \ - on WuBook")) + self._event('on_record_cancel').notify(record) return res @api.multi def confirm(self): can_confirm = True for record in self: - if record.wis_from_channel and int(record.wstatus) in WUBOOK_STATUS_BAD: + if record.is_from_ota and int(record.wstatus) in WUBOOK_STATUS_BAD: can_confirm = False + break if not can_confirm: - raise ValidationError(_("Can't confirm cancelled reservations")) + raise ValidationError(_("Can't confirm OTA's cancelled reservations")) return super(HotelReservation, self).confirm() @api.multi def generate_copy_values(self, checkin=False, checkout=False): self.ensure_one() - res = super(HotelReservation, self).generate_copy_values( - checkin=checkin, checkout=checkout) + res = super().generate_copy_values(checkin=checkin, checkout=checkout) res.update({ - 'wrid': self.wrid, - 'wchannel_id': self.wchannel_id and self.wchannel_id.id or False, - 'wchannel_reservation_code': self.wchannel_reservation_code, - 'wis_from_channel': self.wis_from_channel, + 'channel_reservation_id': self.channel_reservation_id, + 'ota_id': self.ota_id and self.ota_id.id or False, + 'ota_reservation_code': self.ota_reservation_code, + 'is_from_ota': self.is_from_ota, 'to_read': self.to_read, 'wstatus': self.wstatus, 'wstatus_reason': self.wstatus_reason, - 'wcustomer_notes': self.wcustomer_notes, + 'customer_notes': self.customer_notes, }) return res @@ -221,24 +235,37 @@ class HotelReservation(models.Model): @api.model def _hcalendar_reservation_data(self, reservations): - json_reservs, json_tooltips = super( - HotelReservation, - self)._hcalendar_reservation_data(reservations) + json_reservs, json_tooltips = super()._hcalendar_reservation_data(reservations) reserv_obj = self.env['hotel.reservation'] for reserv in json_reservs: reservation = reserv_obj.browse(reserv[1]) - reserv[13] = reservation.splitted or reservation.wis_from_channel + reserv[13] = reservation.splitted or reservation.is_from_ota return (json_reservs, json_tooltips) @api.multi def mark_as_readed(self): - for record in self: - record.write({'to_read': False, 'to_assign': False}) + self.write({'to_read': False, 'to_assign': False}) @api.onchange('checkin', 'checkout', 'product_id') def on_change_checkin_checkout_product_id(self): - if not self.wis_from_channel: - return super(HotelReservation, self).\ - on_change_checkin_checkout_product_id() + if not self.is_from_ota: + return super().on_change_checkin_checkout_product_id() + +class ChannelBindingProductListener(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_write(self, record, fields=None): + record.with_delay(priority=20).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() + + @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() diff --git a/hotel_channel_connector/models/hotel_virtual_room/__init__.py b/hotel_channel_connector/models/hotel_virtual_room/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/hotel_virtual_room/common.py b/hotel_channel_connector/models/hotel_virtual_room/common.py new file mode 100644 index 000000000..c652611e1 --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room/common.py @@ -0,0 +1,139 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +class ChannelHotelVirtualRoom(models.Model): + _name = 'channel.hotel.virtual.room' + _inherit = 'channel.binding' + _inherits = {'hotel.virtual.room': 'odoo_id'} + _description = 'Channel Hotel Virtual Room' + + @api.depends('ota_capacity') + @api.onchange('room_ids', 'room_type_ids') + def _get_capacity(self): + for rec in self: + rec.ota_capacity = rec.get_capacity() + + odoo_id = fields.Many2one(comodel_names='hotel.virtual.room', + string='Reservation', + required=True, + ondelete='cascade') + channel_room_id = fields.Char("Channel Room ID", readonly=True, old_name='wrid') + channel_short_code = fields.Char("Channel Short Code", readonly=True, old_name='wscode') + ota_capacity = fields.Integer("OTA's Capacity", default=1, old_name='wcapacity') + + @api.constrains('ota_capacity') + def _check_ota_capacity(self): + for record in self: + if record.ota_capacity < 1: + raise ValidationError(_("OTA's capacity can't be less than one")) + + @api.multi + @api.constrains('channel_short_code') + def _check_channel_short_code(self): + for record in self: + if len(record.channel_short_code) > 4: # Wubook scode max. length + 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 self._context.get('channel_action', True): + seq_obj = self.env['ir.sequence'] + shortcode = seq_obj.next_by_code('hotel.room.type')[:4] + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + channel_room_id = adapter.create_room( + shortcode, + self.name, + self.ota_capacity, + self.list_price, + self.max_real_rooms) + if channel_room_id: + self.write({ + 'channel_room_id': channel_room_id, + 'channel_short_code': shortcode, + }) + except ValidationError as e: + self.create_issue('room', "Can't create room on channel", "sss") + + @job(default_channel='root.channel') + @related_action(action='related_action_unwrap_binding') + @api.multi + def modify_room(self): + self.ensure_one() + if self._context.get('channel_action', True) and self.channel_room_id: + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + adapter.modify_room( + self.channel_room_id, + self.name, + self.ota_capacity, + self.list_price, + self.max_real_rooms, + self.channel_short_code) + except ValidationError as e: + self.create_issue('room', "Can't modify room on channel", "sss") + + @job(default_channel='root.channel') + @related_action(action='related_action_unwrap_binding') + @api.multi + def delete_room(self): + self.ensure_one() + if self._context.get('channel_action', True) and self.channel_room_id: + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + adapter.delete_room(self.channel_room_id) + except ValidationError as e: + self.create_issue('room', "Can't delete room on channel", "sss") + + @job(default_channel='root.channel') + @api.multi + def import_rooms(self): + if self._context.get('channel_action', True): + with self.backend_id.work_on(self._name) as work: + importer = work.component(usage='channel.importer') + return importer.import_rooms() + +class HotelVirtualRoom(models.Model): + _inherit = 'hotel.virtual.room' + + channel_bind_ids = fields.One2many( + comodel_name='channel.hotel.virtual.room', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') + + + @api.multi + def get_restrictions(self, date): + restriction_plan_id = int(self.env['ir.default'].sudo().get( + 'res.config.settings', 'parity_restrictions_id')) + self.ensure_one() + restriction = self.env['hotel.virtual.room.restriction.item'].search([ + ('date_start', '=', date), + ('date_end', '=', date), + ('virtual_room_id', '=', self.id), + ('restriction_id', '=', restriction_plan_id) + ], limit=1) + return restriction + +class ChannelBindingVirtualRoomListener(Component): + _name = 'channel.binding.virtual.room.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.virtual.room'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.with_delay(priority=20).create_room() + + @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_room() + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.with_delay(priority=20).modidy_room() diff --git a/hotel_channel_connector/models/hotel_virtual_room_availability/__init__.py b/hotel_channel_connector/models/hotel_virtual_room_availability/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room_availability/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/hotel_virtual_room_availability/common.py b/hotel_channel_connector/models/hotel_virtual_room_availability/common.py new file mode 100644 index 000000000..c9e1499ed --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room_availability/common.py @@ -0,0 +1,116 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +class ChannelHotelVirtualRoomAvailability(models.Model): + _name = 'channel.hotel.virtual.room.availability' + _inherit = 'channel.binding' + _inherits = {'hotel.virtual.room.availability': 'odoo_id'} + _description = 'Channel Product Pricelist' + + @api.model + def _default_channel_max_avail(self): + if self.virtual_room_id: + return self.virtual_room_id.max_real_rooms + return -1 + + odoo_id = fields.Many2one(comodel_names='product.pricelist', + string='Pricelist', + required=True, + ondelete='cascade') + channel_max_avail = fields.Integer("Max. Channel Avail", + default=_default_channel_max_avail, + old_name='wmax_avail') + channel_pushed = fields.Boolean("Channel Pushed", readonly=True, default=False, + old_name='wpushed') + + @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): + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + 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") + + @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): + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + 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") + + @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: + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + adapter.delete_plan(self.channel_plan_id) + except ValidationError as e: + self.create_issue('room', "Can't delete plan on channel", "sss") + + @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') + return importer.import_pricing_plans() + +class ProductPricelist(models.Model): + _inherit = 'product.pricelist' + + channel_bind_ids = fields.One2many( + comodel_name='channel.product.pricelist', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') + + @api.multi + @api.depends('name') + def name_get(self): + 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 priclist_id.wpid: + names.append((name[0], '%s (WuBook)' % name[1])) + else: + names.append((name[0], name[1])) + return names + +class ChannelBindingProductPricelistListener(Component): + _name = 'channel.binding.product.pricelist.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.product.pricelist'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.with_delay(priority=20).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() + + @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() diff --git a/hotel_channel_connector/models/hotel_virtual_room_restriction/__init__.py b/hotel_channel_connector/models/hotel_virtual_room_restriction/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room_restriction/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/hotel_virtual_room_restriction/common.py b/hotel_channel_connector/models/hotel_virtual_room_restriction/common.py new file mode 100644 index 000000000..ae4aee64b --- /dev/null +++ b/hotel_channel_connector/models/hotel_virtual_room_restriction/common.py @@ -0,0 +1,106 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +class ChannelHotelVirtualRoomRestriction(models.Model): + _name = 'channel.hotel.virtual.room.restriction' + _inherit = 'channel.binding' + _inherits = {'hotel.virtual.room.restriction': 'odoo_id'} + _description = 'Channel Hotel Virtual Room Restriction' + + odoo_id = fields.Many2one(comodel_names='hotel.virtual.room.restriction', + 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): + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + 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") + + @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): + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + 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") + + @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: + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + adapter.delete_rplan(self.channel_plan_id) + except ValidationError as e: + self.create_issue('room', "Can't delete restriction plan on channel", "sss") + + @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') + return importer.import_restriction_plans() + +class HotelVirtualRoomRestriction(models.Model): + _inherit = 'hotel.virtual.room.restriction' + + channel_bind_ids = fields.One2many( + comodel_name='channel.hotel.virtual.room.restriction', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') + + @api.multi + @api.depends('name') + def name_get(self): + vroom_restriction_obj = self.env['hotel.virtual.room.restriction'] + org_names = super(HotelVirtualRoomRestriction, self).name_get() + names = [] + for name in org_names: + restriction_id = vroom_restriction_obj.browse(name[0]) + if restriction_id.wpid: + names.append((name[0], '%s (WuBook)' % name[1])) + else: + names.append((name[0], name[1])) + return names + +class ChannelBindingHotelVirtualRoomRestrictionListener(Component): + _name = 'channel.binding.hotel.virtual.room.restriction.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.hotel.virtual.room.restriction'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.with_delay(priority=20).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() + + @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() diff --git a/hotel_channel_connector/models/inherited_hotel_folio.py b/hotel_channel_connector/models/inherited_hotel_folio.py index 2fb23e6b2..17637054f 100644 --- a/hotel_channel_connector/models/inherited_hotel_folio.py +++ b/hotel_channel_connector/models/inherited_hotel_folio.py @@ -8,35 +8,37 @@ class HotelFolio(models.Model): @api.depends('room_lines') def _has_channel_reservations(self): - if any(self.room_lines): - for room in self.room_lines: - if room.channel_room_id and room.channel_room_id != '': - self.has_channel_reservations = True - return - self.has_channel_reservations = False + for record in self: + channel_reservations = record.room_lines.filtered(lambda x: x.channel_room_id) + record.has_channel_reservations = any(channel_reservations) - seed = fields.Char("WuBook Session Seed", old_name='wseed', readonly=True) - customer_notes = fields.Text("WuBook Customer Notes", - old_name='wcustomer_notes', readonly=True) - has_channel_reservations = fields.Boolean(old_name='whas_wubook_reservations', - compute=_has_channel_reservations, - store=False) + wseed = fields.Char("Wubook Session Seed", readonly=True) + customer_notes = fields.Text("Channel Customer Notes", + readonly=True, old_name='wcustomer_notes') + has_channel_reservations = fields.Boolean(compute=_has_channel_reservations, + store=False, + old_name='whas_wubook_reservations') + @job(default_channel='root.channel') @api.multi def import_reservations(self): - return self.env['hotel.channel.connector'].fetch_new_bookings() + self.ensure_one() + with self.backend_id.work_on(self._name) as work: + importer = work.component(usage='channel.importer') + importer.fetch_new_bookings() @api.multi def action_confirm(self): for rec in self: - for room in rec.room_lines: - room.to_read = False - room.to_assign = False - return super(HotelFolio, self).action_confirm() + rec.room_lines.write({ + 'to_read': False, + 'to_assign': False, + }) + return super().action_confirm() @api.multi def get_grouped_reservations_json(self, state, import_all=False): - super(HotelFolio, self).get_grouped_reservations_json(state, import_all=import_all) + super().get_grouped_reservations_json(state, import_all=import_all) self.ensure_one() info_grouped = [] for rline in self.room_lines: @@ -68,12 +70,14 @@ class HotelFolio(models.Model): @api.depends('room_lines') def _compute_has_cancelled_reservations_to_send(self): - super(HotelFolio, self)._compute_has_cancelled_reservations_to_send() - has_to_send = False - for rline in self.room_lines: - if rline.splitted: + super()._compute_has_cancelled_reservations_to_send() + hotel_reserv_obj = self.env['hotel.reservation'] + for record in self: + splitted_reservation_ids = record.room_lines.filtered(lambda x: x.splitted) + has_to_send = False + for rline in splitted_reservation_ids: master_reservation = rline.parent_reservation or rline - has_to_send = self.env['hotel.reservation'].search_count([ + has_to_send = hotel_reserv_obj.search_count([ ('splitted', '=', True), ('folio_id', '=', self.id), ('to_send', '=', True), @@ -83,7 +87,6 @@ class HotelFolio(models.Model): ('parent_reservation', '=', master_reservation.id), ('id', '=', master_reservation.id), ]) > 0 - elif rline.to_send and rline.state == 'cancelled' and not rline.wmodified: - has_to_send = True - break - self.has_cancelled_reservations_to_send = has_to_send + if has_to_send: + break + record.has_cancelled_reservations_to_send = has_to_send diff --git a/hotel_channel_connector/models/inherited_hotel_virtual_room.py b/hotel_channel_connector/models/inherited_hotel_virtual_room.py deleted file mode 100644 index 8f5c3f40e..000000000 --- a/hotel_channel_connector/models/inherited_hotel_virtual_room.py +++ /dev/null @@ -1,111 +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 HotelVirtualRoom(models.Model): - _inherit = 'hotel.room.type' - - @api.depends('wcapacity') - @api.onchange('room_ids', 'room_type_ids') - def _get_capacity(self): - hotel_room_obj = self.env['hotel.room'] - for rec in self: - rec.wcapacity = rec.get_capacity() - - wscode = fields.Char("WuBook Short Code", readonly=True) - wrid = fields.Char("WuBook Room ID", readonly=True) - wcapacity = fields.Integer("WuBook Capacity", default=1) - - @api.constrains('wcapacity') - def _check_wcapacity(self): - for record in self: - if record.wcapacity < 1: - raise ValidationError(_("wcapacity can't be less than one")) - - @api.multi - @api.constrains('wscode') - def _check_wscode(self): - for record in self: - if len(record.wscode) > 4: # Wubook scode max. length - raise ValidationError(_("SCODE Can't be longer than 4 characters")) - - @api.multi - def get_restrictions(self, date): - restriction_plan_id = int(self.env['ir.default'].sudo().get( - 'hotel.config.settings', 'parity_restrictions_id')) - self.ensure_one() - restriction = self.env['hotel.virtual.room.restriction.item'].search([ - ('date_start', '=', date), - ('date_end', '=', date), - ('virtual_room_id', '=', self.id), - ('restriction_id', '=', restriction_plan_id) - ], limit=1) - return restriction - # if restriction: - # return restriction - # else: - # vroom_rest_it_obj = self.env['hotel.virtual.room.restriction.item'] - # global_restr = vroom_rest_it_obj.search([ - # ('applied_on', '=', '1_global'), - # ('restriction_id', '=', restriction_plan_id) - # ], limit=1) - # if global_restr: - # return global_restr - # return False - - @api.model - def create(self, vals): - vroom = super(HotelVirtualRoom, self).create(vals) - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - seq_obj = self.env['ir.sequence'] - shortcode = seq_obj.next_by_code('hotel.room.type')[:4] - wrid = self.env['wubook'].create_room( - shortcode, - vroom.name, - vroom.wcapacity, - vroom.list_price, - vroom.max_real_rooms - ) - if not wrid: - raise ValidationError(_("Can't create room on WuBook")) - vroom.with_context(wubook_action=False).write({ - 'wrid': wrid, - 'wscode': shortcode, - }) - return vroom - - @api.multi - def write(self, vals): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - wubook_obj = self.env['wubook'] - for record in self: - if record.wrid and record.wrid != '': - wres = wubook_obj.modify_room( - vals.get('wrid', record.wrid), - vals.get('name', record.name), - vals.get('wcapacity', record.wcapacity), - vals.get('list_price', record.list_price), - vals.get('max_real_rooms', record.max_real_rooms), - vals.get('wscode', record.wscode)) - if not wres: - raise ValidationError(_("Can't modify room on WuBook")) - return super(HotelVirtualRoom, self).write(vals) - - @api.multi - def unlink(self): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - for record in self: - if record.wrid and record.wrid != '': - wres = self.env['wubook'].delete_room(record.wrid) - if not wres: - raise ValidationError(_("Can't delete room on WuBook")) - return super(HotelVirtualRoom, self).unlink() - - @api.multi - def import_rooms(self): - return self.env['wubook'].import_rooms() diff --git a/hotel_channel_connector/models/inherited_product_pricelist.py b/hotel_channel_connector/models/inherited_product_pricelist.py deleted file mode 100644 index 7681f0070..000000000 --- a/hotel_channel_connector/models/inherited_product_pricelist.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright 2018 Alexandre Díaz -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta -from openerp import models, fields, api, _ -from openerp.exceptions import ValidationError -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT - - -class ProductPricelist(models.Model): - _inherit = 'product.pricelist' - - wpid = fields.Char("WuBook Plan ID", readonly=True) - wdaily = fields.Boolean("WuBook Daily Plan", default=True) - - @api.multi - def get_wubook_prices(self): - self.ensure_one() - prices = {} - if self.wdaily: - min_date = False - max_date = False - for item in self.item_ids: - if not item.date_start or not item.date_end: - continue - date_start_dt = fields.Datetime.from_string(item.date_start) - date_end_dt = fields.Datetime.from_string(item.date_end) - if not min_date or date_start_dt < min_date: - min_date = date_start_dt - if not max_date or date_end_dt > max_date: - max_date = date_end_dt - if not min_date or not max_date: - return prices - days_diff = abs((max_date - min_date).days) - vrooms = self.env['hotel.room.type'].search([ - ('wrid', '!=', ''), - ('wrid', '!=', False) - ]) - for vroom in vrooms: - prices.update({vroom.wrid: []}) - for i in range(0, days_diff or 1): - ndate_dt = min_date + timedelta(days=i) - product_id = vroom.product_id.with_context( - quantity=1, - date=ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), - pricelist=self.id, - uom=vroom.product_id.product_tmpl_id.uom_id.id) - prices[vroom.wrid].append(product_id.price) - else: - vrooms = self.env['hotel.room.type'].search([ - ('wrid', '!=', ''), - ('wrid', '!=', False) - ]) - for item in self.item_ids: - if not item.date_start or not item.date_end: - continue - date_start_dt = fields.Datetime.from_string(item.date_start) - date_end_dt = fields.Datetime.from_string(item.date_end) - days_diff = abs((date_end_dt - date_start_dt).days) - vals = {} - for vroom in vrooms: - wdays = [False, False, False, False, False, False, False] - for i in range(0, 7): - ndate_dt = date_start_dt + timedelta(days=i) - product_id = vroom.product_id.with_context( - quantity=1, - date=ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), - pricelist=self.id, - uom=vroom.product_id.product_tmpl_id.uom_id.id) - wdays[ndate_dt.weekday()] = product_id.price - vals.update({vroom.wrid: wdays}) - prices.update({ - 'dfrom': item.date_start, - 'dto': item.date_end, - 'values': vals, - }) - return prices - - @api.model - def create(self, vals): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - wpid = self.env['wubook'].create_plan(vals['name'], - vals.get('wdaily') and - 1 or 0) - if not wpid: - raise ValidationError(_("Can't create plan on WuBook")) - vals.update({'wpid': wpid}) - pricelist = super(ProductPricelist, self).create(vals) - return pricelist - - @api.multi - def write(self, vals): - nname = vals.get('name') - if self._context.get('wubook_action', True) and nname and \ - self.env['wubook'].is_valid_account(): - for record in self: - if record.wpid and record.wpid != '': - wres = self.env['wubook'].update_plan_name( - vals.get('wpid', record.wpid), - nname) - if not wres: - raise ValidationError(_("Can't update plan name \ - on WuBook")) - updated = super(ProductPricelist, self).write(vals) - return updated - - @api.multi - def unlink(self): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - for record in self: - if record.wpid and record.wpid != '': - wres = self.env['wubook'].delete_plan(record.wpid) - if not wres: - raise ValidationError(_("Can't delete plan on WuBook")) - return super(ProductPricelist, self).unlink() - - @api.multi - def import_price_plans(self): - return self.env['wubook'].import_pricing_plans() - - @api.multi - @api.depends('name') - def name_get(self): - pricelistObj = self.env['product.pricelist'] - org_names = super(ProductPricelist, self).name_get() - names = [] - for name in org_names: - priclist_id = pricelistObj.browse(name[0]) - if priclist_id.wpid: - names.append((name[0], '%s (WuBook)' % name[1])) - else: - names.append((name[0], name[1])) - return names diff --git a/hotel_channel_connector/models/inherited_product_pricelist_item.py b/hotel_channel_connector/models/inherited_product_pricelist_item.py index c1c3a58f7..b61bd059c 100644 --- a/hotel_channel_connector/models/inherited_product_pricelist_item.py +++ b/hotel_channel_connector/models/inherited_product_pricelist_item.py @@ -7,50 +7,49 @@ from openerp.exceptions import ValidationError class ProductPricelistItem(models.Model): _inherit = 'product.pricelist.item' - wpushed = fields.Boolean("WuBook Pushed", default=True, readonly=True) - wdaily = fields.Boolean(related='pricelist_id.wdaily', readonly=True) + is_channel_pushed = fields.Boolean("WuBook Pushed", default=True, readonly=True, + old_name='wpushed') + is_daily_plan = fields.Boolean(related='pricelist_id.wdaily', readonly=True, + old_name='wdaily') @api.constrains('fixed_price') def _check_fixed_price(self): - vroom_obj = self.env['hotel.room.type'] + vroom_obj = self.env['hotel.virtual.room'] for record in self: vroom = vroom_obj.search([ ('product_id.product_tmpl_id', '=', record.product_tmpl_id.id) ], limit=1) - if vroom and vroom.wrid and record.compute_price == 'fixed' \ + if vroom and vroom.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('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): + if self._context.get('channel_action', True): pricelist_id = self.env['product.pricelist'].browse( vals.get('pricelist_id')) - vroom = self.env['hotel.room.type'].search([ + vroom = self.env['hotel.virtual.room'].search([ ('product_id.product_tmpl_id', '=', vals.get('product_tmpl_id')), - ('wrid', '!=', False) + ('channel_room_id', '!=', False) ]) - if vroom and pricelist_id.wpid: - vals.update({'wpushed': False}) + if vroom 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('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): + if self._context.get('channel_action', True): prices_obj = self.env['product.pricelist'] for record in self: - pricelist_id = vals.get('pricelist_id') and \ - prices_obj.browse(vals.get('pricelist_id')) or \ - record.pricelist_id + 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 - vroom = self.env['hotel.room.type'].search([ + record.product_tmpl_id.id + vroom = self.env['hotel.virtual.room'].search([ ('product_id.product_tmpl_id', '=', product_tmpl_id), - ('wrid', '!=', False) + ('channel_room_id', '!=', False), ]) - if vroom and pricelist_id.wpid: - vals.update({'wpushed': False}) + if vroom 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/inherited_res_partner.py b/hotel_channel_connector/models/inherited_res_partner.py index 75afaecb9..a396d3b4a 100644 --- a/hotel_channel_connector/models/inherited_res_partner.py +++ b/hotel_channel_connector/models/inherited_res_partner.py @@ -30,18 +30,15 @@ class ResPartner(models.Model): if folio_ids: folio_ids.write({ 'partner_id': org_partner_id.id, + }) + folio_ids = self.env['hotel.folio'].search([ + ('partner_invoice_id', '=', record.id) + ]) + if folio_ids: + folio_ids.write({ 'partner_invoice_id': org_partner_id.id, }) - # DANGER: self-delete... perhaps best invisible? - # record.unlink() This cause mistakes record.write({'active': False}) - # return { - # 'type': 'ir.actions.act_window', - # 'res_model': 'res.partner', - # 'views': [[False, "form"]], - # 'target': 'current', - # 'res_id': org_partner_id.id, - # } else: # If not found, this is the 'confirmed' vals.update({'unconfirmed': False}) diff --git a/hotel_channel_connector/models/inherited_reservation_restriction.py b/hotel_channel_connector/models/inherited_reservation_restriction.py deleted file mode 100644 index 5462500e3..000000000 --- a/hotel_channel_connector/models/inherited_reservation_restriction.py +++ /dev/null @@ -1,125 +0,0 @@ -# Copyright 2018 Alexandre Díaz -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from datetime import datetime, timedelta -from openerp import models, fields, api, _ -from openerp.exceptions import ValidationError -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT - - -class ReservationRestriction(models.Model): - _inherit = 'hotel.virtual.room.restriction' - - wpid = fields.Char("WuBook Restriction Plan ID", readonly=True) - wdaily = fields.Boolean("Plan Daily", default=True, readonly=True) - - @api.multi - def get_wubook_restrictions(self): - self.ensure_one() - prices = {} - min_date = False - max_date = False - for item in self.item_ids: - if not item.date_start or not item.date_end: - continue - date_start_dt = fields.Datetime.from_string(item.date_start) - date_end_dt = fields.Datetime.from_string(item.date_end) - if not min_date or date_start_dt < min_date: - min_date = date_start_dt - if not max_date or date_end_dt > max_date: - max_date = date_end_dt - if not min_date or not max_date: - return prices - days_diff = abs((max_date - min_date).days) - vrooms = self.env['hotel.room.type'].search([ - ('wrid', '!=', ''), - ('wrid', '!=', False) - ]) - for vroom in vrooms: - prices.update({vroom.wrid: []}) - for i in range(0, days_diff or 1): - ndate_dt = min_date + timedelta(days=i) - product_id = vroom.product_id.with_context( - quantity=1, - date=ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT), - pricelist=self.id, - uom=vroom.product_id.product_tmpl_id.uom_id.id) - prices[vroom.wrid].append(product_id.price) - return prices - - @api.model - def create(self, vals): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - wpid = self.env['wubook'].create_rplan(vals['name']) - if not wpid: - raise ValidationError(_("Can't create restriction plan on \ - WuBook")) - vals.update({'wpid': wpid}) - - rules = self._context.get('rules') - if rules: - vals.update({'wdaily': False}) - - restriction = super(ReservationRestriction, self).create(vals) - - if rules: - # Basic Rules - vroom_rest_it_obj = self.env['hotel.virtual.room.restriction.item'] - vroom_rest_it_obj.with_context({'wubook_action': False}).create({ - 'closed_arrival': rules['closed_arrival'], - 'closed': rules['closed'], - 'min_stay': rules['min_stay'], - 'closed_departure': rules['closed_departure'], - 'max_stay': rules['max_stay'], - 'min_stay_arrival': rules['min_stay_arrival'], - 'restriction_id': restriction.id, - 'applied_on': '1_global', - }) - - return restriction - - @api.multi - def write(self, vals): - nname = vals.get('name') - if self._context.get('wubook_action', True) and nname and \ - self.env['wubook'].is_valid_account(): - for record in self: - if record.wpid and record.wpid != '': - wres = self.env['wubook'].rename_rplan( - vals.get('wpid', record.wpid), - nname) - if not wres: - raise ValidationError(_("Can't rename restriction plan \ - on WuBook")) - updated = super(ReservationRestriction, self).write(vals) - return updated - - @api.multi - def unlink(self): - if self._context.get('wubook_action', True) and \ - self.env['wubook'].is_valid_account(): - for record in self: - if record.wpid and record.wpid != '': - wres = self.env['wubook'].delete_rplan(record.wpid) - if not wres: - raise ValidationError(_("Can't delete restriction plan \ - on WuBook")) - return super(ReservationRestriction, self).unlink() - - @api.multi - def import_restriction_plans(self): - return self.env['wubook'].import_restriction_plans() - - @api.multi - @api.depends('name') - def name_get(self): - roomRestrictionObj = self.env['hotel.virtual.room.restriction'] - org_names = super(ReservationRestriction, self).name_get() - names = [] - for name in org_names: - restriction_id = roomRestrictionObj.browse(name[0]) - if restriction_id.wpid: - names.append((name[0], '%s (WuBook)' % name[1])) - else: - names.append((name[0], name[1])) - return names diff --git a/hotel_channel_connector/models/inherited_reservation_restriction_item.py b/hotel_channel_connector/models/inherited_reservation_restriction_item.py index f771907da..181738612 100644 --- a/hotel_channel_connector/models/inherited_reservation_restriction_item.py +++ b/hotel_channel_connector/models/inherited_reservation_restriction_item.py @@ -6,7 +6,8 @@ from openerp import models, fields, api class ReservationRestrictionItem(models.Model): _inherit = 'hotel.virtual.room.restriction.item' - wpushed = fields.Boolean("WuBook Pushed", default=False, readonly=True) + channel_pushed = fields.Boolean("WuBook Pushed", default=False, readonly=True, + old_name='wpushed') @api.onchange('date_start') def _onchange_date_start(self): @@ -22,6 +23,6 @@ class ReservationRestrictionItem(models.Model): def write(self, vals): if vals.get('date_start'): vals['date_end'] = vals.get('date_start') - if self._context.get('wubook_action', True): - vals.update({'wpushed': False}) + if self._context.get('channel_action', True): + vals.update({'channel_pushed': False}) return super(ReservationRestrictionItem, self).write(vals) diff --git a/hotel_channel_connector/models/inherited_virtual_room_availability.py b/hotel_channel_connector/models/inherited_virtual_room_availability.py index 576bef901..cd0cfdd00 100644 --- a/hotel_channel_connector/models/inherited_virtual_room_availability.py +++ b/hotel_channel_connector/models/inherited_virtual_room_availability.py @@ -12,19 +12,21 @@ class VirtualRoomAvailability(models.Model): _inherit = 'hotel.virtual.room.availability' @api.model - def _default_wmax_avail(self): + def _default_channel_max_avail(self): if self.virtual_room_id: return self.virtual_room_id.max_real_rooms return -1 - wmax_avail = fields.Integer("Max. Wubook Avail", - default=_default_wmax_avail) - wpushed = fields.Boolean("WuBook Pushed", readonly=True, default=False) + channel_max_avail = fields.Integer("Max. Channel Avail", + default=_default_channel_max_avail, + old_name='wmax_avail') + channel_pushed = fields.Boolean("Channel Pushed", readonly=True, default=False, + old_name='wpushed') @api.constrains('avail') def _check_avail(self): - vroom_obj = self.env['hotel.room.type'] - issue_obj = self.env['wubook.issue'] + vroom_obj = self.env['hotel.virtual.room'] + issue_obj = self.env['hotel.channel.connector.issue'] wubook_obj = self.env['wubook'] for record in self: cavail = len(vroom_obj.check_availability_virtual_room( @@ -35,7 +37,7 @@ class VirtualRoomAvailability(models.Model): if record.avail > max_avail: issue_obj.sudo().create({ 'section': 'avail', - 'message': _("The new availability can't be greater than \ + 'message': _(r"The new availability can't be greater than \ the actual availability \ \n[%s]\nInput: %d\Limit: %d") % (record.virtual_room_id.name, record.avail, diff --git a/hotel_channel_connector/models/product_pricelist/__init__.py b/hotel_channel_connector/models/product_pricelist/__init__.py new file mode 100644 index 000000000..c3d0f6b2b --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from . import common diff --git a/hotel_channel_connector/models/product_pricelist/common.py b/hotel_channel_connector/models/product_pricelist/common.py new file mode 100644 index 000000000..aed0a90bc --- /dev/null +++ b/hotel_channel_connector/models/product_pricelist/common.py @@ -0,0 +1,107 @@ +# Copyright 2018 Alexandre Díaz +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +class ChannelProductPricelist(models.Model): + _name = 'channel.product.pricelist' + _inherit = 'channel.binding' + _inherits = {'product.pricelist': 'odoo_id'} + _description = 'Channel Product Pricelist' + + odoo_id = fields.Many2one(comodel_names='product.pricelist', + 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') + @related_action(action='related_action_unwrap_binding') + @api.multi + def create_plan(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') + 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") + + @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): + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + 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") + + @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: + with self.backend_id.work_on(self._name) as work: + adapter = work.component(usage='backend.adapter') + try: + adapter.delete_plan(self.channel_plan_id) + except ValidationError as e: + self.create_issue('room', "Can't delete plan on channel", "sss") + + @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') + return importer.import_pricing_plans() + +class ProductPricelist(models.Model): + _inherit = 'product.pricelist' + + channel_bind_ids = fields.One2many( + comodel_name='channel.product.pricelist', + inverse_name='odoo_id', + string='Hotel Channel Connector Bindings') + + @api.multi + @api.depends('name') + def name_get(self): + 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 priclist_id.wpid: + names.append((name[0], '%s (WuBook)' % name[1])) + else: + names.append((name[0], name[1])) + return names + +class ChannelBindingProductPricelistListener(Component): + _name = 'channel.binding.product.pricelist.listener' + _inherit = 'base.connector.listener' + _apply_on = ['channel.product.pricelist'] + + @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) + def on_record_write(self, record, fields=None): + record.with_delay(priority=20).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() + + @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() diff --git a/hotel_channel_connector/models/res_config.py b/hotel_channel_connector/models/res_config.py index 479249564..0478a8e86 100644 --- a/hotel_channel_connector/models/res_config.py +++ b/hotel_channel_connector/models/res_config.py @@ -26,33 +26,33 @@ class WubookConfiguration(models.TransientModel): @api.multi def set_wubook_user(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', 'wubook_user', self.wubook_user) + 'wubook.config.settings', 'wubook_user', self.wubook_user) @api.multi def set_wubook_passwd(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', 'wubook_passwd', self.wubook_passwd) + 'wubook.config.settings', 'wubook_passwd', self.wubook_passwd) @api.multi def set_wubook_lcode(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', 'wubook_lcode', self.wubook_lcode) + 'wubook.config.settings', 'wubook_lcode', self.wubook_lcode) @api.multi def set_wubook_server(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', 'wubook_server', self.wubook_server) + 'wubook.config.settings', 'wubook_server', self.wubook_server) @api.multi def set_wubook_pkey(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', 'wubook_pkey', self.wubook_pkey) + 'wubook.config.settings', 'wubook_pkey', self.wubook_pkey) @api.multi def set_wubook_push_security_token(self): return self.env['ir.default'].sudo().set( - 'wubook.config.settings', - 'wubook_push_security_token', self.wubook_push_security_token) + 'wubook.config.settings', + 'wubook_push_security_token', self.wubook_push_security_token) # Dangerus method: Usefull for cloned instances with new wubook account @api.multi @@ -77,21 +77,21 @@ class WubookConfiguration(models.TransientModel): vrooms = self.env['hotel.room.type'].search([]) for vroom in vrooms: shortcode = ir_seq_obj.next_by_code('hotel.room.type')[:4] - wrid = wubook_obj.create_room( + channel_room_id = wubook_obj.create_room( shortcode, vroom.name, vroom.wcapacity, vroom.list_price, vroom.max_real_rooms ) - if wrid: + if channel_room_id: vroom.with_context(wubook_action=False).write({ - 'wrid': wrid, + 'channel_room_id': channel_room_id, 'wscode': shortcode, }) else: vroom.with_context(wubook_action=False).write({ - 'wrid': '', + 'channel_room_id': '', 'wscode': '', }) # Create Restrictions @@ -99,16 +99,16 @@ class WubookConfiguration(models.TransientModel): restriction_ids = vroom_rest_obj.search([]) for restriction in restriction_ids: if restriction.wpid != '0': - wpid = wubook_obj.create_rplan(restriction.name) + channel_plan_id = wubook_obj.create_rplan(restriction.name) restriction.write({ - 'wpid': wpid or '' + 'channel_plan_id': channel_plan_id or '' }) # Create Pricelist pricelist_ids = self.env['product.pricelist'].search([]) for pricelist in pricelist_ids: - wpid = wubook_obj.create_plan(pricelist.name, pricelist.wdaily) + channel_plan_id = wubook_obj.create_plan(pricelist.name, pricelist.wdaily) pricelist.write({ - 'wpid': wpid or '' + 'channel_plan_id': channel_plan_id or '' }) wubook_obj.close_connection() @@ -120,14 +120,14 @@ class WubookConfiguration(models.TransientModel): # Reset Reservations reservation_ids = self.env['hotel.reservation'].search([ - ('wrid', '!=', ''), - ('wrid', '!=', False) + ('channel_reservation_id', '!=', ''), + ('channel_reservation_id', '!=', False) ]) reservation_ids.with_context(wubook_action=False).write({ - 'wrid': '', - 'wchannel_id': False, - 'wchannel_reservation_code': '', - 'wis_from_channel': False, + 'channel_reservation_id': '', + 'ota_id': False, + 'ota_reservation_id': '', + 'is_from_ota': False, 'wstatus': 0 }) diff --git a/hotel_channel_connector/models/wubook_channel_info.py b/hotel_channel_connector/models/wubook_channel_info.py index 72830aa89..0f5312ca4 100644 --- a/hotel_channel_connector/models/wubook_channel_info.py +++ b/hotel_channel_connector/models/wubook_channel_info.py @@ -3,13 +3,17 @@ from openerp import models, fields, api -class WuBookChannelInfo(models.Model): - _name = 'wubook.channel.info' +class HotelChannelConnectorOTAInfo(models.Model): + _name = 'hote.channel.connector.ota.info' - wid = fields.Char("WuBook Channel ID", required=True) - name = fields.Char("Channel Name", required=True) + ota_id = fields.Char("Channel OTA ID", required=True) + name = fields.Char("OTA Name", required=True) ical = fields.Boolean("ical", default=False) + @job(default_channel='root.channel') @api.multi def import_channels_info(self): - return self.env['wubook'].import_channels_info() + self.ensure_one() + with self.backend_id.work_on(self._name) as work: + importer = work.component(usage='channel.importer') + return importer.import_channels_info() diff --git a/hotel_channel_connector/tests/test_hotel_folio_model.py b/hotel_channel_connector/tests/test_hotel_folio_model.py index ef0987b65..099db49b9 100644 --- a/hotel_channel_connector/tests/test_hotel_folio_model.py +++ b/hotel_channel_connector/tests/test_hotel_folio_model.py @@ -58,8 +58,8 @@ class TestHotelFolio(TestHotelWubook): ('wrid', '=', processed_rids[0]) ], order='id ASC', limit=1) self.assertTrue(nreserv, "Can't found reservation") - self.assertTrue(nreserv.folio_id.whas_wubook_reservations, - "Can't found reservations from wubook") + self.assertTrue(nreserv.folio_id.has_channel_reservations, + "Can't found reservations from channel") def test_import_reservations(self): now_utc_dt = date_utils.now() diff --git a/hotel_channel_connector/tests/test_hotel_reservation_model.py b/hotel_channel_connector/tests/test_hotel_reservation_model.py index c305c36d1..bb3d6eb67 100644 --- a/hotel_channel_connector/tests/test_hotel_reservation_model.py +++ b/hotel_channel_connector/tests/test_hotel_reservation_model.py @@ -31,7 +31,7 @@ from .common import TestHotelWubook class TestHotelReservation(TestHotelWubook): - def test_is_from_channel(self): + def test_is_from_ota(self): now_utc_dt = date_utils.now() checkin_utc_dt = now_utc_dt + timedelta(days=3) checkin_dt = date_utils.dt_as_timezone(checkin_utc_dt, @@ -64,9 +64,9 @@ class TestHotelReservation(TestHotelWubook): ('wrid', 'in', processed_rids) ]) self.assertTrue(nreserv, "Reservation not found") - self.assertTrue(nreserv.wis_from_channel) + self.assertTrue(nreserv.is_from_ota) nreserv.wrid = '' - self.assertFalse(nreserv.wis_from_channel) + self.assertFalse(nreserv.is_from_ota) def test_write(self): now_utc_dt = date_utils.now() diff --git a/hotel_channel_connector/wizard/inherited_duplicate_reservation.py b/hotel_channel_connector/wizard/inherited_duplicate_reservation.py index 3e104d3a1..65eab1c50 100644 --- a/hotel_channel_connector/wizard/inherited_duplicate_reservation.py +++ b/hotel_channel_connector/wizard/inherited_duplicate_reservation.py @@ -29,7 +29,7 @@ class MassiveChangesWizard(models.TransientModel): @api.multi def duplicate_reservation(self): reservation_id = self.env['hotel.reservation'].browse( - self.env.context.get('active_id')) - if reservation_id and reservation_id.wis_from_channel: + self.env.context.get('active_id')) + if reservation_id and reservation_id.is_from_ota: raise ValidationError(_("Can't duplicate a reservation from channel")) return super(MassiveChangesWizard, self).duplicate_reservation() diff --git a/hotel_channel_connector/wizard/inherited_massive_price_reservation_days.py b/hotel_channel_connector/wizard/inherited_massive_price_reservation_days.py index 46445a927..bc2985c70 100644 --- a/hotel_channel_connector/wizard/inherited_massive_price_reservation_days.py +++ b/hotel_channel_connector/wizard/inherited_massive_price_reservation_days.py @@ -34,7 +34,7 @@ class MassivePriceChangeWizard(models.TransientModel): if not reservation_id: return False - if reservation_id.wis_from_channel: + if reservation_id.is_from_ota: raise ValidationError( _("Can't change prices of reservations from OTA's"))