[ADD] pos_pms_link

This commit is contained in:
Vicente
2023-02-23 13:30:27 +01:00
committed by Darío Lodeiros
parent 76da065d39
commit 3a4c3645c8
23 changed files with 1297 additions and 0 deletions

21
pos_pms_link/__init__.py Executable file
View File

@@ -0,0 +1,21 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import models

48
pos_pms_link/__manifest__.py Executable file
View File

@@ -0,0 +1,48 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
'name': 'POS PMS link',
'summary': 'Allows to use PMS reservations on the POS interface',
'version': "14.0.1.0.0",
'author': 'Comunitea Servicios Tecnológicos S.L.',
'website': "http://www.comunitea.com",
'license': 'AGPL-3',
"category": "Point of Sale",
'depends': [
'point_of_sale',
'pms',
],
'data': [
'views/assets_common.xml',
'views/pms_service_line.xml',
'views/pos_order.xml',
'views/pos_config.xml',
],
'demo': [],
'qweb': [
'static/src/xml/ReservationSelectionButton.xml',
'static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml',
'static/src/xml/Screens/ReservationListScreen/ReservationLine.xml',
'static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml',
'static/src/xml/Screens/PaymentScreen/PaymentScreen.xml',
],
'installable': True,
}

24
pos_pms_link/models/__init__.py Executable file
View File

@@ -0,0 +1,24 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import pos_order
from . import pms_service_line
from . import pos_config
from . import pos_payment

View File

@@ -0,0 +1,33 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from odoo import fields, models, api, _
from odoo.osv.expression import AND
import pytz
from datetime import timedelta
from odoo.addons.point_of_sale.wizard.pos_box import PosBox
class PMSServiceLine(models.Model):
_inherit = 'pms.service.line'
pos_order_line_ids = fields.One2many(
string="POS lines",
comodel_name="pos.order.line",
inverse_name="pms_service_line_id",
)

View File

@@ -0,0 +1,33 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2023 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import json
from odoo import api, fields, models, _
from odoo.exceptions import Warning, UserError
import logging
_logger = logging.getLogger(__name__)
class PosConfig(models.Model):
_inherit = 'pos.config'
pay_on_reservation = fields.Boolean('Pay on reservation', default=False)
pay_on_reservation_method_id = fields.Many2one('pos.payment.method', string='Pay on reservation method')

View File

@@ -0,0 +1,103 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from odoo import fields, models, api, _
from odoo.osv.expression import AND
import pytz
from datetime import datetime, timedelta
from odoo.addons.point_of_sale.wizard.pos_box import PosBox
from odoo.exceptions import UserError
class PosOrder(models.Model):
_inherit = 'pos.order'
paid_on_reservation = fields.Boolean('Paid on reservation', default=False)
pms_reservation_id = fields.Many2one('pms.reservation', string='PMS reservation')
def _get_fields_for_draft_order(self):
res = super(PosOrder, self)._get_fields_for_draft_order()
res.append('paid_on_reservation')
res.append('pms_reservation_id')
return res
@api.model
def _order_fields(self, ui_order):
order_fields = super(PosOrder, self)._order_fields(ui_order)
order_fields['paid_on_reservation'] = ui_order.get('paid_on_reservation', False)
order_fields['pms_reservation_id'] = ui_order.get('pms_reservation_id', False)
return order_fields
def _get_fields_for_order_line(self):
res = super(PosOrder, self)._get_fields_for_order_line()
res.append('pms_service_line_id')
return res
def _get_order_lines(self, orders):
super(PosOrder, self)._get_order_lines(orders)
for order in orders:
if 'lines' in order:
for line in order['lines']:
line[2]['pms_service_line_id'] = line[2]['pms_service_line_id'][0] if line[2]['pms_service_line_id'] else False
@api.model
def _process_order(self, pos_order, draft, existing_order):
data = pos_order.get('data', False)
if data and data.get("paid_on_reservation", False) and data.get("pms_reservation_id", False):
res = super()._process_order(pos_order, draft, existing_order)
order_id = self.env['pos.order'].browse(res)
order_id.add_order_lines_to_reservation(data.get("pms_reservation_id"))
return res
else:
return super()._process_order(pos_order, draft, existing_order)
def add_order_lines_to_reservation(self, reservation_id):
pms_reservation_id = self.env['pms.reservation'].browse(reservation_id)
if not pms_reservation_id:
raise UserError(_("Reservation does not exists."))
self.lines.filtered(lambda x: not x.pms_service_line_id)._generate_pms_service(pms_reservation_id)
class PosOrderLine(models.Model):
_inherit = 'pos.order.line'
pms_service_line_id = fields.Many2one('pms.service.line', string='PMS Service line')
def _generate_pms_service(self, pms_reservation_id):
for line in self:
vals = {
"product_id": line.product_id.id,
"reservation_id": pms_reservation_id.id,
"is_board_service": False,
"service_line_ids": [
(
0,
False,
{
"date": datetime.now(),
"price_unit": line.price_unit,
"discount": line.discount,
"day_qty": line.qty,
},
)
],
}
service = self.env["pms.service"].create(vals)
line.write({
'pms_service_line_id': service.service_line_ids.id
})

