book: new field 'purchase_amount', optimize form

speedup: indexes, caching
This commit is contained in:
Frederik Jaeckel 2023-02-26 22:51:24 +01:00
parent 5e7552e589
commit 91b88a48e5
6 changed files with 151 additions and 50 deletions

133
book.py
View file

@ -15,12 +15,13 @@ from sql import Literal
from sql.functions import CurrentDate from sql.functions import CurrentDate
from sql.aggregate import Sum from sql.aggregate import Sum
from sql.conditionals import Case, Coalesce from sql.conditionals import Case, Coalesce
from trytond.modules.cashbook.model import CACHEKEY_CURRENCY
class Book(SymbolMixin, metaclass=PoolMeta): class Book(SymbolMixin, metaclass=PoolMeta):
__name__ = 'cashbook.book' __name__ = 'cashbook.book'
asset = fields.Many2One(string='Asset', asset = fields.Many2One(string='Asset', select=True,
model_name='investment.asset', ondelete='RESTRICT', model_name='investment.asset', ondelete='RESTRICT',
states={ states={
'required': Eval('feature', '') == 'asset', 'required': Eval('feature', '') == 'asset',
@ -47,7 +48,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
asset_uomcat = fields.Function(fields.Many2One(string='UOM Category', asset_uomcat = fields.Function(fields.Many2One(string='UOM Category',
readonly=True, model_name='product.uom.category', readonly=True, model_name='product.uom.category',
states={'invisible': True}), 'on_change_with_asset_uomcat') states={'invisible': True}), 'on_change_with_asset_uomcat')
quantity_uom = fields.Many2One(string='UOM', quantity_uom = fields.Many2One(string='UOM', select=True,
model_name='product.uom', ondelete='RESTRICT', model_name='product.uom', ondelete='RESTRICT',
domain=[ domain=[
('category.id', '=', Eval('asset_uomcat', -1)), ('category.id', '=', Eval('asset_uomcat', -1)),
@ -118,6 +119,13 @@ class Book(SymbolMixin, metaclass=PoolMeta):
states={ states={
'invisible': Eval('feature', '') != 'asset', 'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_asset_quantity') }, depends=['currency_digits', 'feature']), 'get_asset_quantity')
purchase_amount = fields.Function(fields.Numeric(string='Purchase Amount',
help='Total purchase amount, from shares and fees.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
'get_asset_quantity')
# yield # yield
yield_dividend_total = fields.Function(fields.Numeric(string='Dividend', yield_dividend_total = fields.Function(fields.Numeric(string='Dividend',
@ -200,14 +208,13 @@ class Book(SymbolMixin, metaclass=PoolMeta):
""" """
return 4 return 4
@fields.depends('yield_sales', 'yield_fee_total', 'yield_dividend_total', 'diff_amount') @fields.depends('yield_sales', 'yield_dividend_total', 'diff_amount')
def on_change_with_yield_balance(self, name=None): def on_change_with_yield_balance(self, name=None):
""" calculate yield total """ calculate yield total
fee is already contained in 'diff_amount'
""" """
sum_lst = [self.diff_amount, self.yield_dividend_total, self.yield_sales] sum_lst = [self.diff_amount, self.yield_dividend_total, self.yield_sales]
sum2 = sum([x for x in sum_lst if x is not None]) sum2 = sum([x for x in sum_lst if x is not None])
if self.yield_fee_total is not None:
sum2 -= self.yield_fee_total
return sum2 return sum2
@classmethod @classmethod
@ -250,9 +257,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
pool = Pool() pool = Pool()
CashBook = pool.get('cashbook.book') CashBook = pool.get('cashbook.book')
IrDate = pool.get('ir.date') IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor() cursor = Transaction().connection.cursor()
context = Transaction().context context = Transaction().context
result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names} result = {
x:{y.id: Decimal('0.0') for y in cashbooks}
for x in ['yield_fee_total', 'yield_dividend_total',
'yield_sales', 'yield_fee_12m', 'yield_dividend_12m',
'yield_sales_12m']}
def quantize_val(value, line): def quantize_val(value, line):
""" quantize... """ quantize...
@ -261,23 +273,50 @@ class Book(SymbolMixin, metaclass=PoolMeta):
value or Decimal('0.0') value or Decimal('0.0')
).quantize(Decimal(str(1/10**line.currency_digits))) ).quantize(Decimal(str(1/10**line.currency_digits)))
query_date = context.get('date', IrDate.today())
cache_keys = {
x.id: MemCache.get_key_by_record(
name = 'get_yield_data',
record = x,
query = [{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
'cachekey': CACHEKEY_CURRENCY % x.currency.id,
}, {
'model': 'investment.rate',
'query': [('asset.id', '=', x.asset.id)],
} if x.asset is not None else {}],
addkeys = [query_date.isoformat()])
for x in cashbooks
}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
# results for 'total' # results for 'total'
records_total = []
records_12m = []
if len(todo_cashbook) > 0:
(tab_book1, query_total) = cls.get_yield_data_sql() (tab_book1, query_total) = cls.get_yield_data_sql()
query_total.where &= tab_book1.id.in_([x.id for x in cashbooks]) query_total.where &= tab_book1.id.in_([x.id for x in todo_cashbook])
cursor.execute(*query_total) cursor.execute(*query_total)
records_total = cursor.fetchall() records_total = cursor.fetchall()
# results for 12 months # results for 12 months
query_date = context.get('date', IrDate.today())
(tab_book2, query_12m) = cls.get_yield_data_sql( (tab_book2, query_12m) = cls.get_yield_data_sql(
date_to = query_date, date_to = query_date,
date_from = query_date - timedelta(days=365), date_from = query_date - timedelta(days=365),
) )
query_12m.where &= tab_book2.id.in_([x.id for x in cashbooks]) query_12m.where &= tab_book2.id.in_([x.id for x in todo_cashbook])
cursor.execute(*query_12m) cursor.execute(*query_12m)
records_12m = cursor.fetchall() records_12m = cursor.fetchall()
result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
for record in records_total: for record in records_total:
book = CashBook(record[0]) book = CashBook(record[0])
result['yield_fee_total'][record[0]] = quantize_val(record[1], book) result['yield_fee_total'][record[0]] = quantize_val(record[1], book)
@ -290,7 +329,9 @@ class Book(SymbolMixin, metaclass=PoolMeta):
result['yield_dividend_12m'][record[0]] = quantize_val(record[2], book) result['yield_dividend_12m'][record[0]] = quantize_val(record[2], book)
result['yield_sales_12m'][record[0]] = quantize_val(record[3], book) result['yield_sales_12m'][record[0]] = quantize_val(record[3], book)
return result # store to cache
MemCache.store_result(cashbooks, cache_keys, result)
return {x:result[x] for x in names}
@classmethod @classmethod
def get_asset_quantity_sql(cls): def get_asset_quantity_sql(cls):
@ -309,6 +350,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
tab_asset = Asset.__table__() tab_asset = Asset.__table__()
(tab_rate, tab2) = Asset.get_rate_data_sql() (tab_rate, tab2) = Asset.get_rate_data_sql()
(tab_balance, tab2) = CBook.get_balance_of_cashbook_sql() (tab_balance, tab2) = CBook.get_balance_of_cashbook_sql()
(tab_line_yield, query_yield) = Line.get_yield_data_sql()
context = Transaction().context context = Transaction().context
query_date = context.get('qdate', CurrentDate()) query_date = context.get('qdate', CurrentDate())
@ -320,6 +362,8 @@ class Book(SymbolMixin, metaclass=PoolMeta):
condition=tab_book.currency==tab_cur.id, condition=tab_book.currency==tab_cur.id,
).join(tab_asset, ).join(tab_asset,
condition=tab_book.asset==tab_asset.id, condition=tab_book.asset==tab_asset.id,
).join(query_yield,
condition=query_yield.id==tab_line.id,
).join(tab_balance, ).join(tab_balance,
condition=tab_book.id==tab_balance.cashbook, condition=tab_book.id==tab_balance.cashbook,
type_ = 'LEFT OUTER', type_ = 'LEFT OUTER',
@ -341,6 +385,9 @@ class Book(SymbolMixin, metaclass=PoolMeta):
tab_book.quantity_uom, # 7 tab_book.quantity_uom, # 7
tab_asset.currency.as_('asset_currency'), #8 tab_asset.currency.as_('asset_currency'), #8
Coalesce(tab_balance.balance, Decimal('0.0')).as_('balance'), #9 Coalesce(tab_balance.balance, Decimal('0.0')).as_('balance'), #9
(
Sum(query_yield.fee) + tab_balance.balance
).as_('purchase_amount'), #10
group_by=[tab_book.id, tab_rate.rate, group_by=[tab_book.id, tab_rate.rate,
tab_book.currency, tab_cur.digits, tab_asset.uom, tab_book.currency, tab_cur.digits, tab_asset.uom,
tab_book.quantity_uom, tab_asset.currency, tab_book.quantity_uom, tab_asset.currency,
@ -357,6 +404,8 @@ class Book(SymbolMixin, metaclass=PoolMeta):
CBook = pool.get('cashbook.book') CBook = pool.get('cashbook.book')
Uom = pool.get('product.uom') Uom = pool.get('product.uom')
Currency = pool.get('currency.currency') Currency = pool.get('currency.currency')
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor() cursor = Transaction().connection.cursor()
context = Transaction().context context = Transaction().context
(query, tab_book) = cls.get_asset_quantity_sql() (query, tab_book) = cls.get_asset_quantity_sql()
@ -364,6 +413,33 @@ class Book(SymbolMixin, metaclass=PoolMeta):
company_currency = CBook.default_currency() company_currency = CBook.default_currency()
result = {x:{y.id: None for y in cashbooks} for x in names} result = {x:{y.id: None for y in cashbooks} for x in names}
cache_keys = {
x.id: MemCache.get_key_by_record(
name = 'get_asset_quantity',
record = x,
query = [{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
}, {
'model': 'investment.rate',
'query': [('asset.id', '=', x.asset.id)],
} if x.asset is not None else {}],
addkeys=[
str(company_currency),
str(context.get('qdate', IrDate.today()).toordinal()),
])
for x in cashbooks
}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
def values_from_record(rdata): def values_from_record(rdata):
""" compute values for record """ compute values for record
""" """
@ -389,16 +465,22 @@ class Book(SymbolMixin, metaclass=PoolMeta):
rdata[3] * rdata[1] / uom_factor, rdata[3] * rdata[1] / uom_factor,
company_currency if company_currency is not None else rdata[8], company_currency if company_currency is not None else rdata[8],
), ),
'diff_amount': current_value - rdata[9], 'diff_amount': current_value - rdata[10],
'diff_percent': ( 'diff_percent': (
Decimal('100.0') * current_value / \ Decimal('100.0') * current_value / \
rdata[9] - Decimal('100.0') rdata[10] - Decimal('100.0')
).quantize(Decimal(str(1/10**rdata[5]))) \ ).quantize(Decimal(str(1/10**rdata[5]))) \
if rdata[9] != Decimal('0.0') else None, if rdata[10] != Decimal('0.0') else None,
'current_rate': ( 'current_rate': (
current_value / rdata[1] current_value / rdata[1]
).quantize(Decimal(str(1/10**rdata[5]))) \ ).quantize(Decimal(str(1/10**rdata[5]))) \
if rdata[1] != Decimal('0.0') else None, if rdata[1] != Decimal('0.0') else None,
'purchase_amount': record[10].quantize(Decimal(str(1/10**rdata[5]))),
'purchase_amount_ref': Currency.compute(
rdata[4],
record[10],
company_currency if company_currency is not None else rdata[4],
),
}) })
result_cache = {} result_cache = {}
@ -416,7 +498,9 @@ class Book(SymbolMixin, metaclass=PoolMeta):
result_cache[book_id] = values result_cache[book_id] = values
result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref
for name in names: for name in values.keys():
if name not in result.keys():
result[name] = {}
result[name][book_id] = values[name] result[name][book_id] = values[name]
# add aggregated values of cashbooks without type # add aggregated values of cashbooks without type
@ -451,9 +535,9 @@ class Book(SymbolMixin, metaclass=PoolMeta):
('parent', 'child_of', [id_none]) ('parent', 'child_of', [id_none])
]) ])
values = {x:Decimal('0.0') for x in aggr_names+['balance_ref']} values = {x:Decimal('0.0') for x in aggr_names+['purchase_amount_ref']}
for record in records: for record in records:
for name in aggr_names+['balance_ref']: for name in aggr_names+['purchase_amount_ref']:
values[name] += \ values[name] += \
result_cache.get(record.id, {}).get(name, Decimal('0.0')) result_cache.get(record.id, {}).get(name, Decimal('0.0'))
@ -468,21 +552,24 @@ class Book(SymbolMixin, metaclass=PoolMeta):
values['diff_amount'] = Currency.compute( values['diff_amount'] = Currency.compute(
company_currency if company_currency is not None else cbook.currency, company_currency if company_currency is not None else cbook.currency,
values['current_value_ref'] - values['balance_ref'], values['current_value_ref'] - values['purchase_amount_ref'],
cbook.currency, cbook.currency,
) )
values['diff_percent'] = \ values['diff_percent'] = \
(Decimal('100.0') * values['current_value_ref'] / \ (Decimal('100.0') * values['current_value_ref'] / \
values['balance_ref'] - Decimal('100.0') values['purchase_amount_ref'] - Decimal('100.0')
).quantize( ).quantize(
Decimal(str(1/10**cbook.currency_digits)) Decimal(str(1/10**cbook.currency_digits))
) if values['balance_ref'] != Decimal('0.0') else None ) if values['purchase_amount_ref'] != Decimal('0.0') else None
for name in values.keys():
for name in queried_names: if name not in result.keys():
result[name] = {}
result[name][id_none] = values[name] result[name][id_none] = values[name]
return result # store to cache
MemCache.store_result(cashbooks, cache_keys, result)
return {x:result[x] for x in names}
@fields.depends('id') @fields.depends('id')
def on_change_with_show_performance(self, name=None): def on_change_with_show_performance(self, name=None):

