[FIX] account_move_line_cumulated_balance: Absolute SQL strategy

As other partial ones doesn't fully work. The performance is OK enough
for a big amount of records (~2 s for 25k records on the same account),
so we accept the trade-off for having correct data.
This commit is contained in:
Pedro M. Baeza
2022-08-24 20:34:08 +02:00
parent db4cf76144
commit 0a07018ddd
5 changed files with 85 additions and 78 deletions

View File

@@ -58,8 +58,13 @@ Usage
Known issues / Roadmap
======================
* For v14, there's only need to migrate the field amount_currency_balance, as
the other one already exists in core.
* For v14, there's a similar feature, but it doesn't cumulate in an absolute way like
this one, and there's no cumulated in amount currency, so this module may be fully
preserved.
* There's no support for filtering at the same time by several entries state (posted,
not posted, cancel, etc).
* In the partner ledger, removing the group by partner won't make the cumulated balances
to be considered globally by account.
Bug Tracker
===========

View File

@@ -28,46 +28,50 @@ class AccountMoveLine(models.Model):
@api.model
def search_read(self, domain=None, fields=None, offset=0, limit=None, order=None):
def to_tuple(t):
return tuple(map(to_tuple, t)) if isinstance(t, (list, tuple)) else t
# Add the domain and order by in order to compute the cumulated
# balance in _compute_cumulated_balance
order = (order or self._order) + ", id desc"
# Add the significant domain in order to compute the cumulated balance in
# _compute_cumulated_balance.
cumulated_domain = []
for term in domain:
if isinstance(term, (tuple, list)) and term[0] == "move_id.state":
# TODO: Allow multiple state conditions joined by OR
cumulated_domain.append(("parent_state", term[1], term[2]))
elif term[0] == "full_reconcile_id":
cumulated_domain.append(tuple(term))
return super(
AccountMoveLine, self.with_context(order_cumulated_balance=order,),
AccountMoveLine,
self.with_context(domain_cumulated_balance=cumulated_domain),
).search_read(domain, fields, offset, limit, order)
@api.depends_context("order_cumulated_balance")
@api.depends_context("domain_cumulated_balance", "partner_ledger")
def _compute_cumulated_balance(self):
self.cumulated_balance = 0
self.cumulated_balance_currency = 0
order_cumulated_balance = (
self.env.context.get("order_cumulated_balance", self._order) + ", id"
)
order_string = ", ".join(
self._generate_order_by_inner(
self._table, order_cumulated_balance, "", reverse_direction=False,
)
)
query = sql.SQL(
"""SELECT account_move_line.id,
SUM(account_move_line.balance) OVER (
ORDER BY {order_by_clause}
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
),
SUM(account_move_line.amount_currency) OVER (
ORDER BY {order_by_clause}
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
)
FROM account_move_line
LEFT JOIN account_move on account_move_line.move_id = account_move.id
WHERE
account_move.state = 'posted'
"""
).format(order_by_clause=sql.SQL(order_string),)
self.env.cr.execute(query)
result = {r[0]: (r[1], r[2]) for r in self.env.cr.fetchall()}
query = self._where_calc(self.env.context.get("domain_cumulated_balance") or [])
_f, where_clause, where_clause_params = query.get_sql()
for record in self:
record.cumulated_balance = result[record.id][0]
record.cumulated_balance_currency = result[record.id][1]
query_args = where_clause_params + [
record.account_id.id,
record.company_id.id,
record.date,
record.date,
record.id,
]
# WHERE clause last line is set according order in view where this is used
query = sql.SQL(
"""
SELECT SUM(balance), SUM(amount_currency)
FROM account_move_line
WHERE {}
AND account_id = %s
AND company_id = %s
AND (date < %s OR (date=%s AND id <= %s))
"""
).format(sql.SQL(where_clause or "TRUE"))
if self.env.context.get("partner_ledger"):
# If showing partner ledger group by partner by default
query_args.append(record.partner_id.id)
query += sql.SQL("AND partner_id = %s")
self.env.cr.execute(query, tuple(query_args))
result = self.env.cr.fetchone()
record.cumulated_balance = result[0]
record.cumulated_balance_currency = result[1]

View File

@@ -1,2 +1,7 @@
* For v14, there's only need to migrate the field amount_currency_balance, as
the other one already exists in core.
* For v14, there's a similar feature, but it doesn't cumulate in an absolute way like
this one, and there's no cumulated in amount currency, so this module may be fully
preserved.
* There's no support for filtering at the same time by several entries state (posted,
not posted, cancel, etc).
* In the partner ledger, removing the group by partner won't make the cumulated balances
to be considered globally by account.

