Calendar Improvements (#89)

* [IMP] Allow change adults resizing reservation on calendar

* [IMP] Board Services

* [WIP] Board Service

* [IMP] Calendar Buttons & Default Options

* [WIP] Availability

* [WIP] Availability

* [WIP] Availability

* [IMP] Calendar buttons
This commit is contained in:
QS5ELkMu
2019-01-30 12:42:36 +01:00
committed by Pablo
parent f9c5775983
commit 8b53b6b624
44 changed files with 945 additions and 673 deletions

View File

@@ -2,9 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import timedelta
from openerp import models, fields, api
from openerp.tools import (
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_DATETIME_FORMAT)
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
class MassiveChangesWizard(models.TransientModel):
@@ -12,10 +10,9 @@ class MassiveChangesWizard(models.TransientModel):
# Common fields
section = fields.Selection([
('0', 'Availability'),
('1', 'Restrictions'),
('2', 'Pricelist'),
], string='Section', default='0')
('restrictions', 'Restrictions'),
('prices', 'Pricelist'),
], string='Section', default='prices')
date_start = fields.Date('Start Date', required=True)
date_end = fields.Date('End Date', required=True)
dmo = fields.Boolean('Monday', default=True)
@@ -33,10 +30,6 @@ class MassiveChangesWizard(models.TransientModel):
# string="Virtual Rooms")
room_type_ids = fields.Many2many('hotel.room.type', string="Room Types")
# Availability fields
change_avail = fields.Boolean(default=False)
avail = fields.Integer('Avail', default=0)
# Restriction fields
restriction_id = fields.Many2one('hotel.room.type.restriction',
'Restriction Plan')
@@ -187,46 +180,6 @@ class MassiveChangesWizard(models.TransientModel):
})
hotel_room_type_re_it_obj.create(vals)
@api.model
def _get_availability_values(self, ndate, room_type, record):
hotel_room_type_obj = self.env['hotel.room.type']
vals = {}
if record.change_avail:
cavail = len(hotel_room_type_obj.check_availability_room_type(
ndate.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
ndate.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
room_type_id=room_type.id))
vals.update({
'avail': min(cavail, room_type.total_rooms_count, record.avail),
})
return vals
@api.model
def _save_availability(self, ndate, room_types, record):
hotel_room_type_avail_obj = self.env['hotel.room.type.availability']
domain = [('date', '=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT))]
for room_type in room_types:
vals = self._get_availability_values(ndate, room_type, record)
if not any(vals):
continue
room_types_avail = hotel_room_type_avail_obj.search(
domain+[('room_type_id', '=', room_type.id)]
)
if any(room_types_avail):
# Mail module want a singleton
for vr_avail in room_types_avail:
vr_avail.write(vals)
else:
vals.update({
'date': ndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
'room_type_id': room_type.id
})
hotel_room_type_avail_obj.with_context({
'mail_create_nosubscribe': True,
}).create(vals)
@api.multi
def massive_change_close(self):
self._do_massive_change()
@@ -239,6 +192,13 @@ class MassiveChangesWizard(models.TransientModel):
"type": "ir.actions.do_nothing",
}
@api.model
def _save(self, ndate, room_types, record):
if record.section == 'restrictions':
self._save_restrictions(ndate, room_types, record)
elif record.section == 'prices':
self._save_prices(ndate, room_types, record)
@api.multi
def _do_massive_change(self):
hotel_room_type_obj = self.env['hotel.room.type']
@@ -256,11 +216,5 @@ class MassiveChangesWizard(models.TransientModel):
ndate = date_start_dt + timedelta(days=i)
if not wedays[ndate.timetuple()[6]]:
continue
if record.section == '0':
self._save_availability(ndate, room_types, record)
elif record.section == '1':
self._save_restrictions(ndate, room_types, record)
elif record.section == '2':
self._save_prices(ndate, room_types, record)
self._save(ndate, room_types, record)
return True

View File

@@ -24,24 +24,9 @@
<field name="dsa" colspan="1" />
<field name="dsu" colspan="1" />
</group>
<!-- Availability Fields -->
<group col="3" colspan="3" attrs="{'invisible':[('section', '!=', '0')]}">
<table class="oe_form_group">
<thead>
<th width="12%"></th>
<th></th>
</thead>
<tbody>
<tr class="oe_form_group_row">
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_avail" /> <strong> Avail</strong></td>
<td class="oe_form_group_cell" colspan="3"><field name="avail" attrs="{'readonly':[('change_avail', '=', False)]}" /></td>
</tr>
</tbody>
</table>
</group>
<!-- Restricion Fields -->
<group col="4" colspan="4" attrs="{'invisible':[('section', '!=', '1')]}">
<field name="restriction_id" colspan="4" attrs="{'required':[('section', '=', '1')]}" />
<group col="4" colspan="4" attrs="{'invisible':[('section', '!=', 'restrictions')]}">
<field name="restriction_id" colspan="4" attrs="{'required':[('section', '=', 'restrictions')]}" />
<table class="oe_form_group" colspan="4">
<thead>
<th width="20%"></th>
@@ -80,9 +65,9 @@
</table>
</group>
<!-- Priclist Fields -->
<group attrs="{'invisible':[('section', '!=', '2')]}">
<field name="pricelist_id" attrs="{'required':[('section', '=', '2')]}"/>
<field name="price" attrs="{'required':[('section', '=', '2')]}"/>
<group attrs="{'invisible':[('section', '!=', 'prices')]}">
<field name="pricelist_id" attrs="{'required':[('section', '=', 'prices')]}"/>
<field name="price" attrs="{'required':[('section', '=', 'prices')]}"/>
</group>
<footer>
<button name="massive_change" string="Massive Change" type="object"

View File

@@ -8,7 +8,6 @@ from . import inherited_res_users
from . import inherited_hotel_room
from . import inherited_hotel_room_type
from . import inherited_hotel_room_type_restriction_item
from . import inherited_hotel_room_type_availability
from . import inherited_product_pricelist
from . import inherited_product_pricelist_item
from . import inherited_hotel_folio

View File

@@ -2,7 +2,7 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
from odoo import models, api, _
from odoo import models, api
from odoo.addons.hotel_calendar.controllers.bus import HOTEL_BUS_CHANNEL_ID
@@ -114,21 +114,6 @@ class BusHotelCalendar(models.TransientModel):
},
}
@api.model
def _generate_availability_notification(self, vals):
date_dt = datetime.strptime(vals['date'], DEFAULT_SERVER_DATE_FORMAT)
return {
'type': 'availability',
'availability': {
vals['room_type_id']: {
date_dt.strftime("%d/%m/%Y"): [
vals['avail'],
vals['id'],
],
},
},
}
@api.model
def send_reservation_notification(self, vals):
notif = self._generate_reservation_notif(vals)
@@ -146,9 +131,3 @@ class BusHotelCalendar(models.TransientModel):
notif = self._generate_restriction_notification(vals)
self.env['bus.bus'].sendone((self._cr.dbname, 'hotel.reservation',
HOTEL_BUS_CHANNEL_ID), notif)
@api.model
def send_availability_notification(self, vals):
notif = self._generate_availability_notification(vals)
self.env['bus.bus'].sendone((self._cr.dbname, 'hotel.reservation',
HOTEL_BUS_CHANNEL_ID), notif)

View File

@@ -1,7 +1,7 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from datetime import datetime, timedelta
from datetime import timedelta
from odoo.tools import (
DEFAULT_SERVER_DATE_FORMAT,
DEFAULT_SERVER_DATETIME_FORMAT)
@@ -33,24 +33,12 @@ class HotelCalendarManagement(models.TransientModel):
}
return vals
@api.model
def _get_availability_values(self, avail, room_type):
room_type_obj = self.env['hotel.room.type']
cavail = len(room_type_obj.check_availability_room_type(
avail['date'], avail['date'], room_type_id=room_type.id))
ravail = min(cavail, room_type.total_rooms_count, int(avail['avail']))
vals = {
'avail': ravail,
}
return vals
@api.model
def save_changes(self, pricelist_id, restriction_id, pricelist,
restrictions, availability):
restrictions, availability=False):
room_type_obj = self.env['hotel.room.type']
product_pricelist_item_obj = self.env['product.pricelist.item']
room_type_rest_item_obj = self.env['hotel.room.type.restriction.item']
room_type_avail_obj = self.env['hotel.room.type.availability']
# Save Pricelist
for k_price in pricelist.keys():
@@ -98,26 +86,6 @@ class HotelCalendarManagement(models.TransientModel):
else:
res_id.write(vals)
# Save Availability
for k_avail in availability.keys():
room_type_id = room_type_obj.browse(int(k_avail))
for avail in availability[k_avail]:
vals = self._get_availability_values(avail, room_type_id)
avail_id = room_type_avail_obj.search([
('date', '=', avail['date']),
('room_type_id', '=', room_type_id.id),
], limit=1)
if not avail_id:
vals.update({
'date': avail['date'],
'room_type_id': room_type_id.id,
})
avail_id = room_type_avail_obj.with_context({
'mail_create_nosubscribe': True,
}).create(vals)
else:
avail_id.write(vals)
@api.model
def _hcalendar_room_json_data(self, rooms):
json_data = []
@@ -167,36 +135,6 @@ class HotelCalendarManagement(models.TransientModel):
})
return json_data
@api.model
def _generate_avalaibility_data(self, room_type, date, avail):
return {
'id': avail and avail.id or False,
'date': avail and avail.date or date,
'avail': avail and avail.avail or room_type.total_rooms_count,
}
@api.model
def _hcalendar_availability_json_data(self, dfrom, dto):
date_start = fields.Date.from_string(dfrom)
date_end = fields.Date.from_string(dto)
date_diff = abs((date_end - date_start).days) + 1
hotel_room_type_avail_obj = self.env['hotel.room.type.availability']
room_types = self.env['hotel.room.type'].search([])
json_data = {}
for room_type in room_types:
json_data[room_type.id] = []
for i in range(0, date_diff):
cur_date = date_start + timedelta(days=i)
cur_date_str = cur_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
avail = hotel_room_type_avail_obj.search([
('date', '=', cur_date_str),
('room_type_id', '=', room_type.id)
])
json_data[room_type.id].append(
self._generate_avalaibility_data(room_type, cur_date_str, avail))
return json_data
@api.model
def _hcalendar_events_json_data(self, dfrom, dto):
date_start = fields.Date.from_string(dfrom) - timedelta(days=1)
@@ -286,13 +224,11 @@ class HotelCalendarManagement(models.TransientModel):
json_prices = self._hcalendar_pricelist_json_data(pricelist_item_ids)
json_rest = self._hcalendar_restriction_json_data(restriction_item_ids)
json_avails = self._hcalendar_availability_json_data(dfrom, dto)
json_rc = self._hcalendar_get_count_reservations_json_data(dfrom, dto)
json_events = self._hcalendar_events_json_data(dfrom, dto)
vals.update({
'prices': json_prices or [],
'restrictions': json_rest or [],
'availability': json_avails or [],
'count_reservations': json_rc or [],
'events': json_events or [],
})

View File

