# -*- 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 sql.functions import CurrentDate from sql.aggregate import Sum from sql.conditionals import Case, Coalesce class Book(SymbolMixin, metaclass=PoolMeta): __name__ = 'cashbook.book' asset = fields.Many2One(string='Asset', 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', 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') # yield yield_dividend_total = fields.Function(fields.Numeric(string='Dividend', help='Total dividends received', readonly=True, digits=(16, Eval('currency_digits', 2)), depends=['currency_digits']), '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)), depends=['currency_digits']), '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)), depends=['currency_digits']), '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)), depends=['currency_digits']), '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)), depends=['currency_digits']), '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)), depends=['currency_digits']), '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)), depends=['currency_digits']), 'get_yield_data') @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 @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 = 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'), having=where ) return (tab_book, query) @classmethod def get_yield_data(cls, cashbooks, names): """ collect yield data """ cursor = Transaction().connection.cursor() (tab_book, query) = cls.get_yield_data_sql() result = {x:{y.id: None for y in cashbooks} for x in names} query.where = tab_book.id.in_([x.id for x in cashbooks]) cursor.execute(*query) records = cursor.fetchall() def quantize_val(value, line): """ quantize... """ return ( value or Decimal('0.0') ).quantize(Decimal(str(1/10**line.currency_digits))) for record in records: line = Line2(record[0]) values = { 'trade_fee': quantize_val(record[1], line), 'asset_dividend': quantize_val(record[2], line), } for name in list(name_set): result[name][record[0]] = values[name] return result @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() 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(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 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') 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} 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[9], 'diff_percent': ( Decimal('100.0') * current_value / \ rdata[9] - Decimal('100.0') ).quantize(Decimal(str(1/10**rdata[5]))) \ if rdata[9] != 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, }) 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 names: 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+['balance_ref']} for record in records: for name in aggr_names+['balance_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['balance_ref'], cbook.currency, ) values['diff_percent'] = \ (Decimal('100.0') * values['current_value_ref'] / \ values['balance_ref'] - Decimal('100.0') ).quantize( Decimal(str(1/10**cbook.currency_digits)) ) if values['balance_ref'] != Decimal('0.0') else None for name in queried_names: result[name][id_none] = values[name] return result @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