View File

@@ -1,31 +1,20 @@
# Copyright 2020 Tecnativa - Víctor Martínez
# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
from odoo.tests import Form
from odoo.tests import Form, tagged
from odoo.tests.common import SavepointCase
@tagged("post_install", "-at_install")
class TestAccount(SavepointCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.account_account_type_model = cls.env["account.account.type"]
cls.account_account_model = cls.env["account.account"]
cls.account_type_receivable = cls.account_account_type_model.create(
{"name": "Test Receivable", "type": "receivable", "internal_group": "asset"}
)
cls.account_type_regular = cls.account_account_type_model.create(
{"name": "Test Regular", "type": "other", "internal_group": "income"}
)
cls.account_receivable = cls.account_account_model.create(
{
"name": "Test Receivable",
"code": "TEST_AR",
"user_type_id": cls.account_type_receivable.id,
"reconcile": True,
}
)
cls.account_income = cls.account_account_model.create(
{
"name": "Test Income",
@@ -34,21 +23,7 @@ class TestAccount(SavepointCase):
"reconcile": False,
}
)
cls.partner = cls.env["res.partner"].create(
{
"name": "Test customer",
"customer_rank": 1,
"property_account_receivable_id": cls.account_receivable.id,
}
)
cls.journal = cls.env["account.journal"].create(
{
"name": "Test journal",
"type": "sale",
"code": "test-sale-jorunal",
"company_id": cls.env.company.id,
}
)
cls.partner = cls.env["res.partner"].create({"name": "Test customer"})
cls.product = cls.env["product.product"].create(
{"name": "Test product", "type": "service"}
)
@@ -58,27 +33,36 @@ class TestAccount(SavepointCase):
)
)
invoice.partner_id = cls.partner
invoice.journal_id = cls.journal
with invoice.invoice_line_ids.new() as line_form:
line_form.name = cls.product.name
line_form.product_id = cls.product
line_form.quantity = 1.0
line_form.price_unit = 10
line_form.account_id = cls.account_income
with invoice.invoice_line_ids.new() as line_form:
line_form.name = cls.product.name
line_form.product_id = cls.product
line_form.quantity = 2.0
line_form.price_unit = 10
line_form.account_id = cls.account_income
invoice = invoice.save()
invoice.action_post()
cls.invoice = invoice
def test_remove_invoice_error(self):
# Delete invoice while name isn't /
def test_basic_check(self):
lines = (
self.env["account.move.line"]
.with_context(order_cumulated_balance="date desc, id desc")
.with_context(domain_cumulated_balance=[("parent_state", "=", "posted")])
.search(
[("move_id.state", "=", "posted"), ("move_id", "=", self.invoice.id)]
[
("account_id", "=", self.account_income.id),
("move_id", "=", self.invoice.id),
],
order="date asc, id asc",
)
)
self.assertAlmostEqual(lines[0].cumulated_balance, 0)
self.assertAlmostEqual(lines[0].cumulated_balance, -10)
# TODO: Test other currency balances
self.assertAlmostEqual(lines[0].cumulated_balance_currency, 0)
self.assertAlmostEqual(lines[1].cumulated_balance, 10)
self.assertAlmostEqual(lines[1].cumulated_balance, -30)
self.assertAlmostEqual(lines[1].cumulated_balance_currency, 0)

View File

@@ -6,7 +6,7 @@
<field name="inherit_id" ref="account.view_move_line_tree_grouped_general" />
<field name="arch" type="xml">
<xpath expr="/tree" position="attributes">
<attribute name="default_order">date asc</attribute>
<attribute name="default_order">date asc, id asc</attribute>
</xpath>
<field name="amount_currency" position="before">
@@ -26,9 +26,9 @@
<field name="inherit_id" ref="account.view_move_line_tree_grouped_partner" />
<field name="arch" type="xml">
<xpath expr="/tree" position="attributes">
<attribute name="default_order">date asc</attribute>
<attribute name="default_order">date asc, id asc</attribute>
</xpath>
<field name="credit" position="after">
<field name="amount_currency" position="before">
<field name="cumulated_balance" optional="show" />
</field>
<field name="amount_currency" position="after">
@@ -40,4 +40,13 @@
</field>
</field>
</record>
<!-- Inject a context key for putting cumulated balance grouped by partner -->
<record
id="account.action_account_moves_ledger_partner"
model="ir.actions.act_window"
>
<field
name="context"
>{'partner_ledger': 1, 'journal_type':'general', 'search_default_group_by_partner': 1, 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
</record>
</odoo>