View File

@@ -0,0 +1,32 @@
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2023 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from odoo import api, fields, models, _
class PosPayment(models.Model):
_inherit = "pos.payment"
@api.constrains('payment_method_id')
def _check_payment_method_id(self):
for payment in self:
if payment.session_id.config_id.pay_on_reservation and payment.session_id.config_id.pay_on_reservation_method_id == payment.payment_method_id:
continue
else:
super(PosPayment, payment)._check_payment_method_id()

View File

@@ -0,0 +1,44 @@
odoo.define('pos_pms_link.ReservationSelectionButton', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const ProductScreen = require('point_of_sale.ProductScreen');
const { useListener } = require('web.custom_hooks');
const Registries = require('point_of_sale.Registries');
const { Gui } = require('point_of_sale.Gui');
var core = require('web.core');
var QWeb = core.qweb;
var _t = core._t;
class ReservationSelectionButton extends PosComponent {
constructor() {
super(...arguments);
useListener('click', this.onClick);
}
get currentOrder() {
return this.env.pos.get_order();
}
async onClick() {
const { confirmed, payload: newReservation } = await this.showTempScreen(
'ReservationListScreen',
{ reservation: null }
);
if (confirmed) {
this.currentOrder.add_reservation_services(newReservation);
}
}
}
ReservationSelectionButton.template = 'ReservationSelectionButton';
ProductScreen.addControlButton({
component: ReservationSelectionButton,
condition: function() {
return true;
},
});
Registries.Component.add(ReservationSelectionButton);
return ReservationSelectionButton;
});

View File

@@ -0,0 +1,36 @@
odoo.define('pos_pms_link.PaymentScreen', function (require) {
'use strict';
const PaymentScreen = require('point_of_sale.PaymentScreen');
const Registries = require('point_of_sale.Registries');
const session = require('web.session');
const PosPMSLinkPaymentScreen = (PaymentScreen) =>
class extends PaymentScreen {
async selectReservation() {
const { confirmed, payload: newReservation } = await this.showTempScreen(
'ReservationListScreen',
{ reservation: null }
);
if (confirmed) {
var self = this;
var payment_method = {
'id': self.env.pos.config.pay_on_reservation_method_id[0],
'name': self.env.pos.config.pay_on_reservation_method_id[1],
'is_cash_count': false,
'pos_mercury_config_id': false,
'use_payment_terminal': false,
}
self.trigger('new-payment-line', payment_method);
this.currentOrder.set_paid_on_reservation(true);
this.currentOrder.set_pms_reservation_id(newReservation['id']);
self.validateOrder(false);
}
}
};
Registries.Component.extend(PaymentScreen, PosPMSLinkPaymentScreen);
return PaymentScreen;
});

View File

