diff --git a/modules/account/CHANGELOG b/modules/account/CHANGELOG
index 10acfa05dd..bbba5e2941 100644
--- a/modules/account/CHANGELOG
+++ b/modules/account/CHANGELOG
@@ -1,3 +1,4 @@
+* Add receivable/payable to company
Version 6.8.0 - 2023-05-01
--------------------------
diff --git a/modules/account/company.py b/modules/account/company.py
index 054ac8d252..27b090d4cc 100644
--- a/modules/account/company.py
+++ b/modules/account/company.py
@@ -1,14 +1,136 @@
# 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 decimal import Decimal
+
+from sql import Literal, Null
+from sql.aggregate import Sum
+from sql.conditionals import Coalesce
+
from trytond.i18n import gettext
+from trytond.model import fields
from trytond.model.exceptions import AccessError
+from trytond.modules.currency.fields import Monetary
from trytond.pool import Pool, PoolMeta
+from trytond.tools import grouped_slice, reduce_ids
from trytond.transaction import Transaction
class Company(metaclass=PoolMeta):
__name__ = 'company.company'
+ receivable = fields.Function(Monetary(
+ "Receivable", currency='currency', digits='currency'),
+ 'get_receivable_payable', searcher='search_receivable_payable')
+ payable = fields.Function(Monetary(
+ "Payable", currency='currency', digits='currency'),
+ 'get_receivable_payable', searcher='search_receivable_payable')
+ receivable_today = fields.Function(Monetary(
+ "Receivable Today", currency='currency', digits='currency'),
+ 'get_receivable_payable', searcher='search_receivable_payable')
+ payable_today = fields.Function(Monetary(
+ "Payable Today", currency='currency', digits='currency'),
+ 'get_receivable_payable', searcher='search_receivable_payable')
+
+ @classmethod
+ def get_receivable_payable(cls, companies, names):
+ amounts = {}
+ pool = Pool()
+ MoveLine = pool.get('account.move.line')
+ Account = pool.get('account.account')
+ AccountType = pool.get('account.account.type')
+ Date = pool.get('ir.date')
+ cursor = Transaction().connection.cursor()
+
+ line = MoveLine.__table__()
+ account = Account.__table__()
+ account_type = AccountType.__table__()
+
+ for name in names:
+ assert name in {
+ 'receivable', 'payable', 'receivable_today', 'payable_today'}
+ amounts[name] = dict.fromkeys(map(int, companies), Decimal(0))
+
+ exp = {
+ c.id: Decimal(str(10.0 ** -c.currency.digits)) for c in companies}
+ today = Date.today()
+
+ amount = Sum(Coalesce(line.debit, 0) - Coalesce(line.credit, 0))
+ for name in names:
+ if name.endswith('_today'):
+ code = name[:-len('_today')]
+ today_where = (
+ (line.maturity_date <= today)
+ | (line.maturity_date == Null))
+ else:
+ code = name
+ today_where = Literal(True)
+ for sub_companies in grouped_slice(companies):
+ sub_ids = [p.id for p in sub_companies]
+ company_where = reduce_ids(account.company, sub_ids)
+ cursor.execute(*line
+ .join(account,
+ condition=account.id == line.account)
+ .join(account_type,
+ condition=account.type == account_type.id)
+ .select(account.company, amount,
+ where=(getattr(account_type, code)
+ & (line.reconciliation == Null)
+ & company_where
+ & today_where),
+ group_by=account.company))
+ for company_id, value in cursor:
+ # SQLite uses float for SUM
+ if not isinstance(value, Decimal):
+ value = Decimal(str(value))
+ amounts[name][company_id] = value.quantize(exp[company_id])
+ 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')
+ Date = pool.get('ir.date')
+
+ line = MoveLine.__table__()
+ account = Account.__table__()
+ account_type = AccountType.__table__()
+
+ assert name in {
+ 'receivable', 'payable', 'receivable_today', 'payable_today'}
+ _, operator, value = clause
+
+ today = Date.today()
+
+ if name.endswith('_today'):
+ code = name[:-len('_today')]
+ today_query = ((line.maturity_date <= today)
+ | (line.maturity_date == Null))
+ else:
+ code = name
+ today_query = Literal(True)
+
+ 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(account.company,
+ where=(getattr(account_type, code)
+ & (line.reconciliation == Null)
+ & today_query),
+ group_by=account.company,
+ having=Operator(amount, value)))
+ return [('id', 'in', query)]
+
@classmethod
def write(cls, *args):
pool = Pool()
diff --git a/modules/account/company.xml b/modules/account/company.xml
new file mode 100644
index 0000000000..cd99ed7157
--- /dev/null
+++ b/modules/account/company.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+ company.company
+
+ company_list
+
+
+
+ company.company
+
+ company_form
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/account/tests/test_module.py b/modules/account/tests/test_module.py
index 5ce6b9da79..f687e6d3d4 100644
--- a/modules/account/tests/test_module.py
+++ b/modules/account/tests/test_module.py
@@ -1330,6 +1330,7 @@ class AccountTestCase(
def test_receivable_payable(self):
'Test party receivable payable'
pool = Pool()
+ Company = pool.get('company.company')
Party = pool.get('party.party')
FiscalYear = pool.get('account.fiscalyear')
Journal = pool.get('account.journal')
@@ -1399,27 +1400,34 @@ class AccountTestCase(
def check_fields():
party_test = Party(party.id)
+ company_test = Company(company.id)
- for field, value in [('receivable', Decimal('300')),
- ('receivable_today', Decimal('100')),
- ('payable', Decimal('90')),
- ('payable_today', Decimal('30')),
+ for record, Model in [
+ (party_test, Party),
+ (company_test, Company),
]:
- msg = 'field: %s, value: %s' % (field, value)
- self.assertEqual(
- getattr(party_test, field), value, msg=msg)
- self.assertEqual(
- Party.search([(field, '=', value)]),
- [party_test], msg=msg)
- self.assertEqual(
- Party.search([(field, 'in', [value])]),
- [party_test], msg=msg)
- self.assertEqual(
- Party.search([(field, '!=', value)]),
- [], msg=msg)
- self.assertEqual(
- Party.search([(field, 'not in', [value])]),
- [], msg=msg)
+ for field, value in [
+ ('receivable', Decimal('300')),
+ ('receivable_today', Decimal('100')),
+ ('payable', Decimal('90')),
+ ('payable_today', Decimal('30')),
+ ]:
+ with self.subTest(
+ record=record, field=field, value=value):
+ self.assertEqual(
+ getattr(record, field), value)
+ self.assertEqual(
+ Model.search([(field, '=', value)]),
+ [record])
+ self.assertEqual(
+ Model.search([(field, 'in', [value])]),
+ [record])
+ self.assertEqual(
+ Model.search([(field, '!=', value)]),
+ [])
+ self.assertEqual(
+ Model.search([(field, 'not in', [value])]),
+ [])
check_fields()
close_fiscalyear(fiscalyear)
diff --git a/modules/account/tryton.cfg b/modules/account/tryton.cfg
index 44be940fce..fd43eeb8eb 100644
--- a/modules/account/tryton.cfg
+++ b/modules/account/tryton.cfg
@@ -9,6 +9,7 @@ depends:
xml:
account.xml
configuration.xml
+ company.xml
fiscalyear.xml
period.xml
journal.xml
diff --git a/modules/account/view/company_form.xml b/modules/account/view/company_form.xml
new file mode 100644
index 0000000000..086f7ff6c2
--- /dev/null
+++ b/modules/account/view/company_form.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/modules/account/view/company_list.xml b/modules/account/view/company_list.xml
new file mode 100644
index 0000000000..3fdf5b18cd
--- /dev/null
+++ b/modules/account/view/company_list.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
diff --git a/modules/account_invoice/view/company_form.xml b/modules/account_invoice/view/company_form.xml
index 2afb47f355..3efca6c3d4 100644
--- a/modules/account_invoice/view/company_form.xml
+++ b/modules/account_invoice/view/company_form.xml
@@ -2,12 +2,11 @@
-
-
-
-
-
-
-
+
+
+
+
+
+