View file

@ -198,10 +198,13 @@ msgctxt "help:cashbook.book,yield_balance:"
msgid "Total income: price gain + dividends + sales gains - fees" msgid "Total income: price gain + dividends + sales gains - fees"
msgstr "Gesamtertrag: Kursgewinn + Dividenden + Verkaufsgewinne - Gebühren" msgstr "Gesamtertrag: Kursgewinn + Dividenden + Verkaufsgewinne - Gebühren"
msgctxt "field:cashbook.book,purchase_amount:"
msgid "Purchase Amount"
msgstr "Kaufbetrag"
# Total return - share price gain plus dividend minus fees msgctxt "help:cashbook.book,purchase_amount:"
# Total return - price gain + sales gain + dividend - fees msgid "Total purchase amount, from shares and fees."
# Rendite insgesamt - Kursgewinn + Verkaufserfolg + Dividende - Gebühren msgstr "Kaufbetrag gesamt, aus Anteilen und Gebühren."
################## ##################

View file

@ -182,6 +182,14 @@ msgctxt "help:cashbook.book,yield_balance:"
msgid "Total income: price gain + dividends + sales gains - fees" msgid "Total income: price gain + dividends + sales gains - fees"
msgstr "Total income: price gain + dividends + sales gains - fees" msgstr "Total income: price gain + dividends + sales gains - fees"
msgctxt "field:cashbook.book,purchase_amount:"
msgid "Purchase Amount"
msgstr "Purchase Amount"
msgctxt "help:cashbook.book,purchase_amount:"
msgid "Total purchase amount, from shares and fees."
msgstr "Total purchase amount, from shares and fees."
msgctxt "field:cashbook.split,quantity_digits:" msgctxt "field:cashbook.split,quantity_digits:"
msgid "Digits" msgid "Digits"
msgstr "Digits" msgstr "Digits"

