From bdb30665fb84cf1e78d243b8b044ee2586cf819d Mon Sep 17 00:00:00 2001 From: andreparames Date: Fri, 9 Feb 2018 12:04:38 +0100 Subject: [PATCH] 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')