details:   https://code.tryton.org/tryton/commit/00d4a20bf151
branch:    default
user:      Cédric Krier <[email protected]>
date:      Sat Sep 13 11:22:40 2025 +0200
description:
        Fallback to effective date to filter line for party payable/receivable 
today

        This implements the same behavior as the payable/receivable date in
        7caa97a96b95 ensuring that the cumulate balance is the same as the 
today field.

        Also it allows to customize the date considered as today from the 
context date
        key or _datetime (when browsing history).

        Closes #14228
diffstat:

 modules/account/party.py                                             |  191 
+++++----
 modules/account/tests/scenario_move_line_delegate.rst                |    4 +-
 modules/account_invoice/tests/scenario_invoice_alternative_payee.rst |    8 +-
 3 files changed, 106 insertions(+), 97 deletions(-)

diffs (287 lines):

diff -r 084495b64f58 -r 00d4a20bf151 modules/account/party.py
--- a/modules/account/party.py  Wed Sep 10 09:50:17 2025 +0200
+++ b/modules/account/party.py  Sat Sep 13 11:22:40 2025 +0200
@@ -1,5 +1,6 @@
 # This file is part of Tryton.  The COPYRIGHT file at the top level of
 # this repository contains the full copyright notices and license terms.
+from collections import defaultdict
 from decimal import Decimal
 
 from sql import Literal, Null
@@ -16,6 +17,7 @@
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval, If
 from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
+from trytond.tools import timezone as tz
 from trytond.transaction import Transaction
 
 from .exceptions import AccountMissing
@@ -117,117 +119,124 @@
             return company.currency
 
     @classmethod
-    def get_receivable_payable(cls, parties, names):
-        '''
-        Function to compute receivable, payable (today or not) for party ids.
-        '''
-        result = {}
+    def _receivable_payable_query(cls, names, company, parties=None):
         pool = Pool()
-        MoveLine = pool.get('account.move.line')
         Account = pool.get('account.account')
         AccountType = pool.get('account.account.type')
-        User = pool.get('res.user')
         Date = pool.get('ir.date')
-        cursor = Transaction().connection.cursor()
+        Move = pool.get('account.move')
+        MoveLine = pool.get('account.move.line')
 
-        line = MoveLine.__table__()
+        transaction = Transaction()
+        context = transaction.context
         account = Account.__table__()
         account_type = AccountType.__table__()
+        move = Move.__table__()
+        move_line = MoveLine.__table__()
 
-        for name in names:
-            if name not in ('receivable', 'payable',
-                    'receivable_today', 'payable_today'):
-                raise Exception('Bad argument')
-            result[name] = dict((p.id, Decimal('0.0')) for p in parties)
+        if date := context.get('date'):
+            today = date
+        elif datetime := context.get('_datetime'):
+            if company.timezone:
+                timezone = tz.ZoneInfo(company.timezone)
+                datetime = datetime.astimezone(timezone)
+            today = datetime.date()
+        else:
+            with transaction.set_context(company=company.id):
+                today = Date.today()
 
-        user = User(Transaction().user)
-        if not user.company:
-            return result
-        company_id = user.company.id
-        exp = Decimal(str(10.0 ** -user.company.currency.digits))
-        with Transaction().set_context(company=company_id):
-            today = Date.today()
-
-        amount = Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0))
+        date = Coalesce(move_line.maturity_date, move.date)
+        expressions = {
+            'receivable': Sum(
+                move_line.debit - move_line.credit,
+                filter_=account_type.receivable),
+            'receivable_today': Sum(
+                move_line.debit - move_line.credit,
+                filter_=account_type.receivable & (date <= today)),
+            'payable': Sum(
+                move_line.debit - move_line.credit,
+                filter_=account_type.payable),
+            'payable_today': Sum(
+                move_line.debit - move_line.credit,
+                filter_=account_type.payable & (date <= today)),
+            }
+        columns = []
         for name in names:
-            code = name
-            today_where = Literal(True)
-            if name in ('receivable_today', 'payable_today'):
-                code = name[:-6]
-                today_where = ((line.maturity_date <= today)
-                    | (line.maturity_date == Null))
-            for sub_parties in grouped_slice(parties):
-                sub_ids = [p.id for p in sub_parties]
-                party_where = reduce_ids(line.party, sub_ids)
-                query = (line.join(account,
-                        condition=account.id == line.account
-                        ).join(account_type,
-                        condition=account.type == account_type.id
-                        ).select(line.party, amount.as_(name),
-                        where=(getattr(account_type, code)
-                            & (line.reconciliation == Null)
-                            & (account.company == company_id)
-                            & party_where
-                            & today_where),
-                        group_by=line.party))
-                if backend.name == 'sqlite':
-                    sqlite_apply_types(query, [None, 'NUMERIC'])
-                cursor.execute(*query)
-                for party, value in cursor:
-                    result[name][party] = value.quantize(exp)
-        return result
+            columns.append(Coalesce(expressions[name], Decimal()).as_(name))
+
+        if parties is not None:
+            party_where = reduce_ids(move_line.party, [p.id for p in parties])
+        else:
+            party_where = Literal(True)
+
+        query = (
+            move_line
+            .join(move, condition=move_line.move == move.id)
+            .join(account, condition=move_line.account == account.id)
+            .join(account_type, condition=account.type == account_type.id)
+            .select(
+                move_line.party.as_('party'),
+                *columns,
+                where=(
+                    (move_line.reconciliation == Null)
+                    & (account.company == company.id)
+                    & party_where),
+                group_by=move_line.party))
+        return query
+
+    @classmethod
+    def get_receivable_payable(cls, parties, names):
+        pool = Pool()
+        User = pool.get('res.user')
+
+        transaction = Transaction()
+        cursor = transaction.connection.cursor()
+        amounts = {name: defaultdict(Decimal) for name in names}
+
+        user = User(transaction.user)
+        if not user.company:
+            return amounts
+
+        company = user.company
+        currency = company.currency
+        for sub_parties in grouped_slice(parties):
+            query = cls._receivable_payable_query(names, company, sub_parties)
+            if backend.name == 'sqlite':
+                sqlite_apply_types(query, [None] + ['NUMERIC'] * 4)
+            cursor.execute(*query)
+            for party_id, *values in cursor:
+                for name, value in zip(names, values):
+                    amounts[name][party_id] = currency.round(value)
+        return amounts
 
     @classmethod
     def search_receivable_payable(cls, name, clause):
         pool = Pool()
