cashbook: abfragen für Felder 'balance*' optimiert + suche + sortierung + test

cashbook-liste: verbergen von Kassenbücher ohne Type
This commit is contained in:
Frederik Jaeckel 2022-12-23 18:01:02 +01:00
parent 5f20001f72
commit e6baaa92f0
4 changed files with 233 additions and 101 deletions

216
book.py
View file

@ -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')

View file

@ -30,6 +30,7 @@ full copyright notices and license terms. -->
<record model="ir.action.act_window" id="act_book_view">
<field name="name">Cashbook</field>
<field name="res_model">cashbook.book</field>
<field name="domain" eval="[('btype', '!=', None)]" pyson="1"/>
</record>
<record model="ir.action.act_window.view" id="act_book_view-1">
<field name="sequence" eval="10"/>

View file

@ -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

View file

@ -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,