From 47b79131dc2304d5b28c6062f95b2844c0cc63d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sat, 13 Sep 2025 11:22:40 +0200 Subject: [PATCH] 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 --- modules/account/party.py | 187 +++++++++--------- .../tests/scenario_move_line_delegate.rst | 4 +- .../scenario_invoice_alternative_payee.rst | 8 +- 3 files changed, 104 insertions(+), 95 deletions(-) diff --git a/modules/account/party.py b/modules/account/party.py index 32a46edc85..0594d29967 100644 --- a/modules/account/party.py +++ b/modules/account/party.py @@ -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.modules.party.exceptions import EraseError 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 @@ class Party(CompanyMultiValueMixin, metaclass=PoolMeta): 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__() + 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() + + 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: - 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) + columns.append(Coalesce(expressions[name], Decimal()).as_(name)) - user = User(Transaction().user) + 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 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() + return amounts - amount = Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0)) - 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 + 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() - 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') + MoveLine = pool.get('account.move.line') - 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() - - 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)) + return [('id', '=', -1)] + 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 --git a/modules/account/tests/scenario_move_line_delegate.rst b/modules/account/tests/scenario_move_line_delegate.rst index 39a4de05e5..4a14de40ec 100644 --- a/modules/account/tests/scenario_move_line_delegate.rst +++ b/modules/account/tests/scenario_move_line_delegate.rst @@ -77,7 +77,7 @@ Create lines to delegate:: Decimal('100.00') >>> party2.reload() >>> party2.receivable - Decimal('0.0') + Decimal('0') Delegate lines:: @@ -92,7 +92,7 @@ Delegate lines:: Decimal('100.00') >>> party1.reload() >>> party1.receivable - Decimal('0.0') + Decimal('0') >>> party2.reload() >>> party2.receivable Decimal('100.00') diff --git a/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst b/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst index 20f4529d96..1be1fc058f 100644 --- a/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst +++ b/modules/account_invoice/tests/scenario_invoice_alternative_payee.rst @@ -73,13 +73,13 @@ Post customer invoice:: >>> 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 @@ Set another payee:: >>> 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')