cashbook_investment/book.py
2023-02-22 21:24:58 +01:00

524 lines
22 KiB
Python

# -*- 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
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)),
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_fee_total', 'yield_dividend_total', 'diff_amount')
def on_change_with_yield_balance(self, name=None):
""" calculate yield total
"""
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])
if self.yield_fee_total is not None:
sum2 -= self.yield_fee_total
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')
cursor = Transaction().connection.cursor()
context = Transaction().context
result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
def quantize_val(value, line):
""" quantize...
"""
return (
value or Decimal('0.0')
).quantize(Decimal(str(1/10**line.currency_digits)))
# results for 'total'
(tab_book1, query_total) = cls.get_yield_data_sql()
query_total.where &= tab_book1.id.in_([x.id for x in cashbooks])
cursor.execute(*query_total)
records_total = cursor.fetchall()
# results for 12 months
query_date = context.get('date', IrDate.today())
(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 cashbooks])
cursor.execute(*query_12m)
records_12m = cursor.fetchall()
result = {x:{y.id: Decimal('0.0') for y in cashbooks} for x in names}
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)
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