diff --git a/book.py b/book.py
index 19cefd8..3493698 100644
--- a/book.py
+++ b/book.py
@@ -11,11 +11,10 @@ from trytond.transaction import Transaction
from trytond.pool import Pool
from trytond.report import Report
from decimal import Decimal
-from sql import Literal
from sql.aggregate import Sum
from sql.conditionals import Case, Coalesce
from sql.functions import CurrentDate
-from .model import order_name_hierarchical
+from .model import order_name_hierarchical, sub_ids_hierarchical, AnyInArray
STATES = {
@@ -104,14 +103,16 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
'invisible': STATES2['invisible'],
'required': ~STATES2['invisible'],
}, depends=DEPENDS2+['lines'])
- balance = fields.Function(fields.Numeric(string='Balance', readonly=True,
+ balance = fields.Function(fields.Numeric(string='Balance',
+ readonly=True, depends=['currency_digits'],
help='Balance of bookings to date',
- digits=(16, Eval('currency_digits', 2)),
- depends=['currency_digits']), 'on_change_with_balance')
+ digits=(16, Eval('currency_digits', 2))),
+ 'get_balance_cashbook', searcher='search_balance')
balance_all = fields.Function(fields.Numeric(string='Total balance',
- readonly=True, help='Balance of all bookings',
- digits=(16, Eval('currency_digits', 2)),
- depends=['currency_digits']), 'on_change_with_balance_all')
+ readonly=True, depends=['currency_digits'],
+ help='Balance of all bookings',
+ digits=(16, Eval('currency_digits', 2))),
+ 'get_balance_cashbook', searcher='search_balance')
balance_ref = fields.Function(fields.Numeric(string='Balance (Ref.)',
help='Balance in company currency',
@@ -119,7 +120,7 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={
'invisible': ~Bool(Eval('company_currency')),
}, depends=['company_currency_digits', 'company_currency']),
- 'on_change_with_balance_ref')
+ 'get_balance_cashbook')
company_currency = fields.Function(fields.Many2One(readonly=True,
string='Company Currency', states={'invisible': True},
model_name='currency.currency'),
@@ -269,63 +270,125 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
}
return recname
- def get_balance_of_cashbooks(self, date_limit=True):
- """ compute balance
+ @classmethod
+ def get_balance_of_cashbook_sql(cls):
+ """ sql for balance of a single cashbook
+ """
+ pool = Pool()
+ Line = pool.get('cashbook.line')
+ Book2 = pool.get('cashbook.book')
+ IrDate = pool.get('ir.date')
+ tab_line = Line.__table__()
+ tab_book = Book2.__table__()
+ context = Transaction().context
+
+ query_date = context.get('date', IrDate.today())
+ query = tab_book.join(tab_line,
+ condition=tab_book.id==tab_line.cashbook,
+ ).select(
+ tab_line.cashbook,
+ tab_book.currency,
+ Sum(Case(
+ (tab_line.date <= query_date, tab_line.credit - tab_line.debit),
+ else_ = Decimal('0.0'),
+ )).as_('balance'),
+ Sum(tab_line.credit - tab_line.debit).as_('balance_all'),
+ group_by=[tab_line.cashbook, tab_book.currency],
+ )
+ return (query, tab_line)
+
+ @staticmethod
+ def order_balance(tables):
+ """ order by balance
+ """
+ Book2 = Pool().get('cashbook.book')
+ (tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
+ table, _ = tables[None]
+
+ query = tab_book.select(tab_book.balance,
+ where=tab_book.cashbook==table.id,
+ )
+ return [query]
+
+ @staticmethod
+ def order_balance_all(tables):
+ """ order by balance-all
+ """
+ Book2 = Pool().get('cashbook.book')
+ (tab_book, tab2) = Book2.get_balance_of_cashbook_sql()
+ table, _ = tables[None]
+
+ query = tab_book.select(tab_book.balance_all,
+ where=tab_book.cashbook==table.id,
+ )
+ return [query]
+
+ @classmethod
+ def search_balance(cls, name, clause):
+ """ search in 'balance'
+ """
+ (tab_line, tab2) = cls.get_balance_of_cashbook_sql()
+ Operator = fields.SQL_OPERATORS[clause[1]]
+
+ query = tab_line.select(
+ tab_line.cashbook,
+ where=Operator(
+ getattr(tab_line, name), clause[2]),
+ )
+ return [('id', 'in', query)]
+
+ @classmethod
+ def get_balance_cashbook(cls, cashbooks, names):
+ """ get balance of cashbook
"""
pool = Pool()
Book2 = pool.get('cashbook.book')
- Book3 = pool.get('cashbook.book')
- Line = pool.get('cashbook.line')
Currency = pool.get('currency.currency')
- IrDate = pool.get('ir.date')
-
- tab_line = Line.__table__()
- tab_book = Book3.__table__()
+ Company = pool.get('company.company')
+ tab_book = Book2.__table__()
+ tab_comp = Company.__table__()
+ (tab_line, tab2) = cls.get_balance_of_cashbook_sql()
cursor = Transaction().connection.cursor()
- context = Transaction().context
- # select cashbook-lines from current cashbook and below
- query = [
- ('cashbook.id', 'in', Book2.search([
- ('parent', 'child_of', [self.id]),
- ], query=True)),
- ]
- if date_limit == True:
- dt1 = context.get('date', None)
- dt2 = IrDate.today()
- if not isinstance(dt1, type(dt2)):
- dt1 = dt2
- query.append(
- ('date', '<=', dt1)
- )
- line_query = Line.search(query, query=True)
+ result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
- # sum lines by currency
- bal_by_currency = line_query.join(tab_line,
- condition=tab_line.id==line_query.id,
- ).join(tab_book,
- condition=tab_book.id==tab_line.cashbook,
+ # query balances of cashbooks and sub-cashbooks
+ tab_subids = sub_ids_hierarchical('cashbook.book')
+ query = tab_book.join(tab_subids,
+ condition=tab_book.id==tab_subids.parent,
+ ).join(tab_comp,
+ condition=tab_book.company==tab_comp.id,
+ ).join(tab_line,
+ condition=tab_line.cashbook==AnyInArray(tab_subids.subids),
).select(
- Sum(tab_line.credit - tab_line.debit).as_('balance'),
- tab_book.currency,
- group_by=[tab_book.currency],
+ tab_book.id,
+ tab_book.currency.as_('to_currency'),
+ tab_line.currency.as_('from_currency'),
+ tab_comp.currency.as_('company_currency'),
+ Sum(tab_line.balance).as_('balance'),
+ Sum(tab_line.balance_all).as_('balance_all'),
+ group_by=[tab_book.id, tab_line.currency, tab_comp.currency],
+ where=tab_book.id.in_([x.id for x in cashbooks]),
)
+ cursor.execute(*query)
+ records = cursor.fetchall()
- if self.id:
- total = Decimal('0.0')
+ for record in records:
+ values = {
+ 'balance': Currency.compute(
+ record[2], record[4], record[1],
+ ),
+ 'balance_all': Currency.compute(
+ record[2], record[5], record[1],
+ ),
+ 'balance_ref': Currency.compute(
+ record[2], record[5], record[3],
+ ),
+ }
- cursor.execute(*bal_by_currency)
- balance_lines = cursor.fetchall()
-
- for line in balance_lines:
- (balance, id_currency) = line
-
- total += Currency.compute(
- Currency(id_currency), # from
- balance,
- self.currency, # to
- )
- return total
+ for name in names:
+ result[name][record[0]] += values[name]
+ return result
@fields.depends('btype')
def on_change_with_feature(self, name=None):
@@ -354,49 +417,6 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
if self.company.currency.id != self.currency.id:
return self.company.currency.id
- @fields.depends('id')
- def on_change_with_balance(self, name=None):
- """ compute balance until today
- """
- return self.get_balance_of_cashbooks()
-
- @fields.depends('id')
- def on_change_with_balance_all(self, name=None):
- """ compute balance of all bookings
- """
- return self.get_balance_of_cashbooks(date_limit=False)
-
- @fields.depends('company', 'currency', 'id', 'btype')
- def on_change_with_balance_ref(self, name=None):
- """ balance converted to company-currency
- """
- pool = Pool()
- Line = pool.get('cashbook.line')
- tab_line = Line.__table__()
- cursor = Transaction().connection.cursor()
-
- if self.btype is None:
- return None
-
- query = tab_line.select(
- Sum(tab_line.credit - tab_line.debit),
- where = tab_line.cashbook == self.id,
- )
-
- if self.id:
- cursor.execute(*query)
- result = cursor.fetchone()
- balance = Decimal('0.0')
- if result:
- if result[0] is not None:
- balance += result[0]
- if self.currency:
- return self.currency.compute(
- self.currency, # from
- balance,
- self.company.currency # to
- )
-
@classmethod
@ModelView.button
@Workflow.transition('open')
diff --git a/book.xml b/book.xml
index 810cf96..9b113ee 100644
--- a/book.xml
+++ b/book.xml
@@ -30,6 +30,7 @@ full copyright notices and license terms. -->
Cashbook
cashbook.book
+
diff --git a/model.py b/model.py
index 78bc435..e27d1ae 100644
--- a/model.py
+++ b/model.py
@@ -10,7 +10,15 @@ from sql import With, Literal
from sql.functions import Function
-class ArrayApppend(Function):
+class ArrayAgg(Function):
+ """input values, including nulls, concatenated into an array.
+ """
+ __slots__ = ()
+ _function = 'ARRAY_AGG'
+
+# end ArrayAgg
+
+class ArrayAppend(Function):
""" sql: array_append
"""
__slots__ = ()
@@ -41,6 +49,31 @@ class Array(Function):
# end Array
+def sub_ids_hierarchical(model_name):
+ """ get table with id and sub-ids
+ """
+ Model2 = Pool().get(model_name)
+ tab_mod = Model2.__table__()
+ tab_mod2 = Model2.__table__()
+
+ lines = With('parent', 'id', recursive=True)
+ lines.query = tab_mod.select(
+ tab_mod.id, tab_mod.id,
+ ) | tab_mod2.join(lines,
+ condition=lines.id==tab_mod2.parent,
+ ).select(
+ lines.parent, tab_mod2.id,
+ )
+ lines.query.all_ = True
+
+ query = lines.select(
+ lines.parent,
+ ArrayAgg(lines.id).as_('subids'),
+ group_by=[lines.parent],
+ with_ = [lines])
+ return query
+
+
def order_name_hierarchical(model_name, tables):
""" order by pos
a recursive sorting
@@ -58,7 +91,7 @@ def order_name_hierarchical(model_name, tables):
lines.query |= tab_mod2.join(lines,
condition=lines.id==tab_mod2.parent,
).select(
- tab_mod2.id, tab_mod2.name, ArrayApppend(lines.name_path, tab_mod2.name),
+ tab_mod2.id, tab_mod2.name, ArrayAppend(lines.name_path, tab_mod2.name),
)
lines.query.all_ = True
diff --git a/tests/test_book.py b/tests/test_book.py
index bc83d60..522c798 100644
--- a/tests/test_book.py
+++ b/tests/test_book.py
@@ -152,7 +152,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.currency.rate, Decimal('1.0'))
self.assertEqual(book.company_currency, None)
self.assertEqual(book.balance, Decimal('9.52'))
- self.assertEqual(book.balance_ref, None)
+ self.assertEqual(book.balance_ref, Decimal('9.52'))
self.assertEqual(len(book.lines), 0)
self.assertEqual(len(book.childs), 1)
@@ -225,6 +225,84 @@ class BookTestCase(ModuleTestCase):
Book.delete,
[book])
+ @with_transaction()
+ def test_book_check_search_and_sort(self):
+ """ create cashbook, check search on balance
+ """
+ pool = Pool()
+ Book = pool.get('cashbook.book')
+
+ types = self.prep_type()
+ category = self.prep_category(cattype='in')
+ company = self.prep_company()
+ party = self.prep_party()
+ books = Book.create([{
+ 'name': 'Book 1',
+ 'btype': types.id,
+ 'company': company.id,
+ 'currency': company.currency.id,
+ 'number_sequ': self.prep_sequence().id,
+ 'start_date': date(2022, 5, 1),
+ 'lines': [('create', [{
+ 'date': date(2022, 5, 1),
+ 'description': 'test 1',
+ 'category': category.id,
+ 'bookingtype': 'in',
+ 'amount': Decimal('10.0'),
+ 'party': party.id,
+ }])],
+ }, {
+ 'name': 'Book 2',
+ 'btype': types.id,
+ 'company': company.id,
+ 'currency': company.currency.id,
+ 'number_sequ': self.prep_sequence().id,
+ 'start_date': date(2022, 5, 1),
+ 'lines': [('create', [{
+ 'date': date(2022, 5, 1),
+ 'description': 'test 2',
+ 'category': category.id,
+ 'bookingtype': 'in',
+ 'amount': Decimal('100.0'),
+ 'party': party.id,
+ }])],
+ }])
+ self.assertEqual(len(books), 2)
+ self.assertEqual(books[0].name, 'Book 1')
+ self.assertEqual(books[0].btype.rec_name, 'CAS - Cash')
+ self.assertEqual(books[1].name, 'Book 2')
+ self.assertEqual(books[1].btype.rec_name, 'CAS - Cash')
+
+ self.assertEqual(
+ Book.search_count([('balance', '=', Decimal('10.0'))]),
+ 1)
+ self.assertEqual(
+ Book.search_count([('balance', '>', Decimal('5.0'))]),
+ 2)
+ self.assertEqual(
+ Book.search_count([('balance', '<', Decimal('5.0'))]),
+ 0)
+
+ books = Book.search([], order=[('balance', 'ASC')])
+ self.assertEqual(len(books), 2)
+ self.assertEqual(books[0].balance, Decimal('10.0'))
+ self.assertEqual(books[1].balance, Decimal('100.0'))
+
+ books = Book.search([], order=[('balance', 'DESC')])
+ self.assertEqual(len(books), 2)
+ self.assertEqual(books[0].balance, Decimal('100.0'))
+ self.assertEqual(books[1].balance, Decimal('10.0'))
+
+ self.assertEqual(
+ Book.search_count([('balance_all', '=', Decimal('10.0'))]),
+ 1)
+ self.assertEqual(
+ Book.search_count([('balance_all', '>', Decimal('5.0'))]),
+ 2)
+ self.assertEqual(
+ Book.search_count([('balance_all', '<', Decimal('5.0'))]),
+ 0)
+
@with_transaction()
def test_book_deny_btype_set_none(self):
""" create cashbook, add lines,