# -*- 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.model import fields, SymbolMixin from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval, Or, Len, Bool, If from trytond.modules.cashbook.book import STATES2, DEPENDS2 from trytond.transaction import Transaction from trytond.report import Report from decimal import Decimal from datetime import timedelta from sql import Literal from sql.functions import CurrentDate from sql.aggregate import Sum from sql.conditionals import Case, Coalesce from trytond.modules.cashbook.model import CACHEKEY_CURRENCY, CACHEKEY_CASHBOOK from .asset import CACHEKEY_ASSETRATE class Book(SymbolMixin, metaclass=PoolMeta): __name__ = 'cashbook.book' asset = fields.Many2One(string='Asset', select=True, model_name='investment.asset', ondelete='RESTRICT', states={ 'required': Eval('feature', '') == 'asset', 'invisible': Eval('feature', '') != 'asset', 'readonly': Or( STATES2['readonly'], Len(Eval('lines')) > 0, ), }, depends=DEPENDS2+['feature', 'lines']) quantity_digits = fields.Integer(string='Digits', help='Quantity Digits', domain=[ ('quantity_digits', '>=', 0), ('quantity_digits', '<=', 6), ], states={ 'required': Eval('feature', '') == 'asset', 'invisible': Eval('feature', '') != 'asset', 'readonly': Or( STATES2['readonly'], Len(Eval('lines')) > 0, ), }, depends=DEPENDS2+['feature', 'lines']) asset_uomcat = fields.Function(fields.Many2One(string='UOM Category', readonly=True, model_name='product.uom.category', states={'invisible': True}), 'on_change_with_asset_uomcat') quantity_uom = fields.Many2One(string='UOM', select=True, model_name='product.uom', ondelete='RESTRICT', domain=[ ('category.id', '=', Eval('asset_uomcat', -1)), ], states={ 'required': Eval('feature', '') == 'asset', 'invisible': Eval('feature', '') != 'asset', 'readonly': Or( STATES2['readonly'], Len(Eval('lines')) > 0, ), }, depends=DEPENDS2+['feature', 'lines', 'asset_uomcat']) symbol = fields.Function(fields.Char(string='Symbol', readonly=True), 'on_change_with_symbol') asset_symbol = fields.Function(fields.Many2One(string='Symbol', readonly=True, model_name='cashbook.book'), 'on_change_with_asset_symbol') quantity = fields.Function(fields.Numeric(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') 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') current_value = fields.Function(fields.Numeric(string='Value', help='Valuation of the investment based on the current stock market price.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('show_performance', False) == False, }, depends=['currency_digits', 'show_performance']), 'get_asset_quantity') current_value_ref = fields.Function(fields.Numeric(string='Value (Ref.)', help='Valuation of the investment based on the current stock exchange price, converted into the company currency.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Or( Eval('show_performance', False) == False, ~Bool(Eval('company_currency', -1)), ), }, depends=['currency_digits', 'show_performance', 'company_currency']), 'get_asset_quantity') # performance diff_amount = fields.Function(fields.Numeric(string='Difference', help='Difference between acquisition value and current value', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('show_performance', False) == False, }, depends=['currency_digits', 'show_performance']), 'get_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) == False, }, depends=['currency_digits', 'show_performance']), 'get_asset_quantity') show_performance = fields.Function(fields.Boolean(string='Performance', readonly=True), 'on_change_with_show_performance') current_rate = fields.Function(fields.Numeric(string='Rate', help='Rate per unit of investment based on current stock exchange price.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, 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_dividend_total = fields.Function(fields.Numeric(string='Dividend', help='Total dividends received', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_dividend_12m = fields.Function(fields.Numeric(string='Dividend 1y', help='Dividends received in the last twelve months', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_fee_total = fields.Function(fields.Numeric(string='Trade Fee', help='Total trade fees payed', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_fee_12m = fields.Function(fields.Numeric(string='Trade Fee 1y', help='Trade fees payed in the last twelve month', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_sales = fields.Function(fields.Numeric(string='Sales', help='Total profit or loss on sale of shares.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_sales_12m = fields.Function(fields.Numeric(string='Sales 1y', help='Total profit or loss on sale of shares in the last twelve month.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'get_yield_data') yield_balance = fields.Function(fields.Numeric(string='Total Yield', help='Total income: price gain + dividends + sales gains - fees', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Eval('feature', '') != 'asset', }, depends=['currency_digits', 'feature']), 'on_change_with_yield_balance') @classmethod def view_attributes(cls): return super(Book, cls).view_attributes() + [ ('/tree', 'visual', If(Eval('show_performance', False) == True, If(Eval('diff_percent', 0) < 0, 'danger', If(Eval('diff_percent', 0) > 0, 'success', '') ), '') ), ] def get_rec_name(self, name): """ add quantities - if its a asset-cashbook """ recname = super(Book, self).get_rec_name(name) if self.feature == 'asset': recname += ' | %(quantity)s %(uom_symbol)s' % { 'quantity': Report.format_number(self.quantity or 0.0, None, digits=self.quantity_digits), 'uom_symbol': getattr(self.quantity_uom, 'symbol', '-'), } return recname @fields.depends('asset', 'quantity_uom') def on_change_asset(self): """ get uom from asset """ if self.asset: self.quantity_uom = self.asset.uom.id @classmethod def default_quantity_digits(cls): """ default: 4 """ return 4 @fields.depends('yield_sales', 'yield_dividend_total', 'diff_amount') def on_change_with_yield_balance(self, name=None): """ calculate yield total fee is already contained in 'diff_amount' """ 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]) return sum2 @classmethod def get_yield_data_sql(cls, date_from=None, date_to=None): """ collect yield data """ pool = Pool() Line = pool.get('cashbook.line') (tab_line1, tab_line_yield) = Line.get_yield_data_sql() (tab_line2, tab_line_gainloss) = Line.get_gainloss_data_sql() tab_book = cls.__table__() tab_line = Line.__table__() where = Literal(True) if date_from: where &= tab_line.date >= date_from if date_to: where &= tab_line.date <= date_to query = tab_book.join(tab_line, condition=tab_line.cashbook==tab_book.id, ).join(tab_line_yield, condition=tab_line_yield.id==tab_line.id, ).join(tab_line_gainloss, condition=tab_line_gainloss.id==tab_line.id, ).select( tab_book.id, Sum(tab_line_yield.fee).as_('fee'), Sum(tab_line_yield.dividend).as_('dividend'), Sum(tab_line_gainloss.gainloss).as_('gainloss'), group_by=[tab_book.id], where=where ) return (tab_book, query) @classmethod def get_yield_data(cls, cashbooks, names): """ collect yield data """ pool = Pool() CashBook = pool.get('cashbook.book') IrDate = pool.get('ir.date') MemCache = pool.get('cashbook.memcache') cursor = Transaction().connection.cursor() context = Transaction().context 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): """ quantize... """ return ( value or Decimal('0.0') ).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])], 'cachekey': CACHEKEY_CASHBOOK % 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)], 'cachekey': CACHEKEY_ASSETRATE % 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' records_total = [] records_12m = [] if len(todo_cashbook) > 0: (tab_book1, query_total) = cls.get_yield_data_sql() query_total.where &= tab_book1.id.in_([x.id for x in todo_cashbook]) cursor.execute(*query_total) records_total = cursor.fetchall() # results for 12 months (tab_book2, query_12m) = cls.get_yield_data_sql( date_to = query_date, date_from = query_date - timedelta(days=365), ) query_12m.where &= tab_book2.id.in_([x.id for x in todo_cashbook]) cursor.execute(*query_12m) records_12m = cursor.fetchall() for record in records_total: book = CashBook(record[0]) result['yield_fee_total'][record[0]] = quantize_val(record[1], book) result['yield_dividend_total'][record[0]] = quantize_val(record[2], book) result['yield_sales'][record[0]] = quantize_val(record[3], book) for record in records_12m: book = CashBook(record[0]) result['yield_fee_12m'][record[0]] = quantize_val(record[1], book) result['yield_dividend_12m'][record[0]] = quantize_val(record[2], book) result['yield_sales_12m'][record[0]] = quantize_val(record[3], book) # store to cache MemCache.store_result(cashbooks, cache_keys, result) return {x:result[x] for x in names} @classmethod def get_asset_quantity_sql(cls): """ get table of asset and its value, rate, ... """ pool = Pool() CBook = pool.get('cashbook.book') BookType = pool.get('cashbook.type') Line = pool.get('cashbook.line') Asset = pool.get('investment.asset') Currency = pool.get('currency.currency') tab_book = CBook.__table__() tab_type = BookType.__table__() tab_line = Line.__table__() tab_cur = Currency.__table__() tab_asset = Asset.__table__() (tab_rate, tab2) = Asset.get_rate_data_sql() (tab_balance, tab2) = CBook.get_balance_of_cashbook_sql() (tab_line_yield, query_yield) = Line.get_yield_data_sql() context = Transaction().context query_date = context.get('qdate', CurrentDate()) query = tab_book.join(tab_line, condition=(tab_book.id==tab_line.cashbook), ).join(tab_type, condition=tab_book.btype==tab_type.id, ).join(tab_cur, condition=tab_book.currency==tab_cur.id, ).join(tab_asset, condition=tab_book.asset==tab_asset.id, ).join(query_yield, condition=query_yield.id==tab_line.id, ).join(tab_balance, condition=tab_book.id==tab_balance.cashbook, type_ = 'LEFT OUTER', ).join(tab_rate, condition=tab_book.asset==tab_rate.id, type_ = 'LEFT OUTER', ).select( tab_book.id, # 0 Coalesce(Sum(Case( (tab_line.date <= query_date, tab_line.quantity_credit - tab_line.quantity_debit), else_ = Decimal('0.0'), )), Decimal('0.0')).as_('quantity'), # 1 Sum(tab_line.quantity_credit - tab_line.quantity_debit).as_('quantity_all'), # 2 Coalesce(tab_rate.rate, Decimal('0.0')).as_('rate'), # 3 tab_book.currency, # 4 tab_cur.digits.as_('currency_digits'), # 5 tab_asset.uom, # 6 tab_book.quantity_uom, # 7 tab_asset.currency.as_('asset_currency'), #8 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, tab_book.currency, tab_cur.digits, tab_asset.uom, tab_book.quantity_uom, tab_asset.currency, tab_balance.balance], where=(tab_type.feature == 'asset'), ) return (query, tab_book) @classmethod def get_asset_quantity(cls, cashbooks, names): """ get quantities """ pool = Pool() CBook = pool.get('cashbook.book') Uom = pool.get('product.uom') Currency = pool.get('currency.currency') IrDate = pool.get('ir.date') MemCache = pool.get('cashbook.memcache') cursor = Transaction().connection.cursor() context = Transaction().context (query, tab_book) = cls.get_asset_quantity_sql() company_currency = CBook.default_currency() 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])], 'cachekey': CACHEKEY_CASHBOOK % 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)], 'cachekey': CACHEKEY_ASSETRATE % 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): """ compute values for record """ # uom-factor if rdata[6] == rdata[7]: uom_factor = Decimal('1.0') else : uom_factor = Decimal( Uom.compute_qty(Uom(rdata[6]), 1.0, Uom(rdata[7]), round=False) ) current_value = Currency.compute( rdata[8], rdata[3] * rdata[1] / uom_factor, rdata[4] ) return (record[0], { 'quantity': rdata[1], 'quantity_all': rdata[2], 'current_value': current_value, 'current_value_ref': Currency.compute( rdata[8], rdata[3] * rdata[1] / uom_factor, company_currency if company_currency is not None else rdata[8], ), 'diff_amount': current_value - rdata[10], 'diff_percent': ( Decimal('100.0') * current_value / \ rdata[10] - Decimal('100.0') ).quantize(Decimal(str(1/10**rdata[5]))) \ if rdata[10] != Decimal('0.0') else None, 'current_rate': ( current_value / rdata[1] ).quantize(Decimal(str(1/10**rdata[5]))) \ 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 = {} 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] # get values of asset-cashbooks in 'cashbooks' of type=asset if len(ids_assetbooks) > 0: query.where &= tab_book.id.in_(ids_assetbooks) cursor.execute(*query) records = cursor.fetchall() for record in records: (book_id, values) = values_from_record(record) result_cache[book_id] = values result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref for name in values.keys(): if name not in result.keys(): result[name] = {} result[name][book_id] = values[name] # add aggregated values of cashbooks without type aggr_names = ['current_value', 'current_value_ref', 'diff_amount', 'diff_percent'] queried_names = list(set(aggr_names).intersection(set(names))) if len(queried_names) > 0: # query all subordered asset-cashbooks for # btype=None-cashbooks query1 = [('btype.feature', '=', 'asset'), ('parent', 'child_of', ids_nonebtypes)] if len(result_cache.keys()) > 0: query1.append(('id', 'not in', result_cache.keys())) books_query = CBook.search(query1, query=True) # add results to cache (query, tab_book) = cls.get_asset_quantity_sql() query.where &= tab_book.id.in_(books_query) cursor.execute(*query) records = cursor.fetchall() for record in records: (book_id, values) = values_from_record(record) result_cache[book_id] = values result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref # aggregate sub-cashbooks to requested cashbooks from cache for id_none in ids_nonebtypes: records = CBook.search([ ('btype.feature', '=', 'asset'), ('parent', 'child_of', [id_none]) ]) values = {x:Decimal('0.0') for x in aggr_names+['purchase_amount_ref']} for record in records: for name in aggr_names+['purchase_amount_ref']: values[name] += \ result_cache.get(record.id, {}).get(name, Decimal('0.0')) # convert current-value-ref in company-currency to # currency of current cashbook cbook = CBook(id_none) values['current_value'] = Currency.compute( company_currency if company_currency is not None else cbook.currency, values['current_value_ref'], cbook.currency, ) values['diff_amount'] = Currency.compute( company_currency if company_currency is not None else cbook.currency, values['current_value_ref'] - values['purchase_amount_ref'], cbook.currency, ) values['diff_percent'] = \ (Decimal('100.0') * values['current_value_ref'] / \ values['purchase_amount_ref'] - Decimal('100.0') ).quantize( Decimal(str(1/10**cbook.currency_digits)) ) if values['purchase_amount_ref'] != Decimal('0.0') else None for name in values.keys(): if name not in result.keys(): result[name] = {} result[name][id_none] = values[name] # store to cache MemCache.store_result(cashbooks, cache_keys, result) return {x:result[x] for x in names} @fields.depends('id') def on_change_with_show_performance(self, name=None): """ return True if current or subordered cashbooks are of type=asset """ Book2 = Pool().get('cashbook.book') if Book2.search_count([ ('btype.feature', '=', 'asset'), ('parent', 'child_of', [self.id]), ]) > 0: return True return False @fields.depends('id') def on_change_with_asset_symbol(self, name=None): """ get current cashbook to enable usage of 'symbol' in the form """ return self.id @fields.depends('quantity_uom', 'currency') def on_change_with_symbol(self, name=None): """ get symbol for asset """ return '%(currency)s/%(unit)s' % { 'currency': getattr(self.currency, 'symbol', '-'), 'unit': getattr(self.quantity_uom, 'symbol', '-'), } @fields.depends('asset', '_parent_asset.uom') def on_change_with_asset_uomcat(self, name=None): """ get uom-category of asset """ if self.asset: return self.asset.uom.category.id # end Book