@@ -0,0 +1,50 @@
odoo.define('pos_pms_link.ReservationDetailsEdit', function(require) {
'use strict';
const { _t } = require('web.core');
const { getDataURLFromFile } = require('web.utils');
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
class ReservationDetailsEdit extends PosComponent {
constructor() {
super(...arguments);
const reservation = this.props.reservation;
}
mounted() {
this.env.bus.on('save-reservation', this, this.saveChanges);
}
willUnmount() {
this.env.bus.off('save-reservation', this);
}
/**
* Save to field `changes` all input changes from the form fields.
*/
captureChange(event) {
this.changes[event.target.name] = event.target.value;
}
saveChanges() {
let processedChanges = {};
for (let [key, value] of Object.entries(this.changes)) {
if (this.intFields.includes(key)) {
processedChanges[key] = parseInt(value) || false;
} else {
processedChanges[key] = value;
}
}
if ((!this.props.reservation.name && !processedChanges.name) ||
processedChanges.name === '' ){
return this.showPopup('ErrorPopup', {
title: _t('A Customer Name Is Required'),
});
}
processedChanges.id = this.props.reservation.id || false;
this.trigger('save-changes', { processedChanges });
}
}
ReservationDetailsEdit.template = 'ReservationDetailsEdit';
Registries.Component.add(ReservationDetailsEdit);
return ReservationDetailsEdit;
});

View File

@@ -0,0 +1,17 @@
odoo.define('pos_pms_link.ReservationLine', function(require) {
'use strict';
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
class ReservationLine extends PosComponent {
get highlight() {
return this.props.reservation !== this.props.selectedReservation ? '' : 'highlight';
}
}
ReservationLine.template = 'ReservationLine';
Registries.Component.add(ReservationLine);
return ReservationLine;
});

View File

@@ -0,0 +1,157 @@
odoo.define('pos_pms_link.ReservationListScreen', function(require) {
'use strict';
const { debounce } = owl.utils;
const PosComponent = require('point_of_sale.PosComponent');
const Registries = require('point_of_sale.Registries');
const { useListener } = require('web.custom_hooks');
const { isRpcError } = require('point_of_sale.utils');
const { useAsyncLockedMethod } = require('point_of_sale.custom_hooks');
/**
* Render this screen using `showTempScreen` to select client.
* When the shown screen is confirmed ('Set Customer' or 'Deselect Customer'
* button is clicked), the call to `showTempScreen` resolves to the
* selected client. E.g.
*
* ```js
* const { confirmed, payload: selectedClient } = await showTempScreen('ClientListScreen');
* if (confirmed) {
* // do something with the selectedClient
* }
* ```
*
* @props client - originally selected client
*/
class ReservationListScreen extends PosComponent {
constructor() {
super(...arguments);
this.lockedSaveChanges = useAsyncLockedMethod(this.saveChanges);
useListener('click-save', () => this.env.bus.trigger('save-customer'));
useListener('click-edit', () => this.editReservation());
useListener('save-changes', this.lockedSaveChanges);
// We are not using useState here because the object
// passed to useState converts the object and its contents
// to Observer proxy. Not sure of the side-effects of making
// a persistent object, such as pos, into owl.Observer. But it
// is better to be safe.
this.state = {
query: null,
selectedReservation: this.props.reservation,
detailIsShown: false,
isEditMode: false,
editModeProps: {
reservation: {}
},
};
this.updateReservationList = debounce(this.updateReservationList, 70);
}
// Lifecycle hooks
back() {
if(this.state.detailIsShown) {
this.state.detailIsShown = false;
this.render();
} else {
this.props.resolve({ confirmed: false, payload: false });
this.trigger('close-temp-screen');
}
}
confirm() {
this.props.resolve({ confirmed: true, payload: this.state.selectedReservation });
this.trigger('close-temp-screen');
}
// Getters
get currentOrder() {
return this.env.pos.get_order();
}
get reservations() {
if (this.state.query && this.state.query.trim() !== '') {
return this.env.pos.db.search_reservation(this.state.query.trim());
} else {
return this.env.pos.db.get_reservations_sorted(1000);
}
}
get isNextButtonVisible() {
return this.state.selectedReservation ? true : false;
}
/**
* Returns the text and command of the next button.
* The command field is used by the clickNext call.
*/
get nextButton() {
if (!this.props.reservation) {
return { command: 'set', text: this.env._t('Set Reservation') };
} else if (this.props.reservation && this.props.reservation === this.state.selectedReservation) {
return { command: 'deselect', text: this.env._t('Deselect Reservation') };
} else {
return { command: 'set', text: this.env._t('Change Reservation') };
}
}
// Methods
// We declare this event handler as a debounce function in
// order to lower its trigger rate.
updateReservationList(event) {
this.state.query = event.target.value;
const reservations = this.reservations;
if (event.code === 'Enter' && reservations.length === 1) {
this.state.selectedReservation = reservations[0];
this.clickNext();
} else {
this.render();
}
}
clickReservation(event) {
let reservation = event.detail.reservation;
if (this.state.selectedReservation === reservation) {
this.state.selectedCReservation = null;
} else {
this.state.selectedReservation = reservation;
}
this.render();
}
editReservation() {
this.state.editModeProps = {
reservation: this.state.selectedReservation,
};
this.state.detailIsShown = true;
this.render();
}
clickNext() {
this.state.selectedReservation = this.nextButton.command === 'set' ? this.state.selectedReservation : null;
this.confirm();
}
activateEditMode(event) {
const { isNewReservation } = event.detail;
this.state.isEditMode = true;
this.state.detailIsShown = true;
this.state.isNewReservation = isNewReservation;
if (!isNewReservation) {
this.state.editModeProps = {
reservation: this.state.selectedReservation,
};
}
this.render();
}
deactivateEditMode() {
this.state.isEditMode = false;
this.state.editModeProps = {
reservation: {},
};
this.render();
}
cancelEdit() {
this.deactivateEditMode();
}
}
ReservationListScreen.template = 'ReservationListScreen';
Registries.Component.add(ReservationListScreen);
return ReservationListScreen;
});

