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,