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/Screens/PaymentScreen | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/point_of_sale/static/src/js/Screens/PaymentScreen')
7 files changed, 500 insertions, 0 deletions
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js new file mode 100644 index 00000000..b5dc6a7b --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PSNumpadInputButton.js @@ -0,0 +1,17 @@ +odoo.define('point_of_sale.PSNumpadInputButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PSNumpadInputButton extends PosComponent { + get _class() { + return this.props.changeClassTo || 'input-button number-char'; + } + } + PSNumpadInputButton.template = 'PSNumpadInputButton'; + + Registries.Component.add(PSNumpadInputButton); + + return PSNumpadInputButton; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentMethodButton.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentMethodButton.js new file mode 100644 index 00000000..8e5853d3 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentMethodButton.js @@ -0,0 +1,13 @@ +odoo.define('point_of_sale.PaymentMethodButton', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PaymentMethodButton extends PosComponent {} + PaymentMethodButton.template = 'PaymentMethodButton'; + + Registries.Component.add(PaymentMethodButton); + + return PaymentMethodButton; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js new file mode 100644 index 00000000..6fe25a11 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js @@ -0,0 +1,376 @@ +odoo.define('point_of_sale.PaymentScreen', function (require) { + 'use strict'; + + const { parse } = require('web.field_utils'); + const PosComponent = require('point_of_sale.PosComponent'); + const { useErrorHandlers } = require('point_of_sale.custom_hooks'); + const NumberBuffer = require('point_of_sale.NumberBuffer'); + const { useListener } = require('web.custom_hooks'); + const Registries = require('point_of_sale.Registries'); + const { onChangeOrder } = require('point_of_sale.custom_hooks'); + + class PaymentScreen extends PosComponent { + constructor() { + super(...arguments); + useListener('delete-payment-line', this.deletePaymentLine); + useListener('select-payment-line', this.selectPaymentLine); + useListener('new-payment-line', this.addNewPaymentLine); + useListener('update-selected-paymentline', this._updateSelectedPaymentline); + useListener('send-payment-request', this._sendPaymentRequest); + useListener('send-payment-cancel', this._sendPaymentCancel); + useListener('send-payment-reverse', this._sendPaymentReverse); + useListener('send-force-done', this._sendForceDone); + NumberBuffer.use({ + // The numberBuffer listens to this event to update its state. + // Basically means 'update the buffer when this event is triggered' + nonKeyboardInputEvent: 'input-from-numpad', + // When the buffer is updated, trigger this event. + // Note that the component listens to it. + triggerAtInput: 'update-selected-paymentline', + }); + onChangeOrder(this._onPrevOrder, this._onNewOrder); + useErrorHandlers(); + this.payment_interface = null; + this.error = false; + this.payment_methods_from_config = this.env.pos.payment_methods.filter(method => this.env.pos.config.payment_method_ids.includes(method.id)); + } + get currentOrder() { + return this.env.pos.get_order(); + } + get paymentLines() { + return this.currentOrder.get_paymentlines(); + } + get selectedPaymentLine() { + return this.currentOrder.selected_paymentline; + } + async selectClient() { + // IMPROVEMENT: This code snippet is repeated multiple times. + // Maybe it's better to create a function for it. + const currentClient = this.currentOrder.get_client(); + const { confirmed, payload: newClient } = await this.showTempScreen( + 'ClientListScreen', + { client: currentClient } + ); + if (confirmed) { + this.currentOrder.set_client(newClient); + this.currentOrder.updatePricelist(newClient); + } + } + addNewPaymentLine({ detail: paymentMethod }) { + // original function: click_paymentmethods + if (this.currentOrder.electronic_payment_in_progress()) { + this.showPopup('ErrorPopup', { + title: this.env._t('Error'), + body: this.env._t('There is already an electronic payment in progress.'), + }); + return false; + } else { + this.currentOrder.add_paymentline(paymentMethod); + NumberBuffer.reset(); + this.payment_interface = paymentMethod.payment_terminal; + if (this.payment_interface) { + this.currentOrder.selected_paymentline.set_payment_status('pending'); + } + return true; + } + } + _updateSelectedPaymentline() { + if (this.paymentLines.every((line) => line.paid)) { + this.currentOrder.add_paymentline(this.payment_methods_from_config[0]); + } + if (!this.selectedPaymentLine) return; // do nothing if no selected payment line + // disable changing amount on paymentlines with running or done payments on a payment terminal + if ( + this.payment_interface && + !['pending', 'retry'].includes(this.selectedPaymentLine.get_payment_status()) + ) { + return; + } + if (NumberBuffer.get() === null) { + this.deletePaymentLine({ detail: { cid: this.selectedPaymentLine.cid } }); + } else { + this.selectedPaymentLine.set_amount(NumberBuffer.getFloat()); + } + } + toggleIsToInvoice() { + // click_invoice + this.currentOrder.set_to_invoice(!this.currentOrder.is_to_invoice()); + this.render(); + } + openCashbox() { + this.env.pos.proxy.printer.open_cashbox(); + } + async addTip() { + // click_tip + const tip = this.currentOrder.get_tip(); + const change = this.currentOrder.get_change(); + let value = tip.toFixed(this.env.pos.decimals); + + if (tip === 0 && change > 0) { + value = change; + } + + const { confirmed, payload } = await this.showPopup('NumberPopup', { + title: tip ? this.env._t('Change Tip') : this.env._t('Add Tip'), + startingValue: value, + }); + + if (confirmed) { + this.currentOrder.set_tip(parse.float(payload)); + } + } + deletePaymentLine(event) { + const { cid } = event.detail; + const line = this.paymentLines.find((line) => line.cid === cid); + + // If a paymentline with a payment terminal linked to + // it is removed, the terminal should get a cancel + // request. + if (['waiting', 'waitingCard', 'timeout'].includes(line.get_payment_status())) { + line.payment_method.payment_terminal.send_payment_cancel(this.currentOrder, cid); + } + + this.currentOrder.remove_paymentline(line); + NumberBuffer.reset(); + this.render(); + } + selectPaymentLine(event) { + const { cid } = event.detail; + const line = this.paymentLines.find((line) => line.cid === cid); + this.currentOrder.select_paymentline(line); + NumberBuffer.reset(); + this.render(); + } + async validateOrder(isForceValidate) { + if(this.env.pos.config.cash_rounding) { + if(!this.env.pos.get_order().check_paymentlines_rounding()) { + this.showPopup('ErrorPopup', { + title: this.env._t('Rounding error in payment lines'), + body: this.env._t("The amount of your payment lines must be rounded to validate the transaction."), + }); + return; + } + } + if (await this._isOrderValid(isForceValidate)) { + // remove pending payments before finalizing the validation + for (let line of this.paymentLines) { + if (!line.is_done()) this.currentOrder.remove_paymentline(line); + } + await this._finalizeValidation(); + } + } + async _finalizeValidation() { + if ((this.currentOrder.is_paid_with_cash() || this.currentOrder.get_change()) && this.env.pos.config.iface_cashdrawer) { + this.env.pos.proxy.printer.open_cashbox(); + } + + this.currentOrder.initialize_validation_date(); + this.currentOrder.finalized = true; + + let syncedOrderBackendIds = []; + + try { + if (this.currentOrder.is_to_invoice()) { + syncedOrderBackendIds = await this.env.pos.push_and_invoice_order( + this.currentOrder + ); + } else { + syncedOrderBackendIds = await this.env.pos.push_single_order(this.currentOrder); + } + } catch (error) { + if (error.code == 700) + this.error = true; + if (error instanceof Error) { + throw error; + } else { + await this._handlePushOrderError(error); + } + } + if (syncedOrderBackendIds.length && this.currentOrder.wait_for_push_order()) { + const result = await this._postPushOrderResolve( + this.currentOrder, + syncedOrderBackendIds + ); + if (!result) { + await this.showPopup('ErrorPopup', { + title: 'Error: no internet connection.', + body: error, + }); + } + } + + this.showScreen(this.nextScreen); + + // If we succeeded in syncing the current order, and + // there are still other orders that are left unsynced, + // we ask the user if he is willing to wait and sync them. + if (syncedOrderBackendIds.length && this.env.pos.db.get_orders().length) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: this.env._t('Remaining unsynced orders'), + body: this.env._t( + 'There are unsynced orders. Do you want to sync these orders?' + ), + }); + if (confirmed) { + // NOTE: Not yet sure if this should be awaited or not. + // If awaited, some operations like changing screen + // might not work. + this.env.pos.push_orders(); + } + } + } + get nextScreen() { + return !this.error? 'ReceiptScreen' : 'ProductScreen'; + } + async _isOrderValid(isForceValidate) { + if (this.currentOrder.get_orderlines().length === 0) { + this.showPopup('ErrorPopup', { + title: this.env._t('Empty Order'), + body: this.env._t( + 'There must be at least one product in your order before it can be validated' + ), + }); + return false; + } + + if (this.currentOrder.is_to_invoice() && !this.currentOrder.get_client()) { + const { confirmed } = await this.showPopup('ConfirmPopup', { + title: this.env._t('Please select the Customer'), + body: this.env._t( + 'You need to select the customer before you can invoice an order.' + ), + }); + if (confirmed) { + this.selectClient(); + } + return false; + } + + if (!this.currentOrder.is_paid() || this.invoicing) { + return false; + } + + if (this.currentOrder.has_not_valid_rounding()) { + var line = this.currentOrder.has_not_valid_rounding(); + this.showPopup('ErrorPopup', { + title: this.env._t('Incorrect rounding'), + body: this.env._t( + 'You have to round your payments lines.' + line.amount + ' is not rounded.' + ), + }); + return false; + } + + // The exact amount must be paid if there is no cash payment method defined. + if ( + Math.abs( + this.currentOrder.get_total_with_tax() - this.currentOrder.get_total_paid() + this.currentOrder.get_rounding_applied() + ) > 0.00001 + ) { + var cash = false; + for (var i = 0; i < this.env.pos.payment_methods.length; i++) { + cash = cash || this.env.pos.payment_methods[i].is_cash_count; + } + if (!cash) { + this.showPopup('ErrorPopup', { + title: this.env._t('Cannot return change without a cash payment method'), + body: this.env._t( + 'There is no cash payment method available in this point of sale to handle the change.\n\n Please pay the exact amount or add a cash payment method in the point of sale configuration' + ), + }); + return false; + } + } + + // if the change is too large, it's probably an input error, make the user confirm. + if ( + !isForceValidate && + this.currentOrder.get_total_with_tax() > 0 && + this.currentOrder.get_total_with_tax() * 1000 < this.currentOrder.get_total_paid() + ) { + this.showPopup('ConfirmPopup', { + title: this.env._t('Please Confirm Large Amount'), + body: + this.env._t('Are you sure that the customer wants to pay') + + ' ' + + this.env.pos.format_currency(this.currentOrder.get_total_paid()) + + ' ' + + this.env._t('for an order of') + + ' ' + + this.env.pos.format_currency(this.currentOrder.get_total_with_tax()) + + ' ' + + this.env._t('? Clicking "Confirm" will validate the payment.'), + }).then(({ confirmed }) => { + if (confirmed) this.validateOrder(true); + }); + return false; + } + + return true; + } + async _postPushOrderResolve(order, order_server_ids) { + return true; + } + async _sendPaymentRequest({ detail: line }) { + // Other payment lines can not be reversed anymore + this.paymentLines.forEach(function (line) { + line.can_be_reversed = false; + }); + + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status('waiting'); + + const isPaymentSuccessful = await payment_terminal.send_payment_request(line.cid); + if (isPaymentSuccessful) { + line.set_payment_status('done'); + line.can_be_reversed = this.payment_interface.supports_reversals; + } else { + line.set_payment_status('retry'); + } + } + async _sendPaymentCancel({ detail: line }) { + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status('waitingCancel'); + const isCancelSuccessful = await payment_terminal.send_payment_cancel(this.currentOrder, line.cid); + if (isCancelSuccessful) { + line.set_payment_status('retry'); + } else { + line.set_payment_status('waitingCard'); + } + } + async _sendPaymentReverse({ detail: line }) { + const payment_terminal = line.payment_method.payment_terminal; + line.set_payment_status('reversing'); + + const isReversalSuccessful = await payment_terminal.send_payment_reversal(line.cid); + if (isReversalSuccessful) { + line.set_amount(0); + line.set_payment_status('reversed'); + } else { + line.can_be_reversed = false; + line.set_payment_status('done'); + } + } + async _sendForceDone({ detail: line }) { + line.set_payment_status('done'); + } + _onPrevOrder(prevOrder) { + prevOrder.off('change', null, this); + prevOrder.paymentlines.off('change', null, this); + if (prevOrder) { + prevOrder.stop_electronic_payment(); + } + } + async _onNewOrder(newOrder) { + newOrder.on('change', this.render, this); + newOrder.paymentlines.on('change', this.render, this); + NumberBuffer.reset(); + await this.render(); + } + } + PaymentScreen.template = 'PaymentScreen'; + + Registries.Component.add(PaymentScreen); + + return PaymentScreen; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenElectronicPayment.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenElectronicPayment.js new file mode 100644 index 00000000..6cafac15 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenElectronicPayment.js @@ -0,0 +1,23 @@ +odoo.define('point_of_sale.PaymentScreenElectronicPayment', function (require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PaymentScreenElectronicPayment extends PosComponent { + mounted() { + this.props.line.on('change', this.render, this); + } + willUnmount() { + if (this.props.line) { + // It could be that the line is deleted before unmounting the element. + this.props.line.off('change', null, this); + } + } + } + PaymentScreenElectronicPayment.template = 'PaymentScreenElectronicPayment'; + + Registries.Component.add(PaymentScreenElectronicPayment); + + return PaymentScreenElectronicPayment; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js new file mode 100644 index 00000000..e661722f --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenNumpad.js @@ -0,0 +1,18 @@ +odoo.define('point_of_sale.PaymentScreenNumpad', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PaymentScreenNumpad extends PosComponent { + constructor() { + super(...arguments); + this.decimalPoint = this.env._t.database.parameters.decimal_point; + } + } + PaymentScreenNumpad.template = 'PaymentScreenNumpad'; + + Registries.Component.add(PaymentScreenNumpad); + + return PaymentScreenNumpad; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js new file mode 100644 index 00000000..8f231146 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenPaymentLines.js @@ -0,0 +1,23 @@ +odoo.define('point_of_sale.PaymentScreenPaymentLines', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PaymentScreenPaymentLines extends PosComponent { + formatLineAmount(paymentline) { + return this.env.pos.format_currency_no_symbol(paymentline.get_amount()); + } + selectedLineClass(line) { + return { 'payment-terminal': line.get_payment_status() }; + } + unselectedLineClass(line) { + return {}; + } + } + PaymentScreenPaymentLines.template = 'PaymentScreenPaymentLines'; + + Registries.Component.add(PaymentScreenPaymentLines); + + return PaymentScreenPaymentLines; +}); diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js new file mode 100644 index 00000000..12ccaa84 --- /dev/null +++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreenStatus.js @@ -0,0 +1,30 @@ +odoo.define('point_of_sale.PaymentScreenStatus', function(require) { + 'use strict'; + + const PosComponent = require('point_of_sale.PosComponent'); + const Registries = require('point_of_sale.Registries'); + + class PaymentScreenStatus extends PosComponent { + get changeText() { + return this.env.pos.format_currency(this.currentOrder.get_change()); + } + get totalDueText() { + return this.env.pos.format_currency( + this.currentOrder.get_total_with_tax() + this.currentOrder.get_rounding_applied() + ); + } + get remainingText() { + return this.env.pos.format_currency( + this.currentOrder.get_due() > 0 ? this.currentOrder.get_due() : 0 + ); + } + get currentOrder() { + return this.env.pos.get_order(); + } + } + PaymentScreenStatus.template = 'PaymentScreenStatus'; + + Registries.Component.add(PaymentScreenStatus); + + return PaymentScreenStatus; +}); |