View File

@@ -0,0 +1,106 @@
/*
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
*/
odoo.define("pos_pms_link.db", function (require) {
"use strict";
var PosDB = require("point_of_sale.DB");
var utils = require("web.utils");
PosDB.include({
init: function (options) {
this._super(options);
this.reservation_sorted = [];
this.reservation_by_id = {};
this.reservation_search_string = "";
this.reservation_id = null;
},
get_reservation_by_id: function(id){
return this.reservation_by_id[id];
},
get_reservations_sorted: function(max_count){
max_count = max_count ? Math.min(this.reservation_sorted.length, max_count) : this.reservation_sorted.length;
var reservations = [];
for (var i = 0; i < max_count; i++) {
reservations.push(this.reservation_by_id[this.reservation_sorted[i]]);
}
return reservations;
},
search_reservation: function(query){
try {
query = query.replace(/[\[\]\(\)\+\*\?\.\-\!\&\^\$\|\~\_\{\}\:\,\\\/]/g,'.');
query = query.replace(/ /g,'.+');
var re = RegExp("([0-9]+):.*?"+utils.unaccent(query),"gi");
}catch(e){
return [];
}
var results = [];
for(var i = 0; i < this.limit; i++){
var r = re.exec(this.reservation_search_string);
if(r){
var id = Number(r[1]);
results.push(this.get_reservation_by_id(id));
}else{
break;
}
}
return results;
},
_reservation_search_string: function(reservation){
var str = reservation.name || '';
str = '' + reservation.id + ':' + str.replace(':', '').replace(/\n/g, ' ') + '\n';
return str;
},
add_reservations: function(reservations){
var updated_count = 0;
var reservation;
for(var i = 0, len = reservations.length; i < len; i++){
reservation = reservations[i];
if (!this.reservation_by_id[reservation.id]) {
this.reservation_sorted.push(reservation.id);
}
this.reservation_by_id[reservation.id] = reservation;
updated_count += 1;
}
if (updated_count) {
this.reservation_search_string = "";
this.reservation_by_barcode = {};
for (var id in this.reservation_by_id) {
reservation = this.reservation_by_id[id];
if(reservation.barcode){
this.reservation_by_barcode[reservation.barcode] = reservation;
}
this.reservation_search_string += this._reservation_search_string(reservation);
}
this.reservation_search_string = utils.unaccent(this.reservation_search_string);
}
return updated_count;
},
});
return PosDB;
});

