Initial commit: Sale packages
|
After Width: | Height: | Size: 6.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="70" height="70" viewBox="0 0 70 70"><defs><path id="a" d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z"/><linearGradient id="c" x1="100%" x2="0%" y1="0%" y2="98.616%"><stop offset="0%" stop-color="#797C79"/><stop offset="100%" stop-color="#545554"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path fill="#FFF" fill-opacity=".383" d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z"/><path fill="#393939" d="M32.25 69H4c-2 0-4-1-4-4V39.181L19 20h32v6.208l1.992 12.632L51 41.123V50L32.25 69z" opacity=".324"/><path fill="#000" fill-opacity=".383" d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z"/><path fill="#000" fill-opacity=".3" d="M51 22H19v3.75h32V22zm2 18.75V37l-2-9.375H19L17 37v3.75h2V52h20V40.75h8V52h4V40.75h2zm-18 7.5H23v-7.5h12v7.5z"/><path fill="#FFF" d="M51 20H19v3.75h32V20zm2 18.75V35l-2-9.375H19L17 35v3.75h2V50h20V38.75h8V50h4V38.75h2zm-18 7.5H23v-7.5h12v7.5z"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
|
@ -0,0 +1,160 @@
|
|||
/* from http://www.movable-type.co.uk/scripts/sha1.html */
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
/* SHA-1 implementation in JavaScript (c) Chris Veness 2002-2014 / MIT Licence */
|
||||
/* */
|
||||
/* - see http://csrc.nist.gov/groups/ST/toolkit/secure_hashing.html */
|
||||
/* http://csrc.nist.gov/groups/ST/toolkit/examples.html */
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
(function (_window) {
|
||||
/* jshint node:true *//* global define, escape, unescape */
|
||||
'use strict';
|
||||
|
||||
|
||||
/**
|
||||
* SHA-1 hash function reference implementation.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
var Sha1 = {};
|
||||
_window.Sha1 = Sha1;
|
||||
|
||||
/**
|
||||
* Generates SHA-1 hash of string.
|
||||
*
|
||||
* @param {string} msg - (Unicode) string to be hashed.
|
||||
* @returns {string} Hash of msg as hex character string.
|
||||
*/
|
||||
Sha1.hash = function(msg) {
|
||||
// convert string to UTF-8, as SHA only deals with byte-streams
|
||||
msg = msg.utf8Encode();
|
||||
|
||||
// constants [§4.2.1]
|
||||
var K = [ 0x5a827999, 0x6ed9eba1, 0x8f1bbcdc, 0xca62c1d6 ];
|
||||
|
||||
// PREPROCESSING
|
||||
|
||||
msg += String.fromCharCode(0x80); // add trailing '1' bit (+ 0's padding) to string [§5.1.1]
|
||||
|
||||
// convert string msg into 512-bit/16-integer blocks arrays of ints [§5.2.1]
|
||||
var l = msg.length/4 + 2; // length (in 32-bit integers) of msg + ‘1’ + appended length
|
||||
var N = Math.ceil(l/16); // number of 16-integer-blocks required to hold 'l' ints
|
||||
var M = new Array(N);
|
||||
|
||||
for (var i=0; i<N; i++) {
|
||||
M[i] = new Array(16);
|
||||
for (var j=0; j<16; j++) { // encode 4 chars per integer, big-endian encoding
|
||||
M[i][j] = (msg.charCodeAt(i*64+j*4)<<24) | (msg.charCodeAt(i*64+j*4+1)<<16) |
|
||||
(msg.charCodeAt(i*64+j*4+2)<<8) | (msg.charCodeAt(i*64+j*4+3));
|
||||
} // note running off the end of msg is ok 'cos bitwise ops on NaN return 0
|
||||
}
|
||||
// add length (in bits) into final pair of 32-bit integers (big-endian) [§5.1.1]
|
||||
// note: most significant word would be (len-1)*8 >>> 32, but since JS converts
|
||||
// bitwise-op args to 32 bits, we need to simulate this by arithmetic operators
|
||||
M[N-1][14] = ((msg.length-1)*8) / Math.pow(2, 32); M[N-1][14] = Math.floor(M[N-1][14]);
|
||||
M[N-1][15] = ((msg.length-1)*8) & 0xffffffff;
|
||||
|
||||
// set initial hash value [§5.3.1]
|
||||
var H0 = 0x67452301;
|
||||
var H1 = 0xefcdab89;
|
||||
var H2 = 0x98badcfe;
|
||||
var H3 = 0x10325476;
|
||||
var H4 = 0xc3d2e1f0;
|
||||
|
||||
// HASH COMPUTATION [§6.1.2]
|
||||
|
||||
var W = new Array(80); var a, b, c, d, e;
|
||||
for (var i=0; i<N; i++) {
|
||||
|
||||
// 1 - prepare message schedule 'W'
|
||||
for (var t=0; t<16; t++) W[t] = M[i][t];
|
||||
for (var t=16; t<80; t++) W[t] = Sha1.ROTL(W[t-3] ^ W[t-8] ^ W[t-14] ^ W[t-16], 1);
|
||||
|
||||
// 2 - initialise five working variables a, b, c, d, e with previous hash value
|
||||
a = H0; b = H1; c = H2; d = H3; e = H4;
|
||||
|
||||
// 3 - main loop
|
||||
for (var t=0; t<80; t++) {
|
||||
var s = Math.floor(t/20); // seq for blocks of 'f' functions and 'K' constants
|
||||
var T = (Sha1.ROTL(a,5) + Sha1.f(s,b,c,d) + e + K[s] + W[t]) & 0xffffffff;
|
||||
e = d;
|
||||
d = c;
|
||||
c = Sha1.ROTL(b, 30);
|
||||
b = a;
|
||||
a = T;
|
||||
}
|
||||
|
||||
// 4 - compute the new intermediate hash value (note 'addition modulo 2^32')
|
||||
H0 = (H0+a) & 0xffffffff;
|
||||
H1 = (H1+b) & 0xffffffff;
|
||||
H2 = (H2+c) & 0xffffffff;
|
||||
H3 = (H3+d) & 0xffffffff;
|
||||
H4 = (H4+e) & 0xffffffff;
|
||||
}
|
||||
|
||||
return Sha1.toHexStr(H0) + Sha1.toHexStr(H1) + Sha1.toHexStr(H2) +
|
||||
Sha1.toHexStr(H3) + Sha1.toHexStr(H4);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Function 'f' [§4.1.1].
|
||||
* @private
|
||||
*/
|
||||
Sha1.f = function(s, x, y, z) {
|
||||
switch (s) {
|
||||
case 0: return (x & y) ^ (~x & z); // Ch()
|
||||
case 1: return x ^ y ^ z; // Parity()
|
||||
case 2: return (x & y) ^ (x & z) ^ (y & z); // Maj()
|
||||
case 3: return x ^ y ^ z; // Parity()
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Rotates left (circular left shift) value x by n positions [§3.2.5].
|
||||
* @private
|
||||
*/
|
||||
Sha1.ROTL = function(x, n) {
|
||||
return (x<<n) | (x>>>(32-n));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Hexadecimal representation of a number.
|
||||
* @private
|
||||
*/
|
||||
Sha1.toHexStr = function(n) {
|
||||
// note can't use toString(16) as it is implementation-dependant,
|
||||
// and in IE returns signed numbers when used on full words
|
||||
var s="", v;
|
||||
for (var i=7; i>=0; i--) { v = (n>>>(i*4)) & 0xf; s += v.toString(16); }
|
||||
return s;
|
||||
};
|
||||
|
||||
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
|
||||
|
||||
/** Extend String object with method to encode multi-byte string to utf8
|
||||
* - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html */
|
||||
if (typeof String.prototype.utf8Encode == 'undefined') {
|
||||
String.prototype.utf8Encode = function() {
|
||||
return unescape( encodeURIComponent( this ) );
|
||||
};
|
||||
}
|
||||
|
||||
/** Extend String object with method to decode utf8 string to multi-byte */
|
||||
if (typeof String.prototype.utf8Decode == 'undefined') {
|
||||
String.prototype.utf8Decode = function() {
|
||||
try {
|
||||
return decodeURIComponent( escape( this ) );
|
||||
} catch (e) {
|
||||
return this; // invalid UTF-8? return as-is
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
|
||||
if (typeof module != 'undefined' && module.exports) module.exports = Sha1; // CommonJs export
|
||||
if (typeof define == 'function' && define.amd) define([], function() { return Sha1; }); // AMD
|
||||
})(window)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
// http://stackoverflow.com/questions/4383226/using-jquery-to-know-when-font-face-fonts-are-loaded
|
||||
(function(){
|
||||
function waitForWebfonts(fonts, callback) {
|
||||
var loadedFonts = 0;
|
||||
for(var i = 0, l = fonts.length; i < l; ++i) {
|
||||
(function(font) {
|
||||
var node = document.createElement('span');
|
||||
// Characters that vary significantly among different fonts
|
||||
node.innerHTML = 'giItT1WQy@!-/#';
|
||||
// Visible - so we can measure it - but not on the screen
|
||||
node.style.position = 'absolute';
|
||||
node.style.left = '-10000px';
|
||||
node.style.top = '-10000px';
|
||||
// Large font size makes even subtle changes obvious
|
||||
node.style.fontSize = '300px';
|
||||
// Reset any font properties
|
||||
node.style.fontFamily = 'sans-serif';
|
||||
node.style.fontVariant = 'normal';
|
||||
node.style.fontStyle = 'normal';
|
||||
node.style.fontWeight = 'normal';
|
||||
node.style.letterSpacing = '0';
|
||||
document.body.appendChild(node);
|
||||
|
||||
// Remember width with no applied web font
|
||||
var width = node.offsetWidth;
|
||||
|
||||
node.style.fontFamily = font;
|
||||
|
||||
var interval;
|
||||
function checkFont() {
|
||||
// Compare current width with original width
|
||||
if(node && node.offsetWidth != width) {
|
||||
++loadedFonts;
|
||||
node.parentNode.removeChild(node);
|
||||
node = null;
|
||||
}
|
||||
|
||||
// If all fonts have been loaded
|
||||
if(loadedFonts >= fonts.length) {
|
||||
if(interval) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
if(loadedFonts == fonts.length) {
|
||||
callback();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if(!checkFont()) {
|
||||
interval = setInterval(checkFont, 50);
|
||||
}
|
||||
})(fonts[i]);
|
||||
}
|
||||
}
|
||||
window.waitForWebfonts = waitForWebfonts;
|
||||
})();
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
function runPoSJSTests({ env }) {
|
||||
return {
|
||||
type: "item",
|
||||
description: env._t("Run Point of Sale JS Tests"),
|
||||
callback: () => {
|
||||
env.services.action.doAction({
|
||||
name: env._t("JS Tests"),
|
||||
target: "new",
|
||||
type: "ir.actions.act_url",
|
||||
url: "/pos/ui/tests?debug=assets",
|
||||
});
|
||||
},
|
||||
sequence: 35,
|
||||
};
|
||||
}
|
||||
|
||||
registry.category("debug").category("default").add("point_of_sale.runPoSJSTests", runPoSJSTests);
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
odoo.define('point_of_sale.tour', function(require) {
|
||||
"use strict";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('point_of_sale_tour', {
|
||||
url: "/web",
|
||||
rainbowMan: false,
|
||||
sequence: 45,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
|
||||
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
|
||||
width: 215,
|
||||
position: 'right',
|
||||
edition: 'community'
|
||||
}, {
|
||||
trigger: '.o_app[data-menu-xmlid="point_of_sale.menu_point_root"]',
|
||||
content: Markup(_t("Ready to launch your <b>point of sale</b>?")),
|
||||
width: 215,
|
||||
position: 'bottom',
|
||||
edition: 'enterprise'
|
||||
}, {
|
||||
trigger: ".o_pos_kanban button.oe_kanban_action_button",
|
||||
content: Markup(_t("<p>Ready to have a look at the <b>POS Interface</b>? Let's start our first session.</p>")),
|
||||
position: "bottom"
|
||||
}]);
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.pos .screen .content-cell{
|
||||
height: 100%;
|
||||
}
|
||||
.pos .subwindow .subwindow-container{
|
||||
height: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,726 @@
|
|||
@keyframes item_in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
margin-top: -30px;
|
||||
}
|
||||
50% {
|
||||
margin-top: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes item_in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
margin-top: -30px;
|
||||
}
|
||||
50% {
|
||||
margin-top: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: geometricPrecision;
|
||||
font-smooth: always;
|
||||
}
|
||||
body .pos-customer_facing_display {
|
||||
background-color: #f6f6f6;
|
||||
font-size: 2vw;
|
||||
font-family: Futura, HelveticaNeue, Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
font-weight: 300;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-moz-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
-o-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products,
|
||||
body .pos-customer_facing_display .pos-payment_info {
|
||||
height: 100%;
|
||||
padding: 2%;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-moz-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
-o-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-moz-box-flex: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: column;
|
||||
-moz-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
-o-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_list {
|
||||
overflow-y: scroll;
|
||||
padding-right: 1.5vw;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item {
|
||||
margin-bottom: 1vw;
|
||||
padding: 1%;
|
||||
border-radius: 0.3vw;
|
||||
height: auto;
|
||||
-webkit-box-flex: 0 1 auto;
|
||||
-webkit-flex: 0 1 auto;
|
||||
-moz-box-flex: 0 1 auto;
|
||||
-ms-flex: 0 1 auto;
|
||||
flex: 0 1 auto;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
-webkit-flex-direction: row;
|
||||
-moz-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
-o-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
-ms-grid-row-align: center;
|
||||
align-items: center;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item:last-of-type {
|
||||
animation: item_in 1s ease;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div, body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
font-weight: normal;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
text-align: left;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div {
|
||||
width: 5%;
|
||||
text-align: left;
|
||||
margin-right: 4%;
|
||||
font-size: 80%;
|
||||
-webkit-box-flex: 1;
|
||||
-webkit-flex-grow: 1;
|
||||
-moz-box-flex: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div:first-child {
|
||||
margin-right: 2%;
|
||||
-webkit-box-flex: 1 1 1%;
|
||||
-webkit-flex: 1 1 1%;
|
||||
-moz-box-flex: 1 1 1%;
|
||||
-ms-flex: 1 1 1%;
|
||||
flex: 1 1 1%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div:nth-child(2) {
|
||||
width: 40%;
|
||||
border-left: 1px solid;
|
||||
padding-left: 2%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div:nth-child(3) {
|
||||
text-align: center;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div:last-child {
|
||||
margin-right: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item > div div {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
padding-top: 75%;
|
||||
display: block;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info {
|
||||
max-width: 30%;
|
||||
padding: 2% 2% 1% 2%;
|
||||
-webkit-flex-direction: column;
|
||||
-moz-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
-o-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-pack: space-between;
|
||||
-webkit-justify-content: space-between;
|
||||
-moz-box-pack: space-between;
|
||||
-ms-flex-pack: space-between;
|
||||
justify-content: space-between;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-adv,
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
background-position: center top;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-adv[style*="url(http://placehold.it"],
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo[style*="url(http://placehold.it"] {
|
||||
background-color: #ccc;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
background-image: url(/logo);
|
||||
margin-bottom: 10%;
|
||||
-webkit-box-flex: 0 0 20%;
|
||||
-webkit-flex: 0 0 20%;
|
||||
-moz-box-flex: 0 0 20%;
|
||||
-ms-flex: 0 0 20%;
|
||||
flex: 0 0 20%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-adv {
|
||||
margin-bottom: 5%;
|
||||
border-bottom: 10px solid transparent;
|
||||
box-shadow: 0 1px rgba(246, 246, 246, 0.2);
|
||||
-webkit-box-flex: 1 1 60%;
|
||||
-webkit-flex: 1 1 60%;
|
||||
-moz-box-flex: 1 1 60%;
|
||||
-ms-flex: 1 1 60%;
|
||||
flex: 1 1 60%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total,
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-paymentlines {
|
||||
-webkit-flex-direction: row;
|
||||
-moz-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
-o-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
-webkit-flex-wrap: wrap;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
-webkit-box-pack: space-between;
|
||||
-webkit-justify-content: space-between;
|
||||
-moz-box-pack: space-between;
|
||||
-ms-flex-pack: space-between;
|
||||
justify-content: space-between;
|
||||
-webkit-box-align: baseline;
|
||||
-webkit-align-items: baseline;
|
||||
-moz-box-align: baseline;
|
||||
-ms-flex-align: baseline;
|
||||
-ms-grid-row-align: baseline;
|
||||
align-items: baseline;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total > div,
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-paymentlines > div {
|
||||
-webkit-box-flex: 1 0 48%;
|
||||
-webkit-flex: 1 0 48%;
|
||||
-moz-box-flex: 1 0 48%;
|
||||
-ms-flex: 1 0 48%;
|
||||
flex: 1 0 48%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total > div:nth-child(even),
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-paymentlines > div:nth-child(even) {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
margin-right: 0;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total {
|
||||
font-size: 2vw;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-paymentlines {
|
||||
margin-top: 2%;
|
||||
font-size: 1.5vw;
|
||||
line-height: 1.3;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-odoo_logo_container {
|
||||
background-image: url(/web/static/img/logo_inverse_white_206px.png);
|
||||
background-position: right top;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
height: 1.5vw;
|
||||
margin-top: 10%;
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
body .pos-customer_facing_display {
|
||||
font-size: 2vh;
|
||||
height: 100%;
|
||||
-webkit-flex-direction: column;
|
||||
-moz-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
-o-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
body .pos-customer_facing_display:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 17vh;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-adv {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 15vh;
|
||||
width: 99vw;
|
||||
margin: 0.5vh;
|
||||
border-width: 0;
|
||||
-webkit-display: flex;
|
||||
-moz-display: flex;
|
||||
-ms-display: flex;
|
||||
-o-display: flex;
|
||||
display: flex;
|
||||
}
|
||||
body .pos-customer_facing_display.pos-js_no_ADV:before {
|
||||
display: none;
|
||||
}
|
||||
body .pos-customer_facing_display.pos-js_no_ADV .pos-customer_products {
|
||||
padding-top: 0;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products {
|
||||
padding-top: 17vh;
|
||||
height: 72vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines {
|
||||
-webkit-box-flex: 1 0 auto;
|
||||
-webkit-flex: 1 0 auto;
|
||||
-moz-box-flex: 1 0 auto;
|
||||
-ms-flex: 1 0 auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_item > div:nth-child(2) {
|
||||
width: 30%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_item.pos_orderlines_header div {
|
||||
font-size: 90%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_list {
|
||||
padding-right: 1.5vh;
|
||||
height: auto;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_list .pos_orderlines_item {
|
||||
box-shadow: 0 0.1vh 0.1vh #dddddd;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_list .pos_orderlines_item > div {
|
||||
font-size: 100%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
padding-top: 0;
|
||||
min-height: 120px;
|
||||
-webkit-box-flex: 0 1 23vw;
|
||||
-webkit-flex: 0 1 23vw;
|
||||
-moz-box-flex: 0 1 23vw;
|
||||
-ms-flex: 0 1 23vw;
|
||||
flex: 0 1 23vw;
|
||||
-webkit-flex-direction: row;
|
||||
-moz-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
-o-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-box-align: center;
|
||||
-webkit-align-items: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
-ms-grid-row-align: center;
|
||||
align-items: center;
|
||||
-webkit-box-pack: space-between;
|
||||
-webkit-justify-content: space-between;
|
||||
-moz-box-pack: space-between;
|
||||
-ms-flex-pack: space-between;
|
||||
justify-content: space-between;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
margin: 0;
|
||||
background-position: left center;
|
||||
margin-right: 5%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
-webkit-box-flex: 1 1 20%;
|
||||
-webkit-flex: 1 1 20%;
|
||||
-moz-box-flex: 1 1 20%;
|
||||
-ms-flex: 1 1 20%;
|
||||
flex: 1 1 20%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details {
|
||||
-webkit-box-flex: 0 1 50%;
|
||||
-webkit-flex: 0 1 50%;
|
||||
-moz-box-flex: 0 1 50%;
|
||||
-ms-flex: 0 1 50%;
|
||||
flex: 0 1 50%;
|
||||
-webkit-flex-direction: column;
|
||||
-moz-flex-direction: column;
|
||||
-ms-flex-direction: column;
|
||||
-o-flex-direction: column;
|
||||
flex-direction: column;
|
||||
min-width: 170px;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total {
|
||||
font-size: 3vw;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total .pos_total-amount {
|
||||
font-size: 3.5vw;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-paymentlines {
|
||||
margin-top: 2%;
|
||||
font-size: 80%;
|
||||
line-height: 1.2;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-odoo_logo_container {
|
||||
position: absolute;
|
||||
right: 3%;
|
||||
bottom: 1%;
|
||||
}
|
||||
}
|
||||
@media all and (orientation: portrait) and (max-width: 340px) {
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_list {
|
||||
padding-right: 0;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_list .pos_orderlines_item > div {
|
||||
font-size: 70%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_header > div {
|
||||
font-size: 60%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-customer_products .pos_orderlines .pos_orderlines_header > div:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
display: none !important;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details {
|
||||
-webkit-box-flex: 1 0 100%;
|
||||
-webkit-flex: 1 0 100%;
|
||||
-moz-box-flex: 1 0 100%;
|
||||
-ms-flex: 1 0 100%;
|
||||
flex: 1 0 100%;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total {
|
||||
font-size: 6vw;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .pos-total .pos_total-amount {
|
||||
font-size: 6.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
body .pos-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.pos-palette_01 .pos-payment_info {
|
||||
background: #3E3E3E;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_01 .pos-customer_products {
|
||||
background: #f6f6f6;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_01 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #aaaaaa;
|
||||
}
|
||||
.pos-palette_01 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_01:before {
|
||||
background: #3E3E3E;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_02 .pos-payment_info {
|
||||
background: #364152;
|
||||
color: #e6e7e8;
|
||||
}
|
||||
.pos-palette_02 .pos-customer_products {
|
||||
background: #ecf2f6;
|
||||
color: #364152;
|
||||
}
|
||||
.pos-palette_02 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #364152;
|
||||
}
|
||||
.pos-palette_02 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_02:before {
|
||||
background: #364152;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_03 .pos-payment_info {
|
||||
background: #1BA39C;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_03 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_03 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_03 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_03:before {
|
||||
background: #1BA39C;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_04 .pos-payment_info {
|
||||
background: #0b7b6c;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_04 .pos-customer_products {
|
||||
background: #efeeec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_04 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a9a499;
|
||||
}
|
||||
.pos-palette_04 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_04:before {
|
||||
background: #0b7b6c;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_05 .pos-payment_info {
|
||||
background: #E26868;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_05 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_05 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_05 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_05:before {
|
||||
background: #E26868;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_06 .pos-payment_info {
|
||||
background: #9E373B;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_06 .pos-customer_products {
|
||||
background: #f6f6f6;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_06 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #aaaaaa;
|
||||
}
|
||||
.pos-palette_06 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_06:before {
|
||||
background: #9E373B;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_07 .pos-payment_info {
|
||||
background: #ce9934;
|
||||
color: white;
|
||||
}
|
||||
.pos-palette_07 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_07 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_07 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_07:before {
|
||||
background: #ce9934;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_08 .pos-payment_info {
|
||||
background: #a48c77;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_08 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_08 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_08 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_08:before {
|
||||
background: #a48c77;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_09 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #014d43, #127e71);
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_09 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_09 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_09 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_09:before {
|
||||
background: linear-gradient(30deg, #014d43, #127e71);
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_10 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #e2316c, #ea4c89);
|
||||
color: white;
|
||||
}
|
||||
.pos-palette_10 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_10 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_10 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_10:before {
|
||||
background: linear-gradient(30deg, #e2316c, #ea4c89);
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_11 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #362b3d, #5b4a63);
|
||||
color: white;
|
||||
}
|
||||
.pos-palette_11 .pos-customer_products {
|
||||
background: #ececec;
|
||||
color: #585858;
|
||||
}
|
||||
.pos-palette_11 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: white;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #a0a0a0;
|
||||
}
|
||||
.pos-palette_11 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_11:before {
|
||||
background: linear-gradient(30deg, #362b3d, #5b4a63);
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_12 .pos-payment_info {
|
||||
background: #434343;
|
||||
color: #e6e6e6;
|
||||
}
|
||||
.pos-palette_12 .pos-customer_products {
|
||||
background: #5b5b5b;
|
||||
color: #bdb9b9;
|
||||
}
|
||||
.pos-palette_12 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: #f5f5f5;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #0f0f0f;
|
||||
}
|
||||
.pos-palette_12 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_12:before {
|
||||
background: #434343;
|
||||
}
|
||||
}
|
||||
|
||||
.pos-palette_13 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #1a1b1f, #3d3f45);
|
||||
color: white;
|
||||
}
|
||||
.pos-palette_13 .pos-customer_products {
|
||||
background: #a2a2ab;
|
||||
color: #f6f6f6;
|
||||
}
|
||||
.pos-palette_13 .pos-customer_products .pos_orderlines_list .pos_orderlines_item {
|
||||
background-color: #f6f6f6;
|
||||
color: #3E3E3E;
|
||||
box-shadow: 0 0.1vh 0.1vh #55555f;
|
||||
}
|
||||
.pos-palette_13 .pos-customer_products .pos_orderlines_list .pos_orderlines_item div:nth-child(2) {
|
||||
border-color: rgba(62, 62, 62, 0.3);
|
||||
}
|
||||
@media all and (orientation: portrait) {
|
||||
.pos-palette_13:before {
|
||||
background: linear-gradient(30deg, #1a1b1f, #3d3f45);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.pos .opening-cash-control .body{
|
||||
margin: 40px;
|
||||
}
|
||||
|
||||
.pos .opening-cash-control .opening-cash-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 40px 0;
|
||||
}
|
||||
|
||||
.pos .opening-cash-section .info-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pos .opening-cash-section .cash-input-sub-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pos .cash-input-sub-section .pos-input {
|
||||
width: 85px;
|
||||
}
|
||||
|
||||
.pos .cash-input-sub-section .button.icon {
|
||||
margin: 0;
|
||||
float: unset;
|
||||
}
|
||||
|
||||
.pos .opening-cash-control .opening-cash-notes {
|
||||
font-style: italic;
|
||||
font-weight: 350;
|
||||
width: calc(100% - 20px); /* textarea has a padding of 10px */
|
||||
line-height: 20px;
|
||||
resize: none;
|
||||
height: 150px;
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
.pos .close-pos-popup {
|
||||
max-width: 800px !important;
|
||||
max-height: 800px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .body{
|
||||
margin: 0;
|
||||
padding: 3% 5%;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: auto;
|
||||
height: 65px;
|
||||
background-color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .title,
|
||||
.pos .close-pos-popup .total-orders {
|
||||
padding: 2.5%;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .total-orders .amount {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.notes-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
padding-top: 1%;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .opening-notes {
|
||||
color: darkgrey;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
min-width: 40%;
|
||||
padding: 5px 10px;
|
||||
border-left: solid 3px darkgray;
|
||||
overflow-y: auto;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .closing-notes {
|
||||
font-style: italic;
|
||||
font-weight: 350;
|
||||
box-sizing: border-box;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
min-width: 60%;
|
||||
min-height: 100px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview {
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview table {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview table tr {
|
||||
height: 35px
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup th:nth-child(1),
|
||||
.pos .close-pos-popup td:nth-child(1) {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup th:nth-child(2),
|
||||
.pos .close-pos-popup td:nth-child(2) {
|
||||
text-align: right;
|
||||
padding-right: 5%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup th:nth-child(3),
|
||||
.pos .close-pos-popup td:nth-child(3) {
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup th:nth-child(4),
|
||||
.pos .close-pos-popup td:nth-child(4) {
|
||||
text-align: left;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.pos .close-pos-popup .payment-methods-overview table .pos-input {
|
||||
width: 75%;
|
||||
text-align: right;
|
||||
padding-right: 7%;
|
||||
margin-right: 3%;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview table .warning {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .body .button.icon {
|
||||
margin: 0;
|
||||
float: unset;
|
||||
margin-right: 0 20px 0 20px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview table .cash-sign {
|
||||
width: 10px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview .cash-overview {
|
||||
border-left: solid 2px #555555;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .payment-methods-overview .cash-overview tr td:first-child {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.pos .close-pos-popup .invisible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .accept-closing label.disabled {
|
||||
cursor: default;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer {
|
||||
box-sizing: border-box;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .button {
|
||||
float: left;
|
||||
width: 16%;
|
||||
min-width: 95px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.pos .close-pos-popup {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: unset;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .button {
|
||||
width: 90% !important;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .button:last-of-type {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .small.button {
|
||||
float: right;
|
||||
width: 6%;
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .button.disabled {
|
||||
cursor: default;
|
||||
background: bottom;
|
||||
color: unset;
|
||||
border: solid 1px rgba(60, 60, 60, 0.1);
|
||||
}
|
||||
|
||||
.pos .close-pos-popup .footer .button.disabled:active {
|
||||
border: solid 1px rgba(60, 60, 60, 0.1);
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/* Input style used in cash control popups */
|
||||
.pos .popup .pos-input {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #555555;
|
||||
background: none;
|
||||
min-height: 0;
|
||||
border-radius: unset;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
width: 50px;
|
||||
margin-right: 15px;
|
||||
border-bottom: solid 2px;
|
||||
}
|
||||
|
||||
.pos .popup .pos-input:focus {
|
||||
box-shadow: none ;
|
||||
border-color: blue;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pos .popup .invalid-cash-input {
|
||||
color: red;
|
||||
animation: blink 0.5s linear;
|
||||
animation-iteration-count: 2;
|
||||
border: 1px solid red;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.pos .popup .invalid-cash-input:focus {
|
||||
border-color: red;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% {
|
||||
border-color: red;
|
||||
}
|
||||
50% {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
.pos .popup.money-details {
|
||||
width: 350px;
|
||||
max-height: 440px;
|
||||
}
|
||||
|
||||
.pos .money-details.invisible {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pos .money-details .body {
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.pos .money-details .money-details-title {
|
||||
text-align: left;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pos .money-details .money-details-info {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: auto;
|
||||
max-height: 280px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.pos .money-details .money-details-value {
|
||||
display: flex;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.pos .money-details .total-section {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
.product-info-popup {
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
.product-info-popup .body {
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title .global-info-title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title div:first-child{
|
||||
max-width: 60%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title .global-info-title.product-name {
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.product-info-popup .subsection-list {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.product-info-popup .subsection-list .subsection-list-value {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.product-info-popup .column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-info-popup .flex-start {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.product-info-popup .flex-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.product-info-popup .section-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-top: 30px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.product-info-popup .section-title-line {
|
||||
width: 100%;
|
||||
border-top: solid black 3px;
|
||||
opacity: 25%;
|
||||
margin: 15px 10px 0 10px;
|
||||
}
|
||||
|
||||
.product-info-popup table {
|
||||
text-align: left;
|
||||
}
|
||||
.product-info-popup .section-financials-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.product-info-popup .section-financials-body table {
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-financials-body table td {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-inventory-body table td {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.product-info-popup .section-supplier-body table {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-supplier-body table div{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.product-info-popup .table-value {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.product-info-popup .searchable {
|
||||
color: blue;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.product-info-popup .searchable:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.product-info-popup .searchable:active {
|
||||
color: darkblue;
|
||||
}
|
||||
|
||||
.product-info-popup .section-order-body table {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.product-info-popup .body {
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title div:first-child{
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title .global-info-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.product-info-popup .section-product-info-title {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-info-popup .flex-end {
|
||||
align-items: unset;
|
||||
}
|
||||
|
||||
.product-info-popup .section-financials-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-info-popup .section-financials-body table {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.product-info-popup .mobile-table tr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.product-info-popup .mobile-table .table-name:before {
|
||||
content: "- ";
|
||||
}
|
||||
|
||||
.product-info-popup .mobile-table td:not(:nth-child(1)) {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.product-info-popup .mobile-line {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.product-info-popup .mobile-line td {
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.product-info-popup .section-supplier-body table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.product-info-popup .section-supplier-body table tr {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.product-info-popup .section-variants-body .table-value {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.product-info-popup .button.cancel {
|
||||
float: unset;
|
||||
margin: 10px auto;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
.pos-receipt-print {
|
||||
width: 512px;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
font-size: 27px;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-right-align {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-center-align {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-left-padding {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-logo {
|
||||
width: 50%;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-qrcode {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-contact {
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-order-data {
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-amount {
|
||||
font-size: 125%;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-title {
|
||||
font-weight: bold;
|
||||
font-size: 125%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-header {
|
||||
font-size: 125%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-order-receipt-cancel {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-customer-note {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pos-payment-terminal-receipt {
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.responsive-price {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-price > .pos-receipt-right-align {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.pos-receipt .pos-receipt-taxes {
|
||||
display: grid;
|
||||
grid-template-columns: min-content min-content auto auto auto;
|
||||
width: 100%;
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.pos-receipt-qty-per-price {
|
||||
/*rtl:ignore*/
|
||||
direction: ltr;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import Chrome from "point_of_sale.Chrome";
|
||||
import ProductScreen from "point_of_sale.ProductScreen";
|
||||
import Registries from "point_of_sale.Registries";
|
||||
import { PosGlobalState } from "point_of_sale.models";
|
||||
import { configureGui } from "point_of_sale.Gui";
|
||||
import { registry } from "@web/core/registry";
|
||||
import env from "point_of_sale.env";
|
||||
import { debounce } from "@web/core/utils/timing";
|
||||
import { batched } from "point_of_sale.utils";
|
||||
|
||||
const { Component, reactive, markRaw, useExternalListener, useSubEnv, onWillUnmount, xml } = owl;
|
||||
|
||||
export class ChromeAdapter extends Component {
|
||||
setup() {
|
||||
this.PosChrome = Registries.Component.get(Chrome);
|
||||
ProductScreen.sortControlButtons();
|
||||
const legacyActionManager = useService("legacy_action_manager");
|
||||
|
||||
// Instantiate PosGlobalState here to ensure that every extension
|
||||
// (or class overloads) is taken into consideration.
|
||||
const pos = PosGlobalState.create({ env: markRaw(env) });
|
||||
|
||||
this.batchedCustomerDisplayRender = batched(() => {
|
||||
reactivePos.send_current_order_to_customer_facing_display();
|
||||
});
|
||||
const reactivePos = reactive(pos, this.batchedCustomerDisplayRender);
|
||||
env.pos = reactivePos;
|
||||
env.legacyActionManager = legacyActionManager;
|
||||
|
||||
// The proxy requires the instance of PosGlobalState to function properly.
|
||||
env.proxy.set_pos(reactivePos);
|
||||
|
||||
// TODO: Should we continue on exposing posmodel as global variable?
|
||||
// Expose only the reactive version of `pos` when in debug mode.
|
||||
window.posmodel = pos.debug ? reactivePos : pos;
|
||||
|
||||
this.env = env;
|
||||
this.__owl__.childEnv = env;
|
||||
useSubEnv({
|
||||
get isMobile() {
|
||||
return window.innerWidth <= 768;
|
||||
},
|
||||
});
|
||||
let currentIsMobile = this.env.isMobile;
|
||||
const updateUI = debounce(() => {
|
||||
if (this.env.isMobile !== currentIsMobile) {
|
||||
currentIsMobile = this.env.isMobile;
|
||||
this.render(true);
|
||||
}
|
||||
}, 15);
|
||||
useExternalListener(window, "resize", updateUI);
|
||||
onWillUnmount(updateUI.cancel);
|
||||
}
|
||||
|
||||
async configureAndStart(chrome) {
|
||||
// Add the pos error handler when the chrome component is available.
|
||||
registry.category('error_handlers').add(
|
||||
'posErrorHandler',
|
||||
(env, ...noEnvArgs) => {
|
||||
if (chrome) {
|
||||
return chrome.errorHandler(this.env, ...noEnvArgs);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
{ sequence: 0 }
|
||||
);
|
||||
// Little trick to avoid displaying the block ui during the POS models loading
|
||||
const BlockUiFromRegistry = registry.category("main_components").get("BlockUI");
|
||||
registry.category("main_components").remove("BlockUI");
|
||||
configureGui({ component: chrome });
|
||||
await chrome.start();
|
||||
registry.category("main_components").add("BlockUI", BlockUiFromRegistry);
|
||||
|
||||
// Subscribe to the changes in the models.
|
||||
this.batchedCustomerDisplayRender();
|
||||
}
|
||||
}
|
||||
ChromeAdapter.template = xml`<t t-component="PosChrome" setupIsDone.bind="configureAndStart"/>`;
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { startWebClient } from "@web/start";
|
||||
|
||||
import { ChromeAdapter } from "@point_of_sale/entry/chrome_adapter";
|
||||
import Registries from "point_of_sale.Registries";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
const { Component, xml } = owl;
|
||||
|
||||
// For consistency's sake, we should trigger"WEB_CLIENT_READY" on the bus when PosApp is mounted
|
||||
// But we can't since mail and some other poll react on that cue, and we don't want those services started
|
||||
class PosApp extends Component {
|
||||
setup() {
|
||||
this.Components = registry.category("main_components").getEntries();
|
||||
}
|
||||
}
|
||||
PosApp.template = xml`
|
||||
<body>
|
||||
<ChromeAdapter />
|
||||
<div>
|
||||
<t t-foreach="Components" t-as="C" t-key="C[0]">
|
||||
<t t-component="C[1].Component" t-props="C[1].props"/>
|
||||
</t>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
PosApp.components = { ChromeAdapter };
|
||||
|
||||
function startPosApp() {
|
||||
Registries.Component.freeze();
|
||||
Registries.Model.freeze();
|
||||
startWebClient(PosApp);
|
||||
}
|
||||
|
||||
startPosApp();
|
||||
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 361 B |
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 522 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 357 B |
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 7 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1,199 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg3162"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.3.1 r9886"
|
||||
width="152"
|
||||
height="152"
|
||||
sodipodi:docname="ios7-icon.png"
|
||||
inkscape:export-filename="/home/fva/Code/openerp/point_of_sale/touch-icon-ipad-retina.png"
|
||||
inkscape:export-xdpi="90"
|
||||
inkscape:export-ydpi="90">
|
||||
<metadata
|
||||
id="metadata3168">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3166">
|
||||
<linearGradient
|
||||
id="linearGradient3944">
|
||||
<stop
|
||||
style="stop-color:#483c98;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop3946" />
|
||||
<stop
|
||||
style="stop-color:#8075c9;stop-opacity:1;"
|
||||
offset="1"
|
||||
id="stop3948" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient3944"
|
||||
id="linearGradient3950"
|
||||
x1="116.83051"
|
||||
y1="0.49999994"
|
||||
x2="115.35169"
|
||||
y2="227.45763"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(0.67555556,0,0,0.67555556,0,-0.67555559)" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1111"
|
||||
id="namedview3164"
|
||||
showgrid="true"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="39.575132"
|
||||
inkscape:cy="237.57664"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="27"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg3162">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3942"
|
||||
empspacing="5"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true" />
|
||||
</sodipodi:namedview>
|
||||
<rect
|
||||
style="fill:url(#linearGradient3950);fill-opacity:1;fill-rule:evenodd;stroke:none"
|
||||
id="rect3172"
|
||||
width="152"
|
||||
height="152"
|
||||
x="0"
|
||||
y="-3.9968029e-15"
|
||||
ry="34.723557" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3952"
|
||||
width="5"
|
||||
height="80"
|
||||
x="25"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="35"
|
||||
height="80"
|
||||
width="3.0532093"
|
||||
id="rect3954"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3956"
|
||||
width="5.0000033"
|
||||
height="80"
|
||||
x="45"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="54.999996"
|
||||
height="80"
|
||||
width="3.0000036"
|
||||
id="rect3958"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3960"
|
||||
width="3.0000036"
|
||||
height="80"
|
||||
x="65"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="75"
|
||||
height="80"
|
||||
width="3.0000036"
|
||||
id="rect3962"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3964"
|
||||
width="5.0000057"
|
||||
height="80"
|
||||
x="80"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="90"
|
||||
height="80"
|
||||
width="3.0000036"
|
||||
id="rect3966"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="100.02039"
|
||||
height="80"
|
||||
width="5.0000057"
|
||||
id="rect3968"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3970"
|
||||
width="3.0000036"
|
||||
height="80"
|
||||
x="107"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
ry="1"
|
||||
y="37"
|
||||
x="114.99999"
|
||||
height="80"
|
||||
width="5.0000057"
|
||||
id="rect3972"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
||||
id="rect3974"
|
||||
width="3.0532093"
|
||||
height="80"
|
||||
x="122"
|
||||
y="37"
|
||||
ry="1" />
|
||||
<rect
|
||||
style="fill:#f80000;fill-opacity:1;stroke:none"
|
||||
id="rect3976"
|
||||
width="2.0000024"
|
||||
height="110"
|
||||
x="-103.85593"
|
||||
y="20"
|
||||
ry="1.375"
|
||||
transform="matrix(0,-1,1,0,0,0)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
|
|
@ -0,0 +1,517 @@
|
|||
odoo.define('point_of_sale.Chrome', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { loadCSS } = require('@web/core/assets');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const BarcodeParser = require('barcodes.BarcodeParser');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const IndependentToOrderScreen = require('point_of_sale.IndependentToOrderScreen');
|
||||
const { identifyError, batched } = require('point_of_sale.utils');
|
||||
const { odooExceptionTitleMap } = require("@web/core/errors/error_dialogs");
|
||||
const { ConnectionLostError, ConnectionAbortedError, RPCError } = require('@web/core/network/rpc_service');
|
||||
const { useBus } = require("@web/core/utils/hooks");
|
||||
const { debounce } = require("@web/core/utils/timing");
|
||||
const { Transition } = require("@web/core/transition");
|
||||
|
||||
const {
|
||||
onError,
|
||||
onMounted,
|
||||
onWillDestroy,
|
||||
useExternalListener,
|
||||
useRef,
|
||||
useState,
|
||||
useSubEnv,
|
||||
reactive,
|
||||
} = owl;
|
||||
|
||||
/**
|
||||
* Chrome is the root component of the PoS App.
|
||||
*/
|
||||
class Chrome extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useExternalListener(window, 'beforeunload', this._onBeforeUnload);
|
||||
useListener('show-main-screen', this.__showScreen);
|
||||
useListener('toggle-debug-widget', debounce(this._toggleDebugWidget, 100));
|
||||
useListener('toggle-mobile-searchbar', this._toggleMobileSearchBar);
|
||||
useListener('show-temp-screen', this.__showTempScreen);
|
||||
useListener('close-temp-screen', this.__closeTempScreen);
|
||||
useListener('close-pos', this._closePos);
|
||||
useListener('loading-skip-callback', () => this.env.proxy.stop_searching());
|
||||
useListener('play-sound', this._onPlaySound);
|
||||
useListener('set-sync-status', this._onSetSyncStatus);
|
||||
useListener('show-notification', this._onShowNotification);
|
||||
useListener('close-notification', this._onCloseNotification);
|
||||
useListener('connect-to-proxy', this.connect_to_proxy);
|
||||
useBus(this.env.posbus, 'start-cash-control', this.openCashControl);
|
||||
NumberBuffer.activate();
|
||||
|
||||
this.state = useState({
|
||||
uiState: 'LOADING', // 'LOADING' | 'READY' | 'CLOSING'
|
||||
debugWidgetIsShown: true,
|
||||
mobileSearchBarIsShown: false,
|
||||
hasBigScrollBars: false,
|
||||
sound: { src: null },
|
||||
notification: {
|
||||
isShown: false,
|
||||
message: '',
|
||||
duration: 2000,
|
||||
},
|
||||
loadingSkipButtonIsShown: 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;
|
||||
|
||||
const pos = reactive(this.env.pos, batched(() => this.render(true)))
|
||||
useSubEnv({ pos });
|
||||
|
||||
onMounted(() => {
|
||||
// remove default webclient handlers that induce click delay
|
||||
$(document).off();
|
||||
$(window).off();
|
||||
$('html').off();
|
||||
$('body').off();
|
||||
});
|
||||
|
||||
onWillDestroy(() => {
|
||||
this.env.pos.destroy();
|
||||
});
|
||||
|
||||
onError((error) => {
|
||||
// error is an OwlError object.
|
||||
console.error(error.cause);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
this.props.setupIsDone(this);
|
||||
});
|
||||
}
|
||||
|
||||
// GETTERS //
|
||||
|
||||
get customerFacingDisplayButtonIsShown() {
|
||||
return this.env.pos.config.iface_customer_facing_display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to give the `state.mobileSearchBarIsShown` value to main screen props
|
||||
*/
|
||||
get mainScreenPropsFielded() {
|
||||
return Object.assign({}, this.mainScreenProps, {
|
||||
mobileSearchBarIsShown: this.state.mobileSearchBarIsShown,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
await this.env.pos.load_server_data();
|
||||
await this.setupBarcodeParser();
|
||||
if(this.env.pos.config.use_proxy){
|
||||
await this.connect_to_proxy();
|
||||
}
|
||||
// Load the saved `env.pos.toRefundLines` from localStorage when
|
||||
// the PosGlobalState is ready.
|
||||
Object.assign(this.env.pos.toRefundLines, this.env.pos.db.load('TO_REFUND_LINES') || {});
|
||||
this._buildChrome();
|
||||
this._closeOtherTabs();
|
||||
this.env.pos.selectedCategoryId = this.env.pos.config.start_category && this.env.pos.config.iface_start_categ_id
|
||||
? this.env.pos.config.iface_start_categ_id[0]
|
||||
: 0;
|
||||
this.state.uiState = 'READY';
|
||||
this._showStartScreen();
|
||||
setTimeout(() => this._runBackgroundTasks());
|
||||
} 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;
|
||||
if (error.cause) {
|
||||
body = error.cause.message;
|
||||
} else {
|
||||
body = error.stack;
|
||||
}
|
||||
}
|
||||
|
||||
await this.showPopup('ErrorTracebackPopup', {
|
||||
title,
|
||||
body,
|
||||
exitButtonIsShown: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_runBackgroundTasks() {
|
||||
// push order in the background, no need to await
|
||||
this.env.pos.push_orders();
|
||||
// Allow using the app even if not all the images are loaded.
|
||||
// Basically, preload the images in the background.
|
||||
this._preloadImages();
|
||||
if (this.env.pos.config.limited_partners_loading && this.env.pos.config.partner_load_background) {
|
||||
// Wrap in fresh reactive: none of the reads during loading should subscribe to anything
|
||||
reactive(this.env.pos).loadPartnersBackground();
|
||||
}
|
||||
if (this.env.pos.config.limited_products_loading && this.env.pos.config.product_load_background) {
|
||||
// Wrap in fresh reactive: none of the reads during loading should subscribe to anything
|
||||
reactive(this.env.pos).loadProductsBackground().then(() => {
|
||||
this.render(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setupBarcodeParser() {
|
||||
if (!this.env.pos.company.nomenclature_id) {
|
||||
const errorMessage = this.env._t("The barcode nomenclature setting is not configured. " +
|
||||
"Make sure to configure it on your Point of Sale configuration settings");
|
||||
throw new Error(this.env._t("Missing barcode nomenclature"), { cause: { message: errorMessage } });
|
||||
|
||||
}
|
||||
const barcode_parser = new BarcodeParser({ nomenclature_id: this.env.pos.company.nomenclature_id });
|
||||
this.env.barcode_reader.set_barcode_parser(barcode_parser);
|
||||
const fallbackNomenclature = this.env.pos.company.fallback_nomenclature_id;
|
||||
if (fallbackNomenclature) {
|
||||
const fallbackBarcodeParser = new BarcodeParser({ nomenclature_id: fallbackNomenclature });
|
||||
this.env.barcode_reader.setFallbackBarcodeParser(fallbackBarcodeParser);
|
||||
}
|
||||
return barcode_parser.is_loaded();
|
||||
}
|
||||
|
||||
connect_to_proxy() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.env.barcode_reader.disconnect_from_proxy();
|
||||
this.state.loadingSkipButtonIsShown = true;
|
||||
this.env.proxy.autoconnect({
|
||||
force_ip: this.env.pos.config.proxy_ip || undefined,
|
||||
progress: function(prog){},
|
||||
}).then(
|
||||
() => {
|
||||
if (this.env.pos.config.iface_scan_via_proxy) {
|
||||
this.env.barcode_reader.connect_to_proxy();
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
(statusText, url) => {
|
||||
// this should reject so that it can be captured when we wait for pos.ready
|
||||
// in the chrome component.
|
||||
// then, if it got really rejected, we can show the error.
|
||||
if (statusText == 'error' && window.location.protocol == 'https:') {
|
||||
reject({
|
||||
title: this.env._t('HTTPS connection to IoT Box failed'),
|
||||
body: _.str.sprintf(
|
||||
this.env._t('Make sure you are using IoT Box v18.12 or higher. Navigate to %s to accept the certificate of your IoT Box.'),
|
||||
url
|
||||
),
|
||||
popup: 'alert',
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
openCashControl() {
|
||||
if (this.shouldShowCashControl()) {
|
||||
this.showPopup('CashOpeningPopup');
|
||||
}
|
||||
}
|
||||
|
||||
shouldShowCashControl() {
|
||||
return this.env.pos.config.cash_control && this.env.pos.pos_session.state == 'opening_control';
|
||||
}
|
||||
|
||||
// EVENT HANDLERS //
|
||||
|
||||
_showStartScreen() {
|
||||
const { name, props } = this.startScreen;
|
||||
this.showScreen(name, props);
|
||||
}
|
||||
_getSavedScreen(order) {
|
||||
return order.get_screen_data();
|
||||
}
|
||||
__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 });
|
||||
this.env.pos.tempScreenIsShown = true;
|
||||
}
|
||||
__closeTempScreen() {
|
||||
this.tempScreen.isShown = false;
|
||||
this.env.pos.tempScreenIsShown = false;
|
||||
this.tempScreen.name = null;
|
||||
}
|
||||
__showScreen({ detail: { name, props = {} } }) {
|
||||
const component = this.constructor.components[name];
|
||||
// 1. Set the information of the screen to display.
|
||||
this.mainScreen.name = name;
|
||||
this.mainScreen.component = component;
|
||||
this.mainScreenProps = props;
|
||||
|
||||
// 2. Save the screen to the order.
|
||||
// - This screen is shown when the order is selected.
|
||||
if (!(component.prototype instanceof IndependentToOrderScreen) && name !== "ReprintReceiptScreen") {
|
||||
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({ 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 there are orders in the db left unsynced, we try to sync.
|
||||
await this.env.pos.push_orders_with_closing_popup();
|
||||
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||
}
|
||||
_toggleDebugWidget() {
|
||||
this.state.debugWidgetIsShown = !this.state.debugWidgetIsShown;
|
||||
}
|
||||
_toggleMobileSearchBar({ detail: isSearchBarEnabled }) {
|
||||
if (isSearchBarEnabled !== null) {
|
||||
this.state.mobileSearchBarIsShown = isSearchBarEnabled;
|
||||
} else {
|
||||
this.state.mobileSearchBarIsShown = !this.state.mobileSearchBarIsShown;
|
||||
}
|
||||
}
|
||||
_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.synch.status = status;
|
||||
this.env.pos.synch.pending = pending;
|
||||
}
|
||||
_onShowNotification({ detail: { message, duration } }) {
|
||||
this.state.notification.isShown = true;
|
||||
this.state.notification.message = message;
|
||||
this.state.notification.duration = duration;
|
||||
}
|
||||
_onCloseNotification() {
|
||||
this.state.notification.isShown = false;
|
||||
}
|
||||
/**
|
||||
* Save `env.pos.toRefundLines` in localStorage on beforeunload - closing the
|
||||
* browser, reloading or going to other page.
|
||||
*/
|
||||
_onBeforeUnload() {
|
||||
this.env.pos.db.save('TO_REFUND_LINES', this.env.pos.toRefundLines);
|
||||
}
|
||||
|
||||
get isTicketScreenShown() {
|
||||
return this.mainScreen.name === 'TicketScreen';
|
||||
}
|
||||
|
||||
// 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}&unique=${product.__last_update}`;
|
||||
}
|
||||
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}&unique=${category.write_date}`;
|
||||
}
|
||||
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.hasBigScrollBars = true;
|
||||
}
|
||||
|
||||
this._disableBackspaceBack();
|
||||
}
|
||||
// prevent backspace from performing a 'back' navigation
|
||||
_disableBackspaceBack() {
|
||||
$(document).on('keydown', function (e) {
|
||||
if (e.which === 8 && !$(e.target).is('input, textarea')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
_closeOtherTabs() {
|
||||
localStorage['message'] = '';
|
||||
localStorage['message'] = JSON.stringify({
|
||||
message: 'close_tabs',
|
||||
session: this.env.pos.pos_session.id,
|
||||
});
|
||||
|
||||
window.addEventListener(
|
||||
'storage',
|
||||
(event) => {
|
||||
if (event.key === 'message' && event.newValue) {
|
||||
const msg = JSON.parse(event.newValue);
|
||||
if (
|
||||
msg.message === 'close_tabs' &&
|
||||
msg.session == this.env.pos.pos_session.id
|
||||
) {
|
||||
console.info(
|
||||
'POS / Session opened in another window. EXITING POS'
|
||||
);
|
||||
this._closePos();
|
||||
}
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
showCashMoveButton() {
|
||||
return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control;
|
||||
}
|
||||
|
||||
// UNEXPECTED ERROR HANDLING //
|
||||
|
||||
/**
|
||||
* This method is used to handle unexpected errors. It is registered to
|
||||
* the `error_handlers` service when this component is properly mounted.
|
||||
* See `onMounted` hook of the `ChromeAdapter` component.
|
||||
* @param {*} env
|
||||
* @param {UncaughtClientError | UncaughtPromiseError} error
|
||||
* @param {*} originalError
|
||||
* @returns {boolean}
|
||||
*/
|
||||
errorHandler(env, error, originalError) {
|
||||
if (!env.pos) return false;
|
||||
const errorToHandle = identifyError(originalError);
|
||||
// Assume that the unhandled falsey rejections can be ignored.
|
||||
if (errorToHandle) {
|
||||
this._errorHandler(error, errorToHandle);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_errorHandler(error, errorToHandle) {
|
||||
if (errorToHandle instanceof RPCError) {
|
||||
const { message, data } = errorToHandle;
|
||||
if (odooExceptionTitleMap.has(errorToHandle.exceptionName)) {
|
||||
const title = odooExceptionTitleMap.get(errorToHandle.exceptionName).toString();
|
||||
this.showPopup('ErrorPopup', { title, body: data.message });
|
||||
} else {
|
||||
this.showPopup('ErrorTracebackPopup', {
|
||||
title: message,
|
||||
body: data.message + '\n' + data.debug + '\n',
|
||||
});
|
||||
}
|
||||
} else if (errorToHandle instanceof ConnectionLostError) {
|
||||
this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Connection is lost'),
|
||||
body: this.env._t('Check the internet connection then try again.'),
|
||||
});
|
||||
} else if (errorToHandle instanceof ConnectionAbortedError) {
|
||||
this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Connection is aborted'),
|
||||
body: this.env._t('Check the internet connection then try again.'),
|
||||
});
|
||||
} else if (errorToHandle instanceof Error) {
|
||||
// If `errorToHandle` is a normal Error (such as TypeError),
|
||||
// the annotated traceback can be found from `error`.
|
||||
this.showPopup('ErrorTracebackPopup', {
|
||||
// Hopefully the message is translated.
|
||||
title: `${errorToHandle.name}: ${errorToHandle.message}`,
|
||||
body: error.traceback,
|
||||
});
|
||||
} else {
|
||||
// Hey developer. It's your fault that the error reach here.
|
||||
// Please, throw an Error object in order to get stack trace of the error.
|
||||
// At least we can find the file that throws the error when you look
|
||||
// at the console.
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Unknown Error'),
|
||||
body: this.env._t('Unable to show information about this error.'),
|
||||
});
|
||||
console.error('Unknown error. Unable to show information about this error.', errorToHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
Chrome.template = 'Chrome';
|
||||
Object.defineProperty(Chrome, "components", {
|
||||
get () {
|
||||
return Object.assign({ Transition }, PosComponent.components);
|
||||
}
|
||||
})
|
||||
|
||||
Registries.Component.add(Chrome);
|
||||
|
||||
return Chrome;
|
||||
});
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
odoo.define('point_of_sale.CashMoveButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _t } = require('web.core');
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
|
||||
const TRANSLATED_CASH_MOVE_TYPE = {
|
||||
in: _t('in'),
|
||||
out: _t('out'),
|
||||
};
|
||||
|
||||
class CashMoveButton extends PosComponent {
|
||||
async onClick() {
|
||||
const { confirmed, payload } = await this.showPopup('CashMovePopup');
|
||||
if (!confirmed) return;
|
||||
const { type, amount, reason } = payload;
|
||||
const translatedType = TRANSLATED_CASH_MOVE_TYPE[type];
|
||||
const formattedAmount = this.env.pos.format_currency(amount);
|
||||
if (!amount) {
|
||||
return this.showNotification(
|
||||
_.str.sprintf(this.env._t('Cash in/out of %s is ignored.'), formattedAmount),
|
||||
3000
|
||||
);
|
||||
}
|
||||
const extras = { formattedAmount, translatedType };
|
||||
await this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'try_cash_in_out',
|
||||
args: [[this.env.pos.pos_session.id], type, amount, reason, extras],
|
||||
});
|
||||
if (this.env.proxy.printer) {
|
||||
const renderedReceipt = renderToString('point_of_sale.CashMoveReceipt', {
|
||||
_receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }),
|
||||
});
|
||||
const printResult = await this.env.proxy.printer.print_receipt(renderedReceipt);
|
||||
if (!printResult.successful) {
|
||||
this.showPopup('ErrorPopup', { title: printResult.message.title, body: printResult.message.body });
|
||||
}
|
||||
}
|
||||
this.showNotification(
|
||||
_.str.sprintf(this.env._t('Successfully made a cash %s of %s.'), type, formattedAmount),
|
||||
3000
|
||||
);
|
||||
}
|
||||
_getReceiptInfo(payload) {
|
||||
const result = { ...payload };
|
||||
result.cashier = this.env.pos.get_cashier();
|
||||
result.company = this.env.pos.company;
|
||||
result.date = new Date().toLocaleString();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
CashMoveButton.template = 'point_of_sale.CashMoveButton';
|
||||
|
||||
Registries.Component.add(CashMoveButton);
|
||||
|
||||
return CashMoveButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
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 { name } = this.env.pos.get_cashier();
|
||||
return name ? name : '';
|
||||
}
|
||||
get avatar() {
|
||||
const user_id = this.env.pos.get_cashier_user_id();
|
||||
const id = user_id ? user_id : -1;
|
||||
return `/web/image/res.users/${id}/avatar_128`;
|
||||
}
|
||||
}
|
||||
CashierName.template = 'CashierName';
|
||||
|
||||
Registries.Component.add(CashierName);
|
||||
|
||||
return CashierName;
|
||||
});
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
odoo.define('point_of_sale.CustomerFacingDisplayButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
class CustomerFacingDisplayButton extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.local = this.env.pos.config.iface_customer_facing_display_local && !this.env.pos.config.iface_customer_facing_display_via_proxy;
|
||||
this.state = useState({ status: this.local ? 'success' : 'failure' });
|
||||
this._start();
|
||||
}
|
||||
get message() {
|
||||
return {
|
||||
success: '',
|
||||
warning: this.env._t('Connected, Not Owned'),
|
||||
failure: this.env._t('Disconnected'),
|
||||
not_found: this.env._t('Customer Screen Unsupported. Please upgrade the IoT Box'),
|
||||
}[this.state.status];
|
||||
}
|
||||
onClick() {
|
||||
if (this.local) {
|
||||
return this.onClickLocal();
|
||||
} else {
|
||||
return this.onClickProxy();
|
||||
}
|
||||
}
|
||||
async onClickLocal() {
|
||||
this.env.pos.customer_display = window.open('', 'Customer Display', 'height=600,width=900');
|
||||
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
|
||||
var $renderedHtml = $('<div>').html(renderedHtml);
|
||||
$(this.env.pos.customer_display.document.body).html($renderedHtml.find('.pos-customer_facing_display'));
|
||||
$(this.env.pos.customer_display.document.head).html($renderedHtml.find('.resources').html());
|
||||
}
|
||||
async onClickProxy() {
|
||||
try {
|
||||
const renderedHtml = await this.env.pos.render_html_for_customer_facing_display();
|
||||
let ownership = await this.env.proxy.take_ownership_over_customer_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.proxy.posbox_supports_display) {
|
||||
this.env.proxy.posbox_supports_display = true;
|
||||
this._start();
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof error == 'undefined') {
|
||||
this.state.status = 'failure';
|
||||
} else {
|
||||
this.state.status = 'not_found';
|
||||
}
|
||||
}
|
||||
}
|
||||
_start() {
|
||||
if (this.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
async function loop() {
|
||||
if (self.env.proxy.posbox_supports_display) {
|
||||
try {
|
||||
let ownership = await self.env.proxy.test_ownership_of_customer_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.proxy.posbox_supports_display = false;
|
||||
}
|
||||
setTimeout(loop, 3000);
|
||||
}
|
||||
}
|
||||
}
|
||||
loop();
|
||||
}
|
||||
}
|
||||
CustomerFacingDisplayButton.template = 'CustomerFacingDisplayButton';
|
||||
|
||||
Registries.Component.add(CustomerFacingDisplayButton);
|
||||
|
||||
return CustomerFacingDisplayButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
odoo.define('point_of_sale.DebugWidget', function (require) {
|
||||
'use strict';
|
||||
|
||||
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');
|
||||
|
||||
const { onMounted, onWillUnmount, useRef, useState } = owl;
|
||||
|
||||
class DebugWidget extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
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.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)
|
||||
);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
NumberBuffer.on('buffer-update', this, this._onBufferUpdate);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
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.proxy.debug_set_weight(weightInKg);
|
||||
}
|
||||
}
|
||||
resetWeight() {
|
||||
this.state.weightInput = '';
|
||||
this.env.proxy.debug_reset_weight();
|
||||
}
|
||||
async barcodeScan() {
|
||||
await this.env.barcode_reader.scan(this.state.barcodeInput);
|
||||
}
|
||||
async barcodeScanEAN() {
|
||||
const ean = this.env.barcode_reader.barcode_parser.sanitize_ean(
|
||||
this.state.barcodeInput || '0'
|
||||
);
|
||||
this.state.barcodeInput = ean;
|
||||
await this.env.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.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,35 @@
|
|||
odoo.define('point_of_sale.HeaderButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { isConnectionError } = require('point_of_sale.utils');
|
||||
|
||||
// Previously HeaderButtonWidget
|
||||
// This is the close session button
|
||||
class HeaderButton extends PosComponent {
|
||||
async onClick() {
|
||||
try {
|
||||
const info = await this.env.pos.getClosePosInfo();
|
||||
this.showPopup('ClosePosPopup', { info: info, keepBehind: true });
|
||||
} catch (e) {
|
||||
if (isConnectionError(e)) {
|
||||
this.showPopup('OfflineErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t('Please check your internet connection and try again.'),
|
||||
});
|
||||
} else {
|
||||
this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Unknown Error'),
|
||||
body: this.env._t('An unknown error prevents us from getting closing information.'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
HeaderButton.template = 'HeaderButton';
|
||||
|
||||
Registries.Component.add(HeaderButton);
|
||||
|
||||
return HeaderButton;
|
||||
});
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
odoo.define('point_of_sale.ProxyStatus', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { onMounted, onWillUnmount, useState } = owl;
|
||||
|
||||
// Previously ProxyStatusWidget
|
||||
class ProxyStatus extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
const initialProxyStatus = this.env.proxy.get('status');
|
||||
this.state = useState({
|
||||
status: initialProxyStatus.status,
|
||||
msg: initialProxyStatus.msg,
|
||||
});
|
||||
this.statuses = ['connected', 'connecting', 'disconnected', 'warning'];
|
||||
this.index = 0;
|
||||
|
||||
onMounted(() => {
|
||||
this.env.proxy.on('change:status', this, this._onChangeStatus);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
this.env.proxy.off('change:status', this, this._onChangeStatus);
|
||||
});
|
||||
}
|
||||
_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,39 @@
|
|||
odoo.define('point_of_sale.SaleDetailsButton', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { renderToString } = require('@web/core/utils/render');
|
||||
|
||||
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 = renderToString(
|
||||
'SaleDetailsReport',
|
||||
Object.assign({}, saleDetails, {
|
||||
date: new Date().toLocaleString(),
|
||||
pos: this.env.pos,
|
||||
})
|
||||
);
|
||||
const printResult = await this.env.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,17 @@
|
|||
odoo.define('point_of_sale.SyncNotification', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class SyncNotification extends PosComponent {
|
||||
onClick() {
|
||||
this.env.pos.push_orders(null, { show_error: true });
|
||||
}
|
||||
}
|
||||
SyncNotification.template = 'SyncNotification';
|
||||
|
||||
Registries.Component.add(SyncNotification);
|
||||
|
||||
return SyncNotification;
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
odoo.define('point_of_sale.TicketButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class TicketButton extends PosComponent {
|
||||
onClick() {
|
||||
if (this.props.isTicketScreenShown) {
|
||||
this.env.posbus.trigger('ticket-button-clicked');
|
||||
} else {
|
||||
this.showScreen('TicketScreen');
|
||||
}
|
||||
}
|
||||
get count() {
|
||||
if (this.env.pos) {
|
||||
return this.env.pos.get_order_list().length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
TicketButton.template = 'TicketButton';
|
||||
|
||||
Registries.Component.add(TicketButton);
|
||||
|
||||
return TicketButton;
|
||||
});
|
||||
|
|
@ -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, B1);
|
||||
* // |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,107 @@
|
|||
odoo.define('point_of_sale.ControlButtonsMixin', function (require) {
|
||||
'use strict';
|
||||
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* Component that has this mixin allows the use of `addControlButton`.
|
||||
* All added control buttons that satisfies the condition can be accessed
|
||||
* thru the `controlButtons` field of the Component's instance. These
|
||||
* control buttons can then be rendered in the Component.
|
||||
* @param {Function} x superclass
|
||||
*/
|
||||
const ControlButtonsMixin = (x) => {
|
||||
const controlButtonsToPosition = [];
|
||||
const sortedControlButtons = [];
|
||||
|
||||
class Extended extends x {
|
||||
get controlButtons() {
|
||||
return sortedControlButtons
|
||||
.filter((cb) => {
|
||||
return cb.condition ? cb.condition.bind(this)() : true;
|
||||
})
|
||||
.map((cb) =>
|
||||
Object.assign({}, cb, { component: Registries.Component.get(cb.component) })
|
||||
);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param {Object} controlButton
|
||||
* @param {Function} controlButton.component
|
||||
* Base class 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
|
||||
*/
|
||||
Extended.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) {
|
||||
sortedControlButtons.push(controlButton);
|
||||
} else {
|
||||
controlButtonsToPosition.push(controlButton);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Call this static method to make the added control buttons in proper
|
||||
* order.
|
||||
* NOTE: This isn't necessarily a fast algorithm. I doubt that the number
|
||||
* of control buttons will exceed an order of hundreds, so for practical
|
||||
* purposes, it is enough.
|
||||
*/
|
||||
Extended.sortControlButtons = function () {
|
||||
function setControlButton(locator, index, cb) {
|
||||
if (locator == 'replace') {
|
||||
sortedControlButtons[index] = cb;
|
||||
} else if (locator == 'before') {
|
||||
sortedControlButtons.splice(index, 0, cb);
|
||||
} else if (locator == 'after') {
|
||||
sortedControlButtons.splice(index + 1, 0, cb);
|
||||
}
|
||||
}
|
||||
function locate(cb) {
|
||||
const [locator, reference] = cb.position;
|
||||
const index = sortedControlButtons.findIndex((cb) => cb.name == reference);
|
||||
return [locator, index, reference];
|
||||
}
|
||||
const cbMissingReference = [];
|
||||
// 1. First pass. If the reference control button isn't there, collect it for second pass.
|
||||
for (let cb of controlButtonsToPosition) {
|
||||
const [locator, index] = locate(cb);
|
||||
if (index == -1) {
|
||||
cbMissingReference.push(cb);
|
||||
continue;
|
||||
}
|
||||
setControlButton(locator, index, cb);
|
||||
}
|
||||
// 2. Second pass.
|
||||
// If during the first pass, 1 -> 2, 2 -> 3, 3 -> 4, 4 -> 5 and 5 is already
|
||||
// in the sorted control buttons, then 1, 2, 3 & 4 are put in `cbMissingReference`.
|
||||
// This only means 2 things about the objects in `cbMissingReference`:
|
||||
// i) They are referencing the cb after them
|
||||
// ii) They really have missing reference.
|
||||
// Thus, we have to iterate the cb with missing reference in reverse.
|
||||
for (let cb of cbMissingReference.reverse()) {
|
||||
const [locator, index, reference] = locate(cb);
|
||||
if (index == -1) {
|
||||
console.warn(`'${cb.name}' is not properly position because '${reference}' is not found. Is '${reference}' spelled correctly?`);
|
||||
sortedControlButtons.push(cb);
|
||||
} else {
|
||||
setControlButton(locator, index, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Extended;
|
||||
};
|
||||
|
||||
return ControlButtonsMixin;
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
odoo.define('point_of_sale.Gui', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { status } = owl;
|
||||
|
||||
/**
|
||||
* This module bridges the data classes (such as those defined in
|
||||
* models.js) to the view (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 {component} component component having the ui methods.
|
||||
*/
|
||||
const configureGui = ({ component }) => {
|
||||
config.component = component;
|
||||
config.availableMethods = new Set([
|
||||
'showScreen',
|
||||
'showPopup',
|
||||
'showTempScreen',
|
||||
'playSound',
|
||||
'setSyncStatus',
|
||||
'showNotification',
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = status(component) === 'mounted';
|
||||
if (availableMethods.has(key) && isMounted) {
|
||||
return component[key].bind(component);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return { configureGui, Gui };
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
odoo.define('point_of_sale.AbstractReceiptScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { nextFrame } = require('point_of_sale.utils');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useRef } = owl;
|
||||
|
||||
/**
|
||||
* This relies on the assumption that there is a reference to
|
||||
* `order-receipt` so it is important to declare a `t-ref` to
|
||||
* `order-receipt` in the template of the Component that extends
|
||||
* this abstract component.
|
||||
*/
|
||||
class AbstractReceiptScreen extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orderReceipt = useRef('order-receipt');
|
||||
}
|
||||
async _printReceipt() {
|
||||
if (this.env.proxy.printer) {
|
||||
const printResult = await this.env.proxy.printer.print_receipt(this.orderReceipt.el.innerHTML);
|
||||
if (printResult.successful) {
|
||||
return true;
|
||||
} else {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: printResult.message.title,
|
||||
body: printResult.message.body,
|
||||
});
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: printResult.message.title,
|
||||
body: this.env._t('Do you want to print using the web printer?'),
|
||||
});
|
||||
if (confirmed) {
|
||||
// We want to call the _printWeb when the popup is fully gone
|
||||
// from the screen which happens after the next animation frame.
|
||||
await nextFrame();
|
||||
return await this._printWeb();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return await this._printWeb();
|
||||
}
|
||||
}
|
||||
async _printWeb() {
|
||||
try {
|
||||
window.print();
|
||||
return true;
|
||||
} catch (_err) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Printing is not supported on some browsers'),
|
||||
body: this.env._t(
|
||||
'Printing is not supported on some browsers due to no default printing protocol ' +
|
||||
'is available. It is possible to print your tickets by making use of an IoT Box.'
|
||||
),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Registries.Component.add(AbstractReceiptScreen);
|
||||
|
||||
return AbstractReceiptScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
odoo.define('point_of_sale.CurrencyAmount', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class CurrencyAmount extends PosComponent {}
|
||||
CurrencyAmount.template = 'CurrencyAmount';
|
||||
|
||||
Registries.Component.add(CurrencyAmount);
|
||||
|
||||
return CurrencyAmount;
|
||||
});
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
odoo.define('point_of_sale.Draggable', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { onMounted, useExternalListener } = owl;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
setup() {
|
||||
super.setup();
|
||||
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);
|
||||
|
||||
onMounted(() => {
|
||||
this.limitArea = this.props.limitArea
|
||||
? document.querySelector(this.props.limitArea)
|
||||
: this.el.offsetParent;
|
||||
if (!this.limitArea) return;
|
||||
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`;
|
||||
}
|
||||
}
|
||||
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,15 @@
|
|||
odoo.define('point_of_sale.IndependentToOrderScreen', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
|
||||
class IndependentToOrderScreen extends PosComponent {
|
||||
close() {
|
||||
const order = this.env.pos.get_order();
|
||||
const { name: screenName } = order.get_screen_data();
|
||||
this.showScreen(screenName);
|
||||
}
|
||||
}
|
||||
|
||||
return IndependentToOrderScreen;
|
||||
});
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
odoo.define('point_of_sale.MobileOrderWidget', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class MobileOrderWidget extends PosComponent {
|
||||
get order() {
|
||||
return this.env.pos.get_order();
|
||||
}
|
||||
get total() {
|
||||
const _total = this.order ? this.order.get_total_with_tax() : 0;
|
||||
return this.env.pos.format_currency(_total);
|
||||
}
|
||||
get items_number() {
|
||||
return this.order ? this.order.orderlines.reduce((items_number,line) => items_number + line.quantity, 0) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
MobileOrderWidget.template = 'MobileOrderWidget';
|
||||
|
||||
Registries.Component.add(MobileOrderWidget);
|
||||
|
||||
return MobileOrderWidget;
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
odoo.define('point_of_sale.NotificationSound', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
class NotificationSound extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('ended', () => (this.props.sound.src = null));
|
||||
}
|
||||
}
|
||||
NotificationSound.template = 'NotificationSound';
|
||||
|
||||
Registries.Component.add(NotificationSound);
|
||||
|
||||
return NotificationSound;
|
||||
});
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
odoo.define('point_of_sale.NumberBuffer', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const { parse } = require('web.field_utils');
|
||||
const { barcodeService } = require('@barcodes/barcode_service');
|
||||
const { _t } = require('web.core');
|
||||
const { Gui } = require('point_of_sale.Gui');
|
||||
|
||||
const { EventBus, onMounted, onWillUnmount, useComponent, useExternalListener } = owl;
|
||||
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()
|
||||
* - capture()
|
||||
*
|
||||
* Note
|
||||
* ====
|
||||
* - No need to instantiate as it is a singleton 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.
|
||||
* - Write more integration tests. NumberPopup can be used as test component.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
/**
|
||||
* Calling this function, we immediately invoke the `handler` method
|
||||
* that handles the contents of the input events buffer (`eventsBuffer`).
|
||||
* This is helpful when we don't want to wait for the timeout that
|
||||
* is supposed to invoke the handler.
|
||||
*/
|
||||
capture() {
|
||||
if (this.handler) {
|
||||
clearTimeout(this._timeout);
|
||||
this.handler();
|
||||
delete this.handler;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @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.triggerAtEsc Event triggered when 'Esc' 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.
|
||||
* @emits config.triggerAtEnter when 'Enter' key is pressed.
|
||||
* @emits config.triggerAtEsc when 'Esc' key is pressed.
|
||||
* @emits config.triggerAtInput when an input is accepted.
|
||||
*/
|
||||
use(config) {
|
||||
this.eventsBuffer = [];
|
||||
const currentComponent = useComponent();
|
||||
config = Object.assign(getDefaultConfig(), config);
|
||||
onMounted(() => {
|
||||
this.bufferHolderStack.push({
|
||||
component: currentComponent,
|
||||
state: config.state ? config.state : { buffer: '', toStartOver: false },
|
||||
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
|
||||
? barcodeService.maxTimeBetweenKeysInMs
|
||||
: 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);
|
||||
this.handler = handler
|
||||
};
|
||||
}
|
||||
_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 === ',' || input === '.') {
|
||||
if (this.state.toStartOver) {
|
||||
this.state.buffer = '';
|
||||
}
|
||||
if (isFirstInput) {
|
||||
this.state.buffer = '0' + this.decimalPoint;
|
||||
} else if (!this.state.buffer.length || this.state.buffer === '-') {
|
||||
this.state.buffer += '0' + this.decimalPoint;
|
||||
} 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;
|
||||
}
|
||||
if (this.state.toStartOver) {
|
||||
this.state.buffer = '';
|
||||
}
|
||||
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 (this.state.toStartOver) { // when we want to erase the current buffer for a new value
|
||||
this.state.buffer = '';
|
||||
}
|
||||
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;
|
||||
// it should not be in a start the buffer over state anymore.
|
||||
this.state.toStartOver = false;
|
||||
|
||||
if (this.config.maxValue && this.state.buffer > this.config.maxValue) {
|
||||
this.state.buffer = this.config.maxValue.toString();
|
||||
this.config.maxValueReached();
|
||||
}
|
||||
|
||||
this.trigger('buffer-update', this.state.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
return new NumberBuffer();
|
||||
});
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
odoo.define('point_of_sale.SearchBar', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useAutofocus, useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useExternalListener, useState } = owl;
|
||||
|
||||
/**
|
||||
* This is a simple configurable search bar component. It has search fields
|
||||
* and selection filter. Search fields allow the users to specify the type
|
||||
* of their searches. The filter is a dropdown menu for selection. Depending on
|
||||
* user's action, this component emits corresponding event with the action
|
||||
* information (payload).
|
||||
*
|
||||
* TODO: This component can be made more generic and be able to replace
|
||||
* all the search bars across pos ui.
|
||||
*
|
||||
* @prop {{
|
||||
* config: {
|
||||
* searchFields: Map<string, string>,
|
||||
* filter: { show: boolean, options: Map<string, { text: string, indented: boolean? }> }
|
||||
* },
|
||||
* placeholder: string,
|
||||
* }}
|
||||
* @emits search @payload { fieldName: string, searchTerm: '' }
|
||||
* @emits filter-selected @payload { filter: string }
|
||||
*
|
||||
* NOTE: The payload of the emitted event is accessible via the `detail`
|
||||
* field of the event.
|
||||
*/
|
||||
class SearchBar extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useAutofocus();
|
||||
useExternalListener(window, 'click', this._hideOptions);
|
||||
useListener('click-search-field', this._onClickSearchField);
|
||||
useListener('select-filter', this._onSelectFilter);
|
||||
this.filterOptionsList = [...this.props.config.filter.options.keys()];
|
||||
this.searchFieldsList = [...this.props.config.searchFields.keys()];
|
||||
const defaultSearchFieldId = this.searchFieldsList.indexOf(
|
||||
this.props.config.defaultSearchDetails.fieldName
|
||||
);
|
||||
this.state = useState({
|
||||
searchInput: this.props.config.defaultSearchDetails.searchTerm || '',
|
||||
selectedSearchFieldId: defaultSearchFieldId == -1 ? 0 : defaultSearchFieldId,
|
||||
showSearchFields: false,
|
||||
showFilterOptions: false,
|
||||
selectedFilter: this.props.config.defaultFilter || this.filterOptionsList[0],
|
||||
});
|
||||
}
|
||||
_onSelectFilter({ detail: key }) {
|
||||
this.state.selectedFilter = key;
|
||||
this.trigger('filter-selected', { filter: this.state.selectedFilter });
|
||||
}
|
||||
/**
|
||||
* When pressing vertical arrow keys, do not move the input cursor.
|
||||
*/
|
||||
onSearchInputKeydown(event) {
|
||||
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* When vertical arrow keys are pressed, select fields for searching.
|
||||
* When enter key is pressed, trigger search event if there is searchInput.
|
||||
*/
|
||||
onSearchInputKeyup(event) {
|
||||
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
|
||||
this.state.selectedSearchFieldId = this._fieldIdToSelect(event.key);
|
||||
} else if (event.key === 'Enter' || this.state.searchInput == '') {
|
||||
this._onClickSearchField({ detail: this.searchFieldsList[this.state.selectedSearchFieldId] });
|
||||
} else {
|
||||
if (this.state.selectedSearchFieldId === -1 && this.searchFieldsList.length) {
|
||||
this.state.selectedSearchFieldId = 0;
|
||||
}
|
||||
this.state.showSearchFields = true;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Called when a search field is clicked.
|
||||
*/
|
||||
_onClickSearchField({ detail: fieldName }) {
|
||||
this.state.showSearchFields = false;
|
||||
this.trigger('search', { fieldName, searchTerm: this.state.searchInput });
|
||||
}
|
||||
/**
|
||||
* Given an arrow key, return the next selectedSearchFieldId.
|
||||
* E.g. If the selectedSearchFieldId is 1 and ArrowDown is pressed, return 2.
|
||||
*
|
||||
* @param {string} key vertical arrow key
|
||||
*/
|
||||
_fieldIdToSelect(key) {
|
||||
const length = this.searchFieldsList.length;
|
||||
if (!length) return null;
|
||||
if (this.state.selectedSearchFieldId === -1) return 0;
|
||||
const current = this.state.selectedSearchFieldId || length;
|
||||
return (current + (key === 'ArrowDown' ? 1 : -1)) % length;
|
||||
}
|
||||
_hideOptions() {
|
||||
this.state.showFilterOptions = false;
|
||||
this.state.showSearchFields = false;
|
||||
}
|
||||
}
|
||||
SearchBar.template = 'point_of_sale.SearchBar';
|
||||
Registries.Component.add(SearchBar);
|
||||
|
||||
return SearchBar;
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
odoo.define('point_of_sale.Notification', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { onMounted } = owl;
|
||||
|
||||
class Notification extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useListener('click', this.closeNotification);
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
this.closeNotification();
|
||||
}, this.props.duration)
|
||||
});
|
||||
}
|
||||
}
|
||||
Notification.template = 'Notification';
|
||||
|
||||
Registries.Component.add(Notification);
|
||||
|
||||
return Notification;
|
||||
});
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const { useBus } = require('@web/core/utils/hooks');
|
||||
|
||||
/**
|
||||
* Implement this abstract class by extending it like so:
|
||||
* ```js
|
||||
* class ConcretePopup extends AbstractAwaitablePopup {
|
||||
* async getPayload() {
|
||||
* return 'result';
|
||||
* }
|
||||
* }
|
||||
* ConcretePopup.template = 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 {
|
||||
setup() {
|
||||
super.setup();
|
||||
if (this.props.confirmKey) {
|
||||
useBus(this.env.posbus, `confirm-popup-${this.props.id}`, this.confirm);
|
||||
}
|
||||
if (this.props.cancelKey) {
|
||||
useBus(this.env.posbus, `cancel-popup-${this.props.id}`, this.cancel);
|
||||
}
|
||||
}
|
||||
async confirm() {
|
||||
this.env.posbus.trigger('close-popup', {
|
||||
popupId: this.props.id,
|
||||
response: { confirmed: true, payload: await this.getPayload() },
|
||||
});
|
||||
}
|
||||
cancel() {
|
||||
this.env.posbus.trigger('close-popup', {
|
||||
popupId: this.props.id,
|
||||
response: { confirmed: false, payload: null },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 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,97 @@
|
|||
odoo.define('point_of_sale.CashMovePopup', function (require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
const { parse } = require('web.field_utils');
|
||||
const { useValidateCashInput, useAsyncLockedMethod } = require('point_of_sale.custom_hooks');
|
||||
|
||||
const { useRef, useState } = owl;
|
||||
|
||||
class CashMovePopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
inputType: '', // '' | 'in' | 'out'
|
||||
inputAmount: '',
|
||||
inputReason: '',
|
||||
inputHasError: false,
|
||||
parsedAmount: 0,
|
||||
});
|
||||
this.inputAmountRef = useRef('input-amount-ref');
|
||||
useValidateCashInput('input-amount-ref');
|
||||
this.confirm = useAsyncLockedMethod(this.confirm);
|
||||
}
|
||||
confirm() {
|
||||
try {
|
||||
parse.float(this.state.inputAmount);
|
||||
} catch (_error) {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Invalid amount');
|
||||
return;
|
||||
}
|
||||
if (this.state.inputType == '') {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Select either Cash In or Cash Out before confirming.');
|
||||
return;
|
||||
}
|
||||
if (this.state.inputType === 'out' && this.state.inputAmount > 0) {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Insert a negative amount with the Cash Out option.');
|
||||
return;
|
||||
}
|
||||
if (this.state.inputType === 'in' && this.state.inputAmount < 0) {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Insert a positive amount with the Cash In option.');
|
||||
return;
|
||||
}
|
||||
if (parse.float(this.state.inputAmount) < 0) {
|
||||
this.state.inputAmount = this.state.inputAmount.substring(1);
|
||||
}
|
||||
return super.confirm();
|
||||
}
|
||||
_onAmountKeypress(event) {
|
||||
if (event.key === '-') {
|
||||
event.preventDefault();
|
||||
this.state.inputAmount = this.state.inputType === 'out' ? this.state.inputAmount.substring(1) : `-${this.state.inputAmount}`;
|
||||
this.state.inputType = this.state.inputType === 'out' ? 'in' : 'out';
|
||||
this.handleInputChange();
|
||||
}
|
||||
}
|
||||
onClickButton(type) {
|
||||
let amount = this.state.inputAmount;
|
||||
if (type === 'in') {
|
||||
this.state.inputAmount = amount.charAt(0) === '-' ? amount.substring(1) : amount;
|
||||
} else {
|
||||
this.state.inputAmount = amount.charAt(0) === '-' ? amount : `-${amount}`;
|
||||
}
|
||||
this.state.inputType = type;
|
||||
this.state.inputHasError = false;
|
||||
this.inputAmountRef.el && this.inputAmountRef.el.focus();
|
||||
if (amount && amount !== '-') {
|
||||
this.handleInputChange();
|
||||
}
|
||||
}
|
||||
getPayload() {
|
||||
return {
|
||||
amount: parse.float(this.state.inputAmount),
|
||||
reason: this.state.inputReason.trim(),
|
||||
type: this.state.inputType,
|
||||
};
|
||||
}
|
||||
handleInputChange() {
|
||||
if (this.inputAmountRef.el.classList.contains('invalid-cash-input')) return;
|
||||
this.state.parsedAmount = parse.float(this.state.inputAmount);
|
||||
}
|
||||
}
|
||||
CashMovePopup.template = 'point_of_sale.CashMovePopup';
|
||||
CashMovePopup.defaultProps = {
|
||||
cancelText: _lt('Cancel'),
|
||||
title: _lt('Cash In/Out'),
|
||||
};
|
||||
|
||||
Registries.Component.add(CashMovePopup);
|
||||
|
||||
return CashMovePopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
odoo.define('point_of_sale.CashOpeningPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const { useValidateCashInput } = require('point_of_sale.custom_hooks');
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { parse } = require('web.field_utils');
|
||||
|
||||
const { useState, useRef } = owl;
|
||||
|
||||
class CashOpeningPopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.manualInputCashCount = null;
|
||||
this.state = useState({
|
||||
notes: "",
|
||||
openingCash: this.env.pos.pos_session.cash_register_balance_start || 0,
|
||||
displayMoneyDetailsPopup: false,
|
||||
});
|
||||
useValidateCashInput("openingCashInput", this.env.pos.pos_session.cash_register_balance_start);
|
||||
this.openingCashInputRef = useRef('openingCashInput');
|
||||
}
|
||||
//@override
|
||||
async confirm() {
|
||||
this.env.pos.pos_session.cash_register_balance_start = this.state.openingCash;
|
||||
this.env.pos.pos_session.state = 'opened';
|
||||
this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'set_cashbox_pos',
|
||||
args: [this.env.pos.pos_session.id, this.state.openingCash, this.state.notes],
|
||||
});
|
||||
super.confirm();
|
||||
}
|
||||
openDetailsPopup() {
|
||||
this.state.openingCash = 0;
|
||||
this.state.notes = "";
|
||||
this.state.displayMoneyDetailsPopup = true;
|
||||
}
|
||||
closeDetailsPopup() {
|
||||
this.state.displayMoneyDetailsPopup = false;
|
||||
}
|
||||
updateCashOpening({ total, moneyDetailsNotes }) {
|
||||
this.openingCashInputRef.el.value = this.env.pos.format_currency_no_symbol(total);
|
||||
this.state.openingCash = total;
|
||||
if (moneyDetailsNotes) {
|
||||
this.state.notes = moneyDetailsNotes;
|
||||
}
|
||||
this.manualInputCashCount = false;
|
||||
this.closeDetailsPopup();
|
||||
}
|
||||
handleInputChange(event) {
|
||||
if (event.target.classList.contains('invalid-cash-input')) return;
|
||||
this.manualInputCashCount = true;
|
||||
this.state.openingCash = parse.float(event.target.value);
|
||||
}
|
||||
}
|
||||
|
||||
CashOpeningPopup.template = 'CashOpeningPopup';
|
||||
CashOpeningPopup.defaultProps = { cancelKey: false };
|
||||
Registries.Component.add(CashOpeningPopup);
|
||||
|
||||
return CashOpeningPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
odoo.define('point_of_sale.ClosePosPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { identifyError } = require('point_of_sale.utils');
|
||||
const { ConnectionLostError, ConnectionAbortedError} = require('@web/core/network/rpc_service')
|
||||
const { useState, useRef } = owl;
|
||||
const { useValidateCashInput } = require('point_of_sale.custom_hooks');
|
||||
const { parse } = require('web.field_utils');
|
||||
|
||||
class ClosePosPopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.manualInputCashCount = false;
|
||||
this.cashControl = this.env.pos.config.cash_control;
|
||||
this.closingCashInputRef = useRef('closingCashInput');
|
||||
this.closeSessionClicked = false;
|
||||
this.moneyDetails = null;
|
||||
Object.assign(this, this.props.info);
|
||||
this.state = useState({
|
||||
displayMoneyDetailsPopup: false,
|
||||
});
|
||||
Object.assign(this.state, this.props.info.state);
|
||||
useValidateCashInput("closingCashInput");
|
||||
if (this.otherPaymentMethods && this.otherPaymentMethods.length > 0) {
|
||||
this.otherPaymentMethods.forEach(pm => {
|
||||
if (this._getShowDiff(pm)) {
|
||||
useValidateCashInput("closingCashInput_" + pm.id, this.state.payments[pm.id].counted);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
//@override
|
||||
async confirm() {
|
||||
if (!this.cashControl || !this.hasDifference()) {
|
||||
this.closeSession();
|
||||
} else if (this.hasUserAuthority()) {
|
||||
const { confirmed } = await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('Payments Difference'),
|
||||
body: this.env._t('Do you want to accept payments difference and post a profit/loss journal entry?'),
|
||||
});
|
||||
if (confirmed) {
|
||||
this.closeSession();
|
||||
}
|
||||
} else {
|
||||
await this.showPopup('ConfirmPopup', {
|
||||
title: this.env._t('Payments Difference'),
|
||||
body: _.str.sprintf(
|
||||
this.env._t('The maximum difference allowed is %s.\n' +
|
||||
'Please contact your manager to accept the closing difference.'),
|
||||
this.env.pos.format_currency(this.amountAuthorizedDiff)
|
||||
),
|
||||
confirmText: this.env._t('OK'),
|
||||
})
|
||||
}
|
||||
}
|
||||
//@override
|
||||
async cancel() {
|
||||
if (this.canCancel()) {
|
||||
super.cancel();
|
||||
}
|
||||
}
|
||||
openDetailsPopup() {
|
||||
this.state.payments[this.defaultCashDetails.id].counted = 0;
|
||||
this.state.payments[this.defaultCashDetails.id].difference = -this.defaultCashDetails.amount;
|
||||
this.state.notes = "";
|
||||
this.state.displayMoneyDetailsPopup = true;
|
||||
}
|
||||
closeDetailsPopup() {
|
||||
this.state.displayMoneyDetailsPopup = false;
|
||||
}
|
||||
async downloadSalesReport() {
|
||||
await this.env.legacyActionManager.do_action('point_of_sale.sale_details_report', {
|
||||
additional_context: {
|
||||
active_ids: [this.env.pos.pos_session.id],
|
||||
},
|
||||
});
|
||||
}
|
||||
handleInputChange(paymentId, event) {
|
||||
if (event.target.classList.contains('invalid-cash-input')) return;
|
||||
let expectedAmount;
|
||||
if (this.defaultCashDetails && paymentId === this.defaultCashDetails.id) {
|
||||
this.manualInputCashCount = true;
|
||||
this.state.notes = '';
|
||||
expectedAmount = this.defaultCashDetails.amount;
|
||||
} else {
|
||||
expectedAmount = this.otherPaymentMethods.find(pm => paymentId === pm.id).amount;
|
||||
}
|
||||
this.state.payments[paymentId].counted = parse.float(event.target.value);
|
||||
this.state.payments[paymentId].difference =
|
||||
this.env.pos.round_decimals_currency(this.state.payments[paymentId].counted - expectedAmount);
|
||||
}
|
||||
updateCountedCash({ total, moneyDetailsNotes, moneyDetails }) {
|
||||
this.closingCashInputRef.el.value = this.env.pos.format_currency_no_symbol(total);
|
||||
this.state.payments[this.defaultCashDetails.id].counted = total;
|
||||
this.state.payments[this.defaultCashDetails.id].difference =
|
||||
this.env.pos.round_decimals_currency(this.state.payments[[this.defaultCashDetails.id]].counted - this.defaultCashDetails.amount);
|
||||
if (moneyDetailsNotes) {
|
||||
this.state.notes = moneyDetailsNotes;
|
||||
}
|
||||
this.manualInputCashCount = false;
|
||||
this.moneyDetails = moneyDetails;
|
||||
this.closeDetailsPopup();
|
||||
}
|
||||
hasDifference() {
|
||||
return Object.entries(this.state.payments).find(pm => pm[1].difference != 0);
|
||||
}
|
||||
hasUserAuthority() {
|
||||
const absDifferences = Object.entries(this.state.payments).map(pm => Math.abs(pm[1].difference));
|
||||
return this.isManager || this.amountAuthorizedDiff == null || Math.max(...absDifferences) <= this.amountAuthorizedDiff;
|
||||
}
|
||||
canCancel() {
|
||||
return true;
|
||||
}
|
||||
closePos() {
|
||||
this.trigger('close-pos');
|
||||
}
|
||||
async closeSession() {
|
||||
if (!this.closeSessionClicked) {
|
||||
this.closeSessionClicked = true;
|
||||
let response;
|
||||
// If there are orders in the db left unsynced, we try to sync.
|
||||
await this.env.pos.push_orders_with_closing_popup();
|
||||
if (this.cashControl) {
|
||||
response = await this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'post_closing_cash_details',
|
||||
args: [this.env.pos.pos_session.id],
|
||||
kwargs: {
|
||||
counted_cash: this.state.payments[this.defaultCashDetails.id].counted,
|
||||
}
|
||||
})
|
||||
if (!response.successful) {
|
||||
return this.handleClosingError(response);
|
||||
}
|
||||
}
|
||||
await this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'update_closing_control_state_session',
|
||||
args: [this.env.pos.pos_session.id, this.state.notes]
|
||||
})
|
||||
try {
|
||||
const bankPaymentMethodDiffPairs = this.otherPaymentMethods
|
||||
.filter((pm) => pm.type == 'bank')
|
||||
.map((pm) => [pm.id, this.state.payments[pm.id].difference]);
|
||||
response = await this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'close_session_from_ui',
|
||||
args: [this.env.pos.pos_session.id, bankPaymentMethodDiffPairs],
|
||||
context: this.env.session.user_context,
|
||||
});
|
||||
if (!response.successful) {
|
||||
return this.handleClosingError(response);
|
||||
}
|
||||
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||
} catch (error) {
|
||||
const iError = identifyError(error);
|
||||
if (iError instanceof ConnectionLostError || iError instanceof ConnectionAbortedError) {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Network Error'),
|
||||
body: this.env._t('Cannot close the session when offline.'),
|
||||
});
|
||||
} else {
|
||||
await this.showPopup('ErrorPopup', {
|
||||
title: this.env._t('Closing session error'),
|
||||
body: this.env._t(
|
||||
'An error has occurred when trying to close the session.\n' +
|
||||
'You will be redirected to the back-end to manually close the session.')
|
||||
})
|
||||
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||
}
|
||||
}
|
||||
this.closeSessionClicked = false;
|
||||
}
|
||||
}
|
||||
async handleClosingError(response) {
|
||||
await this.showPopup('ErrorPopup', {title: 'Error', body: response.message});
|
||||
if (response.redirect) {
|
||||
window.location = '/web#action=point_of_sale.action_client_pos_menu';
|
||||
}
|
||||
}
|
||||
_getShowDiff(pm) {
|
||||
return pm.type == 'bank' && pm.number !== 0;
|
||||
}
|
||||
}
|
||||
|
||||
ClosePosPopup.template = 'ClosePosPopup';
|
||||
Registries.Component.add(ClosePosPopup);
|
||||
|
||||
return ClosePosPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
odoo.define('point_of_sale.ConfirmPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
// formerly ConfirmPopupWidget
|
||||
class ConfirmPopup extends AbstractAwaitablePopup {}
|
||||
ConfirmPopup.template = 'ConfirmPopup';
|
||||
ConfirmPopup.defaultProps = {
|
||||
confirmText: _lt('Ok'),
|
||||
cancelText: _lt('Cancel'),
|
||||
title: _lt('Confirm ?'),
|
||||
body: '',
|
||||
};
|
||||
|
||||
Registries.Component.add(ConfirmPopup);
|
||||
|
||||
return ConfirmPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
odoo.define('point_of_sale.ControlButtonPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
class ControlButtonPopup extends AbstractAwaitablePopup {
|
||||
/**
|
||||
* @param {Object} props
|
||||
* @param {string} props.startingValue
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.controlButtons = this.props.controlButtons;
|
||||
}
|
||||
}
|
||||
ControlButtonPopup.template = 'ControlButtonPopup';
|
||||
ControlButtonPopup.defaultProps = {
|
||||
cancelText: _lt('Back'),
|
||||
controlButtons: [],
|
||||
confirmKey: false,
|
||||
};
|
||||
|
||||
Registries.Component.add(ControlButtonPopup);
|
||||
|
||||
return ControlButtonPopup;
|
||||
});
|
||||
|
|
@ -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,107 @@
|
|||
odoo.define('point_of_sale.EditListPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { useAutoFocusToLast } = require('point_of_sale.custom_hooks');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
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: _lt('Ok'),
|
||||
cancelText: _lt('Cancel'),
|
||||
array: [],
|
||||
isSingleItem: false,
|
||||
};
|
||||
|
||||
Registries.Component.add(EditListPopup);
|
||||
|
||||
return EditListPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
odoo.define('point_of_sale.ErrorBarcodePopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
// formerly ErrorBarcodePopupWidget
|
||||
class ErrorBarcodePopup extends ErrorPopup {
|
||||
get translatedMessage() {
|
||||
return this.env._t(this.props.message);
|
||||
}
|
||||
}
|
||||
ErrorBarcodePopup.template = 'ErrorBarcodePopup';
|
||||
ErrorBarcodePopup.defaultProps = {
|
||||
confirmText: _lt('Ok'),
|
||||
cancelText: _lt('Cancel'),
|
||||
title: _lt('Error'),
|
||||
body: '',
|
||||
message:
|
||||
_lt('The Point of Sale could not find any product, customer, employee or action associated with the scanned barcode.'),
|
||||
};
|
||||
|
||||
Registries.Component.add(ErrorBarcodePopup);
|
||||
|
||||
return ErrorBarcodePopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
odoo.define('point_of_sale.ErrorPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
// formerly ErrorPopupWidget
|
||||
class ErrorPopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
owl.onMounted(this.onMounted);
|
||||
}
|
||||
onMounted() {
|
||||
this.playSound('error');
|
||||
}
|
||||
}
|
||||
ErrorPopup.template = 'ErrorPopup';
|
||||
ErrorPopup.defaultProps = {
|
||||
confirmText: _lt('Ok'),
|
||||
title: _lt('Error'),
|
||||
body: '',
|
||||
cancelKey: false,
|
||||
};
|
||||
|
||||
Registries.Component.add(ErrorPopup);
|
||||
|
||||
return ErrorPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
odoo.define('point_of_sale.ErrorTracebackPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
// 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: _lt('Ok'),
|
||||
cancelText: _lt('Cancel'),
|
||||
confirmKey: false,
|
||||
title: _lt('Error with Traceback'),
|
||||
body: '',
|
||||
exitButtonIsShown: false,
|
||||
exitButtonText: _lt('Exit Pos'),
|
||||
exitButtonTrigger: 'close-pos'
|
||||
};
|
||||
|
||||
Registries.Component.add(ErrorTracebackPopup);
|
||||
|
||||
return ErrorTracebackPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
odoo.define('point_of_sale.MoneyDetailsPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
/**
|
||||
* Even if this component has a "confirm and cancel"-like buttons, this should not be an AbstractAwaitablePopup.
|
||||
* We currently cannot show two popups at the same time, what we do is mount this component with its parent
|
||||
* and hide it with some css. The confirm button will just trigger an event to the parent.
|
||||
*/
|
||||
class MoneyDetailsPopup extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.currency = this.env.pos.currency;
|
||||
this.state = useState({
|
||||
moneyDetails: Object.fromEntries(this.env.pos.bills.map(bill => ([bill.value, 0]))),
|
||||
total: 0,
|
||||
});
|
||||
if (this.props.manualInputCashCount) {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
get firstHalfMoneyDetails() {
|
||||
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
|
||||
return moneyDetailsKeys.slice(0, Math.ceil(moneyDetailsKeys.length/2));
|
||||
}
|
||||
get lastHalfMoneyDetails() {
|
||||
const moneyDetailsKeys = Object.keys(this.state.moneyDetails).sort((a, b) => a - b);
|
||||
return moneyDetailsKeys.slice(Math.ceil(moneyDetailsKeys.length/2), moneyDetailsKeys.length);
|
||||
}
|
||||
updateMoneyDetailsAmount() {
|
||||
let total = Object.entries(this.state.moneyDetails).reduce((total, money) => total + money[0] * money[1], 0);
|
||||
this.state.total = this.env.pos.round_decimals_currency(total);
|
||||
}
|
||||
confirm() {
|
||||
let moneyDetailsNotes = this.state.total ? 'Money details: \n' : null;
|
||||
this.env.pos.bills.forEach(bill => {
|
||||
if (this.state.moneyDetails[bill.value]) {
|
||||
moneyDetailsNotes += ` - ${this.state.moneyDetails[bill.value]} x ${this.env.pos.format_currency(bill.value)}\n`;
|
||||
}
|
||||
})
|
||||
const payload = { total: this.state.total, moneyDetailsNotes, moneyDetails: { ...this.state.moneyDetails } };
|
||||
this.props.onConfirm(payload);
|
||||
}
|
||||
reset() {
|
||||
for (let key in this.state.moneyDetails) { this.state.moneyDetails[key] = 0 }
|
||||
this.state.total = 0;
|
||||
}
|
||||
discard() {
|
||||
this.reset();
|
||||
this.props.onDiscard();
|
||||
}
|
||||
}
|
||||
|
||||
MoneyDetailsPopup.template = 'MoneyDetailsPopup';
|
||||
Registries.Component.add(MoneyDetailsPopup);
|
||||
|
||||
return MoneyDetailsPopup;
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
odoo.define('point_of_sale.NumberPopup', function(require) {
|
||||
'use strict';
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const NumberBuffer = require('point_of_sale.NumberBuffer');
|
||||
const { useListener } = require("@web/core/utils/hooks");
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState } = owl;
|
||||
|
||||
// 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.
|
||||
* @param {Boolean} props.isInputSelected Input is highlighted and will reset upon a change.
|
||||
*
|
||||
* Resolve to { confirmed, payload } when used with showPopup method.
|
||||
* @confirmed {Boolean}
|
||||
* @payload {String}
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
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().replace('.', this.decimalSeparator);
|
||||
}
|
||||
this.state = useState({ buffer: startingBuffer, toStartOver: this.props.isInputSelected });
|
||||
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;
|
||||
}
|
||||
}
|
||||
confirm(event) {
|
||||
if (NumberBuffer.get()) {
|
||||
super.confirm();
|
||||
}
|
||||
}
|
||||
sendInput(key) {
|
||||
this.trigger('numpad-click-input', { key });
|
||||
}
|
||||
getPayload() {
|
||||
return NumberBuffer.get();
|
||||
}
|
||||
}
|
||||
NumberPopup.template = 'NumberPopup';
|
||||
NumberPopup.defaultProps = {
|
||||
confirmText: _t('Ok'),
|
||||
cancelText: _t('Cancel'),
|
||||
title: _t('Confirm ?'),
|
||||
body: '',
|
||||
cheap: false,
|
||||
startingValue: null,
|
||||
isPassword: false,
|
||||
};
|
||||
|
||||
Registries.Component.add(NumberPopup);
|
||||
|
||||
return NumberPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
odoo.define('point_of_sale.OfflineErrorPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const ErrorPopup = require('point_of_sale.ErrorPopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
/**
|
||||
* 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: _lt('Ok'),
|
||||
cancelText: _lt('Cancel'),
|
||||
title: _lt('Offline Error'),
|
||||
body: _lt('Either the server is inaccessible or browser is not connected online.'),
|
||||
};
|
||||
|
||||
Registries.Component.add(OfflineErrorPopup);
|
||||
|
||||
return OfflineErrorPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
odoo.define('point_of_sale.OrderImportPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _lt } = require('@web/core/l10n/translation');
|
||||
|
||||
// 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: _lt('Ok'),
|
||||
cancelKey: false,
|
||||
body: '',
|
||||
};
|
||||
|
||||
Registries.Component.add(OrderImportPopup);
|
||||
|
||||
return OrderImportPopup;
|
||||
});
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
odoo.define('point_of_sale.PosPopupController', function(require) {
|
||||
'use strict';
|
||||
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const { useBus } = require('@web/core/utils/hooks');
|
||||
|
||||
/**
|
||||
* This component is responsible in controlling the popups. It does so
|
||||
* by coordinating with them thru the `env.posbus`. The basic steps follow:
|
||||
* 1. `showPopup` method triggers `show-popup` event resulting to the
|
||||
* mounting of the requested popup.
|
||||
* 2. When the popup is shown, the `confirm`/`cancel` method of the popup
|
||||
* will be called after the popup is used. `confirming`/`cancelling`
|
||||
* will trigger the `close-popup`, which this component also listens to,
|
||||
* resulting to closing of the popup.
|
||||
*
|
||||
* Furthermore, Pressing `confirmKey`/`cancelKey` which defaults to
|
||||
* 'Enter'/'Escape', will automatically `confirm`/`cancel` the `topPopup`.
|
||||
* This behavior is accomplished by listening to `keyup` event of the window.
|
||||
* When the `confirmKey`/`cancelKey` of the `topPopup` is pressed,
|
||||
* 'cancel-popup-{top-popup-id}'/'confirm-popup-{top-popup-id}' will be triggered
|
||||
* and since the popup is listening to that event (@see AbstractAwaitablePopup),
|
||||
* it will result to the call of `confirm`/`cancel` method.
|
||||
*
|
||||
* @typedef {{ id: number, resolve: Function, keepBehind?: boolean, cancelKey?: string, confirmKey?: string }} BasePopupProps
|
||||
* @typedef {{ name: string, component: AbstractAwaitablePopup, props: BasePopupProps, key: string }} Popup
|
||||
*/
|
||||
class PosPopupController extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
useBus(this.env.posbus, 'show-popup', this._showPopup);
|
||||
useBus(this.env.posbus, 'close-popup', this._closePopup);
|
||||
owl.useExternalListener(window, 'keyup', this._onWindowKeyup);
|
||||
this.popups = owl.useState([]);
|
||||
}
|
||||
_showPopup(event) {
|
||||
let { id, name, props, resolve } = event.detail;
|
||||
props = Object.assign(props || {}, { id, resolve });
|
||||
const component = this.constructor.components[name];
|
||||
if (!component) {
|
||||
throw new Error(`'${name}' is not found. Make sure the file is loaded and the component is properly registered using 'Registries.Component.add'.`);
|
||||
}
|
||||
if (component.dontShow) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
this.popups.push({
|
||||
name,
|
||||
component,
|
||||
props: this._constructPopupProps(component, props),
|
||||
key: `${name}-${id}`,
|
||||
});
|
||||
}
|
||||
_closePopup(event) {
|
||||
const { popupId, response } = event.detail;
|
||||
const index = this.popups.findIndex((popup) => popup.props.id == popupId);
|
||||
if (index != -1) {
|
||||
const popup = this.popups[index];
|
||||
popup.props.resolve(response);
|
||||
this.popups.splice(index, 1);
|
||||
}
|
||||
}
|
||||
_onWindowKeyup(event) {
|
||||
const eventIsFromInputField = event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA';
|
||||
const shouldHandleKey = this.topPopup && !eventIsFromInputField;
|
||||
if (!shouldHandleKey) return;
|
||||
|
||||
if (event.key === this.topPopup.props.cancelKey) {
|
||||
this.env.posbus.trigger(`cancel-popup-${this.topPopup.props.id}`);
|
||||
} else if (event.key === this.topPopup.props.confirmKey) {
|
||||
this.env.posbus.trigger(`confirm-popup-${this.topPopup.props.id}`);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* A popup can be cancelled/confirmed with 'Escape'/'Enter' key by default.
|
||||
* Also, if it's not the top popup, it is hidden from the view.
|
||||
* This can be overridden by the default props of the popop component
|
||||
* and the props used in requesting to show the popup.
|
||||
*
|
||||
* @param {AbstractAwaitablePopup} popupComponent
|
||||
* @param {Object} props
|
||||
* @returns {BasePopupProps}
|
||||
*/
|
||||
_constructPopupProps(popupComponent, props) {
|
||||
const defaultProps = popupComponent.defaultProps || {};
|
||||
return Object.assign(
|
||||
{
|
||||
keepBehind: false,
|
||||
cancelKey: 'Escape',
|
||||
confirmKey: 'Enter',
|
||||
},
|
||||
defaultProps,
|
||||
props
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @returns {boolean} Hide the element of this component when this returns false.
|
||||
*/
|
||||
isShown() {
|
||||
return this.popups.length > 0;
|
||||
}
|
||||
get topPopup() {
|
||||
return this.popups[this.popups.length - 1];
|
||||
}
|
||||
/**
|
||||
* By default, only show the top popup. But always show a popup if
|
||||
* `keepBehind` props is true. Meaning, if you have 2 popups, and
|
||||
* the bottom popup has `keepBehind = true`, then the bottom popup
|
||||
* will be visible if it's not blocked in the view by the top popup.
|
||||
*
|
||||
* @param {Popup} popup
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldShow(popup) {
|
||||
return this.topPopup === popup || popup.props.keepBehind;
|
||||
}
|
||||
}
|
||||
PosPopupController.template = 'point_of_sale.PosPopupController';
|
||||
Registries.Component.add(PosPopupController);
|
||||
|
||||
return PosPopupController;
|
||||
});
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
odoo.define('point_of_sale.ProductConfiguratorPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
const { useState, useSubEnv } = owl;
|
||||
|
||||
class ProductConfiguratorPopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
useSubEnv({ attribute_components: [] });
|
||||
}
|
||||
|
||||
getPayload() {
|
||||
var selected_attributes = [];
|
||||
var price_extra = 0.0;
|
||||
|
||||
this.env.attribute_components.forEach((attribute_component) => {
|
||||
let { value, extra } = attribute_component.getValue();
|
||||
selected_attributes.push(value);
|
||||
price_extra += extra;
|
||||
});
|
||||
|
||||
return {
|
||||
selected_attributes,
|
||||
price_extra,
|
||||
};
|
||||
}
|
||||
}
|
||||
ProductConfiguratorPopup.template = 'ProductConfiguratorPopup';
|
||||
Registries.Component.add(ProductConfiguratorPopup);
|
||||
|
||||
class BaseProductAttribute extends PosComponent {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.env.attribute_components.push(this);
|
||||
|
||||
this.attribute = this.props.attribute;
|
||||
this.values = this.attribute.values;
|
||||
this.state = useState({
|
||||
selected_value: parseFloat(this.values[0].id),
|
||||
custom_value: '',
|
||||
});
|
||||
}
|
||||
|
||||
getValue() {
|
||||
let selected_value = this.values.find((val) => val.id === parseFloat(this.state.selected_value));
|
||||
let value = selected_value.name;
|
||||
if (selected_value.is_custom && this.state.custom_value) {
|
||||
value += `: ${this.state.custom_value}`;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
extra: selected_value.price_extra
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RadioProductAttribute extends BaseProductAttribute {
|
||||
setup() {
|
||||
super.setup();
|
||||
owl.onMounted(this.onMounted);
|
||||
}
|
||||
onMounted() {
|
||||
// With radio buttons `t-model` selects the default input by searching for inputs with
|
||||
// a matching `value` attribute. In our case, we use `t-att-value` so `value` is
|
||||
// not found yet and no radio is selected by default.
|
||||
// We then manually select the first input of each radio attribute.
|
||||
$(this.el).find('input[type="radio"]:first').prop('checked', true);
|
||||
}
|
||||
}
|
||||
RadioProductAttribute.template = 'RadioProductAttribute';
|
||||
Registries.Component.add(RadioProductAttribute);
|
||||
|
||||
class SelectProductAttribute extends BaseProductAttribute { }
|
||||
SelectProductAttribute.template = 'SelectProductAttribute';
|
||||
Registries.Component.add(SelectProductAttribute);
|
||||
|
||||
class ColorProductAttribute extends BaseProductAttribute {}
|
||||
ColorProductAttribute.template = 'ColorProductAttribute';
|
||||
Registries.Component.add(ColorProductAttribute);
|
||||
|
||||
return {
|
||||
ProductConfiguratorPopup,
|
||||
BaseProductAttribute,
|
||||
RadioProductAttribute,
|
||||
SelectProductAttribute,
|
||||
ColorProductAttribute,
|
||||
};
|
||||
});
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
odoo.define('point_of_sale.ProductInfoPopup', function(require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
|
||||
/**
|
||||
* Props:
|
||||
* {
|
||||
* info: {object of data}
|
||||
* }
|
||||
*/
|
||||
class ProductInfoPopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
super.setup();
|
||||
Object.assign(this, this.props.info);
|
||||
}
|
||||
searchProduct(productName) {
|
||||
this.env.posbus.trigger('search-product-from-info-popup', productName);
|
||||
this.cancel()
|
||||
}
|
||||
_hasMarginsCostsAccessRights() {
|
||||
const isAccessibleToEveryUser = this.env.pos.config.is_margins_costs_accessible_to_every_user;
|
||||
const isCashierManager = this.env.pos.get_cashier().role === 'manager';
|
||||
return isAccessibleToEveryUser || isCashierManager;
|
||||
}
|
||||
}
|
||||
|
||||
ProductInfoPopup.template = 'ProductInfoPopup';
|
||||
ProductInfoPopup.defaultProps= { confirmKey: false };
|
||||
Registries.Component.add(ProductInfoPopup);
|
||||
|
||||
return ProductInfoPopup;
|
||||
});
|
||||