View file

@ -432,7 +432,7 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase):
self.assertEqual(book2.quantity_all, Decimal('20.0')) self.assertEqual(book2.quantity_all, Decimal('20.0'))
# usd --> eur: 1750 US$ / 1.05 = 1666.666 € # usd --> eur: 1750 US$ / 1.05 = 1666.666 €
# 1 ounce --> 20 gram: 1666.666 € * 20 / 28.3495 = 1175.7996 € # 1 ounce --> 20 gram: 1666.666 € * 20 / 28.3495 = 1175.7996 €
# bette we use 'Troy Ounce': 1 oz.tr. = 31.1034768 gram # better we use 'Troy Ounce': 1 oz.tr. = 31.1034768 gram
self.assertEqual(book2.current_value, Decimal('1175.80')) self.assertEqual(book2.current_value, Decimal('1175.80'))
self.assertEqual(book2.current_value_ref, Decimal('1175.80')) self.assertEqual(book2.current_value_ref, Decimal('1175.80'))
self.assertEqual(book2.diff_amount, Decimal('-74.20')) self.assertEqual(book2.diff_amount, Decimal('-74.20'))

View file

@ -160,7 +160,7 @@ class YieldTestCase(ModuleTestCase):
self.assertEqual(book_asset.yield_dividend_total, Decimal('23.5')) self.assertEqual(book_asset.yield_dividend_total, Decimal('23.5'))
self.assertEqual(book_asset.yield_fee_total, Decimal('4.0')) self.assertEqual(book_asset.yield_fee_total, Decimal('4.0'))
self.assertEqual(book_asset.yield_sales, Decimal('0.0')) self.assertEqual(book_asset.yield_sales, Decimal('0.0'))
self.assertEqual(book_asset.diff_amount, Decimal('-19.5')) self.assertEqual(book_asset.diff_amount, Decimal('-23.5'))
self.assertEqual(book_asset.yield_balance, Decimal('0.0')) self.assertEqual(book_asset.yield_balance, Decimal('0.0'))
@with_transaction() @with_transaction()

