mirror of
https://github.com/bringout/oca-ocb-pos.git
synced 2026-04-22 18:42:00 +02:00
Initial commit: Pos packages
This commit is contained in:
commit
95dfb9edb0
1301 changed files with 264148 additions and 0 deletions
|
|
@ -0,0 +1,94 @@
|
|||
odoo.define('pos_restaurant.chrome', function (require) {
|
||||
'use strict';
|
||||
|
||||
const Chrome = require('point_of_sale.Chrome');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const NON_IDLE_EVENTS = 'mousemove mousedown touchstart touchend touchmove click scroll keypress'.split(/\s+/);
|
||||
let IDLE_TIMER_SETTER;
|
||||
|
||||
const PosResChrome = (Chrome) =>
|
||||
class extends Chrome {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async start() {
|
||||
await super.start();
|
||||
if (this.env.pos.config.iface_floorplan) {
|
||||
this._setActivityListeners();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* Do not set `FloorScreen` to the order.
|
||||
*/
|
||||
_setScreenData(name) {
|
||||
if (name === 'FloorScreen') return;
|
||||
super._setScreenData(...arguments);
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* `FloorScreen` is the start screen if there are floors.
|
||||
*/
|
||||
get startScreen() {
|
||||
if (this.env.pos.config.iface_floorplan) {
|
||||
const table = this.env.pos.table;
|
||||
return { name: 'FloorScreen', props: { floor: table ? table.floor : null } };
|
||||
} else {
|
||||
return super.startScreen;
|
||||
}
|
||||
}
|
||||
_setActivityListeners() {
|
||||
IDLE_TIMER_SETTER = this._setIdleTimer.bind(this);
|
||||
for (const event of NON_IDLE_EVENTS) {
|
||||
window.addEventListener(event, IDLE_TIMER_SETTER);
|
||||
}
|
||||
}
|
||||
_setIdleTimer() {
|
||||
clearTimeout(this.idleTimer);
|
||||
if (this._shouldResetIdleTimer()) {
|
||||
this.idleTimer = setTimeout(() => {
|
||||
this._actionAfterIdle();
|
||||
}, 60000);
|
||||
}
|
||||
}
|
||||
_actionAfterIdle() {
|
||||
if (this.tempScreen.isShown) {
|
||||
this.trigger('close-temp-screen');
|
||||
}
|
||||
const table = this.env.pos.table;
|
||||
const order = this.env.pos.get_order();
|
||||
if (order && order.get_screen_data().name === 'ReceiptScreen') {
|
||||
// When the order is finalized, we can safely remove it from the memory
|
||||
// We check that it's in ReceiptScreen because we want to keep the order if it's in a tipping state
|
||||
this.env.pos.removeOrder(order);
|
||||
}
|
||||
this.showScreen('FloorScreen', { floor: table ? table.floor : null });
|
||||
}
|
||||
_shouldResetIdleTimer() {
|
||||
const stayPaymentScreen = this.mainScreen.name === 'PaymentScreen' && this.env.pos.get_order().paymentlines.length > 0;
|
||||
return this.env.pos.config.iface_floorplan && !stayPaymentScreen && this.mainScreen.name !== 'FloorScreen';
|
||||
}
|
||||
__showScreen() {
|
||||
super.__showScreen(...arguments);
|
||||
this._setIdleTimer();
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* Before closing pos, we remove the event listeners set on window
|
||||
* for detecting activities outside FloorScreen.
|
||||
*/
|
||||
async _closePos() {
|
||||
if (IDLE_TIMER_SETTER) {
|
||||
for (const event of NON_IDLE_EVENTS) {
|
||||
window.removeEventListener(event, IDLE_TIMER_SETTER);
|
||||
}
|
||||
}
|
||||
await super._closePos();
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(Chrome, PosResChrome);
|
||||
|
||||
return Chrome;
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
odoo.define('pos_restaurant.BackToFloorButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* Props: {
|
||||
* onClick: callback
|
||||
* }
|
||||
*/
|
||||
class BackToFloorButton extends PosComponent {
|
||||
get table() {
|
||||
return this.env.pos.table;
|
||||
}
|
||||
get floor() {
|
||||
return this.table ? this.table.floor : null;
|
||||
}
|
||||
get hasTable() {
|
||||
return this.table != null;
|
||||
}
|
||||
backToFloorScreen() {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick();
|
||||
}
|
||||
this.showScreen('FloorScreen', { floor: this.floor });
|
||||
}
|
||||
}
|
||||
BackToFloorButton.template = 'BackToFloorButton';
|
||||
|
||||
Registries.Component.add(BackToFloorButton);
|
||||
|
||||
return BackToFloorButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
odoo.define('pos_restaurant.TicketButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const TicketButton = require('point_of_sale.TicketButton');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { isConnectionError } = require('point_of_sale.utils');
|
||||
|
||||
const PosResTicketButton = (TicketButton) =>
|
||||
class extends TicketButton {
|
||||
async onClick() {
|
||||
if (this.env.pos.config.iface_floorplan && !this.props.isTicketScreenShown && !this.env.pos.table) {
|
||||
try {
|
||||
this.env.pos.setLoadingOrderState(true);
|
||||
await this.env.pos._syncAllOrdersFromServer();
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Due to a connection error, the orders are not synchronized.'),
|
||||
});
|
||||
} else {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Unknown error'),
|
||||
body: error.message,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
this.env.pos.setLoadingOrderState(false);
|
||||
this.showScreen('TicketScreen');
|
||||
}
|
||||
} else {
|
||||
super.onClick();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If no table is set to pos, which means the current main screen
|
||||
* is floor screen, then the order count should be based on all the orders.
|
||||
*/
|
||||
get count() {
|
||||
if (!this.env.pos || !this.env.pos.config) return 0;
|
||||
if (this.env.pos.config.iface_floorplan && this.env.pos.table) {
|
||||
return this.env.pos.getTableOrders(this.env.pos.table.id).length;
|
||||
} else {
|
||||
return super.count;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(TicketButton, PosResTicketButton);
|
||||
|
||||
return TicketButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
odoo.define('pos_restaurant.Resizeable', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { onMounted, useExternalListener } = owl;
|
||||
|
||||
class Resizeable extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useExternalListener(document, 'mousemove', this.resizeN);
|
||||
useExternalListener(document, 'mouseup', this.endResizeN);
|
||||
useListener('mousedown', '.resize-handle-n', this.startResizeN);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeS);
|
||||
useExternalListener(document, 'mouseup', this.endResizeS);
|
||||
useListener('mousedown', '.resize-handle-s', this.startResizeS);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeW);
|
||||
useExternalListener(document, 'mouseup', this.endResizeW);
|
||||
useListener('mousedown', '.resize-handle-w', this.startResizeW);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeE);
|
||||
useExternalListener(document, 'mouseup', this.endResizeE);
|
||||
useListener('mousedown', '.resize-handle-e', this.startResizeE);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeNW);
|
||||
useExternalListener(document, 'mouseup', this.endResizeNW);
|
||||
useListener('mousedown', '.resize-handle-nw', this.startResizeNW);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeNE);
|
||||
useExternalListener(document, 'mouseup', this.endResizeNE);
|
||||
useListener('mousedown', '.resize-handle-ne', this.startResizeNE);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeSW);
|
||||
useExternalListener(document, 'mouseup', this.endResizeSW);
|
||||
useListener('mousedown', '.resize-handle-sw', this.startResizeSW);
|
||||
|
||||
useExternalListener(document, 'mousemove', this.resizeSE);
|
||||
useExternalListener(document, 'mouseup', this.endResizeSE);
|
||||
useListener('mousedown', '.resize-handle-se', this.startResizeSE);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeN);
|
||||
useExternalListener(document, 'touchend', this.endResizeN);
|
||||
useListener('touchstart', '.resize-handle-n', this.startResizeN);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeS);
|
||||
useExternalListener(document, 'touchend', this.endResizeS);
|
||||
useListener('touchstart', '.resize-handle-s', this.startResizeS);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeW);
|
||||
useExternalListener(document, 'touchend', this.endResizeW);
|
||||
useListener('touchstart', '.resize-handle-w', this.startResizeW);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeE);
|
||||
useExternalListener(document, 'touchend', this.endResizeE);
|
||||
useListener('touchstart', '.resize-handle-e', this.startResizeE);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeNW);
|
||||
useExternalListener(document, 'touchend', this.endResizeNW);
|
||||
useListener('touchstart', '.resize-handle-nw', this.startResizeNW);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeNE);
|
||||
useExternalListener(document, 'touchend', this.endResizeNE);
|
||||
useListener('touchstart', '.resize-handle-ne', this.startResizeNE);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeSW);
|
||||
useExternalListener(document, 'touchend', this.endResizeSW);
|
||||
useListener('touchstart', '.resize-handle-sw', this.startResizeSW);
|
||||
|
||||
useExternalListener(document, 'touchmove', this.resizeSE);
|
||||
useExternalListener(document, 'touchend', this.endResizeSE);
|
||||
useListener('touchstart', '.resize-handle-se', this.startResizeSE);
|
||||
|
||||
this.size = { height: 0, width: 0 };
|
||||
this.loc = { top: 0, left: 0 };
|
||||
this.tempSize = {};
|
||||
|
||||
onMounted(() => {
|
||||
this.limitArea = this.props.limitArea
|
||||
? document.querySelector(this.props.limitArea)
|
||||
: this.el.offsetParent;
|
||||
this.limitAreaBoundingRect = this.limitArea.getBoundingClientRect();
|
||||
if (this.limitArea === this.el.offsetParent) {
|
||||
this.limitLeft = 0;
|
||||
this.limitTop = 0;
|
||||
this.limitRight = this.limitAreaBoundingRect.width;
|
||||
this.limitBottom = this.limitAreaBoundingRect.height;
|
||||
} else {
|
||||
this.limitLeft = -this.el.offsetParent.offsetLeft;
|
||||
this.limitTop = -this.el.offsetParent.offsetTop;
|
||||
this.limitRight =
|
||||
this.limitAreaBoundingRect.width - this.el.offsetParent.offsetLeft;
|
||||
this.limitBottom =
|
||||
this.limitAreaBoundingRect.height - this.el.offsetParent.offsetTop;
|
||||
}
|
||||
this.limitAreaWidth = this.limitAreaBoundingRect.width;
|
||||
this.limitAreaHeight = this.limitAreaBoundingRect.height;
|
||||
});
|
||||
}
|
||||
startResizeN(event) {
|
||||
let realEvent;
|
||||
if (event instanceof CustomEvent) {
|
||||
realEvent = event.detail;
|
||||
} else {
|
||||
realEvent = event;
|
||||
}
|
||||
const { y } = this._getEventLoc(realEvent);
|
||||
this.isResizingN = true;
|
||||
this.startY = y;
|
||||
this.size.height = this.el.offsetHeight;
|
||||
this.loc.top = this.el.offsetTop;
|
||||
event.stopPropagation();
|
||||
}
|
||||
resizeN(event) {
|
||||
if (this.isResizingN) {
|
||||
const { y: newY } = this._getEventLoc(event);
|
||||
let dY = newY - this.startY;
|
||||
if (dY < 0 && Math.abs(dY) > this.loc.top) {
|
||||
dY = -this.loc.top;
|
||||
} else if (dY > 0 && dY > this.size.height) {
|
||||
dY = this.size.height;
|
||||
}
|
||||
this.el.style.height = `${this.size.height - dY}px`;
|
||||
this.el.style.top = `${this.loc.top + dY}px`;
|
||||
}
|
||||
}
|
||||
endResizeN() {
|
||||
if (this.isResizingN && !this.isResizingE && !this.isResizingW && !this.isResizingS) {
|
||||
this.isResizingN = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeS(event) {
|
||||
let realEvent;
|
||||
if (event instanceof CustomEvent) {
|
||||
realEvent = event.detail;
|
||||
} else {
|
||||
realEvent = event;
|
||||
}
|
||||
const { y } = this._getEventLoc(realEvent);
|
||||
this.isResizingS = true;
|
||||
this.startY = y;
|
||||
this.size.height = this.el.offsetHeight;
|
||||
this.loc.top = this.el.offsetTop;
|
||||
event.stopPropagation();
|
||||
}
|
||||
resizeS(event) {
|
||||
if (this.isResizingS) {
|
||||
const { y: newY } = this._getEventLoc(event);
|
||||
let dY = newY - this.startY;
|
||||
if (dY > 0 && dY > this.limitAreaHeight - (this.size.height + this.loc.top)) {
|
||||
dY = this.limitAreaHeight - (this.size.height + this.loc.top);
|
||||
} else if (dY < 0 && Math.abs(dY) > this.size.height) {
|
||||
dY = -this.size.height;
|
||||
}
|
||||
this.el.style.height = `${this.size.height + dY}px`;
|
||||
}
|
||||
}
|
||||
endResizeS() {
|
||||
if (!this.isResizingN && !this.isResizingE && !this.isResizingW && this.isResizingS) {
|
||||
this.isResizingS = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeW(event) {
|
||||
let realEvent;
|
||||
if (event instanceof CustomEvent) {
|
||||
realEvent = event.detail;
|
||||
} else {
|
||||
realEvent = event;
|
||||
}
|
||||
const { x } = this._getEventLoc(realEvent);
|
||||
this.isResizingW = true;
|
||||
this.startX = x;
|
||||
this.size.width = this.el.offsetWidth;
|
||||
this.loc.left = this.el.offsetLeft;
|
||||
event.stopPropagation();
|
||||
}
|
||||
resizeW(event) {
|
||||
if (this.isResizingW) {
|
||||
const { x: newX } = this._getEventLoc(event);
|
||||
let dX = newX - this.startX;
|
||||
if (dX > 0 && dX > this.size.width) {
|
||||
dX = this.size.width;
|
||||
} else if (dX < 0 && Math.abs(dX) > this.loc.left + Math.abs(this.limitLeft)) {
|
||||
dX = -this.loc.left + this.limitLeft;
|
||||
}
|
||||
this.el.style.width = `${this.size.width - dX}px`;
|
||||
this.el.style.left = `${this.loc.left + dX}px`;
|
||||
}
|
||||
}
|
||||
endResizeW() {
|
||||
if (!this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) {
|
||||
this.isResizingW = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeE(event) {
|
||||
let realEvent;
|
||||
if (event instanceof CustomEvent) {
|
||||
realEvent = event.detail;
|
||||
} else {
|
||||
realEvent = event;
|
||||
}
|
||||
const { x } = this._getEventLoc(realEvent);
|
||||
this.isResizingE = true;
|
||||
this.startX = x;
|
||||
this.size.width = this.el.offsetWidth;
|
||||
this.loc.left = this.el.offsetLeft;
|
||||
event.stopPropagation();
|
||||
}
|
||||
resizeE(event) {
|
||||
if (this.isResizingE) {
|
||||
const { x: newX } = this._getEventLoc(event);
|
||||
let dX = newX - this.startX;
|
||||
if (
|
||||
dX > 0 &&
|
||||
dX >
|
||||
this.limitAreaWidth -
|
||||
(this.size.width + this.loc.left + Math.abs(this.limitLeft))
|
||||
) {
|
||||
dX =
|
||||
this.limitAreaWidth -
|
||||
(this.size.width + this.loc.left + Math.abs(this.limitLeft));
|
||||
} else if (dX < 0 && Math.abs(dX) > this.size.width) {
|
||||
dX = -this.size.width;
|
||||
}
|
||||
this.el.style.width = `${this.size.width + dX}px`;
|
||||
}
|
||||
}
|
||||
endResizeE() {
|
||||
if (!this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) {
|
||||
this.isResizingE = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeNW(event) {
|
||||
this.startResizeN(event);
|
||||
this.startResizeW(event);
|
||||
}
|
||||
resizeNW(event) {
|
||||
this.resizeN(event);
|
||||
this.resizeW(event);
|
||||
}
|
||||
endResizeNW() {
|
||||
if (this.isResizingN && !this.isResizingE && this.isResizingW && !this.isResizingS) {
|
||||
this.isResizingN = false;
|
||||
this.isResizingW = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeNE(event) {
|
||||
this.startResizeN(event);
|
||||
this.startResizeE(event);
|
||||
}
|
||||
resizeNE(event) {
|
||||
this.resizeN(event);
|
||||
this.resizeE(event);
|
||||
}
|
||||
endResizeNE() {
|
||||
if (this.isResizingN && this.isResizingE && !this.isResizingW && !this.isResizingS) {
|
||||
this.isResizingN = false;
|
||||
this.isResizingE = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeSE(event) {
|
||||
this.startResizeS(event);
|
||||
this.startResizeE(event);
|
||||
}
|
||||
resizeSE(event) {
|
||||
this.resizeS(event);
|
||||
this.resizeE(event);
|
||||
}
|
||||
endResizeSE() {
|
||||
if (!this.isResizingN && this.isResizingE && !this.isResizingW && this.isResizingS) {
|
||||
this.isResizingS = false;
|
||||
this.isResizingE = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
startResizeSW(event) {
|
||||
this.startResizeS(event);
|
||||
this.startResizeW(event);
|
||||
}
|
||||
resizeSW(event) {
|
||||
this.resizeS(event);
|
||||
this.resizeW(event);
|
||||
}
|
||||
endResizeSW() {
|
||||
if (!this.isResizingN && !this.isResizingE && this.isResizingW && this.isResizingS) {
|
||||
this.isResizingS = false;
|
||||
this.isResizingW = false;
|
||||
this._triggerResizeEnd();
|
||||
}
|
||||
}
|
||||
_getEventLoc(event) {
|
||||
let coordX, coordY;
|
||||
if (event.touches && event.touches[0]) {
|
||||
coordX = event.touches[0].clientX;
|
||||
coordY = event.touches[0].clientY;
|
||||
} else {
|
||||
coordX = event.clientX;
|
||||
coordY = event.clientY;
|
||||
}
|
||||
return {
|
||||
x: coordX,
|
||||
y: coordY,
|
||||
};
|
||||
}
|
||||
_triggerResizeEnd() {
|
||||
const size = {
|
||||
height: this.el.offsetHeight,
|
||||
width: this.el.offsetWidth,
|
||||
};
|
||||
const loc = {
|
||||
top: this.el.offsetTop,
|
||||
left: this.el.offsetLeft,
|
||||
};
|
||||
this.trigger('resize-end', { size, loc });
|
||||
}
|
||||
}
|
||||
Resizeable.template = 'Resizeable';
|
||||
|
||||
Registries.Component.add(Resizeable);
|
||||
|
||||
return Resizeable;
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
odoo.define('pos_restaurant.BillScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const ReceiptScreen = require('point_of_sale.ReceiptScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const BillScreen = (ReceiptScreen) => {
|
||||
class BillScreen extends ReceiptScreen {
|
||||
confirm() {
|
||||
this.props.resolve({ confirmed: true, payload: null });
|
||||
this.trigger('close-temp-screen');
|
||||
}
|
||||
whenClosing() {
|
||||
this.confirm();
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
async printReceipt() {
|
||||
const currentOrder = this.currentOrder;
|
||||
await super.printReceipt();
|
||||
currentOrder._printed = false;
|
||||
if (this.env.pos.config.iface_print_skip_screen && !this.env.isMobile) {
|
||||
this.confirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
BillScreen.template = 'BillScreen';
|
||||
return BillScreen;
|
||||
};
|
||||
|
||||
Registries.Component.addByExtending(BillScreen, ReceiptScreen);
|
||||
|
||||
return BillScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
odoo.define('pos_restaurant.EditBar', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
class EditBar extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({ isColorPicker: false })
|
||||
}
|
||||
}
|
||||
EditBar.template = 'EditBar';
|
||||
|
||||
Registries.Component.add(EditBar);
|
||||
|
||||
return EditBar;
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
odoo.define('pos_restaurant.EditableTable', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { onMounted, onPatched } = owl;
|
||||
|
||||
class EditableTable extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('resize-end', this._onResizeEnd);
|
||||
useListener('drag-end', this._onDragEnd);
|
||||
onPatched(this._setElementStyle.bind(this));
|
||||
onMounted(this._setElementStyle.bind(this));
|
||||
}
|
||||
_setElementStyle() {
|
||||
const table = this.props.table;
|
||||
function unit(val) {
|
||||
return `${val}px`;
|
||||
}
|
||||
const style = {
|
||||
width: unit(table.width),
|
||||
height: unit(table.height),
|
||||
'line-height': unit(table.height),
|
||||
top: unit(table.position_v),
|
||||
left: unit(table.position_h),
|
||||
'border-radius': table.shape === 'round' ? unit(1000) : '3px',
|
||||
};
|
||||
if (table.color) {
|
||||
style.background = table.color;
|
||||
}
|
||||
if (table.height >= 150 && table.width >= 150) {
|
||||
style['font-size'] = '32px';
|
||||
}
|
||||
Object.assign(this.el.style, style);
|
||||
}
|
||||
_onResizeEnd(event) {
|
||||
const { size, loc } = event.detail;
|
||||
const table = this.props.table;
|
||||
table.width = size.width;
|
||||
table.height = size.height;
|
||||
table.position_v = loc.top;
|
||||
table.position_h = loc.left;
|
||||
this.props.onSaveTable(this.props.table);
|
||||
}
|
||||
_onDragEnd(event) {
|
||||
const { loc } = event.detail;
|
||||
const table = this.props.table;
|
||||
table.position_v = loc.top;
|
||||
table.position_h = loc.left;
|
||||
this.props.onSaveTable(this.props.table);
|
||||
}
|
||||
}
|
||||
EditableTable.template = 'EditableTable';
|
||||
|
||||
Registries.Component.add(EditableTable);
|
||||
|
||||
return EditableTable;
|
||||
});
|
||||
|
|
@ -0,0 +1,363 @@
|
|||
odoo.define('pos_restaurant.FloorScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { debounce } = require("@web/core/utils/timing");
|
||||
const { isConnectionError } = require('point_of_sale.utils');
|
||||
|
||||
const { onPatched, onMounted, onWillUnmount, useRef, useState } = owl;
|
||||
|
||||
class FloorScreen extends PosComponent {
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {Object} props.floor
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
const floor = this.props.floor ? this.props.floor : this.env.pos.floors[0];
|
||||
this.state = useState({
|
||||
selectedFloorId: floor.id,
|
||||
selectedTableId: null,
|
||||
isEditMode: false,
|
||||
floorBackground: floor.background_color,
|
||||
floorMapScrollTop: 0,
|
||||
});
|
||||
this.floorMapRef = useRef('floor-map-ref');
|
||||
onPatched(this.onPatched);
|
||||
onMounted(this.onMounted);
|
||||
onWillUnmount(this.onWillUnmount);
|
||||
}
|
||||
onPatched() {
|
||||
this.floorMapRef.el.style.background = this.state.floorBackground;
|
||||
this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top;
|
||||
}
|
||||
onMounted() {
|
||||
if (this.env.pos.table) {
|
||||
this.env.pos.unsetTable();
|
||||
}
|
||||
this.env.posbus.trigger('start-cash-control');
|
||||
this.floorMapRef.el.style.background = this.state.floorBackground;
|
||||
this.state.floorMapScrollTop = this.floorMapRef.el.getBoundingClientRect().top;
|
||||
// call _tableLongpolling once then set interval of 5sec.
|
||||
this._tableLongpolling();
|
||||
this.tableLongpolling = setInterval(this._tableLongpolling.bind(this), 5000);
|
||||
}
|
||||
onWillUnmount() {
|
||||
clearInterval(this.tableLongpolling);
|
||||
}
|
||||
_computePinchHypo(ev, callbackFunction) {
|
||||
const touches = ev.touches;
|
||||
// If two pointers are down, check for pinch gestures
|
||||
if (touches.length === 2) {
|
||||
const deltaX = touches[0].pageX - touches[1].pageX;
|
||||
const deltaY = touches[0].pageY - touches[1].pageY;
|
||||
callbackFunction(Math.hypot(deltaX, deltaY))
|
||||
}
|
||||
}
|
||||
_onPinchStart(ev) {
|
||||
ev.currentTarget.style.setProperty('touch-action', 'none');
|
||||
this._computePinchHypo(ev, this.startPinch.bind(this));
|
||||
}
|
||||
_onPinchEnd(ev) {
|
||||
ev.currentTarget.style.removeProperty('touch-action');
|
||||
}
|
||||
_onPinchMove(ev) {
|
||||
debounce(this._computePinchHypo, 10, true)(ev, this.movePinch.bind(this));
|
||||
}
|
||||
_onDeselectTable() {
|
||||
this.state.selectedTableId = null;
|
||||
}
|
||||
async _createTableHelper(copyTable) {
|
||||
let newTable;
|
||||
if (copyTable) {
|
||||
newTable = Object.assign({}, copyTable);
|
||||
newTable.position_h += 10;
|
||||
newTable.position_v += 10;
|
||||
} else {
|
||||
newTable = {
|
||||
position_v: 100,
|
||||
position_h: 100,
|
||||
width: 75,
|
||||
height: 75,
|
||||
shape: 'square',
|
||||
seats: 1,
|
||||
};
|
||||
}
|
||||
newTable.name = this._getNewTableName(newTable.name);
|
||||
delete newTable.id;
|
||||
newTable.floor_id = [this.activeFloor.id, ''];
|
||||
newTable.floor = this.activeFloor;
|
||||
try {
|
||||
await this._save(newTable);
|
||||
this.activeTables.push(newTable);
|
||||
return newTable;
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Unable to create table because you are offline.'),
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
_getNewTableName(name) {
|
||||
if (name) {
|
||||
const num = Number((name.match(/\d+/g) || [])[0] || 0);
|
||||
const str = name.replace(/\d+/g, '');
|
||||
const n = { num: num, str: str };
|
||||
n.num += 1;
|
||||
this._lastName = n;
|
||||
} else if (this._lastName) {
|
||||
this._lastName.num += 1;
|
||||
} else {
|
||||
this._lastName = { num: 1, str: 'T' };
|
||||
}
|
||||
return '' + this._lastName.str + this._lastName.num;
|
||||
}
|
||||
async _save(table) {
|
||||
const tableCopy = { ...table };
|
||||
delete tableCopy.floor;
|
||||
const tableId = await this.rpc({
|
||||
model: 'restaurant.table',
|
||||
method: 'create_from_ui',
|
||||
args: [tableCopy],
|
||||
});
|
||||
table.id = tableId;
|
||||
this.env.pos.tables_by_id[tableId] = table;
|
||||
}
|
||||
async _tableLongpolling() {
|
||||
if (this.state.isEditMode) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await this.rpc({
|
||||
model: 'pos.config',
|
||||
method: 'get_tables_order_count',
|
||||
args: [this.env.pos.config.id],
|
||||
});
|
||||
result.forEach((table) => {
|
||||
const table_obj = this.env.pos.tables_by_id[table.id];
|
||||
if (table_obj === undefined) {
|
||||
console.warn(`Table with id ${table.id} is not found in the POS`);
|
||||
return; // skip the table
|
||||
}
|
||||
const unsynced_orders = this.env.pos
|
||||
.getTableOrders(table_obj.id)
|
||||
.filter(
|
||||
(o) =>
|
||||
o.server_id === undefined &&
|
||||
(o.orderlines.length !== 0 || o.paymentlines.length !== 0) &&
|
||||
// do not count the orders that are already finalized
|
||||
!o.finalized
|
||||
).length;
|
||||
table_obj.order_count = table.orders + unsynced_orders;
|
||||
});
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Unable to get orders count'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
get activeFloor() {
|
||||
return this.env.pos.floors_by_id[this.state.selectedFloorId];
|
||||
}
|
||||
get activeTables() {
|
||||
return this.activeFloor.tables;
|
||||
}
|
||||
get isFloorEmpty() {
|
||||
return this.activeTables.length === 0;
|
||||
}
|
||||
get selectedTable() {
|
||||
return this.state.selectedTableId !== null
|
||||
? this.env.pos.tables_by_id[this.state.selectedTableId]
|
||||
: false;
|
||||
}
|
||||
movePinch(hypot) {
|
||||
const delta = hypot / this.scalehypot ;
|
||||
const value = this.initalScale * delta;
|
||||
this.setScale(value);
|
||||
}
|
||||
startPinch(hypot) {
|
||||
this.scalehypot = hypot;
|
||||
this.initalScale = this.getScale();
|
||||
}
|
||||
getMapNode() {
|
||||
return this.el.querySelector('.floor-map > .tables, .floor-map > .empty-floor');
|
||||
}
|
||||
getScale() {
|
||||
const scale = this.getMapNode().style.getPropertyValue('--scale');
|
||||
const parsedScaleValue = parseFloat(scale);
|
||||
return isNaN(parsedScaleValue) ? 1 : parsedScaleValue;
|
||||
}
|
||||
setScale(value) {
|
||||
// a scale can't be a negative number
|
||||
if (value > 0) {
|
||||
this.getMapNode().style.setProperty('--scale', value);
|
||||
}
|
||||
}
|
||||
selectFloor(floor) {
|
||||
this.state.selectedFloorId = floor.id;
|
||||
this.state.floorBackground = this.activeFloor.background_color;
|
||||
this.state.isEditMode = false;
|
||||
this.state.selectedTableId = null;
|
||||
}
|
||||
toggleEditMode() {
|
||||
this.state.isEditMode = !this.state.isEditMode;
|
||||
this.state.selectedTableId = null;
|
||||
}
|
||||
async onSelectTable(table) {
|
||||
if (this.state.isEditMode) {
|
||||
this.state.selectedTableId = table.id;
|
||||
} else {
|
||||
try {
|
||||
if (this.env.pos.orderToTransfer) {
|
||||
await this.env.pos.transferTable(table);
|
||||
} else {
|
||||
await this.env.pos.setTable(table);
|
||||
}
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Unable to fetch orders'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const order = this.env.pos.get_order();
|
||||
this.showScreen(order.get_screen_data().name);
|
||||
}
|
||||
}
|
||||
async onSaveTable(table) {
|
||||
await this._save(table);
|
||||
}
|
||||
async createTable() {
|
||||
const newTable = await this._createTableHelper();
|
||||
if (newTable) {
|
||||
this.state.selectedTableId = newTable.id;
|
||||
}
|
||||
}
|
||||
async duplicateTable() {
|
||||
if (!this.selectedTable) return;
|
||||
const newTable = await this._createTableHelper(this.selectedTable);
|
||||
if (newTable) {
|
||||
this.state.selectedTableId = newTable.id;
|
||||
}
|
||||
}
|
||||
async renameTable() {
|
||||
const selectedTable = this.selectedTable;
|
||||
if (!selectedTable) return;
|
||||
const { confirmed, payload: newName } = await this.showPopup('TextInputPopup', {
|
||||
startingValue: selectedTable.name,
|
||||
title: this.env._t('Table Name ?'),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
if (newName !== selectedTable.name) {
|
||||
selectedTable.name = newName;
|
||||
await this._save(selectedTable);
|
||||
}
|
||||
}
|
||||
async changeSeatsNum() {
|
||||
const selectedTable = this.selectedTable
|
||||
if (!selectedTable) return;
|
||||
const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', {
|
||||
startingValue: selectedTable.seats,
|
||||
cheap: true,
|
||||
title: this.env._t('Number of Seats ?'),
|
||||
isInputSelected: true,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
const newSeatsNum = parseInt(inputNumber, 10) || selectedTable.seats;
|
||||
if (newSeatsNum !== selectedTable.seats) {
|
||||
selectedTable.seats = newSeatsNum;
|
||||
await this._save(selectedTable);
|
||||
}
|
||||
}
|
||||
async changeShape() {
|
||||
if (!this.selectedTable) return;
|
||||
this.selectedTable.shape = this.selectedTable.shape === 'square' ? 'round' : 'square';
|
||||
this.render();
|
||||
await this._save(this.selectedTable);
|
||||
}
|
||||
async setTableColor(color) {
|
||||
this.selectedTable.color = color;
|
||||
this.render();
|
||||
await this._save(this.selectedTable);
|
||||
}
|
||||
async setFloorColor(color) {
|
||||
this.state.floorBackground = color;
|
||||
this.activeFloor.background_color = color;
|
||||
try {
|
||||
await this.rpc({
|
||||
model: 'restaurant.floor',
|
||||
method: 'write',
|
||||
args: [[this.activeFloor.id], { background_color: color }],
|
||||
});
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Unable to change background color'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
async deleteTable() {
|
||||
if (!this.selectedTable) return;
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('Are you sure ?'),
|
||||
body: this.env._t('Removing a table cannot be undone'),
|
||||
});
|
||||
if (!confirmed) return;
|
||||
try {
|
||||
const originalSelectedTableId = this.state.selectedTableId;
|
||||
await this.rpc({
|
||||
model: 'restaurant.table',
|
||||
method: 'create_from_ui',
|
||||
args: [{ active: false, id: originalSelectedTableId }],
|
||||
});
|
||||
this.activeFloor.tables = this.activeTables.filter(
|
||||
(table) => table.id !== originalSelectedTableId
|
||||
);
|
||||
// Value of an object can change inside async function call.
|
||||
// Which means that in this code block, the value of `state.selectedTableId`
|
||||
// before the await call can be different after the finishing the await call.
|
||||
// Since we wanted to disable the selected table after deletion, we should be
|
||||
// setting the selectedTableId to null. However, we only do this if nothing
|
||||
// else is selected during the rpc call.
|
||||
if (this.state.selectedTableId === originalSelectedTableId) {
|
||||
this.state.selectedTableId = null;
|
||||
}
|
||||
delete this.env.pos.tables_by_id[originalSelectedTableId];
|
||||
this.env.pos.TICKET_SCREEN_STATE.syncedOrders.cache = {};
|
||||
} catch (error) {
|
||||
if (isConnectionError(error)) {
|
||||
await this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Offline'),
|
||||
body: this.env._t('Unable to delete table'),
|
||||
});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FloorScreen.template = 'FloorScreen';
|
||||
FloorScreen.hideOrderSelector = true;
|
||||
|
||||
Registries.Component.add(FloorScreen);
|
||||
|
||||
return FloorScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
odoo.define('pos_restaurant.TableWidget', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* props: {
|
||||
* onClick: callback,
|
||||
* table: table object,
|
||||
* }
|
||||
*/
|
||||
class TableWidget extends PosComponent {
|
||||
setup() {
|
||||
owl.onMounted(this.onMounted);
|
||||
}
|
||||
onMounted() {
|
||||
const table = this.props.table;
|
||||
function unit(val) {
|
||||
return `${val}px`;
|
||||
}
|
||||
const style = {
|
||||
width: unit(table.width),
|
||||
height: unit(table.height),
|
||||
'line-height': unit(table.height),
|
||||
top: unit(table.position_v),
|
||||
left: unit(table.position_h),
|
||||
'border-radius': table.shape === 'round' ? unit(1000) : '3px',
|
||||
};
|
||||
if (table.color) {
|
||||
style.background = table.color;
|
||||
}
|
||||
if (table.height >= 150 && table.width >= 150) {
|
||||
style['font-size'] = '32px';
|
||||
}
|
||||
Object.assign(this.el.style, style);
|
||||
|
||||
const tableCover = this.el.querySelector('.table-cover');
|
||||
Object.assign(tableCover.style, { height: `${Math.ceil(this.fill * 100)}%` });
|
||||
}
|
||||
get fill() {
|
||||
const customerCount = this.env.pos.getCustomerCount(this.props.table.id);
|
||||
return Math.min(1, Math.max(0, customerCount / this.props.table.seats));
|
||||
}
|
||||
get orderCount() {
|
||||
const table = this.props.table;
|
||||
return table.order_count !== undefined
|
||||
? table.order_count
|
||||
: this.env.pos
|
||||
.getTableOrders(table.id)
|
||||
.filter(o => o.orderlines.length !== 0 || o.paymentlines.length !== 0).length;
|
||||
}
|
||||
get orderCountClass() {
|
||||
const countClass = { 'order-count': true }
|
||||
if (this.env.pos.config.iface_printers) {
|
||||
const notifications = this._getNotifications();
|
||||
countClass['notify-printing'] = notifications.printing;
|
||||
countClass['notify-skipped'] = notifications.skipped;
|
||||
}
|
||||
return countClass;
|
||||
}
|
||||
get customerCountDisplay() {
|
||||
return `${this.env.pos.getCustomerCount(this.props.table.id)}/${this.props.table.seats}`;
|
||||
}
|
||||
_getNotifications() {
|
||||
const orders = this.env.pos.getTableOrders(this.props.table.id);
|
||||
|
||||
let hasChangesCount = 0;
|
||||
let hasSkippedCount = 0;
|
||||
for (let i = 0; i < orders.length; i++) {
|
||||
if (orders[i].hasChangesToPrint()) {
|
||||
hasChangesCount++;
|
||||
} else if (orders[i].hasSkippedChanges()) {
|
||||
hasSkippedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChangesCount ? { printing: true } : hasSkippedCount ? { skipped: true } : {};
|
||||
}
|
||||
}
|
||||
TableWidget.template = 'TableWidget';
|
||||
|
||||
Registries.Component.add(TableWidget);
|
||||
|
||||
return TableWidget;
|
||||
});
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
odoo.define('pos_restaurant.PosResPaymentScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PaymentScreen = require('point_of_sale.PaymentScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosResPaymentScreen = (PaymentScreen) =>
|
||||
class extends PaymentScreen {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('send-payment-adjust', this._sendPaymentAdjust);
|
||||
}
|
||||
|
||||
async _sendPaymentAdjust({ detail: line }) {
|
||||
const previous_amount = line.get_amount();
|
||||
const amount_diff = line.order.get_total_with_tax() - line.order.get_total_paid();
|
||||
line.set_amount(previous_amount + amount_diff);
|
||||
line.set_payment_status('waiting');
|
||||
|
||||
const payment_terminal = line.payment_method.payment_terminal;
|
||||
const isAdjustSuccessful = await payment_terminal.send_payment_adjust(line.cid);
|
||||
if (isAdjustSuccessful) {
|
||||
line.set_payment_status('done');
|
||||
} else {
|
||||
line.set_amount(previous_amount);
|
||||
line.set_payment_status('done');
|
||||
}
|
||||
}
|
||||
|
||||
get nextScreen() {
|
||||
const order = this.currentOrder;
|
||||
if (!this.env.pos.config.set_tip_after_payment || order.is_tipped) {
|
||||
return super.nextScreen;
|
||||
}
|
||||
// Take the first payment method as the main payment.
|
||||
const mainPayment = order.get_paymentlines()[0];
|
||||
if (mainPayment.canBeAdjusted()) {
|
||||
return 'TipScreen';
|
||||
}
|
||||
return super.nextScreen;
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(PaymentScreen, PosResPaymentScreen);
|
||||
|
||||
return PosResPaymentScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
odoo.define('pos_restaurant.OrderlineNoteButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class OrderlineNoteButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
get selectedOrderline() {
|
||||
return this.env.pos.get_order().get_selected_orderline();
|
||||
}
|
||||
async onClick() {
|
||||
if (!this.selectedOrderline) return;
|
||||
|
||||
const { confirmed, payload: inputNote } = await this.showPopup('TextAreaPopup', {
|
||||
startingValue: this.selectedOrderline.get_note(),
|
||||
title: this.env._t('Add Internal Note'),
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
this.selectedOrderline.set_note(inputNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
OrderlineNoteButton.template = 'OrderlineNoteButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: OrderlineNoteButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.iface_orderline_notes;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(OrderlineNoteButton);
|
||||
|
||||
return OrderlineNoteButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
odoo.define('pos_restaurant.PrintBillButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class PrintBillButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
const order = this.env.pos.get_order();
|
||||
if (order.get_orderlines().length > 0) {
|
||||
order.initialize_validation_date();
|
||||
await this.showTempScreen('BillScreen');
|
||||
} else {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Nothing to Print'),
|
||||
body: this.env._t('There are no order lines'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
PrintBillButton.template = 'PrintBillButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: PrintBillButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.iface_printbill;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(PrintBillButton);
|
||||
|
||||
return PrintBillButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
odoo.define('pos_restaurant.SplitBillButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class SplitBillButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
const order = this.env.pos.get_order();
|
||||
if (order.get_orderlines().length > 0) {
|
||||
this.showScreen('SplitBillScreen');
|
||||
}
|
||||
}
|
||||
}
|
||||
SplitBillButton.template = 'SplitBillButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: SplitBillButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.iface_splitbill;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(SplitBillButton);
|
||||
|
||||
return SplitBillButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
odoo.define('pos_restaurant.SubmitOrderButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* IMPROVEMENT: Perhaps this class is quite complicated for its worth.
|
||||
* This is because it needs to listen to changes to the current order.
|
||||
* Also, the current order changes when the selectedOrder in pos is changed.
|
||||
* After setting new current order, we update the listeners.
|
||||
*/
|
||||
class SubmitOrderButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.clicked = false; //mutex, we don't want to be able to spam the printers
|
||||
}
|
||||
async _onClick() {
|
||||
if (!this.clicked) {
|
||||
try {
|
||||
this.clicked = true;
|
||||
const order = this.env.pos.get_order();
|
||||
if (order.hasChangesToPrint()) {
|
||||
const isPrintSuccessful = await order.printChanges();
|
||||
if (isPrintSuccessful) {
|
||||
order.updatePrintedResume();
|
||||
} else {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Printing failed'),
|
||||
body: this.env._t('Failed in printing the changes in the order'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.clicked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
get addedClasses() {
|
||||
if (!this.currentOrder) return {};
|
||||
const hasChanges = this.currentOrder.hasChangesToPrint();
|
||||
const skipped = hasChanges ? false : this.currentOrder.hasSkippedChanges();
|
||||
return {
|
||||
highlight: hasChanges,
|
||||
altlight: skipped,
|
||||
};
|
||||
}
|
||||
}
|
||||
SubmitOrderButton.template = 'SubmitOrderButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: SubmitOrderButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.module_pos_restaurant && this.env.pos.unwatched.printers.length;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(SubmitOrderButton);
|
||||
|
||||
return SubmitOrderButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
odoo.define('pos_restaurant.TableGuestsButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class TableGuestsButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
get nGuests() {
|
||||
return this.currentOrder ? this.currentOrder.getCustomerCount() : 0;
|
||||
}
|
||||
async onClick() {
|
||||
const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', {
|
||||
startingValue: this.nGuests,
|
||||
cheap: true,
|
||||
title: this.env._t('Guests ?'),
|
||||
isInputSelected: true
|
||||
});
|
||||
|
||||
if (confirmed) {
|
||||
const guestCount = parseInt(inputNumber, 10) || 1;
|
||||
// Set the maximum number possible for an integer
|
||||
const max_capacity = 2**31 - 1;
|
||||
if (guestCount > max_capacity) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Blocked action'),
|
||||
body: _.str.sprintf(
|
||||
this.env._t('You cannot put a number that exceeds %s '),
|
||||
max_capacity,
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.env.pos.get_order().setCustomerCount(guestCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
TableGuestsButton.template = 'TableGuestsButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: TableGuestsButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.module_pos_restaurant;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(TableGuestsButton);
|
||||
|
||||
return TableGuestsButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
odoo.define('pos_restaurant.TransferOrderButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class TransferOrderButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
async onClick() {
|
||||
this.env.pos.setCurrentOrderToTransfer();
|
||||
this.showScreen('FloorScreen');
|
||||
}
|
||||
}
|
||||
TransferOrderButton.template = 'TransferOrderButton';
|
||||
|
||||
ProductScreen.addControlButton({
|
||||
component: TransferOrderButton,
|
||||
condition: function() {
|
||||
return this.env.pos.config.iface_floorplan;
|
||||
},
|
||||
});
|
||||
|
||||
Registries.Component.add(TransferOrderButton);
|
||||
|
||||
return TransferOrderButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
odoo.define('pos_restaurant.Orderline', function(require) {
|
||||
'use strict';
|
||||
|
||||
const Orderline = require('point_of_sale.Orderline');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosResOrderline = Orderline =>
|
||||
class extends Orderline {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get addedClasses() {
|
||||
const res = super.addedClasses;
|
||||
Object.assign(res, {
|
||||
dirty: this.props.line.mp_dirty,
|
||||
skip: this.props.line.mp_skip,
|
||||
});
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* @override
|
||||
* if doubleclick, change mp_dirty to mp_skip
|
||||
*
|
||||
* IMPROVEMENT: Instead of handling both double click and click in single
|
||||
* method, perhaps we can separate double click from single click.
|
||||
*/
|
||||
selectLine() {
|
||||
const line = this.props.line; // the orderline
|
||||
if (this.env.pos.get_order().selected_orderline.id !== line.id) {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
} else if (!this.mp_dbclk_time) {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
} else if (this.mp_dbclk_time + 500 > new Date().getTime()) {
|
||||
line.set_skip(!line.mp_skip);
|
||||
this.mp_dbclk_time = 0;
|
||||
} else {
|
||||
this.mp_dbclk_time = new Date().getTime();
|
||||
}
|
||||
super.selectLine();
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(Orderline, PosResOrderline);
|
||||
|
||||
return Orderline;
|
||||
});
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
odoo.define('pos_restaurant.ReceiptScreen', function(require) {
|
||||
'use strict';
|
||||
|
||||
const ReceiptScreen = require('point_of_sale.ReceiptScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const PosResReceiptScreen = ReceiptScreen =>
|
||||
class extends ReceiptScreen {
|
||||
//@override
|
||||
_addNewOrder() {
|
||||
if (!this.env.pos.config.iface_floorplan) {
|
||||
super._addNewOrder();
|
||||
}
|
||||
}
|
||||
//@override
|
||||
get nextScreen() {
|
||||
if (this.env.pos.config.iface_floorplan) {
|
||||
const table = this.env.pos.table;
|
||||
return { name: 'FloorScreen', props: { floor: table ? table.floor : null } };
|
||||
} else {
|
||||
return super.nextScreen;
|
||||
}
|
||||
}
|
||||
onBackToFloorButtonClick() {
|
||||
// If we're here and the order is paid, we can remove it from the orders
|
||||
this.env.pos.removeOrder(this.currentOrder);
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(ReceiptScreen, PosResReceiptScreen);
|
||||
|
||||
return ReceiptScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
odoo.define('pos_restaurant.SplitBillScreen', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const { Order } = require('point_of_sale.models');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState, onMounted } = owl;
|
||||
|
||||
class SplitBillScreen extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click-line', this.onClickLine);
|
||||
this.splitlines = useState(this._initSplitLines(this.env.pos.get_order()));
|
||||
this.newOrderLines = {};
|
||||
this.newOrder = undefined;
|
||||
this._isFinal = false;
|
||||
onMounted(() => {
|
||||
// Should create the new order outside of the constructor because
|
||||
// sequence_number of pos_session is modified. which will trigger
|
||||
// rerendering which will rerender this screen and will be infinite loop.
|
||||
this.newOrder = Order.create(
|
||||
{},
|
||||
{
|
||||
pos: this.env.pos,
|
||||
temporary: true,
|
||||
}
|
||||
);
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
get disallow() {
|
||||
return false;
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
get orderlines() {
|
||||
return this.currentOrder.get_orderlines();
|
||||
}
|
||||
onClickLine(event) {
|
||||
const line = event.detail;
|
||||
this._splitQuantity(line);
|
||||
this._updateNewOrder(line);
|
||||
}
|
||||
back() {
|
||||
this.showScreen('ProductScreen');
|
||||
}
|
||||
proceed() {
|
||||
if (_.isEmpty(this.splitlines))
|
||||
// Splitlines is empty
|
||||
return;
|
||||
|
||||
this._isFinal = true;
|
||||
delete this.newOrder.temporary;
|
||||
|
||||
if (!this._isFullPayOrder()) {
|
||||
this._setQuantityOnCurrentOrder();
|
||||
|
||||
this.newOrder.set_screen_data({ name: 'PaymentScreen' });
|
||||
|
||||
// for the kitchen printer we assume that everything
|
||||
// has already been sent to the kitchen before splitting
|
||||
// the bill. So we save all changes both for the old
|
||||
// order and for the new one. This is not entirely correct
|
||||
// but avoids flooding the kitchen with unnecessary orders.
|
||||
// Not sure what to do in this case.
|
||||
if (this.env.pos.config.iface_printers) {
|
||||
this.currentOrder.updatePrintedResume();
|
||||
this.newOrder.updatePrintedResume();
|
||||
}
|
||||
|
||||
this.newOrder.setCustomerCount(1);
|
||||
const newCustomerCount = this.currentOrder.getCustomerCount() - 1;
|
||||
this.currentOrder.setCustomerCount(newCustomerCount || 1);
|
||||
this.currentOrder.set_screen_data({ name: 'ProductScreen' });
|
||||
|
||||
const reactiveNewOrder = this.env.pos.makeOrderReactive(this.newOrder);
|
||||
this.env.pos.orders.add(reactiveNewOrder);
|
||||
this.env.pos.selectedOrder = reactiveNewOrder;
|
||||
}
|
||||
this.showScreen('PaymentScreen');
|
||||
}
|
||||
/**
|
||||
* @param {models.Order} order
|
||||
* @returns {Object<{ quantity: number }>} splitlines
|
||||
*/
|
||||
_initSplitLines(order) {
|
||||
const splitlines = {};
|
||||
for (let line of order.get_orderlines()) {
|
||||
splitlines[line.id] = { product: line.get_product().id, quantity: 0 };
|
||||
}
|
||||
return splitlines;
|
||||
}
|
||||
_splitQuantity(line) {
|
||||
const split = this.splitlines[line.id];
|
||||
const lineQty = line.get_quantity();
|
||||
|
||||
if(lineQty > 0) {
|
||||
if (!line.get_unit().is_pos_groupable) {
|
||||
if (split.quantity !== lineQty) {
|
||||
split.quantity = lineQty;
|
||||
} else {
|
||||
split.quantity = 0;
|
||||
}
|
||||
} else {
|
||||
if (split.quantity < lineQty) {
|
||||
split.quantity += line.get_unit().is_pos_groupable? 1: line.get_unit().rounding;
|
||||
if (split.quantity > lineQty) {
|
||||
split.quantity = lineQty;
|
||||
}
|
||||
} else {
|
||||
split.quantity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_updateNewOrder(line) {
|
||||
const split = this.splitlines[line.id];
|
||||
let orderline = this.newOrderLines[line.id];
|
||||
if (split.quantity) {
|
||||
if (!orderline) {
|
||||
orderline = line.clone();
|
||||
this.newOrder.add_orderline(orderline);
|
||||
this.newOrderLines[line.id] = orderline;
|
||||
}
|
||||
orderline.set_quantity(split.quantity, 'do not recompute unit price');
|
||||
} else if (orderline) {
|
||||
this.newOrder.remove_orderline(orderline);
|
||||
this.newOrderLines[line.id] = null;
|
||||
}
|
||||
}
|
||||
_isFullPayOrder() {
|
||||
let order = this.env.pos.get_order();
|
||||
let full = true;
|
||||
let splitlines = this.splitlines;
|
||||
let groupedLines = _.groupBy(order.get_orderlines(), line => line.get_product().id);
|
||||
|
||||
Object.keys(groupedLines).forEach(function (lineId) {
|
||||
var maxQuantity = groupedLines[lineId].reduce(((quantity, line) => quantity + line.get_quantity()), 0);
|
||||
Object.keys(splitlines).forEach(id => {
|
||||
let split = splitlines[id];
|
||||
if(split.product === groupedLines[lineId][0].get_product().id)
|
||||
maxQuantity -= split.quantity;
|
||||
});
|
||||
if(maxQuantity !== 0)
|
||||
full = false;
|
||||
});
|
||||
|
||||
return full;
|
||||
}
|
||||
_setQuantityOnCurrentOrder() {
|
||||
let order = this.env.pos.get_order();
|
||||
for (var id in this.splitlines) {
|
||||
var split = this.splitlines[id];
|
||||
var line = this.currentOrder.get_orderline(parseInt(id));
|
||||
|
||||
if(!this.disallow) {
|
||||
line.set_quantity(
|
||||
line.get_quantity() - split.quantity,
|
||||
'do not recompute unit price'
|
||||
);
|
||||
if (Math.abs(line.get_quantity()) < 0.00001) {
|
||||
this.currentOrder.remove_orderline(line);
|
||||
}
|
||||
} else {
|
||||
if(split.quantity) {
|
||||
let decreaseLine = line.clone();
|
||||
decreaseLine.order = order;
|
||||
decreaseLine.noDecrease = true;
|
||||
decreaseLine.set_quantity(-split.quantity);
|
||||
order.add_orderline(decreaseLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SplitBillScreen.template = 'SplitBillScreen';
|
||||
|
||||
Registries.Component.add(SplitBillScreen);
|
||||
|
||||
return SplitBillScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
odoo.define('pos_restaurant.SplitOrderline', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class SplitOrderline extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.onClick);
|
||||
}
|
||||
get isSelected() {
|
||||
return this.props.split.quantity !== 0;
|
||||
}
|
||||
onClick() {
|
||||
this.trigger('click-line', this.props.line);
|
||||
}
|
||||
}
|
||||
SplitOrderline.template = 'SplitOrderline';
|
||||
|
||||
Registries.Component.add(SplitOrderline);
|
||||
|
||||
return SplitOrderline;
|
||||
});
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
odoo.define('pos_restaurant.TicketScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const TicketScreen = require('point_of_sale.TicketScreen');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { useAutofocus } = require("@web/core/utils/hooks");
|
||||
const { parse } = require('web.field_utils');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
const PosResTicketScreen = (TicketScreen) =>
|
||||
class extends TicketScreen {
|
||||
close() {
|
||||
if (!this.env.pos.config.iface_floorplan) {
|
||||
super.close();
|
||||
} else {
|
||||
const order = this.env.pos.get_order();
|
||||
if (order) {
|
||||
const { name: screenName } = order.get_screen_data();
|
||||
this.showScreen(screenName);
|
||||
} else {
|
||||
this.showScreen('FloorScreen');
|
||||
}
|
||||
}
|
||||
}
|
||||
_getScreenToStatusMap() {
|
||||
return Object.assign(super._getScreenToStatusMap(), {
|
||||
PaymentScreen: this.env.pos.config.set_tip_after_payment ? 'OPEN' : super._getScreenToStatusMap().PaymentScreen,
|
||||
TipScreen: 'TIPPING',
|
||||
});
|
||||
}
|
||||
getTable(order) {
|
||||
const table = order.getTable();
|
||||
return table ? `${table.floor.name} (${table.name})` : '';
|
||||
}
|
||||
//@override
|
||||
_getSearchFields() {
|
||||
if (!this.env.pos.config.iface_floorplan) {
|
||||
return super._getSearchFields();
|
||||
}
|
||||
return Object.assign({}, super._getSearchFields(), {
|
||||
TABLE: {
|
||||
repr: this.getTable.bind(this),
|
||||
displayName: this.env._t('Table'),
|
||||
modelField: 'table_id.name',
|
||||
}
|
||||
});
|
||||
}
|
||||
async _setOrder(order) {
|
||||
if (!this.env.pos.config.iface_floorplan || this.env.pos.table) {
|
||||
super._setOrder(order);
|
||||
} else {
|
||||
// we came from the FloorScreen
|
||||
const orderTable = order.getTable();
|
||||
await this.env.pos.setTable(orderTable, order.uid);
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
shouldShowNewOrderButton() {
|
||||
return this.env.pos.config.iface_floorplan ? Boolean(this.env.pos.table) : super.shouldShowNewOrderButton();
|
||||
}
|
||||
_getOrderList() {
|
||||
if (this.env.pos.table) {
|
||||
return this.env.pos.getTableOrders(this.env.pos.table.id);
|
||||
}
|
||||
return super._getOrderList();
|
||||
}
|
||||
async settleTips() {
|
||||
// set tip in each order
|
||||
for (const order of this.getFilteredOrderList()) {
|
||||
const tipAmount = parse.float(order.uiState.TipScreen.inputTipAmount || '0');
|
||||
const serverId = this.env.pos.validated_orders_name_server_id_map[order.name];
|
||||
if (!serverId) {
|
||||
console.warn(`${order.name} is not yet sync. Sync it to server before setting a tip.`);
|
||||
} else {
|
||||
const result = await this.setTip(order, serverId, tipAmount);
|
||||
if (!result) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
//@override
|
||||
_selectNextOrder(currentOrder) {
|
||||
if (this.env.pos.config.iface_floorplan && this.env.pos.table) {
|
||||
return super._selectNextOrder(...arguments);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
async _onDeleteOrder() {
|
||||
await super._onDeleteOrder(...arguments);
|
||||
if (this.env.pos.config.iface_floorplan) {
|
||||
if (!this.env.pos.table) {
|
||||
this.env.pos._removeOrdersFromServer();
|
||||
}
|
||||
const orderList = this.env.pos.table ? this.env.pos.getTableOrders(this.env.pos.table.id) : this.env.pos.orders;
|
||||
if (orderList.length == 0) {
|
||||
this.showScreen('FloorScreen');
|
||||
}
|
||||
}
|
||||
}
|
||||
async setTip(order, serverId, amount) {
|
||||
try {
|
||||
const paymentline = order.get_paymentlines()[0];
|
||||
if (paymentline.payment_method.payment_terminal) {
|
||||
paymentline.amount += amount;
|
||||
this.env.pos.set_order(order, {silent: true});
|
||||
await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid);
|
||||
}
|
||||
|
||||
if (!amount) {
|
||||
await this.setNoTip(serverId);
|
||||
} else {
|
||||
order.finalized = false;
|
||||
order.set_tip(amount);
|
||||
order.finalized = true;
|
||||
const tip_line = order.selected_orderline;
|
||||
await this.rpc({
|
||||
method: 'set_tip',
|
||||
model: 'pos.order',
|
||||
args: [serverId, tip_line.export_as_JSON()],
|
||||
});
|
||||
}
|
||||
if (order === this.env.pos.get_order()) {
|
||||
this._selectNextOrder(order);
|
||||
}
|
||||
this.env.pos.removeOrder(order);
|
||||
return true;
|
||||
} catch (_error) {
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: 'Failed to set tip',
|
||||
body: `Failed to set tip to ${order.name}. Do you want to proceed on setting the tips of the remaining?`,
|
||||
});
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
async setNoTip(serverId) {
|
||||
await this.rpc({
|
||||
method: 'set_no_tip',
|
||||
model: 'pos.order',
|
||||
args: [serverId],
|
||||
});
|
||||
}
|
||||
_getOrderStates() {
|
||||
const result = super._getOrderStates();
|
||||
if (this.env.pos.config.set_tip_after_payment) {
|
||||
result.delete('PAYMENT');
|
||||
result.set('OPEN', { text: this.env._t('Open'), indented: true });
|
||||
result.set('TIPPING', { text: this.env._t('Tipping'), indented: true });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async _onDoRefund() {
|
||||
const order = this.getSelectedSyncedOrder();
|
||||
if(order && this.env.pos.config.iface_floorplan && !this.env.pos.table) {
|
||||
this.env.pos.setTable(order.table ? order.table : Object.values(this.env.pos.tables_by_id)[0]);
|
||||
}
|
||||
super._onDoRefund();
|
||||
}
|
||||
isDefaultOrderEmpty(order) {
|
||||
if (this.env.pos.config.iface_floorplan) {
|
||||
return false;
|
||||
}
|
||||
return super.isDefaultOrderEmpty(...arguments);
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(TicketScreen, PosResTicketScreen);
|
||||
|
||||
class TipCell extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({ isEditing: false });
|
||||
this.orderUiState = this.props.order.uiState.TipScreen;
|
||||
useAutofocus();
|
||||
}
|
||||
get tipAmountStr() {
|
||||
return this.env.pos.format_currency(parse.float(this.orderUiState.inputTipAmount || '0'));
|
||||
}
|
||||
onBlur() {
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
onKeydown(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.state.isEditing = false;
|
||||
}
|
||||
}
|
||||
editTip() {
|
||||
this.state.isEditing = true;
|
||||
}
|
||||
}
|
||||
TipCell.template = 'TipCell';
|
||||
|
||||
Registries.Component.add(TipCell);
|
||||
|
||||
return { TicketScreen, TipCell };
|
||||
});
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
odoo.define('pos_restaurant.TipScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const { parse } = require('web.field_utils');
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
|
||||
const { onMounted } = owl;
|
||||
|
||||
class TipScreen extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = this.currentOrder.uiState.TipScreen;
|
||||
this._totalAmount = this.currentOrder.get_total_with_tax();
|
||||
|
||||
onMounted(() => {
|
||||
this.printTipReceipt();
|
||||
});
|
||||
}
|
||||
get overallAmountStr() {
|
||||
const tipAmount = parse.float(this.state.inputTipAmount || '0');
|
||||
const original = this.env.pos.format_currency(this.totalAmount);
|
||||
const tip = this.env.pos.format_currency(tipAmount);
|
||||
const overall = this.env.pos.format_currency(this.totalAmount + tipAmount);
|
||||
return `${original} + ${tip} tip = ${overall}`;
|
||||
}
|
||||
get totalAmount() {
|
||||
return this._totalAmount;
|
||||
}
|
||||
get currentOrder() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
get percentageTips() {
|
||||
return [
|
||||
{ percentage: '15%', amount: 0.15 * this.totalAmount },
|
||||
{ percentage: '20%', amount: 0.2 * this.totalAmount },
|
||||
{ percentage: '25%', amount: 0.25 * this.totalAmount },
|
||||
];
|
||||
}
|
||||
async validateTip() {
|
||||
const amount = parse.float(this.state.inputTipAmount) || 0;
|
||||
const order = this.env.pos.get_order();
|
||||
const serverId = this.env.pos.validated_orders_name_server_id_map[order.name];
|
||||
|
||||
if (!serverId) {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Unsynced order'),
|
||||
body: this.env._t('This order is not yet synced to server. Make sure it is synced then try again.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!amount) {
|
||||
await this.rpc({
|
||||
method: 'set_no_tip',
|
||||
model: 'pos.order',
|
||||
args: [serverId],
|
||||
});
|
||||
this.goNextScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (amount > 0.25 * this.totalAmount) {
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: 'Are you sure?',
|
||||
body: `${this.env.pos.format_currency(
|
||||
amount
|
||||
)} is more than 25% of the order's total amount. Are you sure of this tip amount?`,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
}
|
||||
|
||||
// set the tip by temporarily allowing order modification
|
||||
order.finalized = false;
|
||||
order.set_tip(amount);
|
||||
order.finalized = true;
|
||||
|
||||
const paymentline = this.env.pos.get_order().get_paymentlines()[0];
|
||||
if (paymentline.payment_method.payment_terminal) {
|
||||
paymentline.amount += amount;
|
||||
await paymentline.payment_method.payment_terminal.send_payment_adjust(paymentline.cid);
|
||||
}
|
||||
|
||||
// set_tip calls add_product which sets the new line as the selected_orderline
|
||||
const tip_line = order.selected_orderline;
|
||||
await this.rpc({
|
||||
method: 'set_tip',
|
||||
model: 'pos.order',
|
||||
args: [serverId, tip_line.export_as_JSON()],
|
||||
});
|
||||
this.goNextScreen();
|
||||
}
|
||||
goNextScreen() {
|
||||
this.env.pos.removeOrder(this.currentOrder);
|
||||
if (!this.env.pos.config.iface_floorplan) {
|
||||
this.env.pos.add_new_order();
|
||||
}
|
||||
const { name, props } = this.nextScreen;
|
||||
this.showScreen(name, props);
|
||||
}
|
||||
get nextScreen() {
|
||||
if (this.env.pos.config.module_pos_restaurant && this.env.pos.config.iface_floorplan) {
|
||||
const table = this.env.pos.table;
|
||||
return { name: 'FloorScreen', props: { floor: table ? table.floor : null } };
|
||||
} else {
|
||||
return { name: 'ProductScreen' };
|
||||
}
|
||||
}
|
||||
async printTipReceipt() {
|
||||
const receipts = [
|
||||
this.currentOrder.selected_paymentline.ticket,
|
||||
this.currentOrder.selected_paymentline.cashier_receipt
|
||||
];
|
||||
|
||||
for (let i = 0; i < receipts.length; i++) {
|
||||
const data = receipts[i];
|
||||
var receipt = renderToString('TipReceipt', {
|
||||
receipt: this.currentOrder.getOrderReceiptEnv().receipt,
|
||||
data: data,
|
||||
total: this.env.pos.format_currency(this.totalAmount),
|
||||
});
|
||||
|
||||
if (this.env.proxy.printer) {
|
||||
await this._printIoT(receipt);
|
||||
} else {
|
||||
await this._printWeb(receipt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _printIoT(receipt) {
|
||||
const printResult = await this.env.proxy.printer.print_receipt(receipt);
|
||||
if (!printResult.successful) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: printResult.message.title,
|
||||
body: printResult.message.body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _printWeb(receipt) {
|
||||
try {
|
||||
$(this.el).find('.pos-receipt-container').html(receipt);
|
||||
window.print();
|
||||
} catch (_err) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Printing is not supported on some browsers'),
|
||||
body: this.env._t(
|
||||
'Printing is not supported on some browsers due to no default printing protocol ' +
|
||||
'is available. It is possible to print your tickets by making use of an IoT Box.'
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
TipScreen.template = 'pos_restaurant.TipScreen';
|
||||
|
||||
Registries.Component.add(TipScreen);
|
||||
|
||||
return TipScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,651 @@
|
|||
odoo.define('pos_restaurant.models', function (require) {
|
||||
"use strict";
|
||||
|
||||
const { PosGlobalState, Order, Orderline, Payment } = require('point_of_sale.models');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { uuidv4 } = require('point_of_sale.utils');
|
||||
const core = require('web.core');
|
||||
const Printer = require('point_of_sale.Printer').Printer;
|
||||
const { batched } = require('point_of_sale.utils')
|
||||
const QWeb = core.qweb;
|
||||
|
||||
const TIMEOUT = 7500;
|
||||
|
||||
const PosRestaurantPosGlobalState = (PosGlobalState) => class PosRestaurantPosGlobalState extends PosGlobalState {
|
||||
constructor(obj) {
|
||||
super(obj);
|
||||
this.orderToTransfer = null; // table transfer feature
|
||||
this.ordersToUpdateSet = new Set(); // used to know which orders need to be sent to the back end when syncing
|
||||
this.transferredOrdersSet = new Set(); // used to know which orders has been transferred but not sent to the back end yet
|
||||
this.loadingOrderState = false; // used to prevent orders fetched to be put in the update set during the reactive change
|
||||
}
|
||||
//@override
|
||||
async _processData(loadedData) {
|
||||
await super._processData(...arguments);
|
||||
if (this.config.is_table_management) {
|
||||
this.floors = loadedData['restaurant.floor'];
|
||||
this.loadRestaurantFloor();
|
||||
}
|
||||
if (this.config.module_pos_restaurant) {
|
||||
this._loadRestaurantPrinter(loadedData['restaurant.printer']);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
_onReactiveOrderUpdated(order) {
|
||||
super._onReactiveOrderUpdated(...arguments)
|
||||
if (this.config.iface_floorplan && !this.loadingOrderState) {
|
||||
this.ordersToUpdateSet.add(order);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
removeOrder(order, removeFromServer=true) {
|
||||
super.removeOrder(...arguments);
|
||||
if (this.config.iface_floorplan && removeFromServer) {
|
||||
if (this.ordersToUpdateSet.has(order)) {
|
||||
this.ordersToUpdateSet.delete(order)
|
||||
}
|
||||
if (order.server_id && !order.finalized) {
|
||||
this.db.set_order_to_remove_from_server(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
//@override
|
||||
async after_load_server_data() {
|
||||
var res = await super.after_load_server_data(...arguments);
|
||||
if (this.config.iface_floorplan) {
|
||||
this.table = null;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
//@override
|
||||
// if we have tables, we do not load a default order, as the default order will be
|
||||
// set when the user selects a table.
|
||||
set_start_order() {
|
||||
if (!this.config.iface_floorplan) {
|
||||
super.set_start_order(...arguments);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
add_new_order() {
|
||||
const order = super.add_new_order();
|
||||
this.ordersToUpdateSet.add(order);
|
||||
return order;
|
||||
}
|
||||
//@override
|
||||
createReactiveOrder(json) {
|
||||
let reactiveOrder = super.createReactiveOrder(...arguments);
|
||||
if (this.config.iface_printers) {
|
||||
const updateOrderChanges = () => {
|
||||
if (reactiveOrder.get_screen_data().name === 'ProductScreen') {
|
||||
reactiveOrder.updateChangesToPrint();
|
||||
}
|
||||
}
|
||||
reactiveOrder = owl.reactive(reactiveOrder, batched(updateOrderChanges));
|
||||
reactiveOrder.updateChangesToPrint();
|
||||
}
|
||||
return reactiveOrder;
|
||||
}
|
||||
//@override
|
||||
async load_orders() {
|
||||
this.loadingOrderState = true;
|
||||
await super.load_orders();
|
||||
this.loadingOrderState = false;
|
||||
}
|
||||
_loadRestaurantPrinter(printers) {
|
||||
this.unwatched.printers = [];
|
||||
// list of product categories that belong to one or more order printer
|
||||
this.printers_category_ids_set = new Set();
|
||||
for (let printerConfig of printers) {
|
||||
let printer = this.create_printer(printerConfig);
|
||||
printer.config = printerConfig;
|
||||
this.unwatched.printers.push(printer);
|
||||
for (let id of printer.config.product_categories_ids) {
|
||||
this.printers_category_ids_set.add(id);
|
||||
}
|
||||
}
|
||||
this.config.iface_printers = !!this.unwatched.printers.length;
|
||||
}
|
||||
async _getTableOrdersFromServer(tableIds) {
|
||||
this.set_synch('connecting', 1);
|
||||
try {
|
||||
const orders = await this.env.services.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'get_table_draft_orders',
|
||||
args: [tableIds],
|
||||
}, {
|
||||
timeout: TIMEOUT,
|
||||
shadow: true,
|
||||
});
|
||||
this.set_synch('connected');
|
||||
return orders;
|
||||
} catch (error) {
|
||||
this.set_synch('error');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sync orders that got updated to the back end
|
||||
* @param tableId ID of the table we want to sync
|
||||
*/
|
||||
async _syncTableOrdersToServer() {
|
||||
await this._pushOrdersToServer();
|
||||
await this._removeOrdersFromServer();
|
||||
// This need to be called here otherwise _onReactiveOrderUpdated() will be called after the set is being cleared
|
||||
this.ordersToUpdateSet.clear();
|
||||
this.transferredOrdersSet.clear();
|
||||
}
|
||||
/**
|
||||
* Send the orders to be saved to the back end
|
||||
* @throw error
|
||||
*/
|
||||
async _pushOrdersToServer() {
|
||||
const ordersUidsToSync = [...this.ordersToUpdateSet].map(order => order.uid);
|
||||
const ordersToSync = this.db.get_unpaid_orders_to_sync(ordersUidsToSync);
|
||||
const ordersResponse = await this._save_to_server(ordersToSync, {'draft': true});
|
||||
const tableOrders = [...this.ordersToUpdateSet].map(order => order);
|
||||
ordersResponse.forEach(orderResponseData => this._updateTableOrder(orderResponseData, tableOrders));
|
||||
}
|
||||
// created this hook for modularity
|
||||
_updateTableOrder(ordersResponseData, tableOrders) {
|
||||
const order = tableOrders.find(order => order.name === ordersResponseData.pos_reference);
|
||||
order.server_id = ordersResponseData.id;
|
||||
return order;
|
||||
}
|
||||
/**
|
||||
* Remove the deleted orders from the backend.
|
||||
* @throw error
|
||||
*/
|
||||
async _removeOrdersFromServer() {
|
||||
const removedOrdersIds = this.db.get_ids_to_remove_from_server();
|
||||
if (removedOrdersIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeout = TIMEOUT * removedOrdersIds.length;
|
||||
this.set_synch('connecting', removedOrdersIds.length);
|
||||
try {
|
||||
const removeOrdersResponseData = await this.env.services.rpc({
|
||||
model: 'pos.order',
|
||||
method: 'remove_from_ui',
|
||||
args: [removedOrdersIds],
|
||||
}, {
|
||||
timeout: timeout,
|
||||
shadow: true,
|
||||
});
|
||||
this.set_synch('connected');
|
||||
this._postRemoveFromServer(removedOrdersIds, removeOrdersResponseData);
|
||||
} catch (reason) {
|
||||
let error = reason.message;
|
||||
if (error.code === 200) {
|
||||
// Business Logic Error, not a connection problem
|
||||
//if warning do not need to display traceback!!
|
||||
if (error.data.exception_type == 'warning') {
|
||||
delete error.data.debug;
|
||||
}
|
||||
}
|
||||
// important to throw error here and let the rendering component handle the error
|
||||
console.warn('Failed to remove orders:', removedOrdersIds);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// to override
|
||||
_postRemoveFromServer(serverIds, data) {
|
||||
this.db.set_ids_removed_from_server(serverIds);
|
||||
}
|
||||
/**
|
||||
* Replace all the orders of a table by orders fetched from the backend
|
||||
* @param tableId ID of the table
|
||||
* @throws error
|
||||
*/
|
||||
async _syncTableOrdersFromServer(tableId) {
|
||||
await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time
|
||||
const ordersJsons = await this._getTableOrdersFromServer([tableId]);
|
||||
const tableOrders = this.getTableOrders(tableId);
|
||||
this._replaceOrders(tableOrders, ordersJsons);
|
||||
}
|
||||
async _syncAllOrdersFromServer() {
|
||||
await this._removeOrdersFromServer(); // in case we were offline and we deleted orders in the mean time
|
||||
const tableIds = [].concat(...this.floors.map(floor => floor.tables.map(table => table.id)));
|
||||
const ordersJsons = await this._getTableOrdersFromServer(tableIds); // get all orders
|
||||
await this._syncTableOrdersToServer(); // to prevent losing the transferred orders
|
||||
const allOrders = [...this.get_order_list()];
|
||||
this._replaceOrders(allOrders, ordersJsons);
|
||||
}
|
||||
_replaceOrders(ordersToReplace, newOrdersJsons) {
|
||||
ordersToReplace.forEach(order => {
|
||||
// We don't remove the validated orders because we still want to see them in the ticket screen.
|
||||
// Orders in 'ReceiptScreen' or 'TipScreen' are validated orders.
|
||||
if (order.server_id && !order.finalized && !this.transferredOrdersSet.has(order)){
|
||||
this.removeOrder(order, false);
|
||||
}
|
||||
});
|
||||
newOrdersJsons.forEach(json => {
|
||||
// Because of the offline feature, some draft orders fetched from the backend will appear
|
||||
// to belong in different table, but in fact they are already moved.
|
||||
const transferredOrder = [...this.transferredOrdersSet].find(order => order.uid === json.uid)
|
||||
const isSameTable = transferredOrder && transferredOrder.tableId === json.tableId;
|
||||
if (isSameTable) {
|
||||
// this means we transferred back to the original table, we'll prioritize the server state
|
||||
this.removeOrder(transferredOrder, false);
|
||||
}
|
||||
if (!transferredOrder || isSameTable) {
|
||||
const order = this.createReactiveOrder(json);
|
||||
this.orders.add(order);
|
||||
}
|
||||
});
|
||||
}
|
||||
setLoadingOrderState(bool) {
|
||||
this.loadingOrderState = bool;
|
||||
}
|
||||
loadRestaurantFloor() {
|
||||
// we do this in the front end due to the circular/recursive reference needed
|
||||
// Ignore floorplan features if no floor specified.
|
||||
this.config.iface_floorplan = !!(this.floors && this.floors.length > 0);
|
||||
if (this.config.iface_floorplan) {
|
||||
this.floors_by_id = {};
|
||||
this.tables_by_id = {};
|
||||
for (let floor of this.floors) {
|
||||
this.floors_by_id[floor.id] = floor;
|
||||
for (let table of floor.tables) {
|
||||
this.tables_by_id[table.id] = table;
|
||||
table.floor = floor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async setTable(table, orderUid=null) {
|
||||
this.table = table;
|
||||
try {
|
||||
this.loadingOrderState = true;
|
||||
await this._syncTableOrdersFromServer(table.id);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.loadingOrderState = false;
|
||||
const currentOrder = this.getTableOrders(table.id).find(order => orderUid ? order.uid === orderUid : !order.finalized);
|
||||
if (currentOrder) {
|
||||
this.set_order(currentOrder);
|
||||
} else {
|
||||
this.add_new_order();
|
||||
}
|
||||
}
|
||||
}
|
||||
getTableOrders(tableId) {
|
||||
return this.get_order_list().filter(order => order.tableId === tableId);
|
||||
}
|
||||
unsetTable() {
|
||||
this._syncTableOrdersToServer();
|
||||
this.table = null;
|
||||
this.set_order(null);
|
||||
}
|
||||
setCurrentOrderToTransfer() {
|
||||
this.orderToTransfer = this.selectedOrder;
|
||||
}
|
||||
async transferTable(table) {
|
||||
this.table = table;
|
||||
try {
|
||||
this.loadingOrderState = true;
|
||||
await this._syncTableOrdersFromServer(table.id);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.loadingOrderState = false;
|
||||
this.orderToTransfer.tableId = table.id;
|
||||
this.set_order(this.orderToTransfer);
|
||||
this.transferredOrdersSet.add(this.orderToTransfer);
|
||||
this.orderToTransfer = null;
|
||||
}
|
||||
}
|
||||
getCustomerCount(tableId) {
|
||||
const tableOrders = this.getTableOrders(tableId).filter(order => !order.finalized);
|
||||
return tableOrders.reduce((count, order) => count + order.getCustomerCount(), 0);
|
||||
}
|
||||
create_printer(config) {
|
||||
var url = config.proxy_ip || '';
|
||||
if(url.indexOf('//') < 0) {
|
||||
url = window.location.protocol + '//' + url;
|
||||
}
|
||||
if(url.indexOf(':', url.indexOf('//') + 2) < 0 && window.location.protocol !== 'https:') {
|
||||
url = url + ':8069';
|
||||
}
|
||||
return new Printer(url, this);
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(PosGlobalState, PosRestaurantPosGlobalState);
|
||||
|
||||
// New orders are now associated with the current table, if any.
|
||||
const PosRestaurantOrder = (Order) => class PosRestaurantOrder extends Order {
|
||||
constructor(obj, options) {
|
||||
super(...arguments);
|
||||
if (this.pos.config.module_pos_restaurant) {
|
||||
if (this.pos.config.iface_floorplan && !this.tableId && !options.json) {
|
||||
this.tableId = this.pos.table.id;
|
||||
}
|
||||
this.customerCount = this.customerCount || 1;
|
||||
}
|
||||
if (this.pos.config.iface_printers) {
|
||||
// printedResume will store the previous state of the orderlines (when there were no skip), it will
|
||||
// store all the orderlines even if the product are not printable. This way, when we add a new category in
|
||||
// the printers, the already added products of the newly added category are not printed.
|
||||
this.printedResume = owl.markRaw(this.printedResume || {}); // we don't wanna track it and re-render
|
||||
// no need to store this in the backend, we can just compute it once the order is fetched from clicking a table
|
||||
if (!this.printingChanges) {
|
||||
this._resetPrintingChanges();
|
||||
}
|
||||
}
|
||||
}
|
||||
//@override
|
||||
export_as_JSON() {
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
if (this.pos.config.module_pos_restaurant) {
|
||||
if (this.pos.config.iface_floorplan) {
|
||||
json.table_id = this.tableId
|
||||
}
|
||||
json.customer_count = this.customerCount;
|
||||
}
|
||||
if (this.pos.config.iface_printers) {
|
||||
json.multiprint_resume = JSON.stringify(this.printedResume);
|
||||
// so that it can be stored in local storage and be used when loading the pos in the floorscreen
|
||||
json.printing_changes = JSON.stringify(this.printingChanges);
|
||||
}
|
||||
return json;
|
||||
}
|
||||
//@override
|
||||
init_from_JSON(json) {
|
||||
super.init_from_JSON(...arguments);
|
||||
if (this.pos.config.module_pos_restaurant) {
|
||||
if (this.pos.config.iface_floorplan) {
|
||||
this.tableId = json.table_id;
|
||||
this.validation_date = moment.utc(json.creation_date).local().toDate();
|
||||
}
|
||||
this.customerCount = json.customer_count;
|
||||
}
|
||||
if (this.pos.config.iface_printers) {
|
||||
this.printedResume = json.multiprint_resume && JSON.parse(json.multiprint_resume);
|
||||
this.printingChanges = json.printing_changes && JSON.parse(json.printing_changes);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
export_for_printing() {
|
||||
const json = super.export_for_printing(...arguments);
|
||||
if (this.pos.config.module_pos_restaurant) {
|
||||
if (this.pos.config.iface_floorplan) {
|
||||
json.table = this.getTable().name;
|
||||
}
|
||||
json.customer_count = this.getCustomerCount();
|
||||
}
|
||||
return json;
|
||||
}
|
||||
_resetPrintingChanges() {
|
||||
this.printingChanges = { new:[], cancelled:[] };
|
||||
}
|
||||
/**
|
||||
* @returns {{ [productKey: string]: { product_id: number, name: string, note: string, quantity: number } }}
|
||||
*/
|
||||
_computePrintChanges() {
|
||||
const changes = {};
|
||||
|
||||
// If there's a new orderline, we add it otherwise we add the change if there's one
|
||||
this.orderlines.forEach(line => {
|
||||
if (!line.mp_skip) {
|
||||
const productId = line.get_product().id;
|
||||
const note = line.get_note();
|
||||
const productKey = `${productId} - ${line.get_full_product_name()} - ${note}`;
|
||||
const lineKey = `${line.uuid} - ${note}`;
|
||||
const quantityDiff = line.get_quantity() - (this.printedResume[lineKey] ? this.printedResume[lineKey]['quantity'] : 0);
|
||||
if (quantityDiff) {
|
||||
if (!changes[productKey]) {
|
||||
changes[productKey] = {
|
||||
product_id: productId,
|
||||
name: line.get_full_product_name(),
|
||||
note: note,
|
||||
quantity: quantityDiff,
|
||||
}
|
||||
} else {
|
||||
changes[productKey]['quantity'] += quantityDiff;
|
||||
}
|
||||
line.set_dirty(true);
|
||||
} else {
|
||||
line.set_dirty(false);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If there's an orderline that's not present anymore, we consider it as removed (even if note changed)
|
||||
for (const [lineKey, lineResume] of Object.entries(this.printedResume)) {
|
||||
if (!this._getPrintedLine(lineKey)) {
|
||||
const productKey = `${lineResume['product_id']} - ${lineResume['name']} - ${lineResume['note']}`;
|
||||
if (!changes[productKey]) {
|
||||
changes[productKey] = {
|
||||
product_id: lineResume['product_id'],
|
||||
name: lineResume['name'],
|
||||
note: lineResume['note'],
|
||||
quantity: -lineResume['quantity'],
|
||||
}
|
||||
} else {
|
||||
changes[productKey]['quantity'] -= lineResume['quantity'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
_getPrintingCategoriesChanges(categories) {
|
||||
return {
|
||||
new: this.printingChanges['new'].filter(change => this.pos.db.is_product_in_category(categories, change['product_id'])),
|
||||
cancelled: this.printingChanges['cancelled'].filter(change => this.pos.db.is_product_in_category(categories, change['product_id'])),
|
||||
}
|
||||
}
|
||||
_getPrintedLine(lineKey) {
|
||||
return this.orderlines.find(line => line.uuid === this.printedResume[lineKey]['line_uuid'] &&
|
||||
line.note === this.printedResume[lineKey]['note']);
|
||||
}
|
||||
getCustomerCount(){
|
||||
return this.customerCount;
|
||||
}
|
||||
setCustomerCount(count) {
|
||||
this.customerCount = Math.max(count,0);
|
||||
}
|
||||
getTable() {
|
||||
if (this.pos.config.iface_floorplan) {
|
||||
return this.pos.tables_by_id[this.tableId];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
updatePrintedResume(){
|
||||
// we first remove the removed orderlines
|
||||
for (const lineKey in this.printedResume) {
|
||||
if (!this._getPrintedLine(lineKey)) {
|
||||
delete this.printedResume[lineKey];
|
||||
}
|
||||
}
|
||||
// we then update the added orderline or product quantity change
|
||||
this.orderlines.forEach(line => {
|
||||
if (!line.mp_skip) {
|
||||
const note = line.get_note();
|
||||
const lineKey = `${line.uuid} - ${note}`;
|
||||
if (this.printedResume[lineKey]) {
|
||||
this.printedResume[lineKey]['quantity'] = line.get_quantity();
|
||||
} else {
|
||||
this.printedResume[lineKey] = {
|
||||
line_uuid: line.uuid,
|
||||
product_id: line.get_product().id,
|
||||
name: line.get_full_product_name(),
|
||||
note: note,
|
||||
quantity: line.get_quantity()
|
||||
}
|
||||
}
|
||||
line.set_dirty(false);
|
||||
}
|
||||
});
|
||||
this._resetPrintingChanges();
|
||||
}
|
||||
updateChangesToPrint() {
|
||||
const changes = this._computePrintChanges(); // it's possible to have a change's quantity of 0
|
||||
// we thoroughly parse the changes we just computed to properly separate them into two
|
||||
const toAdd = [];
|
||||
const toRemove = [];
|
||||
|
||||
for (const lineChange of Object.values(changes)) {
|
||||
if (lineChange['quantity'] > 0) {
|
||||
toAdd.push(lineChange);
|
||||
} else if (lineChange['quantity'] < 0) {
|
||||
lineChange['quantity'] *= -1; // we change the sign because that's how it is
|
||||
toRemove.push(lineChange);
|
||||
}
|
||||
}
|
||||
|
||||
this.printingChanges = { new: toAdd, cancelled: toRemove };
|
||||
}
|
||||
hasChangesToPrint(){
|
||||
for (const printer of this.pos.unwatched.printers) {
|
||||
const changes = this._getPrintingCategoriesChanges(printer.config.product_categories_ids);
|
||||
if (changes['new'].length > 0 || changes['cancelled'].length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
hasSkippedChanges() {
|
||||
var orderlines = this.get_orderlines();
|
||||
for (var i = 0; i < orderlines.length; i++) {
|
||||
if (orderlines[i].mp_skip) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async printChanges(){
|
||||
let isPrintSuccessful = true;
|
||||
const d = new Date();
|
||||
let hours = '' + d.getHours();
|
||||
hours = hours.length < 2 ? ('0' + hours) : hours;
|
||||
let minutes = '' + d.getMinutes();
|
||||
minutes = minutes.length < 2 ? ('0' + minutes) : minutes;
|
||||
|
||||
|
||||
for (const printer of this.pos.unwatched.printers) {
|
||||
const changes = this._getPrintingCategoriesChanges(printer.config.product_categories_ids);
|
||||
if (changes['new'].length > 0 || changes['cancelled'].length > 0) {
|
||||
const printingChanges = {
|
||||
new: changes['new'],
|
||||
cancelled: changes['cancelled'],
|
||||
table_name: this.pos.config.iface_floorplan ? this.getTable().name : false,
|
||||
floor_name: this.pos.config.iface_floorplan ? this.getTable().floor.name : false,
|
||||
name: this.name || 'unknown order',
|
||||
time: {
|
||||
hours,
|
||||
minutes,
|
||||
},
|
||||
};
|
||||
const receipt = QWeb.render('OrderChangeReceipt', { changes: printingChanges });
|
||||
const result = await printer.print_receipt(receipt);
|
||||
if (!result.successful) {
|
||||
isPrintSuccessful = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isPrintSuccessful;
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(Order, PosRestaurantOrder);
|
||||
|
||||
|
||||
const PosRestaurantOrderline = (Orderline) => class PosRestaurantOrderline extends Orderline {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.note = this.note || "";
|
||||
if (this.pos.config.iface_printers) {
|
||||
this.uuid = this.uuid || uuidv4();
|
||||
// mp dirty is true if this orderline has changed since the last kitchen print
|
||||
this.mp_dirty = false
|
||||
if (!this.mp_skip) {
|
||||
// mp_skip is true if the cashier want this orderline
|
||||
// not to be sent to the kitchen
|
||||
this.mp_skip = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//@override
|
||||
can_be_merged_with(orderline) {
|
||||
if (orderline.get_note() !== this.get_note()) {
|
||||
return false;
|
||||
} else {
|
||||
return (!this.mp_skip) && (!orderline.mp_skip) && super.can_be_merged_with(...arguments);
|
||||
}
|
||||
}
|
||||
//@override
|
||||
clone(){
|
||||
const orderline = super.clone(...arguments);
|
||||
orderline.note = this.note;
|
||||
return orderline;
|
||||
}
|
||||
//@override
|
||||
export_as_JSON(){
|
||||
const json = super.export_as_JSON(...arguments);
|
||||
json.note = this.note;
|
||||
if (this.pos.config.iface_printers) {
|
||||
json.uuid = this.uuid;
|
||||
json.mp_skip = this.mp_skip;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
//@override
|
||||
init_from_JSON(json){
|
||||
super.init_from_JSON(...arguments);
|
||||
this.note = json.note;
|
||||
if (this.pos.config.iface_printers) {
|
||||
this.uuid = json.uuid;
|
||||
this.mp_skip = json.mp_skip;
|
||||
}
|
||||
}
|
||||
set_note(note){
|
||||
this.note = note;
|
||||
}
|
||||
get_note(){
|
||||
return this.note;
|
||||
}
|
||||
set_skip(skip) {
|
||||
if (this.mp_dirty && skip && !this.mp_skip) {
|
||||
this.mp_skip = true;
|
||||
}
|
||||
if (this.mp_skip && !skip) {
|
||||
this.mp_skip = false;
|
||||
}
|
||||
}
|
||||
set_dirty(dirty) {
|
||||
if (this.printable()) {
|
||||
this.mp_dirty = dirty;
|
||||
}
|
||||
}
|
||||
get_line_diff_hash(){
|
||||
if (this.get_note()) {
|
||||
return this.id + '|' + this.get_note();
|
||||
} else {
|
||||
return '' + this.id;
|
||||
}
|
||||
}
|
||||
// can this orderline be potentially printed ?
|
||||
printable() {
|
||||
return this.pos.db.is_product_in_category(this.pos.printers_category_ids_set, this.get_product().id);
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(Orderline, PosRestaurantOrderline);
|
||||
|
||||
const PosRestaurantPayment = (Payment) => class PosRestaurantPayment extends Payment {
|
||||
/**
|
||||
* Override this method to be able to show the 'Adjust Authorisation' button
|
||||
* on a validated payment_line and to show the tip screen which allow
|
||||
* tipping even after payment. By default, this returns true for all
|
||||
* non-cash payment.
|
||||
*/
|
||||
canBeAdjusted() {
|
||||
if (this.payment_method.payment_terminal) {
|
||||
return this.payment_method.payment_terminal.canBeAdjusted(this.cid);
|
||||
}
|
||||
return !this.payment_method.is_cash_count;
|
||||
}
|
||||
}
|
||||
Registries.Model.extend(Payment, PosRestaurantPayment);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
odoo.define('pos_restaurant.PaymentInterface', function (require) {
|
||||
"use strict";
|
||||
|
||||
var PaymentInterface = require('point_of_sale.PaymentInterface');
|
||||
|
||||
PaymentInterface.include({
|
||||
/**
|
||||
* Return true if the amount that was authorized can be modified,
|
||||
* false otherwise
|
||||
* @param {string} cid - The id of the paymentline
|
||||
*/
|
||||
canBeAdjusted(cid) {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called when the amount authorized by a payment request should
|
||||
* be adjusted to account for a new order line, it can only be called if
|
||||
* canBeAdjusted returns True
|
||||
* @param {string} cid - The id of the paymentline
|
||||
*/
|
||||
send_payment_adjust: function (cid) {},
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,586 @@
|
|||
/* --- Restaurant Specific CSS --- */
|
||||
|
||||
.screen .screen-content-flexbox {
|
||||
margin: 0px auto;
|
||||
text-align: left;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: -webkit-flex;
|
||||
-webkit-flex-flow: column nowrap;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
/* ------ FLOOR SELECTOR ------- */
|
||||
|
||||
.floor-selector {
|
||||
line-height: 48px;
|
||||
font-size: 18px;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.floor-selector .button {
|
||||
cursor: pointer;
|
||||
border-left: solid 1px $gray-400;
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.floor-selector .button:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
.floor-selector .button.active {
|
||||
background: $o-navbar-badge-bg;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ------ FLOOR MAP ------- */
|
||||
|
||||
.floor-map {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
box-shadow: 0px 6px 0px -3px rgba(0,0,0,0.07) inset;
|
||||
background: #D8D7D7;
|
||||
background-repeat: no-repeat;
|
||||
overflow: auto;
|
||||
background-size: cover;
|
||||
transition: all 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.floor-map .tables {
|
||||
position: relative;
|
||||
--scale: 1;
|
||||
transform: scale(var(--scale));
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
.floor-map .tables {
|
||||
max-width: 1024px;
|
||||
margin: auto;
|
||||
max-height: 540px;
|
||||
border-radius: 0px 0px 6px 6px;
|
||||
border: dashed 2px rgba(0,0,0,0.1);
|
||||
border-top: none;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.floor-map .table{
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
background: rgb(53, 211, 116);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0px 3px rgba(0,0,0,0.07);
|
||||
transition: background, background-color 300ms ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
.floor-map .table .table-cover {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
border-radius: 0px 0px 3px 3px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
.floor-map .table .table-cover.full {
|
||||
border-radius: 3px 3px 3px 3px;
|
||||
}
|
||||
.floor-map .table .table-seats {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
margin-left: -16px;
|
||||
margin-bottom: 4px;
|
||||
background: black;
|
||||
color: white;
|
||||
opacity: 0.2;
|
||||
z-index: 3;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.floor-map .table .label {
|
||||
display: block;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
bottom: 5px;
|
||||
}
|
||||
.floor-map .table.selected {
|
||||
outline: solid rgba(255,255,255,0.3);
|
||||
cursor: move;
|
||||
z-index: 50;
|
||||
}
|
||||
.floor-map .table.selected .table-seats {
|
||||
margin-left: -12px;
|
||||
}
|
||||
.floor-map .edit-button.editing {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 20px;
|
||||
margin: 8px;
|
||||
line-height: 32px;
|
||||
width: 32px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
border: solid 1px rgba(0,0,0,0.2);
|
||||
}
|
||||
.floor-map .edit-button.editing.active {
|
||||
background: #444;
|
||||
border-color: transparent;
|
||||
color: white;
|
||||
}
|
||||
.floor-map .edit-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 40px;
|
||||
margin: 8px;
|
||||
line-height: 34px;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
z-index: 100;
|
||||
}
|
||||
.floor-map .edit-bar .edit-button {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
margin-right: -4px;
|
||||
border-right: solid 1px rgba(0,0,0,0.2);
|
||||
transition: all 150ms linear;
|
||||
}
|
||||
.floor-map .edit-bar .edit-button.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
.floor-map .edit-bar .edit-button.disabled > * {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.floor-map .edit-bar .color-picker {
|
||||
position: absolute;
|
||||
left: 36px;
|
||||
top: 40px;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 3px;
|
||||
z-index: 100;
|
||||
}
|
||||
.floor-map .edit-bar .color-picker .color {
|
||||
display: block;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: gray;
|
||||
}
|
||||
.floor-map .edit-bar .color-picker .color.tl { border-top-left-radius: 3px; }
|
||||
.floor-map .edit-bar .color-picker .color.tr { border-top-right-radius: 3px; }
|
||||
.floor-map .edit-bar .color-picker .color.bl { border-bottom-left-radius: 3px; }
|
||||
.floor-map .edit-bar .color-picker .color.br { border-bottom-right-radius: 3px; }
|
||||
|
||||
.floor-map .edit-bar .close-picker {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
margin-left: -16px;
|
||||
margin-bottom: -16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
border-radius: 16px;
|
||||
background: black;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.floor-map .edit-bar .edit-button:last-child {
|
||||
margin-right: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.floor-map .table.selected .table-handle {
|
||||
padding: 0px;
|
||||
position: absolute;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 24px;
|
||||
background: white;
|
||||
box-shadow: 0px 2px 3px rgba(0,0,0,0.2);
|
||||
/* See o-grab-cursor mixin */
|
||||
cursor: url(/web/static/img/openhand.cur), grab;
|
||||
transition: all 150ms linear;
|
||||
z-index: 100;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.floor-map .table.selected .table-handle:hover {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
.floor-map .table .table-handle.top { top: 0; }
|
||||
.floor-map .table .table-handle.bottom { top: 100%; }
|
||||
.floor-map .table .table-handle.left { left: 0; }
|
||||
.floor-map .table .table-handle.right { left: 100%; }
|
||||
|
||||
.floor-map .table .order-count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
background: black;
|
||||
width: 20px;
|
||||
margin-top: 1px;
|
||||
margin-left: -10px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
z-index: 10;
|
||||
}
|
||||
.floor-map .table .order-count.notify-printing {
|
||||
background: red;
|
||||
}
|
||||
.floor-map .table .order-count.notify-skipped {
|
||||
background: blue;
|
||||
}
|
||||
|
||||
.floor-map .empty-floor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
width: 400px;
|
||||
height: 40px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.floor-map .empty-floor i {
|
||||
display: inline-block;
|
||||
padding: 6px 7px 3px;
|
||||
margin: 0px 3px;
|
||||
background: rgba(255,255,255,0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
/* ------ FLOOR BUTTON IN THE ORDER SELECTOR ------- */
|
||||
|
||||
.pos .order-button.floor-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $o-navbar-badge-bg;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
overflow-wrap: anywhere;
|
||||
text-overflow: hidden;
|
||||
}
|
||||
.pos .order-button.floor-button .table-name {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.pos .order-button.floor-button .fa{
|
||||
font-size: 24px;
|
||||
line-height: 42px;
|
||||
}
|
||||
/* ------ ORDER LINE STATUS ------- */
|
||||
|
||||
.pos .order .orderline.dirty {
|
||||
border-left: solid 6px #6EC89B;
|
||||
color: #6EC89B;
|
||||
padding-left: 9px;
|
||||
}
|
||||
.pos .order .orderline.skip {
|
||||
border-left: solid 6px #7F82AC;
|
||||
color: #7F82AC;
|
||||
padding-left: 9px;
|
||||
}
|
||||
|
||||
/* ------ SPLIT BILL SCREEN ------- */
|
||||
|
||||
.splitbill-screen.screen .contents {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 0px auto;
|
||||
max-width: 1024px;
|
||||
border-left: dashed 1px rgb(215,215,215);
|
||||
border-right: dashed 1px rgb(215,215,215);
|
||||
height: 100%;
|
||||
}
|
||||
.splitbill-screen.screen .main {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
/* take the remaining vertical space */
|
||||
flex: 1;
|
||||
/* do not capture overflow in this element */
|
||||
overflow: hidden;
|
||||
}
|
||||
.splitbill-screen.screen .main .lines {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
/* show scrollbar inside this element if its content overflows */
|
||||
overflow-y: auto;
|
||||
}
|
||||
.splitbill-screen.screen .main .controls {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.splitbill-screen.screen .pay-button {
|
||||
margin: 16px;
|
||||
}
|
||||
.splitbill-screen.screen .pay-button .button {
|
||||
background: $gray-200;
|
||||
line-height: 74px;
|
||||
font-size: 16px;
|
||||
border: solid 1px rgb(202, 202, 202);
|
||||
border-top-width: 0px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.splitbill-screen.screen .pay-button .button:first-child {
|
||||
border-top-width: 1px;
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
.splitbill-screen.screen .pay-button .button:last-child {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
.splitbill-screen.screen .pay-button .button:active {
|
||||
background: black;
|
||||
border-color: black;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.splitbill-screen.screen .main .lines {
|
||||
border-right: dashed 1px rgb(215,215,215);
|
||||
}
|
||||
|
||||
.splitbill-screen.screen .main .controls {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* ------ NARROWER SCREEN ------ */
|
||||
@media screen and (max-width: 768px) {
|
||||
.splitbill-screen.screen .main {
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.splitbill-screen.screen .main .controls {
|
||||
border-top: dashed 1px rgb(215,215,215);
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tip-screen .tip-options .total-amount {
|
||||
text-align: center;
|
||||
font-size: x-large;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tip-screen .tip-amount-options {
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
margin-left: 4rem;
|
||||
margin-right: 4rem;
|
||||
}
|
||||
|
||||
.tip-screen .percentage-amounts {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tip-screen .tip-options .button {
|
||||
background: #FAFAFA;
|
||||
border: solid 1px rgb(209, 209, 209);
|
||||
border-radius: 3px;
|
||||
flex: 1;
|
||||
margin: 1rem;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tip-screen .tip-options .button .percentage {
|
||||
text-align: center;
|
||||
font-size: xx-large;
|
||||
color: #2196F3;
|
||||
padding-top: 4rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.tip-screen .tip-options .button .amount {
|
||||
text-align: center;
|
||||
font-size: large;
|
||||
color: #757575;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .item {
|
||||
font-size: x-large;
|
||||
color: #2196F3;
|
||||
margin: 1rem;
|
||||
flex: 1
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .label {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .input {
|
||||
background: #FAFAFA;
|
||||
border: solid 1px rgb(209, 209, 209);
|
||||
border-radius: 3px;
|
||||
margin: 1rem;
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .input .currency {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .input input {
|
||||
border: none;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
font-size: x-large;
|
||||
text-align: right;
|
||||
margin-left: 1rem;
|
||||
margin-right: 3rem;
|
||||
height: 100%;
|
||||
width: calc(100% - 4rem);
|
||||
background: #FAFAFA;
|
||||
color: #2196F3;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .input input:focus {
|
||||
border: none;
|
||||
padding: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tip-screen .custom-amount-form .add {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tip-screen .no-tip {
|
||||
display: flex;
|
||||
color: #2196F3;
|
||||
font-size: x-large;
|
||||
}
|
||||
|
||||
.tip-screen .no-tip .button {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tip-screen .pos-receipt-container {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.tip-screen .pos-receipt-container {
|
||||
display: block;
|
||||
}
|
||||
.tip-screen .pos-receipt-container * {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-receipt .tip-form {
|
||||
padding-top: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.pos-receipt .tip-form .title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pos-receipt .tip-form > div {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.pos-receipt .tip-form .option {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pos-receipt .tip-form .percentage-options {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.ticket-screen .tip-cell {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ticket-screen .tip-cell input {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
font-size: medium;
|
||||
color: #555555;
|
||||
padding-bottom: 5px;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.ticket-screen .tip-cell input:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: solid 1px #555555;
|
||||
}
|
||||
|
||||
.multiprint-flex {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.multiprint-flex .product-quantity {
|
||||
margin-right: 50px;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="Chrome" t-inherit="point_of_sale.Chrome" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('status-buttons')]" position="before">
|
||||
<div class="back-to-floor-portal"/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="BackToFloorButton" owl="1">
|
||||
<span
|
||||
t-if="hasTable"
|
||||
class="order-button floor-button"
|
||||
t-att-class="{ oe_hidden: props.mobileSearchBarIsShown }"
|
||||
t-on-click="backToFloorScreen"
|
||||
>
|
||||
<i class="fa fa-angle-double-left" role="img" aria-label="Back to floor" title="Back to floor" />
|
||||
<t t-if="env.isMobile">
|
||||
<span class="table-name" t-esc="table.name"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span t-esc="floor.name" /><span class="table-name">(<t t-esc="table.name" />)</span>
|
||||
</t>
|
||||
</span>
|
||||
<span t-else=""></span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="Resizeable" owl="1">
|
||||
<t t-slot="default"></t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="BillScreen" owl="1">
|
||||
<div class="receipt-screen screen">
|
||||
<div class="screen-content">
|
||||
<div class="top-content">
|
||||
<div class="top-content-center">
|
||||
<h1>Bill Printing</h1>
|
||||
</div>
|
||||
<span class="button next highlight" t-on-click="confirm">
|
||||
<span>Ok</span>
|
||||
<span> </span>
|
||||
<i class="fa fa-angle-double-right"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="centered-content">
|
||||
<div class="button print" t-on-click="printReceipt">
|
||||
<i class="fa fa-print"></i>
|
||||
<span> </span>
|
||||
<span>Print</span>
|
||||
</div>
|
||||
<div class="pos-receipt-container" t-ref="order-receipt">
|
||||
<OrderReceipt order="currentOrder" isBill="true"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="EditBar" owl="1">
|
||||
<div class="edit-bar" t-attf-style="top:{{props.floorMapScrollTop}}px;">
|
||||
<span class="edit-button" t-on-click.stop="props.createTable">
|
||||
<i class="fa fa-plus" role="img" aria-label="Add" title="Add"></i>
|
||||
</span>
|
||||
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.duplicateTable">
|
||||
<i class="fa fa-files-o" role="img" aria-label="Duplicate" title="Duplicate"></i>
|
||||
</span>
|
||||
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.renameTable">
|
||||
<i class="fa fa-font" role="img" aria-label="Rename" title="Rename"></i>
|
||||
</span>
|
||||
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeSeatsNum">
|
||||
<i class="fa fa-user" role="img" aria-label="Seats" title="Seats"></i>
|
||||
</span>
|
||||
<span class="edit-button" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.changeShape">
|
||||
<span t-if="!props.selectedTable or props.selectedTable.shape == 'square'" class="button-option square">
|
||||
<i class="fa fa-square-o" role="img" aria-label="Square Shape" title="Square Shape"></i>
|
||||
</span>
|
||||
<span t-else="" class="button-option round">
|
||||
<i class="fa fa-circle-o" role="img" aria-label="Round Shape" title="Round Shape"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="edit-button" t-on-click.stop="() => { state.isColorPicker = !state.isColorPicker }">
|
||||
<i class="fa fa-tint" role="img" aria-label="Tint" title="Tint"></i>
|
||||
</span>
|
||||
<div t-if="state.isColorPicker and props.selectedTable" class="color-picker fg-picker">
|
||||
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
|
||||
<i class="fa fa-times" />
|
||||
</div>
|
||||
<span class="color tl" style="background-color:#EB6D6D" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setTableColor('#EB6D6D')" />
|
||||
<span class="color" style="background-color:#35D374" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setTableColor('#35D374')" />
|
||||
<span class="color tr" style="background-color:#6C6DEC" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setTableColor('#6C6DEC')" />
|
||||
<span class="color" style="background-color:#EBBF6D" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setTableColor('#EBBF6D')" />
|
||||
<span class="color" style="background-color:#EBEC6D" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setTableColor('#EBEC6D')" />
|
||||
<span class="color" style="background-color:#AC6DAD" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setTableColor('#AC6DAD')" />
|
||||
<span class="color bl" style="background-color:#6C6D6D" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setTableColor('#6C6D6D')" />
|
||||
<span class="color" style="background-color:#ACADAD" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setTableColor('#ACADAD')" />
|
||||
<span class="color br" style="background-color:#4ED2BE" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setTableColor('#4ED2BE')" />
|
||||
</div>
|
||||
<div t-if="state.isColorPicker and !props.selectedTable" class="color-picker bg-picker">
|
||||
<div class="close-picker" title="Close" role="img" aria-label="Close" t-on-click.stop="() => { state.isColorPicker = false; }">
|
||||
<i class="fa fa-times" />
|
||||
</div>
|
||||
<span class="color tl" style="background-color:rgb(244, 149, 149)" role="img" aria-label="Red" title="Red" t-on-click.stop="() => props.setFloorColor('rgb(244, 149, 149)')" />
|
||||
<span class="color" style="background-color:rgb(130, 233, 171)" role="img" aria-label="Green" title="Green" t-on-click.stop="() => props.setFloorColor('rgb(130, 233, 171)')" />
|
||||
<span class="color tr" style="background-color:rgb(136, 137, 242)" role="img" aria-label="Blue" title="Blue" t-on-click.stop="() => props.setFloorColor('rgb(136, 137, 242)')" />
|
||||
<span class="color" style="background-color:rgb(255, 214, 136)" role="img" aria-label="Orange" title="Orange" t-on-click.stop="() => props.setFloorColor('rgb(255, 214, 136)')" />
|
||||
<span class="color" style="background-color:rgb(254, 255, 154)" role="img" aria-label="Yellow" title="Yellow" t-on-click.stop="() => props.setFloorColor('rgb(254, 255, 154)')" />
|
||||
<span class="color" style="background-color:rgb(209, 171, 210)" role="img" aria-label="Purple" title="Purple" t-on-click.stop="() => props.setFloorColor('rgb(209, 171, 210)')" />
|
||||
<span class="color bl" style="background-color:rgb(75, 75, 75)" role="img" aria-label="Grey" title="Grey" t-on-click.stop="() => props.setFloorColor('rgb(75, 75, 75)')" />
|
||||
<span class="color" style="background-color:rgb(210, 210, 210)" role="img" aria-label="Light grey" title="Light grey" t-on-click.stop="() => props.setFloorColor('rgb(210, 210, 210)')" />
|
||||
<span class="color br" style="background-color:rgb(127, 221, 236)" role="img" aria-label="Turquoise" title="Turquoise" t-on-click.stop="() => props.setFloorColor('rgb(127, 221, 236)')" />
|
||||
</div>
|
||||
<span class="edit-button trash" t-att-class="{ disabled: !props.selectedTable }" t-on-click.stop="props.deleteTable">
|
||||
<i class="fa fa-trash" role="img" aria-label="Delete" title="Delete"></i>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="EditableTable" owl="1">
|
||||
<Draggable limitArea="'.floor-map'">
|
||||
<Resizeable limitArea="'.floor-map'">
|
||||
<div class="table selected" t-on-click.stop="">
|
||||
<span class="label drag-handle">
|
||||
<t t-esc="props.table.name" />
|
||||
</span>
|
||||
<span class="table-seats">
|
||||
<t t-esc="props.table.seats" />
|
||||
</span>
|
||||
<t t-if="props.table.shape === 'round'">
|
||||
<div class="table-handle top resize-handle-n"></div>
|
||||
<div class="table-handle bottom resize-handle-s"></div>
|
||||
<div class="table-handle left resize-handle-w"></div>
|
||||
<div class="table-handle right resize-handle-e"></div>
|
||||
</t>
|
||||
<t t-if="props.table.shape === 'square'">
|
||||
<span class='table-handle top right resize-handle-ne'></span>
|
||||
<span class='table-handle top left resize-handle-nw'></span>
|
||||
<span class='table-handle bottom right resize-handle-se'></span>
|
||||
<span class='table-handle bottom left resize-handle-sw'></span>
|
||||
</t>
|
||||
</div>
|
||||
</Resizeable>
|
||||
</Draggable>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="FloorScreen" owl="1">
|
||||
<div class="floor-screen screen">
|
||||
<div class="screen-content-flexbox">
|
||||
<t t-if="env.pos.floors.length > 1">
|
||||
<div class="floor-selector">
|
||||
<t t-foreach="env.pos.floors" t-as="floor" t-key="floor.id">
|
||||
<span class="button button-floor" t-att-class="{ active: floor.id === state.selectedFloorId }" t-on-click="() => this.selectFloor(floor)">
|
||||
<t t-esc="floor.name" />
|
||||
</span>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<div
|
||||
t-on-click="_onDeselectTable"
|
||||
t-on-touchstart="_onPinchStart"
|
||||
t-on-touchmove="_onPinchMove"
|
||||
t-on-touchend="_onPinchEnd"
|
||||
class="floor-map"
|
||||
t-ref="floor-map-ref"
|
||||
>
|
||||
<div t-if="isFloorEmpty" class="empty-floor">
|
||||
<span>This floor has no tables yet, use the </span>
|
||||
<i class="fa fa-plus" role="img" aria-label="Add button" title="Add button"></i>
|
||||
<span> button in the editing toolbar to create new tables.</span>
|
||||
</div>
|
||||
<div t-else="" class="tables">
|
||||
<t t-foreach="activeTables" t-as="table" t-key="table.id">
|
||||
<TableWidget t-if="table.id !== state.selectedTableId" onClick.bind="onSelectTable" table="table" />
|
||||
<EditableTable t-else="" table="table" onSaveTable.bind="onSaveTable" />
|
||||
</t>
|
||||
</div>
|
||||
<span t-if="env.pos.user.role == 'manager'" class="edit-button editing" t-att-class="{ active: state.isEditMode }" t-on-click.stop="toggleEditMode"
|
||||
t-attf-style="top:{{state.floorMapScrollTop}}px;">
|
||||
<i class="fa fa-pencil" role="img" aria-label="Edit" title="Edit"></i>
|
||||
</span>
|
||||
<EditBar t-if="state.isEditMode" selectedTable="selectedTable" floorMapScrollTop="state.floorMapScrollTop"
|
||||
createTable.bind="createTable" duplicateTable.bind="duplicateTable" renameTable.bind="renameTable"
|
||||
changeSeatsNum.bind="changeSeatsNum" changeShape.bind="changeShape" setTableColor.bind="setTableColor"
|
||||
setFloorColor.bind="setFloorColor" deleteTable.bind="deleteTable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="TableWidget" owl="1">
|
||||
<div class="table" t-on-click.stop="() => props.onClick(props.table)">
|
||||
<span class="table-cover" t-att-class="{ full: fill >= 1 }"></span>
|
||||
<span t-att-class="orderCountClass" t-att-hidden="orderCount === 0">
|
||||
<t t-esc="orderCount" />
|
||||
</span>
|
||||
<span class="label">
|
||||
<t t-esc="props.table.name" />
|
||||
</span>
|
||||
<span class="table-seats">
|
||||
<t t-esc="customerCountDisplay" />
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="PaymentScreen" t-inherit="point_of_sale.PaymentScreen" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('payment-screen')]" position="inside">
|
||||
<t t-portal="'.pos .back-to-floor-portal'" position="before">
|
||||
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
|
||||
</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('button') and hasclass('next')]" position="attributes">
|
||||
<attribute name="t-att-hidden">env.pos.config.set_tip_after_payment and !currentOrder.is_paid()</attribute>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('button') and hasclass('back')]/span[hasclass('back_text')]" position="replace">
|
||||
<t t-if="env.pos.config.set_tip_after_payment and currentOrder.is_paid()">
|
||||
<span class="back_text">Keep Open</span>
|
||||
</t>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[hasclass('button') and hasclass('next')]/span[hasclass('next_text')]" position="replace">
|
||||
<t t-if="env.pos.config.set_tip_after_payment and currentOrder.is_paid()">
|
||||
<span class="back_text">Close Tab</span>
|
||||
</t>
|
||||
<t t-else="">$0</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="PaymentScreenPaymentLines" t-inherit="point_of_sale.PaymentScreenPaymentLines" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('send_payment_reversal')]/.." position="replace">
|
||||
<t t-if="line.canBeAdjusted() && line.order.get_total_paid() < line.order.get_total_with_tax()">
|
||||
<div class="button send_adjust_amount" title="Adjust Amount" t-on-click="() => this.trigger('send-payment-adjust', line)">
|
||||
Adjust Amount
|
||||
</div>
|
||||
</t>
|
||||
<t t-elif="line.can_be_reversed">
|
||||
<div class="button send_payment_reversal" title="Reverse Payment" t-on-click="() => this.trigger('send-payment-reverse', line)">
|
||||
Reverse
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderlineNoteButton" owl="1">
|
||||
<div class="control-button">
|
||||
<i class="fa fa-tag" />
|
||||
<span> </span>
|
||||
<span>Internal Note</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="PrintBillButton" owl="1">
|
||||
<span class="control-button order-printbill">
|
||||
<i class="fa fa-print"></i>
|
||||
<span> </span>
|
||||
<span>Bill</span>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SplitBillButton" owl="1">
|
||||
<span class="control-button order-split">
|
||||
<i class="fa fa-files-o"></i>
|
||||
<span> </span>
|
||||
<span>Split</span>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SubmitOrderButton" owl="1">
|
||||
<span class="control-button" t-att-class="addedClasses" t-on-click="_onClick">
|
||||
<i class="fa fa-cutlery"></i>
|
||||
<span> </span>
|
||||
<span>Order</span>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="TableGuestsButton" owl="1">
|
||||
<div class="control-button">
|
||||
<span class="control-button-number">
|
||||
<t t-esc="nGuests" />
|
||||
</span>
|
||||
<span> </span>
|
||||
<span>Guests</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="TransferOrderButton" owl="1">
|
||||
<div class="control-button">
|
||||
<i class="fa fa-arrow-right" />
|
||||
<span> </span>
|
||||
<span>Transfer</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="Orderline" t-inherit="point_of_sale.Orderline" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//ul[hasclass('info-list')]" position="inside">
|
||||
<t t-if="props.line.get_note()">
|
||||
<li class="info orderline-note">
|
||||
<i class="fa fa-tag" role="img" aria-label="Note" title="Note"/>
|
||||
<t t-esc="props.line.get_note()" />
|
||||
</li>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="ProductScreen" t-inherit="point_of_sale.ProductScreen" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('product-screen')]" position="inside">
|
||||
<t t-portal="'.pos .back-to-floor-portal'" position="before">
|
||||
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderReceipt" t-inherit="point_of_sale.OrderReceipt" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('pos-receipt-order-data')]" position="inside">
|
||||
<t t-if="props.isBill">
|
||||
<div>PRO FORMA</div>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('receipt-change')]" position="attributes">
|
||||
<attribute name="t-if">!props.isBill</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('cashier')]" position="after">
|
||||
<t t-if="receipt.table">
|
||||
at table <t t-esc="receipt.table" />
|
||||
</t>
|
||||
<t t-if="receipt.table and receipt.customer_count">
|
||||
<div>Guests: <t t-esc="receipt.customer_count" /></div>
|
||||
</t>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('before-footer')]" position="after">
|
||||
<t t-if="props.isBill and env.pos.config.set_tip_after_payment">
|
||||
<div class="tip-form">
|
||||
<div class="title">For convenience, we are providing the following gratuity calculations:</div>
|
||||
<div class="percentage-options">
|
||||
<div class="option">
|
||||
<div>15%</div>
|
||||
<div class="amount">
|
||||
<t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.15)"></t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div>20%</div>
|
||||
<div class="amount">
|
||||
<t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.20)"></t>
|
||||
</div>
|
||||
</div>
|
||||
<div class="option">
|
||||
<div>25%</div>
|
||||
<div class="amount">
|
||||
<t t-esc="env.pos.format_currency(receipt.total_with_tax * 0.25)"></t>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="ReceiptScreen" t-inherit="point_of_sale.ReceiptScreen" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('receipt-screen')]" position="inside">
|
||||
<t t-portal="'.pos .back-to-floor-portal'" position="before">
|
||||
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"
|
||||
onClick.bind="onBackToFloorButtonClick"
|
||||
/>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SplitBillScreen" owl="1">
|
||||
<div class="splitbill-screen screen">
|
||||
<div class="contents">
|
||||
<div class="top-content">
|
||||
<span class="button back" t-on-click="back">
|
||||
<i class="fa fa-angle-double-left"></i>
|
||||
<span> </span>
|
||||
<span>Back</span>
|
||||
</span>
|
||||
<div class="top-content-center">
|
||||
<h1>Bill Splitting</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="newOrder" class="main">
|
||||
<div class="lines">
|
||||
<div class="order">
|
||||
<ul class="orderlines">
|
||||
<t t-foreach="orderlines" t-as="line" t-key="line.cid">
|
||||
<SplitOrderline line="line" split="splitlines[line.id]" />
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="order-info">
|
||||
<span class="subtotal">
|
||||
<t t-esc="env.pos.format_currency(newOrder.get_subtotal())" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="pay-button">
|
||||
<div class="button" t-on-click="proceed">
|
||||
<i class="fa fa-chevron-right" />
|
||||
<span> </span>
|
||||
<span>Payment</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="SplitOrderline" owl="1">
|
||||
<li class="orderline" t-att-class="{ selected: isSelected, partially: props.split.quantity !== props.line.get_quantity() }">
|
||||
<span class="product-name">
|
||||
<t t-esc="props.line.get_product().display_name" />
|
||||
</span>
|
||||
<span class="price">
|
||||
<t t-esc="env.pos.format_currency(props.line.get_display_price())" />
|
||||
</span>
|
||||
<ul class="info-list">
|
||||
<t t-if="props.line.get_quantity_str() !== '1'">
|
||||
<li class="info">
|
||||
<t t-if="isSelected and props.line.get_unit().is_pos_groupable">
|
||||
<em class="big">
|
||||
<t t-esc="props.split.quantity" />
|
||||
</em>
|
||||
/
|
||||
<t t-esc="props.line.get_quantity_str()" />
|
||||
</t>
|
||||
<t t-if="!(isSelected and props.line.get_unit().is_pos_groupable)">
|
||||
<em>
|
||||
<t t-esc="props.line.get_quantity_str()" />
|
||||
</em>
|
||||
</t>
|
||||
<t t-esc="props.line.get_unit().name" />
|
||||
at
|
||||
<t t-esc="env.pos.format_currency(props.line.get_unit_price())" />
|
||||
/
|
||||
<t t-esc="props.line.get_unit().name" />
|
||||
</li>
|
||||
</t>
|
||||
<t t-if="props.line.get_discount_str() !== '0'">
|
||||
<li class="info">
|
||||
<span>With a </span>
|
||||
<em>
|
||||
<t t-esc="props.line.get_discount_str()" />
|
||||
<span>%</span>
|
||||
</em>
|
||||
<span> discount</span>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
</li>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="TicketScreen" t-inherit="point_of_sale.TicketScreen" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('header-row')]//div[@name='delete']" position="before">
|
||||
<div t-if="env.pos.config.iface_floorplan" class="col" name="table">Table</div>
|
||||
<div t-if="_state.ui.filter == 'TIPPING'" class="col end narrow" name="tip">Tip</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('order-row')]//div[@name='delete']" position="before">
|
||||
<div t-if="env.pos.config.iface_floorplan" class="col" name="table">
|
||||
<t t-if="order.tableId">
|
||||
<div t-if="env.isMobile">Table</div>
|
||||
<div><t t-esc="getTable(order)"></t></div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="_state.ui.filter == 'TIPPING'" class="col end narrow" name="tip">
|
||||
<div t-if="env.isMobile">Tip</div>
|
||||
<div><TipCell order="order" /></div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('buttons')]" position="inside">
|
||||
<button class="settle-tips" t-if="_state.ui.filter == 'TIPPING'" t-on-click="settleTips">Settle</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="TipCell" owl="1">
|
||||
<div class="tip-cell" t-on-click.stop="editTip">
|
||||
<t t-if="state.isEditing">
|
||||
<input type="text" name="tip-amount" t-ref="autofocus" t-model="orderUiState.inputTipAmount" t-on-blur="onBlur" t-on-keydown="onKeydown" />
|
||||
</t>
|
||||
<div t-else="">
|
||||
<t t-esc="tipAmountStr"></t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="pos_restaurant.TipScreen" owl="1">
|
||||
<div class="tip-screen screen">
|
||||
<div class="pos-receipt-container"/>
|
||||
<div class="screen-content">
|
||||
<div class="top-content">
|
||||
<span class="button back" t-on-click="() => this.showScreen('FloorScreen')">
|
||||
<i class="fa fa-angle-double-left"></i>
|
||||
<span> </span>
|
||||
<span>Back</span>
|
||||
</span>
|
||||
<span class="button" t-if="env.proxy.printer" t-on-click="printTipReceipt">
|
||||
<i class="fa fa-print"></i>
|
||||
<span> </span>
|
||||
<span>Reprint receipts</span>
|
||||
</span>
|
||||
<div class="top-content-center">
|
||||
<h1>Add a tip</h1>
|
||||
</div>
|
||||
<div class="button highlight next" t-on-click="validateTip">
|
||||
Settle <i class="fa fa-angle-double-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tip-options">
|
||||
<div class="total-amount">
|
||||
<t t-esc="overallAmountStr" />
|
||||
</div>
|
||||
<div class="tip-amount-options">
|
||||
<div class="percentage-amounts">
|
||||
<t t-foreach="percentageTips" t-as="tip" t-key="tip.percentage">
|
||||
<div class="button" t-on-click="() => { state.inputTipAmount = tip.amount.toFixed(2); }">
|
||||
<div class="percentage">
|
||||
<t t-esc="tip.percentage"></t>
|
||||
</div>
|
||||
<div class="amount">
|
||||
<t t-esc="env.pos.format_currency(tip.amount)" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="no-tip" t-on-click="() => { state.inputTipAmount = '0'; }">
|
||||
<div class="button">No Tip</div>
|
||||
</div>
|
||||
<div class="custom-amount-form">
|
||||
<div class="item label">Amount</div>
|
||||
<div class="item input">
|
||||
<input type="text" t-model="state.inputTipAmount" t-att-data-amount="state.inputTipAmount" />
|
||||
<div class="currency">
|
||||
<t t-esc="env.pos.getCurrencySymbol()" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<t t-portal="'.pos .back-to-floor-portal'" position="before">
|
||||
<BackToFloorButton mobileSearchBarIsShown="props.mobileSearchBarIsShown"/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="TipReceipt" owl="1">
|
||||
<div class="pos-receipt">
|
||||
<t t-if="receipt.company.logo">
|
||||
<img class="pos-receipt-logo" t-att-src="receipt.company.logo" alt="Logo"/>
|
||||
<br/>
|
||||
</t>
|
||||
<t t-if="!receipt.company.logo">
|
||||
<h2 class="pos-receipt-center-align">
|
||||
<t t-esc="receipt.company.name" />
|
||||
</h2>
|
||||
<br/>
|
||||
</t>
|
||||
<div class="pos-receipt-contact">
|
||||
<t t-if="receipt.company.contact_address">
|
||||
<div><t t-esc="receipt.company.contact_address" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.company.phone">
|
||||
<div>Tel:<t t-esc="receipt.company.phone" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.company.vat">
|
||||
<div>VAT:<t t-esc="receipt.company.vat" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.company.email">
|
||||
<div><t t-esc="receipt.company.email" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.company.website">
|
||||
<div><t t-esc="receipt.company.website" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.header_html">
|
||||
<t t-out="receipt.header_html" />
|
||||
</t>
|
||||
<t t-if="!receipt.header_html and receipt.header">
|
||||
<div><t t-esc="receipt.header" /></div>
|
||||
</t>
|
||||
<t t-if="receipt.cashier">
|
||||
<div class="cashier">
|
||||
<div>--------------------------------</div>
|
||||
<div>Served by <t t-esc="receipt.cashier" /></div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="pos-payment-terminal-receipt">
|
||||
<t t-out="data"/>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
|
||||
<div class="subtotal">
|
||||
<span>Subtotal</span>
|
||||
<div class="pos-receipt-right-align"><t t-esc="total"/></div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="tip">
|
||||
<span>Tip:</span>
|
||||
<div class="pos-receipt-right-align">________________________</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div class="total">
|
||||
<span>Total:</span>
|
||||
<div class="pos-receipt-right-align">________________________</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div class="signature">
|
||||
<div>______________________________________________</div>
|
||||
<div>Signature</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="OrderChangeReceipt">
|
||||
<div class="pos-receipt">
|
||||
<div class="pos-receipt-order-data"><t t-esc="changes.name" /></div>
|
||||
<t t-if="changes.floor_name || changes.table_name">
|
||||
<br />
|
||||
<div class="pos-receipt-title">
|
||||
<t t-esc="changes.floor_name" /> / <t t-esc="changes.table_name"/>
|
||||
</div>
|
||||
</t>
|
||||
<br />
|
||||
<br />
|
||||
<t t-if="changes.cancelled.length > 0">
|
||||
<div class="pos-order-receipt-cancel">
|
||||
<div class="pos-receipt-title">
|
||||
CANCELLED
|
||||
<t t-esc='changes.time.hours'/>:<t t-esc='changes.time.minutes'/>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<t t-foreach="changes.cancelled" t-as="change">
|
||||
<div class="multiprint-flex">
|
||||
<span class="product-quantity" t-esc="change.quantity"/>
|
||||
<span class="product-name" t-esc="change.name"/>
|
||||
</div>
|
||||
<t t-if="change.note">
|
||||
<div>
|
||||
NOTE
|
||||
<span class="pos-receipt-right-align">...</span>
|
||||
</div>
|
||||
<div><span class="pos-receipt-left-padding">--- <t t-esc="change.note" /></span></div>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<br />
|
||||
<br />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="changes.new.length > 0">
|
||||
<div class="pos-receipt-title">
|
||||
NEW
|
||||
<t t-esc='changes.time.hours'/>:<t t-esc='changes.time.minutes'/>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<t t-foreach="changes.new" t-as="change">
|
||||
<div class="multiprint-flex">
|
||||
<span class="product-quantity" t-esc="change.quantity"/>
|
||||
<span class="product-name" t-esc="change.name"/>
|
||||
</div>
|
||||
<t t-if="change.note">
|
||||
<div>
|
||||
NOTE
|
||||
<span class="pos-receipt-right-align">...</span>
|
||||
</div>
|
||||
<div><span class="pos-receipt-left-padding">--- <t t-esc="change.note" /></span></div>
|
||||
<br/>
|
||||
</t>
|
||||
</t>
|
||||
<br />
|
||||
<br />
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Loading…
Add table
Add a link
Reference in a new issue