add worker-based precalculation of cashbook-values

This commit is contained in:
Frederik Jaeckel 2023-12-29 23:07:39 +01:00
parent 0bc55b8076
commit 246f035417
5 changed files with 216 additions and 17 deletions

View file

@ -11,6 +11,7 @@ from .line import Line
from .splitline import SplitLine
from .assetsetting import AssetSetting
from .asset import AssetRate
from .valuestore import ValueStore
def register():
@ -22,4 +23,5 @@ def register():
SplitLine,
Reconciliation,
AssetSetting,
ValueStore,
module='cashbook_investment', type_='model')

134
book.py
View file

@ -5,10 +5,12 @@
from trytond.model import fields, SymbolMixin, Index
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval, Or, Len, Bool, If
from trytond.pyson import Eval, Or, Bool, If
from trytond.modules.cashbook.book import STATES2, DEPENDS2
from trytond.transaction import Transaction
from trytond.report import Report
from trytond.exceptions import UserError
from trytond.i18n import gettext
from decimal import Decimal
from datetime import timedelta
from sql import Literal
@ -68,12 +70,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
string='Quantity', help='Quantity of assets until to date',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['quantity_digits', 'feature']), 'get_asset_quantity')
depends=['quantity_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
quantity_all = fields.Function(fields.Numeric(
string='Total Quantity', help='Total quantity of all assets',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['quantity_digits', 'feature']), 'get_asset_quantity')
depends=['quantity_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
current_value = fields.Function(fields.Numeric(
string='Value',
help='Valuation of the investment based on the current ' +
@ -81,7 +85,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
'get_asset_quantity', searcher='search_asset_quantity')
current_value_ref = fields.Function(fields.Numeric(
string='Value (Ref.)',
help='Valuation of the investment based on the current stock' +
@ -92,7 +96,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
~Eval('show_performance', False),
~Bool(Eval('company_currency', -1)))},
depends=['currency_digits', 'show_performance', 'company_currency']),
'get_asset_quantity')
'get_asset_quantity', searcher='search_asset_quantity')
# performance
diff_amount = fields.Function(fields.Numeric(
@ -101,14 +105,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
'get_asset_quantity', searcher='search_asset_quantity')
diff_percent = fields.Function(fields.Numeric(
string='Percent',
help='percentage performance since acquisition',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
'get_asset_quantity', searcher='search_asset_quantity')
show_performance = fields.Function(fields.Boolean(
string='Performance', readonly=True), 'on_change_with_show_performance')
current_rate = fields.Function(fields.Numeric(
@ -117,13 +121,15 @@ class Book(SymbolMixin, metaclass=PoolMeta):
'exchange price.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']), 'get_asset_quantity')
depends=['currency_digits', 'feature']),
'get_asset_quantity', searcher='search_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')
depends=['currency_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
# yield
yield_dividend_total = fields.Function(fields.Numeric(
@ -268,6 +274,25 @@ class Book(SymbolMixin, metaclass=PoolMeta):
@classmethod
def get_yield_data(cls, cashbooks, names):
""" get yield data - stored or computed
"""
context = Transaction().context
result = {x: {y.id: Decimal('0.0') for y in cashbooks} for x in names}
# return computed values if 'date' is in context
query_date = context.get('date', None)
if query_date is not None:
return cls.get_yield_values(cashbooks, names)
for cashbook in cashbooks:
for value in cashbook.value_store:
if value.field_name in names:
result[value.field_name][cashbook.id] = value.numvalue
return result
@classmethod
def get_yield_values(cls, cashbooks, names):
""" collect yield data
"""
pool = Pool()
@ -345,7 +370,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
(tab_line_yield, query_yield) = Line.get_yield_data_sql()
context = Transaction().context
query_date = context.get('qdate', CurrentDate())
query_date = context.get('date', CurrentDate())
query = tab_book.join(
tab_line,
condition=(tab_book.id == tab_line.cashbook),
@ -397,7 +422,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
return (query, tab_book)
@classmethod
def get_asset_quantity(cls, cashbooks, names):
def get_asset_quantity_values(cls, cashbooks, names):
""" get quantities
field: quantity, quantity_all, current_value,
current_value_ref, diff_amount, diff_percent,
@ -464,8 +489,15 @@ class Book(SymbolMixin, metaclass=PoolMeta):
if company_currency is not None else rdata[4]),
})
ids_assetbooks = [x.id for x in cashbooks if x.btype is not None]
ids_nonebtypes = [x.id for x in cashbooks if x.btype is None]
ids_assetbooks = []
ids_nonebtypes = []
for x in cashbooks:
if x.btype is None:
if x.id not in ids_nonebtypes:
ids_nonebtypes.append(x.id)
else:
if x.id not in ids_assetbooks:
ids_assetbooks.append(x.id)
# get values of asset-cashbooks in 'cashbooks' of type=asset
if len(ids_assetbooks) > 0:
@ -562,6 +594,82 @@ class Book(SymbolMixin, metaclass=PoolMeta):
result['digits'][id_book] = None
return {x: result[x] for x in names}
@classmethod
def get_asset_quantity(cls, cashbooks, names):
""" get quantities - stored or computed
"""
context = Transaction().context
result = {x: {y.id: Decimal('0.0') for y in cashbooks} for x in names}
# return computed values if 'date' is in context
query_date = context.get('date', None)
if query_date is not None:
return cls.get_asset_quantity_values(cashbooks, names)
for cashbook in cashbooks:
for value in cashbook.value_store:
if value.field_name in names:
result[value.field_name][cashbook.id] = value.numvalue
return result
@classmethod
def search_asset_quantity(cls, name, clause):
""" search in stored data
"""
ValueStore = Pool().get('cashbook.values')
context = Transaction().context
query_date = context.get('date', None)
if query_date is not None:
raise UserError(gettext(
'cashbook.msg_nosearch_with_date',
fname=name, model=cls.__name__))
else:
value_query = ValueStore.search([
('field_name', '=', clause[0]),
('numvalue',) + tuple(clause[1:]),
],
query=True)
return [('value_store', 'in', value_query)]
@classmethod
def valuestore_fields(cls):
""" field to update
"""
result = super(Book, cls).valuestore_fields()
# get_asset_quantity_values
result.extend([
'quantity', 'quantity_all', 'current_value', 'current_value_ref',
'diff_amount', 'diff_percent', 'current_rate', 'purchase_amount',
'yield_fee_total', 'yield_dividend_total', 'yield_sales',
'yield_fee_12m', 'yield_dividend_12m', 'yield_sales_12m'])
return result
@classmethod
def valuestore_update_records(cls, records):
""" compute current values of records,
store to global storage
"""
ValStore = Pool().get('cashbook.values')
super(Book, cls).valuestore_update_records(records)
if records:
ValStore.update_values(
cls.get_asset_quantity_values(
records, [
'quantity', 'quantity_all', 'current_value',
'current_value_ref', 'diff_amount', 'diff_percent',
'current_rate', 'purchase_amount']))
ValStore.update_values(
cls.get_yield_values(
records, [
'yield_fee_total', 'yield_dividend_total',
'yield_sales', 'yield_fee_12m', 'yield_dividend_12m',
'yield_sales_12m']))
@fields.depends('id')
def on_change_with_show_performance(self, name=None):
""" return True if current or subordered cashbooks

View file

@ -174,6 +174,24 @@ class CbInvTestCase(object):
self.assertEqual(books[0].diff_amount, Decimal('-5.49'))
self.assertEqual(books[0].diff_percent, Decimal('-18.74'))
# searcher
self.assertEqual(Book.search_count([
('current_value', '=', Decimal('23.8'))]), 1)
self.assertEqual(Book.search_count([
('current_value_ref', '=', Decimal('23.8'))]), 1)
self.assertEqual(Book.search_count([
('diff_amount', '=', Decimal('-5.49'))]), 1)
self.assertEqual(Book.search_count([
('diff_percent', '=', Decimal('-18.74'))]), 1)
self.assertEqual(Book.search_count([
('quantity', '=', Decimal('1.0'))]), 2)
self.assertEqual(Book.search_count([
('quantity_all', '=', Decimal('1.0'))]), 2)
self.assertEqual(Book.search_count([
('current_rate', '=', Decimal('11.9'))]), 1)
self.assertEqual(Book.search_count([
('purchase_amount', '=', Decimal('15.0'))]), 2)
self.assertEqual(len(books[0].childs), 4)
self.assertEqual(
@ -327,10 +345,11 @@ class CbInvTestCase(object):
# wf --> check
Line.wfcheck(book.lines)
self.prep_valstore_run_worker()
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 5)}):
'date': date(2022, 5, 5)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('2.8')) # usd
self.assertEqual(book2.quantity, Decimal('1.453'))
@ -340,7 +359,7 @@ class CbInvTestCase(object):
self.assertEqual(book2.current_value_ref, Decimal('3.87'))
with Transaction().set_context({
'qdate': date(2022, 5, 12)}):
'date': date(2022, 5, 12)}):
book2, = Book.browse([book])
self.assertEqual(book2.quantity, Decimal('4.753'))
self.assertEqual(book2.quantity_all, Decimal('4.753'))
@ -399,6 +418,17 @@ class CbInvTestCase(object):
'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company)
usd.rates[0].date = date(2022, 5, 1)
usd.rates[0].save()
self.assertEqual(len(usd.rates), 1)
self.assertEqual(usd.rates[0].date, date(2022, 5, 1))
euro.rates[0].date = date(2022, 5, 1)
euro.rates[0].save()
self.assertEqual(len(euro.rates), 1)
self.assertEqual(euro.rates[0].date, date(2022, 5, 1))
self.assertEqual(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz')
@ -441,7 +471,7 @@ class CbInvTestCase(object):
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 1)}):
'date': date(2022, 5, 1)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0'))
@ -508,6 +538,16 @@ class CbInvTestCase(object):
'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company)
usd.rates[0].date = date(2022, 5, 1)
usd.rates[0].save()
self.assertEqual(len(usd.rates), 1)
self.assertEqual(usd.rates[0].date, date(2022, 5, 1))
euro.rates[0].date = date(2022, 5, 1)
euro.rates[0].save()
self.assertEqual(len(euro.rates), 1)
self.assertEqual(euro.rates[0].date, date(2022, 5, 1))
self.assertEqual(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz')
chf, = Currency.create([{
@ -562,7 +602,7 @@ class CbInvTestCase(object):
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 1)}):
'date': date(2022, 5, 1)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0'))
@ -924,6 +964,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
@ -1069,6 +1110,7 @@ class CbInvTestCase(object):
self.assertEqual(len(book2.lines), 0)
Line.wfcheck(list(book1.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book1.rec_name,
@ -1167,6 +1209,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | 1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
@ -1314,6 +1357,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book.rec_name,
@ -1498,6 +1542,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book.rec_name,
@ -1855,6 +1900,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2010,6 +2056,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2192,6 +2239,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2531,6 +2579,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2690,6 +2739,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(books[0].rec_name, 'Book Cash | -11.00 usd | Open')
self.assertEqual(books[0].balance_all, Decimal('-11.0'))
@ -2882,6 +2932,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,

View file

@ -152,6 +152,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -257,6 +258,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -366,6 +368,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -451,6 +454,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -532,6 +536,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -631,6 +636,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -743,6 +749,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -879,6 +886,7 @@ class YieldTestCase(object):
lines[0].rec_name,
'05/02/2022|Exp/Sp|-23.50 usd|all out (1) [-]|-3.0000 u')
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1007,6 +1015,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1150,6 +1159,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1301,6 +1311,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 3)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1438,6 +1449,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,

26
valuestore.py Normal file
View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import PoolMeta
class ValueStore(metaclass=PoolMeta):
__name__ = 'cashbook.values'
@classmethod
def _maintenance_fields(cls):
""" add fields to update job
"""
result = super(ValueStore, cls)._maintenance_fields()
result.extend([
'quantity', 'current_value', 'current_value_ref',
'diff_amount', 'diff_percent', 'current_rate',
'purchase_amount', 'yield_fee_total', 'yield_dividend_total',
'yield_sales', 'yield_fee_12m', 'yield_dividend_12m',
'yield_sales_12m'])
return result
# end ValueStore