From fb625e6015c9f355616f5a7fa20083a97053672c Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Thu, 18 Jul 2019 16:16:03 +0200 Subject: [PATCH 1/8] Add account_payment_mode_auto_reconcile Override write instead of using onchange Add missing utf-8 comment Fix warning message Restrict to customer invoices Add readme and load tests Improvements after reviews Improve doc Unreconcile only automatically reconciled payments --- .../__init__.py | 1 + .../__manifest__.py | 23 +++ .../demo/account_payment_mode.xml | 6 + .../models/__init__.py | 3 + .../models/account_invoice.py | 145 +++++++++++++ .../models/account_partial_reconcile.py | 17 ++ .../models/account_payment_mode.py | 19 ++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 11 + .../tests/__init__.py | 1 + .../tests/test_partner_auto_reconcile.py | 194 ++++++++++++++++++ .../views/account_invoice.xml | 19 ++ .../views/account_payment_mode.xml | 16 ++ 13 files changed, 456 insertions(+) create mode 100644 account_payment_mode_auto_reconcile/__init__.py create mode 100644 account_payment_mode_auto_reconcile/__manifest__.py create mode 100644 account_payment_mode_auto_reconcile/demo/account_payment_mode.xml create mode 100644 account_payment_mode_auto_reconcile/models/__init__.py create mode 100644 account_payment_mode_auto_reconcile/models/account_invoice.py create mode 100644 account_payment_mode_auto_reconcile/models/account_partial_reconcile.py create mode 100644 account_payment_mode_auto_reconcile/models/account_payment_mode.py create mode 100644 account_payment_mode_auto_reconcile/readme/CONTRIBUTORS.rst create mode 100644 account_payment_mode_auto_reconcile/readme/DESCRIPTION.rst create mode 100644 account_payment_mode_auto_reconcile/tests/__init__.py create mode 100644 account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py create mode 100644 account_payment_mode_auto_reconcile/views/account_invoice.xml create mode 100644 account_payment_mode_auto_reconcile/views/account_payment_mode.xml diff --git a/account_payment_mode_auto_reconcile/__init__.py b/account_payment_mode_auto_reconcile/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/account_payment_mode_auto_reconcile/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_payment_mode_auto_reconcile/__manifest__.py b/account_payment_mode_auto_reconcile/__manifest__.py new file mode 100644 index 0000000000..f46019f57e --- /dev/null +++ b/account_payment_mode_auto_reconcile/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +{ + "name": "Account Payment Mode Auto Reconcile", + "summary": "Reconcile outstanding credits according to payment mode", + "version": "10.0.1.0.0", + "category": "Banking addons", + "website": "https://github.com/OCA/bank-payment", + "author": "Camptocamp, Odoo Community Association (OCA)", + "license": "AGPL-3", + "installable": True, + "depends": [ + "account_payment_partner", + ], + "data": [ + "views/account_invoice.xml", + "views/account_payment_mode.xml", + ], + "demo": [ + "demo/account_payment_mode.xml", + ], +} diff --git a/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml b/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml new file mode 100644 index 0000000000..1bea1d5c8b --- /dev/null +++ b/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/account_payment_mode_auto_reconcile/models/__init__.py b/account_payment_mode_auto_reconcile/models/__init__.py new file mode 100644 index 0000000000..bc3defd590 --- /dev/null +++ b/account_payment_mode_auto_reconcile/models/__init__.py @@ -0,0 +1,3 @@ +from . import account_invoice +from . import account_partial_reconcile +from . import account_payment_mode diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py new file mode 100644 index 0000000000..c315dcec79 --- /dev/null +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +import json + +from odoo import api, fields, models, _ + + +class AccountInvoice(models.Model): + + _inherit = "account.invoice" + + # Allow changing payment mode in open state + # TODO: Check if must be done in account_payment_partner instead + payment_mode_id = fields.Many2one( + states={'draft': [('readonly', False)], 'open': [('readonly', False)]} + ) + payment_mode_warning = fields.Char( + compute='_compute_payment_mode_warning', + ) + display_payment_mode_warning = fields.Boolean( + compute='_compute_payment_mode_warning', + ) + + @api.multi + def action_invoice_open(self): + res = super(AccountInvoice, self).action_invoice_open() + for invoice in self: + if invoice.type != 'out_invoice': + continue + if not invoice.payment_mode_id.auto_reconcile_outstanding_credits: + continue + partial_allowed = self.payment_mode_id.auto_reconcile_allow_partial + invoice.with_context( + _payment_mode_auto_reconcile=True + ).auto_reconcile_credits(partial_allowed=partial_allowed) + return res + + @api.multi + def write(self, vals): + res = super(AccountInvoice, self).write(vals) + if 'payment_mode_id' in vals: + for invoice in self: + # Do not auto reconcile anything else than open customer inv + if invoice.state != 'open' or invoice.type != 'out_invoice': + continue + payment_mode = invoice.payment_mode_id + # Auto reconcile if payment mode sets it + if ( + payment_mode + and payment_mode.auto_reconcile_outstanding_credits + ): + partial_allowed = payment_mode.auto_reconcile_allow_partial + invoice.with_context( + _payment_mode_auto_reconcile=True + ).auto_reconcile_credits( + partial_allowed=partial_allowed + ) + # If the payment mode is not using auto reconcile we remove + # the existing reconciliations + elif invoice.payment_move_line_ids: + invoice.auto_unreconcile_credits() + return res + + @api.multi + def auto_reconcile_credits(self, partial_allowed=True): + for invoice in self: + if not invoice.has_outstanding: + continue + credits_info = json.loads( + invoice.outstanding_credits_debits_widget + ) + # Get outstanding credits in chronological order + # (using reverse because aml is sorted by date desc as default) + credits_dict = credits_info.get('content') + credits_dict.reverse() + for credit in credits_dict: + if ( + not partial_allowed + and credit.get('amount') > invoice.residual + ): + continue + invoice.assign_outstanding_credit(credit.get('id')) + + @api.multi + def auto_unreconcile_credits(self): + for invoice in self: + payments_info = json.loads(invoice.payments_widget or '{}') + for payment in payments_info.get('content', []): + aml = self.env['account.move.line'].browse( + payment.get('payment_id') + ) + for apr in aml.matched_debit_ids: + if apr.amount != payment.get('amount'): + continue + if ( + apr.payment_mode_auto_reconcile + and apr.debit_move_id.invoice_id == invoice + ): + aml.with_context( + invoice_id=invoice.id + ).remove_move_reconcile() + + @api.depends( + 'type', 'payment_mode_id', 'payment_move_line_ids', 'state', + 'has_outstanding' + ) + def _compute_payment_mode_warning(self): + # TODO Improve me but watch out + for invoice in self: + if invoice.type != 'out_invoice' or invoice.state == 'paid': + invoice.payment_mode_warning = '' + invoice.display_payment_mode_warning = False + continue + invoice.display_payment_mode_warning = True + if ( + invoice.state != 'open' and invoice.payment_mode_id and + invoice.payment_mode_id.auto_reconcile_outstanding_credits + ): + invoice.payment_mode_warning = _( + 'Validating invoices with this payment mode will reconcile' + ' any outstanding credits.' + ) + elif ( + invoice.state == 'open' and invoice.payment_move_line_ids and ( + not invoice.payment_mode_id or not + invoice.payment_mode_id.auto_reconcile_outstanding_credits + ) + ): + invoice.payment_mode_warning = _( + 'Changing payment mode will unreconcile existing auto ' + 'reconciled payments.' + ) + elif ( + invoice.state == 'open' and not invoice.payment_move_line_ids + and invoice.payment_mode_id + and invoice.payment_mode_id.auto_reconcile_outstanding_credits + and invoice.has_outstanding + ): + invoice.payment_mode_warning = _( + 'Changing payment mode will reconcile outstanding credits.' + ) + else: + invoice.payment_mode_warning = '' + invoice.display_payment_mode_warning = False diff --git a/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py b/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py new file mode 100644 index 0000000000..e83cb86601 --- /dev/null +++ b/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import models, fields, api + + +class AccountPartialReconcile(models.Model): + + _inherit = 'account.partial.reconcile' + + payment_mode_auto_reconcile = fields.Boolean() + + @api.model + def create(self, vals): + if self.env.context.get('_payment_mode_auto_reconcile'): + vals['payment_mode_auto_reconcile'] = True + return super(AccountPartialReconcile, self).create(vals) diff --git a/account_payment_mode_auto_reconcile/models/account_payment_mode.py b/account_payment_mode_auto_reconcile/models/account_payment_mode.py new file mode 100644 index 0000000000..97f81e8a22 --- /dev/null +++ b/account_payment_mode_auto_reconcile/models/account_payment_mode.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import models, fields + + +class AccountPaymentMode(models.Model): + + _inherit = "account.payment.mode" + + auto_reconcile_outstanding_credits = fields.Boolean( + help="Reconcile automatically outstanding credits when an invoice " + "using this payment mode is validated, or when this payment mode " + "is defined on an open invoice." + ) + auto_reconcile_allow_partial = fields.Boolean( + default=True, + help="Allows automatic partial reconciliation of outstanding credits", + ) diff --git a/account_payment_mode_auto_reconcile/readme/CONTRIBUTORS.rst b/account_payment_mode_auto_reconcile/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..e31e2f0c4f --- /dev/null +++ b/account_payment_mode_auto_reconcile/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Akim Juillerat diff --git a/account_payment_mode_auto_reconcile/readme/DESCRIPTION.rst b/account_payment_mode_auto_reconcile/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..b692478934 --- /dev/null +++ b/account_payment_mode_auto_reconcile/readme/DESCRIPTION.rst @@ -0,0 +1,11 @@ +This module adds a checkbox `auto_reconcile_outstanding_credits` on account +payment modes to allow automatic reconciliation on account invoices if it is +checked. + +Automatic reconciliation of outstanding credits will only happen on customer +invoices at validation if the payment mode is set or when the payment mode is +changed on an open invoice. If a payment mode using auto-reconcile is removed +from an open invoice, the existing auto reconciled payments will be removed. + +Another option `auto_reconcile_allow_partial` on account payment mode defines +if outstanding credits can be partially used for the auto reconciliation. diff --git a/account_payment_mode_auto_reconcile/tests/__init__.py b/account_payment_mode_auto_reconcile/tests/__init__.py new file mode 100644 index 0000000000..729e1f4dae --- /dev/null +++ b/account_payment_mode_auto_reconcile/tests/__init__.py @@ -0,0 +1 @@ +from . import test_partner_auto_reconcile diff --git a/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py new file mode 100644 index 0000000000..05f00976ab --- /dev/null +++ b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from datetime import date, timedelta +import json + +from odoo.tests import SavepointCase +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT + + +class TestPartnerAutoReconcile(SavepointCase): + + @classmethod + def setUpClass(cls): + super(TestPartnerAutoReconcile, cls).setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.acc_rec = cls.env['account.account'].search( + [('user_type_id', '=', cls.env.ref( + 'account.data_account_type_receivable').id + )], limit=1 + ) + cls.acc_pay = cls.env['account.account'].search( + [('user_type_id', '=', cls.env.ref( + 'account.data_account_type_payable').id + )], limit=1 + ) + cls.acc_rev = cls.env['account.account'].search( + [('user_type_id', '=', cls.env.ref( + 'account.data_account_type_revenue').id + )], limit=1 + ) + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test partner', + 'customer': True, + 'property_account_receivable_id': cls.acc_rec.id, + 'property_account_payable_id': cls.acc_pay.id, + }) + cls.payment_mode = cls.env.ref( + 'account_payment_mode.payment_mode_inbound_dd1' + ) + # TODO check why it's not set from demo data + cls.payment_mode.auto_reconcile_outstanding_credits = True + cls.product = cls.env.ref('product.consu_delivery_02') + cls.invoice = cls.env['account.invoice'].create({ + 'partner_id': cls.partner.id, + 'type': 'out_invoice', + 'payment_term_id': cls.env.ref('account.account_payment_term').id, + 'account_id': cls.acc_rec.id, + 'invoice_line_ids': [(0, 0, { + 'product_id': cls.product.id, + 'name': cls.product.name, + 'price_unit': 1000.0, + 'quantity': 1, + 'account_id': cls.acc_rev.id, + })], + }) + cls.invoice.action_invoice_open() + bank_journal = cls.env['account.journal'].search( + [('type', '=', 'bank')], limit=1 + ) + cls.invoice.pay_and_reconcile(bank_journal) + cls.refund_wiz = cls.env['account.invoice.refund'].with_context( + active_ids=cls.invoice.ids).create({ + 'filter_refund': 'refund', + 'description': 'test' + }) + refund_id = cls.refund_wiz.invoice_refund().get('domain')[1][2] + cls.refund = cls.env['account.invoice'].browse(refund_id) + cls.refund.action_invoice_open() + cls.invoice_copy = cls.invoice.copy() + cls.invoice_copy.write({ + 'invoice_line_ids': [(0, 0, { + 'product_id': cls.product.id, + 'name': cls.product.name, + 'price_unit': 500.0, + 'quantity': 1, + 'account_id': cls.acc_rev.id, + })] + }) + cls.invoice_copy.action_invoice_open() + + def test_invoice_validate_auto_reconcile(self): + auto_rec_invoice = self.invoice.copy({ + 'payment_mode_id': self.payment_mode.id, + }) + auto_rec_invoice.write({ + 'invoice_line_ids': [(0, 0, { + 'product_id': self.product.id, + 'name': self.product.name, + 'price_unit': 500.0, + 'quantity': 1, + 'account_id': self.acc_rev.id, + })] + }) + self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) + self.assertEqual(self.invoice_copy.residual, 1500) + auto_rec_invoice.action_invoice_open() + self.assertEqual(auto_rec_invoice.residual, 500) + + def test_invoice_change_auto_reconcile(self): + self.assertEqual(self.invoice_copy.residual, 1500) + self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.assertEqual(self.invoice_copy.residual, 500) + self.invoice_copy.write({'payment_mode_id': False}) + self.assertEqual(self.invoice_copy.residual, 1500) + # Copy the refund so there's more outstanding credit than invoice total + new_refund = self.refund.copy() + new_refund.date = (date.today() + timedelta(days=1)).strftime( + DATE_FORMAT + ) + new_refund.invoice_line_ids.write({'price_unit': 1200}) + new_refund.action_invoice_open() + # Set reconcile partial to False + self.payment_mode.auto_reconcile_allow_partial = False + self.assertFalse(self.payment_mode.auto_reconcile_allow_partial) + self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + # Only the older move is used as payment + self.assertEqual(self.invoice_copy.residual, 500) + self.invoice_copy.write({'payment_mode_id': False}) + self.assertEqual(self.invoice_copy.residual, 1500) + # Set allow partial will reconcile both moves + self.payment_mode.auto_reconcile_allow_partial = True + self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.assertEqual(self.invoice_copy.state, 'paid') + self.assertEqual(self.invoice_copy.residual, 0) + + def test_invoice_auto_unreconcile(self): + # Copy the refund so there's more outstanding credit than invoice total + new_refund = self.refund.copy() + new_refund.date = (date.today() + timedelta(days=1)).strftime( + DATE_FORMAT + ) + new_refund.invoice_line_ids.write({'price_unit': 1200}) + new_refund.action_invoice_open() + + auto_rec_invoice = self.invoice.copy({ + 'payment_mode_id': self.payment_mode.id, + }) + auto_rec_invoice.invoice_line_ids.write({'price_unit': 800}) + auto_rec_invoice.action_invoice_open() + self.assertEqual(auto_rec_invoice.state, 'paid') + self.assertEqual(auto_rec_invoice.residual, 0) + # As we had 2200 of outstanding credits and 800 was assigned, there's + # 1400 left + self.assertTrue(self.payment_mode.auto_reconcile_allow_partial) + self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.assertEqual(self.invoice_copy.residual, 100) + # Unreconcile of an invoice doesn't change the reconciliation of the + # other invoice + self.invoice_copy.write({'payment_mode_id': False}) + self.assertEqual(self.invoice_copy.residual, 1500) + self.assertEqual(auto_rec_invoice.state, 'paid') + self.assertEqual(auto_rec_invoice.residual, 0) + + def test_invoice_auto_unreconcile_only_auto_reconcile(self): + refund = self.refund.copy() + refund.invoice_line_ids.write({'price_unit': 100}) + refund.action_invoice_open() + new_invoice = self.invoice_copy.copy() + new_invoice.action_invoice_open() + # Only reconcile 1000 refund manually + new_invoice_credits = json.loads( + new_invoice.outstanding_credits_debits_widget + ).get('content') + for cred in new_invoice_credits: + if cred.get('amount') == 100.0: + new_invoice.assign_outstanding_credit(cred.get('id')) + self.assertEqual(new_invoice.residual, 1400.0) + # Assign payment mode adds the outstanding credit of 1000.0 + new_invoice.write({'payment_mode_id': self.payment_mode.id}) + self.assertEqual(new_invoice.residual, 400.0) + # Remove payment mode only removes automatically added credit + new_invoice.write({'payment_mode_id': False}) + self.assertEqual(new_invoice.residual, 1400.0) + + # use the same payment partially on different invoices. + other_invoice = self.invoice.copy() + other_invoice.invoice_line_ids.write({ + 'price_unit': 200, + }) + other_invoice.write({ + 'payment_mode_id': self.payment_mode.id, + }) + other_invoice.action_invoice_open() + self.assertEqual(other_invoice.state, 'paid') + # since 200 were assigned on other invoice adding auto-rec payment mode + # on new_invoice will reconcile 800 and residual will be 600 + new_invoice.write({'payment_mode_id': self.payment_mode.id}) + self.assertEqual(new_invoice.residual, 600.0) + # Removing the payment mode should not remove the partial payment on + # the other invoice + new_invoice.write({'payment_mode_id': False}) + self.assertEqual(new_invoice.residual, 1400.0) + self.assertEqual(other_invoice.state, 'paid') diff --git a/account_payment_mode_auto_reconcile/views/account_invoice.xml b/account_payment_mode_auto_reconcile/views/account_invoice.xml new file mode 100644 index 0000000000..1cf1c02d74 --- /dev/null +++ b/account_payment_mode_auto_reconcile/views/account_invoice.xml @@ -0,0 +1,19 @@ + + + + account_payment_partner.invoice_form.inherit + account.invoice + + + + " + + + + + + diff --git a/account_payment_mode_auto_reconcile/views/account_payment_mode.xml b/account_payment_mode_auto_reconcile/views/account_payment_mode.xml new file mode 100644 index 0000000000..2e8b245ffb --- /dev/null +++ b/account_payment_mode_auto_reconcile/views/account_payment_mode.xml @@ -0,0 +1,16 @@ + + + + account.payment.mode.form.inherit + account.payment.mode + + + + + + + + + + + From e8181ee32281618cf7e78ed197a4ae52f3916bcb Mon Sep 17 00:00:00 2001 From: vrenaville Date: Wed, 21 Aug 2019 13:29:28 +0200 Subject: [PATCH 2/8] [FIX] typo in variable name --- account_payment_mode_auto_reconcile/models/account_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py index c315dcec79..8dfc75b51e 100644 --- a/account_payment_mode_auto_reconcile/models/account_invoice.py +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -30,7 +30,7 @@ def action_invoice_open(self): continue if not invoice.payment_mode_id.auto_reconcile_outstanding_credits: continue - partial_allowed = self.payment_mode_id.auto_reconcile_allow_partial + partial_allowed = invoice.payment_mode_id.auto_reconcile_allow_partial invoice.with_context( _payment_mode_auto_reconcile=True ).auto_reconcile_credits(partial_allowed=partial_allowed) From 4e8a2b06a4571e4602ff3179f09ba743e73ce6b8 Mon Sep 17 00:00:00 2001 From: Thierry Ducrest Date: Tue, 24 Sep 2019 13:16:42 +0200 Subject: [PATCH 3/8] Add auto reconcile only same journal (#2) * Add auto reconcile only same journal Add an option to auto reconcile only credits that are on the same journal than the invoice. This make sure that unrelated payment will not be used automatically. * Update account_payment_mode_auto_reconcile/models/account_invoice.py Co-Authored-By: Akim Juillerat * Update account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py Co-Authored-By: Akim Juillerat --- .../models/account_invoice.py | 22 +++++++-- .../models/account_payment_mode.py | 7 +++ .../tests/test_partner_auto_reconcile.py | 45 ++++++++++++++++++- .../views/account_payment_mode.xml | 1 + 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py index 8dfc75b51e..78509fbe59 100644 --- a/account_payment_mode_auto_reconcile/models/account_invoice.py +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -30,10 +30,10 @@ def action_invoice_open(self): continue if not invoice.payment_mode_id.auto_reconcile_outstanding_credits: continue - partial_allowed = invoice.payment_mode_id.auto_reconcile_allow_partial + partial = invoice.payment_mode_id.auto_reconcile_allow_partial invoice.with_context( _payment_mode_auto_reconcile=True - ).auto_reconcile_credits(partial_allowed=partial_allowed) + ).auto_reconcile_credits(partial_allowed=partial) return res @api.multi @@ -50,11 +50,11 @@ def write(self, vals): payment_mode and payment_mode.auto_reconcile_outstanding_credits ): - partial_allowed = payment_mode.auto_reconcile_allow_partial + partial = payment_mode.auto_reconcile_allow_partial invoice.with_context( _payment_mode_auto_reconcile=True ).auto_reconcile_credits( - partial_allowed=partial_allowed + partial_allowed=partial ) # If the payment mode is not using auto reconcile we remove # the existing reconciliations @@ -73,6 +73,10 @@ def auto_reconcile_credits(self, partial_allowed=True): # Get outstanding credits in chronological order # (using reverse because aml is sorted by date desc as default) credits_dict = credits_info.get('content') + if invoice.payment_mode_id.auto_reconcile_same_journal: + credits_dict = invoice._filter_payment_same_journal( + credits_dict + ) credits_dict.reverse() for credit in credits_dict: if ( @@ -82,6 +86,16 @@ def auto_reconcile_credits(self, partial_allowed=True): continue invoice.assign_outstanding_credit(credit.get('id')) + @api.multi + def _filter_payment_same_journal(self, credits_dict): + """Keep only credits on the same journal than the invoice.""" + self.ensure_one() + line_ids = [credit['id'] for credit in credits_dict] + lines = self.env['account.move.line'].search([ + ('id', 'in', line_ids), ('journal_id', '=', self.journal_id.id) + ]) + return [credit for credit in credits_dict if credit['id'] in lines.ids] + @api.multi def auto_unreconcile_credits(self): for invoice in self: diff --git a/account_payment_mode_auto_reconcile/models/account_payment_mode.py b/account_payment_mode_auto_reconcile/models/account_payment_mode.py index 97f81e8a22..6d9c6357e7 100644 --- a/account_payment_mode_auto_reconcile/models/account_payment_mode.py +++ b/account_payment_mode_auto_reconcile/models/account_payment_mode.py @@ -9,11 +9,18 @@ class AccountPaymentMode(models.Model): _inherit = "account.payment.mode" auto_reconcile_outstanding_credits = fields.Boolean( + string="Auto reconcile", help="Reconcile automatically outstanding credits when an invoice " "using this payment mode is validated, or when this payment mode " "is defined on an open invoice." ) auto_reconcile_allow_partial = fields.Boolean( default=True, + string="Allow partial", help="Allows automatic partial reconciliation of outstanding credits", ) + auto_reconcile_same_journal = fields.Boolean( + default=False, + string="Only same journal", + help="Only reconcile payment in the same journal than the invoice", + ) diff --git a/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py index 05f00976ab..4c47aa111f 100644 --- a/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py +++ b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py @@ -55,10 +55,10 @@ def setUpClass(cls): })], }) cls.invoice.action_invoice_open() - bank_journal = cls.env['account.journal'].search( + cls.bank_journal = cls.env['account.journal'].search( [('type', '=', 'bank')], limit=1 ) - cls.invoice.pay_and_reconcile(bank_journal) + cls.invoice.pay_and_reconcile(cls.bank_journal) cls.refund_wiz = cls.env['account.invoice.refund'].with_context( active_ids=cls.invoice.ids).create({ 'filter_refund': 'refund', @@ -192,3 +192,44 @@ def test_invoice_auto_unreconcile_only_auto_reconcile(self): new_invoice.write({'payment_mode_id': False}) self.assertEqual(new_invoice.residual, 1400.0) self.assertEqual(other_invoice.state, 'paid') + + def test_invoice_auto_reconcile_same_journal(self): + """Check reconciling credits on same journal.""" + self.payment_mode.auto_reconcile_same_journal = True + auto_rec_invoice = self.invoice.copy({ + 'payment_mode_id': self.payment_mode.id, + }) + auto_rec_invoice.write({ + 'invoice_line_ids': [(0, 0, { + 'product_id': self.product.id, + 'name': self.product.name, + 'price_unit': 500.0, + 'quantity': 1, + 'account_id': self.acc_rev.id, + })] + }) + self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) + self.assertEqual(self.invoice_copy.residual, 1500) + auto_rec_invoice.action_invoice_open() + self.assertEqual(auto_rec_invoice.residual, 500) + + def test_invoice_auto_reconcile_different_journal(self): + """Check not reconciling credits on different journal.""" + self.payment_mode.auto_reconcile_same_journal = True + auto_rec_invoice = self.invoice.copy({ + 'payment_mode_id': self.payment_mode.id, + 'journal_id': self.bank_journal.id, + }) + auto_rec_invoice.write({ + 'invoice_line_ids': [(0, 0, { + 'product_id': self.product.id, + 'name': self.product.name, + 'price_unit': 500.0, + 'quantity': 1, + 'account_id': self.acc_rev.id, + })] + }) + self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) + self.assertEqual(self.invoice_copy.residual, 1500) + auto_rec_invoice.action_invoice_open() + self.assertEqual(auto_rec_invoice.residual, 1500) diff --git a/account_payment_mode_auto_reconcile/views/account_payment_mode.xml b/account_payment_mode_auto_reconcile/views/account_payment_mode.xml index 2e8b245ffb..519c62ecb5 100644 --- a/account_payment_mode_auto_reconcile/views/account_payment_mode.xml +++ b/account_payment_mode_auto_reconcile/views/account_payment_mode.xml @@ -9,6 +9,7 @@ + From 942cf3efc15dcb718574c08794bf5b11df6e442e Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Tue, 15 Oct 2019 16:01:09 +0200 Subject: [PATCH 4/8] FIX: Override invoice_validate instead of action_invoice_open This small change is needed since action_invoice_open filters invoices that can be open from other invoices that cannot. Therefore using invoice_validate here allows for better customizations by ensuring that the invoices to be handled are really open (i.e. have a move generated). --- account_payment_mode_auto_reconcile/models/account_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py index 78509fbe59..762dcf6d41 100644 --- a/account_payment_mode_auto_reconcile/models/account_invoice.py +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -23,8 +23,8 @@ class AccountInvoice(models.Model): ) @api.multi - def action_invoice_open(self): - res = super(AccountInvoice, self).action_invoice_open() + def invoice_validate(self): + res = super(AccountInvoice, self).invoice_validate() for invoice in self: if invoice.type != 'out_invoice': continue From c21d6d7e2f1342f865de183cf286ab27d909fc41 Mon Sep 17 00:00:00 2001 From: Akim Juillerat Date: Wed, 16 Oct 2019 09:27:35 +0200 Subject: [PATCH 5/8] FIX: Use function to sort credits As using reverse is not reliable to ensure oldest credits will be reconciled first, use a function to sort the dicts according to their id and allows inheritance. --- .../README.rst | 83 ++++ .../account_payment_mode_auto_reconcile.pot | 98 ++++ .../models/account_invoice.py | 10 +- .../static/description/index.html | 427 ++++++++++++++++++ 4 files changed, 616 insertions(+), 2 deletions(-) create mode 100644 account_payment_mode_auto_reconcile/README.rst create mode 100644 account_payment_mode_auto_reconcile/i18n/account_payment_mode_auto_reconcile.pot create mode 100644 account_payment_mode_auto_reconcile/static/description/index.html diff --git a/account_payment_mode_auto_reconcile/README.rst b/account_payment_mode_auto_reconcile/README.rst new file mode 100644 index 0000000000..2722274464 --- /dev/null +++ b/account_payment_mode_auto_reconcile/README.rst @@ -0,0 +1,83 @@ +=================================== +Account Payment Mode Auto Reconcile +=================================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github + :target: https://github.com/OCA/account-reconcile/tree/10.0/account_payment_mode_auto_reconcile + :alt: OCA/account-reconcile +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-reconcile-10-0/account-reconcile-10-0-account_payment_mode_auto_reconcile + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/98/10.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module adds a checkbox `auto_reconcile_outstanding_credits` on account +payment modes to allow automatic reconciliation on account invoices if it is +checked. + +Automatic reconciliation of outstanding credits will only happen on customer +invoices at validation if the payment mode is set or when the payment mode is +changed on an open invoice. If a payment mode using auto-reconcile is removed +from an open invoice, the existing auto reconciled payments will be removed. + +Another option `auto_reconcile_allow_partial` on account payment mode defines +if outstanding credits can be partially used for the auto reconciliation. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Akim Juillerat + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/account-reconcile `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_payment_mode_auto_reconcile/i18n/account_payment_mode_auto_reconcile.pot b/account_payment_mode_auto_reconcile/i18n/account_payment_mode_auto_reconcile.pot new file mode 100644 index 0000000000..090610b9e7 --- /dev/null +++ b/account_payment_mode_auto_reconcile/i18n/account_payment_mode_auto_reconcile.pot @@ -0,0 +1,98 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_payment_mode_auto_reconcile +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_allow_partial +msgid "Allow partial" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_allow_partial +msgid "Allows automatic partial reconciliation of outstanding credits" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_outstanding_credits +msgid "Auto reconcile" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.ui.view,arch_db:account_payment_mode_auto_reconcile.account_payment_mode_form_inherit +msgid "Auto reconcile outstanding credits" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:160 +#, python-format +msgid "Changing payment mode will reconcile outstanding credits." +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:150 +#, python-format +msgid "Changing payment mode will unreconcile existing auto reconciled payments." +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_invoice_display_payment_mode_warning +msgid "Display payment mode warning" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_same_journal +msgid "Only reconcile payment in the same journal than the invoice" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_same_journal +msgid "Only same journal" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_partial_reconcile +msgid "Partial Reconcile" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_payment_mode +msgid "Payment Modes" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_partial_reconcile_payment_mode_auto_reconcile +msgid "Payment mode auto reconcile" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_invoice_payment_mode_warning +msgid "Payment mode warning" +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_outstanding_credits +msgid "Reconcile automatically outstanding credits when an invoice using this payment mode is validated, or when this payment mode is defined on an open invoice." +msgstr "" + +#. module: account_payment_mode_auto_reconcile +#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:140 +#, python-format +msgid "Validating invoices with this payment mode will reconcile any outstanding credits." +msgstr "" + diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py index 762dcf6d41..dbbb8c142f 100644 --- a/account_payment_mode_auto_reconcile/models/account_invoice.py +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -2,6 +2,7 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import json +from operator import itemgetter from odoo import api, fields, models, _ @@ -77,8 +78,8 @@ def auto_reconcile_credits(self, partial_allowed=True): credits_dict = invoice._filter_payment_same_journal( credits_dict ) - credits_dict.reverse() - for credit in credits_dict: + sorted_credits = self._sort_credits_dict(credits_dict) + for credit in sorted_credits: if ( not partial_allowed and credit.get('amount') > invoice.residual @@ -86,6 +87,11 @@ def auto_reconcile_credits(self, partial_allowed=True): continue invoice.assign_outstanding_credit(credit.get('id')) + @api.model + def _sort_credits_dict(self, credits_dict): + """Sort credits dict according to their id (oldest recs first)""" + return sorted(credits_dict, key=itemgetter('id')) + @api.multi def _filter_payment_same_journal(self, credits_dict): """Keep only credits on the same journal than the invoice.""" diff --git a/account_payment_mode_auto_reconcile/static/description/index.html b/account_payment_mode_auto_reconcile/static/description/index.html new file mode 100644 index 0000000000..ab021c9d4d --- /dev/null +++ b/account_payment_mode_auto_reconcile/static/description/index.html @@ -0,0 +1,427 @@ + + + + + + +Account Payment Mode Auto Reconcile + + + +
+

