From 5f24f8a3fef188c41cc49dfd75704cf72b65af91 Mon Sep 17 00:00:00 2001 From: tekvinci Date: Thu, 2 Oct 2025 13:22:12 +0000 Subject: [PATCH 001/118] Added translation using Weblate (Chinese (Traditional) (zh_TW)) --- .../i18n/zh_TW.po | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 account_check_printing_report_base/i18n/zh_TW.po diff --git a/account_check_printing_report_base/i18n/zh_TW.po b/account_check_printing_report_base/i18n/zh_TW.po new file mode 100644 index 00000000000..07225c2b7ea --- /dev/null +++ b/account_check_printing_report_base/i18n/zh_TW.po @@ -0,0 +1,153 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_check_printing_report_base +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +msgid "Check Amount:" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields,field_description:account_check_printing_report_base.field_account_journal__check_print_auto +msgid "Automatic check printing" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Balance Due" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields,field_description:account_check_printing_report_base.field_res_company__account_check_printing_layout +msgid "Check Layout" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_res_company +msgid "Companies" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Dear partner" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields,help:account_check_printing_report_base.field_account_journal__check_print_auto +msgid "" +"Default check for the company is printed automatically when invoice payment " +"is validated" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +msgid "Description" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Due Date" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Invoice / Reference" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Invoice date" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_account_journal +msgid "Journal" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Original Amount" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_account_payment_register +msgid "Pay" +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Payment" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.actions.report,name:account_check_printing_report_base.action_report_check_base +msgid "Payment Check Base" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.actions.report,name:account_check_printing_report_base.action_report_check_base_a4 +msgid "Payment Check Base A4" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_account_payment +msgid "Payments" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields.selection,name:account_check_printing_report_base.selection__res_company__account_check_printing_layout__account_check_printing_report_base_action_report_check_base +msgid "Print Check Base" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields.selection,name:account_check_printing_report_base.selection__res_company__account_check_printing_layout__account_check_printing_report_base_action_report_check_base_a4 +msgid "Print Check Base A4" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_report_account_check_printing_report_base_report_check_base +msgid "Report Check Print" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_report_account_check_printing_report_base_report_check_base_a4 +msgid "Report Check Print for A4" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model,name:account_check_printing_report_base.model_report_account_check_printing_report_base_promissory_footer_a4 +msgid "Report Promissory Note Print for A4" +msgstr "" + +#. module: account_check_printing_report_base +#: model:ir.model.fields,help:account_check_printing_report_base.field_res_company__account_check_printing_layout +msgid "" +"Select the format corresponding to the check paper you will be printing your checks on.\n" +"In order to disable the printing feature, select 'None'." +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "Waiting for your agreement, regards." +msgstr "" + +#. module: account_check_printing_report_base +#: model_terms:ir.ui.view,arch_db:account_check_printing_report_base.report_check_base_a4_document +msgid "We attach the list of invoices paid with this check with amount of" +msgstr "" From 72603e0ed95c4bf47b641bcbdb9f9efdd702a13b Mon Sep 17 00:00:00 2001 From: Carlos Dauden Date: Mon, 7 Oct 2024 13:07:50 +0200 Subject: [PATCH 002/118] [FIX] account_payment_return_import_iso20022: Amount is not computed when match by payment reference This commit call supper method without processed lines in self: https://github.com/OCA/account-payment/commit/cd89ad977665da8d005549ffcf899fc2758a3388 and that proceed lines are not computed here: https://github.com/OCA/account-payment/blob/f6f205acd14a474074e4cd301dad8ed426bcd15c/account_payment_return/models/payment_return.py#L393 TT58268 --- .../models/payment_return.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/account_payment_return_import_iso20022/models/payment_return.py b/account_payment_return_import_iso20022/models/payment_return.py index 9c409037411..2bbed183b05 100644 --- a/account_payment_return_import_iso20022/models/payment_return.py +++ b/account_payment_return_import_iso20022/models/payment_return.py @@ -11,23 +11,21 @@ class PaymentReturnLine(models.Model): def _find_match(self): """Include in the matches the lines coming from payment orders.""" - matched = self.env["payment.return.line"] - # noqa B023 for line in self.filtered(lambda x: not x.move_line_ids and x.reference): - move_id = int(line.reference) if line.reference.isdigit() else -1 + if not line.reference.isdigit(): + continue payments = self.env["account.payment"].search( [ - ("move_id", "=", move_id), + ("move_id", "=", int(line.reference)), ("payment_order_id", "!=", False), ], ) if payments: line.partner_id = payments[0].partner_id - matched += line for payment in payments: line.move_line_ids |= payment.move_id.line_ids.filtered( lambda x, payment=payment: x.account_id == payment.destination_account_id and x.partner_id == payment.partner_id ) - return super(PaymentReturnLine, self - matched)._find_match() + return super()._find_match() From 12eed789f263a7560aa17b936a352a990f4ec072 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Sat, 4 Oct 2025 11:48:31 +0000 Subject: [PATCH 003/118] [BOT] post-merge updates --- README.md | 2 +- account_payment_return_import_iso20022/README.rst | 2 +- account_payment_return_import_iso20022/__manifest__.py | 2 +- .../static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 56b6e3a7167..b391f41226a 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ addon | version | maintainers | summary [account_payment_promissory_note](account_payment_promissory_note/) | 18.0.1.0.1 | | Account Payment Promissory Note [account_payment_return](account_payment_return/) | 18.0.1.0.2 | | Manage the return of your payments [account_payment_return_import](account_payment_return_import/) | 18.0.1.0.1 | | This module adds a generic wizard to import payment returnfile formats. Is only the base to be extended by anothermodules -[account_payment_return_import_iso20022](account_payment_return_import_iso20022/) | 18.0.1.1.0 | | This addon allows to import payment returns from ISO 20022 files like PAIN or CAMT. +[account_payment_return_import_iso20022](account_payment_return_import_iso20022/) | 18.0.1.1.1 | | This addon allows to import payment returns from ISO 20022 files like PAIN or CAMT. [account_payment_show_invoice](account_payment_show_invoice/) | 18.0.1.0.0 | | Extends the tree view of payments to show the paid invoices related to the payments using the vendor reference by default [account_payment_term_extension](account_payment_term_extension/) | 18.0.1.0.0 | | Adds rounding, months, weeks and multiple payment days properties on payment term lines [account_payment_tier_validation](account_payment_tier_validation/) | 18.0.1.0.1 | | Extends the functionality of Payment to support a tier validation process. diff --git a/account_payment_return_import_iso20022/README.rst b/account_payment_return_import_iso20022/README.rst index 76e900742aa..931a5dec2be 100644 --- a/account_payment_return_import_iso20022/README.rst +++ b/account_payment_return_import_iso20022/README.rst @@ -11,7 +11,7 @@ Account Payment Return Import Iso20022 !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:8709fba0e914293def05cc250c2ca250b7dc6e2fbb78dd4a1469e3bc9bf35414 + !! source digest: sha256:51c389b6e4afb13ea8dabf2e57d4a8fab7b3de62b2c14cae842749ed2be78237 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/account_payment_return_import_iso20022/__manifest__.py b/account_payment_return_import_iso20022/__manifest__.py index 02e425b2277..1e9e84f793a 100644 --- a/account_payment_return_import_iso20022/__manifest__.py +++ b/account_payment_return_import_iso20022/__manifest__.py @@ -7,7 +7,7 @@ "summary": """ This addon allows to import payment returns from ISO 20022 files like PAIN or CAMT.""", - "version": "18.0.1.1.0", + "version": "18.0.1.1.1", "development_status": "Mature", "license": "AGPL-3", "author": "Odoo Community Association (OCA),Tecnativa,ACSONE SA/NV", diff --git a/account_payment_return_import_iso20022/static/description/index.html b/account_payment_return_import_iso20022/static/description/index.html index bf36a691af1..c2d8afa39ef 100644 --- a/account_payment_return_import_iso20022/static/description/index.html +++ b/account_payment_return_import_iso20022/static/description/index.html @@ -372,7 +372,7 @@

Account Payment Return Import Iso20022

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:8709fba0e914293def05cc250c2ca250b7dc6e2fbb78dd4a1469e3bc9bf35414 +!! source digest: sha256:51c389b6e4afb13ea8dabf2e57d4a8fab7b3de62b2c14cae842749ed2be78237 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

Module to import payment returns from pain.002.001.03, camt.053.001.02 From 0a3a6443fd100a203aac702f2a726d581231d8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Ra=C3=AFch?= Date: Thu, 9 Oct 2025 11:15:32 +0200 Subject: [PATCH 004/118] [IMP] Eficent -> ForgeFlow --- account_check_printing_report_base/__manifest__.py | 6 +++--- .../models/account_payment.py | 4 ++-- account_check_printing_report_base/models/res_company.py | 4 ++-- account_check_printing_report_base/readme/CONTRIBUTORS.md | 4 ++-- account_check_printing_report_base/report/check_print.py | 4 ++-- .../tests/test_check_printing_report.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/account_check_printing_report_base/__manifest__.py b/account_check_printing_report_base/__manifest__.py index 90ae4fc6cce..750ef064da2 100644 --- a/account_check_printing_report_base/__manifest__.py +++ b/account_check_printing_report_base/__manifest__.py @@ -1,5 +1,5 @@ -# Copyright 2016 Eficent Business and IT Consulting Services S.L. -# (http://www.eficent.com) +# Copyright 2016 ForgeFlow, S.L. +# (http://www.forgeflow.com) # Copyright 2016 Serpent Consulting Services Pvt. Ltd. # Copyright 2017 Tecnativa. # Copyright 2018 iterativo. @@ -9,7 +9,7 @@ "name": "Account Check Printing Report Base", "version": "18.0.1.0.0", "license": "AGPL-3", - "author": "Eficent," + "author": "ForgeFlow," "Serpent Consulting Services Pvt. Ltd.," "Ursa Information Systems," "Odoo Community Association (OCA)", diff --git a/account_check_printing_report_base/models/account_payment.py b/account_check_printing_report_base/models/account_payment.py index a629998402e..8014ef4421a 100644 --- a/account_check_printing_report_base/models/account_payment.py +++ b/account_check_printing_report_base/models/account_payment.py @@ -1,5 +1,5 @@ -# Copyright 2016 Eficent Business and IT Consulting Services S.L. -# (http://www.eficent.com) +# Copyright 2016 ForgeFLow, S.L. +# (http://www.forgeflow.com) # Copyright 2016 Serpent Consulting Services Pvt. Ltd. # Copyright 2018 iterativo. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/account_check_printing_report_base/models/res_company.py b/account_check_printing_report_base/models/res_company.py index 658e144be1c..18c2bb2777c 100644 --- a/account_check_printing_report_base/models/res_company.py +++ b/account_check_printing_report_base/models/res_company.py @@ -1,5 +1,5 @@ -# Copyright 2016 Eficent Business and IT Consulting Services S.L. -# (http://www.eficent.com) +# Copyright 2016 ForgeFlow, S.L. +# (http://www.forgeflow.com) # Copyright 2016 Serpent Consulting Services Pvt. Ltd. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/account_check_printing_report_base/readme/CONTRIBUTORS.md b/account_check_printing_report_base/readme/CONTRIBUTORS.md index 0db76faa65d..c1c1b25a444 100644 --- a/account_check_printing_report_base/readme/CONTRIBUTORS.md +++ b/account_check_printing_report_base/readme/CONTRIBUTORS.md @@ -1,6 +1,6 @@ -- Jordi Ballester Alomar \<\> +- Jordi Ballester Alomar \<\> -- Lois Rilo Antelo \<\> +- Lois Rilo Antelo \<\> - Sandip Mangukiya \<\> diff --git a/account_check_printing_report_base/report/check_print.py b/account_check_printing_report_base/report/check_print.py index fd188e99e2c..0957974cf23 100644 --- a/account_check_printing_report_base/report/check_print.py +++ b/account_check_printing_report_base/report/check_print.py @@ -1,5 +1,5 @@ -# Copyright 2016 Eficent Business and IT Consulting Services S.L. -# (http://www.eficent.com) +# Copyright 2016 ForgeFlow, S.L. +# (http://www.forgeflow.com) # Copyright 2016 Serpent Consulting Services Pvt. Ltd. # Copyright 2018 iterativo. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). diff --git a/account_check_printing_report_base/tests/test_check_printing_report.py b/account_check_printing_report_base/tests/test_check_printing_report.py index 8da4f526fa7..026153f33d4 100644 --- a/account_check_printing_report_base/tests/test_check_printing_report.py +++ b/account_check_printing_report_base/tests/test_check_printing_report.py @@ -1,4 +1,4 @@ -# © 2016 Eficent Business and IT Consulting Services S.L. +# Copyright 2016 ForgeFlow, S.L. # Copyright 2018 iterativo. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). From 166dc06974634479be1865454394bf0fa4fdb10a Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 9 Oct 2025 11:48:28 +0000 Subject: [PATCH 005/118] [BOT] post-merge updates --- README.md | 2 +- account_check_printing_report_base/README.rst | 14 ++++--- .../__manifest__.py | 2 +- .../static/description/index.html | 38 +++++++++++-------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b391f41226a..39a640c8ca9 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Available addons ---------------- addon | version | maintainers | summary --- | --- | --- | --- -[account_check_printing_report_base](account_check_printing_report_base/) | 18.0.1.0.0 | | Account Check Printing Report Base +[account_check_printing_report_base](account_check_printing_report_base/) | 18.0.1.0.1 | | Account Check Printing Report Base [account_due_list](account_due_list/) | 18.0.1.0.0 | | List of open credits and debits, with due date [account_due_list_payment_mode](account_due_list_payment_mode/) | 18.0.1.0.0 | | Payment Due List Payment Mode [account_payment_method_base](account_payment_method_base/) | 18.0.1.0.0 | alexis-via | Add form and list view for account.payment.method diff --git a/account_check_printing_report_base/README.rst b/account_check_printing_report_base/README.rst index 2e87b344b37..b84444b5c80 100644 --- a/account_check_printing_report_base/README.rst +++ b/account_check_printing_report_base/README.rst @@ -1,3 +1,7 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + ================================== Account Check Printing Report Base ================================== @@ -7,13 +11,13 @@ Account Check Printing Report Base !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:27ebc7c54703854d7c51fafb184e47fc0bbb992b6f9d4a063af3570107ee61c2 + !! source digest: sha256:fb169864e7abd3215b6cd98ebea3e8ea6d55d9e6d19440b656b555512131d2fe !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |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 +.. |badge2| image:: https://img.shields.io/badge/license-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--payment-lightgray.png?logo=github @@ -76,16 +80,16 @@ Credits Authors ------- -* Eficent +* ForgeFlow * Serpent Consulting Services Pvt. Ltd. * Ursa Information Systems Contributors ------------ -- Jordi Ballester Alomar +- Jordi Ballester Alomar -- Lois Rilo Antelo +- Lois Rilo Antelo - Sandip Mangukiya diff --git a/account_check_printing_report_base/__manifest__.py b/account_check_printing_report_base/__manifest__.py index 750ef064da2..d013f8c52bf 100644 --- a/account_check_printing_report_base/__manifest__.py +++ b/account_check_printing_report_base/__manifest__.py @@ -7,7 +7,7 @@ { "name": "Account Check Printing Report Base", - "version": "18.0.1.0.0", + "version": "18.0.1.0.1", "license": "AGPL-3", "author": "ForgeFlow," "Serpent Consulting Services Pvt. Ltd.," diff --git a/account_check_printing_report_base/static/description/index.html b/account_check_printing_report_base/static/description/index.html index bfd8e4dc69e..339914f7f2b 100644 --- a/account_check_printing_report_base/static/description/index.html +++ b/account_check_printing_report_base/static/description/index.html @@ -3,7 +3,7 @@ -Account Check Printing Report Base +README.rst -

-

Account Check Printing Report Base

+
+ + +Odoo Community Association + +
+

Account Check Printing Report Base

-

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

This module provides the basic framework for check printing, and a sample layout.

Table of contents

@@ -388,7 +393,7 @@

Account Check Printing Report Base

-

Configuration

+

Configuration

Add your user to ‘Show Full Accounting Features’ group.

Go to ‘Invoicing / Configuration / Settings’ and assign the desired check format on ‘Check Layout’ field. This module proposes a basic @@ -396,7 +401,7 @@

Configuration

provide formats adjusted to known check formats such as DLT103.

-

Usage

+

Usage

  • Go to ‘Invoicing / Vendors / Payments’. Select one of the payments with type ‘Check’ and print the check.
  • @@ -405,14 +410,14 @@

    Usage

-

Known issues / Roadmap

+

Known issues / Roadmap

  • When print check automatically in the payment validation process, the wizard remains opened.
-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed @@ -420,21 +425,21 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

    -
  • Eficent
  • +
  • ForgeFlow
  • Serpent Consulting Services Pvt. Ltd.
  • Ursa Information Systems
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

Odoo Community Association @@ -464,5 +469,6 @@

Maintainers

+
From b97b2a93f194a0e6a657cf58c98d1bb0c6309e55 Mon Sep 17 00:00:00 2001 From: Christopher Ormaza Date: Wed, 28 Dec 2022 15:03:12 -0500 Subject: [PATCH 006/118] [14.0][ADD] account_payment_lines --- account_payment_line/README.rst | 98 ++ account_payment_line/__init__.py | 5 + account_payment_line/__manifest__.py | 21 + account_payment_line/hooks.py | 92 ++ account_payment_line/models/__init__.py | 3 + account_payment_line/models/account_move.py | 12 + .../models/account_payment.py | 294 +++++ .../models/counterpart_line.py | 189 +++ account_payment_line/readme/CONTRIBUTORS.rst | 1 + account_payment_line/readme/DESCRIPTION.rst | 5 + account_payment_line/readme/USAGE.rst | 6 + .../security/ir.model.access.csv | 5 + .../static/description/index.html | 435 +++++++ account_payment_line/tests/__init__.py | 3 + .../tests/test_account_payment_line.py | 1014 +++++++++++++++++ .../views/account_payment_views.xml | 147 +++ 16 files changed, 2330 insertions(+) create mode 100644 account_payment_line/README.rst create mode 100644 account_payment_line/__init__.py create mode 100644 account_payment_line/__manifest__.py create mode 100644 account_payment_line/hooks.py create mode 100644 account_payment_line/models/__init__.py create mode 100644 account_payment_line/models/account_move.py create mode 100644 account_payment_line/models/account_payment.py create mode 100644 account_payment_line/models/counterpart_line.py create mode 100644 account_payment_line/readme/CONTRIBUTORS.rst create mode 100644 account_payment_line/readme/DESCRIPTION.rst create mode 100644 account_payment_line/readme/USAGE.rst create mode 100644 account_payment_line/security/ir.model.access.csv create mode 100644 account_payment_line/static/description/index.html create mode 100644 account_payment_line/tests/__init__.py create mode 100644 account_payment_line/tests/test_account_payment_line.py create mode 100644 account_payment_line/views/account_payment_views.xml diff --git a/account_payment_line/README.rst b/account_payment_line/README.rst new file mode 100644 index 00000000000..8a82296e677 --- /dev/null +++ b/account_payment_line/README.rst @@ -0,0 +1,98 @@ +========================= +Payment Counterpart Lines +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:255742abf8b18f2210925ddcb12a12a065e1c601d70058f9f45d56df5ff0f6fb + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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--payment-lightgray.png?logo=github + :target: https://github.com/OCA/account-payment/tree/14.0/account_payment_line + :alt: OCA/account-payment +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-payment-14-0/account-payment-14-0-account_payment_line + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-payment&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module is an utility module to add lines in payment, allowing users make +more complicated cases when processing payments, split on many invoices, +set up specific write-off and adding some analytic information + +Add tool to proposal of payment distributions, ordering by due date + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +You can use payment distribution suggestion, and if system found moves +pending to reconcile related with partner selected, system will create +all lines trying to pay all invoices until amount remain + +You can add manually lines, if payment don't detect lines specified, payment +works as a normal payment + +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 to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow S.L. + +Contributors +~~~~~~~~~~~~ + +* Christopher Ormaza. + +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. + +.. |maintainer-ChrisOForgeFlow| image:: https://github.com/ChrisOForgeFlow.png?size=40px + :target: https://github.com/ChrisOForgeFlow + :alt: ChrisOForgeFlow + +Current `maintainer `__: + +|maintainer-ChrisOForgeFlow| + +This module is part of the `OCA/account-payment `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_payment_line/__init__.py b/account_payment_line/__init__.py new file mode 100644 index 00000000000..e2b838b2208 --- /dev/null +++ b/account_payment_line/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from . import models +from .hooks import post_load_hook diff --git a/account_payment_line/__manifest__.py b/account_payment_line/__manifest__.py new file mode 100644 index 00000000000..e39b5e864d2 --- /dev/null +++ b/account_payment_line/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +{ + "name": "Payment Counterpart Lines", + "summary": """Payment Counterpart Lines""", + "author": "ForgeFlow S.L., Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-payment", + "category": "Account", + "version": "14.0.1.0.0", + "license": "AGPL-3", + "depends": ["account"], + "data": [ + "security/ir.model.access.csv", + "views/account_payment_views.xml", + ], + "maintainers": ["ChrisOForgeFlow"], + "installable": True, + "post_load": "post_load_hook", + "auto_install": False, +} diff --git a/account_payment_line/hooks.py b/account_payment_line/hooks.py new file mode 100644 index 00000000000..6db7dc17b25 --- /dev/null +++ b/account_payment_line/hooks.py @@ -0,0 +1,92 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _ +from odoo.exceptions import UserError + +from odoo.addons.account.models.account_payment import ( + AccountPayment as AccountPaymentClass, +) + + +def post_load_hook(): # noqa: C901 + def _synchronize_from_moves_new(self, changed_fields): + if not self.line_payment_counterpart_ids: + return self._synchronize_from_moves_original(changed_fields) + if self._context.get("skip_account_move_synchronization"): + return + + for pay in self.with_context(skip_account_move_synchronization=True): + + if pay.move_id.statement_line_id: + continue + + move = pay.move_id + move_vals_to_write = {} + payment_vals_to_write = {} + + if "journal_id" in changed_fields: + if pay.journal_id.type not in ("bank", "cash"): + raise UserError( + _("A payment must always belongs to a bank or cash journal.") + ) + + if "line_ids" in changed_fields: + all_lines = move.line_ids + ( + liquidity_lines, + counterpart_lines, + writeoff_lines, + ) = pay._seek_for_lines() + + if any( + line.currency_id != all_lines[0].currency_id for line in all_lines + ): + raise UserError( + _( + "Journal Entry %s is not valid. " + "In order to proceed, " + "the journal items must " + "share the same currency.", + move.display_name, + ) + ) + + if "receivable" in counterpart_lines.mapped( + "account_id.user_type_id.type" + ): + partner_type = "customer" + else: + partner_type = "supplier" + + liquidity_amount = liquidity_lines.amount_currency + + move_vals_to_write.update( + { + "currency_id": liquidity_lines.currency_id.id, + "partner_id": liquidity_lines.partner_id.id, + } + ) + destination_account_id = counterpart_lines.mapped("account_id")[0].id + payment_vals_to_write.update( + { + "amount": abs(liquidity_amount), + "partner_type": partner_type, + "currency_id": liquidity_lines.currency_id.id, + "destination_account_id": destination_account_id, + "partner_id": liquidity_lines.partner_id.id, + } + ) + if liquidity_amount > 0.0: + payment_vals_to_write.update({"payment_type": "inbound"}) + elif liquidity_amount < 0.0: + payment_vals_to_write.update({"payment_type": "outbound"}) + + move.write(move._cleanup_write_orm_values(move, move_vals_to_write)) + pay.write(move._cleanup_write_orm_values(pay, payment_vals_to_write)) + + if not hasattr(AccountPaymentClass, "_synchronize_from_moves_original"): + AccountPaymentClass._synchronize_from_moves_original = ( + AccountPaymentClass._synchronize_from_moves + ) + AccountPaymentClass._synchronize_from_moves = _synchronize_from_moves_new diff --git a/account_payment_line/models/__init__.py b/account_payment_line/models/__init__.py new file mode 100644 index 00000000000..a41b4214589 --- /dev/null +++ b/account_payment_line/models/__init__.py @@ -0,0 +1,3 @@ +from . import counterpart_line +from . import account_payment +from . import account_move diff --git a/account_payment_line/models/account_move.py b/account_payment_line/models/account_move.py new file mode 100644 index 00000000000..dbfb5428c46 --- /dev/null +++ b/account_payment_line/models/account_move.py @@ -0,0 +1,12 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import fields, models + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + payment_line_id = fields.Many2one( + "account.payment.counterpart.line", string="Payment line", ondelete="set null" + ) diff --git a/account_payment_line/models/account_payment.py b/account_payment_line/models/account_payment.py new file mode 100644 index 00000000000..eb9b7053684 --- /dev/null +++ b/account_payment_line/models/account_payment.py @@ -0,0 +1,294 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from odoo import _, fields, models +from odoo.exceptions import ValidationError +from odoo.tools import float_is_zero + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + line_payment_counterpart_ids = fields.One2many( + "account.payment.counterpart.line", + "payment_id", + string="Counterpart Lines", + readonly=True, + states={"draft": [("readonly", False)]}, + help="Use these lines to add matching lines, for example in a credit" + "card payment, financing interest or commission is added", + ) + writeoff_account_id = fields.Many2one( + "account.account", + string="Write-off Account", + domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + ) + + def _process_post_reconcile(self): + for rec in self: + for line in rec.line_payment_counterpart_ids: + if line.aml_id: + to_reconcile = (line.aml_id + line.move_ids).filtered( + lambda x: not x.reconciled and x.account_id.reconcile + ) + if to_reconcile: + to_reconcile.reconcile() + return True + + def _get_moves_domain(self): + domain = [ + ("amount_residual", "!=", 0.0), + ("state", "=", "posted"), + ("company_id", "=", self.company_id.id), + ( + "commercial_partner_id", + "=", + self.partner_id.commercial_partner_id.id, + ), + ] + if self.partner_type == "supplier": + if self.payment_type == "outbound": + domain.append(("move_type", "in", ("in_invoice", "in_receipt"))) + if self.payment_type == "inbound": + domain.append(("move_type", "=", "in_refund")) + elif self.partner_type == "customer": + if self.payment_type == "outbound": + domain.append(("move_type", "=", "out_refund")) + if self.payment_type == "inbound": + domain.append(("move_type", "in", ("out_invoice", "out_receipt"))) + return domain + + def _filter_amls(self, amls): + return amls.filtered( + lambda x: x.partner_id.commercial_partner_id.id + == self.partner_id.commercial_partner_id.id + and x.amount_residual != 0 + and x.account_id.internal_type in ("receivable", "payable") + ) + + def _hook_create_new_line(self, invoice, aml, amount_to_apply): + line_model = self.env["account.payment.counterpart.line"] + self.ensure_one() + return line_model.create( + { + "payment_id": self.id, + "name": "/", + "move_id": invoice.id, + "aml_id": aml.id, + "account_id": aml.account_id.id, + "partner_id": self.partner_id.commercial_partner_id.id, + "amount": amount_to_apply, + } + ) + + def action_propose_payment_distribution(self): + move_model = self.env["account.move"] + for rec in self: + if self.is_internal_transfer: + continue + domain = self._get_moves_domain() + pending_invoices = move_model.search(domain, order="invoice_date_due ASC") + pending_amount = rec.amount + rec.line_payment_counterpart_ids.unlink() + for invoice in pending_invoices: + for aml in self._filter_amls(invoice.line_ids): + amount_to_apply = 0 + amount_residual = rec.company_id.currency_id._convert( + aml.amount_residual, + rec.currency_id, + rec.company_id, + date=rec.date, + ) + if pending_amount >= 0: + amount_to_apply = min(abs(amount_residual), pending_amount) + pending_amount -= abs(amount_residual) + rec._hook_create_new_line(invoice, aml, amount_to_apply) + + def action_delete_counterpart_lines(self): + if self.line_payment_counterpart_ids and self.state == "draft": + self.line_payment_counterpart_ids = [(5, 0, 0)] + + def _prepare_move_line_default_vals(self, write_off_line_vals=False): + res = super(AccountPayment, self)._prepare_move_line_default_vals( + write_off_line_vals + ) + write_off_amount_currency = ( + write_off_line_vals and write_off_line_vals.get("amount", 0.0) or 0.0 + ) + if self.payment_type == "outbound": + write_off_amount_currency *= -1 + write_off_balance = self.currency_id._convert( + write_off_amount_currency, + self.company_id.currency_id, + self.company_id, + self.date, + ) + new_aml_lines = [] + for line in self.line_payment_counterpart_ids.filtered( + lambda x: not float_is_zero( + x.amount, precision_digits=self.currency_id.decimal_places + ) + ): + line_balance = ( + line.amount if self.payment_type == "outbound" else line.amount * -1 + ) + line_balance_currency = ( + line.amount_currency + if self.payment_type == "outbound" + else line.amount_currency * -1 + ) + aml_value = line_balance_currency + write_off_balance + aml_value_currency = line_balance + write_off_amount_currency + if line.fully_paid and not float_is_zero( + line.writeoff_amount, precision_digits=self.currency_id.decimal_places + ): + write_off_account = ( + line.writeoff_account_id.id or self.writeoff_account_id.id + ) + if not write_off_account: + raise ValidationError( + _( + "Write-off account is not set for payment %s" + % self.display_name + ) + ) + # Fully Paid line + new_aml_lines.append( + { + "name": line.display_name, + "debit": line.aml_amount_residual < 0.0 + and abs(line.aml_amount_residual) + or 0.0, + "credit": line.aml_amount_residual > 0.0 + and abs(line.aml_amount_residual) + or 0.0, + "amount_currency": abs(line.aml_amount_residual_currency) + * (line.aml_amount_residual > 0.0 and -1 or 1), + "date_maturity": self.date, + "partner_id": line.partner_id.commercial_partner_id.id, + "account_id": line.account_id.id, + "currency_id": line.payment_id.currency_id.id, + "payment_id": self.id, + "payment_line_id": line.id, + "analytic_account_id": line.analytic_account_id.id, + "analytic_tag_ids": line.analytic_tag_ids + and [(6, 0, line.analytic_tag_ids.ids)] + or [], + } + ) + # write-off line + new_aml_lines.append( + { + "name": _("Write-off"), + "debit": line.writeoff_amount > 0.0 + and line.writeoff_amount + or 0.0, + "credit": line.writeoff_amount < 0.0 + and -line.writeoff_amount + or 0.0, + "amount_currency": abs(line.writeoff_amount_currency) + * (line.writeoff_amount < 0.0 and -1 or 1), + "date_maturity": self.date, + "partner_id": line.partner_id.commercial_partner_id.id, + "account_id": write_off_account, + "currency_id": line.payment_id.currency_id.id, + "payment_id": self.id, + "payment_line_id": line.id, + "analytic_account_id": line.analytic_account_id.id, + "analytic_tag_ids": line.analytic_tag_ids + and [(6, 0, line.analytic_tag_ids.ids)] + or [], + } + ) + + else: + new_aml_lines.append( + { + "name": line.display_name, + "debit": aml_value > 0.0 and aml_value or 0.0, + "credit": aml_value < 0.0 and -aml_value or 0.0, + "amount_currency": abs(aml_value_currency) + * (aml_value < 0.0 and -1 or 1), + "date_maturity": self.date, + "partner_id": line.partner_id.commercial_partner_id.id, + "account_id": line.account_id.id, + "currency_id": line.payment_id.currency_id.id, + "payment_id": self.id, + "payment_line_id": line.id, + "analytic_account_id": line.analytic_account_id.id, + "analytic_tag_ids": line.analytic_tag_ids + and [(6, 0, line.analytic_tag_ids.ids)] + or [], + } + ) + if len(res) >= 2 and new_aml_lines: + res.pop(1) + res += new_aml_lines + return res + + def _check_writeoff_lines(self): + for rec in self: + writeoff_lines = rec.line_payment_counterpart_ids.filtered( + lambda x: x.fully_paid + and not float_is_zero( + x.writeoff_amount, precision_digits=rec.currency_id.decimal_places + ) + ) + if not rec.writeoff_account_id and not all( + line.writeoff_account_id for line in writeoff_lines + ): + raise ValidationError( + _( + "You should set up write-off account on lines or in header to continue" + ) + ) + + def action_post(self): + self._check_writeoff_lines() + for rec in self.filtered(lambda x: x.line_payment_counterpart_ids): + if rec.move_id.line_ids: + rec.move_id.line_ids.unlink() + rec.move_id.line_ids = [ + (0, 0, line_vals) for line_vals in rec._prepare_move_line_default_vals() + ] + res = super(AccountPayment, self).action_post() + self._process_post_reconcile() + return res + + def action_draft(self): + res = super().action_draft() + for rec in self.filtered(lambda x: x.line_payment_counterpart_ids): + # CHECK ME: force to recreate lines + # if document back to draft state, + # because we can change counterpart lines, + # but change will not be propagated properly + rec.move_id.line_ids.unlink() + return res + + +class AccountPaymentCounterLine(models.Model): + _name = "account.payment.counterpart.line" + _inherit = "account.payment.counterpart.line.abstract" + _description = "Counterpart line payment" + + payment_id = fields.Many2one( + "account.payment", string="Payment", required=False, ondelete="cascade" + ) + analytic_tag_ids = fields.Many2many( + comodel_name="account.analytic.tag", + relation="counterpart_line_analytic_tag_rel", + column1="line_id", + column2="tag_id", + string="Analytic Tags", + domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", + check_company=True, + ) + + def _get_onchange_fields(self): + return ( + "aml_id.amount_residual", + "amount", + "payment_id.currency_id", + "payment_id.date", + "fully_paid", + ) diff --git a/account_payment_line/models/counterpart_line.py b/account_payment_line/models/counterpart_line.py new file mode 100644 index 00000000000..bbf7016d876 --- /dev/null +++ b/account_payment_line/models/counterpart_line.py @@ -0,0 +1,189 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +dict_payment_type = dict( + inbound=["out_invoice", "in_refund", "out_receipt"], + outbound=["in_invoice", "out_refund", "in_receipt"], +) + + +class AccountPaymentCounterLinesAbstract(models.AbstractModel): + _name = "account.payment.counterpart.line.abstract" + _description = "Counterpart line payment Abstract" + + company_id = fields.Many2one( + comodel_name="res.company", + compute="_compute_company_fields", + default=lambda self: self.env.company, + ) + name = fields.Char(string="Description", required=True, default="/") + account_id = fields.Many2one( + "account.account", + string="Account", + required=True, + ondelete="restrict", + check_company=True, + ) + analytic_account_id = fields.Many2one( + comodel_name="account.analytic.account", + string="Analytic Account", + ondelete="restrict", + check_company=True, + ) + currency_id = fields.Many2one( + comodel_name="res.currency", + string="Currency", + compute="_compute_company_fields", + default=lambda self: self.env.company.currency_id, + ) + + fully_paid = fields.Boolean(string="Fully Paid?") + writeoff_account_id = fields.Many2one( + comodel_name="account.account", + string="Write-off account", + domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + ) + writeoff_amount = fields.Monetary( + required=False, + compute="_compute_amounts", + ) + writeoff_amount_currency = fields.Monetary( + required=False, + compute="_compute_amounts", + ) + + def _compute_company_fields(self): + for rec in self: + rec.company_id = self.env.company.id + rec.currency_id = self.env.company.currency_id.id + + amount = fields.Monetary(string="Amount", required=True) + amount_currency = fields.Monetary( + string="Amount in Company Currency", compute="_compute_amounts" + ) + aml_amount_residual = fields.Monetary( + string="Amount Residual", + compute="_compute_amounts", + ) + residual_after_payment = fields.Monetary( + compute="_compute_amounts", + ) + aml_amount_residual_currency = fields.Monetary( + string="Amount Residual Currency", + compute="_compute_amounts", + ) + residual_after_payment_currency = fields.Monetary( + compute="_compute_amounts", + ) + + def _get_onchange_fields(self): + return "aml_id.amount_residual", "amount", "fully_paid" + + @api.depends(lambda x: x._get_onchange_fields()) + def _compute_amounts(self): + for rec in self: + payment_date = ( + hasattr(rec.payment_id, "payment_date") + and rec.payment_id.payment_date + or rec.payment_id.date + ) + rec.amount_currency = rec.payment_id.currency_id._convert( + rec.amount, + rec.payment_id.company_id.currency_id, + rec.payment_id.company_id, + date=payment_date, + ) + rec.aml_amount_residual = rec.aml_id.amount_residual + rec.residual_after_payment = ( + not rec.fully_paid + and max(abs(rec.aml_id.amount_residual) - rec.amount_currency, 0) + or 0.0 + ) + rec.writeoff_amount = ( + rec.fully_paid and (rec.aml_id.amount_residual - rec.amount) or 0.0 + ) + rec.aml_amount_residual_currency = rec.aml_id.amount_residual_currency + rec.residual_after_payment_currency = ( + not rec.fully_paid + and max( + abs(rec.aml_id.amount_residual_currency) - rec.amount_currency, 0 + ) + or 0.0 + ) + rec.writeoff_amount_currency = ( + rec.fully_paid + and (rec.aml_id.amount_residual_currency - rec.amount_currency) + or 0.0 + ) + + partner_id = fields.Many2one("res.partner", string="Partner", ondelete="restrict") + commercial_partner_id = fields.Many2one(related="partner_id.commercial_partner_id") + move_id = fields.Many2one( + "account.move", string="Journal Entry", ondelete="set null" + ) + move_ids = fields.One2many( + "account.move.line", + "payment_line_id", + string="Journal Entries Created", + ) + aml_id = fields.Many2one( + "account.move.line", string="Journal Item to Reconcile", ondelete="set null" + ) + aml_date_maturity = fields.Date( + string="Date Maturity", required=False, related="aml_id.date_maturity" + ) + + @api.onchange("move_id", "aml_id") + def _onchange_move_id(self): + aml_model = self.env["account.move.line"] + for rec in self: + type_move = dict_payment_type.get(rec.payment_id.payment_type, []) + if rec.move_id and not rec.aml_id: + domain = [ + ("move_id", "=", rec.move_id.id), + ("amount_residual", "!=", 0.0), + ] + lines_ordered = aml_model.search( + domain, order="date_maturity ASC", limit=1 + ) + if lines_ordered: + rec.aml_id = lines_ordered.id + if rec.aml_id: + rec.move_id = rec.aml_id.move_id.id + rec.account_id = rec.aml_id.account_id.id + rec.amount = abs(rec.aml_id.amount_residual) + rec.partner_id = rec.aml_id.partner_id.id + if rec.move_id.move_type == "entry": + if rec.payment_id.partner_type == "supplier": + if rec.payment_id.payment_type == "outbound": + rec.amount = -rec.aml_id.amount_residual + else: + rec.amount = rec.aml_id.amount_residual + else: + if rec.payment_id.payment_type == "outbound": + rec.amount = -rec.aml_id.amount_residual + else: + rec.amount = rec.aml_id.amount_residual + else: + if ( + type_move + and rec.move_id.move_type not in type_move + and rec.amount + ): + rec.amount *= -1 + + @api.constrains("amount", "aml_amount_residual") + def constrains_amount_residual(self): + for rec in self: + if ( + rec.aml_id + and 0 < rec.aml_amount_residual_currency < rec.amount_currency + ): + raise ValidationError( + _( + "the amount exceeds the residual amount, please check the invoice %s" + ) + % (rec.aml_id.move_id.name or rec.aml_id.name), + ) diff --git a/account_payment_line/readme/CONTRIBUTORS.rst b/account_payment_line/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..9e071bc4c7d --- /dev/null +++ b/account_payment_line/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Christopher Ormaza. diff --git a/account_payment_line/readme/DESCRIPTION.rst b/account_payment_line/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..eaa373004a9 --- /dev/null +++ b/account_payment_line/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module is an utility module to add lines in payment, allowing users make +more complicated cases when processing payments, split on many invoices, +set up specific write-off and adding some analytic information + +Add tool to proposal of payment distributions, ordering by due date diff --git a/account_payment_line/readme/USAGE.rst b/account_payment_line/readme/USAGE.rst new file mode 100644 index 00000000000..f6c186260e1 --- /dev/null +++ b/account_payment_line/readme/USAGE.rst @@ -0,0 +1,6 @@ +You can use payment distribution suggestion, and if system found moves +pending to reconcile related with partner selected, system will create +all lines trying to pay all invoices until amount remain + +You can add manually lines, if payment don't detect lines specified, payment +works as a normal payment diff --git a/account_payment_line/security/ir.model.access.csv b/account_payment_line/security/ir.model.access.csv new file mode 100644 index 00000000000..1d8229944fe --- /dev/null +++ b/account_payment_line/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_payment_counterpart_line_all,access_account_payment_counterpart_line_all,model_account_payment_counterpart_line,,1,0,0,0 +access_account_payment_counterpart_line_group_account_invoice,access_account_payment_counterpart_line_group_account_invoice,model_account_payment_counterpart_line,account.group_account_invoice,1,1,1,1 +access_account_payment_counterpart_line_group_account_user,access_account_payment_counterpart_line_group_account_user,model_account_payment_counterpart_line,account.group_account_user,1,1,1,1 +access_account_payment_counterpart_line_group_account_manager,access_account_payment_counterpart_line_group_account_manager,model_account_payment_counterpart_line,account.group_account_manager,1,1,1,1 diff --git a/account_payment_line/static/description/index.html b/account_payment_line/static/description/index.html new file mode 100644 index 00000000000..4323f5eecdc --- /dev/null +++ b/account_payment_line/static/description/index.html @@ -0,0 +1,435 @@ + + + + + + +Payment Counterpart Lines + + + +
+

Payment Counterpart Lines

+ + +

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

+

This module is an utility module to add lines in payment, allowing users make +more complicated cases when processing payments, split on many invoices, +set up specific write-off and adding some analytic information

+

Add tool to proposal of payment distributions, ordering by due date

+

Table of contents

+ +
+

Usage

+

You can use payment distribution suggestion, and if system found moves +pending to reconcile related with partner selected, system will create +all lines trying to pay all invoices until amount remain

+

You can add manually lines, if payment don’t detect lines specified, payment +works as a normal payment

+
+
+

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 to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow S.L.
  • +
+
+
+

Contributors

+ +
+
+

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.

+

Current maintainer:

+

ChrisOForgeFlow

+

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

+

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

+
+
+
+ + diff --git a/account_payment_line/tests/__init__.py b/account_payment_line/tests/__init__.py new file mode 100644 index 00000000000..7a4851544fa --- /dev/null +++ b/account_payment_line/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from . import test_account_payment_line diff --git a/account_payment_line/tests/test_account_payment_line.py b/account_payment_line/tests/test_account_payment_line.py new file mode 100644 index 00000000000..5b248bc021f --- /dev/null +++ b/account_payment_line/tests/test_account_payment_line.py @@ -0,0 +1,1014 @@ +# Copyright 2022 ForgeFlow, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +import json + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests.common import Form, TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestAccountPaymentLines(TransactionCase): + def setUp(self): + super().setUp() + self.test_product = self.env["product.product"].create( + { + "name": "test_product", + "type": "service", + } + ) + self.customer = self.env["res.partner"].create( + { + "name": "test_customer", + } + ) + self.customer2 = self.env["res.partner"].create( + { + "name": "test_customer", + } + ) + self.supplier = self.env["res.partner"].create( + { + "name": "test_vendor", + } + ) + self.supplier2 = self.env["res.partner"].create( + { + "name": "test_vendor", + } + ) + self.bank_journal = self.env["account.journal"].search( + [("type", "=", "bank")], limit=1 + ) + self.account_receivable = self.env["account.account"].search( + [ + ("company_id", "=", self.env.company.id), + ("user_type_id.type", "=", "receivable"), + ], + limit=1, + ) + self.account_payable = self.env["account.account"].search( + [ + ("company_id", "=", self.env.company.id), + ("user_type_id.type", "=", "payable"), + ], + limit=1, + ) + self.account_expense = self.env["account.account"].search( + [ + ("company_id", "=", self.env.company.id), + ("user_type_id.type", "=", "other"), + ], + limit=1, + ) + self.currency_2x = self.env["res.currency"].create( + { + "name": "2X", # Foreign currency, 2 time + "symbol": "X", + "rate_ids": [ + ( + 0, + 0, + { + "name": fields.Date.today(), + "rate": self.env.company.currency_id.rate * 2, + }, + ) + ], + } + ) + self.payment_terms_split = self.env["account.payment.term"].create( + { + "name": "50% Advance End of Following Month", + "note": "Payment terms: 30% Advance End of Following Month", + "line_ids": [ + ( + 0, + 0, + { + "value": "percent", + "value_amount": 50.0, + "sequence": 400, + "days": 0, + "option": "day_after_invoice_date", + }, + ), + ( + 0, + 0, + { + "value": "balance", + "value_amount": 0.0, + "sequence": 500, + "days": 31, + "option": "day_following_month", + }, + ), + ], + } + ) + + def _create_invoice( + self, move_type, partner, amount, currency=False, payment_term=False + ): + move_form = Form( + self.env["account.move"].with_context( + default_move_type=move_type, + account_predictive_bills_disable_prediction=True, + ) + ) + if not currency: + currency = self.env.company.currency_id + move_form.invoice_date = fields.Date.today() + move_form.date = move_form.invoice_date + move_form.partner_id = partner + move_form.currency_id = currency + if payment_term: + move_form.invoice_payment_term_id = payment_term + with move_form.invoice_line_ids.new() as line_form: + line_form.product_id = self.test_product + line_form.price_unit = amount + line_form.tax_ids.clear() + move = move_form.save() + move.action_post() + return move + + def _create_refund(self, invoice, refund_method="cancel"): + ctx = {"active_model": "account.move", "active_ids": [invoice.id]} + move_reversal = ( + self.env["account.move.reversal"] + .with_context(ctx) + .create( + { + "date": fields.Date.today(), + "reason": "no reason", + "refund_method": refund_method, + } + ) + ) + reversal = move_reversal.reverse_moves() + reverse_move = self.env["account.move"].browse(reversal["res_id"]) + return reverse_move + + def _create_payment( + self, + main_partner, + total_amount, + payment_type, + partner_type, + lines=False, + currency=False, + post=False, + suggest_payment_distribution=False, + writeoff_account=False, + ): + payment_form = Form( + self.env["account.payment"].with_context( + default_journal_id=self.bank_journal.id + ) + ) + payment_form.partner_id = main_partner + payment_form.payment_type = payment_type + payment_form.partner_type = partner_type + payment_form.amount = total_amount + account = ( + partner_type == "customer" + and self.account_receivable + or partner_type == "supplier" + and self.account_payable + ) + payment_form.destination_account_id = account + if writeoff_account: + payment_form.writeoff_account_id = writeoff_account + if not currency: + currency = self.env.company.currency_id + payment_form.currency_id = currency + if not lines: + lines = [] + for line in lines: + with payment_form.line_payment_counterpart_ids.new() as line_form: + if line.get("move_id", False): + line_form.move_id = line.get("move_id", False) + if line.get("account_id", False): + line_form.account_id = line.get("account_id", False) + if line.get("amount", False): + line_form.amount = line.get("amount", False) + if line.get("fully_paid", False): + line_form.fully_paid = line.get("fully_paid", False) + if line.get("writeoff_account_id", False): + line_form.writeoff_account_id = line.get( + "writeoff_account_id", False + ) + payment = payment_form.save() + if suggest_payment_distribution: + payment.action_propose_payment_distribution() + if post: + payment.action_post() + return payment + + def test_01_customer_payment(self): + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 100.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + } + ], + post=True, + ) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual(new_payment.reconciled_invoice_ids, new_invoice) + + new_invoice2 = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + new_payment2 = self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice2, + "amount": 50.0, + } + ], + post=True, + ) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual(new_payment2.reconciled_invoice_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 50.0) + + new_payment3 = self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice2, + } + ], + post=True, + ) + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual(new_payment3.reconciled_invoice_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 0.0) + + new_payment2.action_draft() + new_payment2.action_cancel() + + self.assertEqual(new_payment2.state, "cancel") + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual(new_payment3.reconciled_invoice_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 50.0) + + def test_02_customer_refund_payment(self): + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice2 = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 200.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + }, + { + "move_id": new_invoice2, + }, + ], + post=True, + ) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual(new_payment.reconciled_invoice_ids, new_invoice + new_invoice2) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + + new_refund = self._create_refund(new_invoice, "refund") + new_refund.action_post() + self.assertEqual(new_refund.payment_state, "not_paid") + self.assertEqual(new_refund.amount_total, 100.0) + new_payment_refund = self._create_payment( + self.customer, + 100.0, + "outbound", + "customer", + [ + { + "move_id": new_refund, + }, + ], + post=True, + ) + self.assertEqual(new_payment_refund.state, "posted") + self.assertTrue(new_payment_refund.is_reconciled) + self.assertEqual(new_payment_refund.reconciled_invoice_ids, new_refund) + self.assertIn(new_refund.payment_state, ["paid", "in_payment"]) + + def test_03_supplier_payment(self): + new_invoice = self._create_invoice("in_invoice", self.supplier, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + new_payment = self._create_payment( + self.supplier, + 100.0, + "outbound", + "supplier", + [ + { + "move_id": new_invoice, + } + ], + post=True, + ) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual(new_payment.reconciled_bill_ids, new_invoice) + + new_invoice2 = self._create_invoice("in_invoice", self.supplier, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + new_payment2 = self._create_payment( + self.supplier, + 50.0, + "outbound", + "supplier", + [ + { + "move_id": new_invoice2, + "amount": 50.0, + } + ], + post=True, + ) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual(new_payment2.reconciled_bill_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 50.0) + + new_payment3 = self._create_payment( + self.supplier, + 50.0, + "outbound", + "supplier", + [ + { + "move_id": new_invoice2, + } + ], + post=True, + ) + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual(new_payment3.reconciled_bill_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 0.0) + + new_payment2.action_draft() + new_payment2.action_cancel() + + self.assertEqual(new_payment2.state, "cancel") + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual(new_payment3.reconciled_bill_ids, new_invoice2) + self.assertEqual(new_invoice2.amount_residual, 50.0) + + def test_04_supplier_refund_payment(self): + new_invoice = self._create_invoice("in_invoice", self.supplier, 100.0) + new_invoice2 = self._create_invoice("in_invoice", self.supplier, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + new_payment = self._create_payment( + self.supplier, + 200.0, + "outbound", + "supplier", + [ + { + "move_id": new_invoice, + }, + { + "move_id": new_invoice2, + }, + ], + post=True, + ) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual(new_payment.reconciled_bill_ids, new_invoice + new_invoice2) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + + new_refund = self._create_refund(new_invoice, "refund") + new_refund.action_post() + self.assertEqual(new_refund.payment_state, "not_paid") + self.assertEqual(new_refund.amount_total, 100.0) + new_payment_refund = self._create_payment( + self.supplier, + 100.0, + "inbound", + "supplier", + [ + { + "move_id": new_refund, + }, + ], + post=True, + ) + self.assertEqual(new_payment_refund.state, "posted") + self.assertTrue(new_payment_refund.is_reconciled) + self.assertEqual(new_payment_refund.reconciled_bill_ids, new_refund) + self.assertIn(new_refund.payment_state, ["paid", "in_payment"]) + + def test_05_partial_payments(self): + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice2 = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice3 = self._create_invoice( + "out_invoice", self.customer, 100.0, payment_term=self.payment_terms_split + ) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + self.assertEqual(new_invoice3.payment_state, "not_paid") + self.assertEqual(new_invoice3.amount_total, 100.0) + self.assertEqual( + len( + new_invoice3.line_ids.filtered( + lambda x: x.partner_id.id == new_invoice3.partner_id.id + and x.account_id.user_type_id.type == "receivable" + ) + ), + 2, + ) + new_payment = self._create_payment( + self.customer, + 150.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + "amount": 50.0, + }, + { + "move_id": new_invoice2, + "amount": 50.0, + }, + { + "move_id": new_invoice3, + "amount": 50.0, + }, + ], + post=True, + ) + self.assertEqual(new_invoice.payment_state, "partial") + self.assertEqual(new_invoice.amount_residual, 50.0) + self.assertEqual(new_invoice2.payment_state, "partial") + self.assertEqual(new_invoice2.amount_residual, 50.0) + self.assertEqual(new_invoice3.payment_state, "partial") + self.assertEqual(new_invoice3.amount_residual, 50.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice + new_invoice2 + new_invoice3, + ) + + new_payment2 = self._create_payment( + self.customer, + 100.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + "amount": 50.0, + }, + { + "move_id": new_invoice2, + "amount": 50.0, + }, + ], + post=True, + ) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice.amount_residual, 0.0) + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice2.amount_residual, 0.0) + self.assertEqual(new_invoice3.payment_state, "partial") + self.assertEqual(new_invoice3.amount_residual, 50.0) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual( + new_payment2.reconciled_invoice_ids, + new_invoice + new_invoice2, + ) + new_payment3 = self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice3, + "amount": 50.0, + }, + ], + post=True, + ) + self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice3.amount_residual, 0.0) + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual( + new_payment3.reconciled_invoice_ids, + new_invoice3, + ) + + def test_06_payments_without_invoices(self): + new_payment = self._create_payment( + self.customer, + 100.0, + "inbound", + "customer", + [ + { + "account_id": self.account_expense, + "amount": 50.0, + }, + { + "account_id": self.customer.property_account_receivable_id, + "amount": 50.0, + }, + ], + post=True, + ) + self.assertEqual(new_payment.state, "posted") + self.assertFalse(new_payment.is_reconciled) + self.assertFalse(bool(new_payment.reconciled_invoice_ids)) + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + payments = json.loads(new_invoice.invoice_outstanding_credits_debits_widget) + self.assertEqual(sum(p.get("amount") for p in payments.get("content")), 50) + new_invoice.js_assign_outstanding_line(payments.get("content", [])[0].get("id")) + self.assertEqual(new_invoice.payment_state, "partial") + self.assertEqual(new_invoice.amount_residual, 50.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice, + ) + + def test_07_payment_multi_currency(self): + new_invoice = self._create_invoice( + "out_invoice", self.customer, 100.0, currency=self.currency_2x + ) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + "amount": 50.0, + }, + ], + post=True, + ) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice.amount_residual, 0.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice, + ) + + new_invoice2 = self._create_invoice( + "out_invoice", self.customer, 100.0, currency=self.currency_2x + ) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + new_payment2 = self._create_payment( + self.customer, + 100.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice2, + "amount": 100.0, + }, + ], + post=True, + currency=self.currency_2x, + ) + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice2.amount_residual, 0.0) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual( + new_payment2.reconciled_invoice_ids, + new_invoice2, + ) + + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 200.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + "amount": 200.0, + }, + ], + post=True, + currency=self.currency_2x, + ) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice.amount_residual, 0.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice, + ) + + def test_08_payment_term_split(self): + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice2 = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice3 = self._create_invoice( + "out_invoice", self.customer, 100.0, payment_term=self.payment_terms_split + ) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + self.assertEqual(new_invoice3.payment_state, "not_paid") + self.assertEqual(new_invoice3.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 300.0, + "inbound", + "customer", + suggest_payment_distribution=True, + ) + self.assertEqual(len(new_payment.line_payment_counterpart_ids), 4) + self.assertEqual( + len( + new_payment.line_payment_counterpart_ids.filtered( + lambda x: x.amount == 50.0 + ) + ), + 2, + ) + self.assertEqual( + len( + new_payment.line_payment_counterpart_ids.filtered( + lambda x: x.amount == 100.0 + ) + ), + 2, + ) + new_payment.action_post() + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice.amount_residual, 0.0) + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice2.amount_residual, 0.0) + self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice3.amount_residual, 0.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice + new_invoice2 + new_invoice3, + ) + + def test_09_offset_payment(self): + new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice.payment_state, "not_paid") + self.assertEqual(new_invoice.amount_total, 100.0) + new_payment = self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice, + "amount": 50.0, + "fully_paid": True, + }, + ], + writeoff_account=self.account_expense, + post=True, + ) + self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice.amount_residual, 0.0) + self.assertEqual(new_payment.state, "posted") + self.assertTrue(new_payment.is_reconciled) + self.assertEqual( + new_payment.reconciled_invoice_ids, + new_invoice, + ) + + new_invoice2 = self._create_invoice("out_invoice", self.customer, 100.0) + new_invoice3 = self._create_invoice("out_invoice", self.customer, 100.0) + self.assertEqual(new_invoice2.payment_state, "not_paid") + self.assertEqual(new_invoice2.amount_total, 100.0) + self.assertEqual(new_invoice3.payment_state, "not_paid") + self.assertEqual(new_invoice3.amount_total, 100.0) + new_payment2 = self._create_payment( + self.customer, + 150.0, + "inbound", + "customer", + [ + { + "move_id": new_invoice2, + "amount": 50.0, + "writeoff_account_id": self.account_expense, + "fully_paid": True, + }, + { + "move_id": new_invoice3, + "amount": 100.0, + }, + ], + ) + writeoff_line = new_payment2.line_payment_counterpart_ids.filtered( + lambda x: x.move_id == new_invoice2 + ) + self.assertEqual(writeoff_line.writeoff_amount, 50.0) + self.assertEqual(writeoff_line.residual_after_payment, 0.0) + new_payment2.action_post() + self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice2.amount_residual, 0.0) + self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_invoice3.amount_residual, 0.0) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual( + new_payment2.reconciled_invoice_ids, + new_invoice2 + new_invoice3, + ) + + def test_10_payment_distribution_proposition(self): + new_out_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + new_out_refund = self._create_invoice("out_refund", self.customer, 100.0) + new_in_invoice = self._create_invoice("in_invoice", self.customer, 100.0) + new_in_refund = self._create_invoice("in_refund", self.customer, 100.0) + new_out_invoice2 = self._create_invoice("out_invoice", self.customer2, 100.0) + new_out_refund2 = self._create_invoice("out_refund", self.customer2, 100.0) + new_in_invoice2 = self._create_invoice("in_invoice", self.customer2, 100.0) + new_in_refund2 = self._create_invoice("in_refund", self.customer2, 100.0) + + payment_form = Form( + self.env["account.payment"].with_context( + default_journal_id=self.bank_journal.id + ) + ) + payment_form.partner_id = self.customer + payment_form.payment_type = "inbound" + payment_form.partner_type = "customer" + payment_form.amount = 50.0 + payment = payment_form.save() + payment.action_propose_payment_distribution() + self.assertEqual(len(payment.line_payment_counterpart_ids), 1) + self.assertEqual( + sum(payment.line_payment_counterpart_ids.mapped("amount")), 50.0 + ) + self.assertEqual( + payment.line_payment_counterpart_ids.mapped("move_id"), new_out_invoice + ) + + payment.action_delete_counterpart_lines() + self.assertEqual(len(payment.line_payment_counterpart_ids), 0) + + payment.action_propose_payment_distribution() + payment.action_post() + self.assertEqual(new_out_invoice.payment_state, "partial") + self.assertEqual(new_out_invoice.amount_residual, 50.0) + self.assertEqual(payment.state, "posted") + self.assertTrue(payment.is_reconciled) + self.assertEqual( + payment.reconciled_invoice_ids, + new_out_invoice, + ) + + new_payment2 = self._create_payment( + self.customer, + 50.0, + "outbound", + "customer", + [ + { + "move_id": new_out_invoice, + "amount": -50.0, + }, + { + "move_id": new_out_refund, + "amount": 100.0, + }, + ], + post=True, + ) + self.assertIn(new_out_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_out_invoice.amount_residual, 0.0) + self.assertIn(new_out_refund.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_out_refund.amount_residual, 0.0) + self.assertEqual(new_payment2.state, "posted") + self.assertTrue(new_payment2.is_reconciled) + self.assertEqual( + new_payment2.reconciled_invoice_ids, + new_out_invoice + new_out_refund, + ) + + payment_form = Form( + self.env["account.payment"].with_context( + default_journal_id=self.bank_journal.id + ) + ) + payment_form.partner_id = self.customer2 + payment_form.payment_type = "outbound" + payment_form.partner_type = "customer" + payment_form.amount = 100.0 + payment = payment_form.save() + payment.action_propose_payment_distribution() + self.assertEqual(len(payment.line_payment_counterpart_ids), 1) + self.assertEqual( + sum(payment.line_payment_counterpart_ids.mapped("amount")), 100.0 + ) + self.assertEqual( + payment.line_payment_counterpart_ids.mapped("move_id"), new_out_refund2 + ) + + payment.action_delete_counterpart_lines() + self.assertEqual(len(payment.line_payment_counterpart_ids), 0) + payment.write( + { + "payment_type": "outbound", + "partner_type": "supplier", + } + ) + payment.action_propose_payment_distribution() + self.assertEqual(len(payment.line_payment_counterpart_ids), 1) + self.assertEqual( + sum(payment.line_payment_counterpart_ids.mapped("amount")), 100.0 + ) + self.assertEqual( + payment.line_payment_counterpart_ids.mapped("move_id"), new_in_invoice2 + ) + + payment.action_delete_counterpart_lines() + self.assertEqual(len(payment.line_payment_counterpart_ids), 0) + payment.write( + { + "payment_type": "inbound", + "partner_type": "supplier", + } + ) + payment.action_propose_payment_distribution() + self.assertEqual(len(payment.line_payment_counterpart_ids), 1) + self.assertEqual( + sum(payment.line_payment_counterpart_ids.mapped("amount")), 100.0 + ) + self.assertEqual( + payment.line_payment_counterpart_ids.mapped("move_id"), new_in_refund2 + ) + payment.unlink() + + new_payment3 = self._create_payment( + self.customer, + 0.0, + "outbound", + "supplier", + [ + { + "move_id": new_in_refund, + "amount": -100.0, + }, + { + "move_id": new_in_invoice, + "amount": 100.0, + }, + ], + post=True, + ) + self.assertIn(new_in_refund.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_in_refund.amount_residual, 0.0) + self.assertIn(new_in_invoice.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_in_invoice.amount_residual, 0.0) + self.assertEqual(new_payment3.state, "posted") + self.assertTrue(new_payment3.is_reconciled) + self.assertEqual( + new_payment3.reconciled_bill_ids, + new_in_refund + new_in_invoice, + ) + + new_payment4 = self._create_payment( + self.customer2, + 0.0, + "inbound", + "customer", + [ + { + "move_id": new_out_refund2, + "amount": -100.0, + }, + { + "move_id": new_out_invoice2, + "amount": 100.0, + }, + { + "move_id": new_in_refund2, + "amount": 100.0, + }, + { + "move_id": new_in_invoice2, + "amount": -100.0, + }, + ], + post=True, + ) + self.assertIn(new_out_refund2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_out_refund2.amount_residual, 0.0) + self.assertIn(new_out_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_out_invoice2.amount_residual, 0.0) + self.assertIn(new_in_refund2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_in_refund2.amount_residual, 0.0) + self.assertIn(new_in_invoice2.payment_state, ["paid", "in_payment"]) + self.assertEqual(new_in_invoice2.amount_residual, 0.0) + self.assertEqual(new_payment4.state, "posted") + self.assertTrue(new_payment4.is_reconciled) + self.assertEqual( + new_payment4.reconciled_invoice_ids, + new_out_invoice2 + new_out_refund2, + ) + self.assertEqual( + new_payment4.reconciled_bill_ids, + new_in_refund2 + new_in_invoice2, + ) + + def test_11_exceptions(self): + new_out_invoice = self._create_invoice("out_invoice", self.customer, 100.0) + + # Should select a writeoff_account + with self.assertRaises(ValidationError): + self._create_payment( + self.customer, + 50.0, + "inbound", + "customer", + [ + { + "move_id": new_out_invoice, + "amount": 50.0, + "fully_paid": True, + }, + ], + post=True, + ) + + # Should input lower or equal amount than invoice selected in line + with self.assertRaises(ValidationError): + self._create_payment( + self.customer, + 150.0, + "inbound", + "customer", + [ + { + "move_id": new_out_invoice, + "amount": 150.0, + }, + ], + post=True, + ) diff --git a/account_payment_line/views/account_payment_views.xml b/account_payment_line/views/account_payment_views.xml new file mode 100644 index 00000000000..3f04e3982df --- /dev/null +++ b/account_payment_line/views/account_payment_views.xml @@ -0,0 +1,147 @@ + + + + + view.account.payment.line.tree + account.payment.counterpart.line + + + + + + + + + + + + + + + + + + + + + + + + + + + + view.inherit.account.payment.form + account.payment + + + + - \ No newline at end of file + From 8ce61b52922fb1084057db0897b9a00a470e0024 Mon Sep 17 00:00:00 2001 From: Jordi Ballester Alomar Date: Mon, 20 May 2019 13:59:00 +0200 Subject: [PATCH 022/118] - adds fixes to the logic to reconcile the amount maid - adds tests to the python side --- .../models/account_move_line.py | 33 +- .../tests/__init__.py | 1 + .../test_account_payment_widget_amount.py | 375 ++++++++++++++++++ 3 files changed, 395 insertions(+), 14 deletions(-) create mode 100644 account_payment_widget_amount/tests/__init__.py create mode 100644 account_payment_widget_amount/tests/test_account_payment_widget_amount.py diff --git a/account_payment_widget_amount/models/account_move_line.py b/account_payment_widget_amount/models/account_move_line.py index 9c6c3402d4a..c85912c6710 100644 --- a/account_payment_widget_amount/models/account_move_line.py +++ b/account_payment_widget_amount/models/account_move_line.py @@ -1,6 +1,6 @@ # Copyright 2018 Eficent Business and IT Consulting Services S.L. -from odoo import api, models, _ +from odoo import api, fields, models, _ from odoo.exceptions import UserError @@ -15,9 +15,12 @@ def update_amount_reconcile( super(AccountMoveLine, self).update_amount_reconcile( temp_amount_residual, temp_amount_residual_currency, amount_reconcile, credit_move, debit_move) - # Check if amount is positive - paid_amt = self.env.context.get('paid_amount', False) + paid_amt = self.env.context.get('paid_amount', 0.0) + if not paid_amt: + return temp_amount_residual, temp_amount_residual_currency, \ + amount_reconcile + paid_amt = float(paid_amt) if paid_amt < 0: raise UserError(_( "The specified amount has to be strictly positive")) @@ -26,24 +29,26 @@ def update_amount_reconcile( # be wrong below # Compute paid_amount currency - paid_amt_currency = paid_amt and min( - float(paid_amt), -credit_move.amount_residual_currency) or \ - -credit_move.amount_residual_currency + if debit_move.amount_residual_currency or \ + credit_move.amount_residual_currency: - temp_amount_residual_currency = min( - debit_move.amount_residual_currency, paid_amt_currency) + temp_amount_residual_currency = min( + debit_move.amount_residual_currency, + -credit_move.amount_residual_currency, + paid_amt) + else: + temp_amount_residual_currency = 0.0 # If previous value is not 0 we compute paid amount in the company # currency taking into account the rate if temp_amount_residual_currency: - paid_amt = paid_amt * credit_move.company_currency_id.rate - paid_amt = paid_amt and min( - float(paid_amt), -credit_move.amount_residual_currency) or \ - -credit_move.amount_residual - + paid_amt = debit_move.currency_id._convert( + paid_amt, debit_move.company_id.currency_id, + debit_move.company_id, + credit_move.date or fields.Date.today()) temp_amount_residual = min(debit_move.amount_residual, + -credit_move.amount_residual, paid_amt) - amount_reconcile = temp_amount_residual_currency or \ temp_amount_residual diff --git a/account_payment_widget_amount/tests/__init__.py b/account_payment_widget_amount/tests/__init__.py new file mode 100644 index 00000000000..c08d604ae8d --- /dev/null +++ b/account_payment_widget_amount/tests/__init__.py @@ -0,0 +1 @@ +from . import test_account_payment_widget_amount diff --git a/account_payment_widget_amount/tests/test_account_payment_widget_amount.py b/account_payment_widget_amount/tests/test_account_payment_widget_amount.py new file mode 100644 index 00000000000..4d5df4a8fb2 --- /dev/null +++ b/account_payment_widget_amount/tests/test_account_payment_widget_amount.py @@ -0,0 +1,375 @@ +# Copyright 2017 Eficent Business and IT Consulting Services, S.L. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase +import time + + +class TestAccountPaymentWidgetAmount(TransactionCase): + + def setUp(self): + super(TestAccountPaymentWidgetAmount, self).setUp() + # Models + self.partner = self.env['res.partner'].create({'name': 'Test'}) + self.account_account_type_model = self.env['account.account.type'] + self.account_account_model = self.env['account.account'] + self.account_payment_model = self.env['account.payment'] + self.account_journal_model = self.env['account.journal'] + self.account_invoice_model = self.env['account.invoice'] + self.account_move_line_model = self.env['account.move.line'] + self.invoice_line_model = self.env['account.invoice.line'] + # Records + self.company = self.env.ref('base.main_company') + + self.account_type_bank = self.account_account_type_model.create({ + 'name': 'Test Bank', + 'type': 'liquidity', + }) + self.account_type_receivable = self.account_account_type_model.create({ + 'name': 'Test Receivable', + 'type': 'receivable', + }) + self.account_type_payable = self.account_account_type_model.create({ + 'name': 'Test Payable', + 'type': 'receivable', + }) + self.account_type_regular = self.account_account_type_model.create({ + 'name': 'Test Regular', + 'type': 'other', + }) + self.account_bank = self.account_account_model.create({ + 'name': 'Test Bank', + 'code': 'TEST_BANK', + 'user_type_id': self.account_type_bank.id, + 'reconcile': False, + 'company_id': self.company.id, + }) + self.account_receivable = self.account_account_model.create({ + 'name': 'Test Receivable', + 'code': 'TEST_AR', + 'user_type_id': self.account_type_receivable.id, + 'reconcile': True, + 'company_id': self.company.id, + }) + self.account_payable = self.account_account_model.create({ + 'name': 'Test Payable', + 'code': 'TEST_AP', + 'user_type_id': self.account_type_payable.id, + 'reconcile': True, + 'company_id': self.company.id, + }) + self.partner.property_account_receivable_id = self.account_receivable + self.partner.property_account_payable_id = self.account_payable + self.account_income = self.account_account_model.create({ + 'name': 'Test Income', + 'code': 'TEST_IN', + 'user_type_id': self.account_type_regular.id, + 'reconcile': False, + 'company_id': self.company.id, + }) + self.account_expense = self.account_account_model.create({ + 'name': 'Test Expense', + 'code': 'TEST_EX', + 'user_type_id': self.account_type_regular.id, + 'reconcile': False, + 'company_id': self.company.id, + }) + self.bank_journal = self.account_journal_model.create({ + 'name': 'Test Bank', + 'code': 'TBK', + 'type': 'bank', + 'company_id': self.company.id, + }) + self.sale_journal = self.account_journal_model.\ + search([('type', '=', 'sale'), + ('company_id', '=', self.company.id)])[0] + self.purchase_journal = self.account_journal_model. \ + search([('type', '=', 'purchase'), + ('company_id', '=', self.company.id)])[0] + # Create a new currency. It is 2x the valuation + # of the company currency. + self.new_usd = self.env['res.currency'].create({ + 'name': 'us2', + 'symbol': '$²', + 'rate_ids': [(0, 0, {'rate': 2, + 'name': time.strftime('%Y-%m-%d')})], + }) + + def test_01(self): + """ Tests that I can create an invoice in company currency, + register a payment in company currency, and then reconcile part + of the payment to the invoice. + I expect: + - The residual amount of the invoice is reduced by the amount assigned. + - The residual amount of the payment is reduced by the amount assigned. + """ + invoice = self.account_invoice_model.create({ + 'name': "Test Customer Invoice", + 'journal_id': self.sale_journal.id, + 'partner_id': self.partner.id, + 'account_id': self.account_receivable.id, + 'company_id': self.company.id, + 'currency_id': self.company.currency_id.id, + }) + + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': self.account_income.id, + 'quantity': 1, + 'company_id': self.company.id, + }) + # Open invoice + invoice.action_invoice_open() + # Create a payment + payment = self.account_payment_model.create( + {'payment_type': 'inbound', + 'payment_method_id': + self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner.id, + 'amount': 1000.0, + 'currency_id': self.company.currency_id.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'journal_id': self.bank_journal.id, + 'company_id': self.company.id, + }) + payment.post() + payment_ml = payment.move_line_ids.filtered( + lambda l: l.account_id == self.account_receivable) + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual, 100.0) + self.assertFalse(payment.move_reconciled) + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual, 0.0) + self.assertEqual(invoice.state, 'paid') + self.assertFalse(payment.move_reconciled) + + def test_02(self): + """ Tests that I can create an invoice in foreign currency, + register a payment in company currency, and then reconcile part + of the payment to the invoice. + I expect: + - The residual amount of the invoice is reduced by the amount assigned. + - The residual amount of the payment is reduced by the amount assigned. + """ + self.company.currency_id.rate_ids = False + # The invoice is for 200 in the new currency, which translates in 100 + # in company currency. + invoice = self.account_invoice_model.create({ + 'name': "Test Customer Invoice", + 'journal_id': self.sale_journal.id, + 'partner_id': self.partner.id, + 'account_id': self.account_receivable.id, + 'company_id': self.company.id, + 'currency_id': self.new_usd.id, + }) + + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': self.account_income.id, + 'quantity': 1, + 'company_id': self.company.id, + }) + # Open invoice + invoice.action_invoice_open() + # Create a payment + payment = self.account_payment_model.create( + {'payment_type': 'inbound', + 'payment_method_id': + self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner.id, + 'amount': 1000.0, + 'currency_id': self.company.currency_id.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'journal_id': self.bank_journal.id, + 'company_id': self.company.id, + }) + payment.post() + payment_ml = payment.move_line_ids.filtered( + lambda l: l.account_id == self.account_receivable) + # We pay 100 in the currency of the invoice. Which means that in + # company currency we are paying 50. + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual_signed, 100.0) + self.assertEqual(invoice.state, 'open') + self.assertFalse(payment.move_reconciled) + self.assertEqual(payment_ml.amount_residual, -950.0) + + def test_03(self): + """ Tests that I can create an refund invoice in foreign currency, + register an outgoing payment in company currency, and then + reconcile part of the payment to the invoice. + I expect: + - The residual amount of the invoice is reduced by the amount assigned. + - The residual amount of the payment is reduced by the amount assigned. + """ + self.company.currency_id.rate_ids = False + # The invoice is for 200 in the new currency, which translates in 100 + # in company currency. + invoice = self.account_invoice_model.create({ + 'name': "Test Customer Invoice", + 'type': "out_refund", + 'journal_id': self.sale_journal.id, + 'partner_id': self.partner.id, + 'account_id': self.account_receivable.id, + 'company_id': self.company.id, + 'currency_id': self.new_usd.id, + }) + + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': self.account_income.id, + 'quantity': 1, + 'company_id': self.company.id, + }) + # Open invoice + invoice.action_invoice_open() + # Create a payment + payment = self.account_payment_model.create( + {'payment_type': 'outbound', + 'payment_method_id': + self.env.ref('account.account_payment_method_manual_out').id, + 'partner_type': 'customer', + 'partner_id': self.partner.id, + 'amount': 1000.0, + 'currency_id': self.company.currency_id.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'journal_id': self.bank_journal.id, + 'company_id': self.company.id, + }) + payment.post() + payment_ml = payment.move_line_ids.filtered( + lambda l: l.account_id == self.account_receivable) + # We collect 100 in the currency of the refund. Which means that in + # company currency we are reconciling 50. + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual_signed, -100.0) + self.assertEqual(invoice.state, 'open') + self.assertFalse(payment.move_reconciled) + self.assertEqual(payment_ml.amount_residual, 950.0) + + def test_04(self): + """ Tests that I can create an invoice in company currency, + register a payment in foreign currency, and then reconcile part + of the payment to the invoice. + I expect: + - The residual amount of the invoice is reduced by the amount assigned. + - The residual amount of the payment is reduced by the amount assigned. + """ + self.company.currency_id.rate_ids = False + invoice = self.account_invoice_model.create({ + 'name': "Test Customer Invoice", + 'journal_id': self.sale_journal.id, + 'partner_id': self.partner.id, + 'account_id': self.account_receivable.id, + 'company_id': self.company.id, + 'currency_id': self.company.currency_id.id, + }) + + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': self.account_income.id, + 'quantity': 1, + 'company_id': self.company.id, + }) + # Open invoice + invoice.action_invoice_open() + # Create a payment for 1000 of foreign currency, which translates + # to 500 in company currency. + payment = self.account_payment_model.create( + {'payment_type': 'inbound', + 'payment_method_id': + self.env.ref('account.account_payment_method_manual_in').id, + 'partner_type': 'customer', + 'partner_id': self.partner.id, + 'amount': 1000.0, + 'currency_id': self.new_usd.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'journal_id': self.bank_journal.id, + 'company_id': self.company.id, + }) + payment.post() + payment_ml = payment.move_line_ids.filtered( + lambda l: l.account_id == self.account_receivable) + # We pay 100 in the currency of the invoice, which is the + # company currency + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual_signed, 100.0) + self.assertEqual(invoice.state, 'open') + self.assertFalse(payment.move_reconciled) + self.assertEqual(payment_ml.amount_residual, -400.0) + self.assertEqual(payment_ml.amount_residual_currency, -800.0) + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual_signed, 0.0) + self.assertEqual(invoice.state, 'paid') + self.assertEqual(payment_ml.amount_residual, -300.0) + self.assertEqual(payment_ml.amount_residual_currency, -600.0) + + def test_05(self): + """ Tests that I can create a vendor bill in company currency, + register a payment in company currency, and then reconcile part + of the payment to the bill. + I expect: + - The residual amount of the invoice is reduced by the amount assigned. + - The residual amount of the payment is reduced by the amount assigned. + """ + invoice = self.account_invoice_model.create({ + 'name': "Test Vendor Bill", + 'type': 'in_invoice', + 'journal_id': self.purchase_journal.id, + 'partner_id': self.partner.id, + 'account_id': self.account_payable.id, + 'company_id': self.company.id, + 'currency_id': self.company.currency_id.id, + }) + + self.invoice_line_model.create({ + 'invoice_id': invoice.id, + 'name': 'Line 1', + 'price_unit': 200.0, + 'account_id': self.account_expense.id, + 'quantity': 1, + 'company_id': self.company.id, + }) + # Open invoice + invoice.action_invoice_open() + # Create a payment + payment = self.account_payment_model.create( + {'payment_type': 'outbound', + 'payment_method_id': + self.env.ref('account.account_payment_method_manual_out').id, + 'partner_type': 'supplier', + 'partner_id': self.partner.id, + 'amount': 1000.0, + 'currency_id': self.company.currency_id.id, + 'payment_date': time.strftime('%Y-%m-%d'), + 'journal_id': self.bank_journal.id, + 'company_id': self.company.id, + }) + payment.post() + payment_ml = payment.move_line_ids.filtered( + lambda l: l.account_id == self.account_payable) + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual, 100.0) + self.assertFalse(payment.move_reconciled) + invoice.with_context(paid_amount=100.0).assign_outstanding_credit( + payment_ml.id) + self.assertEqual(invoice.residual, 0.0) + self.assertEqual(invoice.state, 'paid') + self.assertFalse(payment.move_reconciled) From 8bded230019b5a52301f5f21e5f1a4f45af31228 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Mon, 20 May 2019 13:10:31 +0000 Subject: [PATCH 023/118] [UPD] Update account_payment_widget_amount.pot --- .../i18n/account_payment_widget_amount.pot | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 account_payment_widget_amount/i18n/account_payment_widget_amount.pot diff --git a/account_payment_widget_amount/i18n/account_payment_widget_amount.pot b/account_payment_widget_amount/i18n/account_payment_widget_amount.pot new file mode 100644 index 00000000000..9cefc992670 --- /dev/null +++ b/account_payment_widget_amount/i18n/account_payment_widget_amount.pot @@ -0,0 +1,40 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_payment_widget_amount +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.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_widget_amount +#. openerp-web +#: code:addons/account_payment_widget_amount/static/src/xml/account_payment.xml:19 +#, python-format +msgid "Apply" +msgstr "" + +#. module: account_payment_widget_amount +#: model:ir.model,name:account_payment_widget_amount.model_account_move_line +msgid "Journal Item" +msgstr "" + +#. module: account_payment_widget_amount +#. openerp-web +#: code:addons/account_payment_widget_amount/static/src/xml/account_payment.xml:9 +#, python-format +msgid "Paid Amount:" +msgstr "" + +#. module: account_payment_widget_amount +#: code:addons/account_payment_widget_amount/models/account_move_line.py:25 +#, python-format +msgid "The specified amount has to be strictly positive" +msgstr "" + From 05ace0b2d2125108f8b95cc354ac1e57df8dd57d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jul 2019 02:38:41 +0000 Subject: [PATCH 024/118] [UPD] README.rst --- account_payment_widget_amount/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/account_payment_widget_amount/static/description/index.html b/account_payment_widget_amount/static/description/index.html index 1c66d668a20..4ac27285a94 100644 --- a/account_payment_widget_amount/static/description/index.html +++ b/account_payment_widget_amount/static/description/index.html @@ -3,7 +3,7 @@ - + Account Payment Widget Amount -
-

Account Payment Widget Amount

+
+ + +Odoo Community Association + +
+

Account Payment Widget Amount

-

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

This module adds a popover widget in the invoice form view when there are outstanding payments that allows the user to select the amount of the selected payment that wants to assign to the invoice.

@@ -387,7 +393,7 @@

Account Payment Widget Amount

-

Usage

+

Usage

To use this module, you need to:

  1. Go to ‘Invoicing > Vendors > Bills’ or to ‘Invoicing > Customers > @@ -399,7 +405,7 @@

    Usage

    Payment Widget Amount Reconciled

-

Bug Tracker

+

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 to smash it by providing a detailed and welcomed @@ -407,16 +413,16 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • ForgeFlow S.L.
  • OERP Canada
-

Contributors

+

Contributors

-

Maintainers

+

Maintainers

This module is maintained by the OCA.

-Odoo Community Association + +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.

@@ -437,5 +445,6 @@

Maintainers

+
diff --git a/setup/_metapackage/pyproject.toml b/setup/_metapackage/pyproject.toml index 9389098d398..8e604612e5b 100644 --- a/setup/_metapackage/pyproject.toml +++ b/setup/_metapackage/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "odoo-addons-oca-account-payment" -version = "18.0.20250905.0" +version = "18.0.20251023.0" dependencies = [ "odoo-addon-account_check_printing_report_base==18.0.*", "odoo-addon-account_due_list==18.0.*", @@ -14,6 +14,7 @@ dependencies = [ "odoo-addon-account_payment_show_invoice==18.0.*", "odoo-addon-account_payment_term_extension==18.0.*", "odoo-addon-account_payment_tier_validation==18.0.*", + "odoo-addon-account_payment_widget_amount==18.0.*", "odoo-addon-account_refund_early_payment_discount==18.0.*", "odoo-addon-account_voucher_killer==18.0.*", ] From 1193fbededd729c603a8e2d84669e2b950e5fbce Mon Sep 17 00:00:00 2001 From: Carlos Dauden Date: Tue, 26 Aug 2025 16:38:59 +0200 Subject: [PATCH 050/118] [FIX] account_payment_return: Missed reconciled aml without payment related TT57767 --- account_payment_return/models/account_move.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/account_payment_return/models/account_move.py b/account_payment_return/models/account_move.py index f5254260ca5..d587a669b11 100644 --- a/account_payment_return/models/account_move.py +++ b/account_payment_return/models/account_move.py @@ -67,14 +67,12 @@ def _compute_payments_widget_reconciled_info(self): "move_id": move.id, "title": _("Returned on"), } - reconciled_payments = move._get_reconciled_payments() domain = [("origin_returned_move_ids.move_id", "=", move.id)] - if len(reconciled_payments) > 0: - for rec_payment in reconciled_payments: - vals_rec_payment = self.prepare_values_returned_widget( - rec_payment, rec_payment.amount - ) - values_returned.append(vals_rec_payment) + for reconciled_aml in move._get_reconciled_amls(): + vals_rec_payment = self.prepare_values_returned_widget( + reconciled_aml, -reconciled_aml.balance + ) + values_returned.append(vals_rec_payment) move_reconciles = self.env["account.partial.reconcile"].search(domain) for move_reconcile in move_reconciles: payment_ret = move_reconcile.debit_move_id @@ -88,6 +86,10 @@ def _compute_payments_widget_reconciled_info(self): ) values_returned.append(vals_reconcile) if payments_widget_vals["content"]: + payments_widget_vals["content"] = sorted( + payments_widget_vals["content"], + key=lambda x: (x["date"], x["partial_id"]), + ) move.invoice_payments_widget = payments_widget_vals else: move.invoice_payments_widget = False From 8285c598a4cf19513d58af6d41ac47d7dae65a10 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 23 Oct 2025 10:41:28 +0000 Subject: [PATCH 051/118] [BOT] post-merge updates --- README.md | 2 +- account_payment_return/README.rst | 2 +- account_payment_return/__manifest__.py | 2 +- account_payment_return/static/description/index.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c6e27bb7202..bb7bd886c36 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ addon | version | maintainers | summary [account_payment_method_base](account_payment_method_base/) | 18.0.1.0.0 | alexis-via | Add form and list view for account.payment.method [account_payment_notification](account_payment_notification/) | 18.0.1.0.1 | yajo rafaelbn | Notifiy upcoming payments [account_payment_promissory_note](account_payment_promissory_note/) | 18.0.1.0.1 | | Account Payment Promissory Note -[account_payment_return](account_payment_return/) | 18.0.1.0.2 | | Manage the return of your payments +[account_payment_return](account_payment_return/) | 18.0.1.0.3 | | Manage the return of your payments [account_payment_return_import](account_payment_return_import/) | 18.0.1.0.1 | | This module adds a generic wizard to import payment returnfile formats. Is only the base to be extended by anothermodules [account_payment_return_import_iso20022](account_payment_return_import_iso20022/) | 18.0.1.1.1 | | This addon allows to import payment returns from ISO 20022 files like PAIN or CAMT. [account_payment_show_invoice](account_payment_show_invoice/) | 18.0.1.0.0 | | Extends the tree view of payments to show the paid invoices related to the payments using the vendor reference by default diff --git a/account_payment_return/README.rst b/account_payment_return/README.rst index 11866ccadcb..fad7b4dde3e 100644 --- a/account_payment_return/README.rst +++ b/account_payment_return/README.rst @@ -11,7 +11,7 @@ Account Payment Returns !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:13030f6554430f279c56b52d3550cff4641eae60a260c65e683e41335157785a + !! source digest: sha256:b0b4ecc4389035b918a0cf2d22202d7f65a4a27f7fd2bbf8ff7517c3279ed206 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Mature-brightgreen.png diff --git a/account_payment_return/__manifest__.py b/account_payment_return/__manifest__.py index b687d69a095..0fcc50e2f82 100644 --- a/account_payment_return/__manifest__.py +++ b/account_payment_return/__manifest__.py @@ -11,7 +11,7 @@ { "name": "Account Payment Returns", - "version": "18.0.1.0.2", + "version": "18.0.1.0.3", "summary": "Manage the return of your payments", "license": "AGPL-3", "depends": ["mail", "account"], diff --git a/account_payment_return/static/description/index.html b/account_payment_return/static/description/index.html index 79bf9f647b3..30ac1f04b91 100644 --- a/account_payment_return/static/description/index.html +++ b/account_payment_return/static/description/index.html @@ -372,7 +372,7 @@

Account Payment Returns

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:13030f6554430f279c56b52d3550cff4641eae60a260c65e683e41335157785a +!! source digest: sha256:b0b4ecc4389035b918a0cf2d22202d7f65a4a27f7fd2bbf8ff7517c3279ed206 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Mature License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

This module implements customer receivables returns and allows to send From 7b5c901adcd0126749efd602ab2d395a4911ada2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Tue, 18 Feb 2025 15:31:00 +0700 Subject: [PATCH 052/118] MIG] account_payment_line: Migration to 18.0 --- account_payment_line/README.rst | 1 + account_payment_line/__init__.py | 1 - account_payment_line/__manifest__.py | 5 +- account_payment_line/hooks.py | 91 --------- .../models/account_payment.py | 189 ++++++++++-------- .../models/counterpart_line.py | 51 +++-- account_payment_line/readme/CONTRIBUTORS.md | 1 + .../security/ir.model.access.csv | 2 +- .../static/description/index.html | 1 + .../tests/test_account_payment_line.py | 177 ++++++---------- .../views/account_payment_views.xml | 38 ++-- 11 files changed, 225 insertions(+), 332 deletions(-) delete mode 100644 account_payment_line/hooks.py diff --git a/account_payment_line/README.rst b/account_payment_line/README.rst index 01e135edebf..f1f2c7028ef 100644 --- a/account_payment_line/README.rst +++ b/account_payment_line/README.rst @@ -71,6 +71,7 @@ Contributors ------------ - Christopher Ormaza. +- Do Anh Duy Maintainers ----------- diff --git a/account_payment_line/__init__.py b/account_payment_line/__init__.py index e2b838b2208..0ccfa881b02 100644 --- a/account_payment_line/__init__.py +++ b/account_payment_line/__init__.py @@ -2,4 +2,3 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) from . import models -from .hooks import post_load_hook diff --git a/account_payment_line/__manifest__.py b/account_payment_line/__manifest__.py index 1a0e2a803a9..35400314550 100644 --- a/account_payment_line/__manifest__.py +++ b/account_payment_line/__manifest__.py @@ -7,15 +7,14 @@ "author": "ForgeFlow S.L., Odoo Community Association (OCA)", "website": "https://github.com/OCA/account-payment", "category": "Account", - "version": "16.0.1.0.1", + "version": "18.0.1.0.0", "license": "AGPL-3", - "depends": ["account"], + "depends": ["account_payment"], "data": [ "security/ir.model.access.csv", "views/account_payment_views.xml", ], "maintainers": ["ChrisOForgeFlow"], "installable": True, - "post_load": "post_load_hook", "auto_install": False, } diff --git a/account_payment_line/hooks.py b/account_payment_line/hooks.py deleted file mode 100644 index 3ddc3de47b8..00000000000 --- a/account_payment_line/hooks.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright 2022 ForgeFlow, S.L. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) - -from odoo import _ -from odoo.exceptions import UserError - -from odoo.addons.account.models.account_payment import ( - AccountPayment as AccountPaymentClass, -) - - -def post_load_hook(): # noqa: C901 - def _synchronize_from_moves_new(self, changed_fields): - if "line_payment_counterpart_ids" not in self._fields: - return self._synchronize_from_moves_original(changed_fields) - if self._context.get("skip_account_move_synchronization"): - return - - for pay in self.with_context(skip_account_move_synchronization=True): - if pay.move_id.statement_line_id: - continue - - move = pay.move_id - move_vals_to_write = {} - payment_vals_to_write = {} - - if "journal_id" in changed_fields: - if pay.journal_id.type not in ("bank", "cash"): - raise UserError( - _("A payment must always belongs to a bank or cash journal.") - ) - - if "line_ids" in changed_fields: - all_lines = move.line_ids - ( - liquidity_lines, - counterpart_lines, - writeoff_lines, - ) = pay._seek_for_lines() - - if any( - line.currency_id != all_lines[0].currency_id for line in all_lines - ): - raise UserError( - _( - "Journal Entry %s is not valid. " - "In order to proceed, " - "the journal items must " - "share the same currency.", - move.display_name, - ) - ) - - if "asset_receivable" in counterpart_lines.mapped( - "account_id.account_type" - ): - partner_type = "customer" - else: - partner_type = "supplier" - - liquidity_amount = liquidity_lines.amount_currency - - move_vals_to_write.update( - { - "currency_id": liquidity_lines.currency_id.id, - "partner_id": liquidity_lines.partner_id.id, - } - ) - destination_account_id = counterpart_lines.mapped("account_id")[0].id - payment_vals_to_write.update( - { - "amount": abs(liquidity_amount), - "partner_type": partner_type, - "currency_id": liquidity_lines.currency_id.id, - "destination_account_id": destination_account_id, - "partner_id": liquidity_lines.partner_id.id, - } - ) - if liquidity_amount > 0.0: - payment_vals_to_write.update({"payment_type": "inbound"}) - elif liquidity_amount < 0.0: - payment_vals_to_write.update({"payment_type": "outbound"}) - - move.write(move._cleanup_write_orm_values(move, move_vals_to_write)) - pay.write(move._cleanup_write_orm_values(pay, payment_vals_to_write)) - - if not hasattr(AccountPaymentClass, "_synchronize_from_moves_original"): - AccountPaymentClass._synchronize_from_moves_original = ( - AccountPaymentClass._synchronize_from_moves - ) - AccountPaymentClass._synchronize_from_moves = _synchronize_from_moves_new diff --git a/account_payment_line/models/account_payment.py b/account_payment_line/models/account_payment.py index 13388b99327..e57ddca8e92 100644 --- a/account_payment_line/models/account_payment.py +++ b/account_payment_line/models/account_payment.py @@ -1,9 +1,8 @@ # Copyright 2022 ForgeFlow, S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import _, fields, models +from odoo import Command, fields, models from odoo.exceptions import ValidationError -from odoo.tools import float_is_zero class AccountPayment(models.Model): @@ -13,26 +12,23 @@ class AccountPayment(models.Model): "account.payment.counterpart.line", "payment_id", string="Counterpart Lines", - readonly=True, - states={"draft": [("readonly", False)]}, help="Use these lines to add matching lines, for example in a credit" "card payment, financing interest or commission is added", ) writeoff_account_id = fields.Many2one( "account.account", string="Write-off Account", - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + domain="[('deprecated', '=', False), ('company_ids', '=', company_id)]", ) def _process_post_reconcile(self): - for rec in self: - for line in rec.line_payment_counterpart_ids: - if line.aml_id: - to_reconcile = (line.aml_id + line.move_ids).filtered( - lambda x: not x.reconciled and x.account_id.reconcile - ) - if to_reconcile: - to_reconcile.reconcile() + for rec in self.filtered("line_payment_counterpart_ids"): + for line in rec.line_payment_counterpart_ids.filtered("aml_id"): + to_reconcile = (line.aml_id + line.move_ids).filtered( + lambda x: not x.reconciled and x.account_id.reconcile + ) + if to_reconcile: + to_reconcile.reconcile() return True def _get_moves_domain(self): @@ -46,16 +42,17 @@ def _get_moves_domain(self): self.partner_id.commercial_partner_id.id, ), ] - if self.partner_type == "supplier": - if self.payment_type == "outbound": - domain.append(("move_type", "in", ("in_invoice", "in_receipt"))) - if self.payment_type == "inbound": - domain.append(("move_type", "=", "in_refund")) - elif self.partner_type == "customer": - if self.payment_type == "outbound": - domain.append(("move_type", "=", "out_refund")) - if self.payment_type == "inbound": - domain.append(("move_type", "in", ("out_invoice", "out_receipt"))) + move_type_map = { + ("supplier", "outbound"): [ + ("move_type", "in", ("in_invoice", "in_receipt")) + ], + ("supplier", "inbound"): [("move_type", "=", "in_refund")], + ("customer", "outbound"): [("move_type", "=", "out_refund")], + ("customer", "inbound"): [ + ("move_type", "in", ("out_invoice", "out_receipt")) + ], + } + domain.extend(move_type_map.get((self.partner_type, self.payment_type), [])) return domain def _filter_amls(self, amls): @@ -82,12 +79,11 @@ def _hook_create_new_line(self, invoice, aml, amount_to_apply): ) def action_propose_payment_distribution(self): - move_model = self.env["account.move"] for rec in self: - if self.is_internal_transfer: - continue - domain = self._get_moves_domain() - pending_invoices = move_model.search(domain, order="invoice_date_due ASC") + domain = rec._get_moves_domain() + pending_invoices = rec.env["account.move"].search( + domain, order="invoice_date_due ASC" + ) pending_amount = rec.amount rec.line_payment_counterpart_ids.unlink() for invoice in pending_invoices: @@ -108,46 +104,70 @@ def action_propose_payment_distribution(self): def action_delete_counterpart_lines(self): if self.line_payment_counterpart_ids and self.state == "draft": - self.line_payment_counterpart_ids = [(5, 0, 0)] + self.line_payment_counterpart_ids = [Command.clear()] + + def _prepare_move_line_default_vals( + self, write_off_line_vals=None, force_balance=None + ): + new_write_off_line_vals = self._prepare_move_line_counterpart_vals( + write_off_line_vals + ) + if write_off_line_vals: + new_write_off_line_vals += write_off_line_vals + vals_list = super()._prepare_move_line_default_vals( + write_off_line_vals=new_write_off_line_vals, force_balance=force_balance + ) + # filter line with both debit and credit equal 0 + filter_vals_list = [ + vals + for vals in vals_list + if not (vals["debit"] == 0.0 and vals["credit"] == 0.0) + ] + return filter_vals_list if filter_vals_list else vals_list - def _prepare_move_line_default_vals(self, write_off_line_vals=None): - res = super()._prepare_move_line_default_vals(write_off_line_vals) + def _prepare_move_line_counterpart_vals(self, write_off_line_vals=None): + """return list of dictionary + * amount: The amount to be added to the counterpart amount. + * name: The label to set on the line. + * account_id: The account on which create the counterpart line. + """ + self.ensure_one() write_off_line_vals_list = write_off_line_vals or [] write_off_amount_currency = sum( x["amount_currency"] for x in write_off_line_vals_list ) write_off_balance = sum(x["balance"] for x in write_off_line_vals_list) - - if self.payment_type == "outbound": + is_outbound = self.payment_type == "outbound" + if is_outbound: write_off_amount_currency *= -1 new_aml_lines = [] + currency_id = self.currency_id + company_id = self.company_id + payment_date = self.date for line in self.line_payment_counterpart_ids.filtered( - lambda x: not float_is_zero( - x.amount, precision_digits=self.currency_id.decimal_places - ) + lambda x: not currency_id.is_zero(x.amount) ): - line_balance = ( - line.amount if self.payment_type == "outbound" else line.amount * -1 - ) + line_balance = line.amount if is_outbound else line.amount * -1 line_balance_currency = ( - line.amount_currency - if self.payment_type == "outbound" - else line.amount_currency * -1 + line.amount_currency if is_outbound else line.amount_currency * -1 ) aml_value = line_balance_currency + write_off_balance aml_value_currency = line_balance + write_off_amount_currency - if line.fully_paid and not float_is_zero( - line.writeoff_amount, precision_digits=self.currency_id.decimal_places - ): + if line.fully_paid and not currency_id.is_zero(line.writeoff_amount): write_off_account = ( line.writeoff_account_id.id or self.writeoff_account_id.id ) if not write_off_account: raise ValidationError( - _("Write-off account is not set for payment %(name)s") - % {"name": self.display_name} + self.env._( + "Write-off account is not set for payment %(name)s", + name=self.display_name, + ) ) # Fully Paid line + amount_currency_fully_paid = abs(line.aml_amount_residual_currency) * ( + line.aml_amount_residual > 0.0 and -1 or 1 + ) new_aml_lines.append( { "name": line.display_name, @@ -157,9 +177,14 @@ def _prepare_move_line_default_vals(self, write_off_line_vals=None): "credit": line.aml_amount_residual > 0.0 and abs(line.aml_amount_residual) or 0.0, - "amount_currency": abs(line.aml_amount_residual_currency) - * (line.aml_amount_residual > 0.0 and -1 or 1), - "date_maturity": self.date, + "amount_currency": amount_currency_fully_paid, + "balance": currency_id._convert( + amount_currency_fully_paid, + company_id.currency_id, + company_id, + payment_date, + ), + "date_maturity": payment_date, "partner_id": line.partner_id.commercial_partner_id.id, "account_id": line.account_id.id, "currency_id": line.payment_id.currency_id.id, @@ -168,18 +193,26 @@ def _prepare_move_line_default_vals(self, write_off_line_vals=None): } ) # write-off line + amount_currency_write_off = abs(line.writeoff_amount_currency) * ( + line.writeoff_amount < 0.0 and -1 or 1 + ) new_aml_lines.append( { - "name": _("Write-off"), + "name": self.env._("Write-off"), "debit": line.writeoff_amount > 0.0 and line.writeoff_amount or 0.0, "credit": line.writeoff_amount < 0.0 and -line.writeoff_amount or 0.0, - "amount_currency": abs(line.writeoff_amount_currency) - * (line.writeoff_amount < 0.0 and -1 or 1), - "date_maturity": self.date, + "amount_currency": amount_currency_write_off, + "balance": currency_id._convert( + amount_currency_write_off, + company_id.currency_id, + company_id, + payment_date, + ), + "date_maturity": payment_date, "partner_id": line.partner_id.commercial_partner_id.id, "account_id": write_off_account, "currency_id": line.payment_id.currency_id.id, @@ -187,17 +220,24 @@ def _prepare_move_line_default_vals(self, write_off_line_vals=None): "payment_line_id": line.id, } ) - else: - aml_value *= self.payment_type == "outbound" and -1 or 1 + aml_value *= is_outbound and -1 or 1 + amount_currency = abs(aml_value_currency) * ( + aml_value < 0.0 and -1 or 1 + ) new_aml_lines.append( { "name": line.display_name, "debit": aml_value > 0.0 and aml_value or 0.0, "credit": aml_value < 0.0 and -aml_value or 0.0, - "amount_currency": abs(aml_value_currency) - * (aml_value < 0.0 and -1 or 1), - "date_maturity": self.date, + "amount_currency": amount_currency, + "balance": currency_id._convert( + amount_currency, + company_id.currency_id, + company_id, + payment_date, + ), + "date_maturity": payment_date, "partner_id": line.partner_id.commercial_partner_id.id, "account_id": line.account_id.id, "currency_id": line.payment_id.currency_id.id, @@ -205,31 +245,27 @@ def _prepare_move_line_default_vals(self, write_off_line_vals=None): "payment_line_id": line.id, } ) - if len(res) >= 2 and new_aml_lines: - res.pop(1) - res += new_aml_lines - return res + return new_aml_lines def _check_writeoff_lines(self): for rec in self: writeoff_lines = rec.line_payment_counterpart_ids.filtered( - lambda x: x.fully_paid - and not float_is_zero( - x.writeoff_amount, precision_digits=rec.currency_id.decimal_places - ) + lambda x, currency=rec.currency_id: x.fully_paid + and not currency.is_zero(x.writeoff_amount) ) if not rec.writeoff_account_id and not all( line.writeoff_account_id for line in writeoff_lines ): raise ValidationError( - _( - "You should set up write-off account on lines or in header to continue" + self.env._( + "You should set up write-off account on lines " + "or in header to continue" ) ) def action_post(self): self._check_writeoff_lines() - for rec in self.filtered(lambda x: x.line_payment_counterpart_ids): + for rec in self.filtered("line_payment_counterpart_ids"): if rec.move_id.line_ids: rec.move_id.line_ids.unlink() rec.move_id.line_ids = [ @@ -241,7 +277,7 @@ def action_post(self): def action_draft(self): res = super().action_draft() - for rec in self.filtered(lambda x: x.line_payment_counterpart_ids): + for rec in self.filtered("line_payment_counterpart_ids"): # CHECK ME: force to recreate lines # if document back to draft state, # because we can change counterpart lines, @@ -256,14 +292,9 @@ class AccountPaymentCounterLine(models.Model): _description = "Counterpart line payment" payment_id = fields.Many2one( - "account.payment", string="Payment", required=False, ondelete="cascade" + "account.payment", string="Payment", ondelete="cascade" ) def _get_onchange_fields(self): - return ( - "aml_id.amount_residual", - "amount", - "payment_id.currency_id", - "payment_id.date", - "fully_paid", - ) + res = super()._get_onchange_fields() + return res + ("payment_id.currency_id", "payment_id.date") diff --git a/account_payment_line/models/counterpart_line.py b/account_payment_line/models/counterpart_line.py index 1ade21232b6..26df8ffb611 100644 --- a/account_payment_line/models/counterpart_line.py +++ b/account_payment_line/models/counterpart_line.py @@ -1,6 +1,6 @@ # Copyright 2022 ForgeFlow, S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import ValidationError dict_payment_type = dict( @@ -14,9 +14,7 @@ class AccountPaymentCounterLinesAbstract(models.AbstractModel): _description = "Counterpart line payment Abstract" company_id = fields.Many2one( - comodel_name="res.company", - compute="_compute_company_fields", - default=lambda self: self.env.company, + comodel_name="res.company", compute="_compute_company_fields" ) name = fields.Char(string="Description", required=True, default="/") account_id = fields.Many2one( @@ -36,46 +34,56 @@ class AccountPaymentCounterLinesAbstract(models.AbstractModel): comodel_name="res.currency", string="Currency", compute="_compute_company_fields", - default=lambda self: self.env.company.currency_id, ) fully_paid = fields.Boolean(string="Fully Paid?") writeoff_account_id = fields.Many2one( comodel_name="account.account", string="Write-off account", - domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", + domain="[('deprecated', '=', False), ('company_ids', '=', company_id)]", ) writeoff_amount = fields.Monetary( required=False, compute="_compute_amounts", + currency_field="currency_id", ) writeoff_amount_currency = fields.Monetary( required=False, compute="_compute_amounts", + currency_field="currency_id", ) + @api.depends_context("company") + @api.depends("company_id") def _compute_company_fields(self): + company = self.env.company for rec in self: - rec.company_id = self.env.company.id - rec.currency_id = self.env.company.currency_id.id + rec.company_id = company.id + rec.currency_id = company.currency_id.id amount = fields.Monetary(required=True) amount_currency = fields.Monetary( - string="Amount in Company Currency", compute="_compute_amounts" + string="Amount in Company Currency", + compute="_compute_amounts", + currency_field="currency_id", ) aml_amount_residual = fields.Monetary( string="Amount Residual", compute="_compute_amounts", + currency_field="currency_id", ) residual_after_payment = fields.Monetary( compute="_compute_amounts", + currency_field="currency_id", ) aml_amount_residual_currency = fields.Monetary( string="Amount Residual Currency", compute="_compute_amounts", + currency_field="currency_id", ) residual_after_payment_currency = fields.Monetary( compute="_compute_amounts", + currency_field="currency_id", ) def _get_onchange_fields(self): @@ -84,19 +92,20 @@ def _get_onchange_fields(self): @api.depends(lambda x: x._get_onchange_fields()) def _compute_amounts(self): for rec in self: + payment = rec.payment_id payment_date = ( - hasattr(rec.payment_id, "payment_date") - and rec.payment_id.payment_date - or rec.payment_id.date + hasattr(payment, "payment_date") + and payment.payment_date + or payment.date ) - rec.amount_currency = rec.payment_id.currency_id._convert( + rec.amount_currency = payment.currency_id._convert( rec.amount, - rec.payment_id.company_id.currency_id, - rec.payment_id.company_id, + payment.company_id.currency_id, + payment.company_id, date=payment_date, ) rec.aml_amount_residual = rec.aml_id.amount_residual - min_max = rec.payment_id.payment_type == "outbound" and min or max + min_max = payment.payment_type == "outbound" and min or max rec.residual_after_payment = ( not rec.fully_paid and min_max(rec.aml_id.amount_residual - rec.amount, 0) @@ -162,16 +171,18 @@ def _onchange_move_id(self): else: rec.amount = rec.aml_id.amount_residual - @api.constrains("amount", "aml_amount_residual") + @api.constrains("amount") def constrains_amount_residual(self): for rec in self: if ( rec.aml_id and 0 < rec.aml_amount_residual_currency < rec.amount_currency ): + move_name = rec.aml_id.move_id.name or rec.aml_id.name raise ValidationError( - _( - "the amount exceeds the residual amount, please check the invoice %s" + self.env._( + "the amount exceeds the residual amount, " + "please check the invoice %s", + move_name, ) - % (rec.aml_id.move_id.name or rec.aml_id.name), ) diff --git a/account_payment_line/readme/CONTRIBUTORS.md b/account_payment_line/readme/CONTRIBUTORS.md index 049f2c6bad6..e2daf8bf739 100644 --- a/account_payment_line/readme/CONTRIBUTORS.md +++ b/account_payment_line/readme/CONTRIBUTORS.md @@ -1 +1,2 @@ - Christopher Ormaza. \<\> +- Do Anh Duy \<\> diff --git a/account_payment_line/security/ir.model.access.csv b/account_payment_line/security/ir.model.access.csv index 1d8229944fe..5e27e28cceb 100644 --- a/account_payment_line/security/ir.model.access.csv +++ b/account_payment_line/security/ir.model.access.csv @@ -1,5 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_account_payment_counterpart_line_all,access_account_payment_counterpart_line_all,model_account_payment_counterpart_line,,1,0,0,0 +access_account_payment_counterpart_line_all,access_account_payment_counterpart_line_all,model_account_payment_counterpart_line,base.group_user,1,0,0,0 access_account_payment_counterpart_line_group_account_invoice,access_account_payment_counterpart_line_group_account_invoice,model_account_payment_counterpart_line,account.group_account_invoice,1,1,1,1 access_account_payment_counterpart_line_group_account_user,access_account_payment_counterpart_line_group_account_user,model_account_payment_counterpart_line,account.group_account_user,1,1,1,1 access_account_payment_counterpart_line_group_account_manager,access_account_payment_counterpart_line_group_account_manager,model_account_payment_counterpart_line,account.group_account_manager,1,1,1,1 diff --git a/account_payment_line/static/description/index.html b/account_payment_line/static/description/index.html index b7d4eb8db87..510866678ad 100644 --- a/account_payment_line/static/description/index.html +++ b/account_payment_line/static/description/index.html @@ -415,6 +415,7 @@

Authors

Contributors

diff --git a/account_payment_line/tests/test_account_payment_line.py b/account_payment_line/tests/test_account_payment_line.py index ee1e334cf99..c81fe40411f 100644 --- a/account_payment_line/tests/test_account_payment_line.py +++ b/account_payment_line/tests/test_account_payment_line.py @@ -3,9 +3,9 @@ import json -from odoo import fields +from odoo import Command, fields from odoo.exceptions import ValidationError -from odoo.tests.common import Form, tagged +from odoo.tests import Form, tagged from odoo.addons.account.tests.common import AccountTestInvoicingCommon @@ -13,44 +13,26 @@ @tagged("post_install", "-at_install") class TestAccountPaymentLines(AccountTestInvoicingCommon): @classmethod - def setUpClass(cls, chart_template_ref=None): - super().setUpClass(chart_template_ref=chart_template_ref) + def setUpClass(cls): + super().setUpClass() cls.test_product = cls.env["product.product"].create( { "name": "test_product", "type": "service", } ) - cls.customer = cls.env["res.partner"].create( - { - "name": "test_customer", - } - ) - cls.customer2 = cls.env["res.partner"].create( - { - "name": "test_customer", - } - ) - cls.supplier = cls.env["res.partner"].create( - { - "name": "test_vendor", - } - ) - cls.supplier2 = cls.env["res.partner"].create( - { - "name": "test_vendor", - } - ) + cls.customer = cls.env["res.partner"].create({"name": "test_customer"}) + cls.customer2 = cls.env["res.partner"].create({"name": "test_customer"}) + cls.supplier = cls.env["res.partner"].create({"name": "test_vendor"}) + cls.supplier2 = cls.env["res.partner"].create({"name": "test_vendor"}) cls.currency_2x = cls.env["res.currency"].create( { "name": "2X", # Foreign currency, 2 time "symbol": "X", "rate_ids": [ - ( - 0, - 0, + Command.create( { - "name": fields.Date.today(), + "name": fields.Date.context_today(cls.env.user), "rate": cls.env.company.currency_id.rate * 2, }, ) @@ -63,22 +45,18 @@ def setUpClass(cls, chart_template_ref=None): "note": "Payment terms: 30% Advance End of Following Month", "sequence": 500, "line_ids": [ - ( - 0, - 0, + Command.create( { "value": "percent", "value_amount": 50.0, - "days": 0, + "nb_days": 0, }, ), - ( - 0, - 0, + Command.create( { - "value": "balance", - "value_amount": 0.0, - "days": 31, + "value": "percent", + "value_amount": 50.0, + "nb_days": 31, }, ), ], @@ -102,7 +80,7 @@ def _create_invoice( ) if not currency: currency = self.env.company.currency_id - move_form.invoice_date = fields.Date.today() + move_form.invoice_date = fields.Date.context_today(self.env.user) move_form.partner_id = partner move_form.currency_id = currency if payment_term: @@ -115,21 +93,20 @@ def _create_invoice( move.action_post() return move - def _create_refund(self, invoice, refund_method="cancel"): + def _create_refund(self, invoice): ctx = {"active_model": "account.move", "active_ids": [invoice.id]} move_reversal = ( self.env["account.move.reversal"] .with_context(**ctx) .create( { - "date": fields.Date.today(), + "date": fields.Date.context_today(self.env.user), "reason": "no reason", - "refund_method": refund_method, "journal_id": invoice.journal_id.id, } ) ) - reversal = move_reversal.reverse_moves() + reversal = move_reversal.refund_moves() reverse_move = self.env["account.move"].browse(reversal["res_id"]) return reverse_move @@ -190,14 +167,10 @@ def test_01_customer_payment(self): 100.0, "inbound", "customer", - [ - { - "move_id": new_invoice, - } - ], + [{"move_id": new_invoice}], post=True, ) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual(new_payment.reconciled_invoice_ids, new_invoice) @@ -217,7 +190,7 @@ def test_01_customer_payment(self): ], post=True, ) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "in_process") self.assertTrue(new_payment2.is_reconciled) self.assertEqual(new_payment2.reconciled_invoice_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 50.0) @@ -227,14 +200,10 @@ def test_01_customer_payment(self): 50.0, "inbound", "customer", - [ - { - "move_id": new_invoice2, - } - ], + [{"move_id": new_invoice2}], post=True, ) - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment3.state, "paid") self.assertTrue(new_payment3.is_reconciled) self.assertEqual(new_payment3.reconciled_invoice_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 0.0) @@ -242,8 +211,8 @@ def test_01_customer_payment(self): new_payment2.action_draft() new_payment2.action_cancel() - self.assertEqual(new_payment2.state, "cancel") - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment2.state, "canceled") + self.assertEqual(new_payment3.state, "in_process") self.assertTrue(new_payment3.is_reconciled) self.assertEqual(new_payment3.reconciled_invoice_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 50.0) @@ -261,22 +230,18 @@ def test_02_customer_refund_payment(self): "inbound", "customer", [ - { - "move_id": new_invoice, - }, - { - "move_id": new_invoice2, - }, + {"move_id": new_invoice}, + {"move_id": new_invoice2}, ], post=True, ) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual(new_payment.reconciled_invoice_ids, new_invoice + new_invoice2) self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) - new_refund = self._create_refund(new_invoice, "refund") + new_refund = self._create_refund(new_invoice) new_refund.action_post() self.assertEqual(new_refund.payment_state, "not_paid") self.assertEqual(new_refund.amount_total, 100.0) @@ -285,14 +250,10 @@ def test_02_customer_refund_payment(self): 100.0, "outbound", "customer", - [ - { - "move_id": new_refund, - }, - ], + [{"move_id": new_refund}], post=True, ) - self.assertEqual(new_payment_refund.state, "posted") + self.assertEqual(new_payment_refund.state, "paid") self.assertTrue(new_payment_refund.is_reconciled) self.assertEqual(new_payment_refund.reconciled_invoice_ids, new_refund) self.assertIn(new_refund.payment_state, ["paid", "in_payment"]) @@ -306,14 +267,10 @@ def test_03_supplier_payment(self): 100.0, "outbound", "supplier", - [ - { - "move_id": new_invoice, - } - ], + [{"move_id": new_invoice}], post=True, ) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "in_process") self.assertTrue(new_payment.is_reconciled) self.assertEqual(new_payment.reconciled_bill_ids, new_invoice) @@ -333,7 +290,7 @@ def test_03_supplier_payment(self): ], post=True, ) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "in_process") self.assertTrue(new_payment2.is_reconciled) self.assertEqual(new_payment2.reconciled_bill_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 50.0) @@ -343,14 +300,10 @@ def test_03_supplier_payment(self): 50.0, "outbound", "supplier", - [ - { - "move_id": new_invoice2, - } - ], + [{"move_id": new_invoice2}], post=True, ) - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment3.state, "in_process") self.assertTrue(new_payment3.is_reconciled) self.assertEqual(new_payment3.reconciled_bill_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 0.0) @@ -358,8 +311,8 @@ def test_03_supplier_payment(self): new_payment2.action_draft() new_payment2.action_cancel() - self.assertEqual(new_payment2.state, "cancel") - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment2.state, "canceled") + self.assertEqual(new_payment3.state, "in_process") self.assertTrue(new_payment3.is_reconciled) self.assertEqual(new_payment3.reconciled_bill_ids, new_invoice2) self.assertEqual(new_invoice2.amount_residual, 50.0) @@ -377,22 +330,18 @@ def test_04_supplier_refund_payment(self): "outbound", "supplier", [ - { - "move_id": new_invoice, - }, - { - "move_id": new_invoice2, - }, + {"move_id": new_invoice}, + {"move_id": new_invoice2}, ], post=True, ) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "in_process") self.assertTrue(new_payment.is_reconciled) self.assertEqual(new_payment.reconciled_bill_ids, new_invoice + new_invoice2) self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) - new_refund = self._create_refund(new_invoice, "refund") + new_refund = self._create_refund(new_invoice) new_refund.action_post() self.assertEqual(new_refund.payment_state, "not_paid") self.assertEqual(new_refund.amount_total, 100.0) @@ -401,14 +350,10 @@ def test_04_supplier_refund_payment(self): 100.0, "inbound", "supplier", - [ - { - "move_id": new_refund, - }, - ], + [{"move_id": new_refund}], post=True, ) - self.assertEqual(new_payment_refund.state, "posted") + self.assertEqual(new_payment_refund.state, "in_process") self.assertTrue(new_payment_refund.is_reconciled) self.assertEqual(new_payment_refund.reconciled_bill_ids, new_refund) self.assertIn(new_refund.payment_state, ["paid", "in_payment"]) @@ -461,7 +406,7 @@ def test_05_partial_payments(self): self.assertEqual(new_invoice2.amount_residual, 50.0) self.assertEqual(new_invoice3.payment_state, "partial") self.assertEqual(new_invoice3.amount_residual, 50.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "in_process") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -491,7 +436,7 @@ def test_05_partial_payments(self): self.assertEqual(new_invoice2.amount_residual, 0.0) self.assertEqual(new_invoice3.payment_state, "partial") self.assertEqual(new_invoice3.amount_residual, 50.0) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "paid") self.assertTrue(new_payment2.is_reconciled) self.assertEqual( new_payment2.reconciled_invoice_ids, @@ -512,7 +457,7 @@ def test_05_partial_payments(self): ) self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice3.amount_residual, 0.0) - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment3.state, "paid") self.assertTrue(new_payment3.is_reconciled) self.assertEqual( new_payment3.reconciled_invoice_ids, @@ -537,7 +482,7 @@ def test_06_payments_without_invoices(self): ], post=True, ) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "in_process") self.assertFalse(new_payment.is_reconciled) self.assertFalse(bool(new_payment.reconciled_invoice_ids)) new_invoice = self._create_invoice("out_invoice", self.customer, 100.0) @@ -548,7 +493,7 @@ def test_06_payments_without_invoices(self): new_invoice.js_assign_outstanding_line(payments.get("content", [])[0].get("id")) self.assertEqual(new_invoice.payment_state, "partial") self.assertEqual(new_invoice.amount_residual, 50.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "in_process") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -576,7 +521,7 @@ def test_07_payment_multi_currency(self): ) self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice.amount_residual, 0.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -604,7 +549,7 @@ def test_07_payment_multi_currency(self): ) self.assertIn(new_invoice2.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice2.amount_residual, 0.0) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "paid") self.assertTrue(new_payment2.is_reconciled) self.assertEqual( new_payment2.reconciled_invoice_ids, @@ -630,7 +575,7 @@ def test_07_payment_multi_currency(self): ) self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice.amount_residual, 0.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -680,7 +625,7 @@ def test_08_payment_term_split(self): self.assertEqual(new_invoice2.amount_residual, 0.0) self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice3.amount_residual, 0.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -708,7 +653,7 @@ def test_09_offset_payment(self): ) self.assertIn(new_invoice.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice.amount_residual, 0.0) - self.assertEqual(new_payment.state, "posted") + self.assertEqual(new_payment.state, "paid") self.assertTrue(new_payment.is_reconciled) self.assertEqual( new_payment.reconciled_invoice_ids, @@ -749,7 +694,7 @@ def test_09_offset_payment(self): self.assertEqual(new_invoice2.amount_residual, 0.0) self.assertIn(new_invoice3.payment_state, ["paid", "in_payment"]) self.assertEqual(new_invoice3.amount_residual, 0.0) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "paid") self.assertTrue(new_payment2.is_reconciled) self.assertEqual( new_payment2.reconciled_invoice_ids, @@ -791,7 +736,7 @@ def test_10_payment_distribution_proposition(self): payment.action_post() self.assertEqual(new_out_invoice.payment_state, "partial") self.assertEqual(new_out_invoice.amount_residual, 50.0) - self.assertEqual(payment.state, "posted") + self.assertEqual(payment.state, "in_process") self.assertTrue(payment.is_reconciled) self.assertEqual( payment.reconciled_invoice_ids, @@ -819,7 +764,7 @@ def test_10_payment_distribution_proposition(self): self.assertEqual(new_out_invoice.amount_residual, 0.0) self.assertIn(new_out_refund.payment_state, ["paid", "in_payment"]) self.assertEqual(new_out_refund.amount_residual, 0.0) - self.assertEqual(new_payment2.state, "posted") + self.assertEqual(new_payment2.state, "paid") self.assertTrue(new_payment2.is_reconciled) self.assertEqual( new_payment2.reconciled_invoice_ids, @@ -900,7 +845,7 @@ def test_10_payment_distribution_proposition(self): self.assertEqual(new_in_refund.amount_residual, 0.0) self.assertIn(new_in_invoice.payment_state, ["paid", "in_payment"]) self.assertEqual(new_in_invoice.amount_residual, 0.0) - self.assertEqual(new_payment3.state, "posted") + self.assertEqual(new_payment3.state, "paid") self.assertTrue(new_payment3.is_reconciled) self.assertEqual( new_payment3.reconciled_bill_ids, @@ -940,7 +885,7 @@ def test_10_payment_distribution_proposition(self): self.assertEqual(new_in_refund2.amount_residual, 0.0) self.assertIn(new_in_invoice2.payment_state, ["paid", "in_payment"]) self.assertEqual(new_in_invoice2.amount_residual, 0.0) - self.assertEqual(new_payment4.state, "posted") + self.assertEqual(new_payment4.state, "paid") self.assertTrue(new_payment4.is_reconciled) self.assertEqual( new_payment4.reconciled_invoice_ids, diff --git a/account_payment_line/views/account_payment_views.xml b/account_payment_line/views/account_payment_views.xml index 3e64e8b840f..9a62a81d1d3 100644 --- a/account_payment_line/views/account_payment_views.xml +++ b/account_payment_line/views/account_payment_views.xml @@ -2,11 +2,11 @@ - view.account.payment.line.tree + view.account.payment.line.list account.payment.counterpart.line - - + + - - + @@ -96,14 +95,14 @@