+        User = pool.get('res.user')
         MoveLine = pool.get('account.move.line')
-        Account = pool.get('account.account')
-        AccountType = pool.get('account.account.type')
-        User = pool.get('res.user')
-        Date = pool.get('ir.date')
-
-        line = MoveLine.__table__()
-        account = Account.__table__()
-        account_type = AccountType.__table__()
-
-        if name not in ('receivable', 'payable',
-                'receivable_today', 'payable_today'):
-            raise Exception('Bad argument')
-        _, operator, value = clause
 
-        user = User(Transaction().user)
+        transaction = Transaction()
+        user = User(transaction.user)
         if not user.company:
-            return []
-        company_id = user.company.id
-        with Transaction().set_context(company=company_id):
-            today = Date.today()
+            return [('id', '=', -1)]
 
-        code = name
-        today_query = Literal(True)
-        if name in ('receivable_today', 'payable_today'):
-            code = name[:-6]
-            today_query = ((line.maturity_date <= today)
-                | (line.maturity_date == Null))
-
+        company = user.company
+        query = cls._receivable_payable_query([name], company)
+        columns = list(query.columns)
+        amount = columns.pop(1).expression
+        _, operator, value = clause
         Operator = fields.SQL_OPERATORS[operator]
 
-        # Need to cast numeric for sqlite
-        cast_ = MoveLine.debit.sql_cast
-        amount = cast_(Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0)))
-        if operator in {'in', 'not in'}:
-            value = [cast_(Literal(Decimal(v or 0))) for v in value]
-        else:
-            value = cast_(Literal(Decimal(value or 0)))
-        query = (line.join(account, condition=account.id == line.account
-                ).join(account_type, condition=account.type == account_type.id
-                ).select(line.party,
-                where=(getattr(account_type, code)
-                    & (line.party != Null)
-                    & (line.reconciliation == Null)
-                    & (account.company == company_id)
-                    & today_query),
-                group_by=line.party,
-                having=Operator(amount, value)))
+        if backend.name == 'sqlite':
+            cast_ = MoveLine.debit.sql_cast
+            if operator in {'in', 'not in'}:
+                value = [cast_(Literal(Decimal(v or 0))) for v in value]
+            else:
+                value = cast_(Literal(Decimal(value or 0)))
+            amount = cast_(amount)
+
+        query.having = Operator(amount, value)
+        query.columns = columns
         return [('id', 'in', query)]
 
     @property
diff -r 084495b64f58 -r 00d4a20bf151 
modules/account/tests/scenario_move_line_delegate.rst
--- a/modules/account/tests/scenario_move_line_delegate.rst     Wed Sep 10 
09:50:17 2025 +0200
+++ b/modules/account/tests/scenario_move_line_delegate.rst     Sat Sep 13 
11:22:40 2025 +0200
@@ -77,7 +77,7 @@
     Decimal('100.00')
     >>> party2.reload()
     >>> party2.receivable
-    Decimal('0.0')
+    Decimal('0')
 
 Delegate lines::
 
@@ -92,7 +92,7 @@
     Decimal('100.00')
     >>> party1.reload()
     >>> party1.receivable
-    Decimal('0.0')
+    Decimal('0')
     >>> party2.reload()
     >>> party2.receivable
     Decimal('100.00')
diff -r 084495b64f58 -r 00d4a20bf151 
modules/account_invoice/tests/scenario_invoice_alternative_payee.rst
--- a/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst      
Wed Sep 10 09:50:17 2025 +0200
+++ b/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst      
Sat Sep 13 11:22:40 2025 +0200
@@ -73,13 +73,13 @@
 
     >>> party1.reload()
     >>> party1.receivable
-    Decimal('0.0')
+    Decimal('0')
     >>> party2.reload()
     >>> party2.receivable
     Decimal('10.00')
     >>> party3.reload()
     >>> party3.receivable
-    Decimal('0.0')
+    Decimal('0')
 
 Copying invoice with single alternative payee is kept::
 
@@ -104,10 +104,10 @@
 
     >>> party1.reload()
     >>> party1.receivable
-    Decimal('0.0')
+    Decimal('0')
     >>> party2.reload()
     >>> party2.receivable
-    Decimal('0.0')
+    Decimal('0')
     >>> party3.reload()
     >>> party3.receivable
     Decimal('10.00')

Reply via email to