diff --git a/pos_pms_link/__init__.py b/pos_pms_link/__init__.py new file mode 100755 index 000000000..0106bf486 --- /dev/null +++ b/pos_pms_link/__init__.py @@ -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 +# +# 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 . +# +############################################################################## + +from . import models diff --git a/pos_pms_link/__manifest__.py b/pos_pms_link/__manifest__.py new file mode 100755 index 000000000..49485b9a5 --- /dev/null +++ b/pos_pms_link/__manifest__.py @@ -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 +# +# 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 . +# +############################################################################## + +{ + '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, +} diff --git a/pos_pms_link/models/__init__.py b/pos_pms_link/models/__init__.py new file mode 100755 index 000000000..3793f1226 --- /dev/null +++ b/pos_pms_link/models/__init__.py @@ -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 +# +# 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 . +# +############################################################################## + +from . import pos_order +from . import pms_service_line +from . import pos_config +from . import pos_payment \ No newline at end of file diff --git a/pos_pms_link/models/pms_service_line.py b/pos_pms_link/models/pms_service_line.py new file mode 100644 index 000000000..26f21a1b7 --- /dev/null +++ b/pos_pms_link/models/pms_service_line.py @@ -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 +# +# 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 . +# +############################################################################## +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", + ) diff --git a/pos_pms_link/models/pos_config.py b/pos_pms_link/models/pos_config.py new file mode 100644 index 000000000..18ea0ff7a --- /dev/null +++ b/pos_pms_link/models/pos_config.py @@ -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 +# +# 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 . +# +############################################################################## + +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') diff --git a/pos_pms_link/models/pos_order.py b/pos_pms_link/models/pos_order.py new file mode 100644 index 000000000..52c657bde --- /dev/null +++ b/pos_pms_link/models/pos_order.py @@ -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 +# +# 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 . +# +############################################################################## +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 + }) diff --git a/pos_pms_link/models/pos_payment.py b/pos_pms_link/models/pos_payment.py new file mode 100644 index 000000000..22ddf2cdc --- /dev/null +++ b/pos_pms_link/models/pos_payment.py @@ -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 +# +# 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 . +# +############################################################################## +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() diff --git a/pos_pms_link/static/src/js/ReservationSelectionButton.js b/pos_pms_link/static/src/js/ReservationSelectionButton.js new file mode 100644 index 000000000..e7e7d01ea --- /dev/null +++ b/pos_pms_link/static/src/js/ReservationSelectionButton.js @@ -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; +}); diff --git a/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js new file mode 100644 index 000000000..81904e2b8 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -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; +}); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js new file mode 100644 index 000000000..969f713df --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationDetailsEdit.js @@ -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; +}); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js new file mode 100644 index 000000000..d85fa7baa --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationLine.js @@ -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; +}); diff --git a/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js new file mode 100644 index 000000000..4b76c2df5 --- /dev/null +++ b/pos_pms_link/static/src/js/Screens/ReservationListScreen/ReservationListScreen.js @@ -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; +}); diff --git a/pos_pms_link/static/src/js/db.js b/pos_pms_link/static/src/js/db.js new file mode 100644 index 000000000..24420e0b6 --- /dev/null +++ b/pos_pms_link/static/src/js/db.js @@ -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 +# +# 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 . +# +############################################################################## +*/ + +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; +}); diff --git a/pos_pms_link/static/src/js/models.js b/pos_pms_link/static/src/js/models.js new file mode 100644 index 000000000..0a65b2076 --- /dev/null +++ b/pos_pms_link/static/src/js/models.js @@ -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 +# +# 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 . +# +############################################################################## +*/ + +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; + }); + }, + }); + +}); diff --git a/pos_pms_link/static/src/xml/ReservationSelectionButton.xml b/pos_pms_link/static/src/xml/ReservationSelectionButton.xml new file mode 100644 index 000000000..091dbb6c9 --- /dev/null +++ b/pos_pms_link/static/src/xml/ReservationSelectionButton.xml @@ -0,0 +1,12 @@ + + + + +
+ + + Reservation # +
+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml b/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml new file mode 100644 index 000000000..bc07e9cb0 --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/PaymentScreen/PaymentScreen.xml @@ -0,0 +1,12 @@ + + + + + +
+
Reservation
+
+
+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml new file mode 100644 index 000000000..242bc876f --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationDetailsEdit.xml @@ -0,0 +1,43 @@ + + + + +
+

+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
ServiceLines
+ +
    + +
  • + - - +
  • +
    +
+
+
+
+

+
+ +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml new file mode 100644 index 000000000..b24bd1b84 --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationLine.xml @@ -0,0 +1,25 @@ + + + + + + + + +
+
+ + + + + + + + + + + + + +
diff --git a/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml new file mode 100644 index 000000000..64823a020 --- /dev/null +++ b/pos_pms_link/static/src/xml/Screens/ReservationListScreen/ReservationListScreen.xml @@ -0,0 +1,69 @@ + + + + +
+
+
+ +
+ Discard + + + +
+
+ + +
+
+
+
+ +
+
+
+ + + + + + + + + + + + + + +
NamePartner nameAdultsChildren
+
+
+
+
+
+
+
+
+ +
diff --git a/pos_pms_link/views/assets_common.xml b/pos_pms_link/views/assets_common.xml new file mode 100644 index 000000000..2b1228fcd --- /dev/null +++ b/pos_pms_link/views/assets_common.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/pos_pms_link/views/pms_service_line.xml b/pos_pms_link/views/pms_service_line.xml new file mode 100644 index 000000000..a4bd8a786 --- /dev/null +++ b/pos_pms_link/views/pms_service_line.xml @@ -0,0 +1,15 @@ + + + + + inherit.pms.service.line.view.tree + pms.service.line + + + + + + + + + diff --git a/pos_pms_link/views/pos_config.xml b/pos_pms_link/views/pos_config.xml new file mode 100644 index 000000000..bb370674d --- /dev/null +++ b/pos_pms_link/views/pos_config.xml @@ -0,0 +1,30 @@ + + + + + + + pos.config.form.view + pos.config + + + +
+
+ +
+
+
+
+
+
+
+ +
diff --git a/pos_pms_link/views/pos_order.xml b/pos_pms_link/views/pos_order.xml new file mode 100644 index 000000000..0dad2cbae --- /dev/null +++ b/pos_pms_link/views/pos_order.xml @@ -0,0 +1,15 @@ + + + + + inherit.view.pos.pos.form + pos.order + + + + + + + + +