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.pool import Pool
from trytond.report import Report from trytond.report import Report
from decimal import Decimal from decimal import Decimal
from sql import Literal
from sql.aggregate import Sum from sql.aggregate import Sum
from sql.conditionals import Case, Coalesce from sql.conditionals import Case, Coalesce
from sql.functions import CurrentDate from sql.functions import CurrentDate
from .model import order_name_hierarchical from .model import order_name_hierarchical, sub_ids_hierarchical, AnyInArray
STATES = { STATES = {
@ -104,14 +103,16 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
'invisible': STATES2['invisible'], 'invisible': STATES2['invisible'],
'required': ~STATES2['invisible'], 'required': ~STATES2['invisible'],
}, depends=DEPENDS2+['lines']) }, 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', help='Balance of bookings to date',
digits=(16, Eval('currency_digits', 2)), digits=(16, Eval('currency_digits', 2))),
depends=['currency_digits']), 'on_change_with_balance') 'get_balance_cashbook', searcher='search_balance')
balance_all = fields.Function(fields.Numeric(string='Total balance', balance_all = fields.Function(fields.Numeric(string='Total balance',
readonly=True, help='Balance of all bookings', readonly=True, depends=['currency_digits'],
digits=(16, Eval('currency_digits', 2)), help='Balance of all bookings',
depends=['currency_digits']), 'on_change_with_balance_all') digits=(16, Eval('currency_digits', 2))),
'get_balance_cashbook', searcher='search_balance')
balance_ref = fields.Function(fields.Numeric(string='Balance (Ref.)', balance_ref = fields.Function(fields.Numeric(string='Balance (Ref.)',
help='Balance in company currency', help='Balance in company currency',
@ -119,7 +120,7 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
states={ states={
'invisible': ~Bool(Eval('company_currency')), 'invisible': ~Bool(Eval('company_currency')),
}, depends=['company_currency_digits', 'company_currency']), }, depends=['company_currency_digits', 'company_currency']),
'on_change_with_balance_ref') 'get_balance_cashbook')
company_currency = fields.Function(fields.Many2One(readonly=True, company_currency = fields.Function(fields.Many2One(readonly=True,
string='Company Currency', states={'invisible': True}, string='Company Currency', states={'invisible': True},
model_name='currency.currency'), model_name='currency.currency'),
@ -269,63 +270,125 @@ class Book(tree(separator='/'), Workflow, ModelSQL, ModelView):
} }
return recname return recname
def get_balance_of_cashbooks(self, date_limit=True): @classmethod
""" compute balance 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() pool = Pool()
Book2 = pool.get('cashbook.book') Book2 = pool.get('cashbook.book')
Book3 = pool.get('cashbook.book')
Line = pool.get('cashbook.line')
Currency = pool.get('currency.currency') Currency = pool.get('currency.currency')
IrDate = pool.get('ir.date') Company = pool.get('company.company')
tab_book = Book2.__table__()
tab_line = Line.__table__() tab_comp = Company.__table__()
tab_book = Book3.__table__() (tab_line, tab2) = cls.get_balance_of_cashbook_sql()
cursor = Transaction().connection.cursor() cursor = Transaction().connection.cursor()
context = Transaction().context
# select cashbook-lines from current cashbook and below result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
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)
# sum lines by currency # query balances of cashbooks and sub-cashbooks
bal_by_currency = line_query.join(tab_line, tab_subids = sub_ids_hierarchical('cashbook.book')
condition=tab_line.id==line_query.id, query = tab_book.join(tab_subids,
).join(tab_book, condition=tab_book.id==tab_subids.parent,
condition=tab_book.id==tab_line.cashbook, ).join(tab_comp,
condition=tab_book.company==tab_comp.id,
).join(tab_line,
condition=tab_line.cashbook==AnyInArray(tab_subids.subids),
).select( ).select(
Sum(tab_line.credit - tab_line.debit).as_('balance'), tab_book.id,
tab_book.currency, tab_book.currency.as_('to_currency'),
group_by=[tab_book.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: for record in records:
total = Decimal('0.0') 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) for name in names:
balance_lines = cursor.fetchall() result[name][record[0]] += values[name]
return result
for line in balance_lines:
(balance, id_currency) = line
total += Currency.compute(
Currency(id_currency), # from
balance,
self.currency, # to
)
return total
@fields.depends('btype') @fields.depends('btype')
def on_change_with_feature(self, name=None): 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: if self.company.currency.id != self.currency.id:
return self.company.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 @classmethod
@ModelView.button @ModelView.button
@Workflow.transition('open') @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"> <record model="ir.action.act_window" id="act_book_view">
<field name="name">Cashbook</field> <field name="name">Cashbook</field>
<field name="res_model">cashbook.book</field> <field name="res_model">cashbook.book</field>
<field name="domain" eval="[('btype', '!=', None)]" pyson="1"/>
</record> </record>
<record model="ir.action.act_window.view" id="act_book_view-1"> <record model="ir.action.act_window.view" id="act_book_view-1">
<field name="sequence" eval="10"/> <field name="sequence" eval="10"/>

View file

@ -10,7 +10,15 @@ from sql import With, Literal
from sql.functions import Function 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 """ sql: array_append
""" """
__slots__ = () __slots__ = ()
@ -41,6 +49,31 @@ class Array(Function):
# end Array # 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): def order_name_hierarchical(model_name, tables):
""" order by pos """ order by pos
a recursive sorting a recursive sorting
@ -58,7 +91,7 @@ def order_name_hierarchical(model_name, tables):
lines.query |= tab_mod2.join(lines, lines.query |= tab_mod2.join(lines,
condition=lines.id==tab_mod2.parent, condition=lines.id==tab_mod2.parent,
).select( ).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 lines.query.all_ = True

View file

@ -152,7 +152,7 @@ class BookTestCase(ModuleTestCase):
self.assertEqual(book.currency.rate, Decimal('1.0')) self.assertEqual(book.currency.rate, Decimal('1.0'))
self.assertEqual(book.company_currency, None) self.assertEqual(book.company_currency, None)
self.assertEqual(book.balance, Decimal('9.52')) 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.lines), 0)
self.assertEqual(len(book.childs), 1) self.assertEqual(len(book.childs), 1)
@ -225,6 +225,84 @@ class BookTestCase(ModuleTestCase):
Book.delete, Book.delete,
[book]) [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() @with_transaction()
def test_book_deny_btype_set_none(self): def test_book_deny_btype_set_none(self):
""" create cashbook, add lines, """ create cashbook, add lines,