Account Payment Mode Auto Reconcile

+ + +

Beta License: AGPL-3 OCA/account-reconcile Translate me on Weblate Try me on Runbot

+

This module adds a checkbox auto_reconcile_outstanding_credits on account +payment modes to allow automatic reconciliation on account invoices if it is +checked.

+

Automatic reconciliation of outstanding credits will only happen on customer +invoices at validation if the payment mode is set or when the payment mode is +changed on an open invoice. If a payment mode using auto-reconcile is removed +from an open invoice, the existing auto reconciled payments will be removed.

+

Another option auto_reconcile_allow_partial on account payment mode defines +if outstanding credits can be partially used for the auto reconciliation.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+ +
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/account-reconcile project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From a9a1538683903dcbe2d4dd05f7b3650f035bda2c Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 10 Oct 2022 15:01:27 +0000 Subject: [PATCH 6/8] [ADD] icon.png --- .../static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 account_payment_mode_auto_reconcile/static/description/icon.png diff --git a/account_payment_mode_auto_reconcile/static/description/icon.png b/account_payment_mode_auto_reconcile/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z 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 Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 6394decc3ad3001c7576183f9ecd97a2b8a4b51c Mon Sep 17 00:00:00 2001 From: sonhd91 Date: Mon, 20 Feb 2023 17:59:01 +0700 Subject: [PATCH 7/8] [IMP] account_payment_mode_auto_reconcile: black, isort, prettier --- .../__manifest__.py | 3 +- .../demo/account_payment_mode.xml | 9 +- .../models/account_invoice.py | 102 +++--- .../models/account_partial_reconcile.py | 9 +- .../models/account_payment_mode.py | 7 +- .../tests/test_partner_auto_reconcile.py | 327 +++++++++++------- .../views/account_invoice.xml | 9 +- .../views/account_payment_mode.xml | 19 +- .../odoo/__init__.py | 1 + .../odoo/addons/__init__.py | 1 + .../account_payment_mode_auto_reconcile | 1 + .../setup.py | 6 + 12 files changed, 281 insertions(+), 213 deletions(-) create mode 100644 setup/account_payment_mode_auto_reconcile/odoo/__init__.py create mode 100644 setup/account_payment_mode_auto_reconcile/odoo/addons/__init__.py create mode 120000 setup/account_payment_mode_auto_reconcile/odoo/addons/account_payment_mode_auto_reconcile create mode 100644 setup/account_payment_mode_auto_reconcile/setup.py diff --git a/account_payment_mode_auto_reconcile/__manifest__.py b/account_payment_mode_auto_reconcile/__manifest__.py index f46019f57e..8df1becf84 100644 --- a/account_payment_mode_auto_reconcile/__manifest__.py +++ b/account_payment_mode_auto_reconcile/__manifest__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) { @@ -6,7 +5,7 @@ "summary": "Reconcile outstanding credits according to payment mode", "version": "10.0.1.0.0", "category": "Banking addons", - "website": "https://github.com/OCA/bank-payment", + "website": "https://github.com/OCA/account-reconcile", "author": "Camptocamp, Odoo Community Association (OCA)", "license": "AGPL-3", "installable": True, diff --git a/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml b/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml index 1bea1d5c8b..0f04218a32 100644 --- a/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml +++ b/account_payment_mode_auto_reconcile/demo/account_payment_mode.xml @@ -1,6 +1,9 @@ - + - - + + diff --git a/account_payment_mode_auto_reconcile/models/account_invoice.py b/account_payment_mode_auto_reconcile/models/account_invoice.py index dbbb8c142f..f66efcd4c8 100644 --- a/account_payment_mode_auto_reconcile/models/account_invoice.py +++ b/account_payment_mode_auto_reconcile/models/account_invoice.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) import json from operator import itemgetter -from odoo import api, fields, models, _ +from odoo import _, api, fields, models class AccountInvoice(models.Model): @@ -14,20 +13,20 @@ class AccountInvoice(models.Model): # Allow changing payment mode in open state # TODO: Check if must be done in account_payment_partner instead payment_mode_id = fields.Many2one( - states={'draft': [('readonly', False)], 'open': [('readonly', False)]} + states={"draft": [("readonly", False)], "open": [("readonly", False)]} ) payment_mode_warning = fields.Char( - compute='_compute_payment_mode_warning', + compute="_compute_payment_mode_warning", ) display_payment_mode_warning = fields.Boolean( - compute='_compute_payment_mode_warning', + compute="_compute_payment_mode_warning", ) @api.multi def invoice_validate(self): res = super(AccountInvoice, self).invoice_validate() for invoice in self: - if invoice.type != 'out_invoice': + if invoice.type != "out_invoice": continue if not invoice.payment_mode_id.auto_reconcile_outstanding_credits: continue @@ -40,23 +39,18 @@ def invoice_validate(self): @api.multi def write(self, vals): res = super(AccountInvoice, self).write(vals) - if 'payment_mode_id' in vals: + if "payment_mode_id" in vals: for invoice in self: # Do not auto reconcile anything else than open customer inv - if invoice.state != 'open' or invoice.type != 'out_invoice': + if invoice.state != "open" or invoice.type != "out_invoice": continue payment_mode = invoice.payment_mode_id # Auto reconcile if payment mode sets it - if ( - payment_mode - and payment_mode.auto_reconcile_outstanding_credits - ): + if payment_mode and payment_mode.auto_reconcile_outstanding_credits: partial = payment_mode.auto_reconcile_allow_partial invoice.with_context( _payment_mode_auto_reconcile=True - ).auto_reconcile_credits( - partial_allowed=partial - ) + ).auto_reconcile_credits(partial_allowed=partial) # If the payment mode is not using auto reconcile we remove # the existing reconciliations elif invoice.payment_move_line_ids: @@ -68,98 +62,90 @@ def auto_reconcile_credits(self, partial_allowed=True): for invoice in self: if not invoice.has_outstanding: continue - credits_info = json.loads( - invoice.outstanding_credits_debits_widget - ) + credits_info = json.loads(invoice.outstanding_credits_debits_widget) # Get outstanding credits in chronological order # (using reverse because aml is sorted by date desc as default) - credits_dict = credits_info.get('content') + credits_dict = credits_info.get("content") if invoice.payment_mode_id.auto_reconcile_same_journal: - credits_dict = invoice._filter_payment_same_journal( - credits_dict - ) + credits_dict = invoice._filter_payment_same_journal(credits_dict) sorted_credits = self._sort_credits_dict(credits_dict) for credit in sorted_credits: - if ( - not partial_allowed - and credit.get('amount') > invoice.residual - ): + if not partial_allowed and credit.get("amount") > invoice.residual: continue - invoice.assign_outstanding_credit(credit.get('id')) + invoice.assign_outstanding_credit(credit.get("id")) @api.model def _sort_credits_dict(self, credits_dict): """Sort credits dict according to their id (oldest recs first)""" - return sorted(credits_dict, key=itemgetter('id')) + return sorted(credits_dict, key=itemgetter("id")) @api.multi def _filter_payment_same_journal(self, credits_dict): """Keep only credits on the same journal than the invoice.""" self.ensure_one() - line_ids = [credit['id'] for credit in credits_dict] - lines = self.env['account.move.line'].search([ - ('id', 'in', line_ids), ('journal_id', '=', self.journal_id.id) - ]) - return [credit for credit in credits_dict if credit['id'] in lines.ids] + line_ids = [credit["id"] for credit in credits_dict] + lines = self.env["account.move.line"].search( + [("id", "in", line_ids), ("journal_id", "=", self.journal_id.id)] + ) + return [credit for credit in credits_dict if credit["id"] in lines.ids] @api.multi def auto_unreconcile_credits(self): for invoice in self: - payments_info = json.loads(invoice.payments_widget or '{}') - for payment in payments_info.get('content', []): - aml = self.env['account.move.line'].browse( - payment.get('payment_id') - ) + payments_info = json.loads(invoice.payments_widget or "{}") + for payment in payments_info.get("content", []): + aml = self.env["account.move.line"].browse(payment.get("payment_id")) for apr in aml.matched_debit_ids: - if apr.amount != payment.get('amount'): + if apr.amount != payment.get("amount"): continue if ( apr.payment_mode_auto_reconcile and apr.debit_move_id.invoice_id == invoice ): - aml.with_context( - invoice_id=invoice.id - ).remove_move_reconcile() + aml.with_context(invoice_id=invoice.id).remove_move_reconcile() @api.depends( - 'type', 'payment_mode_id', 'payment_move_line_ids', 'state', - 'has_outstanding' + "type", "payment_mode_id", "payment_move_line_ids", "state", "has_outstanding" ) def _compute_payment_mode_warning(self): # TODO Improve me but watch out for invoice in self: - if invoice.type != 'out_invoice' or invoice.state == 'paid': - invoice.payment_mode_warning = '' + if invoice.type != "out_invoice" or invoice.state == "paid": + invoice.payment_mode_warning = "" invoice.display_payment_mode_warning = False continue invoice.display_payment_mode_warning = True if ( - invoice.state != 'open' and invoice.payment_mode_id and - invoice.payment_mode_id.auto_reconcile_outstanding_credits + invoice.state != "open" + and invoice.payment_mode_id + and invoice.payment_mode_id.auto_reconcile_outstanding_credits ): invoice.payment_mode_warning = _( - 'Validating invoices with this payment mode will reconcile' - ' any outstanding credits.' + "Validating invoices with this payment mode will reconcile" + " any outstanding credits." ) elif ( - invoice.state == 'open' and invoice.payment_move_line_ids and ( - not invoice.payment_mode_id or not - invoice.payment_mode_id.auto_reconcile_outstanding_credits + invoice.state == "open" + and invoice.payment_move_line_ids + and ( + not invoice.payment_mode_id + or not invoice.payment_mode_id.auto_reconcile_outstanding_credits ) ): invoice.payment_mode_warning = _( - 'Changing payment mode will unreconcile existing auto ' - 'reconciled payments.' + "Changing payment mode will unreconcile existing auto " + "reconciled payments." ) elif ( - invoice.state == 'open' and not invoice.payment_move_line_ids + invoice.state == "open" + and not invoice.payment_move_line_ids and invoice.payment_mode_id and invoice.payment_mode_id.auto_reconcile_outstanding_credits and invoice.has_outstanding ): invoice.payment_mode_warning = _( - 'Changing payment mode will reconcile outstanding credits.' + "Changing payment mode will reconcile outstanding credits." ) else: - invoice.payment_mode_warning = '' + invoice.payment_mode_warning = "" invoice.display_payment_mode_warning = False diff --git a/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py b/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py index e83cb86601..13f4d08fff 100644 --- a/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py +++ b/account_payment_mode_auto_reconcile/models/account_partial_reconcile.py @@ -1,17 +1,16 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models, fields, api +from odoo import api, fields, models class AccountPartialReconcile(models.Model): - _inherit = 'account.partial.reconcile' + _inherit = "account.partial.reconcile" payment_mode_auto_reconcile = fields.Boolean() @api.model def create(self, vals): - if self.env.context.get('_payment_mode_auto_reconcile'): - vals['payment_mode_auto_reconcile'] = True + if self.env.context.get("_payment_mode_auto_reconcile"): + vals["payment_mode_auto_reconcile"] = True return super(AccountPartialReconcile, self).create(vals) diff --git a/account_payment_mode_auto_reconcile/models/account_payment_mode.py b/account_payment_mode_auto_reconcile/models/account_payment_mode.py index 6d9c6357e7..d4807c4755 100644 --- a/account_payment_mode_auto_reconcile/models/account_payment_mode.py +++ b/account_payment_mode_auto_reconcile/models/account_payment_mode.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from odoo import models, fields +from odoo import fields, models class AccountPaymentMode(models.Model): @@ -11,8 +10,8 @@ class AccountPaymentMode(models.Model): auto_reconcile_outstanding_credits = fields.Boolean( string="Auto reconcile", help="Reconcile automatically outstanding credits when an invoice " - "using this payment mode is validated, or when this payment mode " - "is defined on an open invoice." + "using this payment mode is validated, or when this payment mode " + "is defined on an open invoice.", ) auto_reconcile_allow_partial = fields.Boolean( default=True, diff --git a/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py index 4c47aa111f..5e592a4a1a 100644 --- a/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py +++ b/account_payment_mode_auto_reconcile/tests/test_partner_auto_reconcile.py @@ -1,97 +1,136 @@ -# -*- coding: utf-8 -*- # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) -from datetime import date, timedelta import json +from datetime import date, timedelta from odoo.tests import SavepointCase from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT class TestPartnerAutoReconcile(SavepointCase): - @classmethod def setUpClass(cls): super(TestPartnerAutoReconcile, cls).setUpClass() cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) - cls.acc_rec = cls.env['account.account'].search( - [('user_type_id', '=', cls.env.ref( - 'account.data_account_type_receivable').id - )], limit=1 - ) - cls.acc_pay = cls.env['account.account'].search( - [('user_type_id', '=', cls.env.ref( - 'account.data_account_type_payable').id - )], limit=1 - ) - cls.acc_rev = cls.env['account.account'].search( - [('user_type_id', '=', cls.env.ref( - 'account.data_account_type_revenue').id - )], limit=1 - ) - cls.partner = cls.env['res.partner'].create({ - 'name': 'Test partner', - 'customer': True, - 'property_account_receivable_id': cls.acc_rec.id, - 'property_account_payable_id': cls.acc_pay.id, - }) - cls.payment_mode = cls.env.ref( - 'account_payment_mode.payment_mode_inbound_dd1' + cls.acc_rec = cls.env["account.account"].search( + [ + ( + "user_type_id", + "=", + cls.env.ref("account.data_account_type_receivable").id, + ) + ], + limit=1, + ) + cls.acc_pay = cls.env["account.account"].search( + [ + ( + "user_type_id", + "=", + cls.env.ref("account.data_account_type_payable").id, + ) + ], + limit=1, + ) + cls.acc_rev = cls.env["account.account"].search( + [ + ( + "user_type_id", + "=", + cls.env.ref("account.data_account_type_revenue").id, + ) + ], + limit=1, ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Test partner", + "customer": True, + "property_account_receivable_id": cls.acc_rec.id, + "property_account_payable_id": cls.acc_pay.id, + } + ) + cls.payment_mode = cls.env.ref("account_payment_mode.payment_mode_inbound_dd1") # TODO check why it's not set from demo data cls.payment_mode.auto_reconcile_outstanding_credits = True - cls.product = cls.env.ref('product.consu_delivery_02') - cls.invoice = cls.env['account.invoice'].create({ - 'partner_id': cls.partner.id, - 'type': 'out_invoice', - 'payment_term_id': cls.env.ref('account.account_payment_term').id, - 'account_id': cls.acc_rec.id, - 'invoice_line_ids': [(0, 0, { - 'product_id': cls.product.id, - 'name': cls.product.name, - 'price_unit': 1000.0, - 'quantity': 1, - 'account_id': cls.acc_rev.id, - })], - }) + cls.product = cls.env.ref("product.consu_delivery_02") + cls.invoice = cls.env["account.invoice"].create( + { + "partner_id": cls.partner.id, + "type": "out_invoice", + "payment_term_id": cls.env.ref("account.account_payment_term").id, + "account_id": cls.acc_rec.id, + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + "name": cls.product.name, + "price_unit": 1000.0, + "quantity": 1, + "account_id": cls.acc_rev.id, + }, + ) + ], + } + ) cls.invoice.action_invoice_open() - cls.bank_journal = cls.env['account.journal'].search( - [('type', '=', 'bank')], limit=1 + cls.bank_journal = cls.env["account.journal"].search( + [("type", "=", "bank")], limit=1 ) cls.invoice.pay_and_reconcile(cls.bank_journal) - cls.refund_wiz = cls.env['account.invoice.refund'].with_context( - active_ids=cls.invoice.ids).create({ - 'filter_refund': 'refund', - 'description': 'test' - }) - refund_id = cls.refund_wiz.invoice_refund().get('domain')[1][2] - cls.refund = cls.env['account.invoice'].browse(refund_id) + cls.refund_wiz = ( + cls.env["account.invoice.refund"] + .with_context(active_ids=cls.invoice.ids) + .create({"filter_refund": "refund", "description": "test"}) + ) + refund_id = cls.refund_wiz.invoice_refund().get("domain")[1][2] + cls.refund = cls.env["account.invoice"].browse(refund_id) cls.refund.action_invoice_open() cls.invoice_copy = cls.invoice.copy() - cls.invoice_copy.write({ - 'invoice_line_ids': [(0, 0, { - 'product_id': cls.product.id, - 'name': cls.product.name, - 'price_unit': 500.0, - 'quantity': 1, - 'account_id': cls.acc_rev.id, - })] - }) + cls.invoice_copy.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": cls.product.id, + "name": cls.product.name, + "price_unit": 500.0, + "quantity": 1, + "account_id": cls.acc_rev.id, + }, + ) + ] + } + ) cls.invoice_copy.action_invoice_open() def test_invoice_validate_auto_reconcile(self): - auto_rec_invoice = self.invoice.copy({ - 'payment_mode_id': self.payment_mode.id, - }) - auto_rec_invoice.write({ - 'invoice_line_ids': [(0, 0, { - 'product_id': self.product.id, - 'name': self.product.name, - 'price_unit': 500.0, - 'quantity': 1, - 'account_id': self.acc_rev.id, - })] - }) + auto_rec_invoice = self.invoice.copy( + { + "payment_mode_id": self.payment_mode.id, + } + ) + auto_rec_invoice.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "name": self.product.name, + "price_unit": 500.0, + "quantity": 1, + "account_id": self.acc_rev.id, + }, + ) + ] + } + ) self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) self.assertEqual(self.invoice_copy.residual, 1500) auto_rec_invoice.action_invoice_open() @@ -99,115 +138,127 @@ def test_invoice_validate_auto_reconcile(self): def test_invoice_change_auto_reconcile(self): self.assertEqual(self.invoice_copy.residual, 1500) - self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.invoice_copy.write({"payment_mode_id": self.payment_mode.id}) self.assertEqual(self.invoice_copy.residual, 500) - self.invoice_copy.write({'payment_mode_id': False}) + self.invoice_copy.write({"payment_mode_id": False}) self.assertEqual(self.invoice_copy.residual, 1500) # Copy the refund so there's more outstanding credit than invoice total new_refund = self.refund.copy() - new_refund.date = (date.today() + timedelta(days=1)).strftime( - DATE_FORMAT - ) - new_refund.invoice_line_ids.write({'price_unit': 1200}) + new_refund.date = (date.today() + timedelta(days=1)).strftime(DATE_FORMAT) + new_refund.invoice_line_ids.write({"price_unit": 1200}) new_refund.action_invoice_open() # Set reconcile partial to False self.payment_mode.auto_reconcile_allow_partial = False self.assertFalse(self.payment_mode.auto_reconcile_allow_partial) - self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.invoice_copy.write({"payment_mode_id": self.payment_mode.id}) # Only the older move is used as payment self.assertEqual(self.invoice_copy.residual, 500) - self.invoice_copy.write({'payment_mode_id': False}) + self.invoice_copy.write({"payment_mode_id": False}) self.assertEqual(self.invoice_copy.residual, 1500) # Set allow partial will reconcile both moves self.payment_mode.auto_reconcile_allow_partial = True - self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) - self.assertEqual(self.invoice_copy.state, 'paid') + self.invoice_copy.write({"payment_mode_id": self.payment_mode.id}) + self.assertEqual(self.invoice_copy.state, "paid") self.assertEqual(self.invoice_copy.residual, 0) def test_invoice_auto_unreconcile(self): # Copy the refund so there's more outstanding credit than invoice total new_refund = self.refund.copy() - new_refund.date = (date.today() + timedelta(days=1)).strftime( - DATE_FORMAT - ) - new_refund.invoice_line_ids.write({'price_unit': 1200}) + new_refund.date = (date.today() + timedelta(days=1)).strftime(DATE_FORMAT) + new_refund.invoice_line_ids.write({"price_unit": 1200}) new_refund.action_invoice_open() - auto_rec_invoice = self.invoice.copy({ - 'payment_mode_id': self.payment_mode.id, - }) - auto_rec_invoice.invoice_line_ids.write({'price_unit': 800}) + auto_rec_invoice = self.invoice.copy( + { + "payment_mode_id": self.payment_mode.id, + } + ) + auto_rec_invoice.invoice_line_ids.write({"price_unit": 800}) auto_rec_invoice.action_invoice_open() - self.assertEqual(auto_rec_invoice.state, 'paid') + self.assertEqual(auto_rec_invoice.state, "paid") self.assertEqual(auto_rec_invoice.residual, 0) # As we had 2200 of outstanding credits and 800 was assigned, there's # 1400 left self.assertTrue(self.payment_mode.auto_reconcile_allow_partial) - self.invoice_copy.write({'payment_mode_id': self.payment_mode.id}) + self.invoice_copy.write({"payment_mode_id": self.payment_mode.id}) self.assertEqual(self.invoice_copy.residual, 100) # Unreconcile of an invoice doesn't change the reconciliation of the # other invoice - self.invoice_copy.write({'payment_mode_id': False}) + self.invoice_copy.write({"payment_mode_id": False}) self.assertEqual(self.invoice_copy.residual, 1500) - self.assertEqual(auto_rec_invoice.state, 'paid') + self.assertEqual(auto_rec_invoice.state, "paid") self.assertEqual(auto_rec_invoice.residual, 0) def test_invoice_auto_unreconcile_only_auto_reconcile(self): refund = self.refund.copy() - refund.invoice_line_ids.write({'price_unit': 100}) + refund.invoice_line_ids.write({"price_unit": 100}) refund.action_invoice_open() new_invoice = self.invoice_copy.copy() new_invoice.action_invoice_open() # Only reconcile 1000 refund manually new_invoice_credits = json.loads( new_invoice.outstanding_credits_debits_widget - ).get('content') + ).get("content") for cred in new_invoice_credits: - if cred.get('amount') == 100.0: - new_invoice.assign_outstanding_credit(cred.get('id')) + if cred.get("amount") == 100.0: + new_invoice.assign_outstanding_credit(cred.get("id")) self.assertEqual(new_invoice.residual, 1400.0) # Assign payment mode adds the outstanding credit of 1000.0 - new_invoice.write({'payment_mode_id': self.payment_mode.id}) + new_invoice.write({"payment_mode_id": self.payment_mode.id}) self.assertEqual(new_invoice.residual, 400.0) # Remove payment mode only removes automatically added credit - new_invoice.write({'payment_mode_id': False}) + new_invoice.write({"payment_mode_id": False}) self.assertEqual(new_invoice.residual, 1400.0) # use the same payment partially on different invoices. other_invoice = self.invoice.copy() - other_invoice.invoice_line_ids.write({ - 'price_unit': 200, - }) - other_invoice.write({ - 'payment_mode_id': self.payment_mode.id, - }) + other_invoice.invoice_line_ids.write( + { + "price_unit": 200, + } + ) + other_invoice.write( + { + "payment_mode_id": self.payment_mode.id, + } + ) other_invoice.action_invoice_open() - self.assertEqual(other_invoice.state, 'paid') + self.assertEqual(other_invoice.state, "paid") # since 200 were assigned on other invoice adding auto-rec payment mode # on new_invoice will reconcile 800 and residual will be 600 - new_invoice.write({'payment_mode_id': self.payment_mode.id}) + new_invoice.write({"payment_mode_id": self.payment_mode.id}) self.assertEqual(new_invoice.residual, 600.0) # Removing the payment mode should not remove the partial payment on # the other invoice - new_invoice.write({'payment_mode_id': False}) + new_invoice.write({"payment_mode_id": False}) self.assertEqual(new_invoice.residual, 1400.0) - self.assertEqual(other_invoice.state, 'paid') + self.assertEqual(other_invoice.state, "paid") def test_invoice_auto_reconcile_same_journal(self): """Check reconciling credits on same journal.""" self.payment_mode.auto_reconcile_same_journal = True - auto_rec_invoice = self.invoice.copy({ - 'payment_mode_id': self.payment_mode.id, - }) - auto_rec_invoice.write({ - 'invoice_line_ids': [(0, 0, { - 'product_id': self.product.id, - 'name': self.product.name, - 'price_unit': 500.0, - 'quantity': 1, - 'account_id': self.acc_rev.id, - })] - }) + auto_rec_invoice = self.invoice.copy( + { + "payment_mode_id": self.payment_mode.id, + } + ) + auto_rec_invoice.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "name": self.product.name, + "price_unit": 500.0, + "quantity": 1, + "account_id": self.acc_rev.id, + }, + ) + ] + } + ) self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) self.assertEqual(self.invoice_copy.residual, 1500) auto_rec_invoice.action_invoice_open() @@ -216,19 +267,29 @@ def test_invoice_auto_reconcile_same_journal(self): def test_invoice_auto_reconcile_different_journal(self): """Check not reconciling credits on different journal.""" self.payment_mode.auto_reconcile_same_journal = True - auto_rec_invoice = self.invoice.copy({ - 'payment_mode_id': self.payment_mode.id, - 'journal_id': self.bank_journal.id, - }) - auto_rec_invoice.write({ - 'invoice_line_ids': [(0, 0, { - 'product_id': self.product.id, - 'name': self.product.name, - 'price_unit': 500.0, - 'quantity': 1, - 'account_id': self.acc_rev.id, - })] - }) + auto_rec_invoice = self.invoice.copy( + { + "payment_mode_id": self.payment_mode.id, + "journal_id": self.bank_journal.id, + } + ) + auto_rec_invoice.write( + { + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "name": self.product.name, + "price_unit": 500.0, + "quantity": 1, + "account_id": self.acc_rev.id, + }, + ) + ] + } + ) self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits) self.assertEqual(self.invoice_copy.residual, 1500) auto_rec_invoice.action_invoice_open() diff --git a/account_payment_mode_auto_reconcile/views/account_invoice.xml b/account_payment_mode_auto_reconcile/views/account_invoice.xml index 1cf1c02d74..0c0f812228 100644 --- a/account_payment_mode_auto_reconcile/views/account_invoice.xml +++ b/account_payment_mode_auto_reconcile/views/account_invoice.xml @@ -1,4 +1,4 @@ - + account_payment_partner.invoice_form.inherit @@ -6,11 +6,14 @@ - " + "