View File

@@ -0,0 +1,355 @@
/*
##############################################################################
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
# Copyright (C) 2022 Comunitea Servicios Tecnológicos S.L. All Rights Reserved
# Vicente Ángel Gutiérrez <vicente@comunitea.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
*/
odoo.define('pos_pms_link.models', function (require) {
"use strict";
var models = require('point_of_sale.models');
var utils = require('web.utils');
var round_di = utils.round_decimals;
var core = require('web.core');
const { Gui } = require('point_of_sale.Gui');
var QWeb = core.qweb;
var _t = core._t;
var _super_order = models.Order.prototype;
models.Order = models.Order.extend({
initialize: function(attr, options) {
_super_order.initialize.apply(this,arguments);
this.paid_on_reservation = this.paid_on_reservation || null;
this.pms_reservation_id = this.pms_reservation_id || null;
},
get_paid_on_reservation: function() {
var paid_on_reservation = this.paid_on_reservation;
return paid_on_reservation;
},
set_paid_on_reservation: function(value) {
this.paid_on_reservation = value;
this.trigger('change', this);
},
get_pms_reservation_id: function() {
var pms_reservation_id = this.pms_reservation_id;
return pms_reservation_id;
},
set_pms_reservation_id: function(value) {
this.pms_reservation_id = value;
this.trigger('change', this);
},
export_as_JSON: function() {
var json = _super_order.export_as_JSON.apply(this,arguments);
json.paid_on_reservation = this.paid_on_reservation;
json.pms_reservation_id = this.pms_reservation_id;
return json;
},
init_from_JSON: function(json) {
_super_order.init_from_JSON.apply(this,arguments);
this.paid_on_reservation = json.paid_on_reservation;
this.pms_reservation_id = json.pms_reservation_id;
},
apply_ms_data: function(data) {
if (typeof data.paid_on_reservation !== "undefined") {
this.set_paid_on_reservation(data.paid_on_reservation);
}
if (typeof data.pms_reservation_id !== "undefined") {
this.set_pms_reservation_id(data.pms_reservation_id);
}
this.trigger('change', this);
},
add_reservation_services: function(reservation) {
var self = this;
var d = new Date();
var month = d.getMonth()+1;
var day = d.getDate();
var current_date = d.getFullYear() + '-' +
(month<10 ? '0' : '') + month + '-' +
(day<10 ? '0' : '') + day;
var service_line_ids = reservation.service_ids.map(x => x.service_line_ids) || false;
var today_service_lines = []
_.each(service_line_ids, function(service_array){
today_service_lines.push(service_array.find(x => x.date === current_date));
});
_.each(today_service_lines, function(service_line_id){
if (service_line_id){
var qty = service_line_id.day_qty
if (service_line_id.pos_order_line_ids.length > 0) {
_.each(service_line_id.pos_order_line_ids, function(order_line_id){
qty -= order_line_id.qty;
});
}
if (qty > 0) {
var options = {
'quantity': qty,
'pms_service_line_id': service_line_id.id,
'price': 0.0,
};
var service_product = self.pos.db.get_product_by_id(service_line_id.product_id[0]);
self.pos.get_order().add_product(service_product, options);
var r_service_line_id = reservation.service_ids.map(x => x.service_line_ids)[0].find(x=>x.id==service_line_id.id);
if (r_service_line_id.pos_order_line_ids.length == 0) {
r_service_line_id.pos_order_line_ids.push({
'id': 0,
'qty': parseInt(qty)
});
} else if (r_service_line_id.pos_order_line_ids.length == 1 && r_service_line_id.pos_order_line_ids[0].id == 0){
r_service_line_id.pos_order_line_ids[0].qty = parseInt(qty);
} else if (r_service_line_id.pos_order_line_ids.length == 1 && r_service_line_id.pos_order_line_ids[0].id != 0){
r_service_line_id.pos_order_line_ids.push({
'id': 0,
'qty': parseInt(qty)
});
} else if (r_service_line_id.pos_order_line_ids.length > 1){
var id_in_lines = false;
_.each(r_service_line_id.pos_order_line_ids, function(pos_line_id){
if(pos_line_id.id == self.id) {
pos_line_id.qty = parseInt(qty);
id_in_lines = true;
}
});
if (id_in_lines == false) {
r_service_line_id.pos_order_line_ids.push({
'id': self.id,
'qty': parseInt(qty)
});
}
}
}
}
});
},
add_product: function(product, options){
_super_order.add_product.apply(this,arguments);
if (options.pms_service_line_id) {
this.selected_orderline.set_pms_service_line_id(options.pms_service_line_id);
}
},
})
var _super_orderline = models.Orderline.prototype;
models.Orderline = models.Orderline.extend({
initialize: function(attr, options) {
_super_orderline.initialize.call(this,attr,options);
this.server_id = this.server_id || null;
this.pms_service_line_id = this.pms_service_line_id || null;
},
get_pms_service_line_id: function() {
var pms_service_line_id = this.pms_service_line_id;
return pms_service_line_id;
},
set_pms_service_line_id: function(value) {
this.pms_service_line_id = value;
this.trigger('change', this);
},
export_as_JSON: function() {
var json = _super_orderline.export_as_JSON.apply(this,arguments);
json.pms_service_line_id = this.pms_service_line_id;
return json;
},
init_from_JSON: function(json) {
_super_orderline.init_from_JSON.apply(this,arguments);
this.pms_service_line_id = json.pms_service_line_id;
this.server_id = json.server_id;
},
apply_ms_data: function(data) {
if (typeof data.pms_service_line_id !== "undefined") {
this.set_pms_service_line_id(data.pms_service_line_id);
}
this.trigger('change', this);
},
set_quantity: function(quantity, keep_price) {
_super_orderline.set_quantity.apply(this, arguments);
var is_real_qty = true;
if (!quantity || quantity == "remove") {
is_real_qty = false;
}
var self = this;
if (self.pms_service_line_id) {
this.pos.reservations.map(function(x) {
_.each(x.service_ids, function(service){
_.each(service.service_line_ids, function(line){
if (line.id == self.pms_service_line_id) {
if (line.pos_order_line_ids.length == 0 && is_real_qty) {
line.pos_order_line_ids.push({
'id': self.server_id || 0,
'qty': parseInt(quantity)
});
} else if (line.pos_order_line_ids.length == 1 && line.pos_order_line_ids[0].id == self.server_id){
if (is_real_qty) {
line.pos_order_line_ids[0].qty = parseInt(quantity);
} else {
line.pos_order_line_ids.pop(line.pos_order_line_ids[0]);
}
} else if (line.pos_order_line_ids.length == 1 && line.pos_order_line_ids[0].id != self.server_id && is_real_qty){
line.pos_order_line_ids.push({
'id': self.server_id || 0,
'qty': parseInt(quantity)
});
} else if (line.pos_order_line_ids.length > 1){
var id_in_lines = false;
_.each(line.pos_order_line_ids, function(pos_line_id){
if(pos_line_id.id == self.server_id) {
if (is_real_qty) {
pos_line_id.qty = parseInt(quantity);
} else {
line.pos_order_line_ids.pop(pos_line_id);
}
id_in_lines = true;
}
});
_.each(line.pos_order_line_ids, function(pos_line_id){
if(pos_line_id.id == 0) {
if (is_real_qty) {
pos_line_id.qty = parseInt(quantity);
} else {
line.pos_order_line_ids.pop(pos_line_id);
}
id_in_lines = true;
}
});
if (id_in_lines == false && is_real_qty) {
line.pos_order_line_ids.push({
'id': self.server_id || 0,
'qty': parseInt(quantity)
});
}
}
}
});
});
})
}
},
});
var _super_posmodel = models.PosModel.prototype;
models.PosModel = models.PosModel.extend({
initialize: function(attr, options) {
_super_posmodel.initialize.apply(this,arguments);
this.reservations = [];
},
});
models.load_models({
model: 'pms.reservation',
fields: ['name', 'id', 'state', 'service_ids', 'partner_name', 'adults', 'children'],
domain: function(self){
/* return [['state', '=', 'onboard']]; */
return [];
},
loaded: function(self, reservations) {
self.reservations = reservations;
self.db.add_reservations(reservations);
}
});
models.load_models({
model: 'pms.service',
fields: ['name', 'id', 'service_line_ids', 'product_id', 'reservation_id'],
domain: function(self){
return [['reservation_id', 'in', self.reservations.map(x => x.id)]];
},
loaded: function (self, services){
self.services = services;
var services = []
_.each(self.reservations, function(reservation){
services = [];
_.each(reservation.service_ids, function(service_id){
services.push(self.services.find(x => x.id === service_id));
});
reservation.service_ids = services;
});
},
});
models.load_models({
model: 'pms.service.line',
fields: ['date', 'service_id', 'id', 'product_id', 'day_qty', 'pos_order_line_ids'],
domain: function(self){
return [['service_id', 'in', self.services.map(x => x.id)]];
},
loaded: function (self, service_lines){
self.service_lines = service_lines;
var service_lines = []
_.each(self.reservations, function(reservation){
_.each(reservation.service_ids, function(service_id){
service_lines = [];
_.each(service_id.service_line_ids, function(line_id){
service_lines.push(self.service_lines.find(x => x.id === line_id));
});
service_id.service_line_ids = service_lines;
});
});
},
});
models.load_models({
model: 'pos.order.line',
fields: ['qty', 'id'],
domain: function(self){
var order_line_ids = [];
_.each(self.service_lines, function(service_line) {
if(service_line.pos_order_line_ids.length > 0) {
_.each(service_line.pos_order_line_ids, function(line_id) {
order_line_ids.push(line_id);
})
}
});
return [['id', 'in', order_line_ids]];
},
loaded: function (self, pos_order_lines){
self.pos_order_lines = pos_order_lines;
_.each(self.service_lines, function(service_line){
var order_lines = []
_.each(service_line.pos_order_line_ids, function(order_line){
order_lines.push(self.pos_order_lines.find(x => x.id === order_line));
});
service_line.pos_order_line_ids = order_lines;
});
},
});
});

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ReservationSelectionButton" owl="1">
<div class="control-button">
<i class="fa fa-sort" />
<span> </span>
<span>Reservation #</span>
</div>
</t>
</templates>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="pos_pms_link.PaymentScreen" t-inherit="point_of_sale.PaymentScreen" t-inherit-mode="extension" owl="1">
<xpath expr="//div[hasclass('paymentmethods')]" position="inside">
<div class="button paymentmethod">
<div class="payment-name" t-if="env.pos.config.pay_on_reservation" t-on-click="selectReservation">Reservation</div>
</div>
</xpath>
</t>
</templates>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ReservationDetailsEdit" owl="1">
<section class="client-details edit">
<h3 class="detail client-name" t-esc="props.reservation.name"/>
<div class="client-details-box clearfix">
<div class="client-details-left">
<span class="detail client-partner-name" t-esc="props.reservation.partner_name"/><br/>
<span class="detail client-aduls" t-esc="props.reservation.adults"/><br/>
<span class="detail client-children" t-esc="props.reservation.children"/><br/>
</div>
<div class="client-details-right">
<table>
<thead>
<tr>
<th>Service</th>
<th>Lines</th>
</tr>
</thead>
<tbody>
<tr>
<t t-foreach="props.reservation.service_ids" t-as="service" t-key="service.id">
<td t-esc="service_value['name']"/>
<td>
<ul>
<t t-foreach="service_value['service_line_ids']" t-as="line_service" t-key="line_service.id">
<li>
<span t-esc="line_service_value['date']"/> - <span t-esc="line_service_value['product_id'][1]"/> - <span t-esc="line_service_value['day_qty']"/>
</li>
</t>
</ul>
</td>
</t>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</t>
</templates>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ReservationLine" owl="1">
<tr t-attf-class="client-line {{highlight}}" t-att-data-id="props.reservation.id"
t-on-click="trigger('click-reservation', {reservation: props.reservation})">
<td>
<t t-esc="props.reservation.name" />
<span t-if="highlight">
<br/><button class="edit-client-button" t-on-click.stop="trigger('click-edit')">VIEW</button>
</span>
</td>
<td>
<t t-esc="props.reservation.partner_name" />
</td>
<td>
<t t-esc="props.reservation.adults" />
</td>
<td>
<t t-esc="props.reservation.children" />
</td>
</tr>
</t>
</templates>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="ReservationListScreen" owl="1">
<div class="clientlist-screen screen" t-on-activate-edit-mode="activateEditMode">
<div class="screen-content">
<div class="top-content">
<div t-if="isNextButtonVisible" t-on-click="clickNext"
class="button next highlight">
<t t-if="!env.isMobile">
<t t-esc="nextButton.text" />
</t>
<t t-else="">
<i t-if="nextButton.command === 'deselect'" class="fa fa-trash"></i>
<i t-if="nextButton.command === 'set'" class="fa fa-check"></i>
</t>
</div>
<div class="button back" t-on-click="back">
<t t-if="!env.isMobile">Discard</t>
<t t-else="">
<i class="fa fa-undo"></i>
</t>
</div>
<div t-if="!state.detailIsShown" class="searchbox-client top-content-center">
<input placeholder="Search Reservations" size="1" t-on-keyup="updateReservationList" />
<span class="search-clear-client"></span>
</div>
</div>
<section class="full-content">
<div class="client-window">
<section class="subwindow collapsed">
<div class="subwindow-container collapsed">
<div t-if="state.detailIsShown" class="client-details-contents subwindow-container-fix">
<ReservationDetailsEdit t-props="state.editModeProps" t-on-cancel-edit="cancelEdit"/>
</div>
</div>
</section>
<section class="subwindow list">
<div class="subwindow-container">
<div t-if="!state.detailIsShown" class="subwindow-container-fix scrollable-y">
<table class="client-list">
<thead>
<tr>
<th>Name</th>
<th>Partner name</th>
<th>Adults</th>
<th>Children</th>
</tr>
</thead>
<tbody class="client-list-contents">
<t t-foreach="reservations" t-as="reservation"
t-key="reservation.id">
<ReservationLine reservation="reservation"
selectedReservation="state.selectedReservation"
detailIsShown="state.detailIsShown"
t-on-click-reservation="clickReservation" />
</t>
</tbody>
</table>
</div>
</div>
</section>
</div>
</section>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets" name="pos_pms_link assets" inherit_id="point_of_sale.assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/pos_pms_link/static/src/js/db.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/models.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/ReservationSelectionButton.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js"></script>
<script type="text/javascript" src="/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pms_service_line_view_tree" model="ir.ui.view">
<field name="name">inherit.pms.service.line.view.tree</field>
<field name="model">pms.service.line</field>
<field name="inherit_id" ref="pms.pms_service_line_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_day_total']" position="after">
<field name="pos_order_line_ids" widget="many2many_tags"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- pos.config -->
<record id="pos_config_view_form" model="ir.ui.view">
<field name="name">pos.config.form.view</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='category_reference']" position="before">
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="pay_on_reservation"/>
</div>
<div class="o_setting_right_pane">
<label for="pay_on_reservation"/>
<div class="text-muted">
Allow pay on reservations
</div>
<div class="content-group mt16" attrs="{'invisible': [('pay_on_reservation', '=', False)]}">
<field name="pay_on_reservation_method_id"/>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_pos_pos_form" model="ir.ui.view">
<field name="name">inherit.view.pos.pos.form</field>
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='products']//field[@name='price_subtotal_incl']" position="after">
<field name="pms_service_line_id"/>
</xpath>
</field>
</record>
</odoo>