View file

@ -8,25 +8,29 @@ full copyright notices and license terms. -->
<separator colspan="4" name="current_value" string="Current valuation of the investment"/> <separator colspan="4" name="current_value" string="Current valuation of the investment"/>
</xpath> </xpath>
<xpath expr="/form/notebook/page[@name='balance']/field[@name='balance']" position="after"> <xpath expr="/form/notebook/page[@name='balance']/field[@name='balance']" position="after">
<label name="current_value"/> <label name="purchase_amount"/>
<field name="current_value" symbol="currency"/> <field name="purchase_amount" symbol="currency"/>
<label name="current_value_ref"/> <label name="yield_balance"/>
<field name="current_value_ref" symbol="company_currency"/> <field name="yield_balance" symbol="currency"/>
</xpath> </xpath>
<xpath expr="/form/notebook/page[@name='balance']/field[@name='balance_all']" position="after"> <xpath expr="/form/notebook/page[@name='balance']/field[@name='balance_all']" position="after">
<label name="current_value"/>
<field name="current_value" symbol="currency"/>
<label name="diff_amount"/> <label name="diff_amount"/>
<field name="diff_amount" symbol="currency"/> <field name="diff_amount" symbol="currency"/>
</xpath>
<xpath expr="/form/notebook/page[@name='balance']/field[@name='balance_ref']" position="after">
<label name="current_rate"/>
<field name="current_rate" symbol="asset_symbol"/>
<label name="diff_percent"/> <label name="diff_percent"/>
<group id="diff_percent" col="2"> <group id="diff_percent" col="2">
<field name="diff_percent" xexpand="0"/> <field name="diff_percent" xexpand="0"/>
<label name="diff_percent" xalign="0.0" string="%" xexpand="1"/> <label name="diff_percent" xalign="0.0" string="%" xexpand="1"/>
</group> </group>
</xpath>
<xpath expr="/form/notebook/page[@name='balance']/field[@name='balance_ref']" position="after"> <label name="current_value_ref" colspan="2" string=" "/>
<label name="yield_balance"/> <label name="current_value_ref"/>
<field name="yield_balance" symbol="currency"/> <field name="current_value_ref" symbol="company_currency"/>
<label name="current_rate"/>
<field name="current_rate" symbol="asset_symbol"/>
<newline/> <newline/>
<separator colspan="2" name="quantity" string="Quantity"/> <separator colspan="2" name="quantity" string="Quantity"/>
@ -52,7 +56,6 @@ full copyright notices and license terms. -->
<field name="yield_dividend_12m" symbol="currency"/> <field name="yield_dividend_12m" symbol="currency"/>
<label name="yield_fee_12m"/> <label name="yield_fee_12m"/>
<field name="yield_fee_12m" symbol="currency"/> <field name="yield_fee_12m" symbol="currency"/>
</xpath> </xpath>
<xpath expr="/form/notebook/page[@id='pggeneral']" position="after"> <xpath expr="/form/notebook/page[@id='pggeneral']" position="after">