cashbook: abfragen für Felder 'balance*' optimiert + suche + sortierung + test
cashbook-liste: verbergen von Kassenbücher ohne Type
This commit is contained in:
parent
5f20001f72
commit
e6baaa92f0
4 changed files with 233 additions and 101 deletions
216
book.py
216
book.py
|
@ -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')
|
||||||
|
|
1
book.xml
1
book.xml
|
@ -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"/>
|
||||||
|
|
37
model.py
37
model.py
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue