summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/static/src/js
diff options
context:
space:
mode:
authorJoren Van Onder <jov@odoo.com>2019-08-19 12:30:39 -0700
committerJoren Van Onder <jov@odoo.com>2019-08-23 13:33:28 -0700
commitcd4b10b004f69098b6cf8193a171bb3c14f22b97 (patch)
tree17c2e6f89f39408aa37b6b00196c6d19fabc85f8 /addons/point_of_sale/static/src/js
parent296c2ea13d206a10adb868b143120896d46e3519 (diff)
[IMP] point_of_sale: generic electronic payment interface
This is an interface that can be implemented to support some electronic payment method in the POS. It's extracted from the existing pos_iot code. To implement a payment method you: - Python: add a selection to the use_payment_terminal field in Python - JS: implement the PaymentInterface class - JS: register your PaymentInterface in point_of_sale.models: models.register_payment_method( 'use_payment_terminal_selection_value', ImplementedPaymentInterface ); See the documentation of PaymentInterface for details on what each method does and when it is called. Co-authored-by: Antoine Prieels <anp@odoo.com>
Diffstat (limited to 'addons/point_of_sale/static/src/js')
-rw-r--r--addons/point_of_sale/static/src/js/models.js119
-rw-r--r--addons/point_of_sale/static/src/js/payment.js95
-rw-r--r--addons/point_of_sale/static/src/js/screens.js139
3 files changed, 337 insertions, 16 deletions
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index c976c76c594..bd78fb1dee7 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -380,7 +380,7 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'pos.payment.method',
- fields: ['name', 'is_cash_count'],
+ fields: ['name', 'is_cash_count', 'use_payment_terminal'],
domain: function(self, tmp) {
return [['id', 'in', tmp.payment_method_ids]];
},
@@ -395,10 +395,15 @@ exports.PosModel = Backbone.Model.extend({
return a.id - b.id;
}
});
- self.payment_methods_by_id = {}
+ self.payment_methods_by_id = {};
_.each(self.payment_methods, function(payment_method) {
- self.payment_methods_by_id[payment_method.id] = payment_method
- })
+ self.payment_methods_by_id[payment_method.id] = payment_method;
+
+ var PaymentInterface = self.electronic_payment_interfaces[payment_method.use_payment_terminal];
+ if (PaymentInterface) {
+ payment_method.payment_terminal = new PaymentInterface(self, payment_method);
+ }
+ });
}
},{
model: 'account.fiscal.position',
@@ -1166,8 +1171,25 @@ exports.PosModel = Backbone.Model.extend({
}
},
+ electronic_payment_interfaces: {},
});
+/**
+ * Call this function to map your PaymentInterface implementation to
+ * the use_payment_terminal field. When the POS loads it will take
+ * care of instantiating your interface and setting it on the right
+ * payment methods.
+ *
+ * @param {string} use_payment_terminal - value used in the
+ * use_payment_terminal selection field
+ *
+ * @param {Object} ImplementedPaymentInterface - implemented
+ * PaymentInterface
+ */
+exports.register_payment_method = function(use_payment_terminal, ImplementedPaymentInterface) {
+ exports.PosModel.prototype.electronic_payment_interfaces[use_payment_terminal] = ImplementedPaymentInterface;
+};
+
// Add fields to the list of read fields when a model is loaded
// by the point of sale.
// e.g: module.load_fields("product.product",['price','category'])
@@ -2064,6 +2086,11 @@ exports.Paymentline = Backbone.Model.extend({
this.order = options.order;
this.amount = 0;
this.selected = false;
+ this.ticket = '';
+ this.payment_status = '';
+ this.card_type = '';
+ this.transaction_id = '';
+
if (options.json) {
this.init_from_JSON(options.json);
return;
@@ -2077,6 +2104,10 @@ exports.Paymentline = Backbone.Model.extend({
init_from_JSON: function(json){
this.amount = json.amount;
this.payment_method = this.pos.payment_methods_by_id[json.payment_method_id];
+ this.payment_status = json.payment_status;
+ this.ticket = json.ticket;
+ this.card_type = json.card_type;
+ this.transaction_id = json.transaction_id;
},
//sets the amount of money on this payment line
set_amount: function(value){
@@ -2097,13 +2128,45 @@ exports.Paymentline = Backbone.Model.extend({
this.trigger('change',this);
}
},
+ /**
+ * returns {string} payment status.
+ */
+ get_payment_status: function() {
+ return this.payment_status;
+ },
+
+ /**
+ * Set the new payment status.
+ *
+ * @param {string} value - new status.
+ */
+ set_payment_status: function(value) {
+ this.payment_status = value;
+ this.trigger('change', this);
+ },
+
+ /**
+ * Set additional info to be printed on the receipts. value should
+ * be compatible with both the QWeb and ESC/POS receipts.
+ *
+ * @param {string} value - receipt info
+ */
+ set_receipt_info: function(value) {
+ this.ticket += value;
+ this.trigger('change', this);
+ },
+
// returns the associated cashregister
//exports as JSON for server communication
export_as_JSON: function(){
return {
name: time.datetime_to_str(new Date()),
payment_method_id: this.payment_method.id,
- amount: this.get_amount()
+ amount: this.get_amount(),
+ payment_status: this.payment_status,
+ ticket: this.ticket,
+ card_type: this.card_type,
+ transaction_id: this.transaction_id,
};
},
//exports as JSON for receipt printing
@@ -2616,6 +2679,17 @@ exports.Order = Backbone.Model.extend({
get_paymentlines: function(){
return this.paymentlines.models;
},
+ /**
+ * Retrieve the paymentline with the specified cid
+ *
+ * @param {String} cid
+ */
+ get_paymentline: function (cid) {
+ var lines = this.get_paymentlines();
+ return lines.find(function (line) {
+ return line.cid === cid;
+ });
+ },
remove_paymentline: function(line){
this.assert_editable();
if(this.selected_paymentline === line){
@@ -2647,6 +2721,32 @@ exports.Order = Backbone.Model.extend({
this.trigger('change:selected_paymentline',this.selected_paymentline);
}
},
+ electronic_payment_in_progress: function() {
+ return this.get_paymentlines()
+ .some(function(pl) {
+ if (pl.payment_status) {
+ return !['done', 'reversed'].includes(pl.payment_status);
+ } else {
+ return false;
+ }
+ });
+ },
+ /**
+ * Stops a payment on the terminal if one is running
+ */
+ stop_electronic_payment: function () {
+ var lines = this.get_paymentlines();
+ var line = lines.find(function (line) {
+ var status = line.get_payment_status();
+ return status && !['done', 'reversed', 'reversing', 'pending', 'retry'].includes(status);
+ });
+ if (line) {
+ line.set_payment_status('waitingCancel');
+ line.payment_method.payment_terminal.send_payment_cancel(this, line.cid).finally(function () {
+ line.set_payment_status('retry');
+ });
+ }
+ },
/* ---- Payment Status --- */
get_subtotal : function(){
return round_pr(this.orderlines.reduce((function(sum, orderLine){
@@ -2702,7 +2802,14 @@ exports.Order = Backbone.Model.extend({
},
get_total_paid: function() {
return round_pr(this.paymentlines.reduce((function(sum, paymentLine) {
- return sum + paymentLine.get_amount();
+ if (paymentLine.get_payment_status()) {
+ if (paymentLine.get_payment_status() == 'done') {
+ sum += paymentLine.get_amount();
+ }
+ } else {
+ sum += paymentLine.get_amount();
+ }
+ return sum;
}), 0), this.pos.currency.rounding);
},
get_tax_details: function(){
diff --git a/addons/point_of_sale/static/src/js/payment.js b/addons/point_of_sale/static/src/js/payment.js
new file mode 100644
index 00000000000..ae73f552604
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/payment.js
@@ -0,0 +1,95 @@
+odoo.define('point_of_sale.PaymentInterface', function (require) {
+"use strict";
+
+var core = require('web.core');
+
+/**
+ * Implement this interface to support a new payment method in the POS:
+ *
+ * var PaymentInterface = require('point_of_sale.PaymentInterface');
+ * var MyPayment = PaymentInterface.extend({
+ * ...
+ * })
+ *
+ * To connect the interface to the right payment methods register it:
+ *
+ * var models = require('point_of_sale.models');
+ * models.register_payment_method('my_payment', MyPayment);
+ *
+ * my_payment is the technical name of the added selection in
+ * use_payment_terminal.
+ *
+ * If necessary new fields can be loaded on any model:
+ *
+ * models.load_fields('pos.payment.method', ['new_field1', 'new_field2']);
+ */
+var PaymentInterface = core.Class.extend({
+ init: function (pos, payment_method) {
+ this.pos = pos;
+ this.payment_method = payment_method;
+ this.supports_reversals = false;
+ },
+
+ /**
+ * Call this function to enable UI elements that allow a user to
+ * reverse a payment. This requires that you implement
+ * send_payment_reversal.
+ */
+ enable_reversals: function () {
+ this.supports_reversals = true;
+ },
+
+ /**
+ * Called when a user clicks the "Send" button in the
+ * interface. This should initiate a payment request and return a
+ * Promise that resolves when the final status of the payment line
+ * is set with set_payment_status.
+ *
+ * For successful transactions set_receipt_info() should be used
+ * to set info that should to be printed on the receipt. You
+ * should also set card_type and transaction_id on the line for
+ * successful transactions.
+ *
+ * @param {string} cid - The id of the paymentline
+ * @returns {Promise} resolved with a boolean that is false when
+ * the payment should be retried. Rejected when the status of the
+ * paymentline will be manually updated.
+ */
+ send_payment_request: function (cid) {},
+
+ /**
+ * Called when a user removes a payment line that's still waiting
+ * on send_payment_request to complete. Should execute some
+ * request to ensure the current payment request is
+ * cancelled. This is not to refund payments, only to cancel
+ * them. The payment line being cancelled will be deleted
+ * automatically after the returned promise resolves.
+ *
+ * @param {} order - The order of the paymentline
+ * @param {string} cid - The id of the paymentline
+ * @returns {Promise}
+ */
+ send_payment_cancel: function (order, cid) {},
+
+ /**
+ * This is an optional method. When implementing this make sure to
+ * call enable_reversals() in the constructor of your
+ * interface. This should reverse a previous payment with status
+ * 'done'. The paymentline will be removed based on returned
+ * Promise.
+ *
+ * @param {string} cid - The id of the paymentline
+ * @returns {Promise} returns true if the reversal was successful.
+ */
+ send_payment_reversal: function (cid) {},
+
+ /**
+ * Called when the payment screen in the POS is closed (by
+ * e.g. clicking the "Back" button). Could be used to cancel in
+ * progress payments.
+ */
+ close: function () {},
+});
+
+return PaymentInterface;
+});
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index d56e2f1fa27..b625a4f2bb0 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -131,6 +131,10 @@ var ScreenWidget = PosBaseWidget.extend({
if(this.pos.barcode_reader){
this.pos.barcode_reader.reset_action_callbacks();
}
+
+ if(this.payment_interface){
+ this.payment_interface.close();
+ }
},
// this methods hides the screen. It's not a good place to put your cleanup stuff as it is called on the
@@ -1869,6 +1873,13 @@ var PaymentScreenWidget = ScreenWidget.extend({
// handle both keyboard and numpad input. Accepts
// a string that represents the key pressed.
payment_input: function(input) {
+ var paymentline = this.pos.get_order().selected_paymentline;
+
+ // disable changing amount on paymentlines with running or done payments on a payment terminal
+ if (this.payment_interface && !['pending', 'retry'].includes(paymentline.get_payment_status())) {
+ return;
+ }
+
var newbuf = this.gui.numpad_input(this.inputbuffer, input, {'firstinput': this.firstinput});
this.firstinput = (newbuf.length === 0);
@@ -1881,14 +1892,14 @@ var PaymentScreenWidget = ScreenWidget.extend({
if (newbuf !== this.inputbuffer) {
this.inputbuffer = newbuf;
var order = this.pos.get_order();
- if (order.selected_paymentline) {
+ if (paymentline) {
var amount = this.inputbuffer;
if (this.inputbuffer !== "-") {
amount = field_utils.parse.float(this.inputbuffer);
}
- order.selected_paymentline.set_amount(amount);
+ paymentline.set_amount(amount);
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected .edit').text(this.format_currency_no_symbol(amount));
@@ -1923,8 +1934,16 @@ var PaymentScreenWidget = ScreenWidget.extend({
click_delete_paymentline: function(cid){
var lines = this.pos.get_order().get_paymentlines();
for ( var i = 0; i < lines.length; i++ ) {
- if (lines[i].cid === cid) {
- this.pos.get_order().remove_paymentline(lines[i]);
+ var line = lines[i];
+ if (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(lines[i].get_payment_status())) {
+ line.payment_method.payment_terminal.send_payment_cancel(this.pos.get_order(), cid);
+ }
+
+ this.pos.get_order().remove_paymentline(line);
this.reset_input();
this.render_paymentlines();
return;
@@ -1942,6 +1961,82 @@ var PaymentScreenWidget = ScreenWidget.extend({
}
}
},
+ /**
+ * link the proper functions to buttons for payment terminals
+ * send_payment_request, force_payment_done and cancel_payment.
+ */
+ render_payment_terminal: function() {
+ var self = this;
+ var order = this.pos.get_order();
+ if (!order) {
+ return;
+ }
+
+ this.$el.find('.send_payment_request').click(function () {
+ var cid = $(this).data('cid');
+ // Other payment lines can not be reversed anymore
+ order.get_paymentlines().forEach(function (line) {
+ line.can_be_reversed = false;
+ });
+
+ var line = self.pos.get_order().get_paymentline(cid);
+ var payment_terminal = line.payment_method.payment_terminal;
+ line.set_payment_status('waiting');
+ self.render_paymentlines();
+
+ payment_terminal.send_payment_request(cid).then(function (payment_successful) {
+ if (payment_successful) {
+ line.set_payment_status('done');
+ line.can_be_reversed = self.payment_interface.supports_reversals;
+ self.reset_input(); // in case somebody entered a tip the amount tendered should be updated
+ } else {
+ line.set_payment_status('retry');
+ }
+ }).finally(function () {
+ self.render_paymentlines();
+ });
+
+ self.render_paymentlines();
+ });
+ this.$el.find('.send_payment_cancel').click(function () {
+ var cid = $(this).data('cid');
+ var line = self.pos.get_order().get_paymentline($(this).data('cid'));
+ var payment_terminal = line.payment_method.payment_terminal;
+ line.set_payment_status('waitingCancel');
+ self.render_paymentlines();
+
+ payment_terminal.send_payment_cancel(self.pos.get_order(), cid).finally(function () {
+ line.set_payment_status('retry');
+ self.render_paymentlines();
+ });
+
+ self.render_paymentlines();
+ });
+ this.$el.find('.send_payment_reversal').click(function () {
+ var cid = $(this).data('cid');
+ var line = self.pos.get_order().get_paymentline($(this).data('cid'));
+ var payment_terminal = line.payment_method.payment_terminal;
+ line.set_payment_status('reversing');
+ self.render_paymentlines();
+
+ payment_terminal.send_payment_reversal(cid).then(function (reversal_successful) {
+ if (reversal_successful) {
+ line.set_amount(0);
+ line.set_payment_status('reversed');
+ } else {
+ line.set_payment_status('done');
+ }
+ self.render_paymentlines();
+ });
+ });
+
+ this.$el.find('.send_force_done').click(function () {
+ var line = self.pos.get_order().get_paymentline($(this).data('cid'));
+ var payment_terminal = line.payment_method.payment_terminal;
+ line.set_payment_status('done');
+ self.render_paymentlines();
+ });
+ },
render_paymentlines: function() {
var self = this;
var order = this.pos.get_order();
@@ -1969,20 +2064,38 @@ var PaymentScreenWidget = ScreenWidget.extend({
});
lines.appendTo(this.$('.paymentlines-container'));
+
+ this.render_payment_terminal();
},
compute_extradue: function (order) {
var lines = order.get_paymentlines();
var due = order.get_due();
- if (due && lines.length && due !== order.get_due(lines[lines.length-1])) {
+ if (due && lines.length && (due !== order.get_due(lines[lines.length-1]) || lines[lines.length - 1].payment_status === 'reversed')) {
return due;
}
return 0;
},
click_paymentmethods: function(id) {
- var payment_method = this.pos.payment_methods_by_id[id]
- this.pos.get_order().add_paymentline(payment_method);
- this.reset_input();
- this.render_paymentlines();
+ var payment_method = this.pos.payment_methods_by_id[id];
+ var order = this.pos.get_order();
+
+ if (order.electronic_payment_in_progress()) {
+ this.gui.show_popup('error',{
+ 'title': _t('Error'),
+ 'body': _t('There is already an electronic payment in progress.'),
+ });
+ } else {
+ order.add_paymentline(payment_method);
+ this.reset_input();
+
+ this.payment_interface = payment_method.payment_terminal;
+ if (this.payment_interface) {
+ order.selected_paymentline.set_payment_status('pending');
+ }
+
+ this.render_paymentlines();
+ this.order_changes();
+ }
},
render_paymentmethods: function() {
var self = this;
@@ -2082,6 +2195,10 @@ var PaymentScreenWidget = ScreenWidget.extend({
hide: function(){
$('body').off('keypress', this.keyboard_handler);
$('body').off('keydown', this.keyboard_keydown_handler);
+ var order = this.pos.get_order();
+ if (order) {
+ order.stop_electronic_payment();
+ }
this._super();
},
// sets up listeners to watch for order changes
@@ -2092,7 +2209,9 @@ var PaymentScreenWidget = ScreenWidget.extend({
return;
}
if(this.old_order){
- this.old_order.unbind(null,null,this);
+ this.old_order.stop_electronic_payment();
+ this.old_order.unbind(null, null, this);
+ this.old_order.paymentlines.unbind(null, null, this);
}
order.bind('all',function(){
self.order_changes();