[REF] point_of_sale: migration to OWL framework
This commit is contained in:
committed by
Pierre Masereel
parent
cbd6360570
commit
c6397ab3b3
@@ -17,8 +17,9 @@
|
|||||||
'wizard/pos_box.xml',
|
'wizard/pos_box.xml',
|
||||||
'wizard/pos_details.xml',
|
'wizard/pos_details.xml',
|
||||||
'wizard/pos_payment.xml',
|
'wizard/pos_payment.xml',
|
||||||
'views/pos_templates.xml',
|
'views/pos_assets_common.xml',
|
||||||
'views/point_of_sale_template.xml',
|
'views/pos_assets_index.xml',
|
||||||
|
'views/pos_assets_qunit.xml',
|
||||||
'views/point_of_sale_report.xml',
|
'views/point_of_sale_report.xml',
|
||||||
'views/point_of_sale_view.xml',
|
'views/point_of_sale_view.xml',
|
||||||
'views/pos_order_view.xml',
|
'views/pos_order_view.xml',
|
||||||
@@ -30,7 +31,7 @@
|
|||||||
'views/pos_config_view.xml',
|
'views/pos_config_view.xml',
|
||||||
'views/pos_session_view.xml',
|
'views/pos_session_view.xml',
|
||||||
'views/point_of_sale_sequence.xml',
|
'views/point_of_sale_sequence.xml',
|
||||||
'views/point_of_sale.xml',
|
'views/customer_facing_display.xml',
|
||||||
'data/point_of_sale_data.xml',
|
'data/point_of_sale_data.xml',
|
||||||
'views/pos_order_report_view.xml',
|
'views/pos_order_report_view.xml',
|
||||||
'views/account_statement_view.xml',
|
'views/account_statement_view.xml',
|
||||||
@@ -46,6 +47,62 @@
|
|||||||
],
|
],
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': True,
|
'application': True,
|
||||||
'qweb': ['static/src/xml/pos.xml'],
|
'qweb': [
|
||||||
|
'static/src/xml/Chrome.xml',
|
||||||
|
'static/src/xml/debug_manager.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ProductScreen.xml',
|
||||||
|
'static/src/xml/Screens/ClientListScreen/ClientLine.xml',
|
||||||
|
'static/src/xml/Screens/ClientListScreen/ClientDetails.xml',
|
||||||
|
'static/src/xml/Screens/ClientListScreen/ClientDetailsEdit.xml',
|
||||||
|
'static/src/xml/Screens/ClientListScreen/ClientListScreen.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PSNumpadInputButton.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PaymentScreenNumpad.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PaymentScreenElectronicPayment.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PaymentScreenPaymentLines.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PaymentMethodButton.xml',
|
||||||
|
'static/src/xml/Screens/PaymentScreen/PaymentScreen.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/Orderline.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/OrderSummary.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/OrderWidget.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/NumpadWidget.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ActionpadWidget.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/CategoryBreadcrumb.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/CategoryButton.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/CategorySimpleButton.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/HomeCategoryBreadcrumb.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ProductsWidgetControlPanel.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ProductItem.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ProductList.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ProductsWidget.xml',
|
||||||
|
'static/src/xml/Screens/ReceiptScreen/WrappedProductNameLines.xml',
|
||||||
|
'static/src/xml/Screens/ReceiptScreen/OrderReceipt.xml',
|
||||||
|
'static/src/xml/Screens/ReceiptScreen/ReceiptScreen.xml',
|
||||||
|
'static/src/xml/Screens/ScaleScreen/ScaleScreen.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/OrderSelector.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/CashierName.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/ProxyStatus.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/SyncNotification.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/HeaderButton.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/SaleDetailsButton.xml',
|
||||||
|
'static/src/xml/SaleDetailsReport.xml',
|
||||||
|
'static/src/xml/Misc/Draggable.xml',
|
||||||
|
'static/src/xml/Misc/NotificationSound.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/DebugWidget.xml',
|
||||||
|
'static/src/xml/Popups/ErrorPopup.xml',
|
||||||
|
'static/src/xml/Popups/ErrorBarcodePopup.xml',
|
||||||
|
'static/src/xml/Popups/ConfirmPopup.xml',
|
||||||
|
'static/src/xml/Popups/TextInputPopup.xml',
|
||||||
|
'static/src/xml/Popups/TextAreaPopup.xml',
|
||||||
|
'static/src/xml/Popups/ErrorTracebackPopup.xml',
|
||||||
|
'static/src/xml/Popups/SelectionPopup.xml',
|
||||||
|
'static/src/xml/Popups/EditListInput.xml',
|
||||||
|
'static/src/xml/Popups/EditListPopup.xml',
|
||||||
|
'static/src/xml/Popups/NumberPopup.xml',
|
||||||
|
'static/src/xml/Popups/OfflineErrorPopup.xml',
|
||||||
|
'static/src/xml/Popups/OrderImportPopup.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ControlButtons/SetPricelistButton.xml',
|
||||||
|
'static/src/xml/Screens/ProductScreen/ControlButtons/SetFiscalPositionButton.xml',
|
||||||
|
'static/src/xml/ChromeWidgets/ClientScreenButton.xml',
|
||||||
|
],
|
||||||
'website': 'https://www.odoo.com/page/point-of-sale-shop',
|
'website': 'https://www.odoo.com/page/point-of-sale-shop',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ class PosController(http.Controller):
|
|||||||
}
|
}
|
||||||
return request.render('point_of_sale.index', qcontext=context)
|
return request.render('point_of_sale.index', qcontext=context)
|
||||||
|
|
||||||
|
@http.route('/pos/web/tests', type='http', auth="user")
|
||||||
|
def test_suite(self, mod=None, **kwargs):
|
||||||
|
domain = [
|
||||||
|
('state', '=', 'opened'),
|
||||||
|
('user_id', '=', request.session.uid),
|
||||||
|
('rescue', '=', False)
|
||||||
|
]
|
||||||
|
pos_session = request.env['pos.session'].sudo().search(domain, limit=1)
|
||||||
|
session_info = request.env['ir.http'].session_info()
|
||||||
|
session_info['user_context']['allowed_company_ids'] = pos_session.company_id.ids
|
||||||
|
context = {
|
||||||
|
'session_info': session_info,
|
||||||
|
}
|
||||||
|
return request.render('point_of_sale.qunit_suite', qcontext=context)
|
||||||
|
|
||||||
@http.route('/pos/sale_details_report', type='http', auth='user')
|
@http.route('/pos/sale_details_report', type='http', auth='user')
|
||||||
def print_sale_details(self, date_start=False, date_stop=False, **kw):
|
def print_sale_details(self, date_start=False, date_stop=False, **kw):
|
||||||
r = request.env['report.point_of_sale.report_saledetails']
|
r = request.env['report.point_of_sale.report_saledetails']
|
||||||
|
|||||||
@@ -1772,6 +1772,21 @@ td {
|
|||||||
margin-left: 120px;
|
margin-left: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* These css declarations is to make sure that the list of clients
|
||||||
|
* always occupies the remaining space regardless when the client
|
||||||
|
* details is displayed or not.
|
||||||
|
*/
|
||||||
|
.clientlist-screen .client-window {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clientlist-screen .client-window section.list {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ********* The OrderWidget ********* */
|
/* ********* The OrderWidget ********* */
|
||||||
|
|
||||||
@@ -2102,6 +2117,9 @@ td {
|
|||||||
.pos .debug-widget .content{
|
.pos .debug-widget .content{
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
.pos .debug-widget header {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.pos .debug-widget h1{
|
.pos .debug-widget h1{
|
||||||
background:black;
|
background:black;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
@@ -2164,9 +2182,11 @@ td {
|
|||||||
background-color: rgba(0,0,0,0.5);
|
background-color: rgba(0,0,0,0.5);
|
||||||
z-index:1000;
|
z-index:1000;
|
||||||
}
|
}
|
||||||
|
.pos .modal-dialog header{
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.pos .modal-dialog .popup{
|
.pos .modal-dialog .popup{
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0; left: 0; right: 0; bottom: 0;
|
|
||||||
margin: auto;
|
margin: auto;
|
||||||
width:500px;
|
width:500px;
|
||||||
height:400px;
|
height:400px;
|
||||||
@@ -2179,6 +2199,10 @@ td {
|
|||||||
z-index:1200;
|
z-index:1200;
|
||||||
font-family: 'Lato';
|
font-family: 'Lato';
|
||||||
font-family: Lato;
|
font-family: Lato;
|
||||||
|
/* position the popup at center and and still making it draggable*/
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
.pos .modal-dialog .popup-confirm{
|
.pos .modal-dialog .popup-confirm{
|
||||||
height: 250px;
|
height: 250px;
|
||||||
@@ -2238,6 +2262,11 @@ td {
|
|||||||
|
|
||||||
background: rgba(0,0,0,0.05);
|
background: rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pos .popup .button.dont-show-again {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
.pos .popup .button.icon {
|
.pos .popup .button.icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
@@ -2287,12 +2316,12 @@ td {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
}
|
}
|
||||||
.pos .popup .packlot-lines{
|
.pos .popup .list-lines{
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
.pos .popup .packlot-line-input {
|
.pos .popup .list-line-input {
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2387,7 +2416,6 @@ td {
|
|||||||
}
|
}
|
||||||
.pos .popup-password .popup-numpad {
|
.pos .popup-password .popup-numpad {
|
||||||
width: 190px;
|
width: 190px;
|
||||||
text-align: left;
|
|
||||||
}
|
}
|
||||||
.pos .popup-password .popup-input {
|
.pos .popup-password .popup-input {
|
||||||
width: 70%;
|
width: 70%;
|
||||||
@@ -2474,3 +2502,46 @@ td {
|
|||||||
.pos .not-supported-browser img{
|
.pos .not-supported-browser img{
|
||||||
border-collapse: separate;
|
border-collapse: separate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fade-enter-active, .fade-leave-active {
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter, .fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swing-enter-active, .swing-leave-active {
|
||||||
|
transition: opacity 0.8s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swing-enter, .swing-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
We block the top-header when a temp screen is displayed.
|
||||||
|
Similar to blocking the whole ui when a popup is displayed.
|
||||||
|
*/
|
||||||
|
.pos .block-top-header {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height:100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
z-index:1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
|
cursor: grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
odoo.define('point_of_sale.Chrome', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState, useRef } = owl.hooks;
|
||||||
|
const { debounce } = owl.utils;
|
||||||
|
const { loadCSS } = require('web.ajax');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const { CrashManager } = require('web.CrashManager');
|
||||||
|
const { BarcodeEvents } = require('barcodes.BarcodeEvents');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||||
|
const PopupControllerMixin = require('point_of_sale.PopupControllerMixin');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// This is kind of a trick.
|
||||||
|
// We get a reference to the whole exports so that
|
||||||
|
// when we create an instance of one of the classes,
|
||||||
|
// we instantiate the extended one.
|
||||||
|
const models = require('point_of_sale.models');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chrome is the root component of the PoS App.
|
||||||
|
*/
|
||||||
|
class Chrome extends PopupControllerMixin(PosComponent) {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('show-main-screen', this.__showScreen);
|
||||||
|
useListener('toggle-debug-widget', debounce(this._toggleDebugWidget, 100));
|
||||||
|
useListener('show-temp-screen', this.__showTempScreen);
|
||||||
|
useListener('close-temp-screen', this.__closeTempScreen);
|
||||||
|
useListener('close-pos', this._closePos);
|
||||||
|
useListener('loading-skip-callback', () => this._loadingSkipCallback());
|
||||||
|
useListener('play-sound', this._onPlaySound);
|
||||||
|
useListener('set-sync-status', this._onSetSyncStatus);
|
||||||
|
NumberBuffer.activate();
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
uiState: 'LOADING', // 'LOADING' | 'READY' | 'CLOSING'
|
||||||
|
debugWidgetIsShown: true,
|
||||||
|
hasBigScrollBars: false,
|
||||||
|
sound: { src: null },
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loading = useState({
|
||||||
|
message: 'Loading',
|
||||||
|
skipButtonIsShown: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mainScreen = useState({ name: null, component: null });
|
||||||
|
this.mainScreenProps = {};
|
||||||
|
|
||||||
|
this.tempScreen = useState({ isShown: false, name: null, component: null });
|
||||||
|
this.tempScreenProps = {};
|
||||||
|
|
||||||
|
this.progressbar = useRef('progressbar');
|
||||||
|
|
||||||
|
this.previous_touch_y_coordinate = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OVERLOADED METHODS //
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
// remove default webclient handlers that induce click delay
|
||||||
|
$(document).off();
|
||||||
|
$(window).off();
|
||||||
|
$('html').off();
|
||||||
|
$('body').off();
|
||||||
|
// The above lines removed the bindings, but we really need them for the barcode
|
||||||
|
BarcodeEvents.start();
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
BarcodeEvents.stop();
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
super.destroy(...arguments);
|
||||||
|
this.env.pos.destroy();
|
||||||
|
}
|
||||||
|
catchError(error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// GETTERS //
|
||||||
|
|
||||||
|
get clientScreenButtonIsShown() {
|
||||||
|
return (
|
||||||
|
this.env.pos.config.use_proxy && this.env.pos.config.iface_customer_facing_display
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Startup screen can be based on pos config so the startup screen
|
||||||
|
* is only determined after pos data is completely loaded.
|
||||||
|
*
|
||||||
|
* NOTE: Wait for pos data to be completed before calling this getter.
|
||||||
|
*/
|
||||||
|
get startScreen() {
|
||||||
|
if (this.state.uiState !== 'READY') {
|
||||||
|
console.warn(
|
||||||
|
`Accessing startScreen of Chrome component before 'state.uiState' to be 'READY' is not recommended.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { name: 'ProductScreen' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// CONTROL METHODS //
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this function after the Chrome component is mounted.
|
||||||
|
* This will load pos and assign it to the environment.
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
try {
|
||||||
|
// Instead of passing chrome to the instantiation the PosModel,
|
||||||
|
// we inject functions needed by pos.
|
||||||
|
// This way, we somehow decoupled Chrome from PosModel.
|
||||||
|
// We can then test PosModel independently from Chrome by supplying
|
||||||
|
// mocked version of these default attributes.
|
||||||
|
const posModelDefaultAttributes = {
|
||||||
|
rpc: this.rpc.bind(this),
|
||||||
|
session: this.env.session,
|
||||||
|
do_action: this.props.webClient.do_action.bind(this.props.webClient),
|
||||||
|
setLoadingMessage: this.setLoadingMessage.bind(this),
|
||||||
|
showLoadingSkip: this.showLoadingSkip.bind(this),
|
||||||
|
setLoadingProgress: this.setLoadingProgress.bind(this),
|
||||||
|
};
|
||||||
|
this.env.pos = new models.PosModel(posModelDefaultAttributes);
|
||||||
|
await this.env.pos.ready;
|
||||||
|
this._buildChrome();
|
||||||
|
this.env.pos.set(
|
||||||
|
'selectedCategoryId',
|
||||||
|
this.env.pos.config.iface_start_categ_id
|
||||||
|
? this.env.pos.config.iface_start_categ_id[0]
|
||||||
|
: 0
|
||||||
|
);
|
||||||
|
this.state.uiState = 'READY';
|
||||||
|
this.env.pos.on('change:selectedOrder', this._showSavedScreen, this);
|
||||||
|
this._showStartScreen();
|
||||||
|
this.env.pos.push_orders(); // push order in the background, no need to await
|
||||||
|
// Allow using the app even if not all the images are loaded.
|
||||||
|
// Basically, preload the images in the background.
|
||||||
|
setTimeout(() => this._preloadImages());
|
||||||
|
} catch (error) {
|
||||||
|
let title = 'Unknown Error',
|
||||||
|
body;
|
||||||
|
|
||||||
|
if (error.message && [100, 200, 404, -32098].includes(error.message.code)) {
|
||||||
|
// this is the signature of rpc error
|
||||||
|
if (error.message.code === -32098) {
|
||||||
|
title = 'Network Failure (XmlHttpRequestError)';
|
||||||
|
body =
|
||||||
|
'The Point of Sale could not be loaded due to a network problem.\n' +
|
||||||
|
'Please check your internet connection.';
|
||||||
|
} else if (error.message.code === 200) {
|
||||||
|
title = error.message.data.message || this.env._t('Server Error');
|
||||||
|
body =
|
||||||
|
error.message.data.debug ||
|
||||||
|
this.env._t(
|
||||||
|
'The server encountered an error while receiving your order.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (error instanceof Error) {
|
||||||
|
title = error.message;
|
||||||
|
body = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.showPopup('ErrorTracebackPopup', {
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
exitButtonIsShown: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EVENT HANDLERS //
|
||||||
|
|
||||||
|
_showStartScreen() {
|
||||||
|
const { name, props } = this.startScreen;
|
||||||
|
this.showScreen(name, props);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Show the screen saved in the order when the `selectedOrder` of pos is changed.
|
||||||
|
* @param {models.PosModel} pos
|
||||||
|
* @param {models.Order} newSelectedOrder
|
||||||
|
*/
|
||||||
|
_showSavedScreen(pos, newSelectedOrder) {
|
||||||
|
const { name, props } = this._getSavedScreen(newSelectedOrder);
|
||||||
|
this.showScreen(name, props);
|
||||||
|
}
|
||||||
|
_getSavedScreen(order) {
|
||||||
|
return order.get_screen_data('screen') || { name: 'ProductScreen' };
|
||||||
|
}
|
||||||
|
__showTempScreen(event) {
|
||||||
|
const { name, props, resolve } = event.detail;
|
||||||
|
this.tempScreen.isShown = true;
|
||||||
|
this.tempScreen.name = name;
|
||||||
|
this.tempScreen.component = this.constructor.components[name];
|
||||||
|
this.tempScreenProps = Object.assign({}, props, { resolve });
|
||||||
|
}
|
||||||
|
__closeTempScreen() {
|
||||||
|
this.tempScreen.isShown = false;
|
||||||
|
}
|
||||||
|
__showScreen({ detail: { name, props } }) {
|
||||||
|
// 1. Set the information of the screen to display.
|
||||||
|
this.mainScreen.name = name;
|
||||||
|
this.mainScreen.component = this.constructor.components[name];
|
||||||
|
this.mainScreenProps = props || {};
|
||||||
|
// 2. Save the screen to the order.
|
||||||
|
// - This screen is shown when the order is selected.
|
||||||
|
this._setScreenData(name, props);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Set the latest screen to the current order. This is done so that
|
||||||
|
* when the order is selected again, the ui returns to the latest screen
|
||||||
|
* saved in the order.
|
||||||
|
*
|
||||||
|
* @param {string} name Screen name
|
||||||
|
* @param {Object} props props for the Screen component
|
||||||
|
*/
|
||||||
|
_setScreenData(name, props) {
|
||||||
|
const order = this.env.pos.get_order();
|
||||||
|
if (order) {
|
||||||
|
order.set_screen_data('screen', { name, props });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _closePos() {
|
||||||
|
// If pos is not properly loaded, we just go back to /web without
|
||||||
|
// doing anything in the order data.
|
||||||
|
if (!this.env.pos || this.env.pos.db.get_orders().length === 0) {
|
||||||
|
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.pos.db.get_orders().length) {
|
||||||
|
// If there are orders in the db left unsynced, we try to sync.
|
||||||
|
// If sync successful, close without asking.
|
||||||
|
// Otherwise, ask again saying that some orders are not yet synced.
|
||||||
|
try {
|
||||||
|
await this.env.pos.push_orders();
|
||||||
|
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
const reason = this.env.pos.get('failed')
|
||||||
|
? this.env._t(
|
||||||
|
'Some orders could not be submitted to ' +
|
||||||
|
'the server due to configuration errors. ' +
|
||||||
|
'You can exit the Point of Sale, but do ' +
|
||||||
|
'not close the session before the issue ' +
|
||||||
|
'has been resolved.'
|
||||||
|
)
|
||||||
|
: this.env._t(
|
||||||
|
'Some orders could not be submitted to ' +
|
||||||
|
'the server due to internet connection issues. ' +
|
||||||
|
'You can exit the Point of Sale, but do ' +
|
||||||
|
'not close the session before the issue ' +
|
||||||
|
'has been resolved.'
|
||||||
|
);
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Offline Orders'),
|
||||||
|
body: reason,
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
this.state.uiState = 'CLOSING';
|
||||||
|
this.loading.skipButtonIsShown = false;
|
||||||
|
this.setLoadingMessage(this.env._t('Closing ...'));
|
||||||
|
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_toggleDebugWidget() {
|
||||||
|
this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown;
|
||||||
|
}
|
||||||
|
_onPlaySound({ detail: name }) {
|
||||||
|
let src;
|
||||||
|
if (name === 'error') {
|
||||||
|
src = "/point_of_sale/static/src/sounds/error.wav";
|
||||||
|
} else if (name === 'bell') {
|
||||||
|
src = "/point_of_sale/static/src/sounds/bell.wav";
|
||||||
|
}
|
||||||
|
this.state.sound.src = src;
|
||||||
|
}
|
||||||
|
_onSetSyncStatus({ detail: { status, pending }}) {
|
||||||
|
this.env.pos.set('synch', { status, pending });
|
||||||
|
}
|
||||||
|
|
||||||
|
// TO PASS AS PARAMETERS //
|
||||||
|
|
||||||
|
setLoadingProgress(fac) {
|
||||||
|
if (this.progressbar.el) {
|
||||||
|
this.progressbar.el.style.width = `${Math.floor(fac * 100)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLoadingMessage(msg, progress) {
|
||||||
|
this.loading.message = msg;
|
||||||
|
if (typeof progress !== 'undefined') {
|
||||||
|
this.setLoadingProgress(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Show Skip button in the loading screen and allow to assign callback
|
||||||
|
* when the button is pressed.
|
||||||
|
*
|
||||||
|
* @param {Function} callback function to call when Skip button is pressed.
|
||||||
|
*/
|
||||||
|
showLoadingSkip(callback) {
|
||||||
|
if (callback) {
|
||||||
|
this.loading.skipButtonIsShown = true;
|
||||||
|
this._loadingSkipCallback = callback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MISC METHODS //
|
||||||
|
|
||||||
|
_preloadImages() {
|
||||||
|
for (let product of this.env.pos.db.get_product_by_category(0)) {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`;
|
||||||
|
}
|
||||||
|
for (let category of Object.values(this.env.pos.db.category_by_id)) {
|
||||||
|
if (category.id == 0) continue;
|
||||||
|
const image = new Image();
|
||||||
|
image.src = `/web/image?model=pos.category&field=image_128&id=${category.id}&write_date=${category.write_date}&unique=1`;
|
||||||
|
}
|
||||||
|
const staticImages = ['backspace.png', 'bc-arrow-big.png'];
|
||||||
|
for (let imageName of staticImages) {
|
||||||
|
const image = new Image();
|
||||||
|
image.src = `/point_of_sale/static/src/img/${imageName}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildChrome() {
|
||||||
|
if ($.browser.chrome) {
|
||||||
|
var chrome_version = $.browser.version.split('.')[0];
|
||||||
|
if (parseInt(chrome_version, 10) >= 50) {
|
||||||
|
loadCSS('/point_of_sale/static/src/css/chrome50.css');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.env.pos.config.iface_big_scrollbars) {
|
||||||
|
this.state.uiState.hasBigScrollBars = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._disableRubberbanding();
|
||||||
|
this._disableBackspaceBack();
|
||||||
|
this._replaceCrashmanager();
|
||||||
|
}
|
||||||
|
// replaces the error handling of the existing crashmanager which
|
||||||
|
// uses jquery dialog to display the error, to use the pos popup
|
||||||
|
// instead
|
||||||
|
_replaceCrashmanager() {
|
||||||
|
var self = this;
|
||||||
|
CrashManager.include({
|
||||||
|
show_error: function (error) {
|
||||||
|
if (self.env.pos) {
|
||||||
|
// self == this component
|
||||||
|
self.showPopup('ErrorTracebackPopup', {
|
||||||
|
title: error.type,
|
||||||
|
body: error.message + '\n' + error.data.debug + '\n',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// this == CrashManager instance
|
||||||
|
this._super(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_disableRubberbanding() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
document.body.addEventListener('touchstart', function (event) {
|
||||||
|
self.previous_touch_y_coordinate = event.touches[0].clientY;
|
||||||
|
});
|
||||||
|
|
||||||
|
// prevent the pos body from being scrollable.
|
||||||
|
document.body.addEventListener('touchmove', function (event) {
|
||||||
|
var node = event.target;
|
||||||
|
var current_touch_y_coordinate = event.touches[0].clientY;
|
||||||
|
var scrolling_down;
|
||||||
|
|
||||||
|
if (current_touch_y_coordinate < self.previous_touch_y_coordinate) {
|
||||||
|
scrolling_down = true;
|
||||||
|
} else {
|
||||||
|
scrolling_down = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (node) {
|
||||||
|
if (
|
||||||
|
node.classList &&
|
||||||
|
node.classList.contains('touch-scrollable') &&
|
||||||
|
self._scrollable(node, scrolling_down)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
node = node.parentNode;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// prevent backspace from performing a 'back' navigation
|
||||||
|
_disableBackspaceBack() {
|
||||||
|
$(document).on('keydown', function (e) {
|
||||||
|
if (e.which === 8 && !$(e.target).is('input, textarea')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_scrollable(element, scrolling_down) {
|
||||||
|
var $element = $(element);
|
||||||
|
var scrollable = true;
|
||||||
|
|
||||||
|
if (!scrolling_down && $element.scrollTop() <= 0) {
|
||||||
|
scrollable = false;
|
||||||
|
} else if (
|
||||||
|
scrolling_down &&
|
||||||
|
$element.scrollTop() + $element.height() >= element.scrollHeight
|
||||||
|
) {
|
||||||
|
scrollable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return scrollable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Chrome.template = 'Chrome';
|
||||||
|
|
||||||
|
Registries.Component.add(Chrome);
|
||||||
|
|
||||||
|
return Chrome;
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
odoo.define('point_of_sale.CashierName', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Previously UsernameWidget
|
||||||
|
class CashierName extends PosComponent {
|
||||||
|
get username() {
|
||||||
|
const cashier = this.env.pos.get_cashier();
|
||||||
|
if (cashier) {
|
||||||
|
return cashier.name;
|
||||||
|
} else {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CashierName.template = 'CashierName';
|
||||||
|
|
||||||
|
Registries.Component.add(CashierName);
|
||||||
|
|
||||||
|
return CashierName;
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
odoo.define('point_of_sale.ClientScreenButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Formerly ClientScreenWidget
|
||||||
|
class ClientScreenButton extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({ status: 'failure' });
|
||||||
|
this._start();
|
||||||
|
}
|
||||||
|
get message() {
|
||||||
|
return {
|
||||||
|
success: '',
|
||||||
|
warning: this.env._t('Connected, Not Owned'),
|
||||||
|
failure: this.env._t('Disconnected'),
|
||||||
|
not_found: this.env._t('Client Screen Unsupported. Please upgrade the IoT Box'),
|
||||||
|
}[this.state.status];
|
||||||
|
}
|
||||||
|
async onClick() {
|
||||||
|
try {
|
||||||
|
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
|
||||||
|
const ownership = await this.env.pos.proxy.take_ownership_over_client_screen(
|
||||||
|
renderedHtml
|
||||||
|
);
|
||||||
|
if (typeof ownership === 'string') {
|
||||||
|
ownership = JSON.parse(ownership);
|
||||||
|
}
|
||||||
|
if (ownership.status === 'success') {
|
||||||
|
this.state.status = 'success';
|
||||||
|
} else {
|
||||||
|
this.state.status = 'warning';
|
||||||
|
}
|
||||||
|
if (!this.env.pos.proxy.posbox_supports_display) {
|
||||||
|
this.env.pos.proxy.posbox_supports_display = true;
|
||||||
|
this._start();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (typeof error == 'undefined') {
|
||||||
|
this.state.status = 'failure';
|
||||||
|
} else {
|
||||||
|
this.state.status = 'not_found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_start() {
|
||||||
|
const self = this;
|
||||||
|
async function loop() {
|
||||||
|
if (self.env.pos.proxy.posbox_supports_display) {
|
||||||
|
try {
|
||||||
|
const ownership = await self.env.pos.proxy.test_ownership_of_client_screen();
|
||||||
|
if (typeof ownership === 'string') {
|
||||||
|
ownership = JSON.parse(ownership);
|
||||||
|
}
|
||||||
|
if (ownership.status === 'OWNER') {
|
||||||
|
self.state.status = 'success';
|
||||||
|
} else {
|
||||||
|
self.state.status = 'warning';
|
||||||
|
}
|
||||||
|
setTimeout(loop, 3000);
|
||||||
|
} catch (error) {
|
||||||
|
if (error.abort) {
|
||||||
|
// Stop the loop
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof error == 'undefined') {
|
||||||
|
self.state.status = 'failure';
|
||||||
|
} else {
|
||||||
|
self.state.status = 'not_found';
|
||||||
|
self.env.pos.proxy.posbox_supports_display = false;
|
||||||
|
}
|
||||||
|
setTimeout(loop, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientScreenButton.template = 'ClientScreenButton';
|
||||||
|
|
||||||
|
Registries.Component.add(ClientScreenButton);
|
||||||
|
|
||||||
|
return ClientScreenButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
odoo.define('point_of_sale.DebugWidget', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
const { getFileAsText } = require('point_of_sale.utils');
|
||||||
|
const { parse } = require('web.field_utils');
|
||||||
|
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class DebugWidget extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({
|
||||||
|
barcodeInput: '',
|
||||||
|
weightInput: '',
|
||||||
|
isPaidOrdersReady: false,
|
||||||
|
isUnpaidOrdersReady: false,
|
||||||
|
buffer: NumberBuffer.get(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Perhaps this can still be improved.
|
||||||
|
// What we do here is loop thru the `event` elements
|
||||||
|
// then we assign animation that happens when the event is triggered
|
||||||
|
// in the proxy. E.g. if open_cashbox is sent, the open_cashbox element
|
||||||
|
// changes color from '#6CD11D' to '#1E1E1E' for a duration of 2sec.
|
||||||
|
this.eventElementsRef = {};
|
||||||
|
this.animations = {};
|
||||||
|
for (let eventName of ['open_cashbox', 'print_receipt', 'scale_read']) {
|
||||||
|
this.eventElementsRef[eventName] = useRef(eventName);
|
||||||
|
this.env.pos.proxy.add_notification(
|
||||||
|
eventName,
|
||||||
|
(() => {
|
||||||
|
if (this.animations[eventName]) {
|
||||||
|
this.animations[eventName].cancel();
|
||||||
|
}
|
||||||
|
const eventElement = this.eventElementsRef[eventName].el;
|
||||||
|
eventElement.style.backgroundColor = '#6CD11D';
|
||||||
|
this.animations[eventName] = eventElement.animate(
|
||||||
|
{ backgroundColor: ['#6CD11D', '#1E1E1E'] },
|
||||||
|
2000
|
||||||
|
);
|
||||||
|
}).bind(this)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
NumberBuffer.on('buffer-update', this, this._onBufferUpdate);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
NumberBuffer.off('buffer-update', this, this._onBufferUpdate);
|
||||||
|
}
|
||||||
|
toggleWidget() {
|
||||||
|
this.state.isShown = !this.state.isShown;
|
||||||
|
}
|
||||||
|
setWeight() {
|
||||||
|
var weightInKg = parse.float(this.state.weightInput);
|
||||||
|
if (!isNaN(weightInKg)) {
|
||||||
|
this.env.pos.proxy.debug_set_weight(weightInKg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resetWeight() {
|
||||||
|
this.state.weightInput = '';
|
||||||
|
this.env.pos.proxy.debug_reset_weight();
|
||||||
|
}
|
||||||
|
barcodeScan() {
|
||||||
|
this.env.pos.barcode_reader.scan(this.state.barcodeInput);
|
||||||
|
}
|
||||||
|
barcodeScanEAN() {
|
||||||
|
const ean = this.env.pos.barcode_reader.barcode_parser.sanitize_ean(
|
||||||
|
this.state.barcodeInput || '0'
|
||||||
|
);
|
||||||
|
this.state.barcodeInput = ean;
|
||||||
|
this.env.pos.barcode_reader.scan(ean);
|
||||||
|
}
|
||||||
|
async deleteOrders() {
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Delete Paid Orders ?'),
|
||||||
|
body: this.env._t(
|
||||||
|
'This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
this.env.pos.db.remove_all_orders();
|
||||||
|
this.env.pos.set_synch('connected', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async deleteUnpaidOrders() {
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Delete Unpaid Orders ?'),
|
||||||
|
body: this.env._t(
|
||||||
|
'This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
this.env.pos.db.remove_all_unpaid_orders();
|
||||||
|
window.location = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_createBlob(contents) {
|
||||||
|
if (typeof contents !== 'string') {
|
||||||
|
contents = JSON.stringify(contents, null, 2);
|
||||||
|
}
|
||||||
|
return new Blob([contents]);
|
||||||
|
}
|
||||||
|
// IMPROVEMENT: Duplicated codes for downloading paid and unpaid orders.
|
||||||
|
// The implementation can be better.
|
||||||
|
preparePaidOrders() {
|
||||||
|
try {
|
||||||
|
this.paidOrdersBlob = this._createBlob(this.env.pos.export_paid_orders());
|
||||||
|
this.state.isPaidOrdersReady = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get paidOrdersFilename() {
|
||||||
|
return `${this.env._t('paid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`;
|
||||||
|
}
|
||||||
|
get paidOrdersURL() {
|
||||||
|
var URL = window.URL || window.webkitURL;
|
||||||
|
return URL.createObjectURL(this.paidOrdersBlob);
|
||||||
|
}
|
||||||
|
prepareUnpaidOrders() {
|
||||||
|
try {
|
||||||
|
this.unpaidOrdersBlob = this._createBlob(this.env.pos.export_unpaid_orders());
|
||||||
|
this.state.isUnpaidOrdersReady = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get unpaidOrdersFilename() {
|
||||||
|
return `${this.env._t('unpaid orders')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.json`;
|
||||||
|
}
|
||||||
|
get unpaidOrdersURL() {
|
||||||
|
var URL = window.URL || window.webkitURL;
|
||||||
|
return URL.createObjectURL(this.unpaidOrdersBlob);
|
||||||
|
}
|
||||||
|
async importOrders(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const report = this.env.pos.import_orders(await getFileAsText(file));
|
||||||
|
await this.showPopup('OrderImportPopup', { report });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshDisplay() {
|
||||||
|
this.env.pos.proxy.message('display_refresh', {});
|
||||||
|
}
|
||||||
|
_onBufferUpdate(buffer) {
|
||||||
|
this.state.buffer = buffer;
|
||||||
|
}
|
||||||
|
get bufferRepr() {
|
||||||
|
return `"${this.state.buffer}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DebugWidget.template = 'DebugWidget';
|
||||||
|
|
||||||
|
Registries.Component.add(DebugWidget);
|
||||||
|
|
||||||
|
return DebugWidget;
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
odoo.define('point_of_sale.HeaderButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Previously HeaderButtonWidget
|
||||||
|
// This is the close session button
|
||||||
|
class HeaderButton extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({ label: 'Close' });
|
||||||
|
this.confirmed = null;
|
||||||
|
}
|
||||||
|
get translatedLabel() {
|
||||||
|
return this.env._t(this.state.label);
|
||||||
|
}
|
||||||
|
onClick() {
|
||||||
|
if (!this.confirmed) {
|
||||||
|
this.state.label = 'Confirm';
|
||||||
|
this.confirmed = setTimeout(() => {
|
||||||
|
this.state.label = 'Close';
|
||||||
|
this.confirmed = null;
|
||||||
|
}, 2000);
|
||||||
|
} else {
|
||||||
|
this.trigger('close-pos');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HeaderButton.template = 'HeaderButton';
|
||||||
|
|
||||||
|
Registries.Component.add(HeaderButton);
|
||||||
|
|
||||||
|
return HeaderButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
odoo.define('point_of_sale.OrderSelector', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Previously OrderSelectorWidget
|
||||||
|
class OrderSelector extends PosComponent {
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.get('orders').on('add remove change', () => this.render(), this);
|
||||||
|
this.env.pos.on('change:selectedOrder', () => this.render(), this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.get('orders').off('add remove change', null, this);
|
||||||
|
this.env.pos.off('change:selectedOrder', null, this);
|
||||||
|
}
|
||||||
|
selectOrder(order) {
|
||||||
|
this.env.pos.set_order(order);
|
||||||
|
}
|
||||||
|
addNewOrder() {
|
||||||
|
this.env.pos.add_new_order();
|
||||||
|
}
|
||||||
|
get orderList() {
|
||||||
|
return this.env.pos.get_order_list();
|
||||||
|
}
|
||||||
|
async deleteCurrentOrder() {
|
||||||
|
const order = this.env.pos.get_order();
|
||||||
|
if (!order) {
|
||||||
|
return;
|
||||||
|
} else if (!order.is_empty()) {
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Destroy Current Order ?'),
|
||||||
|
body: this.env._t('You will lose any data associated with the current order'),
|
||||||
|
});
|
||||||
|
if (confirmed) this.env.pos.delete_current_order();
|
||||||
|
} else {
|
||||||
|
this.env.pos.delete_current_order();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
creationTime(order) {
|
||||||
|
return moment(order.creation_date).format('hh:mm');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OrderSelector.template = 'OrderSelector';
|
||||||
|
|
||||||
|
Registries.Component.add(OrderSelector);
|
||||||
|
|
||||||
|
return OrderSelector;
|
||||||
|
});
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
odoo.define('point_of_sale.ProxyStatus', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Previously ProxyStatusWidget
|
||||||
|
class ProxyStatus extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
const initialProxyStatus = this.env.pos.proxy.get('status');
|
||||||
|
this.state = useState({
|
||||||
|
status: initialProxyStatus.status,
|
||||||
|
msg: initialProxyStatus.msg,
|
||||||
|
});
|
||||||
|
this.statuses = ['connected', 'connecting', 'disconnected', 'warning'];
|
||||||
|
this.index = 0;
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.proxy.on('change:status', this, this._onChangeStatus);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.proxy.off('change:status', this, this._onChangeStatus);
|
||||||
|
}
|
||||||
|
async onClick() {
|
||||||
|
try {
|
||||||
|
await this.env.pos.connect_to_proxy();
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
this.showPopup('ErrorPopup', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onChangeStatus(posProxy, statusChange) {
|
||||||
|
this._setStatus(statusChange.newValue);
|
||||||
|
}
|
||||||
|
_setStatus(newStatus) {
|
||||||
|
if (newStatus.status === 'connected') {
|
||||||
|
var warning = false;
|
||||||
|
var msg = '';
|
||||||
|
if (this.env.pos.config.iface_scan_via_proxy) {
|
||||||
|
var scannerStatus = newStatus.drivers.scanner
|
||||||
|
? newStatus.drivers.scanner.status
|
||||||
|
: false;
|
||||||
|
if (scannerStatus != 'connected' && scannerStatus != 'connecting') {
|
||||||
|
warning = true;
|
||||||
|
msg += this.env._t('Scanner');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
this.env.pos.config.iface_print_via_proxy ||
|
||||||
|
this.env.pos.config.iface_cashdrawer
|
||||||
|
) {
|
||||||
|
var printerStatus = newStatus.drivers.printer
|
||||||
|
? newStatus.drivers.printer.status
|
||||||
|
: false;
|
||||||
|
if (printerStatus != 'connected' && printerStatus != 'connecting') {
|
||||||
|
warning = true;
|
||||||
|
msg = msg ? msg + ' & ' : msg;
|
||||||
|
msg += this.env._t('Printer');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.env.pos.config.iface_electronic_scale) {
|
||||||
|
var scaleStatus = newStatus.drivers.scale
|
||||||
|
? newStatus.drivers.scale.status
|
||||||
|
: false;
|
||||||
|
if (scaleStatus != 'connected' && scaleStatus != 'connecting') {
|
||||||
|
warning = true;
|
||||||
|
msg = msg ? msg + ' & ' : msg;
|
||||||
|
msg += this.env._t('Scale');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg = msg ? msg + ' ' + this.env._t('Offline') : msg;
|
||||||
|
|
||||||
|
this.state.status = warning ? 'warning' : 'connected';
|
||||||
|
this.state.msg = msg;
|
||||||
|
} else {
|
||||||
|
this.state.status = newStatus.status;
|
||||||
|
this.state.msg = newStatus.msg || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProxyStatus.template = 'ProxyStatus';
|
||||||
|
|
||||||
|
Registries.Component.add(ProxyStatus);
|
||||||
|
|
||||||
|
return ProxyStatus;
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
odoo.define('point_of_sale.SaleDetailsButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class SaleDetailsButton extends PosComponent {
|
||||||
|
async onClick() {
|
||||||
|
// IMPROVEMENT: Perhaps put this logic in a parent component
|
||||||
|
// so that for unit testing, we can check if this simple
|
||||||
|
// component correctly triggers an event.
|
||||||
|
const saleDetails = await this.rpc({
|
||||||
|
model: 'report.point_of_sale.report_saledetails',
|
||||||
|
method: 'get_sale_details',
|
||||||
|
args: [false, false, false, [this.env.pos.pos_session.id]],
|
||||||
|
});
|
||||||
|
const report = this.env.qweb.renderToString(
|
||||||
|
'SaleDetailsReport',
|
||||||
|
Object.assign({}, saleDetails, {
|
||||||
|
date: new Date().toLocaleString(),
|
||||||
|
pos: this.env.pos,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const printResult = await this.env.pos.proxy.printer.print_receipt(report);
|
||||||
|
if (!printResult.successful) {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: printResult.message.title,
|
||||||
|
body: printResult.message.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SaleDetailsButton.template = 'SaleDetailsButton';
|
||||||
|
|
||||||
|
Registries.Component.add(SaleDetailsButton);
|
||||||
|
|
||||||
|
return SaleDetailsButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
odoo.define('point_of_sale.SyncNotification', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// Previously SynchNotificationWidget
|
||||||
|
class SyncNotification extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
const synch = this.env.pos.get('synch');
|
||||||
|
this.state = useState({ status: synch.status, msg: synch.pending });
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.on(
|
||||||
|
'change:synch',
|
||||||
|
(pos, synch) => {
|
||||||
|
this.state.status = synch.status;
|
||||||
|
this.state.msg = synch.pending;
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.on('change:synch', null, this);
|
||||||
|
}
|
||||||
|
onClick() {
|
||||||
|
this.env.pos.push_orders(null, { show_error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SyncNotification.template = 'SyncNotification';
|
||||||
|
|
||||||
|
Registries.Component.add(SyncNotification);
|
||||||
|
|
||||||
|
return SyncNotification;
|
||||||
|
});
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
odoo.define('point_of_sale.ClassRegistry', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* **Usage:**
|
||||||
|
* ```
|
||||||
|
* const Registry = new ClassRegistry();
|
||||||
|
*
|
||||||
|
* class A {}
|
||||||
|
* Registry.add(A);
|
||||||
|
*
|
||||||
|
* const AExt1 = A => class extends A {}
|
||||||
|
* Registry.extend(A, AExt1)
|
||||||
|
*
|
||||||
|
* const B = A => class extends A {}
|
||||||
|
* Registry.addByExtending(B, A)
|
||||||
|
*
|
||||||
|
* const AExt2 = A => class extends A {}
|
||||||
|
* Registry.extend(A, AExt2)
|
||||||
|
*
|
||||||
|
* Registry.get(A)
|
||||||
|
* // above returns: AExt2 -> AExt1 -> A
|
||||||
|
* // Basically, 'A' in the registry points to
|
||||||
|
* // the inheritance chain above.
|
||||||
|
*
|
||||||
|
* Registry.get(B)
|
||||||
|
* // above returns: B -> AExt2 -> AExt1 -> A
|
||||||
|
* // Even though B extends A before applying all
|
||||||
|
* // the extensions of A, when getting it from the
|
||||||
|
* // registry, the return points to a class with
|
||||||
|
* // inheritance chain that includes all the extensions
|
||||||
|
* // of 'A'.
|
||||||
|
*
|
||||||
|
* Registry.freeze()
|
||||||
|
* // Example 'B' above is lazy. Basically, it is only
|
||||||
|
* // computed when we call `get` from the registry.
|
||||||
|
* // If we know that no more dynamic inheritances will happen,
|
||||||
|
* // we can freeze the registry and cache the final form
|
||||||
|
* // of each class in the registry.
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* IMPROVEMENT:
|
||||||
|
* * So far, mixin can be accomplished by creating a method
|
||||||
|
* the takes a class and returns a class expression. This is then
|
||||||
|
* used before the extends keyword like so:
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* class A {}
|
||||||
|
* Registry.add(A)
|
||||||
|
* const Mixin = x => class extends x {}
|
||||||
|
* // apply mixin
|
||||||
|
* // |
|
||||||
|
* // v
|
||||||
|
* const B = x => class extends Mixin(x) {}
|
||||||
|
* Registry.addByExtending(B, A)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* In the example, `|B| => B -> Mixin -> A`, and this is pretty convenient
|
||||||
|
* already. However, this can still be improved since classes are only
|
||||||
|
* compiled after `Registry.freeze()`. Perhaps, we can make the
|
||||||
|
* Mixins extensible as well, such as so:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* class A {}
|
||||||
|
* Registry.add(A)
|
||||||
|
* const Mixin = x => class extends x {}
|
||||||
|
* Registry.add(Mixin)
|
||||||
|
* const OtherMixin = x => class extends x {}
|
||||||
|
* Registry.add(OtherMixin)
|
||||||
|
* const B = x => class extends x {}
|
||||||
|
* Registry.addByExtending(B, A, [Mixin, OtherMixin])
|
||||||
|
* const ExtendMixin = x => class extends x {}
|
||||||
|
* Registry.extend(Mixin, ExtendMixin)
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* In the above, after `Registry.freeze()`,
|
||||||
|
* `|B| => B -> OtherMixin -> ExtendMixin -> Mixin -> A`
|
||||||
|
*/
|
||||||
|
class ClassRegistry {
|
||||||
|
constructor() {
|
||||||
|
// base name map
|
||||||
|
this.baseNameMap = {};
|
||||||
|
// Object that maps `baseClass` to the class implementation extended in-place.
|
||||||
|
this.includedMap = new Map();
|
||||||
|
// Object that maps `baseClassCB` to the array of callbacks to generate the extended class.
|
||||||
|
this.extendedCBMap = new Map();
|
||||||
|
// Object that maps `baseClassCB` extended class to the `baseClass` of its super in the includedMap.
|
||||||
|
this.extendedSuperMap = new Map();
|
||||||
|
// For faster access, we can `freeze` the registry so that instead of dynamically generating
|
||||||
|
// the extended classes, it is taken from the cache instead.
|
||||||
|
this.cache = new Map();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a new class in the Registry.
|
||||||
|
* @param {Function} baseClass `class`
|
||||||
|
*/
|
||||||
|
add(baseClass) {
|
||||||
|
this.includedMap.set(baseClass, []);
|
||||||
|
this.baseNameMap[baseClass.name] = baseClass;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add a new class in the Registry based on other class
|
||||||
|
* in the registry.
|
||||||
|
* @param {Function} baseClassCB `class -> class`
|
||||||
|
* @param {Function} base `class | class -> class`
|
||||||
|
*/
|
||||||
|
addByExtending(baseClassCB, base) {
|
||||||
|
this.extendedCBMap.set(baseClassCB, [baseClassCB]);
|
||||||
|
this.extendedSuperMap.set(baseClassCB, base);
|
||||||
|
this.baseNameMap[baseClassCB.name] = baseClassCB;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Extend in-place a class in the registry. E.g.
|
||||||
|
* ```
|
||||||
|
* // Using the following notation:
|
||||||
|
* // * |A| - compiled class in the registry
|
||||||
|
* // * A - class or an extension callback
|
||||||
|
* // * |A| => A2 -> A1 -> A
|
||||||
|
* // - the above means, compiled class A
|
||||||
|
* // points to the class inheritance derived from
|
||||||
|
* // A2(A1(A))
|
||||||
|
*
|
||||||
|
* class A {};
|
||||||
|
* Registry.add(A);
|
||||||
|
* // |A| => A
|
||||||
|
*
|
||||||
|
* let A1 = x => class extends x {};
|
||||||
|
* Registry.extend(A, A1);
|
||||||
|
* // |A| => A1 -> A
|
||||||
|
*
|
||||||
|
* let B = x => class extends x {};
|
||||||
|
* Registry.addByExtending(B, A);
|
||||||
|
* // |B| => B -> |A|
|
||||||
|
* // |B| => B -> A1 -> A
|
||||||
|
*
|
||||||
|
* let B1 = x => class extends x {};
|
||||||
|
* Registry.extend(B);
|
||||||
|
* // |B| => B1 -> B -> |A|
|
||||||
|
*
|
||||||
|
* let C = x => class extends x {};
|
||||||
|
* Registry.addByExtending(C, B);
|
||||||
|
* // |C| => C -> |B|
|
||||||
|
*
|
||||||
|
* let B2 = x => class extends x {};
|
||||||
|
* Registry.extend(B, B2);
|
||||||
|
* // |B| => B2 -> B1 -> B -> |A|
|
||||||
|
*
|
||||||
|
* // Overall:
|
||||||
|
* // |A| => A1 -> A
|
||||||
|
* // |B| => B2 -> B1 -> B -> A1 -> A
|
||||||
|
* // |C| => C -> B2 -> B1 -> B -> A1 -> A
|
||||||
|
* ```
|
||||||
|
* @param {Function} base `class | class -> class`
|
||||||
|
* @param {Function} extensionCB `class -> class`
|
||||||
|
*/
|
||||||
|
extend(base, extensionCB) {
|
||||||
|
if (typeof base === 'string') {
|
||||||
|
base = this.baseNameMap[base];
|
||||||
|
}
|
||||||
|
let extensionArray;
|
||||||
|
if (this.includedMap.get(base)) {
|
||||||
|
extensionArray = this.includedMap.get(base);
|
||||||
|
} else if (this.extendedCBMap.get(base)) {
|
||||||
|
extensionArray = this.extendedCBMap.get(base);
|
||||||
|
} else {
|
||||||
|
throw new Error(
|
||||||
|
`'${base.name}' is not in the Registry. Add it to Registry before extending.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
extensionArray.push(extensionCB);
|
||||||
|
const locOfNewExtension = extensionArray.length - 1;
|
||||||
|
const self = this;
|
||||||
|
const oldCompiled = this.isFrozen ? this.cache.get(base) : null;
|
||||||
|
return {
|
||||||
|
remove() {
|
||||||
|
extensionArray.splice(locOfNewExtension, 1);
|
||||||
|
self._recompute(base, oldCompiled);
|
||||||
|
},
|
||||||
|
compile() {
|
||||||
|
self._recompute(base);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_compile(base) {
|
||||||
|
let res;
|
||||||
|
if (this.includedMap.has(base)) {
|
||||||
|
res = this.includedMap.get(base).reduce((acc, ext) => ext(acc), base);
|
||||||
|
} else {
|
||||||
|
const superClass = this.extendedSuperMap.get(base);
|
||||||
|
const extensionCBs = this.extendedCBMap.get(base);
|
||||||
|
res = extensionCBs.reduce((acc, ext) => ext(acc), this._compile(superClass));
|
||||||
|
}
|
||||||
|
Object.defineProperty(res, 'name', { value: base.name });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the compiled class (containing all the extensions) of the base class.
|
||||||
|
* @param {Function} base `class | class -> class` function used in adding the class
|
||||||
|
*/
|
||||||
|
get(base) {
|
||||||
|
if (typeof base === 'string') {
|
||||||
|
base = this.baseNameMap[base];
|
||||||
|
}
|
||||||
|
if (this.isFrozen) {
|
||||||
|
return this.cache.get(base);
|
||||||
|
}
|
||||||
|
return this._compile(base);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Uses the callbacks registered in the registry to compile the classes.
|
||||||
|
*/
|
||||||
|
freeze() {
|
||||||
|
// Step: Compile the `included classes`.
|
||||||
|
for (let [baseClass, extensionCBs] of this.includedMap.entries()) {
|
||||||
|
const compiled = extensionCBs.reduce((acc, ext) => ext(acc), baseClass);
|
||||||
|
this.cache.set(baseClass, compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Compile the `extended classes` based on `included classes`.
|
||||||
|
// Also gather those the are based on `extended classes`.
|
||||||
|
const remaining = [];
|
||||||
|
for (let [baseClassCB, extensionCBArray] of this.extendedCBMap.entries()) {
|
||||||
|
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
|
||||||
|
if (!compiled) {
|
||||||
|
remaining.push([baseClassCB, extensionCBArray]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const extendedClass = extensionCBArray.reduce(
|
||||||
|
(acc, extensionCB) => extensionCB(acc),
|
||||||
|
compiled
|
||||||
|
);
|
||||||
|
this.cache.set(baseClassCB, extendedClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Compile the `extended classes` based on `extended classes`.
|
||||||
|
for (let [baseClassCB, extensionCBArray] of remaining) {
|
||||||
|
const compiled = this.cache.get(this.extendedSuperMap.get(baseClassCB));
|
||||||
|
const extendedClass = extensionCBArray.reduce(
|
||||||
|
(acc, extensionCB) => extensionCB(acc),
|
||||||
|
compiled
|
||||||
|
);
|
||||||
|
this.cache.set(baseClassCB, extendedClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Set the name of the compiled classess
|
||||||
|
for (let [base, compiledClass] of this.cache.entries()) {
|
||||||
|
Object.defineProperty(compiledClass, 'name', { value: base.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step: Set the flag to true;
|
||||||
|
this.isFrozen = true;
|
||||||
|
}
|
||||||
|
_recompute(base, old) {
|
||||||
|
if (typeof base === 'string') {
|
||||||
|
base = this.baseNameMap[base];
|
||||||
|
}
|
||||||
|
return old ? old : this._compile(base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ClassRegistry;
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
odoo.define('point_of_sale.ComponentRegistry', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const ClassRegistry = require('point_of_sale.ClassRegistry');
|
||||||
|
|
||||||
|
class ComponentRegistry extends ClassRegistry {
|
||||||
|
freeze() {
|
||||||
|
super.freeze();
|
||||||
|
// Make sure PosComponent has the compiled classes.
|
||||||
|
// This way, we don't need to explicitly declare that
|
||||||
|
// a set of components is children of another.
|
||||||
|
PosComponent.components = {};
|
||||||
|
for (let [base, compiledClass] of this.cache.entries()) {
|
||||||
|
PosComponent.components[base.name] = compiledClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_recompute(base, old) {
|
||||||
|
const res = super._recompute(base, old);
|
||||||
|
if (typeof base === 'string') {
|
||||||
|
base = this.baseNameMap[base];
|
||||||
|
}
|
||||||
|
PosComponent.components[base.name] = res;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComponentRegistry;
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
odoo.define('point_of_sale.Gui', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This module bridges the data classes (such as those defined in
|
||||||
|
* models.js) to the view (owl.Component) but not vice versa.
|
||||||
|
*
|
||||||
|
* The idea is to be able to perform side-effects to the user interface
|
||||||
|
* during calculation. Think of console.log during times we want to see
|
||||||
|
* the result of calculations. This is no different, except that instead
|
||||||
|
* of printing something in the console, we access a method in the user
|
||||||
|
* interface then the user interface reacts, e.g. calling `showPopup`.
|
||||||
|
*
|
||||||
|
* This however can be dangerous to the user interface as it can be possible
|
||||||
|
* that a rendered component is destroyed during the calculation. Because of
|
||||||
|
* this, we are going to limit external ui controls to those safe ones to
|
||||||
|
* use such as:
|
||||||
|
* - `showPopup`
|
||||||
|
* - `showTempScreen`
|
||||||
|
*
|
||||||
|
* IMPROVEMENT: After all, this Gui layer seems to be a good abstraction because
|
||||||
|
* there is a complete decoupling between data and view despite the data being
|
||||||
|
* able to use selected functionalities in the view layer. More formalized
|
||||||
|
* implementation is welcome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const config = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call this when the user interface is ready. Provide the component
|
||||||
|
* that will be used to control the ui.
|
||||||
|
* @param {owl.component} component component having the ui methods.
|
||||||
|
*/
|
||||||
|
const configureGui = ({ component }) => {
|
||||||
|
config.component = component;
|
||||||
|
config.availableMethods = new Set([
|
||||||
|
'showPopup',
|
||||||
|
'showTempScreen',
|
||||||
|
'playSound',
|
||||||
|
'setSyncStatus',
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import this and consume like so: `Gui.showPopup(<PopupName>, <props>)`.
|
||||||
|
* Like you would call `showPopup` in a component.
|
||||||
|
*/
|
||||||
|
const Gui = new Proxy(config, {
|
||||||
|
get(target, key) {
|
||||||
|
const { component, availableMethods } = target;
|
||||||
|
if (!component) throw new Error(`Call 'configureGui' before using Gui.`);
|
||||||
|
const isMounted = component.__owl__.isMounted;
|
||||||
|
if (availableMethods.has(key) && isMounted) {
|
||||||
|
return component[key].bind(component);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { configureGui, Gui };
|
||||||
|
});
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
odoo.define('point_of_sale.Draggable', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useExternalListener } = owl.hooks;
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap an element or a component with { position: absolute } to make it
|
||||||
|
* draggable around the limitArea or the nearest positioned ancestor.
|
||||||
|
*
|
||||||
|
* e.g.
|
||||||
|
* ```
|
||||||
|
* <div class="limit-area">
|
||||||
|
* <Draggable limitArea="'.limit-area'">
|
||||||
|
* <div class="popup">
|
||||||
|
* <header class="drag-handle"></header>
|
||||||
|
* </div>
|
||||||
|
* <div class="popup body"></div>
|
||||||
|
* </Draggable>
|
||||||
|
* </div>
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* In the above snippet, if the popup div is { position: absolute },
|
||||||
|
* then it becomes draggable around the .limit-area element if it is dragged
|
||||||
|
* thru its Header -- because of the .drag-handle element.
|
||||||
|
*
|
||||||
|
* @trigger 'drag-end' when dragging ended with payload `{ loc: { top, left } }`
|
||||||
|
*/
|
||||||
|
class Draggable extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.isDragging = false;
|
||||||
|
this.dx = 0;
|
||||||
|
this.dy = 0;
|
||||||
|
// drag with mouse
|
||||||
|
useExternalListener(document, 'mousemove', this.move);
|
||||||
|
useExternalListener(document, 'mouseup', this.endDrag);
|
||||||
|
// drag with touch
|
||||||
|
useExternalListener(document, 'touchmove', this.move);
|
||||||
|
useExternalListener(document, 'touchend', this.endDrag);
|
||||||
|
|
||||||
|
useListener('mousedown', '.drag-handle', this.startDrag);
|
||||||
|
useListener('touchstart', '.drag-handle', this.startDrag);
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// absolutely position the element then remove the transform.
|
||||||
|
const elBoundingRect = this.el.getBoundingClientRect();
|
||||||
|
this.el.style.top = `${elBoundingRect.top}px`;
|
||||||
|
this.el.style.left = `${elBoundingRect.left}px`;
|
||||||
|
this.el.style.transform = 'none';
|
||||||
|
}
|
||||||
|
startDrag(event) {
|
||||||
|
let realEvent;
|
||||||
|
if (event instanceof CustomEvent) {
|
||||||
|
realEvent = event.detail;
|
||||||
|
} else {
|
||||||
|
realEvent = event;
|
||||||
|
}
|
||||||
|
const { x, y } = this._getEventLoc(realEvent);
|
||||||
|
this.isDragging = true;
|
||||||
|
this.dx = this.el.offsetLeft - x;
|
||||||
|
this.dy = this.el.offsetTop - y;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
move(event) {
|
||||||
|
if (this.isDragging) {
|
||||||
|
const { x: pointerX, y: pointerY } = this._getEventLoc(event);
|
||||||
|
const posLeft = this._getPosLeft(pointerX, this.dx);
|
||||||
|
const posTop = this._getPosTop(pointerY, this.dy);
|
||||||
|
this.el.style.left = `${posLeft}px`;
|
||||||
|
this.el.style.top = `${posTop}px`;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
endDrag() {
|
||||||
|
if (this.isDragging) {
|
||||||
|
this.isDragging = false;
|
||||||
|
this.trigger('drag-end', {
|
||||||
|
loc: { top: this.el.offsetTop, left: this.el.offsetLeft },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_getPosLeft(pointerX, dx) {
|
||||||
|
const posLeft = pointerX + dx;
|
||||||
|
if (posLeft < this.limitLeft) {
|
||||||
|
return this.limitLeft;
|
||||||
|
} else if (posLeft > this.limitRight - this.el.offsetWidth) {
|
||||||
|
return this.limitRight - this.el.offsetWidth;
|
||||||
|
}
|
||||||
|
return posLeft;
|
||||||
|
}
|
||||||
|
_getPosTop(pointerY, dy) {
|
||||||
|
const posTop = pointerY + dy;
|
||||||
|
if (posTop < this.limitTop) {
|
||||||
|
return this.limitTop;
|
||||||
|
} else if (posTop > this.limitBottom - this.el.offsetHeight) {
|
||||||
|
return this.limitBottom - this.el.offsetHeight;
|
||||||
|
}
|
||||||
|
return posTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Draggable.template = 'Draggable';
|
||||||
|
|
||||||
|
Registries.Component.add(Draggable);
|
||||||
|
|
||||||
|
return Draggable;
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
odoo.define('point_of_sale.NotificationSound', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class NotificationSound extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('ended', () => (this.props.sound.src = null));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotificationSound.template = 'NotificationSound';
|
||||||
|
|
||||||
|
Registries.Component.add(NotificationSound);
|
||||||
|
|
||||||
|
return NotificationSound;
|
||||||
|
});
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
odoo.define('point_of_sale.NumberBuffer', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Component, useState } = owl;
|
||||||
|
const { EventBus } = owl.core;
|
||||||
|
const { onMounted, onWillUnmount, useExternalListener } = owl.hooks;
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const { parse } = require('web.field_utils');
|
||||||
|
const { BarcodeEvents } = require('barcodes.BarcodeEvents');
|
||||||
|
const { _t } = require('web.core');
|
||||||
|
const { Gui } = require('point_of_sale.Gui');
|
||||||
|
|
||||||
|
const INPUT_KEYS = new Set(
|
||||||
|
['Delete', 'Backspace', '+1', '+2', '+5', '+10', '+20', '+50'].concat('0123456789+-.,'.split(''))
|
||||||
|
);
|
||||||
|
const CONTROL_KEYS = new Set(['Enter', 'Esc']);
|
||||||
|
const ALLOWED_KEYS = new Set([...INPUT_KEYS, ...CONTROL_KEYS]);
|
||||||
|
const getDefaultConfig = () => ({
|
||||||
|
decimalPoint: false,
|
||||||
|
triggerAtEnter: false,
|
||||||
|
triggerAtEsc: false,
|
||||||
|
triggerAtInput: false,
|
||||||
|
nonKeyboardInputEvent: false,
|
||||||
|
useWithBarcode: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a singleton.
|
||||||
|
*
|
||||||
|
* Only one component can `use` the buffer at a time.
|
||||||
|
* This is done by keeping track of each component (and its
|
||||||
|
* corresponding state and config) using a stack (bufferHolderStack).
|
||||||
|
* The component on top of the stack is the one that currently
|
||||||
|
* `holds` the buffer.
|
||||||
|
*
|
||||||
|
* When the current component is unmounted, the top of the stack
|
||||||
|
* is popped and NumberBuffer is set up again for the new component
|
||||||
|
* on top of the stack.
|
||||||
|
*
|
||||||
|
* Usage
|
||||||
|
* =====
|
||||||
|
* - Activate in the construction of root component. `NumberBuffer.activate()`
|
||||||
|
* - Use the buffer in a child component by calling `NumberBuffer.use(<config>)`
|
||||||
|
* in the constructor of the child component.
|
||||||
|
* - The component that `uses` the buffer has access to the following instance
|
||||||
|
* methods of the NumberBuffer:
|
||||||
|
* - get()
|
||||||
|
* - set(val)
|
||||||
|
* - reset()
|
||||||
|
* - getFloat()
|
||||||
|
*
|
||||||
|
* Note
|
||||||
|
* ====
|
||||||
|
* - No need to instantiate as it is a single created before exporting in this module.
|
||||||
|
*
|
||||||
|
* Possible Improvements
|
||||||
|
* =====================
|
||||||
|
* - Relieve the buffer from responsibility of handling `Enter` and other control keys.
|
||||||
|
* - Make the constants (ALLOWED_KEYS, etc.) more configurable.
|
||||||
|
*/
|
||||||
|
class NumberBuffer extends EventBus {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.isReset = false;
|
||||||
|
this.bufferHolderStack = [];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {String} value of the buffer, e.g. '-95.79'
|
||||||
|
*/
|
||||||
|
get() {
|
||||||
|
return this.state ? this.state.buffer : null;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Takes a string that is convertible to float, and set it as
|
||||||
|
* value of the buffer. e.g. val = '2.99';
|
||||||
|
*
|
||||||
|
* @param {String} val
|
||||||
|
*/
|
||||||
|
set(val) {
|
||||||
|
this.state.buffer = !isNaN(parseFloat(val)) ? val : '';
|
||||||
|
this.trigger('buffer-update', this.state.buffer);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resets the buffer to empty string.
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.isReset = true;
|
||||||
|
this.state.buffer = '';
|
||||||
|
this.trigger('buffer-update', this.state.buffer);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @returns {number} float equivalent of the value of buffer
|
||||||
|
*/
|
||||||
|
getFloat() {
|
||||||
|
return parse.float(this.get());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Add keyup listener to window via the useExternalListener hook.
|
||||||
|
* When the component calling this is unmounted, the listener is also
|
||||||
|
* removed from window.
|
||||||
|
*/
|
||||||
|
activate() {
|
||||||
|
this.defaultDecimalPoint = _t.database.parameters.decimal_point;
|
||||||
|
useExternalListener(window, 'keyup', this._onKeyboardInput.bind(this));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Object} config Use to setup the buffer
|
||||||
|
* @param {String|null} config.decimalPoint The decimal character.
|
||||||
|
* @param {String|null} config.triggerAtEnter Event triggered when 'Enter' key is pressed.
|
||||||
|
* @param {String|null} config.triggerAtInput Event triggered for every accepted input.
|
||||||
|
* @param {String|null} config.nonKeyboardInputEvent Also listen to a non-keyboard input event
|
||||||
|
* that carries a payload of { key }. The key is checked if it is a valid input. If valid,
|
||||||
|
* the number buffer is modified just as it is modified when a keyboard key is pressed.
|
||||||
|
* @param {Boolean} config.useWithBarcode Whether this buffer is used with barcode.
|
||||||
|
*/
|
||||||
|
use(config) {
|
||||||
|
this.eventsBuffer = [];
|
||||||
|
const currentComponent = Component.current;
|
||||||
|
config = Object.assign(getDefaultConfig(), config);
|
||||||
|
onMounted(() => {
|
||||||
|
this.bufferHolderStack.push({
|
||||||
|
component: currentComponent,
|
||||||
|
state: config.state ? config.state : useState({ buffer: '' }),
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
this._setUp();
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
this.bufferHolderStack.pop();
|
||||||
|
this._setUp();
|
||||||
|
});
|
||||||
|
// Add listener that accepts non keyboard inputs
|
||||||
|
if (typeof config.nonKeyboardInputEvent === 'string') {
|
||||||
|
useListener(config.nonKeyboardInputEvent, this._onNonKeyboardInput.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get _currentBufferHolder() {
|
||||||
|
return this.bufferHolderStack[this.bufferHolderStack.length - 1];
|
||||||
|
}
|
||||||
|
_setUp() {
|
||||||
|
if (!this._currentBufferHolder) return;
|
||||||
|
const { component, state, config } = this._currentBufferHolder;
|
||||||
|
this.component = component;
|
||||||
|
this.state = state;
|
||||||
|
this.config = config;
|
||||||
|
this.decimalPoint = config.decimalPoint || this.defaultDecimalPoint;
|
||||||
|
this.maxTimeBetweenKeys = this.config.useWithBarcode
|
||||||
|
? BarcodeEvents.max_time_between_keys_in_ms
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
_onKeyboardInput(event) {
|
||||||
|
return this._bufferEvents(this._onInput(event => event.key))(event);
|
||||||
|
}
|
||||||
|
_onNonKeyboardInput(event) {
|
||||||
|
return this._bufferEvents(this._onInput(event => event.detail.key))(event);
|
||||||
|
}
|
||||||
|
_bufferEvents(handler) {
|
||||||
|
return event => {
|
||||||
|
if (['INPUT', 'TEXTAREA'].includes(event.target.tagName) || !this.eventsBuffer) return;
|
||||||
|
clearTimeout(this._timeout);
|
||||||
|
this.eventsBuffer.push(event);
|
||||||
|
this._timeout = setTimeout(handler, this.maxTimeBetweenKeys);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_onInput(keyAccessor) {
|
||||||
|
return () => {
|
||||||
|
if (this.eventsBuffer.length <= 2) {
|
||||||
|
// Check first the buffer if its contents are all valid
|
||||||
|
// number input.
|
||||||
|
for (let event of this.eventsBuffer) {
|
||||||
|
if (!ALLOWED_KEYS.has(keyAccessor(event))) {
|
||||||
|
this.eventsBuffer = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// At this point, all the events in buffer
|
||||||
|
// contains number input. It's now okay to handle
|
||||||
|
// each input.
|
||||||
|
for (let event of this.eventsBuffer) {
|
||||||
|
this._handleInput(keyAccessor(event));
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.eventsBuffer = [];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_handleInput(key) {
|
||||||
|
if (key === 'Enter' && this.config.triggerAtEnter) {
|
||||||
|
this.component.trigger(this.config.triggerAtEnter, this.state);
|
||||||
|
} else if (key === 'Esc' && this.config.triggerAtEsc) {
|
||||||
|
this.component.trigger(this.config.triggerAtEsc, this.state);
|
||||||
|
} else if (INPUT_KEYS.has(key)) {
|
||||||
|
this._updateBuffer(key);
|
||||||
|
if (this.config.triggerAtInput)
|
||||||
|
this.component.trigger(this.config.triggerAtInput, { buffer: this.state.buffer, key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Updates the current buffer state using the given input.
|
||||||
|
* @param {String} input valid input
|
||||||
|
*/
|
||||||
|
_updateBuffer(input) {
|
||||||
|
const isEmpty = val => {
|
||||||
|
return val === '' || val === null;
|
||||||
|
};
|
||||||
|
if (input === undefined || input === null) return;
|
||||||
|
let isFirstInput = isEmpty(this.state.buffer);
|
||||||
|
if (input === this.decimalPoint) {
|
||||||
|
if (isFirstInput) {
|
||||||
|
this.state.buffer = '0.';
|
||||||
|
} else if (!this.state.buffer.length || this.state.buffer === '-') {
|
||||||
|
this.state.buffer += '0.';
|
||||||
|
} else if (this.state.buffer.indexOf(this.decimalPoint) < 0) {
|
||||||
|
this.state.buffer = this.state.buffer + this.decimalPoint;
|
||||||
|
}
|
||||||
|
} else if (input === 'Delete') {
|
||||||
|
if (this.isReset) {
|
||||||
|
this.state.buffer = '';
|
||||||
|
this.isReset = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.buffer = isEmpty(this.state.buffer) ? null : '';
|
||||||
|
} else if (input === 'Backspace') {
|
||||||
|
if (this.isReset) {
|
||||||
|
this.state.buffer = '';
|
||||||
|
this.isReset = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = this.state.buffer;
|
||||||
|
if (isEmpty(buffer)) {
|
||||||
|
this.state.buffer = null;
|
||||||
|
} else {
|
||||||
|
const nCharToRemove = buffer[buffer.length - 1] === this.decimalPoint ? 2 : 1;
|
||||||
|
this.state.buffer = buffer.substring(0, buffer.length - nCharToRemove);
|
||||||
|
}
|
||||||
|
} else if (input === '+') {
|
||||||
|
if (this.state.buffer[0] === '-') {
|
||||||
|
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
|
||||||
|
}
|
||||||
|
} else if (input === '-') {
|
||||||
|
if (isFirstInput) {
|
||||||
|
this.state.buffer = '-0';
|
||||||
|
} else if (this.state.buffer[0] === '-') {
|
||||||
|
this.state.buffer = this.state.buffer.substring(1, this.state.buffer.length);
|
||||||
|
} else {
|
||||||
|
this.state.buffer = '-' + this.state.buffer;
|
||||||
|
}
|
||||||
|
} else if (input[0] === '+' && !isNaN(parseFloat(input))) {
|
||||||
|
// when input is like '+10', '+50', etc
|
||||||
|
const inputValue = parse.float(input.slice(1));
|
||||||
|
const currentBufferValue = this.state.buffer ? parse.float(this.state.buffer) : 0;
|
||||||
|
this.state.buffer = this.component.env.pos.formatFixed(
|
||||||
|
inputValue + currentBufferValue
|
||||||
|
);
|
||||||
|
} else if (!isNaN(parseInt(input, 10))) {
|
||||||
|
if (isFirstInput) {
|
||||||
|
this.state.buffer = '' + input;
|
||||||
|
} else if (this.state.buffer.length > 12) {
|
||||||
|
Gui.playSound('bell');
|
||||||
|
} else {
|
||||||
|
this.state.buffer += input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.state.buffer === '-') {
|
||||||
|
this.state.buffer = '';
|
||||||
|
}
|
||||||
|
// once an input is accepted and updated the buffer,
|
||||||
|
// the buffer should not be in reset state anymore.
|
||||||
|
this.isReset = false;
|
||||||
|
|
||||||
|
this.trigger('buffer-update', this.state.buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NumberBuffer();
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
odoo.define('point_of_sale.PopupControllerMixin', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the component declared with this mixin the ability show popup dynamically,
|
||||||
|
* provided the following:
|
||||||
|
* 1. The following element is declared in the template. It is where the Popup will be rendered.
|
||||||
|
* `<t t-if="popup.isShown" t-component="popup.component" t-props="popupProps" t-key="popup.name" />`
|
||||||
|
* 2. The component should trigger `show-popup` event to show the popup and `close-popup` event
|
||||||
|
* to close. In PosComponent, `showPopup` is conveniently declared to satisfy this requirement.
|
||||||
|
* @param {Function} x class definition to mix with during extension
|
||||||
|
*/
|
||||||
|
const PopupControllerMixin = x =>
|
||||||
|
class extends x {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('show-popup', this.__showPopup);
|
||||||
|
useListener('close-popup', this.__closePopup);
|
||||||
|
|
||||||
|
this.popup = useState({ isShown: false, name: null, component: null });
|
||||||
|
this.popupProps = {}; // We want to avoid making the props to become Proxy!
|
||||||
|
}
|
||||||
|
__showPopup(event) {
|
||||||
|
const { name, props, resolve } = event.detail;
|
||||||
|
const popupConstructor = this.constructor.components[name];
|
||||||
|
if (popupConstructor.dontShow) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.popup.isShown = true;
|
||||||
|
this.popup.name = name;
|
||||||
|
this.popup.component = popupConstructor;
|
||||||
|
this.popupProps = Object.assign({}, props, { resolve });
|
||||||
|
}
|
||||||
|
__closePopup() {
|
||||||
|
this.popup.isShown = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return PopupControllerMixin;
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useExternalListener } = owl.hooks;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implement this abstract class by extending it like so:
|
||||||
|
* ```js
|
||||||
|
* class ConcretePopup extends AbstractAwaitablePopup {
|
||||||
|
* async getPayload() {
|
||||||
|
* return 'result';
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ConcretePopup.template = owl.tags.xml`
|
||||||
|
* <div>
|
||||||
|
* <button t-on-click="confirm">Okay</button>
|
||||||
|
* <button t-on-click="cancel">Cancel</button>
|
||||||
|
* </div>
|
||||||
|
* `
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The concrete popup can now be instantiated and be awaited for
|
||||||
|
* the user's response like so:
|
||||||
|
* ```js
|
||||||
|
* const { confirmed, payload } = await this.showPopup('ConcretePopup');
|
||||||
|
* // based on the implementation above,
|
||||||
|
* // if confirmed, payload = 'result'
|
||||||
|
* // otherwise, payload = null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class AbstractAwaitablePopup extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useExternalListener(window, 'keyup', this._cancelAtEscape);
|
||||||
|
}
|
||||||
|
async confirm() {
|
||||||
|
this.props.resolve({ confirmed: true, payload: await this.getPayload() });
|
||||||
|
this.trigger('close-popup');
|
||||||
|
}
|
||||||
|
cancel() {
|
||||||
|
this.props.resolve({ confirmed: false, payload: null });
|
||||||
|
this.trigger('close-popup');
|
||||||
|
}
|
||||||
|
_cancelAtEscape(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Override this in the concrete popup implementation to set the
|
||||||
|
* payload when the popup is confirmed.
|
||||||
|
*/
|
||||||
|
async getPayload() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AbstractAwaitablePopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
odoo.define('point_of_sale.ConfirmPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly ConfirmPopupWidget
|
||||||
|
class ConfirmPopup extends AbstractAwaitablePopup {}
|
||||||
|
ConfirmPopup.template = 'ConfirmPopup';
|
||||||
|
ConfirmPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Confirm ?',
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(ConfirmPopup);
|
||||||
|
|
||||||
|
return ConfirmPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
odoo.define('point_of_sale.EditListInput', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class EditListInput extends PosComponent {
|
||||||
|
onKeyup(event) {
|
||||||
|
if (event.key === "Enter" && event.target.value.trim() !== '') {
|
||||||
|
this.trigger('create-new-item');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditListInput.template = 'EditListInput';
|
||||||
|
|
||||||
|
Registries.Component.add(EditListInput);
|
||||||
|
|
||||||
|
return EditListInput;
|
||||||
|
});
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
odoo.define('point_of_sale.EditListPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
const { useAutoFocusToLast } = require('point_of_sale.custom_hooks');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a array of { id, text }, we show the user this popup to be able to modify this given array.
|
||||||
|
* (used to replace PackLotLinePopupWidget)
|
||||||
|
*
|
||||||
|
* The expected return of showPopup when this popup is used is an array of { _id, [id], text }.
|
||||||
|
* - _id is the assigned unique identifier for each item.
|
||||||
|
* - id is the original id. if not provided, then it means that the item is new.
|
||||||
|
* - text is the modified/unmodified text.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* -- perhaps inside a click handler --
|
||||||
|
* // gather the items to edit
|
||||||
|
* const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }];
|
||||||
|
*
|
||||||
|
* // supply the items to the popup and wait for user's response
|
||||||
|
* // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function.
|
||||||
|
* const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', {
|
||||||
|
* title: "Can you confirm this item?",
|
||||||
|
* array: names })
|
||||||
|
*
|
||||||
|
* // we then consume the new data. In this example, it is only logged.
|
||||||
|
* if (confirmed) {
|
||||||
|
* console.log(newNames);
|
||||||
|
* // the above might log the following:
|
||||||
|
* // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }]
|
||||||
|
* // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph'
|
||||||
|
* // The one with id=2 did not change. And a new item with text='James' is added.
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class EditListPopup extends AbstractAwaitablePopup {
|
||||||
|
/**
|
||||||
|
* @param {String} title required title of popup
|
||||||
|
* @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings
|
||||||
|
* @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item)
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this._id = 0;
|
||||||
|
this.state = useState({ array: this._initialize(this.props.array) });
|
||||||
|
useAutoFocusToLast();
|
||||||
|
}
|
||||||
|
_nextId() {
|
||||||
|
return this._id++;
|
||||||
|
}
|
||||||
|
_emptyItem() {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
_id: this._nextId(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_initialize(array) {
|
||||||
|
// If no array is provided, we initialize with one empty item.
|
||||||
|
if (array.length === 0) return [this._emptyItem()];
|
||||||
|
// Put _id for each item. It will serve as unique identifier of each item.
|
||||||
|
return array.map((item) => Object.assign({}, { _id: this._nextId() }, typeof item === 'object'? item: { 'text': item}));
|
||||||
|
}
|
||||||
|
removeItem(event) {
|
||||||
|
const itemToRemove = event.detail;
|
||||||
|
this.state.array.splice(
|
||||||
|
this.state.array.findIndex(item => item._id == itemToRemove._id),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
// We keep a minimum of one empty item in the popup.
|
||||||
|
if (this.state.array.length === 0) {
|
||||||
|
this.state.array.push(this._emptyItem());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createNewItem() {
|
||||||
|
if (this.props.isSingleItem) return;
|
||||||
|
this.state.array.push(this._emptyItem());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
getPayload() {
|
||||||
|
return {
|
||||||
|
newArray: this.state.array
|
||||||
|
.filter((item) => item.text.trim() !== '')
|
||||||
|
.map((item) => Object.assign({}, item)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EditListPopup.template = 'EditListPopup';
|
||||||
|
EditListPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
array: [],
|
||||||
|
isSingleItem: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(EditListPopup);
|
||||||
|
|
||||||
|
return EditListPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
odoo.define('point_of_sale.ErrorBarcodePopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly ErrorBarcodePopupWidget
|
||||||
|
class ErrorBarcodePopup extends ErrorPopup {
|
||||||
|
get translatedMessage() {
|
||||||
|
return this.env._t(this.props.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorBarcodePopup.template = 'ErrorBarcodePopup';
|
||||||
|
ErrorBarcodePopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Error',
|
||||||
|
body: '',
|
||||||
|
message:
|
||||||
|
'The Point of Sale could not find any product, client, employee or action associated with the scanned barcode.',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(ErrorBarcodePopup);
|
||||||
|
|
||||||
|
return ErrorBarcodePopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
odoo.define('point_of_sale.ErrorPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly ErrorPopupWidget
|
||||||
|
class ErrorPopup extends AbstractAwaitablePopup {
|
||||||
|
mounted() {
|
||||||
|
this.playSound('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorPopup.template = 'ErrorPopup';
|
||||||
|
ErrorPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Error',
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(ErrorPopup);
|
||||||
|
|
||||||
|
return ErrorPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
odoo.define('point_of_sale.ErrorTracebackPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly ErrorTracebackPopupWidget
|
||||||
|
class ErrorTracebackPopup extends ErrorPopup {
|
||||||
|
get tracebackUrl() {
|
||||||
|
const blob = new Blob([this.props.body]);
|
||||||
|
const URL = window.URL || window.webkitURL;
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
}
|
||||||
|
get tracebackFilename() {
|
||||||
|
return `${this.env._t('error')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.txt`;
|
||||||
|
}
|
||||||
|
emailTraceback() {
|
||||||
|
const address = this.env.pos.company.email;
|
||||||
|
const subject = this.env._t('IMPORTANT: Bug Report From Odoo Point Of Sale');
|
||||||
|
window.open(
|
||||||
|
'mailto:' +
|
||||||
|
address +
|
||||||
|
'?subject=' +
|
||||||
|
(subject ? window.encodeURIComponent(subject) : '') +
|
||||||
|
'&body=' +
|
||||||
|
(this.props.body ? window.encodeURIComponent(this.props.body) : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ErrorTracebackPopup.template = 'ErrorTracebackPopup';
|
||||||
|
ErrorTracebackPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Error with Traceback',
|
||||||
|
body: '',
|
||||||
|
exitButtonIsShown: false,
|
||||||
|
exitButtonText: 'Exit Pos',
|
||||||
|
exitButtonTrigger: 'close-pos'
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(ErrorTracebackPopup);
|
||||||
|
|
||||||
|
return ErrorTracebackPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
odoo.define('point_of_sale.NumberPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly NumberPopupWidget
|
||||||
|
class NumberPopup extends AbstractAwaitablePopup {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Boolean} props.isPassword Show password popup.
|
||||||
|
* @param {number|null} props.startingValue Starting value of the popup.
|
||||||
|
*
|
||||||
|
* Resolve to { confirmed, payload } when used with showPopup method.
|
||||||
|
* @confirmed {Boolean}
|
||||||
|
* @payload {String}
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('accept-input', this.confirm);
|
||||||
|
useListener('close-this-popup', this.cancel);
|
||||||
|
let startingBuffer = '';
|
||||||
|
if (typeof this.props.startingValue === 'number' && this.props.startingValue > 0) {
|
||||||
|
startingBuffer = this.props.startingValue.toString();
|
||||||
|
}
|
||||||
|
this.state = useState({ buffer: startingBuffer });
|
||||||
|
NumberBuffer.use({
|
||||||
|
nonKeyboardInputEvent: 'numpad-click-input',
|
||||||
|
triggerAtEnter: 'accept-input',
|
||||||
|
triggerAtEscape: 'close-this-popup',
|
||||||
|
state: this.state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
get decimalSeparator() {
|
||||||
|
return this.env._t.database.parameters.decimal_point;
|
||||||
|
}
|
||||||
|
get inputBuffer() {
|
||||||
|
if (this.state.buffer === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (this.props.isPassword) {
|
||||||
|
return this.state.buffer.replace(/./g, '•');
|
||||||
|
} else {
|
||||||
|
return this.state.buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendInput(key) {
|
||||||
|
this.trigger('numpad-click-input', { key });
|
||||||
|
}
|
||||||
|
getPayload() {
|
||||||
|
return NumberBuffer.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NumberPopup.template = 'NumberPopup';
|
||||||
|
NumberPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Confirm ?',
|
||||||
|
body: '',
|
||||||
|
cheap: false,
|
||||||
|
startingValue: null,
|
||||||
|
isPassword: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(NumberPopup);
|
||||||
|
|
||||||
|
return NumberPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
odoo.define('point_of_sale.OfflineErrorPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a special kind of error popup as it introduces
|
||||||
|
* an option to not show it again.
|
||||||
|
*/
|
||||||
|
class OfflineErrorPopup extends ErrorPopup {
|
||||||
|
dontShowAgain() {
|
||||||
|
this.constructor.dontShow = true;
|
||||||
|
this.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OfflineErrorPopup.template = 'OfflineErrorPopup';
|
||||||
|
OfflineErrorPopup.dontShow = false;
|
||||||
|
OfflineErrorPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Offline Error',
|
||||||
|
body: 'Either the server is inaccessible or browser is not connected online.',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(OfflineErrorPopup);
|
||||||
|
|
||||||
|
return OfflineErrorPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
odoo.define('point_of_sale.OrderImportPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly OrderImportPopupWidget
|
||||||
|
class OrderImportPopup extends AbstractAwaitablePopup {
|
||||||
|
get unpaidSkipped() {
|
||||||
|
return (
|
||||||
|
(this.props.report.unpaid_skipped_existing || 0) +
|
||||||
|
(this.props.report.unpaid_skipped_session || 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
getPayload() {}
|
||||||
|
}
|
||||||
|
OrderImportPopup.template = 'OrderImportPopup';
|
||||||
|
OrderImportPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(OrderImportPopup);
|
||||||
|
|
||||||
|
return OrderImportPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
odoo.define('point_of_sale.SelectionPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly SelectionPopupWidget
|
||||||
|
class SelectionPopup extends AbstractAwaitablePopup {
|
||||||
|
/**
|
||||||
|
* Value of the `item` key of the selected element in the Selection
|
||||||
|
* Array is the payload of this popup.
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {String} [props.confirmText='Confirm']
|
||||||
|
* @param {String} [props.cancelText='Cancel']
|
||||||
|
* @param {String} [props.title='Select']
|
||||||
|
* @param {String} [props.body='']
|
||||||
|
* @param {Array<Selection>} [props.list=[]]
|
||||||
|
* Selection {
|
||||||
|
* id: integer,
|
||||||
|
* label: string,
|
||||||
|
* isSelected: boolean,
|
||||||
|
* item: any,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.list = useState([...this.props.list]);
|
||||||
|
}
|
||||||
|
selectItem(itemId) {
|
||||||
|
for (let item of this.list) {
|
||||||
|
if (item.id === itemId) {
|
||||||
|
item.isSelected = true;
|
||||||
|
} else {
|
||||||
|
item.isSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.confirm();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* We send as payload of the response the selected item.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
getPayload() {
|
||||||
|
const selected = this.props.list.find(item => item.isSelected);
|
||||||
|
return selected && selected.item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectionPopup.template = 'SelectionPopup';
|
||||||
|
SelectionPopup.defaultProps = {
|
||||||
|
confirmText: 'Confirm',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: 'Select',
|
||||||
|
body: '',
|
||||||
|
list: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(SelectionPopup);
|
||||||
|
|
||||||
|
return SelectionPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
odoo.define('point_of_sale.TextAreaPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState, useRef } = owl.hooks;
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly TextAreaPopupWidget
|
||||||
|
// IMPROVEMENT: This code is very similar to TextInputPopup.
|
||||||
|
// Combining them would reduce the code.
|
||||||
|
class TextAreaPopup extends AbstractAwaitablePopup {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {string} props.startingValue
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({ inputValue: this.props.startingValue });
|
||||||
|
this.inputRef = useRef('input');
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.inputRef.el.focus();
|
||||||
|
}
|
||||||
|
getPayload() {
|
||||||
|
return this.state.inputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextAreaPopup.template = 'TextAreaPopup';
|
||||||
|
TextAreaPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: '',
|
||||||
|
body: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(TextAreaPopup);
|
||||||
|
|
||||||
|
return TextAreaPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
odoo.define('point_of_sale.TextInputPopup', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState, useRef } = owl.hooks;
|
||||||
|
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
// formerly TextInputPopupWidget
|
||||||
|
class TextInputPopup extends AbstractAwaitablePopup {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({ inputValue: this.props.startingValue });
|
||||||
|
this.inputRef = useRef('input');
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.inputRef.el.focus();
|
||||||
|
}
|
||||||
|
getPayload() {
|
||||||
|
return this.state.inputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextInputPopup.template = 'TextInputPopup';
|
||||||
|
TextInputPopup.defaultProps = {
|
||||||
|
confirmText: 'Ok',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
title: '',
|
||||||
|
body: '',
|
||||||
|
startingValue: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(TextInputPopup);
|
||||||
|
|
||||||
|
return TextInputPopup;
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
odoo.define('point_of_sale.PosComponent', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Component } = owl;
|
||||||
|
|
||||||
|
class PosComponent extends Component {
|
||||||
|
/**
|
||||||
|
* This function is available to all Components that inherit this class.
|
||||||
|
* The goal of this function is to show an awaitable dialog (popup) that
|
||||||
|
* returns a response after user interaction. See the following for quick
|
||||||
|
* demonstration:
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* async getUserName() {
|
||||||
|
* const userResponse = await this.showPopup(
|
||||||
|
* 'TextInputPopup',
|
||||||
|
* { title: 'What is your name?' }
|
||||||
|
* );
|
||||||
|
* // at this point, the TextInputPopup is displayed. Depending on how the popup is defined,
|
||||||
|
* // say the input contains the name, the result of the interaction with the user is
|
||||||
|
* // saved in `userResponse`.
|
||||||
|
* console.log(userResponse); // logs { confirmed: true, payload: <name> }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {String} name Name of the popup component
|
||||||
|
* @param {Object} props Object that will be used to render to popup
|
||||||
|
*/
|
||||||
|
showPopup(name, props) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.trigger('show-popup', { name, props, resolve });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showTempScreen(name, props) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.trigger('show-temp-screen', { name, props, resolve });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showScreen(name, props) {
|
||||||
|
this.trigger('show-main-screen', { name, props });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {String} name 'bell' | 'error'
|
||||||
|
*/
|
||||||
|
playSound(name) {
|
||||||
|
this.trigger('play-sound', name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Control the SyncNotification component.
|
||||||
|
* @param {String} status 'connected' | 'connecting' | 'disconnected' | 'error'
|
||||||
|
* @param {String} pending number of pending orders to sync
|
||||||
|
*/
|
||||||
|
setSyncStatus(status, pending) {
|
||||||
|
this.trigger('set-sync-status', { status, pending });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return PosComponent;
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
odoo.define('point_of_sale.Registries', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This definition contains all the instances of ClassRegistry.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ComponentRegistry = require('point_of_sale.ComponentRegistry');
|
||||||
|
|
||||||
|
return { Component: new ComponentRegistry() };
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
odoo.define('point_of_sale.ClientDetails', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ClientDetails extends PosComponent {
|
||||||
|
get partnerImageUrl() {
|
||||||
|
const partner = this.props.partner;
|
||||||
|
if (partner) {
|
||||||
|
return `/web/image?model=res.partner&id=${partner.id}&field=image_128&write_date=${partner.write_date}&unique=1`;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientDetails.template = 'ClientDetails';
|
||||||
|
|
||||||
|
Registries.Component.add(ClientDetails);
|
||||||
|
|
||||||
|
return ClientDetails;
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
odoo.define('point_of_sale.ClientDetailsEdit', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { getDataURLFromFile } = require('web.utils');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ClientDetailsEdit extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.intFields = ['country_id', 'state_id', 'property_product_pricelist'];
|
||||||
|
this.changes = {};
|
||||||
|
}
|
||||||
|
get partnerImageUrl() {
|
||||||
|
// We prioritize image_1920 in the `changes` field because we want
|
||||||
|
// to show the uploaded image without fetching new data from the server.
|
||||||
|
const partner = this.props.partner;
|
||||||
|
if (this.changes.image_1920) {
|
||||||
|
return this.changes.image_1920;
|
||||||
|
} else if (partner.id) {
|
||||||
|
return `/web/image?model=res.partner&id=${partner.id}&field=image_128&write_date=${partner.write_date}&unique=1`;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Save to field `changes` all input changes from the form fields.
|
||||||
|
*/
|
||||||
|
captureChange(event) {
|
||||||
|
this.changes[event.target.name] = event.target.value;
|
||||||
|
}
|
||||||
|
saveChanges() {
|
||||||
|
let processedChanges = {};
|
||||||
|
for (let [key, value] of Object.entries(this.changes)) {
|
||||||
|
if (this.intFields.includes(key)) {
|
||||||
|
processedChanges[key] = parseInt(value) || false;
|
||||||
|
} else {
|
||||||
|
processedChanges[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processedChanges.id = this.props.partner.id || false;
|
||||||
|
this.trigger('save-changes', { processedChanges });
|
||||||
|
}
|
||||||
|
async uploadImage(event) {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file.type.match(/image.*/)) {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Unsupported File Format'),
|
||||||
|
body: this.env._t(
|
||||||
|
'Only web-compatible Image formats such as .png or .jpeg are supported.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const imageUrl = await getDataURLFromFile(file);
|
||||||
|
const loadedImage = await this._loadImage(imageUrl);
|
||||||
|
if (loadedImage) {
|
||||||
|
const resizedImage = await this._resizeImage(loadedImage, 800, 600);
|
||||||
|
this.changes.image_1920 = resizedImage.toDataURL();
|
||||||
|
// Rerender to reflect the changes in the screen
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_resizeImage(img, maxwidth, maxheight) {
|
||||||
|
var canvas = document.createElement('canvas');
|
||||||
|
var ctx = canvas.getContext('2d');
|
||||||
|
var ratio = 1;
|
||||||
|
|
||||||
|
if (img.width > maxwidth) {
|
||||||
|
ratio = maxwidth / img.width;
|
||||||
|
}
|
||||||
|
if (img.height * ratio > maxheight) {
|
||||||
|
ratio = maxheight / img.height;
|
||||||
|
}
|
||||||
|
var width = Math.floor(img.width * ratio);
|
||||||
|
var height = Math.floor(img.height * ratio);
|
||||||
|
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Loading image is converted to a Promise to allow await when
|
||||||
|
* loading an image. It resolves to the loaded image if succesful,
|
||||||
|
* else, resolves to false.
|
||||||
|
*
|
||||||
|
* [Source](https://stackoverflow.com/questions/45788934/how-to-turn-this-callback-into-a-promise-using-async-await)
|
||||||
|
*/
|
||||||
|
_loadImage(url) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.addEventListener('load', () => resolve(img));
|
||||||
|
img.addEventListener('error', () => {
|
||||||
|
this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Loading Image Error'),
|
||||||
|
body: this.env._t(
|
||||||
|
'Encountered error when loading image. Please try again.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
img.src = url;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientDetailsEdit.template = 'ClientDetailsEdit';
|
||||||
|
|
||||||
|
Registries.Component.add(ClientDetailsEdit);
|
||||||
|
|
||||||
|
return ClientDetailsEdit;
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
odoo.define('point_of_sale.ClientLine', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ClientLine extends PosComponent {
|
||||||
|
get highlight() {
|
||||||
|
if (this.props.partner !== this.props.selectedClient) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
return this.props.detailIsShown ? 'highlight' : 'lowlight';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientLine.template = 'ClientLine';
|
||||||
|
|
||||||
|
Registries.Component.add(ClientLine);
|
||||||
|
|
||||||
|
return ClientLine;
|
||||||
|
});
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
odoo.define('point_of_sale.ClientListScreen', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { debounce } = owl.utils;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render this screen using `showTempScreen` to select client.
|
||||||
|
* When the shown screen is confirmed ('Set Customer' or 'Deselect Customer'
|
||||||
|
* button is clicked), the call to `showTempScreen` resolves to the
|
||||||
|
* selected client. E.g.
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* const { confirmed, payload: selectedClient } = await showTempScreen('ClientListScreen');
|
||||||
|
* if (confirmed) {
|
||||||
|
* // do something with the selectedClient
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @props client - originally selected client
|
||||||
|
*/
|
||||||
|
class ClientListScreen extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
// We are not using useState here because the object
|
||||||
|
// passed to useState converts the object and its contents
|
||||||
|
// to Observer proxy. Not sure of the side-effects of making
|
||||||
|
// a persistent object, such as pos, into owl.Observer. But it
|
||||||
|
// is better to be safe.
|
||||||
|
this.state = {
|
||||||
|
query: null,
|
||||||
|
selectedClient: this.props.client,
|
||||||
|
detailIsShown: Boolean(this.props.client),
|
||||||
|
isEditMode: false,
|
||||||
|
editModeProps: {
|
||||||
|
partner: {
|
||||||
|
country_id: this.env.pos.company.country_id,
|
||||||
|
state_id: this.env.pos.company.state_id,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.updateClientList = debounce(this.updateClientList, 70);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
back() {
|
||||||
|
this.props.resolve({ confirmed: false, payload: false });
|
||||||
|
this.trigger('close-temp-screen');
|
||||||
|
}
|
||||||
|
confirm() {
|
||||||
|
this.props.resolve({ confirmed: true, payload: this.state.selectedClient });
|
||||||
|
this.trigger('close-temp-screen');
|
||||||
|
}
|
||||||
|
// Getters
|
||||||
|
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
|
||||||
|
get clients() {
|
||||||
|
if (this.state.query && this.state.query.trim() !== '') {
|
||||||
|
return this.env.pos.db.search_partner(this.state.query.trim());
|
||||||
|
} else {
|
||||||
|
return this.env.pos.db.get_partners_sorted(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get isNextButtonVisible() {
|
||||||
|
return this.state.selectedClient ? !this.state.isEditMode : false;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns the text and command of the next button.
|
||||||
|
* The command field is used by the clickNext call.
|
||||||
|
*/
|
||||||
|
get nextButton() {
|
||||||
|
if (!this.props.client) {
|
||||||
|
return { command: 'set', text: 'Set Customer' };
|
||||||
|
} else if (
|
||||||
|
this.props.client &&
|
||||||
|
this.props.client === this.state.selectedClient
|
||||||
|
) {
|
||||||
|
return { command: 'deselect', text: 'Deselect Customer' };
|
||||||
|
} else {
|
||||||
|
return { command: 'set', text: 'Change Customer' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
|
||||||
|
// We declare this event handler as a debounce function in
|
||||||
|
// order to lower its trigger rate.
|
||||||
|
updateClientList(event) {
|
||||||
|
this.state.query = event.target.value;
|
||||||
|
const clients = this.clients;
|
||||||
|
if (event.code === 'Enter' && clients.length === 1) {
|
||||||
|
this.state.selectedClient = clients[0];
|
||||||
|
this.clickNext();
|
||||||
|
} else {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clickClient(event) {
|
||||||
|
let partner = event.detail.client;
|
||||||
|
if (this.state.selectedClient === partner) {
|
||||||
|
this.state.detailIsShown = !this.state.detailIsShown;
|
||||||
|
} else {
|
||||||
|
this.state.selectedClient = partner;
|
||||||
|
this.state.detailIsShown = true;
|
||||||
|
}
|
||||||
|
this.deactivateEditMode();
|
||||||
|
}
|
||||||
|
clickNext() {
|
||||||
|
this.state.selectedClient = this.nextButton.command === 'set' ? this.state.selectedClient : null;
|
||||||
|
this.confirm();
|
||||||
|
}
|
||||||
|
activateEditMode(event) {
|
||||||
|
const { isNewClient } = event.detail;
|
||||||
|
this.state.isEditMode = true;
|
||||||
|
this.state.detailIsShown = true;
|
||||||
|
if (!isNewClient) {
|
||||||
|
this.state.editModeProps = {
|
||||||
|
partner: this.state.selectedClient,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
deactivateEditMode() {
|
||||||
|
this.state.isEditMode = false;
|
||||||
|
this.state.editModeProps = {
|
||||||
|
partner: {
|
||||||
|
country_id: this.env.pos.company.country_id,
|
||||||
|
state_id: this.env.pos.company.state_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
async saveChanges(event) {
|
||||||
|
try {
|
||||||
|
let partnerId = await this.rpc({
|
||||||
|
model: 'res.partner',
|
||||||
|
method: 'create_from_ui',
|
||||||
|
args: [event.detail.processedChanges],
|
||||||
|
});
|
||||||
|
await this.env.pos.load_new_partners();
|
||||||
|
this.state.selectedClient = this.env.pos.db.get_partner_by_id(partnerId);
|
||||||
|
this.deactivateEditMode();
|
||||||
|
} catch (error) {
|
||||||
|
if (error.message.code < 0) {
|
||||||
|
await this.showPopup('OfflineErrorPopup', {
|
||||||
|
title: this.env._t('Offline'),
|
||||||
|
body: this.env._t('Unable to save changes.'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelEdit() {
|
||||||
|
this.deactivateEditMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ClientListScreen.template = 'ClientListScreen';
|
||||||
|
|
||||||
|
Registries.Component.add(ClientListScreen);
|
||||||
|
|
||||||
|
return ClientListScreen;
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
odoo.define('point_of_sale.PSNumpadInputButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class PSNumpadInputButton extends PosComponent {
|
||||||
|
get _class() {
|
||||||
|
return this.props.changeClassTo || 'input-button number-char';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PSNumpadInputButton.template = 'PSNumpadInputButton';
|
||||||
|
|
||||||
|
Registries.Component.add(PSNumpadInputButton);
|
||||||
|
|
||||||
|
return PSNumpadInputButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.PaymentMethodButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class PaymentMethodButton extends PosComponent {}
|
||||||
|
PaymentMethodButton.template = 'PaymentMethodButton';
|
||||||
|
|
||||||
|
Registries.Component.add(PaymentMethodButton);
|
||||||
|
|
||||||
|
return PaymentMethodButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
odoo.define('point_of_sale.PaymentScreen', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { parse } = require('web.field_utils');
|
||||||
|
const { is_email } = require('web.utils');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const { useErrorHandlers } = require('point_of_sale.custom_hooks');
|
||||||
|
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const OrderReceipt = require('point_of_sale.OrderReceipt');
|
||||||
|
const { Printer } = require('point_of_sale.Printer');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
const { onChangeOrder } = require('point_of_sale.custom_hooks');
|
||||||
|
|
||||||
|
class PaymentScreen extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('delete-payment-line', this.deletePaymentLine);
|
||||||
|
useListener('select-payment-line', this.selectPaymentLine);
|
||||||
|
useListener('new-payment-line', this.addNewPaymentLine);
|
||||||
|
useListener('update-selected-paymentline', this._updateSelectedPaymentline);
|
||||||
|
useListener('send-payment-request', this._sendPaymentRequest);
|
||||||
|
useListener('send-payment-cancel', this._sendPaymentCancel);
|
||||||
|
useListener('send-payment-reverse', this._sendPaymentReverse);
|
||||||
|
useListener('send-force-done', this._sendForceDone);
|
||||||
|
onChangeOrder(this._onPrevOrder, this._onNewOrder);
|
||||||
|
useErrorHandlers();
|
||||||
|
NumberBuffer.use({
|
||||||
|
// The numberBuffer listens to this event to update its state.
|
||||||
|
// Basically means 'update the buffer when this event is triggered'
|
||||||
|
nonKeyboardInputEvent: 'input-from-numpad',
|
||||||
|
// When the buffer is updated, trigger this event.
|
||||||
|
// Note that the component listens to it.
|
||||||
|
triggerAtInput: 'update-selected-paymentline',
|
||||||
|
});
|
||||||
|
this.payment_interface = null;
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get paymentLines() {
|
||||||
|
return this.currentOrder.get_paymentlines();
|
||||||
|
}
|
||||||
|
get selectedPaymentLine() {
|
||||||
|
return this.currentOrder.selected_paymentline;
|
||||||
|
}
|
||||||
|
async selectClient() {
|
||||||
|
// IMPROVEMENT: This code snippet is repeated multiple times.
|
||||||
|
// Maybe it's better to create a function for it.
|
||||||
|
const currentClient = this.currentOrder.get_client();
|
||||||
|
const { confirmed, payload: newClient } = await this.showTempScreen(
|
||||||
|
'ClientListScreen',
|
||||||
|
{ client: currentClient }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.currentOrder.set_client(newClient);
|
||||||
|
this.currentOrder.updatePricelist(newClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addNewPaymentLine({ detail: paymentMethod }) {
|
||||||
|
// original function: click_paymentmethods
|
||||||
|
if (this.currentOrder.electronic_payment_in_progress()) {
|
||||||
|
this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Error'),
|
||||||
|
body: this.env._t('There is already an electronic payment in progress.'),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
this.currentOrder.add_paymentline(paymentMethod);
|
||||||
|
NumberBuffer.reset();
|
||||||
|
this.payment_interface = paymentMethod.payment_terminal;
|
||||||
|
if (this.payment_interface) {
|
||||||
|
this.currentOrder.selected_paymentline.set_payment_status('pending');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_updateSelectedPaymentline() {
|
||||||
|
if (this.paymentLines.every((line) => line.paid)) {
|
||||||
|
this.currentOrder.add_paymentline(this.env.pos.payment_methods[0]);
|
||||||
|
}
|
||||||
|
if (!this.selectedPaymentLine) return; // do nothing if no selected payment line
|
||||||
|
// disable changing amount on paymentlines with running or done payments on a payment terminal
|
||||||
|
if (
|
||||||
|
this.payment_interface &&
|
||||||
|
!['pending', 'retry'].includes(this.selectedPaymentLine.get_payment_status())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (NumberBuffer.get() === null) {
|
||||||
|
this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } });
|
||||||
|
} else {
|
||||||
|
this.selectedPaymentLine.set_amount(NumberBuffer.getFloat());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
toggleIsToInvoice() {
|
||||||
|
// click_invoice
|
||||||
|
this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice());
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
toggleIsToEmail() {
|
||||||
|
// click_email
|
||||||
|
this.currentOrder.set_to_email(!this.currentOrder.is_to_email());
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
openCashbox() {
|
||||||
|
this.env.pos.proxy.printer.open_cashbox();
|
||||||
|
}
|
||||||
|
async addTip() {
|
||||||
|
// click_tip
|
||||||
|
const tip = this.currentOrder.get_tip();
|
||||||
|
const change = this.currentOrder.get_change();
|
||||||
|
let value = tip.toFixed(this.env.pos.decimals);
|
||||||
|
|
||||||
|
if (tip === 0 && change > 0) {
|
||||||
|
value = change;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { confirmed, payload } = await this.showPopup('NumberPopup', {
|
||||||
|
title: tip ? this.env._t('Change Tip') : this.env._t('Add Tip'),
|
||||||
|
startingValue: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
this.currentOrder.set_tip(parse.float(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deletePaymentLine(event) {
|
||||||
|
const { cid } = event.detail;
|
||||||
|
const line = this.paymentLines.find((line) => line.cid === cid);
|
||||||
|
|
||||||
|
// If a paymentline with a payment terminal linked to
|
||||||
|
// it is removed, the terminal should get a cancel
|
||||||
|
// request.
|
||||||
|
if (['waiting', 'waitingCard', 'timeout'].includes(line.get_payment_status())) {
|
||||||
|
line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentOrder.remove_paymentline(line);
|
||||||
|
NumberBuffer.reset();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
selectPaymentLine(event) {
|
||||||
|
const { cid } = event.detail;
|
||||||
|
const line = this.paymentLines.find((line) => line.cid === cid);
|
||||||
|
this.currentOrder.select_paymentline(line);
|
||||||
|
NumberBuffer.reset();
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
async validateOrder(isForceValidate) {
|
||||||
|
if (await this._isOrderValid(isForceValidate)) {
|
||||||
|
// remove pending payments before finalizing the validation
|
||||||
|
for (let line of this.paymentLines) {
|
||||||
|
if (!line.is_done()) this.currentOrder.remove_paymentline(line);
|
||||||
|
}
|
||||||
|
await this._finalizeValidation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _finalizeValidation() {
|
||||||
|
if (this.currentOrder.is_paid_with_cash() && this.env.pos.config.iface_cashdrawer) {
|
||||||
|
this.env.pos.proxy.printer.open_cashbox();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentOrder.initialize_validation_date();
|
||||||
|
this.currentOrder.finalized = true;
|
||||||
|
|
||||||
|
let syncedOrderBackendIds = [];
|
||||||
|
let errorCode;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (this.currentOrder.is_to_invoice()) {
|
||||||
|
syncedOrderBackendIds = await this.env.pos.push_and_invoice_order(
|
||||||
|
this.currentOrder
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
syncedOrderBackendIds = await this.env.pos.push_single_order(this.currentOrder);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
errorCode = error.code;
|
||||||
|
await this._handlePushOrderError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (syncedOrderBackendIds.length && this.currentOrder.wait_for_push_order()) {
|
||||||
|
const result = await this._postPushOrderResolve(
|
||||||
|
this.currentOrder,
|
||||||
|
syncedOrderBackendIds
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: 'Error: no internet connection.',
|
||||||
|
body: error,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldShowPrintInvoice = errorCode
|
||||||
|
? this.currentOrder.is_to_invoice() && errorCode < 0
|
||||||
|
: false;
|
||||||
|
this.showScreen('ReceiptScreen', { printInvoiceIsShown: shouldShowPrintInvoice });
|
||||||
|
|
||||||
|
// If we succeeded in syncing the current order, and
|
||||||
|
// there are still other orders that are left unsynced,
|
||||||
|
// we ask the user if he is willing to wait and sync them.
|
||||||
|
if (syncedOrderBackendIds.length && this.env.pos.db.get_orders().length) {
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Remaining unsynced orders'),
|
||||||
|
body: this.env._t(
|
||||||
|
'There are unsynced orders. Do you want to sync these orders?'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
// NOTE: Not yet sure if this should be awaited or not.
|
||||||
|
// If awaited, some operations like changing screen
|
||||||
|
// might not work.
|
||||||
|
this.env.pos.push_orders();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _isOrderValid(isForceValidate) {
|
||||||
|
if (this.currentOrder.get_orderlines().length === 0) {
|
||||||
|
this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Empty Order'),
|
||||||
|
body: this.env._t(
|
||||||
|
'There must be at least one product in your order before it can be validated'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentOrder.is_to_invoice() && !this.currentOrder.get_client()) {
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Please select the Customer'),
|
||||||
|
body: this.env._t(
|
||||||
|
'You need to select the customer before you can invoice an order.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
this.selectClient();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.currentOrder.is_paid() || this.invoicing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.currentOrder.has_not_valid_rounding()) {
|
||||||
|
var line = this.currentOrder.has_not_valid_rounding();
|
||||||
|
this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Incorrect rounding'),
|
||||||
|
body: this.env._t(
|
||||||
|
'You have to round your payments lines.' + line.amount + ' is not rounded.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The exact amount must be paid if there is no cash payment method defined.
|
||||||
|
if (
|
||||||
|
Math.abs(
|
||||||
|
this.currentOrder.get_total_with_tax() - this.currentOrder.get_total_paid() + this.currentOrder.get_rounding_applied()
|
||||||
|
) > 0.00001
|
||||||
|
) {
|
||||||
|
var cash = false;
|
||||||
|
for (var i = 0; i < this.env.pos.payment_methods.length; i++) {
|
||||||
|
cash = cash || this.env.pos.payment_methods[i].is_cash_count;
|
||||||
|
}
|
||||||
|
if (!cash) {
|
||||||
|
this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Cannot return change without a cash payment method'),
|
||||||
|
body: this.env._t(
|
||||||
|
'There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var client = this.currentOrder.get_client();
|
||||||
|
if (
|
||||||
|
this.currentOrder.is_to_email() &&
|
||||||
|
(!client || (client && !is_email(client.email)))
|
||||||
|
) {
|
||||||
|
var title = !client ? 'Please select the customer' : 'Please provide valid email';
|
||||||
|
var body = !client
|
||||||
|
? 'You need to select the customer before you can send the receipt via email.'
|
||||||
|
: 'This customer does not have a valid email address, define one or do not send an email.';
|
||||||
|
|
||||||
|
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t(title),
|
||||||
|
body: this.env._t(body),
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
this.selectClient();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the change is too large, it's probably an input error, make the user confirm.
|
||||||
|
if (
|
||||||
|
!isForceValidate &&
|
||||||
|
this.currentOrder.get_total_with_tax() > 0 &&
|
||||||
|
this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid()
|
||||||
|
) {
|
||||||
|
this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Please Confirm Large Amount'),
|
||||||
|
body:
|
||||||
|
this.env._t('Are you sure that the customer wants to pay') +
|
||||||
|
' ' +
|
||||||
|
this.env.pos.format_currency(this.currentOrder.get_total_paid()) +
|
||||||
|
' ' +
|
||||||
|
this.env._t('for an order of') +
|
||||||
|
' ' +
|
||||||
|
this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) +
|
||||||
|
' ' +
|
||||||
|
this.env._t('? Clicking "Confirm" will validate the payment.'),
|
||||||
|
}).then(({ confirmed }) => {
|
||||||
|
if (confirmed) this.validateOrder(true);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
async _postPushOrderResolve(order, order_server_ids) {
|
||||||
|
if (order.is_to_email()) {
|
||||||
|
return await this._sendReceiptToCustomer(order_server_ids);
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _sendReceiptToCustomer(order_server_ids) {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
const fixture = document.createElement('div');
|
||||||
|
const orderReceipt = new OrderReceipt(this, { order });
|
||||||
|
// Important to mount the component to a HTMLElement.
|
||||||
|
// If not properly mounted, HTMLElement (el) corresponding
|
||||||
|
// the component is not created.
|
||||||
|
await orderReceipt.mount(fixture);
|
||||||
|
const receiptString = orderReceipt.el.outerHTML;
|
||||||
|
fixture.remove();
|
||||||
|
const printer = new Printer();
|
||||||
|
const ticketImage = await printer.htmlToImg(receiptString);
|
||||||
|
const orderName = order.get_name();
|
||||||
|
const orderClient = order.get_client();
|
||||||
|
try {
|
||||||
|
await this.rpc({
|
||||||
|
model: 'pos.order',
|
||||||
|
method: 'action_receipt_to_customer',
|
||||||
|
args: [order_server_ids, orderName, orderClient, ticketImage],
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
order.set_to_email(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
async _sendPaymentRequest({ detail: line }) {
|
||||||
|
// Other payment lines can not be reversed anymore
|
||||||
|
this.paymentLines.forEach(function (line) {
|
||||||
|
line.can_be_reversed = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const payment_terminal = line.payment_method.payment_terminal;
|
||||||
|
line.set_payment_status('waiting');
|
||||||
|
|
||||||
|
const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid);
|
||||||
|
if (isPaymentSuccessful) {
|
||||||
|
line.set_payment_status('done');
|
||||||
|
line.can_be_reversed = this.payment_interface.supports_reversals;
|
||||||
|
} else {
|
||||||
|
line.set_payment_status('retry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _sendPaymentCancel({ detail: line }) {
|
||||||
|
const payment_terminal = line.payment_method.payment_terminal;
|
||||||
|
line.set_payment_status('waitingCancel');
|
||||||
|
try {
|
||||||
|
payment_terminal.send_payment_cancel(this.currentOrder, line.cid);
|
||||||
|
} finally {
|
||||||
|
line.set_payment_status('retry');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _sendPaymentReverse({ detail: line }) {
|
||||||
|
const payment_terminal = line.payment_method.payment_terminal;
|
||||||
|
line.set_payment_status('reversing');
|
||||||
|
|
||||||
|
const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid);
|
||||||
|
if (isReversalSuccessful) {
|
||||||
|
line.set_amount(0);
|
||||||
|
line.set_payment_status('reversed');
|
||||||
|
} else {
|
||||||
|
line.can_be_reversed = false;
|
||||||
|
line.set_payment_status('done');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _sendForceDone({ detail: line }) {
|
||||||
|
line.set_payment_status('done');
|
||||||
|
}
|
||||||
|
_onPrevOrder(prevOrder) {
|
||||||
|
prevOrder.off('change', null, this);
|
||||||
|
prevOrder.paymentlines.off('change', null, this);
|
||||||
|
if (prevOrder) {
|
||||||
|
prevOrder.stop_electronic_payment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _onNewOrder(newOrder) {
|
||||||
|
newOrder.on('change', this.render, this);
|
||||||
|
newOrder.paymentlines.on('change', this.render, this);
|
||||||
|
NumberBuffer.reset();
|
||||||
|
await this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PaymentScreen.template = 'PaymentScreen';
|
||||||
|
|
||||||
|
Registries.Component.add(PaymentScreen);
|
||||||
|
|
||||||
|
return PaymentScreen;
|
||||||
|
});
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
odoo.define('point_of_sale.PaymentScreenElectronicPayment', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class PaymentScreenElectronicPayment extends PosComponent {
|
||||||
|
mounted() {
|
||||||
|
this.props.line.on('change', this.render, this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
if (this.props.line) {
|
||||||
|
// It could be that the line is deleted before unmounting the element.
|
||||||
|
this.props.line.off('change', null, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PaymentScreenElectronicPayment.template = 'PaymentScreenElectronicPayment';
|
||||||
|
|
||||||
|
Registries.Component.add(PaymentScreenElectronicPayment);
|
||||||
|
|
||||||
|
return PaymentScreenElectronicPayment;
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
odoo.define('point_of_sale.PaymentScreenNumpad', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class PaymentScreenNumpad extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.decimalPoint = this.env._t.database.parameters.decimal_point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PaymentScreenNumpad.template = 'PaymentScreenNumpad';
|
||||||
|
|
||||||
|
Registries.Component.add(PaymentScreenNumpad);
|
||||||
|
|
||||||
|
return PaymentScreenNumpad;
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
odoo.define('point_of_sale.PaymentScreenPaymentLines', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class PaymentScreenPaymentLines extends PosComponent {
|
||||||
|
formatLineAmount(paymentline) {
|
||||||
|
return this.env.pos.format_currency_no_symbol(paymentline.get_amount());
|
||||||
|
}
|
||||||
|
get changeText() {
|
||||||
|
return this.env.pos.format_currency(this.currentOrder.get_change());
|
||||||
|
}
|
||||||
|
get totalDueText() {
|
||||||
|
return this.env.pos.format_currency(
|
||||||
|
this.currentOrder.get_total_with_tax() + this.currentOrder.get_rounding_applied()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
get remainingText() {
|
||||||
|
return this.env.pos.format_currency(
|
||||||
|
this.currentOrder.get_due() > 0 ? this.currentOrder.get_due() : 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
selectedLineClass(line) {
|
||||||
|
return { 'payment-terminal': line.get_payment_status() };
|
||||||
|
}
|
||||||
|
unselectedLineClass(line) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PaymentScreenPaymentLines.template = 'PaymentScreenPaymentLines';
|
||||||
|
|
||||||
|
Registries.Component.add(PaymentScreenPaymentLines);
|
||||||
|
|
||||||
|
return PaymentScreenPaymentLines;
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
odoo.define('point_of_sale.ActionpadWidget', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @props client
|
||||||
|
* @trigger click-customer
|
||||||
|
* @trigger click-pay
|
||||||
|
*/
|
||||||
|
class ActionpadWidget extends PosComponent {
|
||||||
|
get isLongName() {
|
||||||
|
return this.client && this.client.name.length > 10;
|
||||||
|
}
|
||||||
|
get client() {
|
||||||
|
return this.props.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionpadWidget.template = 'ActionpadWidget';
|
||||||
|
|
||||||
|
Registries.Component.add(ActionpadWidget);
|
||||||
|
|
||||||
|
return ActionpadWidget;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.CategoryBreadcrumb', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class CategoryBreadcrumb extends PosComponent {}
|
||||||
|
CategoryBreadcrumb.template = 'CategoryBreadcrumb';
|
||||||
|
|
||||||
|
Registries.Component.add(CategoryBreadcrumb);
|
||||||
|
|
||||||
|
return CategoryBreadcrumb;
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
odoo.define('point_of_sale.CategoryButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class CategoryButton extends PosComponent {
|
||||||
|
get imageUrl() {
|
||||||
|
const category = this.props.category
|
||||||
|
return `/web/image?model=pos.category&field=image_128&id=${category.id}&write_date=${category.write_date}&unique=1`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CategoryButton.template = 'CategoryButton';
|
||||||
|
|
||||||
|
Registries.Component.add(CategoryButton);
|
||||||
|
|
||||||
|
return CategoryButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.CategorySimpleButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class CategorySimpleButton extends PosComponent {}
|
||||||
|
CategorySimpleButton.template = 'CategorySimpleButton';
|
||||||
|
|
||||||
|
Registries.Component.add(CategorySimpleButton);
|
||||||
|
|
||||||
|
return CategorySimpleButton;
|
||||||
|
});
|
||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
odoo.define('point_of_sale.SetFiscalPositionButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class SetFiscalPositionButton extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('click', this.onClick);
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.get('orders').on('add remove change', () => this.render(), this);
|
||||||
|
this.env.pos.on('change:selectedOrder', () => this.render(), this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.get('orders').off('add remove change', null, this);
|
||||||
|
this.env.pos.off('change:selectedOrder', null, this);
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get currentFiscalPositionName() {
|
||||||
|
return this.currentOrder && this.currentOrder.fiscal_position
|
||||||
|
? this.currentOrder.fiscal_position.display_name
|
||||||
|
: this.env._t('Tax');
|
||||||
|
}
|
||||||
|
async onClick() {
|
||||||
|
const currentFiscalPosition = this.currentOrder.fiscal_position;
|
||||||
|
const fiscalPosList = [
|
||||||
|
{
|
||||||
|
id: -1,
|
||||||
|
label: this.env._t('None'),
|
||||||
|
isSelected: !currentFiscalPosition,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
for (let fiscalPos of this.env.pos.fiscal_positions) {
|
||||||
|
fiscalPosList.push({
|
||||||
|
id: fiscalPos.id,
|
||||||
|
label: fiscalPos.name,
|
||||||
|
isSelected: currentFiscalPosition
|
||||||
|
? fiscalPos.id === currentFiscalPosition.id
|
||||||
|
: false,
|
||||||
|
item: fiscalPos,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const { confirmed, payload: selectedFiscalPosition } = await this.showPopup(
|
||||||
|
'SelectionPopup',
|
||||||
|
{
|
||||||
|
title: this.env._t('Select Fiscal Position'),
|
||||||
|
list: fiscalPosList,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.currentOrder.fiscal_position = selectedFiscalPosition;
|
||||||
|
// IMPROVEMENT: The following is the old implementation and I believe
|
||||||
|
// there could be a better way of doing it.
|
||||||
|
for (let line of this.currentOrder.orderlines.models) {
|
||||||
|
line.set_quantity(line.quantity);
|
||||||
|
}
|
||||||
|
this.currentOrder.trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SetFiscalPositionButton.template = 'SetFiscalPositionButton';
|
||||||
|
|
||||||
|
ProductScreen.addControlButton({
|
||||||
|
component: SetFiscalPositionButton,
|
||||||
|
condition: function() {
|
||||||
|
return this.env.pos.fiscal_positions.length > 0;
|
||||||
|
},
|
||||||
|
position: ['before', 'SetPricelistButton'],
|
||||||
|
});
|
||||||
|
|
||||||
|
Registries.Component.add(SetFiscalPositionButton);
|
||||||
|
|
||||||
|
return SetFiscalPositionButton;
|
||||||
|
});
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
odoo.define('point_of_sale.SetPricelistButton', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const ProductScreen = require('point_of_sale.ProductScreen');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class SetPricelistButton extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('click', this.onClick);
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.get('orders').on('add remove change', () => this.render(), this);
|
||||||
|
this.env.pos.on('change:selectedOrder', () => this.render(), this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.get('orders').off('add remove change', null, this);
|
||||||
|
this.env.pos.off('change:selectedOrder', null, this);
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get currentPricelistName() {
|
||||||
|
const order = this.currentOrder;
|
||||||
|
return order && order.pricelist
|
||||||
|
? order.pricelist.display_name
|
||||||
|
: this.env._t('Pricelist');
|
||||||
|
}
|
||||||
|
async onClick() {
|
||||||
|
// Create the list to be passed to the SelectionPopup.
|
||||||
|
// Pricelist object is passed as item in the list because it
|
||||||
|
// is the object that will be returned when the popup is confirmed.
|
||||||
|
const selectionList = this.env.pos.pricelists.map(pricelist => ({
|
||||||
|
id: pricelist.id,
|
||||||
|
label: pricelist.name,
|
||||||
|
isSelected: pricelist.id === this.currentOrder.pricelist.id,
|
||||||
|
item: pricelist,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { confirmed, payload: selectedPricelist } = await this.showPopup(
|
||||||
|
'SelectionPopup',
|
||||||
|
{
|
||||||
|
title: this.env._t('Select the pricelist'),
|
||||||
|
list: selectionList,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
this.currentOrder.set_pricelist(selectedPricelist);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SetPricelistButton.template = 'SetPricelistButton';
|
||||||
|
|
||||||
|
ProductScreen.addControlButton({
|
||||||
|
component: SetPricelistButton,
|
||||||
|
condition: function() {
|
||||||
|
return this.env.pos.config.use_pricelist && this.env.pos.pricelists.length > 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Registries.Component.add(SetPricelistButton);
|
||||||
|
|
||||||
|
return SetPricelistButton;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.HomeCategoryBreadcrumb', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class HomeCategoryBreadcrumb extends PosComponent {}
|
||||||
|
HomeCategoryBreadcrumb.template = 'HomeCategoryBreadcrumb';
|
||||||
|
|
||||||
|
Registries.Component.add(HomeCategoryBreadcrumb);
|
||||||
|
|
||||||
|
return HomeCategoryBreadcrumb;
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
odoo.define('point_of_sale.NumpadWidget', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IMPROVEMENT: Whenever new-orderline-selected is triggered,
|
||||||
|
* numpad mode should be set to 'quantity'.
|
||||||
|
*/
|
||||||
|
class NumpadWidget extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.state = useState({ mode: 'quantity' });
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.on('change:cashier', () => {
|
||||||
|
if (!this.hasPriceControlRights && this.state.mode === 'price') {
|
||||||
|
this.state.mode = 'quantity';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.on('change:cashier', null, this);
|
||||||
|
}
|
||||||
|
get hasPriceControlRights() {
|
||||||
|
const cashier = this.env.pos.get('cashier') || this.env.pos.get_cashier();
|
||||||
|
return !this.env.pos.config.restrict_price_control || cashier.role == 'manager';
|
||||||
|
}
|
||||||
|
changeMode(mode) {
|
||||||
|
if (!this.hasPriceControlRights && mode === 'price') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.state.mode = mode;
|
||||||
|
this.trigger('set-numpad-mode', { mode });
|
||||||
|
}
|
||||||
|
sendInput(key) {
|
||||||
|
this.trigger('numpad-click-input', { key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NumpadWidget.template = 'NumpadWidget';
|
||||||
|
|
||||||
|
Registries.Component.add(NumpadWidget);
|
||||||
|
|
||||||
|
return NumpadWidget;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.OrderSummary', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class OrderSummary extends PosComponent {}
|
||||||
|
OrderSummary.template = 'OrderSummary';
|
||||||
|
|
||||||
|
Registries.Component.add(OrderSummary);
|
||||||
|
|
||||||
|
return OrderSummary;
|
||||||
|
});
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
odoo.define('point_of_sale.OrderWidget', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState, useRef, onPatched } = owl.hooks;
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const { onChangeOrder } = require('point_of_sale.custom_hooks');
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class OrderWidget extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('select-line', this._selectLine);
|
||||||
|
useListener('edit-pack-lot-lines', this._editPackLotLines);
|
||||||
|
onChangeOrder(this._onPrevOrder, this._onNewOrder);
|
||||||
|
this.scrollableRef = useRef('scrollable');
|
||||||
|
this.scrollToBottom = false;
|
||||||
|
onPatched(() => {
|
||||||
|
// IMPROVEMENT
|
||||||
|
// This one just stays at the bottom of the orderlines list.
|
||||||
|
// Perhaps it is better to scroll to the added or modified orderline.
|
||||||
|
if (this.scrollToBottom) {
|
||||||
|
this.scrollableRef.el.scrollTop = this.scrollableRef.el.scrollHeight;
|
||||||
|
this.scrollToBottom = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.state = useState({ total: 0, tax: 0 });
|
||||||
|
this._updateSummary();
|
||||||
|
}
|
||||||
|
get order() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get orderlinesArray() {
|
||||||
|
return this.order ? this.order.get_orderlines() : [];
|
||||||
|
}
|
||||||
|
_selectLine(event) {
|
||||||
|
this.order.select_orderline(event.detail.orderline);
|
||||||
|
}
|
||||||
|
// IMPROVEMENT: Might be better to lift this to ProductScreen
|
||||||
|
// because there is similar operation when clicking a product.
|
||||||
|
//
|
||||||
|
// Furthermore, what if a number different from 1 (or -1) is specified
|
||||||
|
// to an orderline that has product tracked by lot. Lot tracking (based
|
||||||
|
// on the current implementation) requires that 1 item per orderline is
|
||||||
|
// allowed.
|
||||||
|
async _editPackLotLines(event) {
|
||||||
|
const orderline = event.detail.orderline;
|
||||||
|
const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot();
|
||||||
|
const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot);
|
||||||
|
const { confirmed, payload } = await this.showPopup('EditListPopup', {
|
||||||
|
title: this.env._t('Lot/Serial Number(s) Required'),
|
||||||
|
isSingleItem: isAllowOnlyOneLot,
|
||||||
|
array: packLotLinesToEdit,
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
// Segregate the old and new packlot lines
|
||||||
|
const modifiedPackLotLines = Object.fromEntries(
|
||||||
|
payload.newArray.filter(item => item.id).map(item => [item.id, item.text])
|
||||||
|
);
|
||||||
|
const newPackLotLines = payload.newArray
|
||||||
|
.filter(item => !item.id)
|
||||||
|
.map(item => ({ lot_name: item.text }));
|
||||||
|
|
||||||
|
orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines });
|
||||||
|
}
|
||||||
|
this.order.select_orderline(event.detail.orderline);
|
||||||
|
}
|
||||||
|
_onNewOrder(order) {
|
||||||
|
if (order) {
|
||||||
|
order.orderlines.on(
|
||||||
|
'new-orderline-selected',
|
||||||
|
() => this.trigger('new-orderline-selected'),
|
||||||
|
this
|
||||||
|
);
|
||||||
|
order.orderlines.on('change', this._updateSummary, this);
|
||||||
|
order.orderlines.on(
|
||||||
|
'add remove',
|
||||||
|
() => {
|
||||||
|
this.scrollToBottom = true;
|
||||||
|
this._updateSummary();
|
||||||
|
},
|
||||||
|
this
|
||||||
|
);
|
||||||
|
order.on('change', this.render, this);
|
||||||
|
}
|
||||||
|
this._updateSummary();
|
||||||
|
this.trigger('new-orderline-selected');
|
||||||
|
}
|
||||||
|
_onPrevOrder(order) {
|
||||||
|
if (order) {
|
||||||
|
order.orderlines.off('new-orderline-selected', null, this);
|
||||||
|
order.orderlines.off('change', null, this);
|
||||||
|
order.orderlines.off('add remove', null, this);
|
||||||
|
order.off('change', null, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_updateSummary() {
|
||||||
|
const total = this.order ? this.order.get_total_with_tax() : 0;
|
||||||
|
const tax = this.order ? total - this.order.get_total_without_tax() : 0;
|
||||||
|
this.state.total = this.env.pos.format_currency(total);
|
||||||
|
this.state.tax = this.env.pos.format_currency(tax);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OrderWidget.template = 'OrderWidget';
|
||||||
|
|
||||||
|
Registries.Component.add(OrderWidget);
|
||||||
|
|
||||||
|
return OrderWidget;
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
odoo.define('point_of_sale.Orderline', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class Orderline extends PosComponent {
|
||||||
|
selectLine() {
|
||||||
|
this.trigger('select-line', { orderline: this.props.line });
|
||||||
|
}
|
||||||
|
lotIconClicked() {
|
||||||
|
this.trigger('edit-pack-lot-lines', { orderline: this.props.line });
|
||||||
|
}
|
||||||
|
get addedClasses() {
|
||||||
|
return {
|
||||||
|
selected: this.props.line.selected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Orderline.template = 'Orderline';
|
||||||
|
|
||||||
|
Registries.Component.add(Orderline);
|
||||||
|
|
||||||
|
return Orderline;
|
||||||
|
});
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
odoo.define('point_of_sale.ProductItem', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ProductItem extends PosComponent {
|
||||||
|
/**
|
||||||
|
* For accessibility, pressing <space> should be like clicking the product.
|
||||||
|
* <enter> is not considered because it conflicts with the barcode.
|
||||||
|
*
|
||||||
|
* @param {KeyPressEvent} event
|
||||||
|
*/
|
||||||
|
spaceClickProduct(event) {
|
||||||
|
if (event.which === 32) {
|
||||||
|
this.trigger('click-product', this.props.product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get imageUrl() {
|
||||||
|
const product = this.props.product;
|
||||||
|
return `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`;
|
||||||
|
}
|
||||||
|
get pricelist() {
|
||||||
|
const current_order = this.env.pos.get_order();
|
||||||
|
if (current_order) {
|
||||||
|
return current_order.pricelist;
|
||||||
|
}
|
||||||
|
return this.env.pos.default_pricelist;
|
||||||
|
}
|
||||||
|
get price() {
|
||||||
|
const formattedUnitPrice = this.env.pos.format_currency(
|
||||||
|
this.props.product.get_price(this.pricelist, 1),
|
||||||
|
'Product Price'
|
||||||
|
);
|
||||||
|
if (this.props.product.to_weight) {
|
||||||
|
return `${formattedUnitPrice}/${
|
||||||
|
this.env.pos.units_by_id[this.props.product.uom_id[0]].name
|
||||||
|
}`;
|
||||||
|
} else {
|
||||||
|
return formattedUnitPrice;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProductItem.template = 'ProductItem';
|
||||||
|
|
||||||
|
Registries.Component.add(ProductItem);
|
||||||
|
|
||||||
|
return ProductItem;
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
odoo.define('point_of_sale.ProductList', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ProductList extends PosComponent {}
|
||||||
|
ProductList.template = 'ProductList';
|
||||||
|
|
||||||
|
Registries.Component.add(ProductList);
|
||||||
|
|
||||||
|
return ProductList;
|
||||||
|
});
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
odoo.define('point_of_sale.ProductScreen', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
const { onChangeOrder, useBarcodeReader } = require('point_of_sale.custom_hooks');
|
||||||
|
|
||||||
|
class ProductScreen extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('update-selected-orderline', this._updateSelectedOrderline);
|
||||||
|
useListener('new-orderline-selected', this._newOrderlineSelected);
|
||||||
|
useListener('set-numpad-mode', this._setNumpadMode);
|
||||||
|
useListener('click-product', this._clickProduct);
|
||||||
|
useListener('click-customer', this._onClickCustomer);
|
||||||
|
useListener('click-pay', this._onClickPay);
|
||||||
|
useBarcodeReader({
|
||||||
|
product: this._barcodeProductAction,
|
||||||
|
weight: this._barcodeProductAction,
|
||||||
|
price: this._barcodeProductAction,
|
||||||
|
client: this._barcodeClientAction,
|
||||||
|
discount: this._barcodeDiscountAction,
|
||||||
|
error: this._barcodeErrorAction,
|
||||||
|
})
|
||||||
|
onChangeOrder(null, (newOrder) => newOrder && this.render());
|
||||||
|
NumberBuffer.use({
|
||||||
|
nonKeyboardInputEvent: 'numpad-click-input',
|
||||||
|
triggerAtInput: 'update-selected-orderline',
|
||||||
|
useWithBarcode: true,
|
||||||
|
});
|
||||||
|
this.numpadMode = 'quantity';
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.on('change:selectedClient', this.render, this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.off('change:selectedClient', null, this);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* To be overridden by modules that checks availability of
|
||||||
|
* connected scale.
|
||||||
|
* @see _onScaleNotAvailable
|
||||||
|
*/
|
||||||
|
get isScaleAvailable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
get client() {
|
||||||
|
return this.env.pos.get_client();
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get controlButtons() {
|
||||||
|
return ProductScreen.controlButtons
|
||||||
|
.filter((cb) => {
|
||||||
|
return cb.condition.bind(this)();
|
||||||
|
})
|
||||||
|
.map((cb) =>
|
||||||
|
Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async _clickProduct(event) {
|
||||||
|
if (!this.currentOrder) {
|
||||||
|
this.env.pos.add_new_order();
|
||||||
|
}
|
||||||
|
const product = event.detail;
|
||||||
|
let draftPackLotLines, weight, packLotLinesToEdit;
|
||||||
|
|
||||||
|
// Gather lot information if required.
|
||||||
|
if (['serial', 'lot'].includes(product.tracking)) {
|
||||||
|
const isAllowOnlyOneLot = product.isAllowOnlyOneLot();
|
||||||
|
if (isAllowOnlyOneLot) {
|
||||||
|
packLotLinesToEdit = [];
|
||||||
|
} else {
|
||||||
|
const orderline = this.currentOrder
|
||||||
|
.get_orderlines()
|
||||||
|
.filter(line => !line.get_discount())
|
||||||
|
.find(line => line.product.id === product.id);
|
||||||
|
if (orderline) {
|
||||||
|
packLotLinesToEdit = orderline.getPackLotLinesToEdit();
|
||||||
|
} else {
|
||||||
|
packLotLinesToEdit = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { confirmed, payload } = await this.showPopup('EditListPopup', {
|
||||||
|
title: this.env._t('Lot/Serial Number(s) Required'),
|
||||||
|
isSingleItem: isAllowOnlyOneLot,
|
||||||
|
array: packLotLinesToEdit,
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
// Segregate the old and new packlot lines
|
||||||
|
const modifiedPackLotLines = Object.fromEntries(
|
||||||
|
payload.newArray.filter(item => item.id).map(item => [item.id, item.text])
|
||||||
|
);
|
||||||
|
const newPackLotLines = payload.newArray
|
||||||
|
.filter(item => !item.id)
|
||||||
|
.map(item => ({ lot_name: item.text }));
|
||||||
|
|
||||||
|
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
|
||||||
|
} else {
|
||||||
|
// We don't proceed on adding product.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take the weight if necessary.
|
||||||
|
if (product.to_weight && this.env.pos.config.iface_electronic_scale) {
|
||||||
|
// Show the ScaleScreen to weigh the product.
|
||||||
|
if (this.isScaleAvailable) {
|
||||||
|
const { confirmed, payload } = await this.showTempScreen('ScaleScreen', {
|
||||||
|
product,
|
||||||
|
});
|
||||||
|
if (confirmed) {
|
||||||
|
weight = payload.weight;
|
||||||
|
} else {
|
||||||
|
// do not add the product;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this._onScaleNotAvailable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the product after having the extra information.
|
||||||
|
this.currentOrder.add_product(product, {
|
||||||
|
draftPackLotLines,
|
||||||
|
quantity: weight,
|
||||||
|
});
|
||||||
|
|
||||||
|
NumberBuffer.reset();
|
||||||
|
}
|
||||||
|
async _setNumpadMode(event) {
|
||||||
|
const { mode } = event.detail;
|
||||||
|
this.numpadMode = mode;
|
||||||
|
NumberBuffer.reset();
|
||||||
|
}
|
||||||
|
disallowLineQuantityChange() {
|
||||||
|
//This function will check if we should check or not the way we handle the line deletion.
|
||||||
|
//If the localisation prevent the line deletion, this function will return true, following the override in the multiple modules.
|
||||||
|
//By default, we'll always set the quantity according to the buffer input.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async _updateSelectedOrderline(event) {
|
||||||
|
if(this.disallowLineQuantityChange()) {
|
||||||
|
let order = this.env.pos.get_order();
|
||||||
|
let selectedLine = order.get_selected_orderline();
|
||||||
|
let last_id = Object.keys(order.orderlines._byId)[Object.keys(order.orderlines._byId).length-1];
|
||||||
|
let currentQuantity = this.env.pos.get_order().get_selected_orderline().get_quantity();
|
||||||
|
|
||||||
|
if(last_id != selectedLine.cid)
|
||||||
|
this._showDecreaseQuantityPopup();
|
||||||
|
else if(currentQuantity === 1 && event.detail.buffer < 1) // Allow to put a value below 1 for the last product entry.
|
||||||
|
this._setValue(event.detail.buffer);
|
||||||
|
else if(currentQuantity < event.detail.buffer)
|
||||||
|
this._setValue(event.detail.buffer);
|
||||||
|
else if(event.detail.buffer < currentQuantity)
|
||||||
|
this._showDecreaseQuantityPopup();
|
||||||
|
} else {
|
||||||
|
let { buffer } = event.detail;
|
||||||
|
let val = buffer === null ? 'remove' : buffer;
|
||||||
|
this._setValue(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _newOrderlineSelected() {
|
||||||
|
NumberBuffer.reset();
|
||||||
|
}
|
||||||
|
_setValue(val) {
|
||||||
|
if (this.currentOrder.get_selected_orderline()) {
|
||||||
|
if (this.numpadMode === 'quantity') {
|
||||||
|
this.currentOrder.get_selected_orderline().set_quantity(val);
|
||||||
|
} else if (this.numpadMode === 'discount') {
|
||||||
|
this.currentOrder.get_selected_orderline().set_discount(val);
|
||||||
|
} else if (this.numpadMode === 'price') {
|
||||||
|
var selected_orderline = this.currentOrder.get_selected_orderline();
|
||||||
|
selected_orderline.price_manually_set = true;
|
||||||
|
selected_orderline.set_unit_price(val);
|
||||||
|
}
|
||||||
|
if (this.env.pos.config.iface_customer_facing_display) {
|
||||||
|
this.env.pos.send_current_order_to_customer_facing_display();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_barcodeProductAction(code) {
|
||||||
|
// NOTE: scan_product call has side effect in pos if it returned true.
|
||||||
|
if (!this.env.pos.scan_product(code)) {
|
||||||
|
this._barcodeErrorAction(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_barcodeClientAction(code) {
|
||||||
|
const partner = this.env.pos.db.get_partner_by_barcode(code.code);
|
||||||
|
if (partner) {
|
||||||
|
if (this.currentOrder.get_client() !== partner) {
|
||||||
|
this.currentOrder.set_client(partner);
|
||||||
|
this.currentOrder.set_pricelist(
|
||||||
|
_.findWhere(this.env.pos.pricelists, {
|
||||||
|
id: partner.property_product_pricelist[0],
|
||||||
|
}) || this.env.pos.default_pricelist
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this._barcodeErrorAction(code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_barcodeDiscountAction(code) {
|
||||||
|
var last_orderline = this.currentOrder.get_last_orderline();
|
||||||
|
if (last_orderline) {
|
||||||
|
last_orderline.set_discount(code.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// IMPROVEMENT: The following two methods should be in PosScreenComponent?
|
||||||
|
// Why? Because once we start declaring barcode actions in different
|
||||||
|
// screens, these methods will also be declared over and over.
|
||||||
|
_barcodeErrorAction(code) {
|
||||||
|
this.showPopup('ErrorBarcodePopup', { code: this._codeRepr(code) });
|
||||||
|
}
|
||||||
|
_codeRepr(code) {
|
||||||
|
if (code.code.length > 32) {
|
||||||
|
return code.code.substring(0, 29) + '...';
|
||||||
|
} else {
|
||||||
|
return code.code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* override this method to perform procedure if the scale is not available.
|
||||||
|
* @see isScaleAvailable
|
||||||
|
*/
|
||||||
|
async _onScaleNotAvailable() {}
|
||||||
|
async _showDecreaseQuantityPopup() {
|
||||||
|
const { confirmed, payload: inputNumber } = await this.showPopup('NumberPopup', {
|
||||||
|
startingValue: 0,
|
||||||
|
title: this.env._t('Decrease the quantity by'),
|
||||||
|
});
|
||||||
|
let decreaseQuantity = Math.abs(inputNumber);
|
||||||
|
if (confirmed && decreaseQuantity) {
|
||||||
|
let order = this.env.pos.get_order();
|
||||||
|
let selectedLine = this.env.pos.get_order().get_selected_orderline();
|
||||||
|
|
||||||
|
let quantity = 0;
|
||||||
|
order.get_orderlines().forEach(orderLine => {
|
||||||
|
if(orderLine.get_product().id === selectedLine.get_product().id && orderLine.get_discount() === selectedLine.get_discount())
|
||||||
|
quantity += orderLine.get_quantity();
|
||||||
|
});
|
||||||
|
|
||||||
|
if(decreaseQuantity > quantity) {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Order error'),
|
||||||
|
body: this.env._t('Not allowed to take back more than was ordered.'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let decreaseLine = selectedLine.clone();
|
||||||
|
decreaseLine.order = order;
|
||||||
|
decreaseLine.set_quantity(-decreaseQuantity);
|
||||||
|
order.add_orderline(decreaseLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async _onClickCustomer() {
|
||||||
|
// IMPROVEMENT: This code snippet is very similar to selectClient of PaymentScreen.
|
||||||
|
const currentClient = this.currentOrder.get_client();
|
||||||
|
const { confirmed, payload: newClient } = await this.showTempScreen(
|
||||||
|
'ClientListScreen',
|
||||||
|
{ client: currentClient }
|
||||||
|
);
|
||||||
|
if (confirmed) {
|
||||||
|
this.currentOrder.set_client(newClient);
|
||||||
|
this.currentOrder.updatePricelist(newClient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_onClickPay() {
|
||||||
|
this.showScreen('PaymentScreen');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProductScreen.template = 'ProductScreen';
|
||||||
|
|
||||||
|
ProductScreen.controlButtons = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object} controlButton
|
||||||
|
* @param {Function} controlButton.component
|
||||||
|
* Can be any base class or base class callback that is added in the Registries.Component.
|
||||||
|
* @param {Function} controlButton.condition zero argument function that is bound
|
||||||
|
* to the instance of ProductScreen, such that `this.env.pos` can be used
|
||||||
|
* inside the function.
|
||||||
|
* @param {Array} [controlButton.position] array of two elements
|
||||||
|
* [locator, relativeTo]
|
||||||
|
* locator: string -> any of ('before', 'after', 'replace')
|
||||||
|
* relativeTo: string -> other controlButtons component name
|
||||||
|
*/
|
||||||
|
ProductScreen.addControlButton = function(controlButton) {
|
||||||
|
// We set the name first.
|
||||||
|
if (!controlButton.name) {
|
||||||
|
controlButton.name = controlButton.component.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no position is set, we just push it to the array.
|
||||||
|
if (!controlButton.position) {
|
||||||
|
this.controlButtons.push(controlButton);
|
||||||
|
} else {
|
||||||
|
// Find where to put the new controlButton.
|
||||||
|
const [locator, relativeTo] = controlButton.position;
|
||||||
|
let whereIndex = -1;
|
||||||
|
for (let i = 0; i < this.controlButtons.length; i++) {
|
||||||
|
if (this.controlButtons[i].name === relativeTo) {
|
||||||
|
if (['before', 'replace'].includes(locator)) {
|
||||||
|
whereIndex = i;
|
||||||
|
} else if (locator === 'after') {
|
||||||
|
whereIndex = i + 1;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If found where to put, then perform the necessary mutation of
|
||||||
|
// the buttons array.
|
||||||
|
// Else, we just push this controlButton to the array.
|
||||||
|
if (whereIndex > -1) {
|
||||||
|
this.controlButtons.splice(
|
||||||
|
whereIndex,
|
||||||
|
locator === 'replace' ? 1 : 0,
|
||||||
|
controlButton
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let warningMessage =
|
||||||
|
`'${controlButton.name}' has invalid 'position' ([${locator}, ${relativeTo}]).` +
|
||||||
|
'It is pushed to the controlButtons stack instead.';
|
||||||
|
console.warn(warningMessage);
|
||||||
|
this.controlButtons.push(controlButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Registries.Component.add(ProductScreen);
|
||||||
|
|
||||||
|
return ProductScreen;
|
||||||
|
});
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
odoo.define('point_of_sale.ProductsWidget', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState } = owl.hooks;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const { useListener } = require('web.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ProductsWidget extends PosComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {number?} props.startCategoryId
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useListener('switch-category', this._switchCategory);
|
||||||
|
useListener('update-search', this._updateSearch);
|
||||||
|
useListener('clear-search', this._clearSearch);
|
||||||
|
this.state = useState({ searchWord: '' });
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
this.env.pos.on('change:selectedCategoryId', this.render, this);
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
this.env.pos.off('change:selectedCategoryId', null, this);
|
||||||
|
}
|
||||||
|
get selectedCategoryId() {
|
||||||
|
return this.env.pos.get('selectedCategoryId');
|
||||||
|
}
|
||||||
|
get searchWord() {
|
||||||
|
return this.state.searchWord.trim();
|
||||||
|
}
|
||||||
|
get productsToDisplay() {
|
||||||
|
if (this.searchWord !== '') {
|
||||||
|
return this.env.pos.db.search_product_in_category(
|
||||||
|
this.selectedCategoryId,
|
||||||
|
this.searchWord
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.env.pos.db.get_product_by_category(this.selectedCategoryId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get subcategories() {
|
||||||
|
return this.env.pos.db
|
||||||
|
.get_category_childs_ids(this.selectedCategoryId)
|
||||||
|
.map(id => this.env.pos.db.get_category_by_id(id));
|
||||||
|
}
|
||||||
|
get breadcrumbs() {
|
||||||
|
if (this.selectedCategoryId === this.env.pos.db.root_category_id) return [];
|
||||||
|
return [
|
||||||
|
...this.env.pos.db
|
||||||
|
.get_category_ancestors_ids(this.selectedCategoryId)
|
||||||
|
.slice(1),
|
||||||
|
this.selectedCategoryId,
|
||||||
|
].map(id => this.env.pos.db.get_category_by_id(id));
|
||||||
|
}
|
||||||
|
_switchCategory(event) {
|
||||||
|
this.env.pos.set('selectedCategoryId', event.detail);
|
||||||
|
}
|
||||||
|
_updateSearch(event) {
|
||||||
|
this.state.searchWord = event.detail;
|
||||||
|
}
|
||||||
|
_clearSearch() {
|
||||||
|
this.state.searchWord = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProductsWidget.template = 'ProductsWidget';
|
||||||
|
|
||||||
|
Registries.Component.add(ProductsWidget);
|
||||||
|
|
||||||
|
return ProductsWidget;
|
||||||
|
});
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
odoo.define('point_of_sale.ProductsWidgetControlPanel', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useRef } = owl.hooks;
|
||||||
|
const { debounce } = owl.utils;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ProductsWidgetControlPanel extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.searchTimeout = null;
|
||||||
|
this.searchWordInput = useRef('search-word-input');
|
||||||
|
this.updateSearch = debounce(this.updateSearch, 100);
|
||||||
|
}
|
||||||
|
clearSearch() {
|
||||||
|
this.searchWordInput.el.value = '';
|
||||||
|
this.trigger('clear-search');
|
||||||
|
}
|
||||||
|
updateSearch(event) {
|
||||||
|
this.trigger('update-search', event.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ProductsWidgetControlPanel.template = 'ProductsWidgetControlPanel';
|
||||||
|
|
||||||
|
Registries.Component.add(ProductsWidgetControlPanel);
|
||||||
|
|
||||||
|
return ProductsWidgetControlPanel;
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
odoo.define('point_of_sale.OrderReceipt', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class OrderReceipt extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this._receiptEnv = this.props.order.getOrderReceiptEnv();
|
||||||
|
}
|
||||||
|
willUpdateProps(nextProps) {
|
||||||
|
this._receiptEnv = nextProps.order.getOrderReceiptEnv();
|
||||||
|
}
|
||||||
|
get receipt() {
|
||||||
|
return this.receiptEnv.receipt;
|
||||||
|
}
|
||||||
|
get orderlines() {
|
||||||
|
return this.receiptEnv.orderlines;
|
||||||
|
}
|
||||||
|
get paymentlines() {
|
||||||
|
return this.receiptEnv.paymentlines;
|
||||||
|
}
|
||||||
|
get isTaxIncluded() {
|
||||||
|
return Math.abs(this.receipt.subtotal - this.receipt.total_with_tax) <= 0.000001;
|
||||||
|
}
|
||||||
|
get receiptEnv () {
|
||||||
|
return this._receiptEnv;
|
||||||
|
}
|
||||||
|
isSimple(line) {
|
||||||
|
return (
|
||||||
|
line.discount === 0 &&
|
||||||
|
line.unit_name === 'Units' &&
|
||||||
|
line.quantity === 1 &&
|
||||||
|
!(
|
||||||
|
line.display_discount_policy == 'without_discount' &&
|
||||||
|
line.price != line.price_lst
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OrderReceipt.template = 'OrderReceipt';
|
||||||
|
|
||||||
|
Registries.Component.add(OrderReceipt);
|
||||||
|
|
||||||
|
return OrderReceipt;
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
odoo.define('point_of_sale.ReceiptScreen', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const core = require('web.core');
|
||||||
|
const { useRef, useState } = owl.hooks;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const { useErrorHandlers, onChangeOrder } = require('point_of_sale.custom_hooks');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
const _t = core._t;
|
||||||
|
|
||||||
|
class ReceiptScreen extends PosComponent {
|
||||||
|
/**
|
||||||
|
* Optional props:
|
||||||
|
* printInvoiceIsShown: Boolean
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useErrorHandlers();
|
||||||
|
onChangeOrder(null, (newOrder) => newOrder && this.render());
|
||||||
|
this.state = useState({ printInvoiceIsShown: this.props.printInvoiceIsShown });
|
||||||
|
this.orderReceipt = useRef('order-receipt');
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
// Here, we send a task to the event loop that handles
|
||||||
|
// the printing of the receipt when the component is mounted.
|
||||||
|
// We are doing this because we want the receipt screen to be
|
||||||
|
// displayed regardless of what happen to the handleAutoPrint
|
||||||
|
// call.
|
||||||
|
setTimeout(async () => await this.handleAutoPrint(), 0);
|
||||||
|
}
|
||||||
|
get change() {
|
||||||
|
return this.env.pos.format_currency(this.currentOrder.get_change());
|
||||||
|
}
|
||||||
|
get currentOrder() {
|
||||||
|
return this.env.pos.get_order();
|
||||||
|
}
|
||||||
|
get nextScreen() {
|
||||||
|
return { name: 'ProductScreen' };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This function is called outside the rendering call stack. This way,
|
||||||
|
* we don't block the displaying of ReceiptScreen when it is mounted; additionally,
|
||||||
|
* any error that can happen during the printing does not affect the rendering.
|
||||||
|
*/
|
||||||
|
async handleAutoPrint() {
|
||||||
|
if (this._shouldAutoPrint() && !this.currentOrder.is_to_email()) {
|
||||||
|
await this.printReceipt();
|
||||||
|
if (this.currentOrder._printed && this._shouldCloseImmediately()) {
|
||||||
|
this.orderDone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
orderDone() {
|
||||||
|
this.currentOrder.finalize();
|
||||||
|
const { name, props } = this.nextScreen;
|
||||||
|
this.showScreen(name, props);
|
||||||
|
}
|
||||||
|
async printReceipt() {
|
||||||
|
if (this.env.pos.proxy.printer) {
|
||||||
|
const printResult = await this.env.pos.proxy.printer.print_receipt(
|
||||||
|
this.orderReceipt.el.outerHTML
|
||||||
|
);
|
||||||
|
if (printResult.successful) {
|
||||||
|
this.currentOrder._printed = true;
|
||||||
|
} else {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: printResult.message.title,
|
||||||
|
body: printResult.message.body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this._printWeb();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async onPrintInvoice() {
|
||||||
|
// The button element of this event handler appears when the order
|
||||||
|
// failed to sync and is to be invoiced. What we do here is to try
|
||||||
|
// to sync again to eventually print the invoice.
|
||||||
|
try {
|
||||||
|
await this.env.pos.push_and_invoice_order(this.currentOrder);
|
||||||
|
this.state.printInvoiceIsShown = false;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw error;
|
||||||
|
} else {
|
||||||
|
await this._handlePushOrderError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_shouldAutoPrint() {
|
||||||
|
return this.env.pos.config.iface_print_auto && !this.currentOrder._printed;
|
||||||
|
}
|
||||||
|
_shouldCloseImmediately() {
|
||||||
|
var invoiced_finalized = this.currentOrder.is_to_invoice()
|
||||||
|
? this.currentOrder.finalized
|
||||||
|
: true;
|
||||||
|
return (
|
||||||
|
this.env.pos.proxy.printer &&
|
||||||
|
this.env.pos.config.iface_print_skip_screen &&
|
||||||
|
invoiced_finalized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
async _printWeb() {
|
||||||
|
if ($.browser.safari) {
|
||||||
|
document.execCommand('print', false, null);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
window.print();
|
||||||
|
this.currentOrder._printed = true;
|
||||||
|
} catch (err) {
|
||||||
|
if (navigator.userAgent.toLowerCase().indexOf('android') > -1) {
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: _t('Printing is not supported on some android browsers'),
|
||||||
|
body: _t(
|
||||||
|
'Printing is not supported on some android browsers due to no default printing protocol ' +
|
||||||
|
'is available. It is possible to print your tickets by making use of an IoT Box.'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReceiptScreen.template = 'ReceiptScreen';
|
||||||
|
|
||||||
|
Registries.Component.add(ReceiptScreen);
|
||||||
|
|
||||||
|
return ReceiptScreen;
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
odoo.define('point_of_sale.WrappedProductNameLines', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class WrappedProductNameLines extends PosComponent {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
this.line = this.props.line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WrappedProductNameLines.template = 'WrappedProductNameLines';
|
||||||
|
|
||||||
|
Registries.Component.add(WrappedProductNameLines);
|
||||||
|
|
||||||
|
return WrappedProductNameLines;
|
||||||
|
});
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
odoo.define('point_of_sale.ScaleScreen', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { useState, useExternalListener } = owl.hooks;
|
||||||
|
const PosComponent = require('point_of_sale.PosComponent');
|
||||||
|
const { round_precision: round_pr } = require('web.utils');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
|
||||||
|
class ScaleScreen extends PosComponent {
|
||||||
|
/**
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} props.product The product to weight.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
useExternalListener(document, 'keyup', this._onHotkeys);
|
||||||
|
this.state = useState({ weight: 0 });
|
||||||
|
}
|
||||||
|
mounted() {
|
||||||
|
// start the scale reading
|
||||||
|
this._readScale();
|
||||||
|
}
|
||||||
|
willUnmount() {
|
||||||
|
// stop the scale reading
|
||||||
|
this.env.pos.proxy_queue.clear();
|
||||||
|
}
|
||||||
|
back() {
|
||||||
|
this.props.resolve({ confirmed: false, payload: null });
|
||||||
|
this.trigger('close-temp-screen');
|
||||||
|
}
|
||||||
|
confirm() {
|
||||||
|
this.props.resolve({
|
||||||
|
confirmed: true,
|
||||||
|
payload: { weight: this.state.weight },
|
||||||
|
});
|
||||||
|
this.trigger('close-temp-screen');
|
||||||
|
}
|
||||||
|
_onHotkeys(event) {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
this.back();
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
this.confirm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_readScale() {
|
||||||
|
this.env.pos.proxy_queue.schedule(this._setWeight.bind(this), {
|
||||||
|
duration: 500,
|
||||||
|
repeat: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _setWeight() {
|
||||||
|
const reading = await this.env.pos.proxy.scale_read();
|
||||||
|
this.state.weight = reading.weight;
|
||||||
|
}
|
||||||
|
get _activePricelist() {
|
||||||
|
const current_order = this.env.pos.get_order();
|
||||||
|
let current_pricelist = this.env.pos.default_pricelist;
|
||||||
|
if (current_order) {
|
||||||
|
current_pricelist = current_order.pricelist;
|
||||||
|
}
|
||||||
|
return current_pricelist;
|
||||||
|
}
|
||||||
|
get productWeightString() {
|
||||||
|
const defaultstr = (this.state.weight || 0).toFixed(3) + ' Kg';
|
||||||
|
if (!this.props.product || !this.env.pos) {
|
||||||
|
return defaultstr;
|
||||||
|
}
|
||||||
|
const unit_id = this.props.product.uom_id;
|
||||||
|
if (!unit_id) {
|
||||||
|
return defaultstr;
|
||||||
|
}
|
||||||
|
const unit = this.env.pos.units_by_id[unit_id[0]];
|
||||||
|
const weight = round_pr(this.state.weight || 0, unit.rounding);
|
||||||
|
let weightstr = weight.toFixed(Math.ceil(Math.log(1.0 / unit.rounding) / Math.log(10)));
|
||||||
|
weightstr += ' ' + unit.name;
|
||||||
|
return weightstr;
|
||||||
|
}
|
||||||
|
get computedPriceString() {
|
||||||
|
return this.env.pos.format_currency(this.productPrice * this.state.weight);
|
||||||
|
}
|
||||||
|
get productPrice() {
|
||||||
|
const product = this.props.product;
|
||||||
|
return (product ? product.get_price(this._activePricelist, this.state.weight) : 0) || 0;
|
||||||
|
}
|
||||||
|
get productName() {
|
||||||
|
return (
|
||||||
|
(this.props.product ? this.props.product.display_name : undefined) ||
|
||||||
|
'Unnamed Product'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
get productUom() {
|
||||||
|
return this.props.product
|
||||||
|
? this.env.pos.units_by_id[this.props.product.uom_id[0]].name
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScaleScreen.template = 'ScaleScreen';
|
||||||
|
|
||||||
|
Registries.Component.add(ScaleScreen);
|
||||||
|
|
||||||
|
return ScaleScreen;
|
||||||
|
});
|
||||||
@@ -16,7 +16,8 @@ var BarcodeReader = core.Class.extend({
|
|||||||
|
|
||||||
init: function (attributes) {
|
init: function (attributes) {
|
||||||
this.pos = attributes.pos;
|
this.pos = attributes.pos;
|
||||||
this.action_callback = {};
|
this.action_callbacks = {};
|
||||||
|
this.exclusive_callbacks = {};
|
||||||
this.proxy = attributes.proxy;
|
this.proxy = attributes.proxy;
|
||||||
this.remote_scanning = false;
|
this.remote_scanning = false;
|
||||||
this.remote_active = 0;
|
this.remote_active = 0;
|
||||||
@@ -34,21 +35,6 @@ var BarcodeReader = core.Class.extend({
|
|||||||
this.barcode_parser = barcode_parser;
|
this.barcode_parser = barcode_parser;
|
||||||
},
|
},
|
||||||
|
|
||||||
save_callbacks: function () {
|
|
||||||
var callbacks = {};
|
|
||||||
for (var name in this.action_callback) {
|
|
||||||
callbacks[name] = this.action_callback[name];
|
|
||||||
}
|
|
||||||
this.action_callback_stack.push(callbacks);
|
|
||||||
},
|
|
||||||
|
|
||||||
restore_callbacks: function () {
|
|
||||||
if (this.action_callback_stack.length) {
|
|
||||||
var callbacks = this.action_callback_stack.pop();
|
|
||||||
this.action_callback = callbacks;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// when a barcode is scanned and parsed, the callback corresponding
|
// when a barcode is scanned and parsed, the callback corresponding
|
||||||
// to its type is called with the parsed_barcode as a parameter.
|
// to its type is called with the parsed_barcode as a parameter.
|
||||||
// (parsed_barcode is the result of parse_barcode(barcode))
|
// (parsed_barcode is the result of parse_barcode(barcode))
|
||||||
@@ -59,36 +45,75 @@ var BarcodeReader = core.Class.extend({
|
|||||||
//
|
//
|
||||||
// possible actions include :
|
// possible actions include :
|
||||||
// 'product' | 'cashier' | 'client' | 'discount'
|
// 'product' | 'cashier' | 'client' | 'discount'
|
||||||
set_action_callback: function (action, callback) {
|
set_action_callback: function (name, callback) {
|
||||||
if (arguments.length == 2) {
|
if (this.action_callbacks[name]) {
|
||||||
this.action_callback[action] = callback;
|
this.action_callbacks[name].add(callback);
|
||||||
} else {
|
} else {
|
||||||
var actions = arguments[0];
|
this.action_callbacks[name] = new Set([callback]);
|
||||||
for (var action in actions) {
|
}
|
||||||
this.set_action_callback(action,actions[action]);
|
},
|
||||||
|
|
||||||
|
remove_action_callback: function(name, callback) {
|
||||||
|
if (!callback) {
|
||||||
|
delete this.action_callbacks[name];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callbacks = this.action_callbacks[name];
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
delete this.action_callbacks[name];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
//remove all action callbacks
|
/**
|
||||||
reset_action_callbacks: function () {
|
* Allow setting of exclusive callbacks. If there are exclusive callbacks,
|
||||||
for (var action in this.action_callback) {
|
* these callbacks are called neglecting the regular callbacks. This is
|
||||||
this.action_callback[action] = undefined;
|
* useful for rendered Components that wants to take exclusive access
|
||||||
|
* to the barcode reader.
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @param {Function} callback function that takes parsed barcode
|
||||||
|
*/
|
||||||
|
set_exclusive_callback: function (name, callback) {
|
||||||
|
if (this.exclusive_callbacks[name]) {
|
||||||
|
this.exclusive_callbacks[name].add(callback);
|
||||||
|
} else {
|
||||||
|
this.exclusive_callbacks[name] = new Set([callback]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
remove_exclusive_callback: function (name, callback) {
|
||||||
|
if (!callback) {
|
||||||
|
delete this.exclusive_callbacks[name];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const callbacks = this.exclusive_callbacks[name];
|
||||||
|
if (callbacks) {
|
||||||
|
callbacks.delete(callback);
|
||||||
|
if (callbacks.size === 0) {
|
||||||
|
delete this.exclusive_callbacks[name];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
scan: function (code) {
|
scan: function (code) {
|
||||||
if (!code) {
|
if (!code) return;
|
||||||
return;
|
|
||||||
}
|
const callbacks = Object.keys(this.exclusive_callbacks).length
|
||||||
var parsed_result = this.barcode_parser.parse_barcode(code);
|
? this.exclusive_callbacks
|
||||||
if (this.action_callback[parsed_result.type]) {
|
: this.action_callbacks;
|
||||||
this.action_callback[parsed_result.type](parsed_result);
|
|
||||||
} else if (this.action_callback.error) {
|
const parsed_result = this.barcode_parser.parse_barcode(code);
|
||||||
this.action_callback.error(parsed_result);
|
if (callbacks[parsed_result.type]) {
|
||||||
|
[...callbacks[parsed_result.type]].map((cb) => cb(parsed_result));
|
||||||
|
} else if (callbacks.error) {
|
||||||
|
[...callbacks.error].map((cb) => cb(parsed_result));
|
||||||
} else {
|
} else {
|
||||||
console.warn("Ignored Barcode Scan:", parsed_result);
|
console.warn('Ignored Barcode Scan:', parsed_result);
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// the barcode scanner will listen on the hw_proxy/scanner interface for
|
// the barcode scanner will listen on the hw_proxy/scanner interface for
|
||||||
|
|||||||
@@ -1,937 +0,0 @@
|
|||||||
odoo.define('point_of_sale.chrome', function (require) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var PosBaseWidget = require('point_of_sale.BaseWidget');
|
|
||||||
var gui = require('point_of_sale.gui');
|
|
||||||
var keyboard = require('point_of_sale.keyboard');
|
|
||||||
var models = require('point_of_sale.models');
|
|
||||||
var AbstractAction = require('web.AbstractAction');
|
|
||||||
var core = require('web.core');
|
|
||||||
var ajax = require('web.ajax');
|
|
||||||
var CrashManager = require('web.CrashManager').CrashManager;
|
|
||||||
var rpc = require('web.rpc');
|
|
||||||
var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
|
|
||||||
|
|
||||||
|
|
||||||
var _t = core._t;
|
|
||||||
var _lt = core._lt;
|
|
||||||
var QWeb = core.qweb;
|
|
||||||
|
|
||||||
/* -------- The Order Selector -------- */
|
|
||||||
|
|
||||||
// Allows the cashier to create / delete and
|
|
||||||
// switch between orders.
|
|
||||||
|
|
||||||
var OrderSelectorWidget = PosBaseWidget.extend({
|
|
||||||
template: 'OrderSelectorWidget',
|
|
||||||
init: function(parent, options) {
|
|
||||||
this._super(parent, options);
|
|
||||||
this.pos.get('orders').bind('add remove change',this.renderElement,this);
|
|
||||||
this.pos.bind('change:selectedOrder',this.renderElement,this);
|
|
||||||
},
|
|
||||||
get_order_by_uid: function(uid) {
|
|
||||||
var orders = this.pos.get_order_list();
|
|
||||||
for (var i = 0; i < orders.length; i++) {
|
|
||||||
if (orders[i].uid === uid) {
|
|
||||||
return orders[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
order_click_handler: function(event,$el) {
|
|
||||||
var order = this.get_order_by_uid($el.data('uid'));
|
|
||||||
if (order) {
|
|
||||||
this.pos.set_order(order);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
neworder_click_handler: function(event, $el) {
|
|
||||||
this.pos.add_new_order();
|
|
||||||
},
|
|
||||||
deleteorder_click_handler: function(event, $el) {
|
|
||||||
var self = this;
|
|
||||||
var order = this.pos.get_order();
|
|
||||||
if (!order) {
|
|
||||||
return;
|
|
||||||
} else if ( !order.is_empty() ){
|
|
||||||
this.gui.show_popup('confirm',{
|
|
||||||
'title': _t('Destroy Current Order ?'),
|
|
||||||
'body': _t('You will lose any data associated with the current order'),
|
|
||||||
confirm: function(){
|
|
||||||
self.pos.delete_current_order();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.pos.delete_current_order();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
renderElement: function(){
|
|
||||||
var self = this;
|
|
||||||
this._super();
|
|
||||||
this.$('.order-button.select-order').click(function(event){
|
|
||||||
self.order_click_handler(event,$(this));
|
|
||||||
});
|
|
||||||
this.$('.neworder-button').click(function(event){
|
|
||||||
self.neworder_click_handler(event,$(this));
|
|
||||||
});
|
|
||||||
this.$('.deleteorder-button').click(function(event){
|
|
||||||
self.deleteorder_click_handler(event,$(this));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------- The User Name Widget ------- */
|
|
||||||
|
|
||||||
// Displays the current cashier's name
|
|
||||||
|
|
||||||
var UsernameWidget = PosBaseWidget.extend({
|
|
||||||
template: 'UsernameWidget',
|
|
||||||
init: function(parent, options){
|
|
||||||
options = options || {};
|
|
||||||
this._super(parent,options);
|
|
||||||
},
|
|
||||||
get_name: function(){
|
|
||||||
var user = this.pos.get_cashier();
|
|
||||||
if(user){
|
|
||||||
return user.name;
|
|
||||||
}else{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* -------- The Header Button --------- */
|
|
||||||
|
|
||||||
// Used to quickly add buttons with simple
|
|
||||||
// labels and actions to the point of sale
|
|
||||||
// header.
|
|
||||||
|
|
||||||
var HeaderButtonWidget = PosBaseWidget.extend({
|
|
||||||
template: 'HeaderButtonWidget',
|
|
||||||
init: function(parent, options){
|
|
||||||
options = options || {};
|
|
||||||
this._super(parent, options);
|
|
||||||
this.action = options.action;
|
|
||||||
this.label = options.label;
|
|
||||||
this.button_class = options.button_class;
|
|
||||||
|
|
||||||
},
|
|
||||||
renderElement: function(){
|
|
||||||
var self = this;
|
|
||||||
this._super();
|
|
||||||
if(this.action){
|
|
||||||
this.$el.click(function(){
|
|
||||||
self.action();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
show: function() { this.$el.removeClass('oe_hidden'); },
|
|
||||||
hide: function() { this.$el.addClass('oe_hidden'); },
|
|
||||||
});
|
|
||||||
|
|
||||||
/* --------- The Debug Widget --------- */
|
|
||||||
|
|
||||||
// The debug widget lets the user control
|
|
||||||
// and monitor the hardware and software status
|
|
||||||
// without the use of the proxy, or to access
|
|
||||||
// the raw locally stored db values, useful
|
|
||||||
// for debugging
|
|
||||||
|
|
||||||
var DebugWidget = PosBaseWidget.extend({
|
|
||||||
template: "DebugWidget",
|
|
||||||
eans:{
|
|
||||||
admin_badge: '0410100000006',
|
|
||||||
client_badge: '0420200000004',
|
|
||||||
invalid_ean: '1232456',
|
|
||||||
soda_33cl: '5449000000996',
|
|
||||||
oranges_kg: '2100002031410',
|
|
||||||
lemon_price: '2301000001560',
|
|
||||||
unknown_product: '9900000000004',
|
|
||||||
},
|
|
||||||
events:[
|
|
||||||
'open_cashbox',
|
|
||||||
'print_receipt',
|
|
||||||
'scale_read',
|
|
||||||
],
|
|
||||||
init: function(parent,options){
|
|
||||||
this._super(parent,options);
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// for dragging the debug widget around
|
|
||||||
this.dragging = false;
|
|
||||||
this.dragpos = {x:0, y:0};
|
|
||||||
|
|
||||||
function eventpos(event){
|
|
||||||
if(event.touches && event.touches[0]){
|
|
||||||
return {x: event.touches[0].screenX, y: event.touches[0].screenY};
|
|
||||||
}else{
|
|
||||||
return {x: event.screenX, y: event.screenY};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dragend_handler = function(event){
|
|
||||||
self.dragging = false;
|
|
||||||
};
|
|
||||||
this.dragstart_handler = function(event){
|
|
||||||
self.dragging = true;
|
|
||||||
self.dragpos = eventpos(event);
|
|
||||||
};
|
|
||||||
this.dragmove_handler = function(event){
|
|
||||||
if(self.dragging){
|
|
||||||
var top = this.offsetTop;
|
|
||||||
var left = this.offsetLeft;
|
|
||||||
var pos = eventpos(event);
|
|
||||||
var dx = pos.x - self.dragpos.x;
|
|
||||||
var dy = pos.y - self.dragpos.y;
|
|
||||||
|
|
||||||
self.dragpos = pos;
|
|
||||||
|
|
||||||
this.style.right = 'auto';
|
|
||||||
this.style.bottom = 'auto';
|
|
||||||
this.style.left = left + dx + 'px';
|
|
||||||
this.style.top = top + dy + 'px';
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
},
|
|
||||||
show: function() {
|
|
||||||
this.$el.css({opacity:0});
|
|
||||||
this.$el.removeClass('oe_hidden');
|
|
||||||
this.$el.animate({opacity:1},250,'swing');
|
|
||||||
},
|
|
||||||
hide: function() {
|
|
||||||
var self = this;
|
|
||||||
this.$el.animate({opacity:0,},250,'swing',function(){
|
|
||||||
self.$el.addClass('oe_hidden');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
start: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if (this.pos.debug) {
|
|
||||||
this.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.el.addEventListener('mouseleave', this.dragend_handler);
|
|
||||||
this.el.addEventListener('mouseup', this.dragend_handler);
|
|
||||||
this.el.addEventListener('touchend', this.dragend_handler);
|
|
||||||
this.el.addEventListener('touchcancel',this.dragend_handler);
|
|
||||||
this.el.addEventListener('mousedown', this.dragstart_handler);
|
|
||||||
this.el.addEventListener('touchstart', this.dragstart_handler);
|
|
||||||
this.el.addEventListener('mousemove', this.dragmove_handler);
|
|
||||||
this.el.addEventListener('touchmove', this.dragmove_handler);
|
|
||||||
|
|
||||||
this.$('.toggle').click(function(){
|
|
||||||
self.hide();
|
|
||||||
});
|
|
||||||
this.$('.button.set_weight').click(function(){
|
|
||||||
var kg = Number(self.$('input.weight').val());
|
|
||||||
if(!isNaN(kg)){
|
|
||||||
self.pos.proxy.debug_set_weight(kg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.$('.button.reset_weight').click(function(){
|
|
||||||
self.$('input.weight').val('');
|
|
||||||
self.pos.proxy.debug_reset_weight();
|
|
||||||
});
|
|
||||||
this.$('.button.custom_ean').click(function(){
|
|
||||||
var ean = self.pos.barcode_reader.barcode_parser.sanitize_ean(self.$('input.ean').val() || '0');
|
|
||||||
self.$('input.ean').val(ean);
|
|
||||||
self.pos.barcode_reader.scan(ean);
|
|
||||||
});
|
|
||||||
this.$('.button.barcode').click(function(){
|
|
||||||
self.pos.barcode_reader.scan(self.$('input.ean').val());
|
|
||||||
});
|
|
||||||
this.$('.button.delete_orders').click(function(){
|
|
||||||
self.gui.show_popup('confirm',{
|
|
||||||
'title': _t('Delete Paid Orders ?'),
|
|
||||||
'body': _t('This operation will permanently destroy all paid orders from the local storage. You will lose all the data. This operation cannot be undone.'),
|
|
||||||
confirm: function(){
|
|
||||||
self.pos.db.remove_all_orders();
|
|
||||||
self.pos.set_synch('connected', 0);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.$('.button.delete_unpaid_orders').click(function(){
|
|
||||||
self.gui.show_popup('confirm',{
|
|
||||||
'title': _t('Delete Unpaid Orders ?'),
|
|
||||||
'body': _t('This operation will destroy all unpaid orders in the browser. You will lose all the unsaved data and exit the point of sale. This operation cannot be undone.'),
|
|
||||||
confirm: function(){
|
|
||||||
self.pos.db.remove_all_unpaid_orders();
|
|
||||||
window.location = '/';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$('.button.export_unpaid_orders').click(function(){
|
|
||||||
self.gui.prepare_download_link(
|
|
||||||
self.pos.export_unpaid_orders(),
|
|
||||||
_t("unpaid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
|
|
||||||
".export_unpaid_orders", ".download_unpaid_orders"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$('.button.export_paid_orders').click(function() {
|
|
||||||
self.gui.prepare_download_link(
|
|
||||||
self.pos.export_paid_orders(),
|
|
||||||
_t("paid orders") + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.json',
|
|
||||||
".export_paid_orders", ".download_paid_orders"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$('.button.display_refresh').click(function () {
|
|
||||||
self.pos.proxy.message('display_refresh', {});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$('.button.import_orders input').on('change', function(event) {
|
|
||||||
var file = event.target.files[0];
|
|
||||||
|
|
||||||
if (file) {
|
|
||||||
var reader = new FileReader();
|
|
||||||
|
|
||||||
reader.onload = function(event) {
|
|
||||||
var report = self.pos.import_orders(event.target.result);
|
|
||||||
self.gui.show_popup('orderimport',{report:report});
|
|
||||||
};
|
|
||||||
|
|
||||||
reader.readAsText(file);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_.each(this.events, function(name){
|
|
||||||
self.pos.proxy.add_notification(name,function(){
|
|
||||||
self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
|
|
||||||
self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* --------- The Status Widget -------- */
|
|
||||||
|
|
||||||
// Base class for widgets that want to display
|
|
||||||
// status in the point of sale header.
|
|
||||||
|
|
||||||
var StatusWidget = PosBaseWidget.extend({
|
|
||||||
status: ['connected','connecting','disconnected','warning','error'],
|
|
||||||
|
|
||||||
set_status: function(status,msg){
|
|
||||||
for(var i = 0; i < this.status.length; i++){
|
|
||||||
this.$('.js_'+this.status[i]).addClass('oe_hidden');
|
|
||||||
}
|
|
||||||
this.$('.js_'+status).removeClass('oe_hidden');
|
|
||||||
|
|
||||||
if(msg){
|
|
||||||
this.$('.js_msg').removeClass('oe_hidden').html(msg);
|
|
||||||
}else{
|
|
||||||
this.$('.js_msg').addClass('oe_hidden').html('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ------- Synch. Notifications ------- */
|
|
||||||
|
|
||||||
// Displays if there are orders that could
|
|
||||||
// not be submitted, and how many.
|
|
||||||
|
|
||||||
var SynchNotificationWidget = StatusWidget.extend({
|
|
||||||
template: 'SynchNotificationWidget',
|
|
||||||
start: function(){
|
|
||||||
var self = this;
|
|
||||||
this.pos.bind('change:synch', function(pos,synch){
|
|
||||||
self.set_status(synch.state, synch.pending);
|
|
||||||
});
|
|
||||||
this.$el.click(function(){
|
|
||||||
self.pos.push_order(null,{'show_error':true});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* --------- The Proxy Status --------- */
|
|
||||||
|
|
||||||
// Displays the status of the hardware proxy
|
|
||||||
// (connected, disconnected, errors ... )
|
|
||||||
|
|
||||||
var ProxyStatusWidget = StatusWidget.extend({
|
|
||||||
template: 'ProxyStatusWidget',
|
|
||||||
set_smart_status: function(status){
|
|
||||||
if(status.status === 'connected'){
|
|
||||||
var warning = false;
|
|
||||||
var msg = '';
|
|
||||||
if(this.pos.config.iface_scan_via_proxy){
|
|
||||||
var scanner = status.drivers.scanner ? status.drivers.scanner.status : false;
|
|
||||||
if( scanner != 'connected' && scanner != 'connecting'){
|
|
||||||
warning = true;
|
|
||||||
msg += _t('Scanner');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( this.pos.config.iface_print_via_proxy ||
|
|
||||||
this.pos.config.iface_cashdrawer ){
|
|
||||||
var printer = status.drivers.printer ? status.drivers.printer.status : false;
|
|
||||||
if (printer != 'connected' && printer != 'connecting') {
|
|
||||||
warning = true;
|
|
||||||
msg = msg ? msg + ' & ' : msg;
|
|
||||||
msg += _t('Printer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if( this.pos.config.iface_electronic_scale ){
|
|
||||||
var scale = status.drivers.scale ? status.drivers.scale.status : false;
|
|
||||||
if( scale != 'connected' && scale != 'connecting' ){
|
|
||||||
warning = true;
|
|
||||||
msg = msg ? msg + ' & ' : msg;
|
|
||||||
msg += _t('Scale');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = msg ? msg + ' ' + _t('Offline') : msg;
|
|
||||||
this.set_status(warning ? 'warning' : 'connected', msg);
|
|
||||||
}else{
|
|
||||||
this.set_status(status.status, status.msg || '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
start: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
this.set_smart_status(this.pos.proxy.get('status'));
|
|
||||||
|
|
||||||
this.pos.proxy.on('change:status',this,function(eh,status){ //FIXME remove duplicate changes
|
|
||||||
self.set_smart_status(status.newValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$el.click(function(){
|
|
||||||
self.pos.connect_to_proxy();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/* --------- The Sale Details --------- */
|
|
||||||
|
|
||||||
/** Print an overview of todays sales.
|
|
||||||
*
|
|
||||||
* If the current cashier is a manager all sales of the day will be printed, else only the sales of the current
|
|
||||||
* session will be printed.
|
|
||||||
*/
|
|
||||||
var SaleDetailsButton = PosBaseWidget.extend({
|
|
||||||
template: 'SaleDetailsButton',
|
|
||||||
start: function(){
|
|
||||||
var self = this;
|
|
||||||
this.$el.click(function(){
|
|
||||||
self.print_sale_details();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/** Print an overview of todays sales.
|
|
||||||
*
|
|
||||||
* By default this will print all sales of the day for current PoS config.
|
|
||||||
*/
|
|
||||||
print_sale_details: function () {
|
|
||||||
var self = this;
|
|
||||||
rpc.query({
|
|
||||||
model: 'report.point_of_sale.report_saledetails',
|
|
||||||
method: 'get_sale_details',
|
|
||||||
args: [false, false, false, [this.pos.pos_session.id]],
|
|
||||||
})
|
|
||||||
.then(function(result){
|
|
||||||
var env = {
|
|
||||||
widget: new PosBaseWidget(self),
|
|
||||||
company: self.pos.company,
|
|
||||||
pos: self.pos,
|
|
||||||
products: result.products,
|
|
||||||
payments: result.payments,
|
|
||||||
taxes: result.taxes,
|
|
||||||
total_paid: result.total_paid,
|
|
||||||
date: (new Date()).toLocaleString(),
|
|
||||||
};
|
|
||||||
var report = QWeb.render('SaleDetailsReport', env);
|
|
||||||
self.pos.proxy.printer.print_receipt(report);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* User interface for distant control over the Client display on the IoT Box */
|
|
||||||
// The boolean posbox_supports_display (in devices.js) will allow interaction to the IoT Box on true, prevents it otherwise
|
|
||||||
// We don't want the incompatible IoT Box to be flooded with 404 errors on arrival of our many requests as it triggers losses of connections altogether
|
|
||||||
var ClientScreenWidget = PosBaseWidget.extend({
|
|
||||||
template: 'ClientScreenWidget',
|
|
||||||
|
|
||||||
change_status_display: function(status) {
|
|
||||||
var msg = ''
|
|
||||||
if (status === 'success') {
|
|
||||||
this.$('.js_warning').addClass('oe_hidden');
|
|
||||||
this.$('.js_disconnected').addClass('oe_hidden');
|
|
||||||
this.$('.js_connected').removeClass('oe_hidden');
|
|
||||||
} else if (status === 'warning') {
|
|
||||||
this.$('.js_disconnected').addClass('oe_hidden');
|
|
||||||
this.$('.js_connected').addClass('oe_hidden');
|
|
||||||
this.$('.js_warning').removeClass('oe_hidden');
|
|
||||||
msg = _t('Connected, Not Owned');
|
|
||||||
} else {
|
|
||||||
this.$('.js_warning').addClass('oe_hidden');
|
|
||||||
this.$('.js_connected').addClass('oe_hidden');
|
|
||||||
this.$('.js_disconnected').removeClass('oe_hidden');
|
|
||||||
msg = _t('Disconnected')
|
|
||||||
if (status === 'not_found') {
|
|
||||||
msg = _t('Client Screen Unsupported. Please upgrade the IoT Box')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$('.oe_customer_display_text').text(msg);
|
|
||||||
},
|
|
||||||
|
|
||||||
status_loop: function() {
|
|
||||||
var self = this;
|
|
||||||
function loop() {
|
|
||||||
if (self.pos.proxy.posbox_supports_display) {
|
|
||||||
self.pos.proxy.test_ownership_of_client_screen().then(
|
|
||||||
function (data) {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
}
|
|
||||||
if (data.status === 'OWNER') {
|
|
||||||
self.change_status_display('success');
|
|
||||||
} else {
|
|
||||||
self.change_status_display('warning');
|
|
||||||
}
|
|
||||||
setTimeout(loop, 3000);
|
|
||||||
},
|
|
||||||
function (err) {
|
|
||||||
if (err.abort) {
|
|
||||||
// Stop the loop
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typeof err == "undefined") {
|
|
||||||
self.change_status_display('failure');
|
|
||||||
} else {
|
|
||||||
self.change_status_display('not_found');
|
|
||||||
self.pos.proxy.posbox_supports_display = false;
|
|
||||||
}
|
|
||||||
setTimeout(loop, 3000);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loop();
|
|
||||||
},
|
|
||||||
|
|
||||||
start: function(){
|
|
||||||
if (this.pos.config.iface_customer_facing_display) {
|
|
||||||
this.show();
|
|
||||||
var self = this;
|
|
||||||
this.$el.click(function(){
|
|
||||||
self.pos.render_html_for_customer_facing_display().then(function(rendered_html) {
|
|
||||||
self.pos.proxy.take_ownership_over_client_screen(rendered_html).then(
|
|
||||||
function(data) {
|
|
||||||
if (typeof data === 'string') {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
}
|
|
||||||
if (data.status === 'success') {
|
|
||||||
self.change_status_display('success');
|
|
||||||
} else {
|
|
||||||
self.change_status_display('warning');
|
|
||||||
}
|
|
||||||
if (!self.pos.proxy.posbox_supports_display) {
|
|
||||||
self.pos.proxy.posbox_supports_display = true;
|
|
||||||
self.status_loop();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
function(err) {
|
|
||||||
if (typeof err == "undefined") {
|
|
||||||
self.change_status_display('failure');
|
|
||||||
} else {
|
|
||||||
self.change_status_display('not_found');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
this.status_loop();
|
|
||||||
} else {
|
|
||||||
this.hide();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*--------------------------------------*\
|
|
||||||
| THE CHROME |
|
|
||||||
\*======================================*/
|
|
||||||
|
|
||||||
// The Chrome is the main widget that contains
|
|
||||||
// all other widgets in the PointOfSale.
|
|
||||||
//
|
|
||||||
// It is the first object instanciated and the
|
|
||||||
// starting point of the point of sale code.
|
|
||||||
//
|
|
||||||
// It is mainly composed of :
|
|
||||||
// - a header, containing the list of orders
|
|
||||||
// - a leftpane, containing the list of bought
|
|
||||||
// products (orderlines)
|
|
||||||
// - a rightpane, containing the screens
|
|
||||||
// (see pos_screens.js)
|
|
||||||
// - popups
|
|
||||||
// - an onscreen keyboard
|
|
||||||
// - .gui which controls the switching between
|
|
||||||
// screens and the showing/closing of popups
|
|
||||||
|
|
||||||
var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
|
|
||||||
template: 'Chrome',
|
|
||||||
init: function() {
|
|
||||||
var self = this;
|
|
||||||
this._super(arguments[0],{});
|
|
||||||
|
|
||||||
this.started = new $.Deferred(); // resolves when DOM is online
|
|
||||||
this.ready = new $.Deferred(); // resolves when the whole GUI has been loaded
|
|
||||||
|
|
||||||
this.pos = new models.PosModel(this.getSession(), {chrome:this});
|
|
||||||
this.gui = new gui.Gui({pos: this.pos, chrome: this});
|
|
||||||
this.chrome = this; // So that chrome's childs have chrome set automatically
|
|
||||||
this.pos.gui = this.gui;
|
|
||||||
|
|
||||||
this.logo_click_time = 0;
|
|
||||||
this.logo_click_count = 0;
|
|
||||||
|
|
||||||
this.previous_touch_y_coordinate = -1;
|
|
||||||
|
|
||||||
this.widget = {}; // contains references to subwidgets instances
|
|
||||||
|
|
||||||
this.cleanup_dom();
|
|
||||||
this.pos.ready.then(function(){
|
|
||||||
self.build_chrome();
|
|
||||||
self.build_widgets();
|
|
||||||
self.disable_rubberbanding();
|
|
||||||
self.disable_backpace_back();
|
|
||||||
self.ready.resolve();
|
|
||||||
self.loading_hide();
|
|
||||||
self.replace_crashmanager();
|
|
||||||
self.pos.push_order();
|
|
||||||
}).guardedCatch(function (err) { // error when loading models data from the backend
|
|
||||||
self.loading_error(err);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
cleanup_dom: function() {
|
|
||||||
// remove default webclient handlers that induce click delay
|
|
||||||
$(document).off();
|
|
||||||
$(window).off();
|
|
||||||
$('html').off();
|
|
||||||
$('body').off();
|
|
||||||
// The above lines removed the bindings, but we really need them for the barcode
|
|
||||||
BarcodeEvents.start();
|
|
||||||
},
|
|
||||||
|
|
||||||
build_chrome: function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
if ($.browser.chrome) {
|
|
||||||
var chrome_version = $.browser.version.split('.')[0];
|
|
||||||
if (parseInt(chrome_version, 10) >= 50) {
|
|
||||||
ajax.loadCSS('/point_of_sale/static/src/css/chrome50.css');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderElement();
|
|
||||||
|
|
||||||
this.$('.pos-logo').click(function(){
|
|
||||||
self.click_logo();
|
|
||||||
});
|
|
||||||
|
|
||||||
if(this.pos.config.iface_big_scrollbars){
|
|
||||||
this.$el.addClass('big-scrollbars');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// displays a system error with the error-traceback
|
|
||||||
// popup.
|
|
||||||
show_error: function(error) {
|
|
||||||
this.gui.show_popup('error-traceback',{
|
|
||||||
'title': error.message,
|
|
||||||
'body': error.message + '\n' + error.data.debug + '\n',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// replaces the error handling of the existing crashmanager which
|
|
||||||
// uses jquery dialog to display the error, to use the pos popup
|
|
||||||
// instead
|
|
||||||
replace_crashmanager: function() {
|
|
||||||
var self = this;
|
|
||||||
CrashManager.include({
|
|
||||||
show_error: function(error) {
|
|
||||||
if (self.gui) {
|
|
||||||
self.show_error(error);
|
|
||||||
} else {
|
|
||||||
this._super(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
click_logo: function() {
|
|
||||||
if (this.pos.debug) {
|
|
||||||
this.widget.debug.show();
|
|
||||||
} else {
|
|
||||||
var self = this;
|
|
||||||
var time = (new Date()).getTime();
|
|
||||||
var delay = 500;
|
|
||||||
if (this.logo_click_time + 500 < time) {
|
|
||||||
this.logo_click_time = time;
|
|
||||||
this.logo_click_count = 1;
|
|
||||||
} else {
|
|
||||||
this.logo_click_time = time;
|
|
||||||
this.logo_click_count += 1;
|
|
||||||
if (this.logo_click_count >= 6) {
|
|
||||||
this.logo_click_count = 0;
|
|
||||||
this.gui.sudo().then(function(){
|
|
||||||
self.widget.debug.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_scrollable: function(element, scrolling_down){
|
|
||||||
var $element = $(element);
|
|
||||||
var scrollable = true;
|
|
||||||
|
|
||||||
if (! scrolling_down && $element.scrollTop() <= 0) {
|
|
||||||
scrollable = false;
|
|
||||||
} else if (scrolling_down && $element.scrollTop() + $element.height() >= element.scrollHeight) {
|
|
||||||
scrollable = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return scrollable;
|
|
||||||
},
|
|
||||||
|
|
||||||
disable_rubberbanding: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
document.body.addEventListener('touchstart', function(event){
|
|
||||||
self.previous_touch_y_coordinate = event.touches[0].clientY;
|
|
||||||
});
|
|
||||||
|
|
||||||
// prevent the pos body from being scrollable.
|
|
||||||
document.body.addEventListener('touchmove',function(event){
|
|
||||||
var node = event.target;
|
|
||||||
var current_touch_y_coordinate = event.touches[0].clientY;
|
|
||||||
var scrolling_down;
|
|
||||||
|
|
||||||
if (current_touch_y_coordinate < self.previous_touch_y_coordinate) {
|
|
||||||
scrolling_down = true;
|
|
||||||
} else {
|
|
||||||
scrolling_down = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
while(node){
|
|
||||||
if(node.classList && node.classList.contains('touch-scrollable') && self._scrollable(node, scrolling_down)){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// prevent backspace from performing a 'back' navigation
|
|
||||||
disable_backpace_back: function() {
|
|
||||||
$(document).on("keydown", function (e) {
|
|
||||||
if (e.which === 8 && !$(e.target).is("input, textarea")) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loading_error: function(err){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var title = err.message;
|
|
||||||
var body = err.stack;
|
|
||||||
|
|
||||||
if(err.message === 'XmlHttpRequestError '){
|
|
||||||
title = 'Network Failure (XmlHttpRequestError)';
|
|
||||||
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
|
|
||||||
}else if(err.code === 200){
|
|
||||||
title = err.data.message;
|
|
||||||
body = err.data.debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
if( typeof body !== 'string' ){
|
|
||||||
body = 'Traceback not available.';
|
|
||||||
}
|
|
||||||
|
|
||||||
var popup = $(QWeb.render('ErrorTracebackPopupWidget',{
|
|
||||||
widget: { options: {title: title , body: body }},
|
|
||||||
}));
|
|
||||||
|
|
||||||
popup.find('.button').click(function(){
|
|
||||||
self.gui.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
popup.css({ zindex: 9001 });
|
|
||||||
|
|
||||||
popup.appendTo(this.$el);
|
|
||||||
},
|
|
||||||
loading_progress: function(fac){
|
|
||||||
this.$('.loader .loader-feedback').removeClass('oe_hidden');
|
|
||||||
this.$('.loader .progress').removeClass('oe_hidden').css({'width': ''+Math.floor(fac*100)+'%'});
|
|
||||||
},
|
|
||||||
loading_message: function(msg,progress){
|
|
||||||
this.$('.loader .loader-feedback').removeClass('oe_hidden');
|
|
||||||
this.$('.loader .message').text(msg);
|
|
||||||
if (typeof progress !== 'undefined') {
|
|
||||||
this.loading_progress(progress);
|
|
||||||
} else {
|
|
||||||
this.$('.loader .progress').addClass('oe_hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loading_skip: function(callback){
|
|
||||||
if(callback){
|
|
||||||
this.$('.loader .loader-feedback').removeClass('oe_hidden');
|
|
||||||
this.$('.loader .button.skip').removeClass('oe_hidden');
|
|
||||||
this.$('.loader .button.skip').off('click');
|
|
||||||
this.$('.loader .button.skip').click(callback);
|
|
||||||
}else{
|
|
||||||
this.$('.loader .button.skip').addClass('oe_hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
loading_hide: function(){
|
|
||||||
var self = this;
|
|
||||||
this.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').addClass('oe_hidden');});
|
|
||||||
},
|
|
||||||
loading_show: function(){
|
|
||||||
this.$('.loader').removeClass('oe_hidden').animate({opacity:1},150,'swing');
|
|
||||||
},
|
|
||||||
widgets: [
|
|
||||||
{
|
|
||||||
'name': 'order_selector',
|
|
||||||
'widget': OrderSelectorWidget,
|
|
||||||
'replace': '.placeholder-OrderSelectorWidget',
|
|
||||||
},{
|
|
||||||
'name': 'sale_details',
|
|
||||||
'widget': SaleDetailsButton,
|
|
||||||
'append': '.pos-rightheader',
|
|
||||||
'condition': function(){ return this.pos.proxy.printer; },
|
|
||||||
},{
|
|
||||||
'name': 'proxy_status',
|
|
||||||
'widget': ProxyStatusWidget,
|
|
||||||
'append': '.pos-rightheader',
|
|
||||||
'condition': function(){ return this.pos.config.use_proxy; },
|
|
||||||
},{
|
|
||||||
'name': 'screen_status',
|
|
||||||
'widget': ClientScreenWidget,
|
|
||||||
'append': '.pos-rightheader',
|
|
||||||
'condition': function(){ return this.pos.config.use_proxy; },
|
|
||||||
},{
|
|
||||||
'name': 'notification',
|
|
||||||
'widget': SynchNotificationWidget,
|
|
||||||
'append': '.pos-rightheader',
|
|
||||||
},{
|
|
||||||
'name': 'close_button',
|
|
||||||
'widget': HeaderButtonWidget,
|
|
||||||
'append': '.pos-rightheader',
|
|
||||||
'args': {
|
|
||||||
label: _t('Close'),
|
|
||||||
action: function(){
|
|
||||||
this.$el.addClass('close_button');
|
|
||||||
var self = this;
|
|
||||||
if (!this.confirmed) {
|
|
||||||
this.$el.addClass('confirm');
|
|
||||||
this.$el.text(_t('Confirm'));
|
|
||||||
this.confirmed = setTimeout(function(){
|
|
||||||
self.$el.removeClass('confirm');
|
|
||||||
self.$el.text(_t('Close'));
|
|
||||||
self.confirmed = false;
|
|
||||||
},2000);
|
|
||||||
} else {
|
|
||||||
clearTimeout(this.confirmed);
|
|
||||||
this.gui.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},{
|
|
||||||
'name': 'username',
|
|
||||||
'widget': UsernameWidget,
|
|
||||||
'replace': '.placeholder-UsernameWidget',
|
|
||||||
},{
|
|
||||||
'name': 'keyboard',
|
|
||||||
'widget': keyboard.OnscreenKeyboardWidget,
|
|
||||||
'replace': '.placeholder-OnscreenKeyboardWidget',
|
|
||||||
},{
|
|
||||||
'name': 'debug',
|
|
||||||
'widget': DebugWidget,
|
|
||||||
'append': '.pos-content',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
load_widgets: function(widgets) {
|
|
||||||
for (var i = 0; i < widgets.length; i++) {
|
|
||||||
var widget = widgets[i];
|
|
||||||
if ( !widget.condition || widget.condition.call(this) ) {
|
|
||||||
var args = typeof widget.args === 'function' ? widget.args(this) : widget.args;
|
|
||||||
var w = new widget.widget(this, args || {});
|
|
||||||
if (widget.replace) {
|
|
||||||
w.replace(this.$(widget.replace));
|
|
||||||
} else if (widget.append) {
|
|
||||||
w.appendTo(this.$(widget.append));
|
|
||||||
} else if (widget.prepend) {
|
|
||||||
w.prependTo(this.$(widget.prepend));
|
|
||||||
} else {
|
|
||||||
w.appendTo(this.$el);
|
|
||||||
}
|
|
||||||
this.widget[widget.name] = w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// This method instantiates all the screens, widgets, etc.
|
|
||||||
build_widgets: function() {
|
|
||||||
var self = this;
|
|
||||||
this.load_widgets(this.widgets);
|
|
||||||
|
|
||||||
this.screens = {};
|
|
||||||
var classe;
|
|
||||||
for (var i = 0; i < this.gui.screen_classes.length; i++) {
|
|
||||||
classe = this.gui.screen_classes[i];
|
|
||||||
if (!classe.condition || classe.condition.call(this)) {
|
|
||||||
var screen = new classe.widget(this,{});
|
|
||||||
screen.appendTo(this.$('.screens'));
|
|
||||||
this.screens[classe.name] = screen;
|
|
||||||
this.gui.add_screen(classe.name, screen);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.popups = {};
|
|
||||||
_.forEach(this.gui.popup_classes, function (classe) {
|
|
||||||
if (!classe.condition || classe.condition.call(self)) {
|
|
||||||
var popup = new classe.widget(self,{});
|
|
||||||
popup.appendTo(self.$('.popups')).then(function () {
|
|
||||||
self.popups[classe.name] = popup;
|
|
||||||
self.gui.add_popup(classe.name, popup);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.gui.set_startup_screen('products');
|
|
||||||
this.gui.set_default_screen('products');
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
destroy: function() {
|
|
||||||
this.pos.destroy();
|
|
||||||
this._super();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
Chrome: Chrome,
|
|
||||||
DebugWidget: DebugWidget,
|
|
||||||
HeaderButtonWidget: HeaderButtonWidget,
|
|
||||||
OrderSelectorWidget: OrderSelectorWidget,
|
|
||||||
ProxyStatusWidget: ProxyStatusWidget,
|
|
||||||
SaleDetailsButton: SaleDetailsButton,
|
|
||||||
ClientScreenWidget: ClientScreenWidget,
|
|
||||||
StatusWidget: StatusWidget,
|
|
||||||
SynchNotificationWidget: SynchNotificationWidget,
|
|
||||||
UsernameWidget: UsernameWidget,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
odoo.define('point_of_sale.custom_hooks', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { Component } = owl;
|
||||||
|
const { onMounted, onPatched, onWillUnmount } = owl.hooks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Introduce error handlers in the component.
|
||||||
|
*
|
||||||
|
* IMPROVEMENT: This is a terrible hook. There could be a better way to handle
|
||||||
|
* the error when the order failed to sync.
|
||||||
|
*/
|
||||||
|
function useErrorHandlers() {
|
||||||
|
const component = Component.current;
|
||||||
|
|
||||||
|
component._handlePushOrderError = async function (error) {
|
||||||
|
// This error handler receives `error` equivalent to `error.message` of the rpc error.
|
||||||
|
if (error.message === 'Backend Invoice') {
|
||||||
|
await this.showPopup('ConfirmPopup', {
|
||||||
|
title: this.env._t('Please print the invoice from the backend'),
|
||||||
|
body:
|
||||||
|
this.env._t(
|
||||||
|
'The order has been synchronized earlier. Please make the invoice from the backend for the order: '
|
||||||
|
) + error.data.order.name,
|
||||||
|
});
|
||||||
|
} else if (error.code < 0) {
|
||||||
|
// XmlHttpRequest Errors
|
||||||
|
const title = this.env._t('Unable to sync order');
|
||||||
|
const body = this.env._t(
|
||||||
|
'Check the internet connection then try to sync again by clicking on the red wifi button (upper right of the screen).'
|
||||||
|
);
|
||||||
|
await this.showPopup('OfflineErrorPopup', { title, body });
|
||||||
|
} else if (error.code === 200) {
|
||||||
|
// OpenERP Server Errors
|
||||||
|
await this.showPopup('ErrorTracebackPopup', {
|
||||||
|
title: error.data.message || this.env._t('Server Error'),
|
||||||
|
body:
|
||||||
|
error.data.debug ||
|
||||||
|
this.env._t('The server encountered an error while receiving your order.'),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// ???
|
||||||
|
await this.showPopup('ErrorPopup', {
|
||||||
|
title: this.env._t('Unknown Error'),
|
||||||
|
body: this.env._t(
|
||||||
|
'The order could not be sent to the server due to an unknown error'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function useAutoFocusToLast() {
|
||||||
|
const current = Component.current;
|
||||||
|
let target = null;
|
||||||
|
function autofocus() {
|
||||||
|
const prevTarget = target;
|
||||||
|
const allInputs = current.el.querySelectorAll('input');
|
||||||
|
target = allInputs[allInputs.length - 1];
|
||||||
|
if (target && target !== prevTarget) {
|
||||||
|
target.focus();
|
||||||
|
target.selectionStart = target.selectionEnd = target.value.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onMounted(autofocus);
|
||||||
|
onPatched(autofocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this hook when you want to do something on previously selected and
|
||||||
|
* newly selected order when the order changes.
|
||||||
|
*
|
||||||
|
* Normally, a component is rendered then the current order is changed. When
|
||||||
|
* this happens, we want to rerender the component because the new information
|
||||||
|
* should be reflected in the screen. Additionally, we might want to remove listeners
|
||||||
|
* to the previous order and attach listeners to the new one. This hook is
|
||||||
|
* perfect for the described situation.
|
||||||
|
*
|
||||||
|
* Internally, this hook performs the following:
|
||||||
|
* 1. call newOrderCB on mounted
|
||||||
|
* 2. listen to order changes and perform the following sequence:
|
||||||
|
* - call prevOrderCB(prevOrder)
|
||||||
|
* - call newOrderCB(newOrder)
|
||||||
|
* 3. call prevOrderCB on willUnmount
|
||||||
|
*
|
||||||
|
* @param {Function} prevOrderCB apply this callback on the previous order
|
||||||
|
* @param {Function} newOrderCB apply this callback on the new order
|
||||||
|
*/
|
||||||
|
function onChangeOrder(prevOrderCB, newOrderCB) {
|
||||||
|
const current = Component.current;
|
||||||
|
prevOrderCB = prevOrderCB ? prevOrderCB.bind(current) : () => {};
|
||||||
|
newOrderCB = newOrderCB ? newOrderCB.bind(current) : () => {};
|
||||||
|
onMounted(() => {
|
||||||
|
current.env.pos.on(
|
||||||
|
'change:selectedOrder',
|
||||||
|
async (pos, newOrder) => {
|
||||||
|
await prevOrderCB(pos.previous('selectedOrder'));
|
||||||
|
await newOrderCB(newOrder);
|
||||||
|
},
|
||||||
|
current
|
||||||
|
);
|
||||||
|
newOrderCB(current.env.pos.get_order());
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
current.env.pos.off('change:selectedOrder', null, current);
|
||||||
|
prevOrderCB(current.env.pos.get_order());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function useBarcodeReader(callbackMap, exclusive = false) {
|
||||||
|
const current = Component.current;
|
||||||
|
const barcodeReader = current.env.pos.barcode_reader;
|
||||||
|
for (let [key, callback] of Object.entries(callbackMap)) {
|
||||||
|
callbackMap[key] = callback.bind(current);
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
if (barcodeReader) {
|
||||||
|
for (let key in callbackMap) {
|
||||||
|
if (exclusive) {
|
||||||
|
barcodeReader.set_exclusive_callback(key, callbackMap[key]);
|
||||||
|
} else {
|
||||||
|
barcodeReader.set_action_callback(key, callbackMap[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onWillUnmount(() => {
|
||||||
|
if (barcodeReader) {
|
||||||
|
for (let key in callbackMap) {
|
||||||
|
if (exclusive) {
|
||||||
|
barcodeReader.remove_exclusive_callback(key, callbackMap[key]);
|
||||||
|
} else {
|
||||||
|
barcodeReader.remove_action_callback(key, callbackMap[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { useErrorHandlers, useAutoFocusToLast, onChangeOrder, useBarcodeReader };
|
||||||
|
});
|
||||||
@@ -180,6 +180,7 @@ var PosDB = core.Class.extend({
|
|||||||
}
|
}
|
||||||
for(var i = 0, len = products.length; i < len; i++){
|
for(var i = 0, len = products.length; i < len; i++){
|
||||||
var product = products[i];
|
var product = products[i];
|
||||||
|
if (product.id in this.product_by_id) continue;
|
||||||
var search_string = utils.unaccent(this._product_search_string(product));
|
var search_string = utils.unaccent(this._product_search_string(product));
|
||||||
var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id;
|
var categ_id = product.pos_categ_id ? product.pos_categ_id[0] : this.root_category_id;
|
||||||
product.product_tmpl_id = product.product_tmpl_id[0];
|
product.product_tmpl_id = product.product_tmpl_id[0];
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
odoo.define('point_of_sale.DebugManager.Backend', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { _t } = require('web.core');
|
||||||
|
const DebugManager = require('web.DebugManager.Backend');
|
||||||
|
|
||||||
|
DebugManager.include({
|
||||||
|
/**
|
||||||
|
* Runs the JS (desktop) tests
|
||||||
|
*/
|
||||||
|
perform_pos_js_tests() {
|
||||||
|
this.do_action({
|
||||||
|
name: _t('JS Tests'),
|
||||||
|
target: 'new',
|
||||||
|
type: 'ir.actions.act_url',
|
||||||
|
url: '/pos/web/tests?mod=*',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
odoo.define('point_of_sale.gui', function (require) {
|
|
||||||
"use strict";
|
|
||||||
// this file contains the Gui, which is the pos 'controller'.
|
|
||||||
// It contains high level methods to manipulate the interface
|
|
||||||
// such as changing between screens, creating popups, etc.
|
|
||||||
//
|
|
||||||
// it is available to all pos objects trough the '.gui' field.
|
|
||||||
|
|
||||||
var core = require('web.core');
|
|
||||||
var field_utils = require('web.field_utils');
|
|
||||||
var session = require('web.session');
|
|
||||||
|
|
||||||
var _t = core._t;
|
|
||||||
|
|
||||||
var Gui = core.Class.extend({
|
|
||||||
screen_classes: [],
|
|
||||||
popup_classes: [],
|
|
||||||
init: function(options){
|
|
||||||
var self = this;
|
|
||||||
this.pos = options.pos;
|
|
||||||
this.chrome = options.chrome;
|
|
||||||
this.screen_instances = {};
|
|
||||||
this.popup_instances = {};
|
|
||||||
this.default_screen = null;
|
|
||||||
this.startup_screen = null;
|
|
||||||
this.current_popup = null;
|
|
||||||
this.current_screen = null;
|
|
||||||
this.show_sync_errors = true;
|
|
||||||
|
|
||||||
this.chrome.ready.then(function(){
|
|
||||||
self.close_other_tabs();
|
|
||||||
self._show_first_screen();
|
|
||||||
self.pos.bind('change:selectedOrder', function(){
|
|
||||||
self.show_saved_screen(self.pos.get_order());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_show_first_screen: function() {
|
|
||||||
var order = this.pos.get_order();
|
|
||||||
if (order) {
|
|
||||||
this.show_saved_screen(order);
|
|
||||||
} else {
|
|
||||||
this.show_screen(this.startup_screen);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: SCREEN MANIPULATION ---- */
|
|
||||||
|
|
||||||
// register a screen widget to the gui,
|
|
||||||
// it must have been inserted into the dom.
|
|
||||||
add_screen: function(name, screen){
|
|
||||||
screen.hide();
|
|
||||||
this.screen_instances[name] = screen;
|
|
||||||
},
|
|
||||||
|
|
||||||
// sets the screen that will be displayed
|
|
||||||
// for new orders
|
|
||||||
set_default_screen: function(name){
|
|
||||||
this.default_screen = name;
|
|
||||||
},
|
|
||||||
|
|
||||||
// sets the screen that will be displayed
|
|
||||||
// when no orders are present
|
|
||||||
set_startup_screen: function(name) {
|
|
||||||
this.startup_screen = name;
|
|
||||||
},
|
|
||||||
|
|
||||||
// display the screen saved in an order,
|
|
||||||
// called when the user changes the current order
|
|
||||||
// no screen saved ? -> display default_screen
|
|
||||||
// no order ? -> display startup_screen
|
|
||||||
show_saved_screen: function(order,options) {
|
|
||||||
options = options || {};
|
|
||||||
this.close_popup();
|
|
||||||
if (order) {
|
|
||||||
this.show_screen(order.get_screen_data('screen') ||
|
|
||||||
options.default_screen ||
|
|
||||||
this.default_screen,
|
|
||||||
null,'refresh');
|
|
||||||
} else {
|
|
||||||
this.show_screen(this.startup_screen);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// display a screen.
|
|
||||||
// If there is an order, the screen will be saved in the order
|
|
||||||
// - params: used to load a screen with parameters, for
|
|
||||||
// example loading a 'product_details' screen for a specific product.
|
|
||||||
// - refresh: if you want the screen to cycle trough show / hide even
|
|
||||||
// if you are already on the same screen.
|
|
||||||
show_screen: function(screen_name,params,refresh,skip_close_popup) {
|
|
||||||
var screen = this.screen_instances[screen_name];
|
|
||||||
if (!screen) {
|
|
||||||
console.error("ERROR: show_screen("+screen_name+") : screen not found");
|
|
||||||
}
|
|
||||||
if (!skip_close_popup){
|
|
||||||
this.close_popup();
|
|
||||||
}
|
|
||||||
var order = this.pos.get_order();
|
|
||||||
if (order) {
|
|
||||||
var old_screen_name = order.get_screen_data('screen');
|
|
||||||
|
|
||||||
order.set_screen_data('screen',screen_name);
|
|
||||||
|
|
||||||
if(params){
|
|
||||||
order.set_screen_data('params',params);
|
|
||||||
}
|
|
||||||
|
|
||||||
if( screen_name !== old_screen_name ){
|
|
||||||
order.set_screen_data('previous-screen',old_screen_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (refresh || screen !== this.current_screen) {
|
|
||||||
if (this.current_screen) {
|
|
||||||
this.current_screen.close();
|
|
||||||
this.current_screen.hide();
|
|
||||||
}
|
|
||||||
this.current_screen = screen;
|
|
||||||
this.current_screen.show(refresh);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// returns the current screen.
|
|
||||||
get_current_screen: function() {
|
|
||||||
return this.pos.get_order() ? ( this.pos.get_order().get_screen_data('screen') || this.default_screen ) : this.startup_screen;
|
|
||||||
},
|
|
||||||
|
|
||||||
// goes to the previous screen (as specified in the order). The history only
|
|
||||||
// goes 1 deep ...
|
|
||||||
back: function() {
|
|
||||||
var previous = this.pos.get_order().get_screen_data('previous-screen');
|
|
||||||
if (previous) {
|
|
||||||
this.show_screen(previous);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// returns the parameter specified when this screen was displayed
|
|
||||||
get_current_screen_param: function(param) {
|
|
||||||
if (this.pos.get_order()) {
|
|
||||||
var params = this.pos.get_order().get_screen_data('params');
|
|
||||||
return params ? params[param] : undefined;
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: POPUP MANIPULATION ---- */
|
|
||||||
|
|
||||||
// registers a new popup in the GUI.
|
|
||||||
// the popup must have been previously inserted
|
|
||||||
// into the dom.
|
|
||||||
add_popup: function(name, popup) {
|
|
||||||
popup.hide();
|
|
||||||
this.popup_instances[name] = popup;
|
|
||||||
},
|
|
||||||
|
|
||||||
// displays a popup. Popup do not stack,
|
|
||||||
// are not remembered by the order, and are
|
|
||||||
// closed by screen changes or new popups.
|
|
||||||
show_popup: function(name,options) {
|
|
||||||
if (this.current_popup) {
|
|
||||||
this.close_popup();
|
|
||||||
}
|
|
||||||
this.current_popup = this.popup_instances[name];
|
|
||||||
return this.current_popup.show(options);
|
|
||||||
},
|
|
||||||
show_sync_error_popup: function() {
|
|
||||||
if (this.show_sync_errors) {
|
|
||||||
this.show_popup('error-sync',{
|
|
||||||
'title':_t('Changes could not be saved'),
|
|
||||||
'body': _t('You must be connected to the internet to save your changes.\n\n' +
|
|
||||||
'Orders that where not synced before will be synced next time you close an order while connected to the internet or when you close the session.'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// close the current popup.
|
|
||||||
close_popup: function() {
|
|
||||||
if (this.current_popup) {
|
|
||||||
this.current_popup.close();
|
|
||||||
this.current_popup.hide();
|
|
||||||
this.current_popup = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// is there an active popup ?
|
|
||||||
has_popup: function() {
|
|
||||||
return !!this.current_popup;
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: INTER TAB COMM ---- */
|
|
||||||
|
|
||||||
// This sets up automatic pos exit when open in
|
|
||||||
// another tab.
|
|
||||||
close_other_tabs: function() {
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// avoid closing itself
|
|
||||||
var now = Date.now();
|
|
||||||
|
|
||||||
localStorage['message'] = '';
|
|
||||||
localStorage['message'] = JSON.stringify({
|
|
||||||
'message':'close_tabs',
|
|
||||||
'config': this.pos.config.id,
|
|
||||||
'window_uid': now,
|
|
||||||
});
|
|
||||||
|
|
||||||
// storage events are (most of the time) triggered only when the
|
|
||||||
// localstorage is updated in a different tab.
|
|
||||||
// some browsers (e.g. IE) does trigger an event in the same tab
|
|
||||||
// This may be a browser bug or a different interpretation of the HTML spec
|
|
||||||
// cf https://connect.microsoft.com/IE/feedback/details/774798/localstorage-event-fired-in-source-window
|
|
||||||
// Use window_uid parameter to exclude the current window
|
|
||||||
window.addEventListener("storage", function(event) {
|
|
||||||
var msg = event.data;
|
|
||||||
|
|
||||||
if ( event.key === 'message' && event.newValue) {
|
|
||||||
|
|
||||||
var msg = JSON.parse(event.newValue);
|
|
||||||
if ( msg.message === 'close_tabs' &&
|
|
||||||
msg.config == self.pos.config.id &&
|
|
||||||
msg.window_uid != now) {
|
|
||||||
|
|
||||||
console.info('POS / Session opened in another window. EXITING POS');
|
|
||||||
self._close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}, false);
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: ACCESS CONTROL ---- */
|
|
||||||
|
|
||||||
// A Generic UI that allow to select a user from a list.
|
|
||||||
// It returns a promise that resolves with the selected user
|
|
||||||
// upon success. Several options are available :
|
|
||||||
// - security: passwords will be asked
|
|
||||||
// - only_managers: restricts the list to managers
|
|
||||||
// - current_user: password will not be asked if this
|
|
||||||
// user is selected.
|
|
||||||
// - title: The title of the employee selection list.
|
|
||||||
|
|
||||||
|
|
||||||
// checks if the current user (or the user provided) has manager
|
|
||||||
// access rights. If not, a popup is shown allowing the user to
|
|
||||||
// temporarily login as an administrator.
|
|
||||||
// This method returns a promise, that succeeds with the
|
|
||||||
// manager user when the login is successfull.
|
|
||||||
sudo: function(user){
|
|
||||||
user = user || this.pos.get_cashier();
|
|
||||||
|
|
||||||
if (user.role === 'manager') {
|
|
||||||
return Promise.resolve(user);
|
|
||||||
} else {
|
|
||||||
return this.select_employee({
|
|
||||||
security: true,
|
|
||||||
only_managers: true,
|
|
||||||
title: _t('Login as a Manager'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: CLOSING THE POINT OF SALE ---- */
|
|
||||||
|
|
||||||
close: function() {
|
|
||||||
var self = this;
|
|
||||||
var pending = this.pos.db.get_orders().length;
|
|
||||||
|
|
||||||
if (!pending) {
|
|
||||||
this._close();
|
|
||||||
} else {
|
|
||||||
var always = function () {
|
|
||||||
var pending = self.pos.db.get_orders().length;
|
|
||||||
if (!pending) {
|
|
||||||
self._close();
|
|
||||||
} else {
|
|
||||||
var reason = self.pos.get('failed') ?
|
|
||||||
_t('Some orders could not be submitted to '+
|
|
||||||
'the server due to configuration errors. '+
|
|
||||||
'You can exit the Point of Sale, but do '+
|
|
||||||
'not close the session before the issue '+
|
|
||||||
'has been resolved.') :
|
|
||||||
_t('Some orders could not be submitted to '+
|
|
||||||
'the server due to internet connection issues. '+
|
|
||||||
'You can exit the Point of Sale, but do '+
|
|
||||||
'not close the session before the issue '+
|
|
||||||
'has been resolved.');
|
|
||||||
|
|
||||||
self.show_popup('confirm', {
|
|
||||||
'title': _t('Offline Orders'),
|
|
||||||
'body': reason,
|
|
||||||
'confirm': function() {
|
|
||||||
self._close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.pos.push_order().then(always, always);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_close: function() {
|
|
||||||
var self = this;
|
|
||||||
this.chrome.loading_show();
|
|
||||||
this.chrome.loading_message(_t('Closing ...'));
|
|
||||||
|
|
||||||
this.pos.push_order().then(function(){
|
|
||||||
window.location = "/web#action=point_of_sale.action_client_pos_menu";
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: SOUND ---- */
|
|
||||||
|
|
||||||
play_sound: function(sound) {
|
|
||||||
var src = '';
|
|
||||||
if (sound === 'error') {
|
|
||||||
src = "/point_of_sale/static/src/sounds/error.wav";
|
|
||||||
} else if (sound === 'bell') {
|
|
||||||
src = "/point_of_sale/static/src/sounds/bell.wav";
|
|
||||||
} else {
|
|
||||||
console.error('Unknown sound: ',sound);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$('body').append('<audio src="'+src+'" autoplay="true"></audio>');
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: FILE I/O ---- */
|
|
||||||
|
|
||||||
// This will make the browser download 'contents' as a
|
|
||||||
// file named 'name'
|
|
||||||
// if 'contents' is not a string, it is converted into
|
|
||||||
// a JSON representation of the contents.
|
|
||||||
|
|
||||||
|
|
||||||
// TODO: remove me in master: deprecated in favor of prepare_download_link
|
|
||||||
// this method is kept for backward compatibility but is likely not going
|
|
||||||
// to work as many browsers do to not accept fake click events on links
|
|
||||||
download_file: function(contents, name) {
|
|
||||||
href_params = this.prepare_file_blob(contents,name);
|
|
||||||
var evt = document.createEvent("HTMLEvents");
|
|
||||||
evt.initEvent("click");
|
|
||||||
|
|
||||||
$("<a>",href_params).get(0).dispatchEvent(evt);
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
prepare_download_link: function(contents, filename, src, target) {
|
|
||||||
var href_params = this.prepare_file_blob(contents, filename);
|
|
||||||
|
|
||||||
$(target).parent().attr(href_params);
|
|
||||||
$(src).addClass('oe_hidden');
|
|
||||||
$(target).removeClass('oe_hidden');
|
|
||||||
|
|
||||||
// hide again after click
|
|
||||||
$(target).click(function() {
|
|
||||||
$(src).removeClass('oe_hidden');
|
|
||||||
$(this).addClass('oe_hidden');
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
prepare_file_blob: function(contents, name) {
|
|
||||||
var URL = window.URL || window.webkitURL;
|
|
||||||
|
|
||||||
if (typeof contents !== 'string') {
|
|
||||||
contents = JSON.stringify(contents,null,2);
|
|
||||||
}
|
|
||||||
|
|
||||||
var blob = new Blob([contents]);
|
|
||||||
|
|
||||||
return {download: name || 'document.txt',
|
|
||||||
href: URL.createObjectURL(blob),}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: EMAILS ---- */
|
|
||||||
|
|
||||||
// This will launch the user's email software
|
|
||||||
// with a new email with the address, subject and body
|
|
||||||
// prefilled.
|
|
||||||
|
|
||||||
send_email: function(address, subject, body) {
|
|
||||||
window.open("mailto:" + address +
|
|
||||||
"?subject=" + (subject ? window.encodeURIComponent(subject) : '') +
|
|
||||||
"&body=" + (body ? window.encodeURIComponent(body) : ''));
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Gui: KEYBOARD INPUT ---- */
|
|
||||||
|
|
||||||
// This is a helper to handle numpad keyboard input.
|
|
||||||
// - buffer: an empty or number string
|
|
||||||
// - input: '[0-9],'+','-','.','CLEAR','BACKSPACE'
|
|
||||||
// - options: 'firstinput' -> will clear buffer if
|
|
||||||
// input is '[0-9]' or '.'
|
|
||||||
// returns the new buffer containing the modifications
|
|
||||||
// (the original is not touched)
|
|
||||||
numpad_input: function(buffer, input, options) {
|
|
||||||
var newbuf = buffer.slice(0);
|
|
||||||
options = options || {};
|
|
||||||
var newbuf_float = newbuf === '-' ? newbuf : field_utils.parse.float(newbuf);
|
|
||||||
var decimal_point = _t.database.parameters.decimal_point;
|
|
||||||
|
|
||||||
if (input === decimal_point) {
|
|
||||||
if (options.firstinput) {
|
|
||||||
newbuf = "0.";
|
|
||||||
}else if (!newbuf.length || newbuf === '-') {
|
|
||||||
newbuf += "0.";
|
|
||||||
} else if (newbuf.indexOf(decimal_point) < 0){
|
|
||||||
newbuf = newbuf + decimal_point;
|
|
||||||
}
|
|
||||||
} else if (input === 'CLEAR') {
|
|
||||||
newbuf = "";
|
|
||||||
} else if (input === 'BACKSPACE') {
|
|
||||||
newbuf = newbuf.substring(0,newbuf.length - 1);
|
|
||||||
} else if (input === '+') {
|
|
||||||
if ( newbuf[0] === '-' ) {
|
|
||||||
newbuf = newbuf.substring(1,newbuf.length);
|
|
||||||
}
|
|
||||||
} else if (input === '-') {
|
|
||||||
if (options.firstinput) {
|
|
||||||
newbuf = '-0';
|
|
||||||
} else if ( newbuf[0] === '-' ) {
|
|
||||||
newbuf = newbuf.substring(1,newbuf.length);
|
|
||||||
} else {
|
|
||||||
newbuf = '-' + newbuf;
|
|
||||||
}
|
|
||||||
} else if (input[0] === '+' && !isNaN(parseFloat(input))) {
|
|
||||||
newbuf = this.chrome.format_currency_no_symbol(newbuf_float + parseFloat(input));
|
|
||||||
} else if (!isNaN(parseInt(input))) {
|
|
||||||
if (options.firstinput) {
|
|
||||||
newbuf = '' + input;
|
|
||||||
} else {
|
|
||||||
newbuf += input;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (newbuf === "-") {
|
|
||||||
newbuf = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// End of input buffer at 12 characters.
|
|
||||||
if (newbuf.length > buffer.length && newbuf.length > 12) {
|
|
||||||
this.play_sound('bell');
|
|
||||||
return buffer.slice(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newbuf;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var define_screen = function (classe) {
|
|
||||||
Gui.prototype.screen_classes.push(classe);
|
|
||||||
};
|
|
||||||
|
|
||||||
var define_popup = function (classe) {
|
|
||||||
Gui.prototype.popup_classes.push(classe);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
Gui: Gui,
|
|
||||||
define_screen: define_screen,
|
|
||||||
define_popup: define_popup,
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,29 @@
|
|||||||
|
odoo.define('web.web_client', function (require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
odoo.define('point_of_sale.main', function (require) {
|
const env = require('web.env');
|
||||||
"use strict";
|
const WebClient = require('web.AbstractWebClient');
|
||||||
|
const Chrome = require('point_of_sale.Chrome');
|
||||||
|
const Registries = require('point_of_sale.Registries');
|
||||||
|
const { configureGui } = require('point_of_sale.Gui');
|
||||||
|
|
||||||
var chrome = require('point_of_sale.chrome');
|
owl.config.mode = env.isDebug() ? 'dev' : 'prod';
|
||||||
var core = require('web.core');
|
owl.Component.env = env;
|
||||||
|
|
||||||
core.action_registry.add('pos.ui', chrome.Chrome);
|
async function startPosApp(webClient) {
|
||||||
|
Registries.Component.freeze();
|
||||||
|
await env.session.is_bound;
|
||||||
|
env.qweb.addTemplates(env.session.owlTemplates);
|
||||||
|
await owl.utils.whenReady();
|
||||||
|
await webClient.setElement(document.body);
|
||||||
|
await webClient.start();
|
||||||
|
const chrome = new (Registries.Component.get(Chrome))(null, { webClient });
|
||||||
|
await chrome.mount(document.querySelector('.o_action_manager'));
|
||||||
|
await chrome.start();
|
||||||
|
configureGui({ component: chrome });
|
||||||
|
}
|
||||||
|
|
||||||
|
const webClient = new WebClient();
|
||||||
|
startPosApp(webClient);
|
||||||
|
return webClient;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
odoo.define('point_of_sale.models', function (require) {
|
odoo.define('point_of_sale.models', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var ajax = require('web.ajax');
|
|
||||||
var BarcodeParser = require('barcodes.BarcodeParser');
|
var BarcodeParser = require('barcodes.BarcodeParser');
|
||||||
var BarcodeReader = require('point_of_sale.BarcodeReader');
|
var BarcodeReader = require('point_of_sale.BarcodeReader');
|
||||||
var PosDB = require('point_of_sale.DB');
|
var PosDB = require('point_of_sale.DB');
|
||||||
@@ -10,8 +9,6 @@ var concurrency = require('web.concurrency');
|
|||||||
var config = require('web.config');
|
var config = require('web.config');
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var field_utils = require('web.field_utils');
|
var field_utils = require('web.field_utils');
|
||||||
var rpc = require('web.rpc');
|
|
||||||
var session = require('web.session');
|
|
||||||
var time = require('web.time');
|
var time = require('web.time');
|
||||||
var utils = require('web.utils');
|
var utils = require('web.utils');
|
||||||
|
|
||||||
@@ -34,12 +31,17 @@ var exports = {};
|
|||||||
// 'pos' and is available to all widgets extending PosWidget.
|
// 'pos' and is available to all widgets extending PosWidget.
|
||||||
|
|
||||||
exports.PosModel = Backbone.Model.extend({
|
exports.PosModel = Backbone.Model.extend({
|
||||||
initialize: function(session, attributes) {
|
initialize: function(attributes) {
|
||||||
Backbone.Model.prototype.initialize.call(this, attributes);
|
Backbone.Model.prototype.initialize.call(this, attributes);
|
||||||
var self = this;
|
var self = this;
|
||||||
this.flush_mutex = new Mutex(); // used to make sure the orders are sent to the server once at time
|
this.flush_mutex = new Mutex(); // used to make sure the orders are sent to the server once at time
|
||||||
this.chrome = attributes.chrome;
|
|
||||||
this.gui = attributes.gui;
|
this.rpc = this.get('rpc');
|
||||||
|
this.session = this.get('session');
|
||||||
|
this.do_action = this.get('do_action');
|
||||||
|
this.setLoadingMessage = this.get('setLoadingMessage');
|
||||||
|
this.setLoadingProgress = this.get('setLoadingProgress');
|
||||||
|
this.showLoadingSkip = this.get('showLoadingSkip');
|
||||||
|
|
||||||
this.proxy = new devices.ProxyDevice(this); // used to communicate to the hardware devices via a local proxy
|
this.proxy = new devices.ProxyDevice(this); // used to communicate to the hardware devices via a local proxy
|
||||||
this.barcode_reader = new BarcodeReader({'pos': this, proxy:this.proxy});
|
this.barcode_reader = new BarcodeReader({'pos': this, proxy:this.proxy});
|
||||||
@@ -73,14 +75,15 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
|
|
||||||
// these dynamic attributes can be watched for change by other models or widgets
|
// these dynamic attributes can be watched for change by other models or widgets
|
||||||
this.set({
|
this.set({
|
||||||
'synch': { state:'connected', pending:0 },
|
'synch': { status:'connected', pending:0 },
|
||||||
'orders': new OrderCollection(),
|
'orders': new OrderCollection(),
|
||||||
'selectedOrder': null,
|
'selectedOrder': null,
|
||||||
'selectedClient': null,
|
'selectedClient': null,
|
||||||
'cashier': null,
|
'cashier': null,
|
||||||
|
'selectedCategoryId': null,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.get('orders').bind('remove', function(order,_unused_,options){
|
this.get('orders').on('remove', function(order,_unused_,options){
|
||||||
self.on_removed_order(order,options.index,options.reason);
|
self.on_removed_order(order,options.index,options.reason);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,8 +92,8 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
var order = self.get_order();
|
var order = self.get_order();
|
||||||
this.set('selectedClient', order ? order.get_client() : null );
|
this.set('selectedClient', order ? order.get_client() : null );
|
||||||
}
|
}
|
||||||
this.get('orders').bind('add remove change', update_client, this);
|
this.get('orders').on('add remove change', update_client, this);
|
||||||
this.bind('change:selectedOrder', update_client, this);
|
this.on('change:selectedOrder', update_client, this);
|
||||||
|
|
||||||
// We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
|
// We fetch the backend data on the server asynchronously. this is done only when the pos user interface is launched,
|
||||||
// Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
|
// Any change on this data made on the server is thus not reflected on the point of sale until it is relaunched.
|
||||||
@@ -123,14 +126,14 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
self.barcode_reader.disconnect_from_proxy();
|
self.barcode_reader.disconnect_from_proxy();
|
||||||
self.chrome.loading_message(_t('Connecting to the IoT Box'), 0);
|
self.setLoadingMessage(_t('Connecting to the IoT Box'), 0);
|
||||||
self.chrome.loading_skip(function () {
|
self.showLoadingSkip(function () {
|
||||||
self.proxy.stop_searching();
|
self.proxy.stop_searching();
|
||||||
});
|
});
|
||||||
self.proxy.autoconnect({
|
self.proxy.autoconnect({
|
||||||
force_ip: self.config.proxy_ip || undefined,
|
force_ip: self.config.proxy_ip || undefined,
|
||||||
progress: function(prog){
|
progress: function(prog){
|
||||||
self.chrome.loading_progress(prog);
|
self.setLoadingProgress(prog);
|
||||||
},
|
},
|
||||||
}).then(
|
}).then(
|
||||||
function () {
|
function () {
|
||||||
@@ -140,16 +143,20 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
function (statusText, url) {
|
function (statusText, url) {
|
||||||
var show_loading_error = (self.gui.current_screen === null);
|
// this should reject so that it can be captured when we wait for pos.ready
|
||||||
resolve();
|
// in the chrome component.
|
||||||
if (show_loading_error && statusText == 'error' && window.location.protocol == 'https:') {
|
// then, if it got really rejected, we can show the error.
|
||||||
self.gui.show_popup('alert', {
|
if (statusText == 'error' && window.location.protocol == 'https:') {
|
||||||
|
reject({
|
||||||
title: _t('HTTPS connection to IoT Box failed'),
|
title: _t('HTTPS connection to IoT Box failed'),
|
||||||
body: _.str.sprintf(
|
body: _.str.sprintf(
|
||||||
_t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'),
|
_t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'),
|
||||||
url
|
url
|
||||||
),
|
),
|
||||||
|
popup: 'alert',
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -166,7 +173,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
{
|
{
|
||||||
label: 'version',
|
label: 'version',
|
||||||
loaded: function (self) {
|
loaded: function (self) {
|
||||||
return session.rpc('/web/webclient/version_info',{}).then(function (version) {
|
return self.session.rpc('/web/webclient/version_info',{}).then(function (version) {
|
||||||
self.version = version;
|
self.version = version;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -174,7 +181,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
},{
|
},{
|
||||||
model: 'res.company',
|
model: 'res.company',
|
||||||
fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id', 'state_id', 'tax_calculation_rounding_method'],
|
fields: [ 'currency_id', 'email', 'website', 'company_registry', 'vat', 'name', 'phone', 'partner_id' , 'country_id', 'state_id', 'tax_calculation_rounding_method'],
|
||||||
ids: function(self){ return [session.user_context.allowed_company_ids[0]]; },
|
ids: function(self){ return [self.session.user_context.allowed_company_ids[0]]; },
|
||||||
loaded: function(self,companies){ self.company = companies[0]; },
|
loaded: function(self,companies){ self.company = companies[0]; },
|
||||||
},{
|
},{
|
||||||
model: 'decimal.precision',
|
model: 'decimal.precision',
|
||||||
@@ -247,7 +254,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
});
|
});
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var tax_ids = _.pluck(self.taxes, 'id');
|
var tax_ids = _.pluck(self.taxes, 'id');
|
||||||
rpc.query({
|
self.rpc({
|
||||||
model: 'account.tax',
|
model: 'account.tax',
|
||||||
method: 'get_real_tax_amount',
|
method: 'get_real_tax_amount',
|
||||||
args: [tax_ids],
|
args: [tax_ids],
|
||||||
@@ -311,7 +318,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (user.id === session.uid) {
|
if (user.id === self.session.uid) {
|
||||||
self.user = user;
|
self.user = user;
|
||||||
self.employee.name = user.name;
|
self.employee.name = user.name;
|
||||||
self.employee.role = user.role;
|
self.employee.role = user.role;
|
||||||
@@ -382,7 +389,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
},
|
},
|
||||||
},{
|
},{
|
||||||
model: 'pos.category',
|
model: 'pos.category',
|
||||||
fields: ['id', 'name', 'parent_id', 'child_id'],
|
fields: ['id', 'name', 'parent_id', 'child_id', 'write_date'],
|
||||||
domain: function(self) {
|
domain: function(self) {
|
||||||
return self.config.limit_categories && self.config.iface_available_categ_ids.length ? [['id', 'in', self.config.iface_available_categ_ids]] : [];
|
return self.config.limit_categories && self.config.iface_available_categ_ids.length ? [['id', 'in', self.config.iface_available_categ_ids]] : [];
|
||||||
},
|
},
|
||||||
@@ -393,7 +400,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
model: 'product.product',
|
model: 'product.product',
|
||||||
fields: ['display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id',
|
fields: ['display_name', 'lst_price', 'standard_price', 'categ_id', 'pos_categ_id', 'taxes_id',
|
||||||
'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description',
|
'barcode', 'default_code', 'to_weight', 'uom_id', 'description_sale', 'description',
|
||||||
'product_tmpl_id','tracking'],
|
'product_tmpl_id','tracking', 'write_date'],
|
||||||
order: _.map(['sequence','default_code','name'], function (name) { return {name: name}; }),
|
order: _.map(['sequence','default_code','name'], function (name) { return {name: name}; }),
|
||||||
domain: function(self){
|
domain: function(self){
|
||||||
var domain = ['&', '&', ['sale_ok','=',true],['available_in_pos','=',true],'|',['company_id','=',self.config.company_id[0]],['company_id','=',false]];
|
var domain = ['&', '&', ['sale_ok','=',true],['available_in_pos','=',true],'|',['company_id','=',self.config.company_id[0]],['company_id','=',false]];
|
||||||
@@ -416,6 +423,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
product.lst_price = round_pr(product.lst_price * conversion_rate, self.currency.rounding);
|
product.lst_price = round_pr(product.lst_price * conversion_rate, self.currency.rounding);
|
||||||
}
|
}
|
||||||
product.categ = _.findWhere(self.product_categories, {'id': product.categ_id[0]});
|
product.categ = _.findWhere(self.product_categories, {'id': product.categ_id[0]});
|
||||||
|
product.pos = self;
|
||||||
return new exports.Product({}, product);
|
return new exports.Product({}, product);
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@@ -533,7 +541,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
reject();
|
reject();
|
||||||
};
|
};
|
||||||
self.company_logo.crossOrigin = "anonymous";
|
self.company_logo.crossOrigin = "anonymous";
|
||||||
self.company_logo.src = '/web/binary/company_logo' + '?dbname=' + session.db + '&_' + Math.random();
|
self.company_logo.src = '/web/binary/company_logo' + '?dbname=' + self.session.db + '&_' + Math.random();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
@@ -559,7 +567,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
var model = self.models[index];
|
var model = self.models[index];
|
||||||
self.chrome.loading_message(_t('Loading')+' '+(model.label || model.model || ''), progress);
|
self.setLoadingMessage(_t('Loading')+' '+(model.label || model.model || ''), progress);
|
||||||
|
|
||||||
var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true;
|
var cond = typeof model.condition === 'function' ? model.condition(self,tmp) : true;
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
@@ -577,7 +585,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
if( model.model ){
|
if( model.model ){
|
||||||
var params = {
|
var params = {
|
||||||
model: model.model,
|
model: model.model,
|
||||||
context: _.extend(context, session.user_context || {}),
|
context: _.extend(context, self.session.user_context || {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (model.ids) {
|
if (model.ids) {
|
||||||
@@ -590,7 +598,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
params.orderBy = order;
|
params.orderBy = order;
|
||||||
}
|
}
|
||||||
|
|
||||||
rpc.query(params).then(function (result) {
|
self.rpc(params).then(function (result) {
|
||||||
try { // catching exceptions in model.loaded(...)
|
try { // catching exceptions in model.loaded(...)
|
||||||
Promise.resolve(model.loaded(self, result, tmp))
|
Promise.resolve(model.loaded(self, result, tmp))
|
||||||
.then(function () { load_model(index + 1); },
|
.then(function () { load_model(index + 1); },
|
||||||
@@ -637,7 +645,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var fields = _.find(self.models, function(model){ return model.label === 'load_partners'; }).fields;
|
var fields = _.find(self.models, function(model){ return model.label === 'load_partners'; }).fields;
|
||||||
var domain = self.prepare_new_partners_domain();
|
var domain = self.prepare_new_partners_domain();
|
||||||
rpc.query({
|
self.rpc({
|
||||||
model: 'res.partner',
|
model: 'res.partner',
|
||||||
method: 'search_read',
|
method: 'search_read',
|
||||||
args: [domain, fields],
|
args: [domain, fields],
|
||||||
@@ -649,7 +657,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
if (self.db.add_partners(partners)) { // check if the partners we got were real updates
|
if (self.db.add_partners(partners)) { // check if the partners we got were real updates
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject();
|
reject('Failed in updating partners.');
|
||||||
}
|
}
|
||||||
}, function (type, err) { reject(); });
|
}, function (type, err) { reject(); });
|
||||||
});
|
});
|
||||||
@@ -819,7 +827,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
if (order) {
|
if (order) {
|
||||||
order.get_orderlines().forEach(function (orderline) {
|
order.get_orderlines().forEach(function (orderline) {
|
||||||
var product = orderline.product;
|
var product = orderline.product;
|
||||||
var image_url = window.location.origin + '/web/image?model=product.product&field=image_128&id=' + product.id;
|
var image_url = `/web/image?model=product.product&field=image_128&id=${product.id}&write_date=${product.write_date}&unique=1`;
|
||||||
|
|
||||||
// only download and convert image if we haven't done it before
|
// only download and convert image if we haven't done it before
|
||||||
if (! product.image_base64) {
|
if (! product.image_base64) {
|
||||||
@@ -832,18 +840,18 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
return Promise.all(get_image_promises).then(function () {
|
return Promise.all(get_image_promises).then(function () {
|
||||||
var rendered_order_lines = "";
|
var rendered_order_lines = "";
|
||||||
var rendered_payment_lines = "";
|
var rendered_payment_lines = "";
|
||||||
var order_total_with_tax = self.chrome.format_currency(0);
|
var order_total_with_tax = self.format_currency(0);
|
||||||
|
|
||||||
if (order) {
|
if (order) {
|
||||||
rendered_order_lines = QWeb.render('CustomerFacingDisplayOrderLines', {
|
rendered_order_lines = QWeb.render('CustomerFacingDisplayOrderLines', {
|
||||||
'orderlines': order.get_orderlines(),
|
'orderlines': order.get_orderlines(),
|
||||||
'widget': self.chrome,
|
'pos': self,
|
||||||
});
|
});
|
||||||
rendered_payment_lines = QWeb.render('CustomerFacingDisplayPaymentLines', {
|
rendered_payment_lines = QWeb.render('CustomerFacingDisplayPaymentLines', {
|
||||||
'order': order,
|
'order': order,
|
||||||
'widget': self.chrome,
|
'pos': self,
|
||||||
});
|
});
|
||||||
order_total_with_tax = self.chrome.format_currency(order.get_total_with_tax());
|
order_total_with_tax = self.format_currency(order.get_total_with_tax());
|
||||||
}
|
}
|
||||||
|
|
||||||
var $rendered_html = $(rendered_html);
|
var $rendered_html = $(rendered_html);
|
||||||
@@ -870,7 +878,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
|
|
||||||
// saves the order locally and try to send it to the backend.
|
// saves the order locally and try to send it to the backend.
|
||||||
// it returns a promise that succeeds after having tried to send the order and all the other pending orders.
|
// it returns a promise that succeeds after having tried to send the order and all the other pending orders.
|
||||||
push_order: function (order, opts) {
|
push_orders: function (order, opts) {
|
||||||
opts = opts || {};
|
opts = opts || {};
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -889,6 +897,22 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
push_single_order: function (order, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
const self = this;
|
||||||
|
const order_id = self.db.add_order(order.export_as_JSON());
|
||||||
|
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
self.flush_mutex.exec(function () {
|
||||||
|
var flushed = self._flush_orders([self.db.get_order(order_id)], opts);
|
||||||
|
|
||||||
|
flushed.then(resolve, reject);
|
||||||
|
|
||||||
|
return flushed;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
// saves the order locally and try to send it to the backend and make an invoice
|
// saves the order locally and try to send it to the backend and make an invoice
|
||||||
// returns a promise that succeeds when the order has been posted and successfully generated
|
// returns a promise that succeeds when the order has been posted and successfully generated
|
||||||
// an invoice. This method can fail in various ways:
|
// an invoice. This method can fail in various ways:
|
||||||
@@ -926,7 +950,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
transfer.then(function(order_server_id){
|
transfer.then(function(order_server_id){
|
||||||
// generate the pdf and download it
|
// generate the pdf and download it
|
||||||
if (order_server_id.length && !order.is_to_email()) {
|
if (order_server_id.length && !order.is_to_email()) {
|
||||||
self.chrome.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
self.do_action('point_of_sale.pos_invoice_report',{additional_context:{
|
||||||
active_ids:order_server_id,
|
active_ids:order_server_id,
|
||||||
}}).then(function () {
|
}}).then(function () {
|
||||||
resolveInvoiced(order_server_id);
|
resolveInvoiced(order_server_id);
|
||||||
@@ -956,6 +980,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// wrapper around the _save_to_server that updates the synch status widget
|
// wrapper around the _save_to_server that updates the synch status widget
|
||||||
|
// Resolves to the backend ids of the synced orders.
|
||||||
_flush_orders: function(orders, options) {
|
_flush_orders: function(orders, options) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.set_synch('connecting', orders.length);
|
this.set_synch('connecting', orders.length);
|
||||||
@@ -969,12 +994,12 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
set_synch: function(state, pending) {
|
set_synch: function(status, pending) {
|
||||||
if (['connected', 'connecting', 'error', 'disconnected'].indexOf(state) === -1) {
|
if (['connected', 'connecting', 'error', 'disconnected'].indexOf(status) === -1) {
|
||||||
console.error(state, ' is not a known connection state.');
|
console.error(status, ' is not a known connection state.');
|
||||||
}
|
}
|
||||||
pending = pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length;
|
pending = pending || this.db.get_orders().length + this.db.get_ids_to_remove_from_server().length;
|
||||||
this.set('synch', { state: state, pending: pending });
|
this.set('synch', { status, pending });
|
||||||
},
|
},
|
||||||
|
|
||||||
// send an array of orders to the server
|
// send an array of orders to the server
|
||||||
@@ -1004,11 +1029,11 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
return order;
|
return order;
|
||||||
})];
|
})];
|
||||||
args.push(options.draft || false);
|
args.push(options.draft || false);
|
||||||
return rpc.query({
|
return this.rpc({
|
||||||
model: 'pos.order',
|
model: 'pos.order',
|
||||||
method: 'create_from_ui',
|
method: 'create_from_ui',
|
||||||
args: args,
|
args: args,
|
||||||
kwargs: {context: session.user_context},
|
kwargs: {context: this.session.user_context},
|
||||||
}, {
|
}, {
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
shadow: !options.to_invoice
|
shadow: !options.to_invoice
|
||||||
@@ -1021,18 +1046,14 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
return server_ids;
|
return server_ids;
|
||||||
}).catch(function (reason){
|
}).catch(function (reason){
|
||||||
var error = reason.message;
|
var error = reason.message;
|
||||||
|
console.warn('Failed to send orders:', orders);
|
||||||
if(error.code === 200 ){ // Business Logic Error, not a connection problem
|
if(error.code === 200 ){ // Business Logic Error, not a connection problem
|
||||||
// Hide error if already shown before ...
|
// Hide error if already shown before ...
|
||||||
if ((!self.get('failed') || options.show_error) && !options.to_invoice) {
|
if ((!self.get('failed') || options.show_error) && !options.to_invoice) {
|
||||||
self.gui.show_popup('error-traceback',{
|
self.set('failed',error);
|
||||||
'title': error.data.message,
|
throw error;
|
||||||
'body': error.data.debug
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
self.set('failed',error);
|
|
||||||
}
|
}
|
||||||
console.warn('Failed to send orders:', orders);
|
|
||||||
self.gui.show_sync_error_popup();
|
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -1053,11 +1074,11 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * server_ids.length;
|
var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * server_ids.length;
|
||||||
|
|
||||||
return rpc.query({
|
return this.rpc({
|
||||||
model: 'pos.order',
|
model: 'pos.order',
|
||||||
method: 'remove_from_ui',
|
method: 'remove_from_ui',
|
||||||
args: [server_ids],
|
args: [server_ids],
|
||||||
kwargs: {context: session.user_context},
|
kwargs: {context: this.session.user_context},
|
||||||
}, {
|
}, {
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
shadow: true,
|
shadow: true,
|
||||||
@@ -1066,8 +1087,17 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
self.db.set_ids_removed_from_server(server_ids);
|
self.db.set_ids_removed_from_server(server_ids);
|
||||||
return server_ids;
|
return server_ids;
|
||||||
}).catch(function (reason){
|
}).catch(function (reason){
|
||||||
self.gui.show_sync_error_popup();
|
var error = reason.message;
|
||||||
console.error('Failed to remove orders:', server_ids);
|
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:', server_ids);
|
||||||
|
throw error;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1137,7 +1167,7 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
this.db.add_order(json.paid_orders[i].data);
|
this.db.add_order(json.paid_orders[i].data);
|
||||||
}
|
}
|
||||||
report.paid = json.paid_orders.length;
|
report.paid = json.paid_orders.length;
|
||||||
this.push_order();
|
this.push_orders();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (json.unpaid_orders) {
|
if (json.unpaid_orders) {
|
||||||
@@ -1212,6 +1242,62 @@ exports.PosModel = Backbone.Model.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
electronic_payment_interfaces: {},
|
electronic_payment_interfaces: {},
|
||||||
|
|
||||||
|
format_currency: function(amount, precision) {
|
||||||
|
var currency =
|
||||||
|
this && this.currency
|
||||||
|
? this.currency
|
||||||
|
: { symbol: '$', position: 'after', rounding: 0.01, decimals: 2 };
|
||||||
|
|
||||||
|
amount = this.format_currency_no_symbol(amount, precision, currency);
|
||||||
|
|
||||||
|
if (currency.position === 'after') {
|
||||||
|
return amount + ' ' + (currency.symbol || '');
|
||||||
|
} else {
|
||||||
|
return (currency.symbol || '') + ' ' + amount;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
format_currency_no_symbol: function(amount, precision, currency) {
|
||||||
|
if (!currency) {
|
||||||
|
currency =
|
||||||
|
this && this.currency
|
||||||
|
? this.currency
|
||||||
|
: { symbol: '$', position: 'after', rounding: 0.01, decimals: 2 };
|
||||||
|
}
|
||||||
|
var decimals = currency.decimals;
|
||||||
|
|
||||||
|
if (precision && this.dp[precision] !== undefined) {
|
||||||
|
decimals = this.dp[precision];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof amount === 'number') {
|
||||||
|
amount = round_di(amount, decimals).toFixed(decimals);
|
||||||
|
amount = field_utils.format.float(round_di(amount, decimals), {
|
||||||
|
digits: [69, decimals],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount;
|
||||||
|
},
|
||||||
|
|
||||||
|
format_pr: function(value, precision) {
|
||||||
|
var decimals =
|
||||||
|
precision > 0
|
||||||
|
? Math.max(0, Math.ceil(Math.log(1.0 / precision) / Math.log(10)))
|
||||||
|
: 0;
|
||||||
|
return value.toFixed(decimals);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (value = 1.0000, decimals = 2) => '1'
|
||||||
|
* (value = 1.1234, decimals = 2) => '1.12'
|
||||||
|
* @param {number} value amount to format
|
||||||
|
*/
|
||||||
|
formatFixed: function(value) {
|
||||||
|
const currency = this.currency || { decimals: 2 };
|
||||||
|
return `${Number(value.toFixed(currency.decimals || 0))}`;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1324,7 +1410,21 @@ exports.Product = Backbone.Model.extend({
|
|||||||
initialize: function(attr, options){
|
initialize: function(attr, options){
|
||||||
_.extend(this, options);
|
_.extend(this, options);
|
||||||
},
|
},
|
||||||
|
isAllowOnlyOneLot: function() {
|
||||||
|
const productUnit = this.get_unit();
|
||||||
|
return this.tracking === 'lot' || !productUnit || !productUnit.is_pos_groupable;
|
||||||
|
},
|
||||||
|
get_unit: function() {
|
||||||
|
var unit_id = this.uom_id;
|
||||||
|
if(!unit_id){
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
unit_id = unit_id[0];
|
||||||
|
if(!this.pos){
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.pos.units_by_id[unit_id];
|
||||||
|
},
|
||||||
// Port of get_product_price on product.pricelist.
|
// Port of get_product_price on product.pricelist.
|
||||||
//
|
//
|
||||||
// Anything related to UOM can be ignored, the POS will always use
|
// Anything related to UOM can be ignored, the POS will always use
|
||||||
@@ -1474,6 +1574,57 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
orderline.price_manually_set = this.price_manually_set;
|
orderline.price_manually_set = this.price_manually_set;
|
||||||
return orderline;
|
return orderline;
|
||||||
},
|
},
|
||||||
|
getPackLotLinesToEdit: function(isAllowOnlyOneLot) {
|
||||||
|
const currentPackLotLines = this.pack_lot_lines.models;
|
||||||
|
let nExtraLines = Math.abs(this.quantity) - currentPackLotLines.length;
|
||||||
|
nExtraLines = nExtraLines > 0 ? nExtraLines : 1;
|
||||||
|
const tempLines = currentPackLotLines
|
||||||
|
.map(lotLine => ({
|
||||||
|
id: lotLine.cid,
|
||||||
|
text: lotLine.get('lot_name'),
|
||||||
|
}))
|
||||||
|
.concat(
|
||||||
|
Array.from(Array(nExtraLines)).map(_ => ({
|
||||||
|
text: '',
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return isAllowOnlyOneLot ? [tempLines[0]] : tempLines;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @param { modifiedPackLotLines, newPackLotLines }
|
||||||
|
* @param {Object} modifiedPackLotLines key-value pair of String (the cid) & String (the new lot_name)
|
||||||
|
* @param {Array} newPackLotLines array of { lot_name: String }
|
||||||
|
*/
|
||||||
|
setPackLotLines: function({ modifiedPackLotLines, newPackLotLines }) {
|
||||||
|
// Set the new values for modified lot lines.
|
||||||
|
let lotLinesToRemove = [];
|
||||||
|
for (let lotLine of this.pack_lot_lines.models) {
|
||||||
|
const modifiedLotName = modifiedPackLotLines[lotLine.cid];
|
||||||
|
if (modifiedLotName) {
|
||||||
|
lotLine.set({ lot_name: modifiedLotName });
|
||||||
|
} else {
|
||||||
|
// We should not call lotLine.remove() here because
|
||||||
|
// we don't want to mutate the array while looping thru it.
|
||||||
|
lotLinesToRemove.push(lotLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove those that needed to be removed.
|
||||||
|
for (let lotLine of lotLinesToRemove) {
|
||||||
|
lotLine.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new pack lot lines.
|
||||||
|
let newPackLotLine;
|
||||||
|
for (let newLotLine of newPackLotLines) {
|
||||||
|
newPackLotLine = new exports.Packlotline({}, { order_line: this });
|
||||||
|
newPackLotLine.set({ lot_name: newLotLine.lot_name });
|
||||||
|
this.pack_lot_lines.add(newPackLotLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the quantity of the line based on number of pack lots.
|
||||||
|
this.pack_lot_lines.set_quantity_by_lot();
|
||||||
|
},
|
||||||
set_product_lot: function(product){
|
set_product_lot: function(product){
|
||||||
this.has_product_lot = product.tracking !== 'none';
|
this.has_product_lot = product.tracking !== 'none';
|
||||||
this.pack_lot_lines = this.has_product_lot && new PacklotlineCollection(null, {'order_line': this});
|
this.pack_lot_lines = this.has_product_lot && new PacklotlineCollection(null, {'order_line': this});
|
||||||
@@ -1552,24 +1703,6 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
return lots_required;
|
return lots_required;
|
||||||
},
|
},
|
||||||
|
|
||||||
compute_lot_lines: function(){
|
|
||||||
var pack_lot_lines = this.pack_lot_lines;
|
|
||||||
var lines = pack_lot_lines.length;
|
|
||||||
var lots_required = this.get_required_number_of_lots();
|
|
||||||
|
|
||||||
if(lots_required > lines){
|
|
||||||
for(var i=0; i<lots_required - lines; i++){
|
|
||||||
pack_lot_lines.add(new exports.Packlotline({}, {'order_line': this}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(lots_required < lines){
|
|
||||||
var to_remove = lines - lots_required;
|
|
||||||
var lot_lines = pack_lot_lines.sortBy('lot_name').slice(0, to_remove);
|
|
||||||
pack_lot_lines.remove(lot_lines);
|
|
||||||
}
|
|
||||||
return this.pack_lot_lines;
|
|
||||||
},
|
|
||||||
|
|
||||||
has_valid_product_lot: function(){
|
has_valid_product_lot: function(){
|
||||||
if(!this.has_product_lot){
|
if(!this.has_product_lot){
|
||||||
return true;
|
return true;
|
||||||
@@ -1580,15 +1713,7 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
|
|
||||||
// return the unit of measure of the product
|
// return the unit of measure of the product
|
||||||
get_unit: function(){
|
get_unit: function(){
|
||||||
var unit_id = this.product.uom_id;
|
return this.product.get_unit();
|
||||||
if(!unit_id){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
unit_id = unit_id[0];
|
|
||||||
if(!this.pos){
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return this.pos.units_by_id[unit_id];
|
|
||||||
},
|
},
|
||||||
// return the product of this orderline
|
// return the product of this orderline
|
||||||
get_product: function(){
|
get_product: function(){
|
||||||
@@ -1597,7 +1722,9 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
// selects or deselects this orderline
|
// selects or deselects this orderline
|
||||||
set_selected: function(selected){
|
set_selected: function(selected){
|
||||||
this.selected = selected;
|
this.selected = selected;
|
||||||
|
// this trigger also triggers the change event of the collection.
|
||||||
this.trigger('change',this);
|
this.trigger('change',this);
|
||||||
|
this.trigger('new-orderline-selected');
|
||||||
},
|
},
|
||||||
// returns true if this orderline is selected
|
// returns true if this orderline is selected
|
||||||
is_selected: function(){
|
is_selected: function(){
|
||||||
@@ -1648,6 +1775,7 @@ exports.Orderline = Backbone.Model.extend({
|
|||||||
//used to create a json of the ticket, to be sent to the printer
|
//used to create a json of the ticket, to be sent to the printer
|
||||||
export_for_printing: function(){
|
export_for_printing: function(){
|
||||||
return {
|
return {
|
||||||
|
id: this.id,
|
||||||
quantity: this.get_quantity(),
|
quantity: this.get_quantity(),
|
||||||
unit_name: this.get_unit().name,
|
unit_name: this.get_unit().name,
|
||||||
price: this.get_unit_display_price(),
|
price: this.get_unit_display_price(),
|
||||||
@@ -2115,14 +2243,6 @@ var PacklotlineCollection = Backbone.Collection.extend({
|
|||||||
this.order_line = options.order_line;
|
this.order_line = options.order_line;
|
||||||
},
|
},
|
||||||
|
|
||||||
get_empty_model: function(){
|
|
||||||
return this.findWhere({'lot_name': null});
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_empty_model: function(){
|
|
||||||
this.remove(this.where({'lot_name': null}));
|
|
||||||
},
|
|
||||||
|
|
||||||
get_valid_lots: function(){
|
get_valid_lots: function(){
|
||||||
return this.filter(function(model){
|
return this.filter(function(model){
|
||||||
return model.get('lot_name');
|
return model.get('lot_name');
|
||||||
@@ -2130,13 +2250,11 @@ var PacklotlineCollection = Backbone.Collection.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_quantity_by_lot: function() {
|
set_quantity_by_lot: function() {
|
||||||
if (this.order_line.product.tracking == 'serial') {
|
var valid_lots_quantity = this.get_valid_lots().length;
|
||||||
var valid_lots_quantity = this.get_valid_lots().length;
|
if (this.order_line.quantity < 0){
|
||||||
if (this.order_line.quantity < 0){
|
valid_lots_quantity = -valid_lots_quantity;
|
||||||
valid_lots_quantity = -valid_lots_quantity;
|
|
||||||
}
|
|
||||||
this.order_line.set_quantity(valid_lots_quantity);
|
|
||||||
}
|
}
|
||||||
|
this.order_line.set_quantity(valid_lots_quantity);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2174,7 +2292,7 @@ exports.Paymentline = Backbone.Model.extend({
|
|||||||
set_amount: function(value){
|
set_amount: function(value){
|
||||||
this.order.assert_editable();
|
this.order.assert_editable();
|
||||||
this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals);
|
this.amount = round_di(parseFloat(value) || 0, this.pos.currency.decimals);
|
||||||
this.pos.send_current_order_to_customer_facing_display();
|
if (this.pos.iface_customer_facing_display) this.pos.send_current_order_to_customer_facing_display();
|
||||||
this.trigger('change',this);
|
this.trigger('change',this);
|
||||||
},
|
},
|
||||||
// returns the amount of money on this paymentline
|
// returns the amount of money on this paymentline
|
||||||
@@ -2242,6 +2360,7 @@ exports.Paymentline = Backbone.Model.extend({
|
|||||||
//exports as JSON for receipt printing
|
//exports as JSON for receipt printing
|
||||||
export_for_printing: function(){
|
export_for_printing: function(){
|
||||||
return {
|
return {
|
||||||
|
cid: this.cid,
|
||||||
amount: this.get_amount(),
|
amount: this.get_amount(),
|
||||||
payment_method: this.payment_method.name,
|
payment_method: this.payment_method.name,
|
||||||
};
|
};
|
||||||
@@ -2455,7 +2574,7 @@ exports.Order = Backbone.Model.extend({
|
|||||||
qweb.default_dict = _.clone(QWeb.default_dict);
|
qweb.default_dict = _.clone(QWeb.default_dict);
|
||||||
qweb.add_template('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
|
qweb.add_template('<templates><t t-name="subreceipt">'+subreceipt+'</t></templates>');
|
||||||
|
|
||||||
return qweb.render('subreceipt',{'pos':self.pos,'widget':self.pos.chrome,'order':self, 'receipt': receipt}) ;
|
return qweb.render('subreceipt',{'pos':self.pos,'order':self, 'receipt': receipt}) ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2644,9 +2763,6 @@ exports.Order = Backbone.Model.extend({
|
|||||||
}
|
}
|
||||||
this.assert_editable();
|
this.assert_editable();
|
||||||
options = options || {};
|
options = options || {};
|
||||||
var attr = JSON.parse(JSON.stringify(product));
|
|
||||||
attr.pos = this.pos;
|
|
||||||
attr.order = this;
|
|
||||||
var line = new exports.Orderline({}, {pos: this.pos, order: this, product: product});
|
var line = new exports.Orderline({}, {pos: this.pos, order: this, product: product});
|
||||||
this.fix_tax_included_price(line);
|
this.fix_tax_included_price(line);
|
||||||
|
|
||||||
@@ -2687,8 +2803,8 @@ exports.Order = Backbone.Model.extend({
|
|||||||
this.select_orderline(this.get_last_orderline());
|
this.select_orderline(this.get_last_orderline());
|
||||||
}
|
}
|
||||||
|
|
||||||
if(line.has_product_lot){
|
if (options.draftPackLotLines) {
|
||||||
this.display_lot_popup();
|
this.selected_orderline.setPackLotLines(options.draftPackLotLines);
|
||||||
}
|
}
|
||||||
if (this.pos.config.iface_customer_facing_display) {
|
if (this.pos.config.iface_customer_facing_display) {
|
||||||
this.pos.send_current_order_to_customer_facing_display();
|
this.pos.send_current_order_to_customer_facing_display();
|
||||||
@@ -2700,6 +2816,10 @@ exports.Order = Backbone.Model.extend({
|
|||||||
select_orderline: function(line){
|
select_orderline: function(line){
|
||||||
if(line){
|
if(line){
|
||||||
if(line !== this.selected_orderline){
|
if(line !== this.selected_orderline){
|
||||||
|
// if line (new line to select) is not the same as the old
|
||||||
|
// selected_orderline, then we set the old line to false,
|
||||||
|
// and set the new line to true. Also, set the new line as
|
||||||
|
// the selected_orderline.
|
||||||
if(this.selected_orderline){
|
if(this.selected_orderline){
|
||||||
this.selected_orderline.set_selected(false);
|
this.selected_orderline.set_selected(false);
|
||||||
}
|
}
|
||||||
@@ -2717,19 +2837,6 @@ exports.Order = Backbone.Model.extend({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
display_lot_popup: function() {
|
|
||||||
var order_line = this.get_selected_orderline();
|
|
||||||
if (order_line){
|
|
||||||
var pack_lot_lines = order_line.compute_lot_lines();
|
|
||||||
this.pos.gui.show_popup('packlotline', {
|
|
||||||
'title': _t('Lot/Serial Number(s) Required'),
|
|
||||||
'pack_lot_lines': pack_lot_lines,
|
|
||||||
'order_line': order_line,
|
|
||||||
'order': this,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* ---- Payment Lines --- */
|
/* ---- Payment Lines --- */
|
||||||
add_paymentline: function(payment_method) {
|
add_paymentline: function(payment_method) {
|
||||||
this.assert_editable();
|
this.assert_editable();
|
||||||
@@ -2739,7 +2846,7 @@ exports.Order = Backbone.Model.extend({
|
|||||||
};
|
};
|
||||||
this.paymentlines.add(newPaymentline);
|
this.paymentlines.add(newPaymentline);
|
||||||
this.select_paymentline(newPaymentline);
|
this.select_paymentline(newPaymentline);
|
||||||
|
return newPaymentline;
|
||||||
},
|
},
|
||||||
get_paymentlines: function(){
|
get_paymentlines: function(){
|
||||||
return this.paymentlines.models;
|
return this.paymentlines.models;
|
||||||
@@ -3064,105 +3171,49 @@ exports.Order = Backbone.Model.extend({
|
|||||||
wait_for_push_order: function () {
|
wait_for_push_order: function () {
|
||||||
return this.is_to_email();
|
return this.is_to_email();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Object} object to use as props for instantiating OrderReceipt.
|
||||||
|
*/
|
||||||
|
getOrderReceiptEnv: function() {
|
||||||
|
// Formerly get_receipt_render_env defined in ScreenWidget.
|
||||||
|
return {
|
||||||
|
order: this,
|
||||||
|
receipt: this.export_for_printing(),
|
||||||
|
orderlines: this.get_orderlines(),
|
||||||
|
paymentlines: this.get_paymentlines(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
updatePricelist: function(newClient) {
|
||||||
|
let newClientPricelist, newClientFiscalPosition;
|
||||||
|
const defaultFiscalPosition = this.pos.fiscal_positions.find(
|
||||||
|
(position) => position.id === this.pos.config.default_fiscal_position_id[0]
|
||||||
|
);
|
||||||
|
if (newClient) {
|
||||||
|
newClientFiscalPosition = newClient.property_account_position_id
|
||||||
|
? this.pos.fiscal_positions.find(
|
||||||
|
(position) => position.id === newClient.property_account_position_id[0]
|
||||||
|
)
|
||||||
|
: defaultFiscalPosition;
|
||||||
|
newClientPricelist =
|
||||||
|
this.pos.pricelists.find(
|
||||||
|
(pricelist) => pricelist.id === newClient.property_product_pricelist[0]
|
||||||
|
) || this.pos.default_pricelist;
|
||||||
|
} else {
|
||||||
|
newClientFiscalPosition = defaultFiscalPosition;
|
||||||
|
newClientPricelist = this.pos.default_pricelist;
|
||||||
|
}
|
||||||
|
this.fiscal_position = newClientFiscalPosition;
|
||||||
|
this.set_pricelist(newClientPricelist);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var OrderCollection = Backbone.Collection.extend({
|
var OrderCollection = Backbone.Collection.extend({
|
||||||
model: exports.Order,
|
model: exports.Order,
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
The numpad handles both the choice of the property currently being modified
|
|
||||||
(quantity, price or discount) and the edition of the corresponding numeric value.
|
|
||||||
*/
|
|
||||||
exports.NumpadState = Backbone.Model.extend({
|
|
||||||
defaults: {
|
|
||||||
buffer: "0",
|
|
||||||
mode: "quantity"
|
|
||||||
},
|
|
||||||
appendNewChar: function(newChar) {
|
|
||||||
var oldBuffer;
|
|
||||||
oldBuffer = this.get('buffer');
|
|
||||||
if (oldBuffer === '0') {
|
|
||||||
this.set({
|
|
||||||
buffer: newChar
|
|
||||||
});
|
|
||||||
} else if (oldBuffer === '-0') {
|
|
||||||
this.set({
|
|
||||||
buffer: "-" + newChar
|
|
||||||
});
|
|
||||||
} else if (!(newChar === '.') || oldBuffer.indexOf('.') === -1) {
|
|
||||||
this.set({
|
|
||||||
buffer: (this.get('buffer')) + newChar
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
},
|
|
||||||
deleteLastChar: function() {
|
|
||||||
if(this.get('buffer') === ""){
|
|
||||||
if(this.get('mode') === 'quantity'){
|
|
||||||
this.trigger('set_value','remove');
|
|
||||||
}else{
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}
|
|
||||||
} else if (this.get('buffer') === "-0") {
|
|
||||||
this.set({ buffer: "0" });
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}else{
|
|
||||||
var newBuffer = this.get('buffer').slice(0,-1) || "";
|
|
||||||
this.set({ buffer: newBuffer });
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
switchSign: function() {
|
|
||||||
var oldBuffer;
|
|
||||||
oldBuffer = this.get('buffer');
|
|
||||||
this.set({
|
|
||||||
buffer: oldBuffer[0] === '-' ? oldBuffer.substr(1) : "-" + oldBuffer
|
|
||||||
});
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
},
|
|
||||||
positiveSign: function() {
|
|
||||||
var oldBuffer;
|
|
||||||
oldBuffer = this.get('buffer');
|
|
||||||
if (oldBuffer[0] === '-'){
|
|
||||||
this.set({buffer: oldBuffer.substr(1)});
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
negativeSign: function() {
|
|
||||||
var oldBuffer;
|
|
||||||
oldBuffer = this.get('buffer');
|
|
||||||
if (oldBuffer[0] !== '-'){
|
|
||||||
this.set({buffer: "-" + oldBuffer});
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
changeMode: function(newMode) {
|
|
||||||
this.set({
|
|
||||||
buffer: "0",
|
|
||||||
mode: newMode
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reset: function() {
|
|
||||||
this.set({
|
|
||||||
buffer: "0",
|
|
||||||
mode: "quantity"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
resetValue: function(){
|
|
||||||
if(this.get('buffer') === "") {
|
|
||||||
this.trigger('set_value','remove');
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.set({ buffer: "" });
|
|
||||||
this.trigger('set_value',this.get('buffer'));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// exports = {
|
// exports = {
|
||||||
// PosModel: PosModel,
|
// PosModel: PosModel,
|
||||||
// NumpadState: NumpadState,
|
|
||||||
// load_fields: load_fields,
|
// load_fields: load_fields,
|
||||||
// load_models: load_models,
|
// load_models: load_models,
|
||||||
// Orderline: Orderline,
|
// Orderline: Orderline,
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
odoo.define('point_of_sale.popups', function (require) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
// This file contains the Popups.
|
|
||||||
// Popups must be loaded and named in chrome.js.
|
|
||||||
// They are instanciated / destroyed with the .gui.show_popup()
|
|
||||||
// and .gui.close_popup() methods.
|
|
||||||
|
|
||||||
var PosBaseWidget = require('point_of_sale.BaseWidget');
|
|
||||||
var gui = require('point_of_sale.gui');
|
|
||||||
var _t = require('web.core')._t;
|
|
||||||
|
|
||||||
|
|
||||||
var PopupWidget = PosBaseWidget.extend({
|
|
||||||
template: 'PopupWidget',
|
|
||||||
init: function(parent, args) {
|
|
||||||
this._super(parent, args);
|
|
||||||
this.options = {};
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
'click .button.cancel': 'click_cancel',
|
|
||||||
'click .button.confirm': 'click_confirm',
|
|
||||||
'click .selection-item': 'click_item',
|
|
||||||
'click .input-button': 'click_numpad',
|
|
||||||
'click .mode-button': 'click_numpad',
|
|
||||||
},
|
|
||||||
|
|
||||||
// show the popup !
|
|
||||||
show: function(options){
|
|
||||||
if(this.$el){
|
|
||||||
this.$el.removeClass('oe_hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof options === 'string') {
|
|
||||||
this.options = {title: options};
|
|
||||||
} else {
|
|
||||||
this.options = options || {};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderElement();
|
|
||||||
|
|
||||||
// popups block the barcode reader ...
|
|
||||||
if (this.pos.barcode_reader) {
|
|
||||||
this.pos.barcode_reader.save_callbacks();
|
|
||||||
this.pos.barcode_reader.reset_action_callbacks();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// called before hide, when a popup is closed.
|
|
||||||
// extend this if you want a custom action when the
|
|
||||||
// popup is closed.
|
|
||||||
close: function(){
|
|
||||||
if (this.pos.barcode_reader) {
|
|
||||||
this.pos.barcode_reader.restore_callbacks();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// hides the popup. keep in mind that this is called in
|
|
||||||
// the initialization pass of the pos instantiation,
|
|
||||||
// so you don't want to do anything fancy in here
|
|
||||||
hide: function(){
|
|
||||||
if (this.$el) {
|
|
||||||
this.$el.addClass('oe_hidden');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// what happens when we click cancel
|
|
||||||
// ( it should close the popup and do nothing )
|
|
||||||
click_cancel: function(){
|
|
||||||
this.gui.close_popup();
|
|
||||||
if (this.options.cancel) {
|
|
||||||
this.options.cancel.call(this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// what happens when we confirm the action
|
|
||||||
click_confirm: function(){
|
|
||||||
this.gui.close_popup();
|
|
||||||
if (this.options.confirm) {
|
|
||||||
this.options.confirm.call(this);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Since Widget does not support extending the events declaration
|
|
||||||
// we declared them all in the top class.
|
|
||||||
click_item: function(){},
|
|
||||||
click_numad: function(){},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'alert', widget: PopupWidget});
|
|
||||||
|
|
||||||
var ErrorPopupWidget = PopupWidget.extend({
|
|
||||||
template:'ErrorPopupWidget',
|
|
||||||
show: function(options){
|
|
||||||
this._super(options);
|
|
||||||
this.gui.play_sound('error');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'error', widget: ErrorPopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var SyncErrorPopupWidget = ErrorPopupWidget.extend({
|
|
||||||
template:'SyncErrorPopupWidget',
|
|
||||||
show: function(opts) {
|
|
||||||
var self = this;
|
|
||||||
this._super(opts);
|
|
||||||
|
|
||||||
this.$('.stop_showing_sync_errors').off('click').click(function(){
|
|
||||||
self.gui.show_sync_errors = false;
|
|
||||||
self.click_confirm();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'error-sync', widget: SyncErrorPopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var ErrorTracebackPopupWidget = ErrorPopupWidget.extend({
|
|
||||||
template:'ErrorTracebackPopupWidget',
|
|
||||||
show: function(opts) {
|
|
||||||
var self = this;
|
|
||||||
this._super(opts);
|
|
||||||
|
|
||||||
this.$('.download').off('click').click(function(){
|
|
||||||
self.gui.prepare_download_link(self.options.body,
|
|
||||||
_t('error') + ' ' + moment().format('YYYY-MM-DD-HH-mm-ss') + '.txt',
|
|
||||||
'.download', '.download_error_file');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$('.email').off('click').click(function(){
|
|
||||||
self.gui.send_email( self.pos.company.email,
|
|
||||||
_t('IMPORTANT: Bug Report From Odoo Point Of Sale'),
|
|
||||||
self.options.body);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'error-traceback', widget: ErrorTracebackPopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var ErrorBarcodePopupWidget = ErrorPopupWidget.extend({
|
|
||||||
template:'ErrorBarcodePopupWidget',
|
|
||||||
show: function(barcode){
|
|
||||||
this._super({barcode: barcode});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'error-barcode', widget: ErrorBarcodePopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var ConfirmPopupWidget = PopupWidget.extend({
|
|
||||||
template: 'ConfirmPopupWidget',
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'confirm', widget: ConfirmPopupWidget});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A popup that allows the user to select one item from a list.
|
|
||||||
*
|
|
||||||
* Example::
|
|
||||||
*
|
|
||||||
* show_popup('selection',{
|
|
||||||
* title: "Popup Title",
|
|
||||||
* list: [
|
|
||||||
* { label: 'foobar', item: 45 },
|
|
||||||
* { label: 'bar foo', item: 'stuff' },
|
|
||||||
* ],
|
|
||||||
* confirm: function(item) {
|
|
||||||
* // get the item selected by the user.
|
|
||||||
* },
|
|
||||||
* cancel: function(){
|
|
||||||
* // user chose nothing
|
|
||||||
* }
|
|
||||||
* });
|
|
||||||
*/
|
|
||||||
|
|
||||||
var SelectionPopupWidget = PopupWidget.extend({
|
|
||||||
template: 'SelectionPopupWidget',
|
|
||||||
show: function(options){
|
|
||||||
var self = this;
|
|
||||||
options = options || {};
|
|
||||||
this._super(options);
|
|
||||||
|
|
||||||
this.list = options.list || [];
|
|
||||||
this.is_selected = options.is_selected || function (item) { return false; };
|
|
||||||
this.renderElement();
|
|
||||||
},
|
|
||||||
click_item : function(event) {
|
|
||||||
this.gui.close_popup();
|
|
||||||
if (this.options.confirm) {
|
|
||||||
var item = this.list[parseInt($(event.target).data('item-index'))];
|
|
||||||
item = item ? item.item : item;
|
|
||||||
this.options.confirm.call(self,item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'selection', widget: SelectionPopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var TextInputPopupWidget = PopupWidget.extend({
|
|
||||||
template: 'TextInputPopupWidget',
|
|
||||||
show: function(options){
|
|
||||||
options = options || {};
|
|
||||||
this._super(options);
|
|
||||||
|
|
||||||
this.renderElement();
|
|
||||||
this.$('input,textarea').focus();
|
|
||||||
},
|
|
||||||
click_confirm: function(){
|
|
||||||
var value = this.$('input,textarea').val();
|
|
||||||
this.gui.close_popup();
|
|
||||||
if( this.options.confirm ){
|
|
||||||
this.options.confirm.call(this,value);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'textinput', widget: TextInputPopupWidget});
|
|
||||||
|
|
||||||
|
|
||||||
var TextAreaPopupWidget = TextInputPopupWidget.extend({
|
|
||||||
template: 'TextAreaPopupWidget',
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'textarea', widget: TextAreaPopupWidget});
|
|
||||||
|
|
||||||
var PackLotLinePopupWidget = PopupWidget.extend({
|
|
||||||
template: 'PackLotLinePopupWidget',
|
|
||||||
events: _.extend({}, PopupWidget.prototype.events, {
|
|
||||||
'click .remove-lot': 'remove_lot',
|
|
||||||
'keydown': 'add_lot',
|
|
||||||
'blur .packlot-line-input': 'lose_input_focus'
|
|
||||||
}),
|
|
||||||
|
|
||||||
show: function(options){
|
|
||||||
this._super(options);
|
|
||||||
this.focus();
|
|
||||||
},
|
|
||||||
|
|
||||||
click_confirm: function(){
|
|
||||||
var pack_lot_lines = this.options.pack_lot_lines;
|
|
||||||
this.$('.packlot-line-input').each(function(index, el){
|
|
||||||
var cid = $(el).attr('cid'),
|
|
||||||
lot_name = $(el).val();
|
|
||||||
var pack_line = pack_lot_lines.get({cid: cid});
|
|
||||||
pack_line.set_lot_name(lot_name);
|
|
||||||
});
|
|
||||||
pack_lot_lines.remove_empty_model();
|
|
||||||
pack_lot_lines.set_quantity_by_lot();
|
|
||||||
this.options.order.save_to_db();
|
|
||||||
this.options.order_line.trigger('change', this.options.order_line);
|
|
||||||
this.gui.close_popup();
|
|
||||||
},
|
|
||||||
|
|
||||||
add_lot: function(ev) {
|
|
||||||
if (ev.keyCode === $.ui.keyCode.ENTER && this.options.order_line.product.tracking == 'serial'){
|
|
||||||
var pack_lot_lines = this.options.pack_lot_lines,
|
|
||||||
$input = $(ev.target),
|
|
||||||
cid = $input.attr('cid'),
|
|
||||||
lot_name = $input.val();
|
|
||||||
|
|
||||||
var lot_model = pack_lot_lines.get({cid: cid});
|
|
||||||
lot_model.set_lot_name(lot_name); // First set current model then add new one
|
|
||||||
if(!pack_lot_lines.get_empty_model()){
|
|
||||||
var new_lot_model = lot_model.add();
|
|
||||||
this.focus_model = new_lot_model;
|
|
||||||
}
|
|
||||||
pack_lot_lines.set_quantity_by_lot();
|
|
||||||
this.renderElement();
|
|
||||||
this.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
remove_lot: function(ev){
|
|
||||||
var pack_lot_lines = this.options.pack_lot_lines,
|
|
||||||
$input = $(ev.target).prev(),
|
|
||||||
cid = $input.attr('cid');
|
|
||||||
var lot_model = pack_lot_lines.get({cid: cid});
|
|
||||||
lot_model.remove();
|
|
||||||
pack_lot_lines.set_quantity_by_lot();
|
|
||||||
this.renderElement();
|
|
||||||
},
|
|
||||||
|
|
||||||
lose_input_focus: function(ev){
|
|
||||||
var $input = $(ev.target),
|
|
||||||
cid = $input.attr('cid');
|
|
||||||
var lot_model = this.options.pack_lot_lines.get({cid: cid});
|
|
||||||
lot_model.set_lot_name($input.val());
|
|
||||||
},
|
|
||||||
|
|
||||||
focus: function(){
|
|
||||||
this.$("input[autofocus]").focus();
|
|
||||||
this.focus_model = false; // after focus clear focus_model on widget
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'packlotline', widget:PackLotLinePopupWidget});
|
|
||||||
|
|
||||||
var NumberPopupWidget = PopupWidget.extend({
|
|
||||||
template: 'NumberPopupWidget',
|
|
||||||
show: function(options){
|
|
||||||
options = options || {};
|
|
||||||
this._super(options);
|
|
||||||
|
|
||||||
this.inputbuffer = '' + (options.value || '');
|
|
||||||
this.decimal_separator = _t.database.parameters.decimal_point;
|
|
||||||
this.renderElement();
|
|
||||||
this.firstinput = true;
|
|
||||||
},
|
|
||||||
click_numpad: function(event){
|
|
||||||
var newbuf = this.gui.numpad_input(
|
|
||||||
this.inputbuffer,
|
|
||||||
$(event.target).data('action'),
|
|
||||||
{'firstinput': this.firstinput});
|
|
||||||
|
|
||||||
this.firstinput = (newbuf.length === 0);
|
|
||||||
|
|
||||||
if (newbuf !== this.inputbuffer) {
|
|
||||||
this.inputbuffer = newbuf;
|
|
||||||
this.$('.value').text(this.inputbuffer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
click_confirm: function(){
|
|
||||||
this.gui.close_popup();
|
|
||||||
if( this.options.confirm ){
|
|
||||||
this.options.confirm.call(this,this.inputbuffer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'number', widget: NumberPopupWidget});
|
|
||||||
|
|
||||||
var PasswordPopupWidget = NumberPopupWidget.extend({
|
|
||||||
renderElement: function(){
|
|
||||||
this._super();
|
|
||||||
this.$('.popup').addClass('popup-password');
|
|
||||||
},
|
|
||||||
click_numpad: function(event){
|
|
||||||
this._super.apply(this, arguments);
|
|
||||||
var $value = this.$('.value');
|
|
||||||
$value.text($value.text().replace(/./g, '•'));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'password', widget: PasswordPopupWidget});
|
|
||||||
|
|
||||||
var OrderImportPopupWidget = PopupWidget.extend({
|
|
||||||
template: 'OrderImportPopupWidget',
|
|
||||||
});
|
|
||||||
gui.define_popup({name:'orderimport', widget: OrderImportPopupWidget});
|
|
||||||
|
|
||||||
return PopupWidget;
|
|
||||||
});
|
|
||||||
@@ -5,31 +5,75 @@ var Session = require('web.Session');
|
|||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
var _t = core._t;
|
var _t = core._t;
|
||||||
|
|
||||||
|
// IMPROVEMENT: This is too much. We can get away from this class.
|
||||||
|
class PrintResult {
|
||||||
|
constructor({ successful, message }) {
|
||||||
|
this.successful = successful;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PrintResultGenerator {
|
||||||
|
IoTActionError() {
|
||||||
|
return new PrintResult({
|
||||||
|
successful: false,
|
||||||
|
message: {
|
||||||
|
title: _t('Connection to IoT Box failed'),
|
||||||
|
body: _t('Please check if the IoT Box is still connected.'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
IoTResultError() {
|
||||||
|
return new PrintResult({
|
||||||
|
successful: false,
|
||||||
|
message: {
|
||||||
|
title: _t('Connection to the printer failed'),
|
||||||
|
body: _t('Please check if the printer is still connected.'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Successful() {
|
||||||
|
return new PrintResult({
|
||||||
|
successful: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var PrinterMixin = {
|
var PrinterMixin = {
|
||||||
init: function () {
|
init: function() {
|
||||||
this.receipt_queue = [];
|
this.receipt_queue = [];
|
||||||
|
this.printResultGenerator = new PrintResultGenerator();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the receipt to the queue of receipts to be printed and process it.
|
* Add the receipt to the queue of receipts to be printed and process it.
|
||||||
|
* We clear the print queue if printing is not successful.
|
||||||
* @param {String} receipt: The receipt to be printed, in HTML
|
* @param {String} receipt: The receipt to be printed, in HTML
|
||||||
|
* @returns {PrintResult}
|
||||||
*/
|
*/
|
||||||
print_receipt: function (receipt) {
|
print_receipt: async function(receipt) {
|
||||||
var self = this;
|
|
||||||
if (receipt) {
|
if (receipt) {
|
||||||
this.receipt_queue.push(receipt);
|
this.receipt_queue.push(receipt);
|
||||||
}
|
}
|
||||||
function process_next_job() {
|
let image, sendPrintResult;
|
||||||
if (self.receipt_queue.length > 0) {
|
while (this.receipt_queue.length > 0) {
|
||||||
var r = self.receipt_queue.shift();
|
receipt = this.receipt_queue.shift();
|
||||||
return self.htmlToImg(r)
|
image = await this.htmlToImg(receipt);
|
||||||
.then(self.send_printing_job.bind(self))
|
try {
|
||||||
.then(self._onIoTActionResult.bind(self))
|
sendPrintResult = await this.send_printing_job(image);
|
||||||
.then(process_next_job)
|
} catch (error) {
|
||||||
.guardedCatch(self._onIoTActionFail.bind(self));
|
// Error in communicating to the IoT box.
|
||||||
|
this.receipt_queue.length = 0;
|
||||||
|
return this.printResultGenerator.IoTActionError();
|
||||||
|
}
|
||||||
|
// rpc call is okay but printing failed because
|
||||||
|
// IoT box can't find a printer.
|
||||||
|
if (!sendPrintResult) {
|
||||||
|
this.receipt_queue.length = 0;
|
||||||
|
return this.printResultGenerator.IoTResultError();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return process_next_job();
|
return this.printResultGenerator.Successful();
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,7 +108,7 @@ var PrinterMixin = {
|
|||||||
|
|
||||||
_onIoTActionResult: function (data){
|
_onIoTActionResult: function (data){
|
||||||
if (this.pos && (data === false || data.result === false)) {
|
if (this.pos && (data === false || data.result === false)) {
|
||||||
this.pos.gui.show_popup('error',{
|
Gui.showPopup('ErrorPopup',{
|
||||||
'title': _t('Connection to the printer failed'),
|
'title': _t('Connection to the printer failed'),
|
||||||
'body': _t('Please check if the printer is still connected.'),
|
'body': _t('Please check if the printer is still connected.'),
|
||||||
});
|
});
|
||||||
@@ -73,7 +117,7 @@ var PrinterMixin = {
|
|||||||
|
|
||||||
_onIoTActionFail: function () {
|
_onIoTActionFail: function () {
|
||||||
if (this.pos) {
|
if (this.pos) {
|
||||||
this.pos.gui.show_popup('error',{
|
Gui.showPopup('ErrorPopup',{
|
||||||
'title': _t('Connection to IoT Box failed'),
|
'title': _t('Connection to IoT Box failed'),
|
||||||
'body': _t('Please check if the IoT Box is still connected.'),
|
'body': _t('Please check if the IoT Box is still connected.'),
|
||||||
});
|
});
|
||||||
@@ -120,5 +164,7 @@ var Printer = core.Class.extend(PrinterMixin, {
|
|||||||
return {
|
return {
|
||||||
PrinterMixin: PrinterMixin,
|
PrinterMixin: PrinterMixin,
|
||||||
Printer: Printer,
|
Printer: Printer,
|
||||||
|
PrintResult,
|
||||||
|
PrintResultGenerator,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
|||||||
|
odoo.define('point_of_sale.utils', function(require) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function getFileAsText(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!file) {
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.addEventListener('load', function() {
|
||||||
|
resolve(reader.result);
|
||||||
|
});
|
||||||
|
reader.addEventListener('abort', reject);
|
||||||
|
reader.addEventListener('error', reject);
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getFileAsText };
|
||||||
|
});
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
odoo.define('point_of_sale.BaseWidget', function (require) {
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
var field_utils = require('web.field_utils');
|
|
||||||
var utils = require('web.utils');
|
|
||||||
var Widget = require('web.Widget');
|
|
||||||
|
|
||||||
var round_di = utils.round_decimals;
|
|
||||||
|
|
||||||
// This is a base class for all Widgets in the POS. It exposes relevant data to the
|
|
||||||
// templates :
|
|
||||||
// - widget.currency : { symbol: '$' | '€' | ..., position: 'before' | 'after }
|
|
||||||
// - widget.format_currency(amount) : this method returns a formatted string based on the
|
|
||||||
// symbol, the position, and the amount of money.
|
|
||||||
// if the PoS is not fully loaded when you instanciate the widget, the currency might not
|
|
||||||
// yet have been initialized. Use __build_currency_template() to recompute with correct values
|
|
||||||
// before rendering.
|
|
||||||
var PosBaseWidget = Widget.extend({
|
|
||||||
init:function(parent,options){
|
|
||||||
this._super(parent);
|
|
||||||
options = options || {};
|
|
||||||
this.pos = options.pos || (parent ? parent.pos : undefined);
|
|
||||||
this.chrome = options.chrome || (parent ? parent.chrome : undefined);
|
|
||||||
this.gui = options.gui || (parent ? parent.gui : undefined);
|
|
||||||
|
|
||||||
// the widget class does not support anymore using $el/el before the
|
|
||||||
// 'start' lifecycle method, but point of sale actually needs it.
|
|
||||||
this.setElement(this._makeDescriptive());
|
|
||||||
},
|
|
||||||
format_currency: function(amount,precision){
|
|
||||||
var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2};
|
|
||||||
|
|
||||||
amount = this.format_currency_no_symbol(amount,precision);
|
|
||||||
|
|
||||||
if (currency.position === 'after') {
|
|
||||||
return amount + ' ' + (currency.symbol || '');
|
|
||||||
} else {
|
|
||||||
return (currency.symbol || '') + ' ' + amount;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
format_currency_no_symbol: function(amount, precision) {
|
|
||||||
var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2};
|
|
||||||
var decimals = currency.decimals;
|
|
||||||
|
|
||||||
if (precision && this.pos.dp[precision] !== undefined) {
|
|
||||||
decimals = this.pos.dp[precision];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof amount === 'number') {
|
|
||||||
amount = round_di(amount,decimals).toFixed(decimals);
|
|
||||||
amount = field_utils.format.float(round_di(amount, decimals), {digits: [69, decimals]});
|
|
||||||
}
|
|
||||||
|
|
||||||
return amount;
|
|
||||||
},
|
|
||||||
show: function(){
|
|
||||||
this.$el.removeClass('oe_hidden');
|
|
||||||
},
|
|
||||||
hide: function(){
|
|
||||||
this.$el.addClass('oe_hidden');
|
|
||||||
},
|
|
||||||
format_pr: function(value,precision){
|
|
||||||
var decimals = precision > 0 ? Math.max(0,Math.ceil(Math.log(1.0/precision) / Math.log(10))) : 0;
|
|
||||||
return value.toFixed(decimals);
|
|
||||||
},
|
|
||||||
format_fixed: function(value,integer_width,decimal_width){
|
|
||||||
value = value.toFixed(decimal_width || 0);
|
|
||||||
var width = value.indexOf('.');
|
|
||||||
if (width < 0 ) {
|
|
||||||
width = value.length;
|
|
||||||
}
|
|
||||||
var missing = integer_width - width;
|
|
||||||
while (missing > 0) {
|
|
||||||
value = '0' + value;
|
|
||||||
missing--;
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return PosBaseWidget;
|
|
||||||
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="Chrome" owl="1">
|
||||||
|
<div class="pos" t-att-class="{ 'big-scrollbars': state.uiState.hasBigScrollBars }">
|
||||||
|
<div class="pos-receipt-print"></div>
|
||||||
|
<t t-if="state.uiState === 'READY'">
|
||||||
|
<div class="pos-topheader">
|
||||||
|
<div t-if="tempScreen.isShown" class="block-top-header" />
|
||||||
|
<div class="pos-branding">
|
||||||
|
<img class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
|
||||||
|
src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
|
||||||
|
</div>
|
||||||
|
<div class="pos-rightheader">
|
||||||
|
<OrderSelector />
|
||||||
|
<CashierName />
|
||||||
|
<SaleDetailsButton t-if="env.pos.proxy.printer" />
|
||||||
|
<ProxyStatus t-if="env.pos.config.use_proxy" />
|
||||||
|
<ClientScreenButton t-if="clientScreenButtonIsShown" />
|
||||||
|
<SyncNotification />
|
||||||
|
<HeaderButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pos-content">
|
||||||
|
<div class="window">
|
||||||
|
<div class="subwindow">
|
||||||
|
<div class="subwindow-container">
|
||||||
|
<div class="subwindow-container-fix screens">
|
||||||
|
<t isShown="!tempScreen.isShown" t-component="mainScreen.component"
|
||||||
|
t-props="mainScreenProps" t-key="mainScreen.name" />
|
||||||
|
<t t-if="tempScreen.isShown" t-component="tempScreen.component"
|
||||||
|
t-props="tempScreenProps" t-key="tempScreen.name" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DebugWidget t-if="env.isDebug() and state.debugWidgetIsShown"
|
||||||
|
t-transition="fade" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<div t-if="['LOADING', 'CLOSING'].includes(state.uiState)" class="loader" t-transition="swing">
|
||||||
|
<div class="loader-feedback">
|
||||||
|
<h1 class="message">
|
||||||
|
<t t-esc="loading.message" />
|
||||||
|
</h1>
|
||||||
|
<div class="progressbar">
|
||||||
|
<div class="progress" t-ref="progressbar"></div>
|
||||||
|
</div>
|
||||||
|
<div t-if="loading.skipButtonIsShown" class="button skip" t-on-click="trigger('loading-skip-callback')">
|
||||||
|
Skip
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Allow popups to be visible at any state of the ui. -->
|
||||||
|
<div t-if="popup.isShown" class="popups">
|
||||||
|
<t t-component="popup.component" t-props="popupProps"
|
||||||
|
t-key="popup.name" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NotificationSound t-if="state.sound.src" sound="state.sound" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="CustomerFacingDisplayHead">
|
||||||
|
<div class="resources">
|
||||||
|
<base t-att-href="origin" />
|
||||||
|
<link href="/point_of_sale/static/src/css/customer_facing_display.css"
|
||||||
|
rel="stylesheet" />
|
||||||
|
<script type="text/javascript">
|
||||||
|
// This function needs to be named that way, call it the foreign JS API
|
||||||
|
// The iotbox will execute it, with the behavior intended
|
||||||
|
function foreign_js() {
|
||||||
|
if ($('.pos-adv').hasClass('pos-hidden')) {
|
||||||
|
$('.pos-customer_facing_display').addClass('pos-js_no_ADV');
|
||||||
|
}
|
||||||
|
$(window).on('resize', function () {
|
||||||
|
$('.pos-customer_facing_display').toggleClass('pos-js_no_ADV', $('.pos-adv').hasClass('pos-hidden'));
|
||||||
|
}).trigger('resize');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="CustomerFacingDisplayOrderLines">
|
||||||
|
<t t-foreach="orderlines" t-as="orderline">
|
||||||
|
<div class="pos_orderlines_item">
|
||||||
|
<div>
|
||||||
|
<div t-attf-style="background-image:url(#{orderline.product.image_base64})" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<t t-esc="orderline.product.display_name" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<t t-esc="orderline.get_quantity_str()" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<t t-esc="pos.format_currency(orderline.get_display_price())" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="CustomerFacingDisplayPaymentLines">
|
||||||
|
<t t-foreach="order.get_paymentlines()" t-as="paymentline">
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<t t-esc="paymentline.name" /></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<t t-esc="pos.format_currency(paymentline.get_amount())" /></span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-if="order.get_paymentlines().length > 0">
|
||||||
|
<div>
|
||||||
|
<span class="pos-change_title">Change:</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="pos-change_amount">
|
||||||
|
<t t-esc="pos.format_currency(order.get_change())" /></span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="CashierName" owl="1">
|
||||||
|
<div class="oe_status">
|
||||||
|
<span class="username">
|
||||||
|
<t t-esc="username" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ClientScreenButton" owl="1">
|
||||||
|
<div class="oe_status" t-on-click="onClick">
|
||||||
|
<span class="message"><t t-esc="message" /></span>
|
||||||
|
<div t-if="state.status === 'warning'" class="js_warning oe_icon oe_orange">
|
||||||
|
<i class="fa fa-fw fa-desktop" role="img" aria-label="Client Screen Warning" title="Client Screen Warning"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'failure'" class="js_disconnected oe_icon oe_red">
|
||||||
|
<i class="fa fa-fw fa-desktop" role="img" aria-label="Client Screen Disconnected" title="Client Screen Disconnected"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'success'" class="js_connected oe_icon oe_green">
|
||||||
|
<i class="fa fa-fw fa-desktop" role="img" aria-label="Client Screen Connected" title="Client Screen Connected"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="DebugWidget" owl="1">
|
||||||
|
<Draggable limitArea="'.pos'">
|
||||||
|
<div class="debug-widget">
|
||||||
|
<header class="drag-handle">
|
||||||
|
<h1>Debug Window</h1>
|
||||||
|
</header>
|
||||||
|
<div class="toggle" t-on-click="trigger('toggle-debug-widget')" title="Dismiss"
|
||||||
|
role="img" aria-label="Dismiss"><i class="fa fa-times" /></div>
|
||||||
|
<div class="content">
|
||||||
|
<p class="category">Electronic Scale</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input t-model="state.weightInput" type="text" class="weight"></input>
|
||||||
|
</li>
|
||||||
|
<li class="button set_weight" t-on-click="setWeight">Set Weight</li>
|
||||||
|
<li class="button reset_weight" t-on-click="resetWeight">Reset</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="category">Barcode Scanner</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<input t-model="state.barcodeInput" type="text" class="ean"></input>
|
||||||
|
</li>
|
||||||
|
<li class="button barcode" t-on-click="barcodeScan">Scan</li>
|
||||||
|
<li class="button custom_ean" t-on-click="barcodeScanEAN">Scan EAN-13</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="category">Orders</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li class="button" t-on-click="deleteOrders">
|
||||||
|
Delete Paid Orders
|
||||||
|
</li>
|
||||||
|
<li class="button" t-on-click="deleteUnpaidOrders">
|
||||||
|
Delete Unpaid Orders
|
||||||
|
</li>
|
||||||
|
<li t-if="!state.isPaidOrdersReady" class="button"
|
||||||
|
t-on-click="preparePaidOrders">
|
||||||
|
Export Paid Orders
|
||||||
|
</li>
|
||||||
|
<a t-else="" t-att-download="paidOrdersFilename" t-att-href="paidOrdersURL"
|
||||||
|
t-on-click="state.isPaidOrdersReady = !state.isPaidOrdersReady">
|
||||||
|
<li class="button">
|
||||||
|
Download Paid Orders
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<li t-if="!state.isUnpaidOrdersReady" class="button"
|
||||||
|
t-on-click="prepareUnpaidOrders">
|
||||||
|
Export Unpaid Orders
|
||||||
|
</li>
|
||||||
|
<a t-else="" t-att-download="unpaidOrdersFilename"
|
||||||
|
t-att-href="unpaidOrdersURL"
|
||||||
|
t-on-click="state.isUnpaidOrdersReady = !state.isUnpaidOrdersReady">
|
||||||
|
<li class="button">
|
||||||
|
Download Unpaid Orders
|
||||||
|
</li>
|
||||||
|
</a>
|
||||||
|
<li class="button import_orders" style="position:relative">
|
||||||
|
Import Orders
|
||||||
|
<input t-on-change="importOrders" type="file"
|
||||||
|
style="opacity:0;position:absolute;top:0;left:0;right:0;bottom:0;margin:0;cursor:pointer" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="category">Hardware Status</p>
|
||||||
|
<ul>
|
||||||
|
<li class="status weighing">Weighing</li>
|
||||||
|
<li class="button display_refresh" t-on-click="refreshDisplay">
|
||||||
|
Refresh Display
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p class="category">Hardware Events</p>
|
||||||
|
<ul>
|
||||||
|
<li class="event" t-ref="open_cashbox">Open Cashbox</li>
|
||||||
|
<li class="event" t-ref="print_receipt">Print Receipt</li>
|
||||||
|
<li class="event" t-ref="scale_read">Read Weighing Scale</li>
|
||||||
|
</ul>
|
||||||
|
<p class="category">Others</p>
|
||||||
|
<ul>
|
||||||
|
<li class="event">
|
||||||
|
<span>Buffer: </span>
|
||||||
|
<t t-esc="bufferRepr" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="HeaderButton" owl="1">
|
||||||
|
<div class="header-button close_button" t-att-class="{ confirm: state.label === 'Confirm' }"
|
||||||
|
t-on-click="onClick">
|
||||||
|
<t t-esc="translatedLabel" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="OrderSelector" owl="1">
|
||||||
|
<div class="order-selector">
|
||||||
|
<span class="orders touch-scrollable">
|
||||||
|
<t t-foreach="orderList" t-as="order" t-key="order.cid">
|
||||||
|
<t t-if="order === env.pos.get_order()">
|
||||||
|
<span class="order-button select-order selected">
|
||||||
|
<span class="order-sequence">
|
||||||
|
<t t-esc="order.sequence_number" />
|
||||||
|
</span>
|
||||||
|
<t t-esc="creationTime(order)" />
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="order-button select-order" t-on-click="selectOrder(order)">
|
||||||
|
<span class="order-sequence">
|
||||||
|
<t t-esc="order.sequence_number" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</span>
|
||||||
|
<span class="order-button square neworder-button" t-on-click="addNewOrder">
|
||||||
|
<i class="fa fa-plus" role="img" aria-label="New order" title="New order" />
|
||||||
|
</span>
|
||||||
|
<span class="order-button square deleteorder-button" t-on-click="deleteCurrentOrder">
|
||||||
|
<i class="fa fa-minus" role="img" aria-label="Delete order" title="Delete order" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ProxyStatus" owl="1">
|
||||||
|
<div class="oe_status js_proxy" t-on-click="onClick">
|
||||||
|
<span t-if="state.msg" class="js_msg">
|
||||||
|
<t t-esc="state.msg" />
|
||||||
|
</span>
|
||||||
|
<div t-if="state.status === 'connected'" class="js_connected oe_green">
|
||||||
|
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Connected"
|
||||||
|
title="Proxy Connected"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'connecting'" class="js_connecting">
|
||||||
|
<i class="fa fa-fw fa-spin fa-spinner" role="img" aria-label="Connecting to Proxy"
|
||||||
|
title="Connecting to Proxy"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'warning'" class="js_warning oe_orange">
|
||||||
|
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Warning"
|
||||||
|
title="Proxy Warning"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'disconnected'" class="js_disconnected oe_red">
|
||||||
|
<i class="fa fa-fw fa-sitemap" role="img" aria-label="Proxy Disconnected"
|
||||||
|
title="Proxy Disconnected"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="SaleDetailsButton" owl="1">
|
||||||
|
<div class="oe_status">
|
||||||
|
<div class="js_connected oe_icon">
|
||||||
|
<i class="fa fa-fw fa-print" role="img" aria-label="Print" t-on-click="onClick"
|
||||||
|
title="Print a report with all the sales of the current PoS Session"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="SyncNotification" owl="1">
|
||||||
|
<div class="oe_status" t-on-click="onClick">
|
||||||
|
<span t-if="state.msg" class="js_msg">
|
||||||
|
<t t-esc="state.msg" />
|
||||||
|
<span> </span>
|
||||||
|
</span>
|
||||||
|
<div t-if="state.status === 'connected'" class="js_connected oe_icon oe_green">
|
||||||
|
<i class="fa fa-fw fa-wifi" role="img" aria-label="Synchronisation Connected"
|
||||||
|
title="Synchronisation Connected"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'connecting'" class="js_connecting oe_icon">
|
||||||
|
<i class="fa fa-fw fa-spin fa-spinner" role="img"
|
||||||
|
aria-label="Synchronisation Connecting" title="Synchronisation Connecting"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'disconnected'" class="js_disconnected oe_icon oe_red">
|
||||||
|
<i class="fa fa-fw fa-wifi" role="img" aria-label="Synchronisation Disconnected"
|
||||||
|
title="Synchronisation Disconnected"></i>
|
||||||
|
</div>
|
||||||
|
<div t-if="state.status === 'error'" class="js_error oe_icon oe_red">
|
||||||
|
<i class="fa fa-fw fa-warning" role="img" aria-label="Synchronisation Error"
|
||||||
|
title="Synchronisation Error"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="Draggable" owl="1">
|
||||||
|
<t t-slot="default"></t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="NotificationSound" owl="1">
|
||||||
|
<audio t-att-src="props.sound.src" autoplay="true"></audio>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ConfirmPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-confirm">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<main class="body">
|
||||||
|
<t t-esc=" props.body" />
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button confirm" t-on-click="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
<t t-esc="props.cancelText" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="EditListInput" owl="1">
|
||||||
|
<div>
|
||||||
|
<input type="text" t-model="props.item.text" class="popup-input list-line-input"
|
||||||
|
placeholder="Serial/Lot Number" t-on-keyup="onKeyup" />
|
||||||
|
<i class="oe_link_icon fa fa-trash-o" role="img" aria-label="Remove" title="Remove"
|
||||||
|
t-on-click="trigger('remove-item', props.item)"></i>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="EditListPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<div class="popup popup-text">
|
||||||
|
<header class="title">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<main class="list-lines" t-on-remove-item="removeItem"
|
||||||
|
t-on-create-new-item="createNewItem">
|
||||||
|
<t t-foreach="state.array" t-as="item" t-key="item._id">
|
||||||
|
<EditListInput item="item" />
|
||||||
|
</t>
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button confirm" t-on-click="confirm">
|
||||||
|
Ok
|
||||||
|
</div>
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ErrorBarcodePopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-barcode">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<span>Unknown Barcode</span>
|
||||||
|
<br />
|
||||||
|
<span class="barcode">
|
||||||
|
<t t-esc="props.code" />
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
<main class="body">
|
||||||
|
<t t-esc="translatedMessage" />
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button cancel" t-on-click="confirm">
|
||||||
|
Ok
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ErrorPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<div class="popup popup-error">
|
||||||
|
<p class="title">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</p>
|
||||||
|
<p class="body">
|
||||||
|
<t t-esc="props.body" />
|
||||||
|
</p>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="button cancel" t-on-click="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="ErrorTracebackPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<div class="popup popup-error">
|
||||||
|
<header class="title">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<main class="body traceback">
|
||||||
|
<t t-esc="props.body" />
|
||||||
|
</main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div t-if="!props.exitButtonIsShown" class="button cancel" t-on-click="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
<div t-if="props.exitButtonIsShown" class="button cancel" t-on-click="trigger(props.exitButtonTrigger)">
|
||||||
|
<t t-esc="props.exitButtonText" />
|
||||||
|
</div>
|
||||||
|
<a t-att-download="tracebackFilename" t-att-href="tracebackUrl">
|
||||||
|
<div class="button icon download">
|
||||||
|
<i class="fa fa-download" role="img"
|
||||||
|
aria-label="Download error traceback"
|
||||||
|
title="Download error traceback"></i>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="button icon email" t-on-click="emailTraceback">
|
||||||
|
<i class="fa fa-paper-plane" role="img" aria-label="Send by email"
|
||||||
|
title="Send by email"></i>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="NumberPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-number" t-att-class="{ 'popup-password': props.isPassword }">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<div class="popup-input value active">
|
||||||
|
<t t-esc="inputBuffer" />
|
||||||
|
</div>
|
||||||
|
<div class="popup-numpad">
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('1')">1</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('2')">2</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('3')">3</button>
|
||||||
|
<t t-if="props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+1')">+1</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="!props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+10')">+10</button>
|
||||||
|
</t>
|
||||||
|
<br />
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('4')">4</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('5')">5</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('6')">6</button>
|
||||||
|
<t t-if="props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+2')">+2</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="!props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+20')">+20</button>
|
||||||
|
</t>
|
||||||
|
<br />
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('7')">7</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('8')">8</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('9')">9</button>
|
||||||
|
<t t-if="props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+5')">+5</button>
|
||||||
|
</t>
|
||||||
|
<t t-if="!props.cheap">
|
||||||
|
<button class="mode-button add" t-on-mousedown.prevent="sendInput('+50')">+50</button>
|
||||||
|
</t>
|
||||||
|
<br />
|
||||||
|
<button class="input-button numpad-char" t-on-mousedown.prevent="sendInput('Delete')">C</button>
|
||||||
|
<button class="input-button number-char" t-on-mousedown.prevent="sendInput('0')">0</button>
|
||||||
|
<button class="input-button number-char dot" t-on-mousedown.prevent="sendInput(decimalSeparator)">
|
||||||
|
<t t-esc="decimalSeparator" /></button>
|
||||||
|
<button class="input-button numpad-backspace" t-on-mousedown.prevent="sendInput('Backspace')">
|
||||||
|
<img style="pointer-events: none;"
|
||||||
|
src="/point_of_sale/static/src/img/backspace.png" width="24"
|
||||||
|
height="21" alt="Backspace" />
|
||||||
|
</button>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
<footer class="footer centered">
|
||||||
|
<div class="button cancel" t-on-mousedown.prevent="cancel">
|
||||||
|
<t t-esc="props.cancelText" />
|
||||||
|
</div>
|
||||||
|
<div class="button confirm" t-on-mousedown.prevent="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="OfflineErrorPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-error">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<main class="body traceback"><t t-esc="props.body"/></main>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
Ok
|
||||||
|
</div>
|
||||||
|
<div class="button dont-show-again" t-on-click="dontShowAgain">
|
||||||
|
Don't show again
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="OrderImportPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-import">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<span>Finished Importing Orders</span>
|
||||||
|
</header>
|
||||||
|
<ul class="body">
|
||||||
|
<li>Successfully imported <b><t t-esc="props.report.paid or 0" /></b> paid orders</li>
|
||||||
|
<li>Successfully imported <b><t t-esc="props.report.unpaid or 0" /></b> unpaid orders</li>
|
||||||
|
<t t-if="unpaidSkipped">
|
||||||
|
<li><b><t t-esc="unpaidSkipped"/></b> unpaid orders could not be imported
|
||||||
|
<ul>
|
||||||
|
<li><b><t t-esc="props.report.unpaid_skipped_existing or 0" /></b> were duplicates of existing orders</li>
|
||||||
|
<li><b><t t-esc="props.report.unpaid_skipped_session or 0" /></b> belong to another session:
|
||||||
|
<t t-if="props.report.unpaid_skipped_sessions">
|
||||||
|
<ul>
|
||||||
|
<li>Session ids: <b><t t-esc="props.report.unpaid_skipped_sessions" /></b></li>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="SelectionPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-selection">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<div class="selection scrollable-y touch-scrollable">
|
||||||
|
<t t-foreach="props.list" t-as="item" t-key="item._id">
|
||||||
|
<div class="selection-item" t-att-class="{ selected: item.isSelected }"
|
||||||
|
t-on-click="selectItem(item.id)">
|
||||||
|
<t t-esc="item.label" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
<t t-esc="props.cancelText" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="TextAreaPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<Draggable>
|
||||||
|
<div class="popup popup-textarea">
|
||||||
|
<header class="title drag-handle">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<textarea t-model="state.inputValue" t-ref="input"></textarea>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="button confirm" t-on-click="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
<t t-esc="props.cancelText" />
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="TextInputPopup" owl="1">
|
||||||
|
<div role="dialog" class="modal-dialog">
|
||||||
|
<div class="popup popup-textinput">
|
||||||
|
<header class="title">
|
||||||
|
<t t-esc="props.title" />
|
||||||
|
</header>
|
||||||
|
<div class="div">
|
||||||
|
<p>
|
||||||
|
<t t-esc="props.body" />
|
||||||
|
</p>
|
||||||
|
<input type="text" t-model="state.inputValue" t-ref="input" />
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<div class="button confirm" t-on-click="confirm">
|
||||||
|
<t t-esc="props.confirmText" />
|
||||||
|
</div>
|
||||||
|
<div class="button cancel" t-on-click="cancel">
|
||||||
|
<t t-esc="props.cancelText" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user