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
This commit is contained in:
Cédric Krier
2025-09-13 11:22:40 +02:00
parent b519f15a05
commit 47b79131dc
3 changed files with 104 additions and 95 deletions
+98 -89
View File
@@ -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
@@ -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')
@@ -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')