From 49e9bb0acc6743eed202ab5a89c2def828cd77d3 Mon Sep 17 00:00:00 2001 From: andreparames Date: Fri, 9 Feb 2018 12:04:38 +0100 Subject: [PATCH 1/6] pos_payment_terminal: receive transaction refs Provide a mechanism to send the order UID to the payment terminal, which can then pass the transaction reference generated by the payment provider back to Odoo, and which is then added to the order payment lines. This allows their subsequent reconciliation. The order is also automatically validated when the payment finishes. --- pos_payment_terminal/models/__init__.py | 1 + pos_payment_terminal/models/pos_order.py | 60 +++++++++++++++++++ .../static/src/js/pos_payment_terminal.js | 38 +++++++++++- pos_payment_terminal/tests/__init__.py | 1 + .../tests/test_transactions.py | 49 +++++++++++++++ 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 pos_payment_terminal/models/pos_order.py create mode 100644 pos_payment_terminal/tests/__init__.py create mode 100644 pos_payment_terminal/tests/test_transactions.py diff --git a/pos_payment_terminal/models/__init__.py b/pos_payment_terminal/models/__init__.py index 3b0e3a1a..819b3c20 100644 --- a/pos_payment_terminal/models/__init__.py +++ b/pos_payment_terminal/models/__init__.py @@ -2,3 +2,4 @@ from . import pos_config from . import account_journal +from . import pos_order diff --git a/pos_payment_terminal/models/pos_order.py b/pos_payment_terminal/models/pos_order.py new file mode 100644 index 00000000..723a992b --- /dev/null +++ b/pos_payment_terminal/models/pos_order.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# © 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections import defaultdict +import logging + +from odoo import models, api + +_logger = logging.getLogger(__name__) + + +class PosOrder(models.Model): + _inherit = 'pos.order' + + @api.model + def _match_transactions_to_payments(self, pos_order): + payments = pos_order['statement_ids'] + transactions = pos_order['transactions'] + card_journals = self.env['account.journal'].search([ + ('id', 'in', [p[2]['journal_id'] for p in payments]), + ('payment_mode', '!=', False), + ]) + card_payments = [record[2] for record in payments + if record[2]['journal_id'] in card_journals.ids] + + def amount_cents(obj): + if 'amount_cents' in obj: + return obj['amount_cents'] + else: + return int(round(obj['amount'] * 100)) + + try: + for payment, transaction in match(card_payments, transactions, + key=amount_cents): + payment['note'] = transaction['reference'] + except ValueError as e: + _logger.error("Error matching transactions to payments: %s", + e.args[0]) + + def _process_order(self, pos_order): + if pos_order.get('transactions'): + self._match_transactions_to_payments(pos_order) + return super(PosOrder, self)._process_order(pos_order) + + +def group_by(lists, key): + count = range(len(lists)) + d = defaultdict(lambda: tuple([[] for _ in count])) + for i, objects in enumerate(lists): + for obj in objects: + d[key(obj)][i].append(obj) + return d + + +def match(al, bl, key): + for key, groups in group_by((al, bl), key).items(): + if groups[0] and len(groups[0]) != len(groups[1]): + raise ValueError("Missing value for {!r}".format(key)) + for val in zip(*groups): + yield val diff --git a/pos_payment_terminal/static/src/js/pos_payment_terminal.js b/pos_payment_terminal/static/src/js/pos_payment_terminal.js index ba99956a..adace081 100755 --- a/pos_payment_terminal/static/src/js/pos_payment_terminal.js +++ b/pos_payment_terminal/static/src/js/pos_payment_terminal.js @@ -20,9 +20,33 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { models.load_fields('account.journal', ['payment_mode']); devices.ProxyDevice.include({ + init: function(parents, options) { + var self = this; + self._super(parents, options); + self.on('change:status', this, function(eh, status) { + var drivers = status.newValue.drivers; + var order = self.pos.get_order(); + Object.keys(drivers).forEach(function(driver_name) { + var transactions = drivers[driver_name].latest_transactions; + if(!!transactions && transactions.hasOwnProperty(order.uid)) { + order.transactions = transactions[order.uid]; + var order_total = Math.round(order.get_total_with_tax() * 100.0); + var paid_total = order.transactions.map(function(t) { + return t.amount_cents; + }).reduce(function add(a, b) { + return a + b; + }, 0); + if(order_total === paid_total) { + self.pos.chrome.screens.payment.validate_order(); + } + } + }); + }); + }, payment_terminal_transaction_start: function(line_cid, currency_iso, currency_decimals){ var line; - var lines = this.pos.get_order().get_paymentlines(); + var order = this.pos.get_order(); + var lines = order.get_paymentlines(); for ( var i = 0; i < lines.length; i++ ) { if (lines[i].cid === line_cid) { line = lines[i]; @@ -32,7 +56,8 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { var data = {'amount' : line.get_amount(), 'currency_iso' : currency_iso, 'currency_decimals' : currency_decimals, - 'payment_mode' : line.cashregister.journal.payment_mode}; + 'payment_mode' : line.cashregister.journal.payment_mode, + 'order_id': order.uid}; //console.log(JSON.stringify(data)); this.message('payment_terminal_transaction_start', {'payment_info' : JSON.stringify(data)}); }, @@ -50,4 +75,13 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { }); }, }); + + var _orderproto = models.Order.prototype; + models.Order = models.Order.extend({ + export_as_JSON: function() { + var vals = _orderproto.export_as_JSON.apply(this, arguments); + vals['transactions'] = this.transactions || {}; + return vals; + } + }) }); diff --git a/pos_payment_terminal/tests/__init__.py b/pos_payment_terminal/tests/__init__.py new file mode 100644 index 00000000..64ce8822 --- /dev/null +++ b/pos_payment_terminal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_transactions diff --git a/pos_payment_terminal/tests/test_transactions.py b/pos_payment_terminal/tests/test_transactions.py new file mode 100644 index 00000000..39597d12 --- /dev/null +++ b/pos_payment_terminal/tests/test_transactions.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2018-TODAY ACSONE SA/NV (). +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import odoo + + +class TestTransactions(odoo.tests.TransactionCase): + + def test_matching(self): + card_journal_id = self.env['account.journal'].create({ + 'name': 'Card Journal', + 'code': 'CARD', + 'type': 'bank', + 'payment_mode': 'card', + }).id + cash_journal_id = 0 + pos_order = { + 'statement_ids': [ + (0, 0, { + 'name': 'Payment1', + 'amount': 45.2, + 'journal_id': card_journal_id, + }), + (0, 0, { + 'name': 'Payment2', + 'amount': 10.5, + 'journal_id': card_journal_id, + }), + (0, 0, { + 'name': 'Payment3', + 'amount': 22.0, + 'journal_id': cash_journal_id, + }), + ], + 'transactions': [ + { + 'reference': 'ABCDE', + 'amount_cents': 1050, + }, + { + 'reference': 'XPTO', + 'amount_cents': 4520, + }, + ] + } + self.env['pos.order']._match_transactions_to_payments(pos_order) + self.assertEquals(pos_order['statement_ids'][0][2]['note'], 'XPTO') + self.assertEquals(pos_order['statement_ids'][1][2]['note'], 'ABCDE') From 9aee051b759b541fa1cff760dab4c3967fd5b926 Mon Sep 17 00:00:00 2001 From: andreparames Date: Wed, 14 Mar 2018 15:05:45 +0100 Subject: [PATCH 2/6] pos_payment_terminal: inform user of transaction status --- .../static/src/js/pos_payment_terminal.js | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/pos_payment_terminal/static/src/js/pos_payment_terminal.js b/pos_payment_terminal/static/src/js/pos_payment_terminal.js index adace081..ef30340f 100755 --- a/pos_payment_terminal/static/src/js/pos_payment_terminal.js +++ b/pos_payment_terminal/static/src/js/pos_payment_terminal.js @@ -24,23 +24,36 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { var self = this; self._super(parents, options); self.on('change:status', this, function(eh, status) { + if(!self.pos.chrome.screens) { + return; + } + var paymentwidget = self.pos.chrome.screens.payment; var drivers = status.newValue.drivers; var order = self.pos.get_order(); + var in_transaction = false; Object.keys(drivers).forEach(function(driver_name) { + if (drivers[driver_name].hasOwnProperty("in_transaction")) { + in_transaction = in_transaction || drivers[driver_name].in_transaction; + } + var transactions = drivers[driver_name].latest_transactions; if(!!transactions && transactions.hasOwnProperty(order.uid)) { order.transactions = transactions[order.uid]; + var order_total = Math.round(order.get_total_with_tax() * 100.0); var paid_total = order.transactions.map(function(t) { return t.amount_cents; }).reduce(function add(a, b) { return a + b; }, 0); + if(order_total === paid_total) { - self.pos.chrome.screens.payment.validate_order(); + paymentwidget.validate_order(); } } }); + order.in_transaction = in_transaction; + paymentwidget.order_changes(); }); }, payment_terminal_transaction_start: function(line_cid, currency_iso, currency_decimals){ @@ -66,22 +79,40 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { screens.PaymentScreenWidget.include({ render_paymentlines : function(){ - this._super.apply(this, arguments); + this._super.apply(this, arguments); var self = this; this.$('.paymentlines-container').unbind('click').on('click', '.payment-terminal-transaction-start', function(event){ // Why this "on" thing links severaltime the button to the action if I don't use "unlink" to reset the button links before ? //console.log(event.target); + self.pos.get_order().in_transaction = true; + self.order_changes(); self.pos.proxy.payment_terminal_transaction_start($(this).data('cid'), self.pos.currency.name, self.pos.currency.decimals); }); }, + order_changes: function(){ + this._super.apply(this, arguments); + var order = this.pos.get_order(); + if (!order) { + return; + } else if (order.in_transaction) { + self.$('.next').html(''); + } else { + self.$('.next').html('Validate '); + } + } }); var _orderproto = models.Order.prototype; models.Order = models.Order.extend({ + initialize: function(){ + _orderproto.initialize.apply(this, arguments); + this.in_transaction = false; + }, export_as_JSON: function() { var vals = _orderproto.export_as_JSON.apply(this, arguments); vals['transactions'] = this.transactions || {}; return vals; } - }) + }); + }); From a46caa710967d4708907cdba093e01f903e6cb29 Mon Sep 17 00:00:00 2001 From: andreparames Date: Thu, 3 May 2018 15:21:08 +0200 Subject: [PATCH 3/6] pos_payment_terminal: use currency precision --- pos_payment_terminal/models/pos_order.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pos_payment_terminal/models/pos_order.py b/pos_payment_terminal/models/pos_order.py index 723a992b..a0d0b66b 100644 --- a/pos_payment_terminal/models/pos_order.py +++ b/pos_payment_terminal/models/pos_order.py @@ -16,6 +16,9 @@ class PosOrder(models.Model): def _match_transactions_to_payments(self, pos_order): payments = pos_order['statement_ids'] transactions = pos_order['transactions'] + pos_session = self.env['pos.session'].browse( + pos_order['pos_session_id']) + currency_digits = pos_session.currency_id.decimal_places card_journals = self.env['account.journal'].search([ ('id', 'in', [p[2]['journal_id'] for p in payments]), ('payment_mode', '!=', False), @@ -27,7 +30,7 @@ class PosOrder(models.Model): if 'amount_cents' in obj: return obj['amount_cents'] else: - return int(round(obj['amount'] * 100)) + return int(round(obj['amount'] * pow(10, currency_digits))) try: for payment, transaction in match(card_payments, transactions, From bfc681fd961f34deb0b65e0d9682e22e1d138e38 Mon Sep 17 00:00:00 2001 From: andreparames Date: Thu, 3 May 2018 15:21:59 +0200 Subject: [PATCH 4/6] pos_payment_terminal: auto-validate orders with mixed payments --- .../static/src/js/pos_payment_terminal.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pos_payment_terminal/static/src/js/pos_payment_terminal.js b/pos_payment_terminal/static/src/js/pos_payment_terminal.js index ef30340f..00f84b01 100755 --- a/pos_payment_terminal/static/src/js/pos_payment_terminal.js +++ b/pos_payment_terminal/static/src/js/pos_payment_terminal.js @@ -38,16 +38,13 @@ odoo.define('pos_payment_terminal.pos_payment_terminal', function (require) { var transactions = drivers[driver_name].latest_transactions; if(!!transactions && transactions.hasOwnProperty(order.uid)) { + var previous_transactions = order.transactions; order.transactions = transactions[order.uid]; - - var order_total = Math.round(order.get_total_with_tax() * 100.0); - var paid_total = order.transactions.map(function(t) { - return t.amount_cents; - }).reduce(function add(a, b) { - return a + b; - }, 0); - - if(order_total === paid_total) { + var has_new_transactions = ( + !previous_transactions || + previous_transactions.length < order.transactions.length + ); + if(has_new_transactions && order.is_paid()) { paymentwidget.validate_order(); } } From aae93b0f1d56177eaa9ea0dd66676144438389f1 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Thu, 29 Aug 2019 14:47:02 +0200 Subject: [PATCH 5/6] [FIX] pos_payment_terminal: Fix tests --- pos_payment_terminal/tests/test_transactions.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pos_payment_terminal/tests/test_transactions.py b/pos_payment_terminal/tests/test_transactions.py index 39597d12..a2241149 100644 --- a/pos_payment_terminal/tests/test_transactions.py +++ b/pos_payment_terminal/tests/test_transactions.py @@ -2,10 +2,10 @@ # Copyright (C) 2018-TODAY ACSONE SA/NV (). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -import odoo +from odoo.addons.point_of_sale.tests.common import TestPointOfSaleCommon -class TestTransactions(odoo.tests.TransactionCase): +class TestTransactions(TestPointOfSaleCommon): def test_matching(self): card_journal_id = self.env['account.journal'].create({ @@ -16,6 +16,7 @@ class TestTransactions(odoo.tests.TransactionCase): }).id cash_journal_id = 0 pos_order = { + 'pos_session_id': self.pos_order_session0.id, 'statement_ids': [ (0, 0, { 'name': 'Payment1', From 0fa703d372ea00ebe5a50b4a45383b1ac330bd4f Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 29 Aug 2019 14:36:22 +0000 Subject: [PATCH 6/6] [UPD] Update pos_payment_terminal.pot --- pos_payment_terminal/i18n/pos_payment_terminal.pot | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pos_payment_terminal/i18n/pos_payment_terminal.pot b/pos_payment_terminal/i18n/pos_payment_terminal.pot index c5443309..3883db26 100644 --- a/pos_payment_terminal/i18n/pos_payment_terminal.pot +++ b/pos_payment_terminal/i18n/pos_payment_terminal.pot @@ -33,6 +33,11 @@ msgstr "" msgid "Payment Mode" msgstr "" +#. module: pos_payment_terminal +#: model:ir.model,name:pos_payment_terminal.model_pos_order +msgid "Point of Sale Orders" +msgstr "" + #. module: pos_payment_terminal #: model:ir.model.fields,help:pos_payment_terminal.field_account_journal_payment_mode msgid "Select the payment mode sent to the payment terminal"