diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/point_of_sale/static/src/js/Popups | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/src/js/Popups')
14 files changed, 652 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js new file mode 100644 index 00000000..6cdd6a04 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/AbstractAwaitablePopup.js @@ -0,0 +1,60 @@ +odoo.define('point_of_sale.AbstractAwaitablePopup', function (require) { + 'use strict'; + + const { useExternalListener } = owl.hooks; + const PosComponent = require('point_of_sale.PosComponent'); + + /** + * Implement this abstract class by extending it like so: + * ```js + * class ConcretePopup extends AbstractAwaitablePopup { + * async getPayload() { + * return 'result'; + * } + * } + * ConcretePopup.template = owl.tags.xml` + * <div> + * <button t-on-click="confirm">Okay</button> + * <button t-on-click="cancel">Cancel</button> + * </div> + * ` + * ``` + * + * The concrete popup can now be instantiated and be awaited for + * the user's response like so: + * ```js + * const { confirmed, payload } = await this.showPopup('ConcretePopup'); + * // based on the implementation above, + * // if confirmed, payload = 'result' + * // otherwise, payload = null + * ``` + */ + class AbstractAwaitablePopup extends PosComponent { + constructor() { + super(...arguments); + useExternalListener(window, 'keyup', this._cancelAtEscape); + } + async confirm() { + this.props.resolve({ confirmed: true, payload: await this.getPayload() }); + this.trigger('close-popup'); + } + cancel() { + this.props.resolve({ confirmed: false, payload: null }); + this.trigger('close-popup'); + } + _cancelAtEscape(event) { + if (event.key === 'Escape') { + this.cancel(); + } + } + /** + * Override this in the concrete popup implementation to set the + * payload when the popup is confirmed. + */ + async getPayload() { + return null; + } + } + + return AbstractAwaitablePopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js new file mode 100644 index 00000000..e22c1aaa --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ConfirmPopup.js @@ -0,0 +1,20 @@ +odoo.define('point_of_sale.ConfirmPopup', function(require) { + 'use strict'; + + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly ConfirmPopupWidget + class ConfirmPopup extends AbstractAwaitablePopup {} + ConfirmPopup.template = 'ConfirmPopup'; + ConfirmPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: 'Confirm ?', + body: '', + }; + + Registries.Component.add(ConfirmPopup); + + return ConfirmPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/EditListInput.js b/addons/point_of_sale/static/src/js/Popups/EditListInput.js new file mode 100644 index 00000000..09b39f21 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/EditListInput.js @@ -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; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/EditListPopup.js b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js new file mode 100644 index 00000000..ac4b262d --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/EditListPopup.js @@ -0,0 +1,105 @@ +odoo.define('point_of_sale.EditListPopup', function(require) { + 'use strict'; + + const { useState } = owl.hooks; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + const { useAutoFocusToLast } = require('point_of_sale.custom_hooks'); + + /** + * Given a array of { id, text }, we show the user this popup to be able to modify this given array. + * (used to replace PackLotLinePopupWidget) + * + * The expected return of showPopup when this popup is used is an array of { _id, [id], text }. + * - _id is the assigned unique identifier for each item. + * - id is the original id. if not provided, then it means that the item is new. + * - text is the modified/unmodified text. + * + * Example: + * + * ``` + * -- perhaps inside a click handler -- + * // gather the items to edit + * const names = [{ id: 1, text: 'Joseph'}, { id: 2, text: 'Kaykay' }]; + * + * // supply the items to the popup and wait for user's response + * // when user pressed `confirm` in the popup, the changes he made will be returned by the showPopup function. + * const { confirmed, payload: newNames } = await this.showPopup('EditListPopup', { + * title: "Can you confirm this item?", + * array: names }) + * + * // we then consume the new data. In this example, it is only logged. + * if (confirmed) { + * console.log(newNames); + * // the above might log the following: + * // [{ _id: 1, id: 1, text: 'Joseph Caburnay' }, { _id: 2, id: 2, 'Kaykay' }, { _id: 3, 'James' }] + * // The result showed that the original item with id=1 was changed to have text 'Joseph Caburnay' from 'Joseph' + * // The one with id=2 did not change. And a new item with text='James' is added. + * } + * ``` + */ + class EditListPopup extends AbstractAwaitablePopup { + /** + * @param {String} title required title of popup + * @param {Array} [props.array=[]] the array of { id, text } to be edited or an array of strings + * @param {Boolean} [props.isSingleItem=false] true if only allowed to edit single item (the first item) + */ + constructor() { + super(...arguments); + this._id = 0; + this.state = useState({ array: this._initialize(this.props.array) }); + useAutoFocusToLast(); + } + _nextId() { + return this._id++; + } + _emptyItem() { + return { + text: '', + _id: this._nextId(), + }; + } + _initialize(array) { + // If no array is provided, we initialize with one empty item. + if (array.length === 0) return [this._emptyItem()]; + // Put _id for each item. It will serve as unique identifier of each item. + return array.map((item) => Object.assign({}, { _id: this._nextId() }, typeof item === 'object'? item: { 'text': item})); + } + removeItem(event) { + const itemToRemove = event.detail; + this.state.array.splice( + this.state.array.findIndex(item => item._id == itemToRemove._id), + 1 + ); + // We keep a minimum of one empty item in the popup. + if (this.state.array.length === 0) { + this.state.array.push(this._emptyItem()); + } + } + createNewItem() { + if (this.props.isSingleItem) return; + this.state.array.push(this._emptyItem()); + } + /** + * @override + */ + getPayload() { + return { + newArray: this.state.array + .filter((item) => item.text.trim() !== '') + .map((item) => Object.assign({}, item)), + }; + } + } + EditListPopup.template = 'EditListPopup'; + EditListPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + array: [], + isSingleItem: false, + }; + + Registries.Component.add(EditListPopup); + + return EditListPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js new file mode 100644 index 00000000..8cf11c40 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js @@ -0,0 +1,26 @@ +odoo.define('point_of_sale.ErrorBarcodePopup', function(require) { + 'use strict'; + + const ErrorPopup = require('point_of_sale.ErrorPopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly ErrorBarcodePopupWidget + class ErrorBarcodePopup extends ErrorPopup { + get translatedMessage() { + return this.env._t(this.props.message); + } + } + ErrorBarcodePopup.template = 'ErrorBarcodePopup'; + ErrorBarcodePopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: 'Error', + body: '', + message: + 'The Point of Sale could not find any product, client, employee or action associated with the scanned barcode.', + }; + + Registries.Component.add(ErrorBarcodePopup); + + return ErrorBarcodePopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js new file mode 100644 index 00000000..865779c4 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ErrorPopup.js @@ -0,0 +1,24 @@ +odoo.define('point_of_sale.ErrorPopup', function(require) { + 'use strict'; + + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly ErrorPopupWidget + class ErrorPopup extends AbstractAwaitablePopup { + mounted() { + this.playSound('error'); + } + } + ErrorPopup.template = 'ErrorPopup'; + ErrorPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: 'Error', + body: '', + }; + + Registries.Component.add(ErrorPopup); + + return ErrorPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js new file mode 100644 index 00000000..1af25e42 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js @@ -0,0 +1,44 @@ +odoo.define('point_of_sale.ErrorTracebackPopup', function(require) { + 'use strict'; + + const ErrorPopup = require('point_of_sale.ErrorPopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly ErrorTracebackPopupWidget + class ErrorTracebackPopup extends ErrorPopup { + get tracebackUrl() { + const blob = new Blob([this.props.body]); + const URL = window.URL || window.webkitURL; + return URL.createObjectURL(blob); + } + get tracebackFilename() { + return `${this.env._t('error')} ${moment().format('YYYY-MM-DD-HH-mm-ss')}.txt`; + } + emailTraceback() { + const address = this.env.pos.company.email; + const subject = this.env._t('IMPORTANT: Bug Report From Odoo Point Of Sale'); + window.open( + 'mailto:' + + address + + '?subject=' + + (subject ? window.encodeURIComponent(subject) : '') + + '&body=' + + (this.props.body ? window.encodeURIComponent(this.props.body) : '') + ); + } + } + ErrorTracebackPopup.template = 'ErrorTracebackPopup'; + ErrorTracebackPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: 'Error with Traceback', + body: '', + exitButtonIsShown: false, + exitButtonText: 'Exit Pos', + exitButtonTrigger: 'close-pos' + }; + + Registries.Component.add(ErrorTracebackPopup); + + return ErrorTracebackPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/NumberPopup.js b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js new file mode 100644 index 00000000..bf63ba8d --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/NumberPopup.js @@ -0,0 +1,79 @@ +odoo.define('point_of_sale.NumberPopup', function(require) { + 'use strict'; + var core = require('web.core'); + var _t = core._t; + + const { useState } = owl; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const NumberBuffer = require('point_of_sale.NumberBuffer'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + + // formerly NumberPopupWidget + class NumberPopup extends AbstractAwaitablePopup { + /** + * @param {Object} props + * @param {Boolean} props.isPassword Show password popup. + * @param {number|null} props.startingValue Starting value of the popup. + * + * Resolve to { confirmed, payload } when used with showPopup method. + * @confirmed {Boolean} + * @payload {String} + */ + constructor() { + super(...arguments); + useListener('accept-input', this.confirm); + useListener('close-this-popup', this.cancel); + let startingBuffer = ''; + if (typeof this.props.startingValue === 'number' && this.props.startingValue > 0) { + startingBuffer = this.props.startingValue.toString(); + } + this.state = useState({ buffer: startingBuffer }); + NumberBuffer.use({ + nonKeyboardInputEvent: 'numpad-click-input', + triggerAtEnter: 'accept-input', + triggerAtEscape: 'close-this-popup', + state: this.state, + }); + } + get decimalSeparator() { + return this.env._t.database.parameters.decimal_point; + } + get inputBuffer() { + if (this.state.buffer === null) { + return ''; + } + if (this.props.isPassword) { + return this.state.buffer.replace(/./g, '•'); + } else { + return this.state.buffer; + } + } + confirm(event) { + const bufferState = event.detail; + if (bufferState.buffer !== '') { + 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; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js new file mode 100644 index 00000000..147ed7c4 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/OfflineErrorPopup.js @@ -0,0 +1,29 @@ +odoo.define('point_of_sale.OfflineErrorPopup', function(require) { + 'use strict'; + + const ErrorPopup = require('point_of_sale.ErrorPopup'); + const Registries = require('point_of_sale.Registries'); + + /** + * This is a special kind of error popup as it introduces + * an option to not show it again. + */ + class OfflineErrorPopup extends ErrorPopup { + dontShowAgain() { + this.constructor.dontShow = true; + this.cancel(); + } + } + OfflineErrorPopup.template = 'OfflineErrorPopup'; + OfflineErrorPopup.dontShow = false; + OfflineErrorPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: 'Offline Error', + body: 'Either the server is inaccessible or browser is not connected online.', + }; + + Registries.Component.add(OfflineErrorPopup); + + return OfflineErrorPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js new file mode 100644 index 00000000..c2c35291 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/OrderImportPopup.js @@ -0,0 +1,27 @@ +odoo.define('point_of_sale.OrderImportPopup', function(require) { + 'use strict'; + + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly OrderImportPopupWidget + class OrderImportPopup extends AbstractAwaitablePopup { + get unpaidSkipped() { + return ( + (this.props.report.unpaid_skipped_existing || 0) + + (this.props.report.unpaid_skipped_session || 0) + ); + } + getPayload() {} + } + OrderImportPopup.template = 'OrderImportPopup'; + OrderImportPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + body: '', + }; + + Registries.Component.add(OrderImportPopup); + + return OrderImportPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js new file mode 100644 index 00000000..b04e55d8 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/ProductConfiguratorPopup.js @@ -0,0 +1,89 @@ +odoo.define('point_of_sale.ProductConfiguratorPopup', function(require) { + 'use strict'; + + const { useState, useSubEnv } = owl.hooks; + const PosComponent = require('point_of_sale.PosComponent'); + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + class ProductConfiguratorPopup extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + 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 { + constructor() { + super(...arguments); + + 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 { + mounted() { + // 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, + }; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js new file mode 100644 index 00000000..5321fdea --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/SelectionPopup.js @@ -0,0 +1,57 @@ +odoo.define('point_of_sale.SelectionPopup', function (require) { + 'use strict'; + + const { useState } = owl.hooks; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly SelectionPopupWidget + class SelectionPopup extends AbstractAwaitablePopup { + /** + * Value of the `item` key of the selected element in the Selection + * Array is the payload of this popup. + * + * @param {Object} props + * @param {String} [props.confirmText='Confirm'] + * @param {String} [props.cancelText='Cancel'] + * @param {String} [props.title='Select'] + * @param {String} [props.body=''] + * @param {Array<Selection>} [props.list=[]] + * Selection { + * id: integer, + * label: string, + * isSelected: boolean, + * item: any, + * } + */ + constructor() { + super(...arguments); + this.state = useState({ selectedId: this.props.list.find((item) => item.isSelected) }); + } + selectItem(itemId) { + this.state.selectedId = itemId; + this.confirm(); + } + /** + * We send as payload of the response the selected item. + * + * @override + */ + getPayload() { + const selected = this.props.list.find((item) => this.state.selectedId === item.id); + return selected && selected.item; + } + } + SelectionPopup.template = 'SelectionPopup'; + SelectionPopup.defaultProps = { + confirmText: 'Confirm', + cancelText: 'Cancel', + title: 'Select', + body: '', + list: [], + }; + + Registries.Component.add(SelectionPopup); + + return SelectionPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js new file mode 100644 index 00000000..1f2735f6 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/TextAreaPopup.js @@ -0,0 +1,39 @@ +odoo.define('point_of_sale.TextAreaPopup', function(require) { + 'use strict'; + + const { useState, useRef } = owl.hooks; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly TextAreaPopupWidget + // IMPROVEMENT: This code is very similar to TextInputPopup. + // Combining them would reduce the code. + class TextAreaPopup extends AbstractAwaitablePopup { + /** + * @param {Object} props + * @param {string} props.startingValue + */ + constructor() { + super(...arguments); + this.state = useState({ inputValue: this.props.startingValue }); + this.inputRef = useRef('input'); + } + mounted() { + this.inputRef.el.focus(); + } + getPayload() { + return this.state.inputValue; + } + } + TextAreaPopup.template = 'TextAreaPopup'; + TextAreaPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: '', + body: '', + }; + + Registries.Component.add(TextAreaPopup); + + return TextAreaPopup; +}); diff --git a/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js new file mode 100644 index 00000000..4a0612d2 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Popups/TextInputPopup.js @@ -0,0 +1,34 @@ +odoo.define('point_of_sale.TextInputPopup', function(require) { + 'use strict'; + + const { useState, useRef } = owl.hooks; + const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup'); + const Registries = require('point_of_sale.Registries'); + + // formerly TextInputPopupWidget + class TextInputPopup extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.state = useState({ inputValue: this.props.startingValue }); + this.inputRef = useRef('input'); + } + mounted() { + this.inputRef.el.focus(); + } + getPayload() { + return this.state.inputValue; + } + } + TextInputPopup.template = 'TextInputPopup'; + TextInputPopup.defaultProps = { + confirmText: 'Ok', + cancelText: 'Cancel', + title: '', + body: '', + startingValue: '', + }; + + Registries.Component.add(TextInputPopup); + + return TextInputPopup; +}); |