@@ -6,7 +6,8 @@ from odoo import models, fields
class ResUsers(models.Model):
_inherit = 'res.users'
pms_divide_rooms_by_capacity = fields.Boolean('Divide rooms by capacity')
pms_divide_rooms_by_capacity = fields.Boolean('Divide rooms by capacity',
default=True)
pms_end_day_week = fields.Selection([
('1', 'Monday'),
('2', 'Tuesday'),

View File

@@ -21,6 +21,8 @@
margin: 0 !important;
}
#pms-menu {
overflow: auto;
background-color: #f8f8f8;
@@ -65,41 +67,57 @@
padding-top: 4px;
}
/* BUTTON STATES */
.navbar-default {
border-color: #f8f8f8;
}
button .led {
position: absolute;
display: inline-block;
right: 5px;
top: -1px;
width: 8px;
height: 8px;
border: 1px solid lightgray;
border-top-width: 0;
border-top-right-radius: 0;
border-top-left-radius: 0;
transition: all 0.5s ease;
}
button .led-disabled {
background-color: dimgrey;
}
button .led-enabled {
background-color: khaki;
box-shadow: 0 0 8px 0px khaki;
.unify-enabled {
background-color: #43A200;
box-shadow: 0 0 8px 0px #43A200;
color: white;
}
button .led-green {
background-color: chartreuse;
box-shadow: 0 0 8px 0px chartreuse;
.divide-enabled {
background-color: #43A200;
box-shadow: 0 0 8px 0px #43A200;
color: white;
}
button .led-blue {
background-color: cornflowerblue;
box-shadow: 0 0 8px 0px cornflowerblue;
.overbooking-enabled {
background-color: #43A200;
box-shadow: 0 0 8px 0px #43A200;
color: white;
}
.cancelled-enabled {
background-color: #43A200;
box-shadow: 0 0 8px 0px #43A200;
}
.swap-from {
background-color: #E7CF1D;
box-shadow: 0 0 8px 0px #E7CF1D;
color: #7C7BAD;
}
.swap-to {
background-color: #43A200;
box-shadow: 0 0 8px 0px #43A200;
color: white;
}
.unify-enabled i, .divide-enabled i, .cancelled-enabled i, .overbooking-enabled i, .swap-to i {
color: white;
}
.swap-from i {
color: #7C7BAD;
}
/* END: BUTTON STATES */
input#bookings_search {
border-top-left-radius: 0;
border-top-right-radius: 0;
@@ -130,14 +148,21 @@ input#bookings_search {
min-height: 3.5em;
padding: 3px;
transition: all 0.5s ease;
overflow: hidden;
}
.button-highlight {
background-color: orange;
.overbooking-highlight {
color: white;
background-color: #FFA500;
}
.button-highlight i {
.cancelled-highlight {
color: white;
background-color: #FFA500;
}
.overbooking-highlight i, .cancelled-highlight i {
color: white;
}
.multi-calendar-tab-plus {
@@ -194,6 +219,11 @@ input#bookings_search {
text-align: left;
}
.text-hidden-xs {
overflow: hidden;
text-overflow: ellipsis;
}
/** TOOLTIPS **/
.popover-content {
font-family: Garuda, sans-serif;
@@ -257,6 +287,12 @@ input#bookings_search {
padding-bottom: 10px;
}
@media (min-width:0px) and (max-width: 1600px) {
/* .text-hidden-xs {
visibility: hidden;
} */
}
/* WARNING: The .row-eq-height class uses CSS3's flexbox layout mode,
which is not supported in Internet Explorer 9 and below. */
.row-eq-height {

View File

@@ -17,7 +17,6 @@ var AbstractController = require('web.AbstractController'),
var PMSCalendarController = AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
onLoadViewFilters: '_onLoadViewFilters',
onUpdateButtonsCounter: '_onUpdateButtonsCounter',
onViewAttached: '_onViewAttached',
onApplyFilters: '_onApplyFilters',
}),
@@ -163,6 +162,7 @@ var PMSCalendarController = AbstractController.extend({
}
self._multi_calendar.set_active_calendar(self._multi_calendar._calendars.length-1);
self._update_buttons_counter();
});
},
@@ -246,6 +246,7 @@ var PMSCalendarController = AbstractController.extend({
self._multi_calendar.merge_reservations(reservs, active_calendar);
self._multi_calendar._assign_extra_info(active_calendar);
self._update_buttons_counter();
});
}.bind(this)).then(function(){
self._last_dates = filterDates;
@@ -264,62 +265,56 @@ var PMSCalendarController = AbstractController.extend({
self._on_change_filter_date();
});
this.renderer.$el.find("#btn_swap button").on('click', function(ev){
this.renderer.$el.find("#btn_swap > button").on('click', function(ev){
var active_calendar = self._multi_calendar.get_active_calendar();
var hcalSwapMode = active_calendar.getSwapMode();
var $led = $(this).find('.led');
var $btn = $(this);
if (hcalSwapMode === HotelCalendar.MODE.NONE) {
active_calendar.setSwapMode(HotelCalendar.MODE.SWAP_FROM);
$("#btn_swap span.ntext").html(_t("Continue"));
$led.removeClass('led-disabled');
$led.addClass('led-green');
$btn.removeClass('swap-to');
$btn.addClass('swap-from');
} else if (active_calendar.getReservationAction().inReservations.length > 0 && hcalSwapMode === HotelCalendar.MODE.SWAP_FROM) {
active_calendar.setSwapMode(HotelCalendar.MODE.SWAP_TO);
$("#btn_swap span.ntext").html(_t("End"));
$led.removeClass('led-green');
$led.addClass('led-blue');
$btn.removeClass('swap-from');
$btn.addClass('swap-to');
} else {
active_calendar.setSwapMode(HotelCalendar.MODE.NONE);
$("#btn_swap span.ntext").html(_t("Start Swap"));
$led.removeClass('led-green');
$led.removeClass('led-blue');
$led.addClass('led-disabled');
$btn.removeClass('swap-from swap-to');
}
});
this.renderer.$el.find('#pms-menu #btn_action_overbooking button').on('click', function(ev){
this.renderer.$el.find('#pms-menu #btn_action_overbooking > button').on('click', function(ev){
var active_calendar = self._multi_calendar.get_active_calendar();
active_calendar.toggleOverbookingsVisibility();
if (active_calendar.options.showOverbookings) {
$(this).find('.led').removeClass('led-disabled');
$(this).find('.led').addClass('led-enabled');
$(this).addClass('overbooking-enabled');
} else {
$(this).find('.led').addClass('led-disabled');
$(this).find('.led').removeClass('led-enabled');
$(this).removeClass('overbooking-enabled');
}
active_calendar.addReservations(_.reject(self._multi_calendar._dataset['reservations'], {overbooking:false}));
});
this.renderer.$el.find('#pms-menu #btn_action_cancelled button').on('click', function(ev){
this.renderer.$el.find('#pms-menu #btn_action_cancelled > button').on('click', function(ev){
var active_calendar = self._multi_calendar.get_active_calendar();
active_calendar.toggleCancelledVisibility();
if (active_calendar.options.showCancelled) {
$(this).find('.led').removeClass('led-disabled');
$(this).find('.led').addClass('led-enabled');
$(this).addClass('cancelled-enabled');
} else {
$(this).find('.led').addClass('led-disabled');
$(this).find('.led').removeClass('led-enabled');
$(this).removeClass('cancelled-enabled');
}
active_calendar.addReservations(_.reject(self._multi_calendar._dataset['reservations'], {cancelled:false}));
});
this.renderer.$el.find('#pms-menu #btn_action_divide button').on('click', function(ev){
this.renderer.$el.find('#pms-menu #btn_action_divide > button').on('click', function(ev){
var active_calendar = self._multi_calendar.get_active_calendar();
var cur_mode = active_calendar.getSelectionMode();
active_calendar.setSelectionMode(cur_mode===HotelCalendar.ACTION.DIVIDE?HotelCalendar.MODE.NONE:HotelCalendar.ACTION.DIVIDE);
});
this.renderer.$el.find('#pms-menu #btn_action_unify button').on('click', function(ev){
this.renderer.$el.find('#pms-menu #btn_action_unify > button').on('click', function(ev){
var active_calendar = self._multi_calendar.get_active_calendar();
var cur_mode = active_calendar.getSelectionMode();
active_calendar.setSelectionMode(cur_mode===HotelCalendar.ACTION.UNIFY?HotelCalendar.MODE.NONE:HotelCalendar.ACTION.UNIFY);
@@ -545,8 +540,8 @@ var PMSCalendarController = AbstractController.extend({
});
this._multi_calendar.on_calendar('hcalOnCancelSwapReservations', function(ev){
$("#btn_swap span.ntext").html(_t("Start Swap"));
var $led = $("#btn_swap").find('.led');
$led.removeClass('led-blue').removeClass('led-green').addClass('led-disabled');
var $btn = $("#btn_swap > button");
$btn.removeClass('swap-from swap-to');
});
this._multi_calendar.on_calendar('hcalOnChangeReservation', function(ev){
var newReservation = ev.detail.newReserv;
@@ -555,64 +550,71 @@ var PMSCalendarController = AbstractController.extend({
var newPrice = ev.detail.newPrice;
var folio_id = newReservation.getUserData('folio_id');
var linkedReservs = _.find(ev.detail.calendar_obj._reservations, function(item){
return item.id !== newReservation.id && !item.unusedZone && item.getUserData('folio_id') === folio_id;
});
var hasChanged = false;
var qdict = {
ncheckin: newReservation.startDate.clone().local().format(HotelConstants.L10N_DATETIME_MOMENT_FORMAT),
ncheckout: newReservation.endDate.clone().local().format(HotelConstants.L10N_DATETIME_MOMENT_FORMAT),
ncheckin: newReservation.startDate.clone().local().format(HotelConstants.L10N_DATE_MOMENT_FORMAT),
ncheckout: newReservation.endDate.clone().local().format(HotelConstants.L10N_DATE_MOMENT_FORMAT),
nroom: newReservation.room.number,
nprice: newPrice,
ocheckin: oldReservation.startDate.clone().local().format(HotelConstants.L10N_DATETIME_MOMENT_FORMAT),
ocheckout: oldReservation.endDate.clone().local().format(HotelConstants.L10N_DATETIME_MOMENT_FORMAT),
nadults: newReservation.adults,
ocheckin: oldReservation.startDate.clone().local().format(HotelConstants.L10N_DATE_MOMENT_FORMAT),
ocheckout: oldReservation.endDate.clone().local().format(HotelConstants.L10N_DATE_MOMENT_FORMAT),
oroom: oldReservation.room.number,
oprice: oldPrice,
hasReservsLinked: (linkedReservs && linkedReservs.length !== 0)?true:false
oadults: oldReservation.adults
};
var dialog = new Dialog(self, {
title: _t("Confirm Reservation Changes"),
buttons: [
{
text: _t("Yes, change it"),
classes: 'btn-primary',
close: true,
disabled: !newReservation.id,
click: function () {
var roomId = newReservation.room.id;
if (newReservation.room.overbooking || newReservation.room.cancelled) {
roomId = +newReservation.room.id.substr(newReservation.room.id.indexOf('@')+1);
if (qdict['ncheckin'] !== qdict['ocheckin'] || qdict['ncheckout'] !== qdict['ocheckout']
|| qdict['nroom'] !== qdict['oroom'] || qdict['nadults'] !== qdict['oadults']) {
var linkedReservs = _.find(ev.detail.calendar_obj._reservations, function(item){
return item.id !== newReservation.id && !item.unusedZone && item.getUserData('folio_id') === folio_id;
});
qdict['hasReservsLinked'] = (linkedReservs && linkedReservs.length !== 0)?true:false;
var hasChanged = false;
var dialog = new Dialog(self, {
title: _t("Confirm Reservation Changes"),
buttons: [
{
text: _t("Yes, change it"),
classes: 'btn-primary',
close: true,
disabled: !newReservation.id,
click: function () {
var roomId = newReservation.room.id;
if (newReservation.room.overbooking || newReservation.room.cancelled) {
roomId = +newReservation.room.id.substr(newReservation.room.id.indexOf('@')+1);
}
var write_values = {
'checkin': newReservation.startDate.format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
'checkout': newReservation.endDate.format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
'room_id': roomId,
'adults': newReservation.adults,
'overbooking': newReservation.room.overbooking
};
if (newReservation.room.cancelled) {
write_values['state'] = 'cancelled';
} else if (!newReservation.room.cancelled && oldReservation.cancelled) {
write_values['state'] = 'draft';
}
self.updateReservations(ev.detail.calendar_obj, [newReservation.id],
write_values, oldReservation, newReservation);
hasChanged = true;
}
},
{
text: _t("No"),
close: true,
}
var write_values = {
'checkin': newReservation.startDate.format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
'checkout': newReservation.endDate.format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
'room_id': roomId,
'overbooking': newReservation.room.overbooking
};
if (newReservation.room.cancelled) {
write_values['state'] = 'cancelled';
} else if (!newReservation.room.cancelled && oldReservation.cancelled) {
write_values['state'] = 'draft';
}
self.updateReservations(ev.detail.calendar_obj, [newReservation.id],
write_values, oldReservation, newReservation);
hasChanged = true;
],
$content: QWeb.render('HotelCalendar.ConfirmReservationChanges', qdict)
}).open();
dialog.on('closed', this, function(e){
if (!hasChanged) {
ev.detail.calendar_obj.replaceReservation(newReservation, oldReservation);
}
},
{
text: _t("No"),
close: true,
}
],
$content: QWeb.render('HotelCalendar.ConfirmReservationChanges', qdict)
}).open();
dialog.on('closed', this, function(e){
if (!hasChanged) {
ev.detail.calendar_obj.replaceReservation(newReservation, oldReservation);
}
});
});
}
});
this._multi_calendar.on_calendar('hcalOnUpdateSelection', function(ev){
for (var td of ev.detail.old_cells) {
@@ -641,17 +643,17 @@ var PMSCalendarController = AbstractController.extend({
}
});
this._multi_calendar.on_calendar('hcalOnChangeSelectionMode', function(ev){
var $ledDivide = this.renderer.$el.find('#pms-menu #btn_action_divide button .led');
var $ledUnify = this.renderer.$el.find('#pms-menu #btn_action_unify button .led');
var $btnDivide = this.renderer.$el.find('#pms-menu #btn_action_divide > button');
var $btnUnify = this.renderer.$el.find('#pms-menu #btn_action_unify > button');
if (ev.detail.newMode === HotelCalendar.ACTION.DIVIDE) {
$ledDivide.removeClass('led-disabled').addClass('led-enabled');
$btnDivide.addClass('divide-enabled');
} else {
$ledDivide.removeClass('led-enabled').addClass('led-disabled');
$btnDivide.removeClass('divide-enabled');
}
if (ev.detail.newMode === HotelCalendar.ACTION.UNIFY) {
$ledUnify.removeClass('led-disabled').addClass('led-enabled');
$btnUnify.addClass('unify-enabled');
} else {
$ledUnify.removeClass('led-enabled').addClass('led-disabled');
$btnUnify.removeClass('unify-enabled');
}
}.bind(this));
this._multi_calendar.on_calendar('hcalOnChangeSelection', function(ev){
@@ -765,19 +767,36 @@ var PMSCalendarController = AbstractController.extend({
};
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onViewAttached: function (ev) {
this._multi_calendar.recalculate_reservation_positions();
},
_onUpdateButtonsCounter: function (ev) {
_update_buttons_counter: function (ev) {
var self = this;
var domain_checkouts = [['checkout', '=', moment().format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT)]];
var domain_checkins = [['checkin', '=', moment().format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT)]];
var domain_overbookings = [['overbooking', '=', true], ['state', 'not in', ['cancelled']]];
var domain_cancelled = [['state', '=', 'cancelled']];
var active_calendar = this._multi_calendar.get_active_calendar();
var filterDates = active_calendar.getDates();
var dfrom_fmt = filterDates[0].format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
dto_fmt = filterDates[1].format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT),
now_fmt = moment().format(HotelConstants.ODOO_DATETIME_MOMENT_FORMAT);
var domain_checkouts = [
['checkout', '=', now_fmt],
['state', 'not in', ['cancelled']]
];
var domain_checkins = [
['checkin', '=', now_fmt],
['state', 'not in', ['cancelled']]
];
var domain_overbookings = [
['checkin', '>=', dfrom_fmt],
['overbooking', '=', true], ['state', 'not in', ['cancelled']]
];
var domain_cancelled = [
'|', '&',
['checkout', '>', dfrom_fmt],
['checkout', '<', dto_fmt],
['checkin', '>=', dfrom_fmt],
['checkin', '<=', dto_fmt],
['state', '=', 'cancelled']
];
$.when(
this.model.search_count(domain_checkouts),
this.model.search_count(domain_checkins),
@@ -788,6 +807,13 @@ var PMSCalendarController = AbstractController.extend({
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onViewAttached: function (ev) {
this._multi_calendar.recalculate_reservation_positions();
},
_onLoadViewFilters: function (ev) {
var self = this;
$.when(
@@ -854,7 +880,7 @@ var PMSCalendarController = AbstractController.extend({
this._multi_calendar.merge_reservations(nreservs);
}
if (need_update_counters) {
this._onUpdateButtonsCounter();
this._update_buttons_counter();
}
},
@@ -923,36 +949,36 @@ var PMSCalendarController = AbstractController.extend({
$dateEndDays.val(active_calendar.getOptions('orig_days'));
$dateEndDays.trigger('change');
/* Overbooking Led */
var $led = this.renderer.$el.find('#pms-menu #btn_action_overbooking button .led');
/* Overbooking */
var $led = this.renderer.$el.find('#pms-menu #btn_action_overbooking > button');
if (active_calendar.options.showOverbookings) {
$led.removeClass('led-disabled').addClass('led-enabled');
$led.addClass('overbooking-enabled');
} else {
$led.removeClass('led-enabled').addClass('led-disabled');
$led.removeClass('overbooking-enabled');
}
/* Cancelled Led */
$led = this.renderer.$el.find('#pms-menu #btn_action_cancelled button .led');
/* Cancelled */
$led = this.renderer.$el.find('#pms-menu #btn_action_cancelled > button');
if (active_calendar.options.showCancelled) {
$led.removeClass('led-disabled').addClass('led-enabled');
$led.addClass('cancelled-enabled');
} else {
$led.removeClass('led-enabled').addClass('led-disabled');
$led.removeClass('cancelled-enabled');
}
/* Divide Led */
$led = this.renderer.$el.find('#pms-menu #btn_action_divide button .led');
/* Divide */
$led = this.renderer.$el.find('#pms-menu #btn_action_divide > button');
if (active_calendar.getSelectionMode() === HotelCalendar.ACTION.DIVIDE) {
$led.removeClass('led-disabled').addClass('led-enabled');
$led.addClass('divide-enabled');
} else {
$led.removeClass('led-enabled').addClass('led-disabled');
$led.removeClass('divide-enabled');
}
/* Unify Led */
$led = this.renderer.$el.find('#pms-menu #btn_action_unify button .led');
$led = this.renderer.$el.find('#pms-menu #btn_action_unify > button');
if (active_calendar.getSelectionMode() === HotelCalendar.ACTION.UNIFY) {
$led.removeClass('led-disabled').addClass('led-enabled');
$led.addClass('unify-enabled');
} else {
$led.removeClass('led-enabled').addClass('led-disabled');
$led.removeClass('unify-enabled');
}
/* Calendar Record */
@@ -962,6 +988,7 @@ var PMSCalendarController = AbstractController.extend({
/* Calendar Filters */
this._refresh_filters(this._multi_calendar.get_active_filters());
this._update_buttons_counter();
},
_refresh_filters: function(calendar_filters) {

View File

@@ -59,41 +59,24 @@ var HotelCalendarView = AbstractRenderer.extend({
update_buttons_counter: function(ncheckouts, ncheckins, noverbookings, ncancelled) {
var self = this;
// Checkouts Button
var $ninfo = self.$el.find('#pms-menu #btn_action_checkout span.ninfo');
$ninfo.text(ncheckouts);
if (ncheckouts) {
$ninfo.parent().parent().addClass('button-highlight');
} else {
$ninfo.parent().parent().removeClass('button-highlight');
}
// Checkins Button
$ninfo = self.$el.find('#pms-menu #btn_action_checkin span.ninfo');
$ninfo.text(ncheckins);
if (ncheckins) {
$ninfo.parent().parent().addClass('button-highlight');
} else {
$ninfo.parent().parent().removeClass('button-highlight');
}
// OverBookings
$ninfo = self.$el.find('#pms-menu #btn_action_overbooking span.ninfo');
var $ninfo = self.$el.find('#pms-menu #btn_action_overbooking span.ninfo');
$ninfo.text(noverbookings);
if (noverbookings) {
$ninfo.parent().parent().addClass('button-highlight');
$ninfo.parent().parent().addClass('overbooking-highlight');
} else {
$ninfo.parent().parent().removeClass('button-highlight');
$ninfo.parent().parent().removeClass('overbooking-highlight');
}
// Cancelled
$ninfo = self.$el.find('#pms-menu #btn_action_cancelled span.ninfo');
$ninfo.text(ncancelled);
if (ncancelled) {
$ninfo.parent().parent().addClass('button-highlight');
} else {
$ninfo.parent().parent().removeClass('button-highlight');
}
// // Cancelled
// $ninfo = self.$el.find('#pms-menu #btn_action_cancelled span.ninfo');
// $ninfo.text(ncancelled);
// if (ncancelled) {
// $ninfo.parent().parent().addClass('cancelled-highlight');
// } else {
// $ninfo.parent().parent().removeClass('cancelled-highlight');
// }
},
init_calendar_view: function(){
@@ -102,20 +85,20 @@ var HotelCalendarView = AbstractRenderer.extend({
/** VIEW CONTROLS INITIALIZATION **/
// DATE TIME PICKERS
var DTPickerOptions = {
viewMode: 'months',
icons : {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down'
},
//language : moment.locale(),
locale : moment.locale(),
format : HotelConstants.L10N_DATE_MOMENT_FORMAT,
widgetPositioning:{
horizontal: 'auto',
vertical: 'bottom'
}
viewMode: 'months',
icons : {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down'
},
//language : moment.locale(),
locale : moment.locale(),
format : HotelConstants.L10N_DATE_MOMENT_FORMAT,
widgetPositioning:{
horizontal: 'auto',
vertical: 'bottom'
}
};
var $dateTimePickerBegin = this.$el.find('#pms-menu #date_begin');
var $dateEndDays = this.$el.find('#pms-menu #date_end_days');
@@ -179,7 +162,6 @@ var HotelCalendarView = AbstractRenderer.extend({
});
return $.when(
this.trigger_up('onUpdateButtonsCounter'),
this.trigger_up('onLoadViewFilters'),
);
},

View File

@@ -126,20 +126,6 @@ var MPMSCalendarController = AbstractController.extend({
for (var notif of notifications) {
if (notif[0][1] === 'hotel.reservation') {
switch (notif[1]['type']) {
case 'availability':
var avail = notif[1]['availability'];
var room_type = Object.keys(avail)[0];
var day = Object.keys(avail[room_type])[0];
var dt = HotelCalendarManagement.toMoment(day);
var availability = {};
availability[room_type] = [{
'date': dt.format(HotelConstants.ODOO_DATE_MOMENT_FORMAT),
'avail': avail[room_type][day][0],
'no_ota': avail[room_type][day][1],
'id': avail[room_type][day][2]
}];
this.renderer._hcalendar.addAvailability(availability);
break;
case 'pricelist':
var prices = notif[1]['price'];
var pricelist_id = Object.keys(prices)[0];

View File

@@ -21,7 +21,7 @@ return AbstractModel.extend({
},
save_changes: function (params) {
//params.splice(0, 0, false); // FIXME: ID=False because first parameter its an integer
params.splice(0, 0, false); // FIXME: ID=False because first parameter its an integer
return this._rpc({
model: this.modelName,
method: 'save_changes',

View File

@@ -69,19 +69,21 @@ var HotelCalendarManagementView = AbstractRenderer.extend({
},
/** CUSTOM METHODS **/
save_changes: function() {
var self = this;
get_values_to_save: function() {
var btn_save = this.$el.find('#btn_save_changes');
if (!btn_save.hasClass('need-save')) {
return;
return false;
}
var pricelist = this._hcalendar.getPricelist(true);
var restrictions = this._hcalendar.getRestrictions(true);
var availability = this._hcalendar.getAvailability(true);
var params = this.generate_params();
var oparams = [params['prices'], params['restrictions'], pricelist, restrictions, availability];
return [params['prices'], params['restrictions'], pricelist, restrictions];
},
save_changes: function() {
var oparams = this.get_values_to_save();
this.trigger_up('onSaveChanges', oparams);
},
@@ -135,7 +137,7 @@ var HotelCalendarManagementView = AbstractRenderer.extend({
this.$CalendarHeaderDays = this.$el.find("div.table-room_type-data-header");
// Sticky Header Days
this.$ehcal.scroll(this._on_scroll.bind(this));
$('.o_content').scroll(this._on_scroll.bind(this));
},
setCalendarData: function (prices, restrictions, availability, count_reservations) {
@@ -169,10 +171,10 @@ var HotelCalendarManagementView = AbstractRenderer.extend({
},
_on_scroll: function() {
var curScrollPos = this.$ehcal.scrollTop();
var curScrollPos = $('.o_content').scrollTop();
if (curScrollPos > 0) {
this.$CalendarHeaderDays.css({
top: `${curScrollPos}px`,
top: `${curScrollPos-this.$ehcal.position().top}px`,
position: 'sticky'
});
} else {

View File

@@ -5,6 +5,14 @@
* Alexandre Díaz <alex@aloxa.eu>
*/
#pms-search-cal-pag {
text-align: center;
}
#mpms-search {
padding: 0.5em;
}
.hcal-management-table, .hcal-management-table-day {
border-collapse: collapse !important;
table-layout: fixed;
@@ -29,7 +37,7 @@
width: 110px !important;
vertical-align: center;
min-width: 110px;
height: 115px;
height: 130px;
min-height: 100px;
white-space: nowrap;
/*overflow: hidden;*/
@@ -77,11 +85,11 @@
}
.hcal-management-low > tbody > tr > td, .hcal-management-low > table > tbody > tr > td {
height: 35px !important;
height: 58px !important;
}
.hcal-management-medium > tbody > tr > td, .hcal-management-medium > table > tbody > tr > td {
height: 60px !important;
height: 80px !important;
}
.hcal-management-medium tr[name='rest_b'], .hcal-management-medium tr[name='rest_c'],
@@ -92,12 +100,20 @@
.table-room_types {
flex: 1 1 auto;
margin-top: 1.6em;
margin-top: 1.2em;
}
.table-room_type-data-header {
z-index: 1;
}
.table-room_type-data-header .hcal-management-table tr td {
background-color: white;
}
#hcal_management_widget {
overflow: auto;
display:flex;
display: flex;
}
#hcal-management-container-dd {
@@ -139,6 +155,11 @@ table.hcal-management-table input {
padding: 0.5em;
}
.filter-title {
overflow: hidden;
text-overflow: ellipsis;
}
button.hcal-management-input {
font-size: 9px;
border: 1px solid gray;

View File

@@ -1936,7 +1936,13 @@ HotelCalendar.prototype = {
var bounds = this.loopedOffsetOptimized(rdiv);
rdiv.addEventListener('mousemove', function(ev){
var posAction = $this._getRerservationPositionAction(this, ev.layerX, ev.layerY);
this.style.cursor = (posAction == HotelCalendar.ACTION.MOVE_LEFT || posAction == HotelCalendar.ACTION.MOVE_RIGHT)?'col-resize':'pointer';
if (posAction == HotelCalendar.ACTION.MOVE_LEFT || posAction == HotelCalendar.ACTION.MOVE_RIGHT) {
this.style.cursor = 'col-resize';
} else if (posAction == HotelCalendar.ACTION.MOVE_DOWN) {
this.style.cursor = 'ns-resize';
} else {
this.style.cursor = 'pointer';
}
}, false);
var _funcEvent = function(ev){
if ($this._isLeftButtonPressed(ev)) {
@@ -2014,8 +2020,8 @@ HotelCalendar.prototype = {
$this._lazyModeReservationsSelection = setTimeout(function($this){
var reserv = $this.getReservation(this.dataset.hcalReservationObjId);
$this._updateHighlightInvalidZones(reserv);
if (reserv.readOnly || (reserv.fixDays && ($this.reservationAction.action == HotelCalendar.ACTION.MOVE_LEFT ||
$this.reservationAction.action == HotelCalendar.ACTION.MOVE_RIGHT))) {
if (reserv.readOnly || (reserv.fixDays && ($this.reservationAction.action == HotelCalendar.ACTION.MOVE_LEFT
|| $this.reservationAction.action == HotelCalendar.ACTION.MOVE_RIGHT))) {
$this.reservationAction.action = HotelCalendar.ACTION.NONE;
return false;
}
@@ -2084,8 +2090,9 @@ HotelCalendar.prototype = {
_getRerservationPositionAction: function(/*HTMLObject*/elm, /*Int*/posX, /*Int*/posY) {
var bounds = this.loopedOffsetOptimized(elm);
if (posX <= 5) { return HotelCalendar.ACTION.MOVE_LEFT; }
else if (posX >= bounds.width-10) { return HotelCalendar.ACTION.MOVE_RIGHT; }
if (posX <= 4) { return HotelCalendar.ACTION.MOVE_LEFT; }
else if (posX >= bounds.width-8) { return HotelCalendar.ACTION.MOVE_RIGHT; }
else if (posY >= bounds.height-4) { return HotelCalendar.ACTION.MOVE_DOWN; }
return HotelCalendar.ACTION.MOVE_ALL;
},
@@ -2542,6 +2549,18 @@ HotelCalendar.prototype = {
reserv.startDate.set({'date': date_cell.date(), 'month': date_cell.month(), 'year': date_cell.year()});
this.reservationAction.newReservationObj = reserv;
needUpdate = true;
} else if (this.reservationAction.action == HotelCalendar.ACTION.MOVE_DOWN) {
var parentRow = ev.target.parentNode.parentNode.parentNode.parentNode;
var room = this.getRoom(parentRow.dataset.hcalRoomObjId);
if (room.id === reserv.room.id) {
if (!this.reservationAction.oldReservationObj) {
this.reservationAction.oldReservationObj = reserv.clone();
}
reserv.adults = +ev.target.dataset.hcalBedNum + 1;
this.reservationAction.newReservationObj = reserv;
needUpdate = true;
}
} else if (this.reservationAction.action == HotelCalendar.ACTION.MOVE_ALL) {
// Relative Movement
date_cell.subtract(this.reservationAction.daysOffset, 'd');
@@ -2856,7 +2875,7 @@ HotelCalendar.prototype = {
/** CONSTANTS **/
HotelCalendar.DOMAIN = { NONE: -1, RESERVATIONS: 0, ROOMS: 1 };
HotelCalendar.ACTION = { NONE: -1, MOVE_ALL: 0, MOVE_LEFT: 1, MOVE_RIGHT: 2, SWAP: 3, DIVIDE: 4, UNIFY: 5 };
HotelCalendar.ACTION = { NONE: -1, MOVE_ALL: 0, MOVE_LEFT: 1, MOVE_RIGHT: 2, MOVE_DOWN: 3, SWAP: 4, DIVIDE: 5, UNIFY: 6 };
HotelCalendar.MODE = { NONE: -1, SWAP_FROM: 0, SWAP_TO: 1 };
HotelCalendar.DATE_FORMAT_SHORT_ = 'DD/MM/YYYY';
HotelCalendar.DATE_FORMAT_LONG_ = HotelCalendar.DATE_FORMAT_SHORT_ + ' HH:mm:ss';

View File

@@ -201,24 +201,42 @@ HotelCalendarManagement.prototype = {
row = table.insertRow();
row.setAttribute('name', 'price');
cell = row.insertCell();
cell.setAttribute('colspan', '3');
cell.setAttribute('colspan', '4');
telm = document.createElement("input");
telm.setAttribute('id', this._sanitizeId(`PRICE_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'price');
telm.setAttribute('type', 'edit');
telm.setAttribute('title', this._t('Price'));
telm.value = room.price;
telm.dataset.orgValue = room.price;
telm.dataset.hcalParentCell = parentCell.getAttribute('id');
telm.classList.add('hcal-management-input');
telm.addEventListener('change', function(ev){ $this.onInputChange(ev, this); }, false);
cell.appendChild(telm);
row = table.insertRow();
row.setAttribute('name', 'avail');
row.style.display = 'none';
cell = row.insertCell();
cell.setAttribute('colspan', '2');
telm = document.createElement("input");
telm.setAttribute('id', this._sanitizeId(`QUOTA_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'quota');
telm.setAttribute('type', 'edit');
telm.setAttribute('title', this._t('Availability Quota'));
telm.value = telm.dataset.orgValue = 0;
telm.dataset.hcalParentCell = parentCell.getAttribute('id');
telm.classList.add('hcal-management-input');
telm.addEventListener('change', function(ev){ $this.onInputChange(ev, this); }, false);
cell.appendChild(telm);
cell = row.insertCell();
cell.setAttribute('colspan', '2');
telm = document.createElement("input");
telm.setAttribute('id', this._sanitizeId(`AVAIL_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'avail');
telm.setAttribute('id', this._sanitizeId(`MAX_AVAIL_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'max_avail');
telm.setAttribute('type', 'edit');
telm.setAttribute('title', this._t('Availability'));
telm.value = 0;
telm.setAttribute('title', this._t('Max. Availability'));
telm.value = telm.dataset.orgValue = 0;
telm.dataset.hcalParentCell = parentCell.getAttribute('id');
telm.classList.add('hcal-management-input');
telm.addEventListener('change', function(ev){ $this.onInputChange(ev, this); }, false);
@@ -305,6 +323,7 @@ HotelCalendarManagement.prototype = {
telm.appendChild(selectOpt);
cell.appendChild(telm);
cell = row.insertCell();
cell.setAttribute('colspan', '1');
telm = document.createElement("input");
telm.setAttribute('id', this._sanitizeId(`FREE_ROOMS_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'free_rooms');
@@ -331,7 +350,6 @@ HotelCalendarManagement.prototype = {
telm.classList.add('hcal-management-input', 'pull-left');
telm.addEventListener('click', function(ev){ $this.onInputChange(ev, this); }, false);
cell.appendChild(telm);
telm = document.createElement("span");
telm.setAttribute('id', this._sanitizeId(`OPTIONS_${roomId}_${dateShortStr}`));
telm.setAttribute('name', 'options');
@@ -376,8 +394,7 @@ HotelCalendarManagement.prototype = {
this._updateRestrictions();
}
if (typeof avail !== 'undefined' && avail) {
this._availability = avail;
this._updateAvailability();
this.setAvailability(avail);
}
if (typeof count_free_rooms !== 'undefined' && count_free_rooms) {
this._free_rooms = count_free_rooms;
@@ -385,6 +402,16 @@ HotelCalendarManagement.prototype = {
}
},
setAvailability: function(avails) {
this._availability = avails;
if (this._availability) {
for (var elm of this.etable.querySelectorAll("tr[name=avail]")) {
elm.style.display = "";
}
}
this._updateAvailability();
},
//==== ROOMS
getRoom: function(/*String*/id) {
return _.find(this.options.rooms, function(item){ return item.id == id; });
@@ -696,7 +723,8 @@ HotelCalendarManagement.prototype = {
for (var avail of this._availability[room_typeId]) {
var dd = HotelCalendarManagement.toMoment(avail.date, this.options.dateFormatShort);
var inputIds = [
`AVAIL_${room_typeId}_${dd.format(HotelCalendarManagement._DATE_FORMAT_SHORT)}`, avail.avail,
`QUOTA_${room_typeId}_${dd.format(HotelCalendarManagement._DATE_FORMAT_SHORT)}`, avail.quota,
`MAX_AVAIL_${room_typeId}_${dd.format(HotelCalendarManagement._DATE_FORMAT_SHORT)}`, avail.max_avail,
`NO_OTA_${room_typeId}_${dd.format(HotelCalendarManagement._DATE_FORMAT_SHORT)}`, avail.no_ota
];
@@ -731,17 +759,21 @@ HotelCalendarManagement.prototype = {
for (var i=0; i<=this.options.days; i++) {
var ndate = this.options.startDate.clone().add(i, 'd');
var ndateStr = ndate.format(HotelCalendarManagement._DATE_FORMAT_SHORT);
var inputAvailId = this._sanitizeId(`AVAIL_${room.id}_${ndateStr}`);
var inputAvail = this.etable.querySelector(`#${inputAvailId}`);
var inputQuotaId = this._sanitizeId(`QUOTA_${room.id}_${ndateStr}`);
var inputQuota = this.etable.querySelector(`#${inputQuotaId}`);
var inputMaxAvailId = this._sanitizeId(`MAX_AVAIL_${room.id}_${ndateStr}`);
var inputMaxAvail = this.etable.querySelector(`#${inputMaxAvailId}`);
var inputNoOTAId = this._sanitizeId(`NO_OTA_${room.id}_${ndateStr}`);
var inputNoOTA = this.etable.querySelector(`#${inputNoOTAId}`);
if (!onlyNew || (onlyNew && (inputAvail.value !== inputAvail.dataset.orgValue ||
(inputNoOTA.dataset.state && inputNoOTA.dataset.state !== inputNoOTA.dataset.orgValue)))) {
if (!onlyNew || (onlyNew && (inputQuota.value !== inputQuota.dataset.orgValue
|| inputMaxAvail.value !== inputMaxAvail.dataset.orgValue
|| (inputNoOTA.dataset.state && inputNoOTA.dataset.state !== inputNoOTA.dataset.orgValue)))) {
if (!(room.id in data)) { data[room.id] = []; }
data[room.id].push({
'date': ndate.format('YYYY-MM-DD'),
'avail': inputAvail.value,
'quota': inputQuota.value,
'max_avail': inputMaxAvail.value,
'no_ota': Boolean(inputNoOTA.dataset.state === 'true') || false
});
}
@@ -790,7 +822,7 @@ HotelCalendarManagement.prototype = {
value = elm.checked;
}
else if (name === 'min_stay' || name === 'min_stay_arrival' || name === 'max_stay' ||
name === 'price' || name === 'avail' || name === 'max_stay_arrival') {
name === 'price' || name === 'quota' || name === 'max_avail' || name === 'max_stay_arrival') {
if (!this._isNumeric(value)) {
elm.style.backgroundColor = 'red';
} else if (orgValue !== value) {
@@ -973,9 +1005,9 @@ HotelCalendarManagement.prototype = {
},
minDate: $this.options.startDate,
maxDate: $this.options.startDate.clone().add($this.options.days, 'd'),
language : moment.locale(),
//language : moment.locale(),
format : HotelCalendarManagement._DATE_FORMAT_SHORT,
disabledHours: true // TODO: Odoo uses old datetimepicker version
disabledHours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 18, 19, 20, 21, 22, 23]
};
$('table#hcal-management-clone-dates #date_begin').datetimepicker(DTPickerOptions);

View File

@@ -4,81 +4,63 @@
<t t-name="hotel_calendar.HotelCalendarManagementView">
<div class="col-xs-12 col-lg-12 nopadding">
<div class="col-xs-12 col-lg-12 nopadding">
<div class="col-xs-12 col-lg-12 nopadding" id="mpms-search">
<div class="col-xs-12 col-lg-12" id="mpms-search">
<table class="col-xs-12 col-lg-12 nopadding" id="pms-search-table">
<tbody>
<tr>
<td class="col-xs-1 col-lg-1">
<button class="btn col-xs-12 col-lg-12" id="btn_save_changes" title="Save Changes">
<i class="fa fa-save fa-stack-2x"> </i> &amp;nbsp;
<i class="fa fa-save fa-stack-2x"> </i>
</button>
</td>
<td class="col-xs-4 col-lg-3">
<div class="col-xs-12 col-lg-12 nopadding">
<div class="col-xs-3 col-lg-3 nopadding text-right filter-title">
<strong style="line-height:2em">PRICELIST:</strong>
</div>
<div class="col-xs-9 col-lg-9">
<select class="list form-control" id="price_list"/>
</div>
</div>
<div class="col-xs-12 col-lg-12 nopadding">
<div class="col-xs-3 col-lg-3 nopadding text-right filter-title">
<strong style="line-height:2em">RESTRICTIONS:</strong>
</div>
<div class="col-xs-9 col-lg-9">
<select class="list form-control" id="restriction_list"/>
</div>
</div>
<td class="col-xs-2 col-lg-2">
<select class="list form-control" id="price_list"/>
<select class="list form-control" id="restriction_list"/>
</td>
<td class="col-xs-4 col-lg-4">
<div class="col-xs-12 col-lg-12">
<table style="margin: 0 auto">
<tbody>
<tr>
<td>
FROM<br/>
<div class="input-group date" id="date_begin">
<input type="text" class="o_datepicker_input form-control" name="date_begin" required="required" />
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
</td>
<td>
TO<br/>
<div class="input-group date" id="date_end">
<input type="text" class="o_datepicker_input form-control" name="date_end" required="required" />
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<table style="margin: 0 auto">
<tbody>
<tr>
<td>
<span class="filter-title">FROM</span><br/>
<div class="input-group date" id="date_begin">
<input type="text" class="o_datepicker_input form-control" name="date_begin" required="required" />
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
</td>
<td>
<span class="filter-title">TO</span><br/>
<div class="input-group date" id="date_end">
<input type="text" class="o_datepicker_input form-control" name="date_end" required="required" />
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
</div>
</td>
</tr>
</tbody>
</table>
</td>
<td class="col-xs-1 col-lg-1">
<strong style="line-height:2em">View Mode:</strong>
<strong class="filter-title">View Mode:</strong>
<select class="list form-control" id="mode_list">
<option value="all">All</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</td>
<td class="col-xs-2 col-lg-2">
<div class="col-xs-12 col-lg-12" id="pms-search-cal-pag">
<button id="cal-pag-prev-plus" class="btn"><i class="fa fa-3x fa-angle-double-left"></i></button>
<button id="cal-pag-prev" class="btn"><i class="fa fa-3x fa-angle-left"></i></button>
<button id="cal-pag-selector" class="btn"><i class="fa fa-3x fa-calendar"></i></button>
<button id="cal-pag-next" class="btn"><i class="fa fa-3x fa-angle-right"></i></button>
<button id="cal-pag-next-plus" class="btn"><i class="fa fa-3x fa-angle-double-right"></i></button>
</div>
<td class="col-xs-2 col-lg-2" id="pms-search-cal-pag">
<button id="cal-pag-prev-plus" class="btn btn-sm"><i class="fa fa-2x fa-angle-double-left"></i></button>
<button id="cal-pag-prev" class="btn btn-sm"><i class="fa fa-2x fa-angle-left"></i></button>
<button id="cal-pag-selector" class="btn btn-sm"><i class="fa fa-2x fa-calendar"></i></button>
<button id="cal-pag-next" class="btn btn-sm"><i class="fa fa-2x fa-angle-right"></i></button>
<button id="cal-pag-next-plus" class="btn btn-sm"><i class="fa fa-2x fa-angle-double-right"></i></button>
</td>
<td class="col-xs-1 col-lg-1">
<button class="btn btn-default col-xs-12 col-lg-12" id="btn_massive_changes" title="Launch Massive Changes">
<i class="fa fa-bolt fa-stack-2x"> </i> &amp;nbsp;
<i class="fa fa-bolt fa-stack-2x"> </i>
</button>
</td>
</tr>

View File

@@ -17,6 +17,9 @@
<t t-if="oroom != nroom">
<strong>Room:</strong> <t t-esc="oroom"/><br/>
</t>
<t t-if="oadults != nadults">
<strong>Adults:</strong> <t t-esc="oadults"/><br/>
</t>
<!--
<t t-if="oprice != nprice">
<strong>Price:</strong> <t t-esc="oprice" widget="monetary"/><br/>
@@ -36,6 +39,9 @@
<t t-if="oroom != nroom">
<strong>Room:</strong> <t t-esc="nroom"/><br/>
</t>
<t t-if="oadults != nadults">
<strong>Adults:</strong> <t t-esc="nadults"/><br/>
</t>
<!--
<t t-if="oprice != nprice">
<strong>Price:</strong> <t t-esc="nprice" widget="monetary"/><br/>

View File

@@ -23,7 +23,7 @@
<i class="fa fa-fw o_button_icon fa-sign-out"> </i>
<div class="o_button_text">
<span class="ninfo">0</span><br/>
Checkouts
<span class="text-hidden-xs">Checkouts</span>
</div>
</button>
</div>
@@ -32,7 +32,7 @@
<i class="fa fa-fw o_button_icon fa-sign-in"> </i>
<div class="o_button_text">
<span class="ninfo">0</span><br/>
Checkins
<span class="text-hidden-xs">Checkins</span>
</div>
</button>
</div>
@@ -41,7 +41,7 @@
<div class='led led-disabled'></div>
<i class="fa fa-fw o_button_icon fa-retweet"> </i>
<div class="o_button_text">
<span class="ntext">Start Swap</span>
<span class="text-hidden-xs ntext">Start Swap</span>
</div>
</button>
</div>
@@ -49,7 +49,7 @@
<button class="btn btn-default col-xs-12 col-md-12 button-box" data-action="hotel.open_wizard_reservations">
<i class="fa fa-fw o_button_icon fa-magic"> </i>
<div class="o_button_text">
Wizard
<span class="text-hidden-xs">Wizard</span>
</div>
</button>
</div>
@@ -59,7 +59,7 @@
<i class="fa fa-fw o_button_icon fa-clock-o"> </i>
<div class="o_button_text">
<span class="ninfo">0</span><br/>
Overbooking
<span class="text-hidden-xs">Overbook.</span>
</div>
</button>
</div>
@@ -69,7 +69,7 @@
<i class="fa fa-fw o_button_icon fa-calendar-times-o"> </i>
<div class="o_button_text">
<span class="ninfo">0</span><br/>
Cancelled
<span class="text-hidden-xs">Cancelled</span>
</div>
</button>
</div>
@@ -78,7 +78,7 @@
<div class='led led-disabled'></div>
<i class="fa fa-fw o_button_icon fa-scissors"> </i>
<div class="o_button_text">
Divide
<span class="text-hidden-xs">Divide</span>
</div>
</button>
</div>
@@ -87,7 +87,7 @@
<div class='led led-disabled'></div>
<i class="fa fa-fw o_button_icon fa-compress"> </i>
<div class="o_button_text">
Unify
<span class="text-hidden-xs">Unify</span>
</div>
</button>
</div>

View File

@@ -1,5 +1,6 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2018-2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import inherited_hotel_reservation
from . import inherited_bus_hotel_calendar
from . import inherited_hotel_calendar_management
from . import inherited_hotel_room_type_availability

View File

@@ -1,4 +1,4 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2018-2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
@@ -9,6 +9,29 @@ from odoo.addons.hotel_calendar.controllers.bus import HOTEL_BUS_CHANNEL_ID
class BusHotelCalendar(models.TransientModel):
_inherit = 'bus.hotel.calendar'
@api.model
def _generate_availability_notification(self, vals):
date_dt = datetime.strptime(vals['date'], DEFAULT_SERVER_DATE_FORMAT)
return {
'type': 'availability',
'availability': {
vals['room_type_id']: {
date_dt.strftime("%d/%m/%Y"): {
'quota': vals['quota'],
'max_avail': vals['max_avail'],
'id': vals['id'],
'no_ota': vals['no_ota'],
},
},
},
}
@api.model
def send_availability_notification(self, vals):
notif = self._generate_availability_notification(vals)
self.env['bus.bus'].sendone((self._cr.dbname, 'hotel.reservation',
HOTEL_BUS_CHANNEL_ID), notif)
@api.model
def _generate_issue_notification(self, ntype, title, issue_id, section,
message):
@@ -26,14 +49,6 @@ class BusHotelCalendar(models.TransientModel):
},
}
@api.model
def _generate_availability_notification(self, vals):
date_dt = datetime.strptime(vals['date'], DEFAULT_SERVER_DATE_FORMAT)
json = super(BusHotelCalendar, self)._generate_availability_notification(vals)
json['availability'][vals['room_type_id']][date_dt.strftime("%d/%m/%Y")].append(
vals['no_ota'])
return json
@api.model
def send_issue_notification(self, ntype, title, issue_id, section, message):
notif = self._generate_issue_notification(ntype, title, issue_id, section, message)

View File

@@ -1,38 +1,96 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, api
from datetime import timedelta
from odoo import models, api, fields
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
class HotelCalendarManagement(models.TransientModel):
_inherit = 'hotel.calendar.management'
@api.model
def _get_availability_values(self, avail, room_type):
vals = super(HotelCalendarManagement, self)._get_availability_values(
avail, room_type)
vals.update({
'channel_max_avail': vals['avail'],
'no_ota': avail['no_ota'],
})
return vals
def _hcalendar_availability_json_data(self, dfrom, dto):
date_start = fields.Date.from_string(dfrom)
date_end = fields.Date.from_string(dto)
date_diff = abs((date_end - date_start).days) + 1
hotel_room_type_avail_obj = self.env['hotel.room.type.availability']
room_types = self.env['hotel.room.type'].search([])
json_data = {}
for room_type in room_types:
json_data[room_type.id] = []
for i in range(0, date_diff):
cur_date = date_start + timedelta(days=i)
cur_date_str = cur_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
avail = hotel_room_type_avail_obj.search([
('date', '=', cur_date_str),
('room_type_id', '=', room_type.id)
])
json_data[room_type.id].append(
self._generate_avalaibility_data(room_type, cur_date_str, avail))
return json_data
@api.model
def _generate_avalaibility_data(self, room_type, date, avail):
vals = super(HotelCalendarManagement, self)._generate_avalaibility_data(
room_type, date, avail)
vals.update({
return {
'id': avail and avail.id or False,
'date': avail and avail.date or date,
'no_ota': avail and avail.no_ota or False,
})
'quota': avail and avail.quota or -1,
'max_avail': avail and avail.max_avail or -1,
}
@api.model
def _get_availability_values(self, vals):
vals = {
'quota': vals['quota'],
'max_avail': vals['max_avail'],
'no_ota': vals['no_ota'],
}
return vals
@api.multi
def save_changes(self, pricelist_id, restriction_id, pricelist,
restrictions, availability):
restrictions, availability=False):
res = super(HotelCalendarManagement, self).save_changes(
pricelist_id,
restriction_id,
pricelist,
restrictions,
availability)
availability=availability)
room_type_obj = self.env['hotel.room.type']
room_type_avail_obj = self.env['hotel.room.type.availability']
# Save Availability
for k_avail in availability.keys():
room_type_id = room_type_obj.browse(int(k_avail))
for avail in availability[k_avail]:
vals = self._get_availability_values(avail)
avail_id = room_type_avail_obj.search([
('date', '=', avail['date']),
('room_type_id', '=', room_type_id.id),
], limit=1)
if not avail_id:
vals.update({
'date': avail['date'],
'room_type_id': room_type_id.id,
})
avail_id = room_type_avail_obj.with_context({
'mail_create_nosubscribe': True,
}).create(vals)
else:
avail_id.write(vals)
self.env['channel.backend'].cron_push_changes()
return res
@api.model
def get_hcalendar_all_data(self, dfrom, dto, pricelist_id, restriction_id,
withRooms):
res = super(HotelCalendarManagement, self).get_hcalendar_all_data(
dfrom, dto, pricelist_id, restriction_id, withRooms)
json_avails = self._hcalendar_availability_json_data(dfrom, dto)
res.update({
'availability': json_avails or [],
})
return res

View File

@@ -1,6 +1,6 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2018-2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields, api
from odoo import models, api
class HotelRoomTypeAvailability(models.Model):
@@ -9,7 +9,9 @@ class HotelRoomTypeAvailability(models.Model):
def _prepare_notif_values(self, record):
return {
'date': record.date,
'avail': record.avail,
'quota': record.quota,
'no_ota': record.no_ota,
'max_avail': record.max_avail,
'room_type_id': record.room_type_id.id,
'id': record.id,
}

View File

@@ -35,7 +35,6 @@
}
}
#pms-menu #btn_channel_manager_request.incoming i {
animation-name: channel-manager-changes;
animation-duration: 2s;

View File

@@ -1,6 +1,6 @@
// Copyright 2018 Alexandre Díaz <dev@redneboa.es>
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('hotel_calendar.PMSHotelCalendarControllerChannelConnector', function (require) {
odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarController', function (require) {
"use strict";
var PMSCalendarController = require('hotel_calendar.PMSCalendarController');
@@ -8,7 +8,7 @@ var Core = require('web.core');
var QWeb = Core.qweb;
var PMSHotelCalendarControllerChannelConnector = PMSCalendarController.include({
var PMSHotelCalendarController = PMSCalendarController.include({
_sounds: [],
SOUNDS: { NONE: 0, BOOK_NEW:1, BOOK_CANCELLED:2 },
@@ -88,6 +88,6 @@ var PMSHotelCalendarControllerChannelConnector = PMSCalendarController.include({
});
return PMSHotelCalendarControllerChannelConnector;
return PMSHotelCalendarController;
});

View File

@@ -0,0 +1,46 @@
// Copyright 2019 Alexandre Díaz <dev@redneboa.es>
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('hotel_calendar_channel_connector.MPMSCalendarController', function (require) {
"use strict";
var MPMSCalendarController = require('hotel_calendar.MPMSCalendarController');
var HotelConstants = require('hotel_calendar.Constants');
var Core = require('web.core');
var QWeb = Core.qweb;
var MPMSCalendarController = MPMSCalendarController.include({
_onBusNotification: function (notifications) {
this._super.apply(this, arguments);
if (!this.renderer._hcalendar) { return; }
for (var notif of notifications) {
if (notif[0][1] === 'hotel.reservation') {
switch (notif[1]['type']) {
case 'availability':
var avail = notif[1]['availability'];
var room_type = Object.keys(avail)[0];
var day = Object.keys(avail[room_type])[0];
var dt = HotelCalendarManagement.toMoment(day);
var availability = {};
availability[room_type] = [{
'date': dt.format(HotelConstants.ODOO_DATE_MOMENT_FORMAT),
'quota': avail[room_type][day]['quota'],
'max_avail': avail[room_type][day]['max_avail'],
'no_ota': avail[room_type][day]['no_ota'],
'id': avail[room_type][day]['id']
}];
this.renderer._hcalendar.addAvailability(availability);
break;
}
}
}
},
});
return MPMSCalendarController;
});

View File

@@ -0,0 +1,28 @@
// Copyright 2019 Alexandre Díaz <dev@redneboa.es>
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('hotel_calendar_channel_connector.MPMSCalendarRenderer', function (require) {
"use strict";
var MPMSCalendarRenderer = require('hotel_calendar.MPMSCalendarRenderer');
var Core = require('web.core');
var QWeb = Core.qweb;
var MPMSCalendarRenderer = MPMSCalendarRenderer.include({
/** CUSTOM METHODS **/
get_values_to_save: function() {
var values = this._super.apply(this, arguments);
if (values) {
var availability = this._hcalendar.getAvailability(true);
values.push(availability);
}
return values;
},
});
return MPMSCalendarRenderer;
});

View File

@@ -1,12 +1,12 @@
/* global odoo, $ */
// Copyright 2018 Alexandre Díaz <dev@redneboa.es>
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarRendererChannelConnector', function (require) {
odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarRenderer', function (require) {
'use strict';
var PMSCalendarRenderer = require('hotel_calendar.PMSCalendarRenderer');
var PMSHotelCalendarRendererChannelConnector = PMSCalendarRenderer.include({
var PMSHotelCalendarRenderer = PMSCalendarRenderer.include({
update_buttons_counter_channel_connector: function (nreservations, nissues) {
// Cloud Reservations
@@ -46,5 +46,5 @@ odoo.define('hotel_calendar_channel_connector.PMSHotelCalendarRendererChannelCon
}
});
return PMSHotelCalendarRendererChannelConnector;
return PMSHotelCalendarRenderer;
});

View File

@@ -8,7 +8,7 @@
<i class="fa fa-fw o_button_icon fa-exclamation-circle"> </i>
<div class="o_button_text">
<span class="ninfo">0</span><br/>
Issues
<span class="text-hidden-xs">Issues</span>
</div>
</button>
</div>
@@ -17,7 +17,7 @@
<i class="fa fa-fw o_button_icon fa-cloud"></i>
<div class="o_button_text">
<span class="cloud-text">0</span><br/>
To Assign
<span class="text-hidden-xs">To Assign</span>
</div>
</button>
</div>

View File

@@ -7,6 +7,8 @@
<link rel="stylesheet" href="/hotel_calendar_channel_connector/static/src/css/view.css" />
<script type="text/javascript" src="/hotel_calendar_channel_connector/static/src/js/views/hotel_calendar_controller.js"></script>
<script type="text/javascript" src="/hotel_calendar_channel_connector/static/src/js/views/hotel_calendar_renderer.js"></script>
<script type="text/javascript" src="/hotel_calendar_channel_connector/static/src/js/views/hotel_calendar_management_controller.js"></script>
<script type="text/javascript" src="/hotel_calendar_channel_connector/static/src/js/views/hotel_calendar_management_renderer.js"></script>
</xpath>
</template>

View File

@@ -19,9 +19,9 @@
},
'data': [
'views/hotel_channel_connector_issue_views.xml',
'views/hotel_room_type_availability_views.xml',
'views/inherited_hotel_reservation_views.xml',
'views/inherited_hotel_room_type_views.xml',
'views/inherited_hotel_room_type_availability_views.xml',
'views/inherited_hotel_folio_views.xml',
'views/inherited_product_pricelist_views.xml',
'views/inherited_product_pricelist_item_views.xml',

View File

@@ -14,3 +14,4 @@ from . import inherited_hotel_folio
from . import inherited_res_partner
from . import channel_ota_info
from . import hotel_channel_connector_issue
from . import inherited_hotel_board_service_room_type

View File

@@ -1,4 +1,4 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2018-2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import timedelta
@@ -10,108 +10,71 @@ from odoo.addons.component.core import Component
from odoo.addons.component_event import skip_if
class HotelRoomTypeAvailability(models.Model):
_name = 'hotel.room.type.availability'
_inherit = 'mail.thread'
@api.model
def _default_max_avail(self):
if self.room_type_id:
return self.room_type_id.total_rooms_count
return -1
@api.model
def _default_quota(self):
room_type_id = self._context.get('room_type_id')
if room_type_id:
room_type_id = self.env['hotel.room_type'].browse(room_type_id)
return room_type_id.default_quota if room_type_id else -1
return -1
room_type_id = fields.Many2one('hotel.room.type', 'Room Type',
required=True, track_visibility='always',
ondelete='cascade')
date = fields.Date('Date', required=True, track_visibility='always')
max_avail = fields.Integer("Max. Avail", default=-1, readonly=True)
quota = fields.Integer("Quota", default=_default_quota)
channel_bind_ids = fields.One2many(
comodel_name='channel.hotel.room.type.availability',
inverse_name='odoo_id',
string='Hotel Room Type Availability Connector Bindings')
no_ota = fields.Boolean('No OTA', default=False)
booked = fields.Boolean('Booked', default=False, readonly=True)
_sql_constraints = [
('room_type_registry_unique',
'unique(room_type_id, date)',
'Only can exists one availability in the same day for the same room \
type!')
]
@api.constrains('max_avail', 'quota')
def _check_max_avail_quota(self):
for record in self:
if record.max_avail != -1 and record.max_avail > record.quota:
raise ValidationError(_("Invalid Max Avail!"))
if record.quota != -1 and record.quota > record.room_type_id.total_rooms_count:
raise ValidationError(_("Invalid Quota!"))
@api.onchange('room_type_id')
def onchange_room_type_id(self):
if self.room_type_id:
self.quota = self.room_type_id.default_quota
class ChannelHotelRoomTypeAvailability(models.Model):
_name = 'channel.hotel.room.type.availability'
_inherit = 'channel.binding'
_inherits = {'hotel.room.type.availability': 'odoo_id'}
_description = 'Channel Availability'
@api.model
def _default_channel_max_avail(self):
if self.odoo_id.room_type_id:
return self.odoo_id.room_type_id.total_rooms_count
return -1
avail = fields.Integer("Avail", default=0, readonly=True)
odoo_id = fields.Many2one(comodel_name='hotel.room.type.availability',
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')
@api.constrains('channel_max_avail')
def _check_channel_max_avail(self):
for record in self:
if record.channel_max_avail > record.odoo_id.room_type_id.total_rooms_count:
raise ValidationError(_("max avail for channel can't be high \
than total rooms \
count: %d") % record.odoo_id.room_type_id.total_rooms_count)
@api.model
def refresh_availability(self, checkin, checkout, room_id):
date_start = fields.Date.from_string(checkin)
date_end = fields.Date.from_string(checkout)
# Not count end day of the reservation
date_diff = (date_end - date_start).days
channel_room_type_obj = self.env['channel.hotel.room.type']
channel_room_type_avail_obj = self.env['hotel.room.type.availability']
room_type_binds = channel_room_type_obj.search([
('backend_id', '=', self.backend_id.id),
('room_ids', '=', room_id),
])
for room_type_bind in room_type_binds:
if room_type_bind.external_id:
for i in range(0, date_diff):
ndate_dt = date_start + timedelta(days=i)
ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
avail = len(channel_room_type_obj.odoo_id.check_availability_room_type(
ndate_str,
ndate_str,
room_type_id=room_type_bind.odoo.id))
max_avail = room_type_bind.total_rooms_count
room_type_avail_id = channel_room_type_avail_obj.search([
('room_type_id', '=', room_type_bind.odoo.id),
('date', '=', ndate_str)], limit=1)
if room_type_avail_id and room_type_avail_id.channel_max_avail >= 0:
max_avail = room_type_avail_id.channel_max_avail
avail = max(
min(avail, room_type_bind.total_rooms_count, max_avail), 0)
if room_type_avail_id:
room_type_avail_id.write({'avail': avail})
else:
channel_room_type_avail_obj.create({
'room_type_id': room_type_bind.odoo.id,
'date': ndate_str,
'avail': avail,
})
@job(default_channel='root.channel')
@api.model
def import_availability(self, backend, dfrom, dto):
with backend.work_on(self._name) as work:
importer = work.component(usage='hotel.room.type.availability.importer')
return importer.import_availability_values(dfrom, dto)
@job(default_channel='root.channel')
@api.model
def push_availability(self, backend):
with backend.work_on(self._name) as work:
exporter = work.component(usage='hotel.room.type.availability.exporter')
return exporter.push_availability()
class HotelRoomTypeAvailability(models.Model):
_inherit = 'hotel.room.type.availability'
channel_bind_ids = fields.One2many(
comodel_name='channel.hotel.room.type.availability',
inverse_name='odoo_id',
string='Hotel Room Type Availability Connector Bindings')
no_ota = fields.Boolean('No OTA', default=False)
booked = fields.Boolean('Booked', default=False, readonly=True)
def _prepare_notif_values(self, record):
vals = super(HotelRoomTypeAvailability, self)._prepare_notif_values(record)
vals.update({
'no_ota': record.no_ota,
})
return vals
channel_pushed = fields.Boolean("Channel Pushed", readonly=True,
default=False)
@api.constrains('avail')
def _check_avail(self):
@@ -126,8 +89,8 @@ class HotelRoomTypeAvailability(models.Model):
if record.avail > max_avail:
issue_obj.sudo().create({
'section': 'avail',
'internal_message': _(r"The new availability can't be greater than \
the max. availability \
'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),
@@ -136,12 +99,74 @@ class HotelRoomTypeAvailability(models.Model):
})
# Auto-Fix channel availability
self._event('on_fix_channel_availability').notify(record)
return super(HotelRoomTypeAvailability, self)._check_avail()
@api.onchange('room_type_id')
def onchange_room_type_id(self):
if self.room_type_id:
self.channel_max_avail = self.room_type_id.total_rooms_count
@api.model
def refresh_availability(self, checkin, checkout, backend_id, room_id=False, room_type_id=False):
date_start = fields.Date.from_string(checkin)
date_end = fields.Date.from_string(checkout)
# Not count end day of the reservation
date_diff = (date_end - date_start).days
channel_room_type_obj = self.env['channel.hotel.room.type']
channel_room_type_avail_obj = self.env['hotel.room.type.availability']
if room_type_id:
room_type_bind = channel_room_type_obj.browse(room_type_id)
else:
domain = [('backend_id', '=', backend_id)]
if room_id:
domain.append(('room_ids', 'in', [room_id]))
room_type_bind = channel_room_type_obj.search(domain, limit=1)
if room_type_bind and room_type_bind.external_id:
for i in range(0, date_diff):
ndate_dt = date_start + timedelta(days=i)
ndate_str = ndate_dt.strftime(DEFAULT_SERVER_DATE_FORMAT)
to_eval = []
cavail = len(channel_room_type_obj.odoo_id.check_availability_room_type(
ndate_str,
ndate_str,
room_type_id=room_type_bind.odoo.id))
to_eval.append(cavail)
to_eval.append(room_type_bind.total_rooms_count)
room_type_avail_id = channel_room_type_avail_obj.search([
('room_type_id', '=', room_type_bind.odoo.id),
('date', '=', ndate_str)], limit=1)
if room_type_avail_id:
if room_type_avail_id.channel_max_avail >= 0:
to_eval.append(
room_type_avail_id.channel_max_avail)
if room_type_avail_id.quota >= 0:
to_eval.append(room_type_avail_id.quota)
if room_type_avail_id.max_avail >= 0:
to_eval.append(room_type_avail_id.max_avail)
avail = max(min(to_eval), 0)
if room_type_avail_id \
and avail != room_type_avail_id.avail:
room_type_avail_id.write({'avail': avail})
else:
channel_room_type_avail_obj.create({
'room_type_id': room_type_bind.odoo.id,
'date': ndate_str,
'avail': avail,
})
@job(default_channel='root.channel')
@api.model
def import_availability(self, backend, dfrom, dto):
with backend.work_on(self._name) as work:
importer = work.component(
usage='hotel.room.type.availability.importer')
return importer.import_availability_values(dfrom, dto)
@job(default_channel='root.channel')
@api.model
def push_availability(self, backend):
with backend.work_on(self._name) as work:
exporter = work.component(
usage='hotel.room.type.availability.exporter')
return exporter.push_availability()
class BindingHotelRoomTypeAvailabilityListener(Component):
_name = 'binding.hotel.room.type.listener'
@@ -150,8 +175,15 @@ class BindingHotelRoomTypeAvailabilityListener(Component):
@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})
fields_to_check = ('quota', 'max_avail')
fields_checked = [elm for elm in fields_to_check if elm in fields]
if any(fields_checked) and any(record.channel_bind_ids):
for binding in record.channel_bind_ids:
binding.refresh_availability(
record.checkin,
record.checkout,
binding.backend_id.id,
room_type_id=record.room_type_id.id)
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_record_create(self, record, fields=None):
@@ -165,11 +197,17 @@ class BindingHotelRoomTypeAvailabilityListener(Component):
('backend_id', '=', backend.id),
])
if not avail_bind:
channel_room_type_avail_obj.create({
avail_bind = channel_room_type_avail_obj.create({
'odoo_id': record.id,
'channel_pushed': False,
'backend_id': backend.id,
})
avail_bind.refresh_availability(
record.checkin,
record.checkout,
backend.id,
room_type_id=record.room_type_id.id)
class ChannelBindingHotelRoomTypeAvailabilityListener(Component):
_name = 'channel.binding.hotel.room.type.availability.listener'
@@ -185,4 +223,10 @@ class ChannelBindingHotelRoomTypeAvailabilityListener(Component):
@skip_if(lambda self, record, **kwargs: self.no_connector_export(record))
def on_fix_channel_availability(self, record, fields=None):
record.update_availability()
if any(record.channel_bind_ids):
for binding in record.channel_bind_ids:
record.refresh_availability(
record.checkin,
record.checkout,
binding.backend_id.id,
room_type_id=record.room_type_id.id)

View File

@@ -0,0 +1,11 @@
# Copyright 2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class HotelBoardServiceRoomType(models.Model):
_inherit = 'hotel.board.service.room.type'
channel_service = fields.Selection([], string='Channel Board Service')

View File

@@ -11,11 +11,10 @@
<field name="backend_id" attrs="{'visible': [('id','=', False)]}" />
</group>
<group>
<field name="no_ota" />
<field name="avail" />
<field name="booked" />
</group>
<group>
<field name="channel_max_avail" />
<field name="channel_pushed" />
</group>
</form>
@@ -27,7 +26,8 @@
<field name="model">channel.hotel.room.type.availability</field>
<field name="arch" type="xml">
<tree string="Hotel Channel Room Availability">
<field name="backend_id"/>
<field name="backend_id" />
<field name="avail" />
</tree>
</field>
</record>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0"?>
<odoo>
<!-- FORM availability -->
<record id="hotel_room_type_availability_view_form" model="ir.ui.view">
<field name="name">hotel.room.type.availability.form</field>
<field name="model">hotel.room.type.availability</field>
<field name="arch" type="xml">
<form string="Availability">
<sheet>
<group>
<!-- <h1><field name="room_type_id" required="1"/></h1> -->
<h1><field name="room_type_id" required="1"/></h1>
</group>
<group>
<field name="date" required="1"/>
<field name="quota"/>
<field name="max_avail"/>
</group>
<notebook>
<page name="connector" string="Channel Connector">
<group string="Hotel Channel Bindings">
<field name="channel_bind_ids" nolabel="1">
<tree>
<field name="backend_id"/>
</tree>
</field>
</group>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<!-- TREE restriction -->
<record id="hotel_room_type_availability_view_tree" model="ir.ui.view">
<field name="name">hotel.room.type.availability.tree</field>
<field name="model">hotel.room.type.availability</field>
<field name="arch" type="xml">
<tree string="Restrictions">
<!-- <field name="room_type_id" required="1"/> -->
<field name="room_type_id" required="1"/>
<field name="date" required="1"/>
<field name="quota"/>
<field name="max_avail"/>
</tree>
</field>
</record>
<!-- Action of reservation restriction -->
<record model="ir.actions.act_window" id="hotel_room_type_availability_action">
<field name="name">Virtual Room Availability</field>
<field name="res_model">hotel.room.type.availability</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
</record>
</odoo>

View File

@@ -9,8 +9,7 @@
<field name="has_channel_reservations" invisible="True" />
<field name="customer_notes" readonly="True" attrs="{'invisible':[('has_channel_reservations', '=', False)]}"/>
</xpath>
<xpath expr="//notebook/page/field[@name='room_lines']/tree/field[@name='checkout']" position="after">
<!-- field name="origin_sale"/-->
<!--xpath expr="//notebook/page/field[@name='room_lines']/tree/field[@name='checkout']" position="after">
<field name="is_from_ota" invisible="1"/>
</xpath>
<xpath expr="//notebook/page/field[@name='room_lines']/tree/field[@name='checkout']" position="attributes">
@@ -20,7 +19,6 @@
<attribute name="attrs">{'readonly': [('is_from_ota', '!=', False)]}</attribute>
</xpath>
<xpath expr="//notebook/page/field[@name='room_lines']/form/sheet/header/field[@name='folio_id']" position="after">
<!-- field name="origin_sale" invisible="1"/-->
<field name="is_from_ota" invisible="1"/>
</xpath>
<xpath expr="//notebook/page/field[@name='room_lines']/form/sheet/h3/field[@name='checkout']" position="attributes">
@@ -28,7 +26,7 @@
</xpath>
<xpath expr="//notebook/page/field[@name='room_lines']/form/sheet/h3/field[@name='checkin']" position="attributes">
<attribute name="attrs">{'readonly': [('is_from_ota', '!=', False)]}</attribute>
</xpath>
</xpath-->
</field>
</record>

View File

@@ -1,37 +0,0 @@
<?xml version="1.0"?>
<odoo>
<!-- FORM availability -->
<record id="room_type_availability_view_form" model="ir.ui.view">
<field name="model">hotel.room.type.availability</field>
<field name="inherit_id" ref="hotel.room_type_availability_view_form" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page name="connector" string="Channel Connector">
<group string="Hotel Channel Bindings">
<field name="channel_bind_ids" nolabel="1">
<tree>
<field name="backend_id"/>
</tree>
</field>
</group>
</page>
</notebook>
</xpath>
</field>
</record>
<!-- TREE restriction -->
<!--record id="room_type_availability_view_tree" model="ir.ui.view">
<field name="model">hotel.room.type.availability</field>
<field name="inherit_id" ref="hotel.room_type_availability_view_tree" />
<field name="type">tree</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='avail']" position="after">
<field name="channel_max_avail" />
</xpath>
</field>
</record-->
</odoo>

View File

@@ -5,6 +5,10 @@
<field name="model">hotel.room.type</field>
<field name="inherit_id" ref="hotel.hotel_room_type_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='board_service_room_type_ids']/tree" position="inside">
<field name="channel_service" />
</xpath>
<xpath expr="//sheet" position="inside">
<notebook>
<page name="connector" string="Channel Connector">

View File

@@ -1,28 +1,76 @@
# Copyright 2018 Alexandre Díaz <dev@redneboa.es>
# Copyright 2018-2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from datetime import datetime, timedelta
from odoo.exceptions import ValidationError
from odoo import fields, models, api
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
class MassiveChangesWizard(models.TransientModel):
_inherit = 'hotel.wizard.massive.changes'
section = fields.Selection(selection_add=[
('avail', 'Availability'),
])
# Availability fields
change_quota = fields.Boolean(default=False)
quota = fields.Integer('Quota', default=-1)
change_max_avail = fields.Boolean(default=False)
max_avail = fields.Integer('Max. Avail.', default=-1)
change_no_ota = fields.Boolean(default=False)
no_ota = fields.Boolean('No OTA', default=False)
@api.model
def _get_availability_values(self, ndate, room_type, record):
vals = super(MassiveChangesWizard, self)._get_availability_values(
ndate, room_type, record)
vals.update({
'channel_max_avail': vals['avail'],
'no_ota': record.no_ota,
})
vals = {}
if record.change_quota:
vals.update({
'quota': record.quota,
})
if record.change_no_ota:
vals.update({
'no_ota': record.no_ota,
})
if record.change_max_avail:
vals.update({
'max_avail': record.max_avail,
})
return vals
@api.model
def _save_availability(self, ndate, room_types, record):
hotel_room_type_avail_obj = self.env['hotel.room.type.availability']
domain = [('date', '=', ndate.strftime(DEFAULT_SERVER_DATE_FORMAT))]
for room_type in room_types:
vals = self._get_availability_values(ndate, room_type, record)
if not any(vals):
continue
room_types_avail = hotel_room_type_avail_obj.search(
domain+[('room_type_id', '=', room_type.id)]
)
if any(room_types_avail):
# Mail module want a singleton
for vr_avail in room_types_avail:
vr_avail.write(vals)
else:
vals.update({
'date': ndate.strftime(DEFAULT_SERVER_DATE_FORMAT),
'room_type_id': room_type.id
})
hotel_room_type_avail_obj.with_context({
'mail_create_nosubscribe': True,
}).create(vals)
@api.multi
def massive_change(self):
res = super(MassiveChangesWizard, self).massive_change()
self.env['channel.backend'].push_changes()
self.env['channel.backend'].cron_push_changes()
return res
@api.model
def _save(self, ndate, room_types, record):
super(MassiveChangesWizard, self)._save(ndate, room_types, record)
if record.section == 'avail':
self._save_availability(ndate, room_types, record)

View File

@@ -5,11 +5,30 @@
<field name="model">hotel.wizard.massive.changes</field>
<field name="inherit_id" ref="hotel.view_hotel_massive_changes_wizard" />
<field name="arch" type="xml">
<xpath expr="//table[hasclass('oe_form_group')][1]/tbody" position="inside">
<tr class="oe_form_group_row">
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_no_ota" /> <strong> No OTA</strong></td>
<td class="oe_form_group_cell"><field name="no_ota" attrs="{'readonly':[('change_no_ota', '=', False)]}" /></td>
</tr>
<xpath expr="//form/group[last()]" position="after">
<!-- Availability Fields -->
<group col="3" colspan="3" attrs="{'invisible':[('section', '!=', 'avail')]}">
<table class="oe_form_group">
<thead>
<th width="12%"></th>
<th></th>
</thead>
<tbody>
<tr class="oe_form_group_row">
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_max_avail" /> <strong> Max. Avail.</strong></td>
<td class="oe_form_group_cell" colspan="3"><field name="max_avail" attrs="{'readonly':[('change_max_avail', '=', False)]}" /></td>
</tr>
<tr class="oe_form_group_row">
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_quota" /> <strong> Quota</strong></td>
<td class="oe_form_group_cell" colspan="3"><field name="quota" attrs="{'readonly':[('change_quota', '=', False)]}" /></td>
</tr>
<tr class="oe_form_group_row">
<td class="oe_form_group_cell oe_form_group_cell_label"><field name="change_no_ota" /> <strong> No OTA</strong></td>
<td class="oe_form_group_cell"><field name="no_ota" attrs="{'readonly':[('change_no_ota', '=', False)]}" /></td>
</tr>
</tbody>
</table>
</group>
</xpath>
</field>
</record>

View File

@@ -12,3 +12,4 @@ from . import hotel_reservation
from . import inherited_hotel_folio
from . import inherited_hotel_room_type_class
from . import channel_ota_info
from . import inherited_hotel_board_service_room_type

View File

@@ -28,14 +28,10 @@ class HotelRoomTypeAvailabilityExporter(Component):
lambda x: x.room_type_id.id == room_type.id)
days = []
for channel_room_type_avail in channel_room_type_avails:
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,
'avail': channel_room_type_avail.avail,
'no_ota': channel_room_type_avail.no_ota and 1 or 0,
# 'booked': room_type_avail.booked and 1 or 0,
})

View File

@@ -3,11 +3,10 @@
import logging
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, only_create
from odoo import api, _, fields
from odoo import api, fields, _
_logger = logging.getLogger(__name__)
@@ -64,9 +63,19 @@ class HotelRoomTypeAvailabilityImporter(Component):
('date', '=', room['date'])
], limit=1)
if room_type_avail_bind:
room_type_avail_bind.with_context({
'connector_no_export': True,
}).write(map_record.values())
if room_type_avail_bind.avail != room['avail']:
room_type_avail_bind.with_context({
'connector_no_export': True,
}).write({'channel_pushed': False})
self.create_issue(
section='avail',
dfrom=room['date'], dto=room['date'],
internal_message=_(
"Channel try to change availiability! \
Updating channel values... \
(Odoo: %d -- Channel: %d" % (
room['avail'],
room_type_avail_bind)))
else:
room_type_avail_bind = channel_room_type_avail_obj.with_context({
'connector_no_export': True,

View File

@@ -0,0 +1,17 @@
# Copyright 2019 Alexandre Díaz <dev@redneboa.es>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import models, fields
class HotelBoardServiceRoomType(models.Model):
_inherit = 'hotel.board.service.room.type'
channel_service = fields.Selection(selection_add=[
('nb', 'No Board'),
('fb', 'Full Board'),
('hb', 'Half Board'),
('bb', 'Breakfast Only'),
('ai', 'All Inclusive')
])