diff --git a/__init__.py b/__init__.py index b4c75e7..a0ba3c1 100644 --- a/__init__.py +++ b/__init__.py @@ -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') diff --git a/book.py b/book.py index 5e27b4b..814d69e 100644 --- a/book.py +++ b/book.py @@ -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 diff --git a/tests/book.py b/tests/book.py index d3be598..0878bb3 100644 --- a/tests/book.py +++ b/tests/book.py @@ -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, diff --git a/tests/yieldtest.py b/tests/yieldtest.py index 5bf99f6..1855c81 100644 --- a/tests/yieldtest.py +++ b/tests/yieldtest.py @@ -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, diff --git a/valuestore.py b/valuestore.py new file mode 100644 index 0000000..5337e8c --- /dev/null +++ b/valuestore.py @@ -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