diff --git a/account_payment_netting/README.rst b/account_payment_netting/README.rst
index 3ab9482929f..3e7f0a3f7a2 100644
--- a/account_payment_netting/README.rst
+++ b/account_payment_netting/README.rst
@@ -7,7 +7,7 @@ Account Payment Netting
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:5a59777a2cbed41a5490951d8fa5d17b69a0cb7cb89a8107a55cbb2ab1b12d3c
+ !! source digest: sha256:8364edf09da1fd6decbf2c97a25bd9e54b3e4c605fd65666712c3cc646d2fc1e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -34,7 +34,7 @@ This module allow net payment on AR/AP invoice from the same business partner.
but make it more user friendly when netting invoices.
While account netting require user to select manually the journal items to do netting
(which create netting journal entry), this module has a new menu "Invoices to netting"
-allowing user to select both customer/supplier invoice to register payment.
+allowing user to select both customer invoice/vendor bill to register payment.
**Table of contents**
@@ -52,7 +52,7 @@ and user decide to make payment on the diff.
- Click on action "Register Payment", the wizard will show the diff amount
- Make payment as normal
-This create Customer Payment if AR > AP, Supplier Payment otherwise.
+This create Customer Payment if AR > AP, Vendor Payment otherwise.
Bug Tracker
===========
@@ -76,6 +76,7 @@ Contributors
~~~~~~~~~~~~
* Kitti Upariphutthiphong
+* Saran Lim.
Maintainers
~~~~~~~~~~~
diff --git a/account_payment_netting/__init__.py b/account_payment_netting/__init__.py
index 13bd5e86165..100487c8ded 100644
--- a/account_payment_netting/__init__.py
+++ b/account_payment_netting/__init__.py
@@ -1,4 +1,4 @@
-# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import models
+from . import wizards
diff --git a/account_payment_netting/__manifest__.py b/account_payment_netting/__manifest__.py
index 3935698cde4..eca1f90cce1 100644
--- a/account_payment_netting/__manifest__.py
+++ b/account_payment_netting/__manifest__.py
@@ -3,18 +3,17 @@
{
"name": "Account Payment Netting",
- "version": "12.0.1.0.1",
+ "version": "16.0.1.0.0",
"summary": "Net Payment on AR/AP invoice from the same partner",
"category": "Accounting & Finance",
- "author": "Ecosoft, " "Odoo Community Association (OCA)",
+ "author": "Ecosoft, Odoo Community Association (OCA)",
"license": "AGPL-3",
"website": "https://github.com/OCA/account-financial-tools",
- "depends": [
- "account",
- ],
+ "depends": ["account"],
"data": [
- "views/account_invoice_view.xml",
+ "views/account_move_view.xml",
"views/account_payment_view.xml",
+ "wizards/account_payment_register_views.xml",
],
"installable": True,
"development_status": "Beta",
diff --git a/account_payment_netting/models/__init__.py b/account_payment_netting/models/__init__.py
index d948651cd4b..fadd1f976f8 100644
--- a/account_payment_netting/models/__init__.py
+++ b/account_payment_netting/models/__init__.py
@@ -1,5 +1,3 @@
-# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import account_payment
-from . import account_invoice
diff --git a/account_payment_netting/models/account_invoice.py b/account_payment_netting/models/account_invoice.py
deleted file mode 100644
index d85ff97dbe5..00000000000
--- a/account_payment_netting/models/account_invoice.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
-# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-
-from odoo import api, fields, models
-
-
-class AccountInvoice(models.Model):
- _inherit = "account.invoice"
-
- unpaid_move_lines = fields.One2many(
- comodel_name="account.move.line",
- compute="_compute_unpaid_move_lines",
- help="Compute unpaid AR/AP move lines of this invoice",
- )
-
- @api.multi
- def _compute_unpaid_move_lines(self):
- for inv in self:
- inv.unpaid_move_lines = inv.move_id.line_ids.filtered(
- lambda r: not r.reconciled
- and r.account_id.internal_type in ("payable", "receivable")
- )
-
- @api.model
- def _get_netting_groups(self, account_groups):
- debtors = []
- creditors = []
- total_debtors = 0
- total_creditors = 0
- for account_group in account_groups:
- balance = account_group["debit"] - account_group["credit"]
- group_vals = {
- "account_id": account_group["account_id"][0],
- "balance": abs(balance),
- }
- if balance > 0:
- debtors.append(group_vals)
- total_debtors += balance
- else:
- creditors.append(group_vals)
- total_creditors += abs(balance)
- return (debtors, total_debtors, creditors, total_creditors)
-
- @api.model
- def _get_netting_move_lines(
- self, payment_line, partner, debtors, total_debtors, creditors, total_creditors
- ):
- netting_amount = min(total_creditors, total_debtors)
- field_map = {1: "debit", 0: "credit"}
- move_lines = []
- for i, group in enumerate([debtors, creditors]):
- available_amount = netting_amount
- for account_group in group:
- if account_group["balance"] > available_amount:
- amount = available_amount
- else:
- amount = account_group["balance"]
- move_line_vals = {
- field_map[i]: amount,
- "partner_id": partner.id,
- "name": payment_line.move_id.ref,
- "account_id": account_group["account_id"],
- "payment_id": payment_line.payment_id.id,
- }
- move_lines.append((0, 0, move_line_vals))
- available_amount -= account_group["balance"]
- if available_amount <= 0:
- break
- return move_lines
-
- @api.multi
- def register_payment(
- self, payment_line, writeoff_acc_id=False, writeoff_journal_id=False
- ):
- """Attempt to reconcile netting first,
- and leave the remaining for normal reconcile"""
- if not payment_line.payment_id.netting:
- return super().register_payment(
- payment_line,
- writeoff_acc_id=writeoff_acc_id,
- writeoff_journal_id=writeoff_journal_id,
- )
- # Case netting payment:
- # 1. create netting lines dr/cr
- # 2. do initial reconcile
- line_to_netting = self.mapped("unpaid_move_lines")
- payment_move = payment_line.move_id
- # Group amounts by account
- account_groups = line_to_netting.read_group(
- [("id", "in", line_to_netting.ids)],
- ["account_id", "debit", "credit"],
- ["account_id"],
- )
- (debtors, total_debtors, creditors, total_creditors) = self._get_netting_groups(
- account_groups
- )
- # Create move lines
- move_lines = self._get_netting_move_lines(
- payment_line,
- line_to_netting[0].partner_id,
- debtors,
- total_debtors,
- creditors,
- total_creditors,
- )
- if move_lines:
- payment_move.write({"line_ids": move_lines})
- # Make reconciliation
- for move_line in payment_move.line_ids:
- if move_line == payment_line: # Keep this for super()
- continue
- to_reconcile = move_line + line_to_netting.filtered(
- lambda x: x.account_id == move_line.account_id
- )
- to_reconcile.filtered("account_id.reconcile").filtered(
- lambda r: not r.reconciled
- ).reconcile()
- return super().register_payment(
- payment_line.filtered(lambda l: not l.reconciled),
- writeoff_acc_id=writeoff_acc_id,
- writeoff_journal_id=writeoff_journal_id,
- )
diff --git a/account_payment_netting/models/account_payment.py b/account_payment_netting/models/account_payment.py
index 19d5132aecc..e3ddc435b36 100644
--- a/account_payment_netting/models/account_payment.py
+++ b/account_payment_netting/models/account_payment.py
@@ -1,108 +1,148 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
-from odoo import _, api, fields, models
-from odoo.exceptions import UserError
+from odoo import fields, models
-class AccountAbstractPayment(models.AbstractModel):
- _inherit = "account.abstract.payment"
+class AccountPayments(models.Model):
+ _inherit = "account.payment"
netting = fields.Boolean(
- string="Netting",
+ readonly=True,
help="Technical field, as user select invoice that are both AR and AP",
)
- @api.model
- def default_get(self, fields):
- rec = super().default_get(fields)
- if not rec.get("multi"):
- return rec
- active_ids = self._context.get("active_ids")
- invoices = self.env["account.invoice"].browse(active_ids)
- types = invoices.mapped("type")
- ap = any({"in_invoice", "in_refund"}.intersection(types))
- ar = any({"out_invoice", "out_refund"}.intersection(types))
- if ap and ar: # Both AP and AR -> Netting
- rec.update(
- {
- "netting": True,
- "multi": False, # With netting, allow edit amount
- "communication": ", ".join(invoices.mapped("number")),
- }
- )
- return rec
-
- def _compute_journal_domain_and_types(self):
- if not self.netting:
- return super()._compute_journal_domain_and_types()
- # For case netting, it is possible to have net amount = 0.0
- # without forcing new journal type and payment diff handling
- domain = []
- if self.payment_type == "inbound":
- domain.append(("at_least_one_inbound", "=", True))
- else:
- domain.append(("at_least_one_outbound", "=", True))
- return {"domain": domain, "journal_types": {"bank", "cash"}}
-
+ def _synchronize_from_moves(self, changed_fields):
+ if self.env.context.get("netting"):
+ self = self.with_context(skip_account_move_synchronization=1)
+ return super()._synchronize_from_moves(changed_fields)
-class AccountRegisterPayments(models.TransientModel):
- _inherit = "account.register.payments"
+ def _get_move_line_vals_netting(
+ self, name, date, remaining_amount_currency, currency, account
+ ):
+ return [
+ {
+ "name": name,
+ "date_maturity": date,
+ "amount_currency": remaining_amount_currency,
+ "currency_id": currency.id,
+ "partner_id": self.partner_id.id,
+ "account_id": account.id,
+ }
+ ]
- @api.multi
- def get_payments_vals(self):
- """When doing netting, combine all invoices"""
- if self.netting:
- return [self._prepare_payment_vals(self.invoice_ids)]
- return super().get_payments_vals()
-
- @api.multi
- def _prepare_payment_vals(self, invoices):
- """When doing netting, partner_type follow payment type"""
- values = super()._prepare_payment_vals(invoices)
- if self.netting:
- values["netting"] = self.netting
- values["communication"] = self.communication
+ def _prepare_move_line_default_vals(self, write_off_line_vals=None):
+ self.ensure_one()
+ if self.env.context.get("netting"):
+ domain = [
+ ("move_id", "in", self.env.context.get("active_ids", [])),
+ ("account_type", "in", ["asset_receivable", "liability_payable"]),
+ ("reconciled", "=", False),
+ ]
+ # Sort by amount
+ # Inbound: AR > AP; loop AP first
+ # Outbound: AP > AR; loop AR first
+ ml_reconciled = self.env["account.move.line"].search(domain)
if self.payment_type == "inbound":
- values["partner_type"] = "customer"
- elif self.payment_type == "outbound":
- values["partner_type"] = "supplier"
- return values
+ move_lines = sorted(
+ ml_reconciled, key=lambda k: (k.move_type, k.amount_residual)
+ )
+ else:
+ move_lines = sorted(
+ ml_reconciled,
+ key=lambda k: (k.move_type, -abs(k.amount_residual)),
+ reverse=True,
+ )
- @api.multi
- def create_payments(self):
- if self.netting:
- self._validate_invoice_netting(self.invoice_ids)
- return super().create_payments()
+ line_vals_list = []
+ liquidity_amount_currency = self.amount
+ remaining_amount_currency = 0.0
+ current_move_type = False
- @api.model
- def _validate_invoice_netting(self, invoices):
- """Ensure valid selection of invoice for netting process"""
- # All invoice must be of the same partner
- if len(invoices.mapped("commercial_partner_id")) > 1:
- raise UserError(_("All invoices must belong to same partner"))
- # All invoice must have residual
- paid_invoices = invoices.filtered(lambda l: not l.residual)
- if paid_invoices:
- raise UserError(
- _("Some selected invoices are already paid: %s")
- % paid_invoices.mapped("number")
- )
+ # Write-off
+ write_off_line_vals = write_off_line_vals or []
+ for i, line in enumerate(move_lines):
+ # AR > AP but line is AP, change sign to positive
+ sign = (
+ 1
+ if self.payment_type == "outbound"
+ and line.move_type == "in_invoice"
+ else -1
+ )
+ amount_residual_currency = line.amount_residual_currency
+ # Last line
+ if (
+ liquidity_amount_currency
+ and i == len(move_lines) - 1
+ and not write_off_line_vals
+ ):
+ # For case netting with 1 move type
+ if (
+ liquidity_amount_currency > abs(amount_residual_currency)
+ and remaining_amount_currency <= amount_residual_currency
+ ):
+ amount_total_currency = abs(amount_residual_currency)
+ else:
+ amount_total_currency = liquidity_amount_currency + abs(
+ remaining_amount_currency
+ )
+ line_vals_list += self._get_move_line_vals_netting(
+ line.move_id.name,
+ self.date,
+ sign * amount_total_currency,
+ line.currency_id,
+ line.account_id,
+ )
+ break
+ # Check if move_type is changed
+ if current_move_type and current_move_type != line.move_type:
+ # Get min amount from remaining_amount_currency and amount_residual_currency
+ if not write_off_line_vals:
+ amount_remaining = min(
+ abs(remaining_amount_currency),
+ abs(amount_residual_currency),
+ )
+ else:
+ amount_remaining = abs(amount_residual_currency)
-class AccountPayments(models.Model):
- _inherit = "account.payment"
+ # No create lines if amount_remaining is 0
+ if not amount_remaining:
+ continue
- @api.one
- @api.depends("invoice_ids", "payment_type", "partner_type", "partner_id")
- def _compute_destination_account_id(self):
- super()._compute_destination_account_id()
- if self.netting:
- if self.partner_type == "customer":
- self.destination_account_id = (
- self.partner_id.property_account_receivable_id.id
- )
- else:
- self.destination_account_id = (
- self.partner_id.property_account_payable_id.id
+ # AR > AP but line is AP, change sign to positive
+ line_vals_list += self._get_move_line_vals_netting(
+ line.move_id.name,
+ self.date,
+ sign * amount_remaining,
+ line.currency_id,
+ line.account_id,
+ )
+ remaining_amount_currency = abs(remaining_amount_currency) - abs(
+ amount_remaining
+ )
+ # First line or same move_type
+ else:
+ current_move_type = line.move_type
+ line_vals_list += self._get_move_line_vals_netting(
+ line.move_id.name,
+ self.date,
+ -1 * amount_residual_currency,
+ line.currency_id,
+ line.account_id,
+ )
+ remaining_amount_currency += amount_residual_currency
+
+ # Liquidity line.
+ if liquidity_amount_currency:
+ line_vals_list += self._get_move_line_vals_netting(
+ self.ref,
+ self.date,
+ liquidity_amount_currency
+ if self.payment_type == "inbound"
+ else -liquidity_amount_currency,
+ self.currency_id,
+ self.outstanding_account_id,
)
+ return line_vals_list + write_off_line_vals
+ return super()._prepare_move_line_default_vals(write_off_line_vals)
diff --git a/account_payment_netting/readme/CONTRIBUTORS.rst b/account_payment_netting/readme/CONTRIBUTORS.rst
index 033e67f43c1..da3b6a139c2 100644
--- a/account_payment_netting/readme/CONTRIBUTORS.rst
+++ b/account_payment_netting/readme/CONTRIBUTORS.rst
@@ -1 +1,2 @@
* Kitti Upariphutthiphong
+* Saran Lim.
\ No newline at end of file
diff --git a/account_payment_netting/readme/DESCRIPTION.rst b/account_payment_netting/readme/DESCRIPTION.rst
index 2daf0affdca..f21b318bd31 100644
--- a/account_payment_netting/readme/DESCRIPTION.rst
+++ b/account_payment_netting/readme/DESCRIPTION.rst
@@ -4,4 +4,4 @@ This module allow net payment on AR/AP invoice from the same business partner.
but make it more user friendly when netting invoices.
While account netting require user to select manually the journal items to do netting
(which create netting journal entry), this module has a new menu "Invoices to netting"
-allowing user to select both customer/supplier invoice to register payment.
+allowing user to select both customer invoice/vendor bill to register payment.
diff --git a/account_payment_netting/readme/USAGE.rst b/account_payment_netting/readme/USAGE.rst
index 4bd9c99868a..02ccc936c54 100644
--- a/account_payment_netting/readme/USAGE.rst
+++ b/account_payment_netting/readme/USAGE.rst
@@ -6,4 +6,4 @@ and user decide to make payment on the diff.
- Click on action "Register Payment", the wizard will show the diff amount
- Make payment as normal
-This create Customer Payment if AR > AP, Supplier Payment otherwise.
+This create Customer Payment if AR > AP, Vendor Payment otherwise.
diff --git a/account_payment_netting/static/description/index.html b/account_payment_netting/static/description/index.html
index e4caebe7e8a..68b8980f5df 100644
--- a/account_payment_netting/static/description/index.html
+++ b/account_payment_netting/static/description/index.html
@@ -367,7 +367,7 @@ Account Payment Netting
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:5a59777a2cbed41a5490951d8fa5d17b69a0cb7cb89a8107a55cbb2ab1b12d3c
+!! source digest: sha256:8364edf09da1fd6decbf2c97a25bd9e54b3e4c605fd65666712c3cc646d2fc1e
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
This module allow net payment on AR/AP invoice from the same business partner.
@@ -375,7 +375,7 @@ Account Payment Netting
but make it more user friendly when netting invoices.
While account netting require user to select manually the journal items to do netting
(which create netting journal entry), this module has a new menu “Invoices to netting”
-allowing user to select both customer/supplier invoice to register payment.
+allowing user to select both customer invoice/vendor bill to register payment.
Table of contents
@@ -399,7 +399,7 @@
- Click on action “Register Payment”, the wizard will show the diff amount
- Make payment as normal
-
This create Customer Payment if AR > AP, Supplier Payment otherwise.
+
This create Customer Payment if AR > AP, Vendor Payment otherwise.
diff --git a/account_payment_netting/tests/test_account_payment_netting.py b/account_payment_netting/tests/test_account_payment_netting.py
index 5bdaa320d29..220b2513c17 100644
--- a/account_payment_netting/tests/test_account_payment_netting.py
+++ b/account_payment_netting/tests/test_account_payment_netting.py
@@ -1,133 +1,61 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
+from odoo import Command, fields
from odoo.exceptions import UserError
-from odoo.tests.common import Form, SavepointCase
+from odoo.tests.common import Form, TransactionCase
-class TestAccountNetting(SavepointCase):
+class TestAccountNetting(TransactionCase):
@classmethod
def setUpClass(cls):
- super(TestAccountNetting, cls).setUpClass()
- cls.invoice_model = cls.env["account.invoice"]
+ super().setUpClass()
+ cls.move_model = cls.env["account.move"]
cls.payment_model = cls.env["account.payment"]
- cls.register_payment_model = cls.env["account.register.payments"]
- cls.account_receivable = cls.env["account.account"].create(
- {
- "code": "AR",
- "name": "Account Receivable",
- "user_type_id": cls.env.ref("account.data_account_type_receivable").id,
- "reconcile": True,
- }
- )
- cls.account_payable = cls.env["account.account"].create(
+ cls.journal_model = cls.env["account.journal"]
+ cls.register_payment_model = cls.env["account.payment.register"]
+
+ cls.partner1 = cls.env.ref("base.res_partner_1")
+ cls.partner2 = cls.env.ref("base.res_partner_2")
+
+ cls.bank_journal = cls.journal_model.create(
{
- "code": "AP",
- "name": "Account Payable",
- "user_type_id": cls.env.ref("account.data_account_type_payable").id,
- "reconcile": True,
+ "name": "Test bank journal",
+ "type": "bank",
+ "code": "TEBNK",
}
)
- cls.account_revenue = cls.env["account.account"].search(
- [
- (
- "user_type_id",
- "=",
- cls.env.ref("account.data_account_type_revenue").id,
- )
- ],
- limit=1,
- )
cls.account_expense = cls.env["account.account"].search(
[
- (
- "user_type_id",
- "=",
- cls.env.ref("account.data_account_type_expenses").id,
- )
+ ("account_type", "=", "expense"),
+ ("company_id", "=", cls.env.company.id),
],
limit=1,
)
- cls.partner1 = cls.env["res.partner"].create(
- {
- "supplier": True,
- "customer": True,
- "name": "Supplier/Customer 1",
- "property_account_receivable_id": cls.account_receivable.id,
- "property_account_payable_id": cls.account_payable.id,
- }
- )
- cls.partner2 = cls.env["res.partner"].create(
- {
- "supplier": True,
- "customer": True,
- "name": "Supplier/Customer 2",
- "property_account_receivable_id": cls.account_receivable.id,
- "property_account_payable_id": cls.account_payable.id,
- }
- )
- cls.sale_journal = cls.env["account.journal"].create(
- {
- "name": "Test sale journal",
- "type": "sale",
- "code": "INV",
- }
- )
- cls.purchase_journal = cls.env["account.journal"].create(
- {
- "name": "Test expense journal",
- "type": "purchase",
- "code": "BIL",
- }
- )
- cls.bank_journal = cls.env["account.journal"].create(
- {
- "name": "Test bank journal",
- "type": "bank",
- "code": "BNK",
- }
- )
- cls.bank_journal.inbound_payment_method_ids |= cls.env.ref(
- "account.account_payment_method_manual_in"
- )
- cls.bank_journal.outbound_payment_method_ids |= cls.env.ref(
- "account.account_payment_method_manual_out"
- )
-
- def create_invoice(self, inv_type, partner, amount):
+ def create_invoice(self, move_type, partner, amount):
"""Returns an open invoice"""
- journal = (
- inv_type == "in_invoice" and self.purchase_journal or self.sale_journal
- )
- arap_account = (
- inv_type == "in_invoice" and self.account_payable or self.account_receivable
- )
- account = (
- inv_type == "in_invoice" and self.account_expense or self.account_revenue
- )
- invoice = self.invoice_model.create(
+ move = self.move_model.create(
{
- "journal_id": journal.id,
- "type": inv_type,
+ "move_type": move_type,
+ "invoice_date": fields.Date.today(),
"partner_id": partner.id,
- "account_id": arap_account.id,
"invoice_line_ids": [
- (
- 0,
- 0,
+ Command.create(
{
"name": "Test",
"price_unit": amount,
- "account_id": account.id,
+ "tax_ids": [],
},
)
],
}
)
- return invoice
+ return move
- def do_test_register_payment(self, invoices, expected_type, expected_diff):
+ def do_test_register_payment(
+ self, invoices, expected_type, expected_diff, fully_paid=False, netting=True
+ ):
"""Test create customer/supplier invoices. Then, select all invoices
and make neting payment. I expect:
- Payment Type (inbound or outbound) = expected_type
@@ -135,22 +63,31 @@ def do_test_register_payment(self, invoices, expected_type, expected_diff):
- Payment can link to all invoices
- All 4 invoices are in paid status"""
# Select all invoices, and register payment
- ctx = {"active_ids": invoices.ids, "active_model": "account.invoice"}
- view_id = "account_payment_netting.view_account_payment_from_invoices"
- with Form(self.register_payment_model.with_context(ctx), view=view_id) as f:
+ ctx = {
+ "active_ids": invoices.ids,
+ "active_model": "account.move",
+ "netting": netting,
+ }
+ with Form(self.register_payment_model.with_context(**ctx)) as f:
f.journal_id = self.bank_journal
+ f.amount = expected_diff
+ if fully_paid:
+ f.payment_difference_handling = "reconcile"
+ f.writeoff_account_id = self.account_expense
payment_wizard = f.save()
# Diff amount = expected_diff, payment_type = expected_type
self.assertEqual(payment_wizard.amount, expected_diff)
self.assertEqual(payment_wizard.payment_type, expected_type)
# Create payments
- res = payment_wizard.create_payments()
+ res = payment_wizard.action_create_payments()
payment = self.payment_model.browse(res["res_id"])
# Payment can link to all invoices
- self.assertEqual(set(payment.invoice_ids.ids), set(invoices.ids))
- invoices = self.invoice_model.browse(invoices.ids)
- # Test that all 4 invoices are paid
- self.assertEqual(list(set(invoices.mapped("state"))), ["paid"])
+ original_ids = (
+ payment.reconciled_bill_ids + payment.reconciled_invoice_ids
+ ).ids
+ self.assertEqual(set(original_ids), set(invoices.ids))
+ invoices = self.move_model.browse(invoices.ids)
+ return invoices
def test_1_payment_netting_neutral(self):
"""Test AR = AP"""
@@ -162,11 +99,21 @@ def test_1_payment_netting_neutral(self):
ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
- invoices.action_invoice_open()
- self.do_test_register_payment(invoices, "outbound", 0.0)
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 0.0)
+ # Test that all 4 invoices are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
def test_2_payment_netting_inbound(self):
- """Test AR > AP"""
+ """
+ Test AR > AP:
+ - Case1 AR > AP fully paid
+ - Case2 AR > AP fully paid payment difference
+ - Case3 AR > AP partially paid
+ - Case4 AR > AP not paid
+ """
+ # ================ Case1 AR > AP fully paid ================
# Create 2 AR Invoice, total amount = 200.0
ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 100.0)
ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 100.0)
@@ -175,11 +122,68 @@ def test_2_payment_netting_inbound(self):
ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 80.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
- invoices.action_invoice_open()
+ invoices.action_post()
self.do_test_register_payment(invoices, "inbound", 40.0)
+ # Test that all 4 invoices are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
+
+ # ================ Case2 AR > AP fully paid with payment difference ================
+ # Create 2 AR Invoice, total amount = 200.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ # Create 2 AP Invoice, total amount = 160.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 30.0, True)
+ # Test that all 4 invoices are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
+
+ # ================ Case3 AR > AP partial ================
+ # Create 2 AR Invoice, total amount = 200.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ # Create 2 AP Invoice, total amount = 160.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 30.0)
+ # Test that all 4 invoices are paid and partial
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertIn("paid", list(set(invoices.mapped("payment_state"))))
+ self.assertIn("partial", list(set(invoices.mapped("payment_state"))))
+
+ # ================ Case4 AR > AP not paid ================
+ # Create 2 AR Invoice, total amount = 200.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ # Create 2 AP Invoice, total amount = 160.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 80.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 0.0)
+ # Test that all 4 invoices are paid and partial
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertIn("paid", list(set(invoices.mapped("payment_state"))))
+ self.assertIn("partial", list(set(invoices.mapped("payment_state"))))
def test_3_payment_netting_outbound(self):
- """Test AR < AP"""
+ """
+ Test AR < AP:
+ - Case1 AR < AP fully paid
+ - Case2 AR < AP fully paid payment difference
+ - Case3 AR < AP partially paid
+ - Case4 AR < AP not paid
+ """
+ # ================ Case1 AR < AP fully paid ================
# Create 2 AR Invoice, total amount = 160.0
ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 80.0)
ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 80.0)
@@ -188,16 +192,89 @@ def test_3_payment_netting_outbound(self):
ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
- invoices.action_invoice_open()
+ invoices.action_post()
self.do_test_register_payment(invoices, "outbound", 40.0)
+ # Test that all 4 invoices are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
+
+ # ================ Case2 AR < AP fully paid with payment difference ================
+ # Create 2 AR Invoice, total amount = 160.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ # Create 2 AP Invoice, total amount = 200.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "outbound", 30.0, True)
+
+ # ================ Case3 AR < AP partially paid ================
+ # Create 2 AR Invoice, total amount = 160.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ # Create 2 AP Invoice, total amount = 200.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "outbound", 30.0)
+ # Test that all 4 invoices are paid and partial
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertIn("paid", list(set(invoices.mapped("payment_state"))))
+ self.assertIn("partial", list(set(invoices.mapped("payment_state"))))
+
+ # ================ Case4 AR < AP not paid ================
+ # Create 2 AR Invoice, total amount = 160.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 80.0)
+ # Create 2 AP Invoice, total amount = 200.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "outbound", 0.0)
+ # Test that all 4 invoices are paid and partial
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertIn("paid", list(set(invoices.mapped("payment_state"))))
+ self.assertIn("partial", list(set(invoices.mapped("payment_state"))))
def test_4_payment_netting_for_one_invoice(self):
"""Test only 1 customer invoice, should also pass test"""
invoices = self.create_invoice("out_invoice", self.partner1, 80.0)
- invoices.action_invoice_open()
+ invoices.action_post()
self.do_test_register_payment(invoices, "inbound", 80.0)
- def test_5_payment_netting_wrong_partner_exception(self):
+ def test_5_payment_netting_for_multi_bill(self):
+ """Test multi bills, should also pass test"""
+ # Create 2 AP Invoice, total amount = 200.0
+ ap_inv_p1_1 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ ap_inv_p1_2 = self.create_invoice("in_invoice", self.partner1, 100.0)
+ # Test Register Payment
+ invoices = ap_inv_p1_1 | ap_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "outbound", 200.0)
+ # Test that all 2 bills are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
+
+ def test_6_payment_netting_for_multi_invoices(self):
+ """Test multi invoices, should also pass test"""
+ # Create 2 AR Invoice, total amount = 200.0
+ ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ ar_inv_p1_2 = self.create_invoice("out_invoice", self.partner1, 100.0)
+ # Test Register Payment
+ invoices = ar_inv_p1_1 | ar_inv_p1_2
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 200.0)
+ # Test that all 2 invoices are paid
+ self.assertEqual(list(set(invoices.mapped("state"))), ["posted"])
+ self.assertEqual(list(set(invoices.mapped("payment_state"))), ["paid"])
+
+ def test_7_payment_netting_wrong_partner_exception(self):
"""Test when not invoices on same partner, show warning"""
# Create 2 AR Invoice, total amount = 160.0
ar_inv_p1_1 = self.create_invoice("out_invoice", self.partner1, 80.0)
@@ -206,7 +283,15 @@ def test_5_payment_netting_wrong_partner_exception(self):
ap_inv_p2 = self.create_invoice("in_invoice", self.partner2, 200.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p2
- invoices.action_invoice_open()
+ invoices.action_post()
with self.assertRaises(UserError) as e:
self.do_test_register_payment(invoices, "outbound", 40.0)
- self.assertEqual(e.exception.name, "All invoices must belong to same partner")
+ self.assertEqual(
+ e.exception.args[0], "All invoices must belong to same partner"
+ )
+
+ def test_8_payment_normal_process(self):
+ """Test only 1 customer invoice, should also pass test"""
+ invoices = self.create_invoice("out_invoice", self.partner1, 80.0)
+ invoices.action_post()
+ self.do_test_register_payment(invoices, "inbound", 80.0, netting=False)
diff --git a/account_payment_netting/views/account_invoice_view.xml b/account_payment_netting/views/account_invoice_view.xml
deleted file mode 100644
index 7a2b44b9b19..00000000000
--- a/account_payment_netting/views/account_invoice_view.xml
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
- Invoices for Netting
- account.invoice
- form
- tree,form
-
- [('state', '=', 'open')]
- {'type':'out_invoice', 'journal_type': 'sale'}
-
-
-
- Create a customer invoice
-
- Create invoices, register payments and keep track of the discussions with your customers.
-
-
-
-
-
-
- tree
-
-
-
-
-
-
- form
-
-
-
-
-
-
-
diff --git a/account_payment_netting/views/account_move_view.xml b/account_payment_netting/views/account_move_view.xml
new file mode 100644
index 00000000000..acd9e8fa733
--- /dev/null
+++ b/account_payment_netting/views/account_move_view.xml
@@ -0,0 +1,29 @@
+
+
+
+ Invoices for Netting
+ account.move
+ tree,form
+ [('state', '=', 'posted'), ('payment_state', "!=", "paid"), ("move_type", "in", ["in_invoice", "out_invoice"])]
+ {'default_move_type': 'out_invoice', 'netting': 1}
+
+
+
+ Create a customer invoice
+
+ Create invoices, register payments and keep track of the discussions with your customers.
+
+
+
+
+
+
+
diff --git a/account_payment_netting/views/account_payment_view.xml b/account_payment_netting/views/account_payment_view.xml
index fbc4210516c..b106529046b 100644
--- a/account_payment_netting/views/account_payment_view.xml
+++ b/account_payment_netting/views/account_payment_view.xml
@@ -1,24 +1,14 @@
-
- view.account.payment.from.invoices
- account.register.payments
-
+
+ account.payment.form
+ account.payment
+
-
-
-
-
- {'invisible': [('amount', '=', 0), ('netting', '=', False)]}
-
-
- {'invisible': [('netting', '=', True)]}
-
+
+
+
diff --git a/account_payment_netting/wizards/__init__.py b/account_payment_netting/wizards/__init__.py
new file mode 100644
index 00000000000..09e22cb07c6
--- /dev/null
+++ b/account_payment_netting/wizards/__init__.py
@@ -0,0 +1,3 @@
+# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
+
+from . import account_payment_register
diff --git a/account_payment_netting/wizards/account_payment_register.py b/account_payment_netting/wizards/account_payment_register.py
new file mode 100644
index 00000000000..892abebf7e0
--- /dev/null
+++ b/account_payment_netting/wizards/account_payment_register.py
@@ -0,0 +1,196 @@
+# Copyright 2023 Ecosoft Co., Ltd (https://ecosoft.co.th/)
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+
+from odoo import Command, _, api, fields, models
+from odoo.exceptions import UserError
+
+
+class AccountPaymentRegister(models.TransientModel):
+ _inherit = "account.payment.register"
+
+ netting = fields.Boolean(
+ help="Technical field, as user select invoice that are both AR and AP",
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ """
+ Override the default behavior in case of netting.
+ No account type checks are performed since netting allows
+ the selection of multiple account types.
+ """
+ netting = self.env.context.get("netting")
+ if netting and "line_ids" in fields_list:
+ fields_list.remove("line_ids")
+ res = super().default_get(fields_list)
+
+ if netting:
+ fields_list.append("line_ids")
+ # Retrieve moves to pay from the context.
+
+ if self._context.get("active_model") == "account.move":
+ lines = (
+ self.env["account.move"]
+ .browse(self._context.get("active_ids", []))
+ .line_ids
+ )
+ elif self._context.get("active_model") == "account.move.line":
+ lines = self.env["account.move.line"].browse(
+ self._context.get("active_ids", [])
+ )
+ else:
+ raise UserError(
+ _(
+ "The register payment wizard should only be called on "
+ "account.move or account.move.line records."
+ )
+ )
+
+ if "journal_id" in res and not self.env["account.journal"].browse(
+ res["journal_id"]
+ ).filtered_domain(
+ [
+ ("company_id", "=", lines.company_id.id),
+ ("type", "in", ("bank", "cash")),
+ ]
+ ):
+ # default can be inherited from the list view, should be computed instead
+ del res["journal_id"]
+
+ # Keep lines having a residual amount to pay.
+ available_lines = self.env["account.move.line"]
+ for line in lines:
+ if line.move_id.state != "posted":
+ raise UserError(
+ _("You can only register payment for posted journal entries.")
+ )
+
+ if line.account_type not in ("asset_receivable", "liability_payable"):
+ continue
+ if line.currency_id:
+ if line.currency_id.is_zero(line.amount_residual_currency):
+ continue
+ else:
+ if line.company_currency_id.is_zero(line.amount_residual):
+ continue
+ available_lines |= line
+
+ # Check.
+ if not available_lines:
+ raise UserError(
+ _(
+ "You can't register a payment because there is nothing left "
+ "to pay on the selected journal items."
+ )
+ )
+ if len(lines.company_id) > 1:
+ raise UserError(
+ _(
+ "You can't create payments for entries belonging "
+ "to different companies."
+ )
+ )
+ if len(lines.partner_id) > 1:
+ raise UserError(_("All invoices must belong to same partner"))
+
+ res.update(
+ {
+ "line_ids": [Command.set(available_lines.ids)],
+ "netting": True,
+ }
+ )
+
+ return res
+
+ @api.model
+ def _get_batch_communication(self, batch_result):
+ if self.netting:
+ labels = {
+ line.name or line.move_id.ref or line.move_id.name
+ for line in self.line_ids._origin
+ }
+ return ", ".join(sorted(labels))
+ return super()._get_batch_communication(batch_result)
+
+ def _create_payment_vals_from_wizard(self, batch_result):
+ payment_vals = super()._create_payment_vals_from_wizard(batch_result)
+ payment_vals["netting"] = self.netting
+ return payment_vals
+
+ @api.depends("line_ids")
+ def _compute_from_lines(self):
+ res = super()._compute_from_lines()
+ for wizard in self:
+ if not wizard.netting:
+ continue
+
+ batches = wizard._get_batches()
+ balance = sum([sum(batch["lines"].mapped("balance")) for batch in batches])
+ amount_currency = sum(
+ [sum(batch["lines"].mapped("amount_currency")) for batch in batches]
+ )
+ if balance < 0.0:
+ payment_type = "outbound"
+ else:
+ payment_type = "inbound"
+
+ for batch in batches:
+ if batch["payment_values"]["payment_type"] == payment_type:
+ batch_result = batch
+
+ wizard_values_from_batch = wizard._get_wizard_values_from_batch(
+ batch_result
+ )
+ wizard_values_from_batch["source_amount"] = abs(balance)
+ wizard_values_from_batch["source_amount_currency"] = abs(amount_currency)
+ wizard.update(wizard_values_from_batch)
+ wizard.can_edit_wizard = True
+ wizard.can_group_payments = True
+ return res
+
+ @api.depends("can_edit_wizard")
+ def _compute_group_payment(self):
+ res = super()._compute_group_payment()
+ for wizard in self:
+ if wizard.netting and wizard.can_edit_wizard:
+ wizard.group_payment = True
+ return res
+
+ def _get_total_amount_in_wizard_currency_to_full_reconcile(
+ self, batch_result, early_payment_discount=True
+ ):
+ self.ensure_one()
+ if self.netting:
+ all_batch = self._get_batches()
+ # Get all value except first value
+ filtered_list = all_batch[1:]
+ for batch in filtered_list:
+ batch_result["lines"] += batch["lines"]
+ return super()._get_total_amount_in_wizard_currency_to_full_reconcile(
+ batch_result, early_payment_discount
+ )
+
+ def _reconcile_netting(self, moves, payment_lines, domain):
+ for move in moves:
+ ml = move.line_ids.filtered_domain(domain)
+ ml |= payment_lines.filtered(lambda l: l.name == move.name)
+ ml.reconcile()
+
+ def _netting_reconcile_payment(self, to_process):
+ moveline_obj = self.env["account.move.line"]
+ domain = [
+ ("parent_state", "=", "posted"),
+ ("account_type", "in", ("asset_receivable", "liability_payable")),
+ ("reconciled", "=", False),
+ ]
+ moves = self.env["account.move"].browse(self.env.context.get("active_ids"))
+ payment_lines = moveline_obj.search(
+ domain + [("payment_id", "=", to_process[0]["payment"].id)]
+ )
+ self._reconcile_netting(moves, payment_lines, domain)
+
+ def _reconcile_payments(self, to_process, edit_mode=False):
+ if self.netting:
+ return self._netting_reconcile_payment(to_process)
+ return super()._reconcile_payments(to_process, edit_mode)
diff --git a/account_payment_netting/wizards/account_payment_register_views.xml b/account_payment_netting/wizards/account_payment_register_views.xml
new file mode 100644
index 00000000000..289b3938271
--- /dev/null
+++ b/account_payment_netting/wizards/account_payment_register_views.xml
@@ -0,0 +1,18 @@
+
+
+
+ account.payment.register.form
+ account.payment.register
+
+
+
+
+
+
+ {'invisible': [('can_group_payments', '=', False)], 'readonly': [('netting', '=', True)]}
+
+
+
+