diff --git a/README.rst b/README.rst
index 3eb85fe..db3f6ab 100644
--- a/README.rst
+++ b/README.rst
@@ -9,11 +9,19 @@ pip install mds-investment
Requires
========
-- Tryton 6.0
+- Tryton 7.0
+
+How to
+======
+
+Store values such as stocks, funds and commodities in Tryton.
+Watch the performance. The Tryton module periodically loads
+quotes of values from one or more price sources.
+You can define the course sources yourself.
Changes
=======
-*6.0.0 - 09.11.2022*
+*7.0.0 - 01.12.2023*
-- init
+- compatibility to Tryton 7.0
diff --git a/__init__.py b/__init__.py
index 161c360..db43519 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,26 +1,34 @@
# -*- coding: utf-8 -*-
-# This file is part of the investment-module from m-ds for Tryton.
+# This file is part of the investment-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import Pool
-from .asset import Asset
+from .asset import Asset, AssetSourceRel
from .rate import Rate
from .identifier import Identifier
from .cron import Cron
from .onlinesource import OnlineSource
from .update_wiz import UpdateSoureWizard
+from .diagram import GraphDef, ChartPoint
+from .import_wiz import ImportWizard, ImportWizardStart
def register():
Pool.register(
OnlineSource,
+ AssetSourceRel,
Asset,
Rate,
Identifier,
Cron,
+ ImportWizardStart,
module='investment', type_='model')
+ Pool.register(
+ GraphDef,
+ ChartPoint,
+ module='investment', type_='model', depends=['diagram'])
Pool.register(
UpdateSoureWizard,
+ ImportWizard,
module='investment', type_='wizard')
-
diff --git a/asset.py b/asset.py
index 4d658b0..578de36 100644
--- a/asset.py
+++ b/asset.py
@@ -1,78 +1,212 @@
# -*- coding: utf-8 -*-
-# This file is part of the investment-module from m-ds for Tryton.
+# This file is part of the investment-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
-from trytond.model import ModelView, ModelSQL, fields
+from trytond.model import ModelView, ModelSQL, fields, SymbolMixin, Index
from trytond.transaction import Transaction
from trytond.pool import Pool
-from trytond.pyson import Eval, Bool, And
+from trytond.pyson import Eval, Bool, If, Date
+from trytond.report import Report
+
from decimal import Decimal
from datetime import time
-from sql.functions import CurrentTime
-from sql.conditionals import Case
+from sql.functions import CurrentDate, CurrentTimestamp, Round, Extract
+from sql.conditionals import Case, Coalesce, NullIf
+from sql import Literal
+from .diagram import Concat2
+from .const import DEF_NONE
-class Asset(ModelSQL, ModelView):
+digits_percent = 2
+
+sel_updtdays = [
+ ('work', 'Mon - Fri'),
+ ('week', 'Mon - Sun'),
+ ]
+
+
+class Asset(SymbolMixin, ModelSQL, ModelView):
'Asset'
__name__ = 'investment.asset'
- company = fields.Many2One(string='Company', model_name='company.company',
+ name = fields.Function(fields.Char(
+ string='Name', readonly=True),
+ 'get_name_symbol', searcher='search_rec_name')
+ company = fields.Many2One(
+ string='Company', model_name='company.company',
required=True, ondelete="RESTRICT")
- product = fields.Many2One(string='Product', required=True,
- model_name='product.product', ondelete='RESTRICT',
- domain=[('type', '=', 'assets')])
- product_uom = fields.Function(fields.Many2One(string='UOM Category',
- readonly=True, model_name='product.uom.category',
- help='Category of unit on the product.'),
- 'on_change_with_product_uom')
- uom = fields.Many2One(string='UOM', required=True,
- model_name='product.uom', ondelete='RESTRICT',
- states={
- 'readonly': ~Bool(Eval('product')),
- },
- domain=[
- ('category', '=', Eval('product_uom')),
- ], depends=['product_uom', 'product'])
- rates = fields.One2Many(string='Rates', field='asset',
- model_name='investment.rate')
- rate = fields.Function(fields.Numeric(string='Current Rate',
- readonly=True, digits=(16, Eval('currency_digits', 4)),
- depends=['currency_digits']), 'on_change_with_rate')
+ product = fields.Many2One(
+ string='Product', required=True, model_name='product.product',
+ ondelete='RESTRICT', domain=[('type', '=', 'assets')])
+ product_uom = fields.Function(fields.Many2One(
+ string='UOM Category', readonly=True,
+ model_name='product.uom.category',
+ help='Category of unit on the product.'), 'get_name_symbol')
+ uom = fields.Many2One(
+ string='UOM', required=True, model_name='product.uom',
+ ondelete='RESTRICT',
+ states={'readonly': ~Bool(Eval('product'))},
+ domain=[('category', '=', Eval('product_uom'))],
+ depends=['product_uom', 'product'])
+ symbol = fields.Function(fields.Char(
+ string='UOM', readonly=True), 'get_name_symbol',
+ searcher='search_uom_symbol')
+ asset_symbol = fields.Function(fields.Many2One(
+ string='Symbol', readonly=True, model_name='investment.asset'),
+ 'get_name_symbol')
- company_currency = fields.Function(fields.Many2One(readonly=True,
- string='Company Currency', states={'invisible': True},
- model_name='currency.currency'),
- 'on_change_with_company_currency')
- company_currency_digits = fields.Function(fields.Integer(
- string='Currency Digits (Ref.)', readonly=True),
- 'on_change_with_currency_digits')
+ rates = fields.One2Many(
+ string='Rates', field='asset', model_name='investment.rate')
+ rate = fields.Function(fields.Numeric(
+ string='Current Rate', readonly=True,
+ digits=(16, Eval('currency_digits', 4)), depends=['currency_digits']),
+ 'get_rate_data', searcher='search_rate')
+ date = fields.Function(fields.Date(
+ string='Date', readonly=True, help='Date of current rate'),
+ 'get_rate_data', searcher='search_date')
- currency = fields.Many2One(string='Currency', select=True,
- required=True, model_name='currency.currency', ondelete='RESTRICT')
- currency_digits = fields.Integer(string='Currency Digits',
- required=True)
+ currency = fields.Many2One(
+ string='Currency', required=True,
+ model_name='currency.currency', ondelete='RESTRICT')
+ currency_digits = fields.Integer(
+ string='Digits', required=True,
+ domain=[('currency_digits', '>=', 0), ('currency_digits', '<=', 6)])
- wkn = fields.Function(fields.Char(string='NSIN', readonly=True,
+ wkn = fields.Function(fields.Char(
+ string='NSIN', readonly=True,
help='National Securities Identifying Number'),
'get_identifiers', searcher='search_identifier')
- isin = fields.Function(fields.Char(string='ISIN', readonly=True,
+ isin = fields.Function(fields.Char(
+ string='ISIN', readonly=True,
help='International Securities Identification Number'),
'get_identifiers', searcher='search_identifier')
- secsymb = fields.Function(fields.Char(string='Symbol', readonly=True,
+ secsymb = fields.Function(fields.Char(
+ string='Symbol', readonly=True,
help='Stock market symbol'),
'get_identifiers', searcher='search_identifier')
- updtsource = fields.Many2One(string='Update Source',
- help='Select a source for the course update.',
- ondelete='SET NULL', model_name='investment.source')
- updttime = fields.Time(string='Time',
+ updtsources = fields.Many2Many(
+ string='Update Sources',
+ help='Select sources for the course update. The course sources ' +
+ 'are tried until a valid value has been read.',
+ relation_name='investment.asset_source_rel',
+ origin='asset', target='source')
+ updturl = fields.Char(
+ string='URL',
+ help='URL for data retrieval.',
states={
- 'readonly': ~Bool(Eval('updtsource')),
- }, depends=['updtsource'])
- updtneeded = fields.Function(fields.Boolean(string='Course update needed',
- readonly=True),
- 'on_change_with_updtneeded', searcher='search_updtneeded')
+ 'invisible': ~Eval('updturl_enable', False),
+ 'required': Eval('updturl_enable', False),
+ }, depends=['updturl_enable'])
+ updturl_enable = fields.Function(fields.Boolean(
+ string='URL required', readonly=True,
+ states={'invisible': True}),
+ 'on_change_with_updturl_enable')
+ updtdays = fields.Selection(
+ string='Select days', required=True, selection=sel_updtdays)
+ updttime = fields.Time(
+ string='Time',
+ states={'readonly': ~Bool(Eval('updtsources'))},
+ depends=['updtsources'])
+ nextupdate = fields.Function(fields.DateTime(
+ string='Next Update', readonly=True),
+ 'get_nextupdates', searcher='search_nextupdate')
+
+ # percentage change
+ change_day1 = fields.Function(fields.Numeric(
+ string='Previous Day', readonly=True,
+ digits=(16, digits_percent)),
+ 'get_percentage_change', searcher='search_percentage')
+ change_month1 = fields.Function(fields.Numeric(
+ string='1 Month', readonly=True,
+ help='percentage change in value compared to last month',
+ digits=(16, digits_percent)),
+ 'get_percentage_change', searcher='search_percentage')
+ change_month3 = fields.Function(fields.Numeric(
+ string='3 Months',
+ help='percentage change in value during 3 months',
+ digits=(16, digits_percent)),
+ 'get_percentage_change', searcher='search_percentage')
+ change_month6 = fields.Function(fields.Numeric(
+ string='6 Months', readonly=True,
+ help='percentage change in value during 6 months',
+ digits=(16, digits_percent)),
+ 'get_percentage_change', searcher='search_percentage')
+ change_month12 = fields.Function(fields.Numeric(
+ string='1 Year', readonly=True,
+ help='percentage change in value during 1 year',
+ digits=(16, digits_percent)),
+ 'get_percentage_change', searcher='search_percentage')
+ change_symbol = fields.Function(fields.Many2One(
+ string='Symbol', readonly=True, model_name='investment.rate'),
+ 'get_rate_data')
+
+ @classmethod
+ def __register__(cls, module_name):
+ """ register and migrate
+ """
+ super(Asset, cls).__register__(module_name)
+ cls.migrate_updtsource(module_name)
+
+ @classmethod
+ def __setup__(cls):
+ super(Asset, cls).__setup__()
+ cls._order.insert(0, ('name', 'ASC'))
+ cls._order.insert(0, ('date', 'DESC'))
+ t = cls.__table__()
+ cls._sql_indexes.update({
+ Index(
+ t,
+ (t.product, Index.Equality())),
+ Index(
+ t,
+ (t.currency, Index.Equality())),
+ Index(
+ t,
+ (t.uom, Index.Equality())),
+ Index(
+ t,
+ (t.updtdays, Index.Equality())),
+ })
+
+ @classmethod
+ def migrate_updtsource(cls, module_name):
+ """ replace 'updtsource' by relation
+ """
+ pool = Pool()
+ Asset2 = pool.get('investment.asset')
+
+ asset_table = Asset2.__table_handler__(module_name)
+ if asset_table.column_exist('updtsource'):
+ AssetSourceRel = pool.get('investment.asset_source_rel')
+ tab_asset = Asset2.__table__()
+ cursor = Transaction().connection.cursor()
+
+ query = tab_asset.select(
+ tab_asset.id,
+ tab_asset.updtsource,
+ where=tab_asset.updtsource != DEF_NONE,
+ )
+ cursor.execute(*query)
+ records = cursor.fetchall()
+ to_create = [{
+ 'asset': x[0],
+ 'source': x[1],
+ } for x in records]
+
+ if to_create:
+ AssetSourceRel.create(to_create)
+ asset_table.drop_column('updtsource')
+
+ @classmethod
+ def view_attributes(cls):
+ return super().view_attributes() + [
+ ('/tree', 'visual',
+ If(Eval('date', Date()) < Date(delta_days=-5), 'muted',
+ If(Eval('change_day1', 0) < 0, 'danger',
+ If(Eval('change_day1', 0) > 0, 'success', '')))),
+ ]
@classmethod
def default_currency(cls):
@@ -96,32 +230,37 @@ class Asset(ModelSQL, ModelView):
"""
return 4
- @fields.depends('updtsource', 'updttime')
- def on_change_updtsource(self):
+ @classmethod
+ def default_updttime(cls):
+ """ 14 o'clock UTC
+ """
+ return time(14, 0)
+
+ @classmethod
+ def default_updtdays(cls):
+ """ default: mon - fri
+ """
+ return 'work'
+
+ @fields.depends('updtsources')
+ def on_change_with_updturl_enable(self, name=None):
+ """ return True if a source has fixed-url
+ """
+ if self.updtsources:
+ for usource in self.updtsources:
+ if usource.fixed_url is True:
+ return True
+ return False
+
+ @fields.depends('updtsources', 'updttime')
+ def on_change_updtsources(self):
""" clear time-fields
"""
- if self.updtsource is None:
+ if not self.updtsources:
self.updttime = None
- else :
+ else:
self.updttime = time(11, 30)
- @fields.depends('id', 'currency_digits')
- def on_change_with_rate(self, name=None):
- """ get current rate
- """
- pool = Pool()
- Rate = pool.get('investment.rate')
- IrDate = pool.get('ir.date')
-
- if self.id:
- rates = Rate.search([
- ('date', '<=', IrDate.today()),
- ('asset.id', '=', self.id),
- ], order=[('date', 'DESC')], limit=1)
- if len(rates) > 0:
- exp = Decimal(Decimal(1) / 10 ** (self.currency_digits or 4))
- return rates[0].rate.quantize(exp)
-
@fields.depends('product', 'uom')
def on_change_product(self):
""" update unit by product
@@ -138,77 +277,423 @@ class Asset(ModelSQL, ModelView):
if self.currency:
self.currency_digits = self.currency.digits
- @fields.depends('product')
- def on_change_with_product_uom(self, name=None):
- """ get category of product-uom
- """
- if self.product:
- return self.product.default_uom.category.id
-
- @fields.depends('currency')
- def on_change_with_currency_digits(self, name=None):
- """ currency of cashbook
- """
- if self.currency:
- return self.currency.digits
- else:
- return 2
-
- @fields.depends('company', 'currency')
- def on_change_with_company_currency(self, name=None):
- """ get company-currency if its different from current
- asset-currency
- """
- if self.company:
- if self.currency:
- if self.company.currency.id != self.currency.id:
- return self.company.currency.id
-
- @fields.depends('id')
- def on_change_with_updtneeded(self, name=None):
- """ get state of update
- """
- Asset2 = Pool().get('investment.asset')
-
- if self.id:
- if Asset2.search_count([
- ('updtneeded', '=', True),
- ('id', '=', self.id)
- ]) == 1:
- return True
- return False
-
@classmethod
- def search_updtneeded(cls, names, clause):
- """ search for assets to update
+ def get_name_symbol_sql(cls):
+ """ get sql for name, uom, digits, etc.
"""
pool = Pool()
- Asset2 = pool.get('investment.asset')
+ Product = pool.get('product.product')
+ ProdTempl = pool.get('product.template')
+ Uom = pool.get('product.uom')
+ Currency = pool.get('currency.currency')
+ tab_asset = cls.__table__()
+ tab_templ = ProdTempl.__table__()
+ tab_prod = Product.__table__()
+ tab_uom = Uom.__table__()
+ tab_cur = Currency.__table__()
+
+ # get translated symbol-column from UOM
+ (tab1, join1, col1) = Uom.symbol._get_translation_column(Uom, 'symbol')
+ tab_symb = join1.select(tab1.id, col1.as_('symbol'))
+
+ query = tab_asset.join(
+ tab_prod,
+ condition=tab_asset.product == tab_prod.id,
+ ).join(
+ tab_templ,
+ condition=tab_templ.id == tab_prod.template,
+ ).join(
+ tab_uom,
+ condition=tab_templ.default_uom == tab_uom.id,
+ ).join(
+ tab_cur,
+ condition=tab_asset.currency == tab_cur.id,
+ ).join(
+ tab_symb,
+ condition=tab_asset.uom == tab_symb.id,
+ ).select(
+ tab_asset.id,
+ tab_templ.name,
+ tab_uom.category.as_('product_uom'),
+ Concat2(tab_cur.symbol, '/', tab_symb.symbol).as_('symbol'),
+ )
+ return (query, tab_asset)
+
+ @classmethod
+ def get_name_symbol(cls, assets, names):
+ """ get date and rate of asset
+ """
+ cursor = Transaction().connection.cursor()
+
+ result = {x: {y.id: None for y in assets} for x in names}
+
+ (query, tab_asset) = cls.get_name_symbol_sql()
+ if assets:
+ query.where = tab_asset.id.in_([x.id for x in assets])
+ cursor.execute(*query)
+ records = cursor.fetchall()
+
+ for record in records:
+ values = {
+ 'name': record[1],
+ 'product_uom': record[2],
+ 'symbol': record[3],
+ 'asset_symbol': record[0],
+ }
+
+ for name in names:
+ result[name][record[0]] = values[name]
+ return result
+
+ @classmethod
+ def search_uom_symbol(cls, names, clause):
+ """ search in uom
+ """
+ return ['OR',
+ (('uom.rec_name',) + tuple(clause[1:])),
+ (('currency.rec_name',) + tuple(clause[1:]))]
+
+ @classmethod
+ def get_rate_data_sql(cls):
+ """ get sql for rate/date
+ """
+ pool = Pool()
+ Asset = pool.get('investment.asset')
Rate = pool.get('investment.rate')
- IrDate = pool.get('ir.date')
- tab_asset = Asset2.__table__()
+ tab_asset = Asset.__table__()
tab_rate = Rate.__table__()
+
+ query = tab_asset.join(
+ tab_rate,
+ condition=tab_asset.id == tab_rate.asset
+ ).select(
+ tab_asset.id,
+ Round(tab_rate.rate, tab_asset.currency_digits).as_('rate'),
+ tab_rate.date,
+ tab_rate.id.as_('id_rate'),
+ distinct_on=[tab_asset.id],
+ order_by=[tab_asset.id, tab_rate.date.desc],
+ )
+ return (query, tab_asset)
+
+ @classmethod
+ def get_rate_data(cls, assets, names):
+ """ get date and rate of asset
+ """
+ cursor = Transaction().connection.cursor()
+
+ result = {x: {y.id: None for y in assets} for x in names}
+
+ if assets:
+ (query, tab_asset) = cls.get_rate_data_sql()
+ query.where = tab_asset.id.in_([x.id for x in assets])
+ curr_digits = {x.id: x.currency_digits for x in assets}
+
+ cursor.execute(*query)
+ records = cursor.fetchall()
+
+ for record in records:
+ (id1, rate1, date1, id_rate) = record
+
+ curr_dig = curr_digits.get(id1, 4)
+ exp = Decimal(Decimal(1) / 10 ** curr_dig)
+
+ values = {
+ 'rate': record[1].quantize(exp),
+ 'date': record[2],
+ 'change_symbol': id_rate,
+ }
+
+ for name in names:
+ result[name][record[0]] = values[name]
+ return result
+
+ @classmethod
+ def search_date(cls, names, clause):
+ """ search in date
+ """
+ (tab_query, tab_asset) = cls.get_rate_data_sql()
Operator = fields.SQL_OPERATORS[clause[1]]
+
+ query = tab_query.select(
+ tab_query.id,
+ where=Operator(tab_query.date, clause[2]))
+ return [('id', 'in', query)]
+
+ @classmethod
+ def search_rate(cls, names, clause):
+ """ search in rate
+ """
+ (tab_query, tab_asset) = cls.get_rate_data_sql()
+ Operator = fields.SQL_OPERATORS[clause[1]]
+
+ query = tab_query.select(
+ tab_query.id,
+ where=Operator(tab_query.rate, clause[2]))
+ return [('id', 'in', query)]
+
+ @staticmethod
+ def order_date(tables):
+ """ order date
+ """
+ (tab_query, tab_asset) = Asset.get_rate_data_sql()
+ table, _ = tables[None]
+
+ query = tab_query.select(
+ tab_query.date,
+ where=tab_query.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_rate(tables):
+ """ order rate
+ """
+ (tab_query, tab_asset) = Asset.get_rate_data_sql()
+ table, _ = tables[None]
+
+ query = tab_query.select(
+ tab_query.rate,
+ where=tab_query.id == table.id)
+ return [query]
+
+ @classmethod
+ def get_percentage_sql(cls, days=0, asset_ids=None):
+ """ get table for percentages and dates,
+ days: delta-days to past to select percent-value
+ 0=yesterday, 30=last month, ...
+ """
+ pool = Pool()
+ Rate = pool.get('investment.rate')
+ tab_rate1 = Rate.__table__()
+ tab_rate2 = Rate.__table__()
context = Transaction().context
- query_date = context.get('qdate', IrDate.today())
- query_time = context.get('qtime', CurrentTime())
+ query_date = context.get('qdate', CurrentDate())
+ where_asset = tab_rate1.date <= query_date
+ if isinstance(asset_ids, list):
+ where_asset &= tab_rate1.asset.in_(asset_ids)
- query = tab_asset.join(tab_rate,
- condition=(tab_asset.id==tab_rate.asset) & \
- (tab_rate.date == query_date),
- type_ = 'LEFT OUTER',
- ).select(tab_asset.id,
- where=Operator(
- Case(
- ((tab_rate.id == None) & \
- (tab_asset.updtsource != None) & \
- (tab_asset.updttime <= query_time), True),
- default_ = False,
- ),
- clause[2]),
- )
- return [('id', '=', query)]
+ tab_today = tab_rate1.select(
+ tab_rate1.asset.as_('id'),
+ tab_rate1.date,
+ tab_rate1.rate,
+ distinct_on=[tab_rate1.asset],
+ order_by=[tab_rate1.asset, tab_rate1.date.desc],
+ where=where_asset)
+
+ days_diff = days + 5
+ query = tab_today.join(
+ tab_rate2,
+ condition=(tab_today.id == tab_rate2.asset) &
+ (tab_today.date > (tab_rate2.date + days)) &
+ (tab_today.date <= (tab_rate2.date + days_diff)),
+ type_='LEFT OUTER',
+ ).select(
+ tab_today.id,
+ tab_today.date,
+ tab_today.rate,
+ (tab_today.rate * 100.0 / NullIf(tab_rate2.rate, 0.00) -
+ 100.0).as_('percent'),
+ distinct_on=[tab_today.id],
+ order_by=[tab_today.id, tab_rate2.date.desc])
+ return query
+
+ @staticmethod
+ def order_change_day1(tables):
+ """ order day1
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_asset = Asset2.get_percentage_sql(days=0)
+ table, _ = tables[None]
+
+ query = tab_asset.select(
+ tab_asset.percent,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_change_month1(tables):
+ """ order month1
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_asset = Asset2.get_percentage_sql(days=30)
+ table, _ = tables[None]
+
+ query = tab_asset.select(
+ tab_asset.percent,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_change_month3(tables):
+ """ order month1
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_asset = Asset2.get_percentage_sql(days=90)
+ table, _ = tables[None]
+
+ query = tab_asset.select(
+ tab_asset.percent,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_change_month6(tables):
+ """ order month1
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_asset = Asset2.get_percentage_sql(days=180)
+ table, _ = tables[None]
+
+ query = tab_asset.select(
+ tab_asset.percent,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_change_month12(tables):
+ """ order month1
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_asset = Asset2.get_percentage_sql(days=365)
+ table, _ = tables[None]
+
+ query = tab_asset.select(
+ tab_asset.percent,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @classmethod
+ def search_percentage(cls, names, clause):
+ """ search for percentages
+ """
+ Operator = fields.SQL_OPERATORS[clause[1]]
+ field_name = clause[0][len('change_'):]
+ tab_percent = cls.get_percentage_sql(days={
+ 'day1': 0,
+ 'month1': 30,
+ 'month3': 90,
+ 'month6': 180,
+ 'month12': 365,
+ }[field_name])
+
+ query = tab_percent.select(
+ tab_percent.id,
+ where=Operator(Round(tab_percent.percent, 2), clause[2]))
+ return [('id', 'in', query)]
+
+ @classmethod
+ def get_percentage_change(cls, assets, names):
+ """ get percentage per period
+ """
+ cursor = Transaction().connection.cursor()
+
+ result = {x: {y.id: None for y in assets} for x in names}
+ exp = Decimal(Decimal(1) / 10 ** digits_percent)
+ asset_id_lst = [x.id for x in assets]
+
+ if asset_id_lst and names:
+ for x in names:
+ tab_percent = cls.get_percentage_sql(
+ days={
+ 'change_day1': 0,
+ 'change_month1': 30,
+ 'change_month3': 90,
+ 'change_month6': 180,
+ 'change_month12': 365,
+ }[x],
+ asset_ids=asset_id_lst)
+ cursor.execute(*tab_percent)
+ records = cursor.fetchall()
+
+ for record in records:
+ result[x][record[0]] = record[3].quantize(exp) \
+ if record[3] is not None else None
+ return result
+
+ @classmethod
+ def get_next_update_datetime_sql(cls):
+ """ get sql for datetime of next planned update
+ """
+ pool = Pool()
+ Asset = pool.get('investment.asset')
+ AssetSourceRel = pool.get('investment.asset_source_rel')
+ Rate = pool.get('investment.rate')
+ tab_asset = Asset.__table__()
+ tab_rate = Rate.__table__()
+ tab_rel = AssetSourceRel.__table__()
+ context = Transaction().context
+
+ query_date = context.get('qdate', CurrentDate() - Literal(1))
+
+ # get last date of rate
+ tab_date = tab_asset.join(
+ tab_rel,
+ # link to asset-source-relation to check if
+ # there are online-sources set
+ condition=tab_rel.asset == tab_asset.id,
+ ).join(
+ tab_rate,
+ condition=tab_asset.id == tab_rate.asset,
+ type_='LEFT OUTER',
+ ).select(
+ tab_asset.id,
+ (Coalesce(tab_rate.date, query_date) + Literal(1)).as_('date'),
+ tab_asset.updtdays,
+ tab_asset.updttime,
+ distinct_on=[tab_asset.id],
+ order_by=[tab_asset.id, tab_rate.date.desc])
+
+ query = tab_date.select(
+ tab_date.id,
+ (Case(
+ ((tab_date.updtdays == 'work') &
+ (Extract('dow', tab_date.date) == 0),
+ tab_date.date + Literal(1)),
+ ((tab_date.updtdays == 'work') &
+ (Extract('dow', tab_date.date) == 6),
+ tab_date.date + Literal(2)),
+ else_=tab_date.date,
+ ) + tab_date.updttime).as_('updttime'))
+ return query
+
+ @classmethod
+ def get_nextupdates(cls, assets, names):
+ """ get timestamp of next update
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_updt = Asset2.get_next_update_datetime_sql()
+ cursor = Transaction().connection.cursor()
+
+ query = tab_updt.select(
+ tab_updt.id,
+ tab_updt.updttime,
+ where=tab_updt.id.in_([x.id for x in assets]))
+ cursor.execute(*query)
+ records = cursor.fetchall()
+
+ result = {x: {y.id: None for y in assets} for x in names}
+
+ for record in records:
+ (id1, updt) = record
+ r1 = {'nextupdate': updt}
+
+ for n in names:
+ result[n][id1] = r1[n]
+ return result
+
+ @classmethod
+ def search_nextupdate(cls, names, clause):
+ """ search for assets to update
+ """
+ Asset2 = Pool().get('investment.asset')
+ tab_updt = Asset2.get_next_update_datetime_sql()
+ Operator = fields.SQL_OPERATORS[clause[1]]
+
+ query = tab_updt.select(
+ tab_updt.id,
+ where=Operator(tab_updt.updttime, clause[2]))
+ return [('id', 'in', query)]
@classmethod
def get_identifier_sql(cls, tab_asset):
@@ -222,28 +707,94 @@ class Asset(ModelSQL, ModelView):
tab_secsymb = Identifier.__table__()
tab_isin = Identifier.__table__()
- query = tab_asset.join(tab_prod,
- condition=tab_asset.product==tab_prod.id,
- ).join(tab_wkn,
- condition=(tab_prod.id==tab_wkn.product) & \
- (tab_wkn.type == 'wkn'),
- type_ = 'LEFT OUTER',
- ).join(tab_secsymb,
- condition=(tab_prod.id==tab_secsymb.product) & \
- (tab_secsymb.type == 'secsymb'),
- type_ = 'LEFT OUTER',
- ).join(tab_isin,
- condition=(tab_prod.id==tab_isin.product) & \
- (tab_isin.type == 'isin'),
- type_ = 'LEFT OUTER',
+ query = tab_asset.join(
+ tab_prod,
+ condition=tab_asset.product == tab_prod.id,
+ ).join(
+ tab_wkn,
+ condition=(tab_prod.id == tab_wkn.product) &
+ (tab_wkn.type == 'wkn'),
+ type_='LEFT OUTER',
+ ).join(
+ tab_secsymb,
+ condition=(tab_prod.id == tab_secsymb.product) &
+ (tab_secsymb.type == 'secsymb'),
+ type_='LEFT OUTER',
+ ).join(
+ tab_isin,
+ condition=(tab_prod.id == tab_isin.product) &
+ (tab_isin.type == 'isin'),
+ type_='LEFT OUTER',
).select(
tab_asset.id,
tab_wkn.code.as_('wkn'),
tab_secsymb.code.as_('secsymb'),
- tab_isin.code.as_('isin'),
- )
+ tab_isin.code.as_('isin'))
return query
+ @staticmethod
+ def order_name(tables):
+ """ order name
+ """
+ pool = Pool()
+ Templ = pool.get('product.template')
+ Product = pool.get('product.product')
+ Asset = pool.get('investment.asset')
+ table, _ = tables[None]
+ tab_asset = Asset.__table__()
+ tab_prod = Product.__table__()
+ tab_templ = Templ.__table__()
+
+ query = tab_asset.join(
+ tab_prod,
+ condition=tab_asset.product == tab_prod.id
+ ).join(
+ tab_templ,
+ condition=tab_templ.id == tab_prod.template
+ ).select(
+ tab_templ.name,
+ where=tab_asset.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_wkn(tables):
+ """ order wkn
+ """
+ Asset = Pool().get('investment.asset')
+ tab_ids = Asset.get_identifier_sql(Asset.__table__())
+ table, _ = tables[None]
+
+ query = tab_ids.select(
+ getattr(tab_ids, 'wkn'),
+ where=tab_ids.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_isin(tables):
+ """ order isin
+ """
+ Asset = Pool().get('investment.asset')
+ tab_ids = Asset.get_identifier_sql(Asset.__table__())
+ table, _ = tables[None]
+
+ query = tab_ids.select(
+ getattr(tab_ids, 'isin'),
+ where=tab_ids.id == table.id)
+ return [query]
+
+ @staticmethod
+ def order_secsymb(tables):
+ """ order secsymb
+ """
+ Asset = Pool().get('investment.asset')
+ tab_ids = Asset.get_identifier_sql(Asset.__table__())
+ table, _ = tables[None]
+
+ query = tab_ids.select(
+ getattr(tab_ids, 'secsymb'),
+ where=tab_ids.id == table.id)
+ return [query]
+
@classmethod
def search_identifier(cls, names, clause):
""" search in identifier
@@ -255,13 +806,13 @@ class Asset(ModelSQL, ModelView):
tab_ids = cls.get_identifier_sql(tab_asset)
field_qu = getattr(tab_ids, names)
- query = tab_ids.join(tab_asset,
- condition=tab_ids.id==tab_asset.id,
+ query = tab_ids.join(
+ tab_asset,
+ condition=tab_ids.id == tab_asset.id,
).select(
tab_asset.id,
- where=Operator(field_qu, clause[2]) & \
- (field_qu != None),
- )
+ where=Operator(field_qu, clause[2]) &
+ (field_qu != DEF_NONE))
return [('id', 'in', query)]
@@ -274,37 +825,49 @@ class Asset(ModelSQL, ModelView):
tab_asset = Asset.__table__()
cursor = Transaction().connection.cursor()
- result = {x:{y.id: None for y in assets} for x in names}
+ result = {x: {y.id: None for y in assets} for x in names}
+ if assets:
+ query = cls.get_identifier_sql(tab_asset)
+ query.where = tab_asset.id.in_([x.id for x in assets])
- query = cls.get_identifier_sql(tab_asset)
- query.where = tab_asset.id.in_([x.id for x in assets])
+ cursor.execute(*query)
+ l1 = cursor.fetchall()
- cursor.execute(*query)
- l1 = cursor.fetchall()
-
- for x in l1:
- (id1, wkn, secsymb, isin) = x
- r1 = {'wkn': wkn, 'secsymb': secsymb, 'isin': isin}
-
- for n in names:
- result[n][id1] = r1[n]
+ for x in l1:
+ (id1, wkn, secsymb, isin) = x
+ r1 = {'wkn': wkn, 'secsymb': secsymb, 'isin': isin}
+ for n in names:
+ result[n][id1] = r1[n]
return result
def get_rec_name(self, name):
""" record name
"""
- return '%(prod)s [%(curr)s/%(unit)s]' % {
+ return '%(prod)s | %(rate)s %(unit)s | %(date)s' % {
'prod': getattr(self.product, 'rec_name', '-'),
- 'curr': getattr(self.currency, 'rec_name', '-'),
- 'unit': getattr(self.uom, 'rec_name', '-'),
- }
+ 'unit': self.symbol,
+ 'rate': Report.format_number(
+ self.rate, lang=None, digits=self.currency_digits or 4)
+ if self.rate is not None else '-',
+ 'date': Report.format_date(self.date)
+ if self.date is not None else '-'}
@classmethod
def search_rec_name(cls, name, clause):
""" search in rec_name
"""
- return [('product.rec_name',) + tuple(clause[1:])]
+ return [
+ 'OR',
+ ('product.rec_name',) + tuple(clause[1:]),
+ ('product.identifiers.code',) + tuple(clause[1:]),
+ ]
+
+ @classmethod
+ def after_update_actions(cls, assets):
+ """ run activities after rate-update
+ """
+ pass
@classmethod
def cron_update(cls):
@@ -313,33 +876,32 @@ class Asset(ModelSQL, ModelView):
pool = Pool()
Asset2 = pool.get('investment.asset')
OnlineSource = pool.get('investment.source')
+ context = Transaction().context
+ query_time = context.get('qdatetime', CurrentTimestamp())
+ to_run_activities = []
for asset in Asset2.search([
- ('updtneeded', '=', True),
- ]):
- OnlineSource.update_rate(asset)
+ ('nextupdate', '<=', query_time)]):
+ if OnlineSource.update_rate(asset):
+ to_run_activities.append(asset)
- @classmethod
- def create(cls, vlist):
- """ add debit/credit
- """
- vlist = [x.copy() for x in vlist]
- for values in vlist:
- if 'updtsource' in values.keys():
- if values['updtsource'] is None:
- values['updttime'] = None
- return super(Asset, cls).create(vlist)
-
- @classmethod
- def write(cls, *args):
- """ deny update if cashbook.line!='open',
- add or update debit/credit
- """
- actions = iter(args)
- for lines, values in zip(actions, actions):
- if 'updtsource' in values.keys():
- if values['updtsource'] is None:
- values['updttime'] = None
- super(Asset, cls).write(*args)
+ if to_run_activities:
+ cls.after_update_actions(to_run_activities)
# end Asset
+
+
+class AssetSourceRel(ModelSQL):
+ 'Asset Source Relation'
+ __name__ = 'investment.asset_source_rel'
+
+ source = fields.Many2One(
+ string='Online Source',
+ required=True, model_name='investment.source',
+ ondelete='CASCADE')
+ asset = fields.Many2One(
+ string='Asset',
+ required=True, model_name='investment.asset',
+ ondelete='CASCADE')
+
+# end AssetSourceRel
diff --git a/asset.xml b/asset.xml
index 6946a72..1328873 100644
--- a/asset.xml
+++ b/asset.xml
@@ -1,5 +1,5 @@
-
@@ -35,6 +35,29 @@ full copyright notices and license terms. -->
+
+
+ Current
+
+
+
+
+
+
+ Inactive
+
+
+
+
+
+
+ All
+
+
+
+
+
+
diff --git a/const.py b/const.py
new file mode 100644
index 0000000..a01e781
--- /dev/null
+++ b/const.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+# This file is part of the cashbook-module from m-ds.de for Tryton.
+# The COPYRIGHT file at the top level of this repository contains the
+# full copyright notices and license terms.
+
+
+DEF_TRUE = True
+DEF_NONE = None
diff --git a/cron.py b/cron.py
index 4dffd61..28ec6d4 100644
--- a/cron.py
+++ b/cron.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-# This file is part of the investment-module from m-ds for Tryton.
+# This file is part of the investment-module from m-ds.de for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
diff --git a/cron.xml b/cron.xml
index 5757c5a..7d1454d 100644
--- a/cron.xml
+++ b/cron.xml
@@ -1,5 +1,5 @@
-
diff --git a/diagram.py b/diagram.py
new file mode 100644
index 0000000..1d654e9
--- /dev/null
+++ b/diagram.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+# This file is part of the investment-module from m-ds.de 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
+from trytond.pool import Pool, PoolMeta
+from trytond.pyson import Eval
+from sql.functions import Function
+from datetime import timedelta
+
+
+class Concat2(Function):
+ """ concat columns
+ """
+ __slots__ = ()
+ _function = 'concat'
+
+# end Concat2
+
+
+class GraphDef(metaclass=PoolMeta):
+ __name__ = 'diagram.graphdef'
+
+ asset = fields.Many2One(
+ string='Asset', model_name='investment.asset',
+ states={
+ 'invisible': Eval('dtype', '') != 'investment.asset',
+ 'required': Eval('dtype', '') == 'investment.asset',
+ }, depends=['dtype'])
+
+ @classmethod
+ def _get_dtypes(cls):
+ """ return list of types
+ """
+ l1 = super(GraphDef, cls)._get_dtypes()
+ l1.append('investment.asset')
+ return l1
+
+ def get_recname_value(self):
+ """ value by dtype
+ """
+ if self.dtype == 'investment.asset':
+ return getattr(self.asset, 'rec_name', '-')
+ return super(GraphDef, self).get_recname_value()
+
+ def get_field_key(self):
+ """ get to read value from json
+ """
+ if self.dtype == 'investment.asset':
+ return 'asset%d' % self.asset.id
+ return super(GraphDef, self).get_field_key()
+
+ def get_scaling_for_investment_asset(self):
+ """ get scaling for currency
+ """
+ Rate = Pool().get('investment.rate')
+
+ if self.scaling == 'fix':
+ return None
+
+ if self.scaling == 'alldata':
+ query = [('asset', '=', self.asset.id)]
+ elif self.scaling == 'view':
+ query = [
+ ('asset', '=', self.asset.id),
+ ('date', '>=', self.chart.used_start_date()),
+ ('date', '<=', self.chart.used_end_date()),
+ ]
+ elif self.scaling == 'six':
+ query = [
+ ('asset', '=', self.asset.id),
+ ('date', '>=', self.chart.used_start_date() -
+ timedelta(days=180)),
+ ('date', '<=', self.chart.used_end_date()),
+ ]
+
+ min_rec = Rate.search(query, limit=1, order=[('rate', 'ASC')])
+ max_rec = Rate.search(query, limit=1, order=[('rate', 'DESC')])
+ min_val = min_rec[0].rate if len(min_rec) > 0 else None
+ max_val = max_rec[0].rate if len(max_rec) > 0 else None
+
+ return self.compute_scaling_factor(min_val, max_val)
+
+# end GraphDef
+
+
+class ChartPoint(metaclass=PoolMeta):
+ __name__ = 'diagram.point'
+
+ @classmethod
+ def get_interpolated_val(cls, keyname, query_date):
+ """ query two neighbour-values to
+ interpolate missing value
+ """
+ Rate = Pool().get('investment.rate')
+
+ if not keyname:
+ return None
+
+ # check if query is for us
+ if keyname.startswith('asset'):
+ asset_id = int(keyname[len('asset'):])
+
+ before = Rate.search([
+ ('date', '<', query_date),
+ ('asset', '=', asset_id),
+ ], limit=1, order=[('date', 'DESC')])
+
+ after = Rate.search([
+ ('date', '>', query_date),
+ ('asset', '=', asset_id),
+ ], limit=1, order=[('date', 'ASC')])
+
+ if (len(before) == 1) and (len(after) == 1):
+ result = cls.interpolate_linear(
+ (after[0].date, after[0].rate),
+ (before[0].date, before[0].rate),
+ query_date
+ )
+ return result
+ elif len(before) == 1:
+ return before[0].rate
+ elif len(after) == 1:
+ return after[0].rate
+ return super(ChartPoint, cls).get_interpolated_val(keyname, query_date)
+
+ @classmethod
+ def get_table_parts(cls):
+ """ return a list of tables to union,
+ table must contain the columns:
+ date, key, val
+ """
+ pool = Pool()
+ Rate = pool.get('investment.rate')
+ tab_rate = Rate.__table__()
+
+ tabparts = super(ChartPoint, cls).get_table_parts()
+
+ # rate
+ tabparts.append(tab_rate.select(
+ tab_rate.date,
+ Concat2('asset', tab_rate.asset).as_('key'),
+ tab_rate.rate.as_('val'),
+ ))
+ return tabparts
+
+# end ChartPoint
diff --git a/diagram.xml b/diagram.xml
new file mode 100644
index 0000000..d8ac9f4
--- /dev/null
+++ b/diagram.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ diagram.graphdef
+
+ graph_form
+
+
+
+
diff --git a/group.xml b/group.xml
index 8061883..5656ff9 100644
--- a/group.xml
+++ b/group.xml
@@ -1,5 +1,5 @@
-
diff --git a/icon.xml b/icon.xml
index 376e9d0..ea62f97 100644
--- a/icon.xml
+++ b/icon.xml
@@ -1,5 +1,5 @@
-
@@ -10,5 +10,15 @@ full copyright notices and license terms. -->
icon/gain_invest.svg
+
+ mds-stockonline
+ icon/stockonline.svg
+
+
+
+ mds-asset
+ icon/asset.svg
+
+
diff --git a/icon/asset.svg b/icon/asset.svg
new file mode 100644
index 0000000..255670f
--- /dev/null
+++ b/icon/asset.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/icon/stockonline.svg b/icon/stockonline.svg
new file mode 100644
index 0000000..2f56e74
--- /dev/null
+++ b/icon/stockonline.svg
@@ -0,0 +1,5 @@
+
+
+
diff --git a/identifier.py b/identifier.py
index ca5692d..334c178 100644
--- a/identifier.py
+++ b/identifier.py
@@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
-# This file is part of the investment-module from m-ds for Tryton.
+# This file is part of the investment-module from m-ds.de 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
from trytond.pool import PoolMeta
diff --git a/import_wiz.py b/import_wiz.py
new file mode 100644
index 0000000..5a4d097
--- /dev/null
+++ b/import_wiz.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+# This file is part of the investment-module from m-ds.de for Tryton.
+# The COPYRIGHT file at the top level of this repository contains the
+# full copyright notices and license terms.
+
+from io import StringIO
+from datetime import datetime, date
+from decimal import Decimal
+import csv
+from trytond.pool import Pool
+from trytond.model import ModelView, fields
+from trytond.wizard import Wizard, StateTransition, StateView, Button
+from trytond.transaction import Transaction
+from trytond.exceptions import UserError
+from trytond.i18n import gettext
+
+
+sel_dec_divider = [
+ (',', ','),
+ ('.', '.'),
+ ]
+
+sel_date_fmt = [
+ ('%d.%m.%Y', 'dd.mm.yyyy'),
+ ('%Y-%m-%d', 'yyyy-mm-dd'),
+ ('%m/%d/%Y', 'mm/dd/yyyy'),
+ ]
+
+sel_field_delimiter = [
+ (';', ';'),
+ (',', ','),
+ ]
+
+
+class ImportWizardStart(ModelView):
+ 'Import CSV-File'
+ __name__ = 'investment.imp_wiz.start'
+
+ asset = fields.Many2One(
+ string='Asset', readonly=True, model_name='investment.asset')
+ file_ = fields.Binary(string="CSV-File", required=True)
+ dec_divider = fields.Selection(
+ string='Decimal divider', required=True, selection=sel_dec_divider)
+ date_fmt = fields.Selection(
+ string='Date format', required=True, selection=sel_date_fmt)
+ field_delimiter = fields.Selection(
+ string='Field delimiter', required=True, selection=sel_field_delimiter)
+
+# end ImportWizardStart
+
+
+class ImportWizard(Wizard):
+ 'Import CSV-File'
+ __name__ = 'investment.imp_wiz'
+
+ start_state = 'start'
+ start = StateView(
+ model_name='investment.imp_wiz.start',
+ view='investment.imp_wiz_start_form',
+ buttons=[
+ Button(string='Cancel', state='end', icon='tryton-cancel'),
+ Button(
+ string='Import File', state='importf',
+ icon='tryton-import', default=True),
+ ])
+ importf = StateTransition()
+
+ def default_start(self, fields):
+ """ show asset
+ """
+ context = Transaction().context
+
+ values = {
+ 'dec_divider': ',',
+ 'date_fmt': '%d.%m.%Y',
+ 'field_delimiter': ';',
+ }
+ values['asset'] = context.get('active_id', None)
+ return values
+
+ def transition_importf(self):
+ """ read file, import
+ """
+ pool = Pool()
+ ImportWiz = pool.get('investment.imp_wiz', type='wizard')
+
+ if self.start.file_:
+ (lines, max_date, min_date) = ImportWiz.read_csv_file(
+ self.start.file_.decode('utf8'),
+ dec_divider=self.start.dec_divider,
+ date_fmt=self.start.date_fmt,
+ delimiter=self.start.field_delimiter)
+
+ if lines:
+ ImportWiz.upload_rates(
+ self.start.asset,
+ lines, min_date, max_date)
+ return 'end'
+
+ @classmethod
+ def upload_rates(cls, asset, rates_list, min_date, max_date):
+ """ upload new rates to asset
+ """
+ Rate = Pool().get('investment.rate')
+
+ # get rate in date-range
+ rates = Rate.search([
+ ('asset.id', '=', asset.id),
+ ('date', '>=', min_date),
+ ('date', '<=', max_date),
+ ])
+ existing_dates = [x.date for x in rates]
+ done_dates = []
+
+ to_create = []
+ for rate in rates_list:
+ if rate['date'] in existing_dates:
+ continue
+ if rate['date'] in done_dates:
+ continue
+
+ to_create.append({
+ 'asset': asset.id,
+ 'date': rate['date'],
+ 'rate': rate['rate'],
+ })
+ done_dates.append(rate['date'])
+
+ if len(to_create) > 0:
+ Rate.create(to_create)
+
+ @classmethod
+ def read_csv_file(cls, file_content, dec_divider, date_fmt, delimiter):
+ """ read file-content from csv
+ """
+ result = []
+
+ del_chars = ['.', ',']
+ del_chars.remove(dec_divider)
+ min_date = None
+ max_date = None
+ min_rate = None
+ max_rate = None
+
+ with StringIO(file_content) as fhdl:
+ csv_lines = csv.DictReader(
+ fhdl,
+ fieldnames=['date', 'rate'],
+ dialect='excel',
+ delimiter=delimiter)
+
+ for line in csv_lines:
+ # skip first line
+ if line.get('date', '') == 'date':
+ continue
+
+ try:
+ date_val = datetime.strptime(
+ line.get('date', None).strip(), date_fmt).date()
+ except Exception:
+ raise UserError(gettext(
+ 'investment.msg_import_err_date',
+ datefmt=date_fmt,
+ colnr='1',
+ ))
+ try:
+ rate_val = line.get('rate', None).replace(
+ del_chars[0], '').strip()
+ rate_val = Decimal(rate_val.replace(dec_divider, '.'))
+ except Exception:
+ raise UserError(gettext(
+ 'investment.msg_import_err_date',
+ datefmt='dd%sdd' % dec_divider,
+ colnr='2'))
+
+ if isinstance(date_val, date) and isinstance(
+ rate_val, Decimal):
+ result.append({'date': date_val, 'rate': rate_val})
+
+ # date range
+ if max_date is None:
+ max_date = date_val
+ else:
+ if max_date < date_val:
+ max_date = date_val
+
+ if min_date is None:
+ min_date = date_val
+ else:
+ if min_date > date_val:
+ min_date = date_val
+
+ # rate range
+ if max_rate is None:
+ max_rate = rate_val
+ else:
+ if max_rate < rate_val:
+ max_rate = rate_val
+
+ if min_rate is None:
+ min_rate = rate_val
+ else:
+ if min_rate > rate_val:
+ min_rate = rate_val
+ else:
+ raise UserError(gettext(
+ 'investment.msg_err_unknown_content',
+ linetxt=line))
+ return (result, max_date, min_date)
+
+# end ImportWizard
diff --git a/import_wiz.xml b/import_wiz.xml
new file mode 100644
index 0000000..809c8b9
--- /dev/null
+++ b/import_wiz.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+ investment.imp_wiz.start
+ form
+ import_start_form
+
+
+
+ Import CSV-File
+ investment.imp_wiz
+
+
+
+
+
+
+
+ form_action
+ investment.asset,-1
+
+
+
+
+
diff --git a/locale/de.po b/locale/de.po
index 0d27885..2e027be 100644
--- a/locale/de.po
+++ b/locale/de.po
@@ -34,6 +34,26 @@ msgctxt "model:ir.message,text:msg_currency_rate_positive"
msgid "A asset rate must be positive."
msgstr "Der Kurs des Vermögenswertes muss positiv sein."
+msgctxt "model:ir.message,text:msg_import_err_date"
+msgid "failed to read column %(colnr)s of file, expected date (format: %(datefmt)s)"
+msgstr "Fehler beim Lesen von Spalte %(colnr)s der Datei, erwartetes Datum (Format: %(datefmt)s)"
+
+msgctxt "model:ir.message,text:msg_err_unknown_content"
+msgid "failed to identify row content: %(linetxt)s"
+msgstr "Zeileninhalt konnte nicht identifiziert werden: %(linetxt)s"
+
+msgctxt "model:ir.message,text:msg_querytype_web"
+msgid "Extract from web page"
+msgstr "aus Webseite auslesen"
+
+msgctxt "model:ir.message,text:msg_missing_url"
+msgid "URL for the online source '%(oname)s' is missing."
+msgstr "URL für die Onlinequelle '%(oname)s' fehlt."
+
+msgctxt "model:ir.message,text:msg_bug_in_regexquery"
+msgid "Error in regex code of field '%(fname)s': %(errmsg)s [%(code)s]"
+msgstr "Fehler in Regex-Code des Feldes '%(fname)s': %(errmsg)s [%(code)s]"
+
##############
# ir.ui.menu #
@@ -66,6 +86,70 @@ msgctxt "model:ir.action,name:updt_source_wiz"
msgid "Update Source"
msgstr "Quelle aktualisieren"
+msgctxt "model:ir.action,name:act_import_wizard"
+msgid "Import CSV-File"
+msgstr "CSV-Datei importieren"
+
+
+###############################
+# ir.action.act_window.domain #
+###############################
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_current"
+msgid "Current"
+msgstr "Aktuell"
+
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_inactive"
+msgid "Inactive"
+msgstr "Inaktiv"
+
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_all"
+msgid "All"
+msgstr "Alle"
+
+
+############################
+# investment.imp_wiz.start #
+############################
+msgctxt "model:investment.imp_wiz.start,name:"
+msgid "Import CSV-File"
+msgstr "CSV-Datei importieren"
+
+msgctxt "field:investment.imp_wiz.start,asset:"
+msgid "Asset"
+msgstr "Vermögenswert"
+
+msgctxt "field:investment.imp_wiz.start,file_:"
+msgid "CSV-File"
+msgstr "CSV-Datei"
+
+msgctxt "field:investment.imp_wiz.start,dec_divider:"
+msgid "Decimal divider"
+msgstr "Dezimaltrenner"
+
+msgctxt "field:investment.imp_wiz.start,date_fmt:"
+msgid "Date format"
+msgstr "Datumsformat"
+
+msgctxt "field:investment.imp_wiz.start,field_delimiter:"
+msgid "Field delimiter"
+msgstr "Feldtrenner"
+
+
+######################
+# investment.imp_wiz #
+######################
+msgctxt "model:investment.imp_wiz,name:"
+msgid "Import CSV-File"
+msgstr "CSV-Datei importieren"
+
+msgctxt "wizard_button:investment.imp_wiz,start,end:"
+msgid "Cancel"
+msgstr "Abbruch"
+
+msgctxt "wizard_button:investment.imp_wiz,start,importf:"
+msgid "Import File"
+msgstr "Datei importieren"
+
############################
# investment.source_update #
@@ -86,6 +170,10 @@ msgctxt "view:investment.asset:"
msgid "Currency and Units"
msgstr "Währung und Einheiten"
+msgctxt "view:investment.asset:"
+msgid "Gain and Loss"
+msgstr "Gewinn und Verlust"
+
msgctxt "view:investment.asset:"
msgid "Identifiers"
msgstr "Bezeichner"
@@ -102,21 +190,13 @@ msgctxt "field:investment.asset,company:"
msgid "Company"
msgstr "Unternehmen"
-msgctxt "field:investment.asset,company_currency:"
-msgid "Company Currency"
-msgstr "Unternehmenswährung"
-
-msgctxt "field:investment.asset,company_currency_digits:"
-msgid "Currency Digits (Ref.)"
-msgstr "Nachkommastellen Währung (Ref.)"
-
msgctxt "field:investment.asset,currency:"
msgid "Currency"
msgstr "Währung"
msgctxt "field:investment.asset,currency_digits:"
-msgid "Currency Digits"
-msgstr "Nachkommastellen Währung"
+msgid "Digits"
+msgstr "Nachkommastellen"
msgctxt "field:investment.asset,product:"
msgid "Product"
@@ -134,6 +214,14 @@ msgctxt "field:investment.asset,uom:"
msgid "UOM"
msgstr "Einheit"
+msgctxt "field:investment.asset,symbol:"
+msgid "UOM"
+msgstr "Einheit"
+
+msgctxt "field:investment.asset,asset_symbol:"
+msgid "Symbol"
+msgstr "Symbol"
+
msgctxt "field:investment.asset,wkn:"
msgid "NSIN"
msgstr "WKN"
@@ -162,25 +250,121 @@ msgctxt "field:investment.asset,rates:"
msgid "Rates"
msgstr "Kurse"
+msgctxt "field:investment.asset,name:"
+msgid "Name"
+msgstr "Name"
+
msgctxt "field:investment.asset,rate:"
msgid "Current Rate"
msgstr "aktueller Kurs"
-msgctxt "field:investment.asset,updtsource:"
-msgid "Update Source"
-msgstr "Kursquelle"
+msgctxt "field:investment.asset,date:"
+msgid "Date"
+msgstr "Datum"
-msgctxt "help:investment.asset,updtsource:"
-msgid "Select a source for the course update."
-msgstr "Wählen Sie eine Quelle für die Kursaktualisierung aus."
+msgctxt "help:investment.asset,date:"
+msgid "Date of current rate"
+msgstr "Datum des aktuellen Kurses"
+
+msgctxt "field:investment.asset,updtsources:"
+msgid "Update Sources"
+msgstr "Kursquellen"
+
+msgctxt "help:investment.asset,updtsources:"
+msgid "Select sources for the course update. The course sources are tried until a valid value has been read."
+msgstr "Wählen Sie Quellen für die Kursaktualisierung aus. Die Kursquellen werden probiert bis ein gültiger Wert gelesen wurde."
msgctxt "field:investment.asset,updttime:"
msgid "Time"
msgstr "Zeitpunkt"
-msgctxt "field:investment.asset,updtneeded:"
-msgid "Course update needed"
-msgstr "Kursaktualisierung nötig"
+msgctxt "field:investment.asset,nextupdate:"
+msgid "Next Update"
+msgstr "nächste Aktualisierung"
+
+msgctxt "field:investment.asset,change_day1:"
+msgid "Previous Day"
+msgstr "Vortag"
+
+msgctxt "help:investment.asset,change_day1:"
+msgid "percentage change in value compared to the previous day"
+msgstr "prozentuale Wertänderung zum Vortag"
+
+msgctxt "field:investment.asset,change_month1:"
+msgid "1 Month"
+msgstr "1 Monat"
+
+msgctxt "help:investment.asset,change_month1:"
+msgid "percentage change in value compared to last month"
+msgstr "prozentuale Wertänderung zum letzten Monat"
+
+msgctxt "field:investment.asset,change_month3:"
+msgid "3 Months"
+msgstr "3 Monate"
+
+msgctxt "help:investment.asset,change_month3:"
+msgid "percentage change in value during 3 months"
+msgstr "Prozentuale Wertänderung während 3 Monate"
+
+msgctxt "field:investment.asset,change_month6:"
+msgid "6 Months"
+msgstr "6 Monate"
+
+msgctxt "help:investment.asset,change_month6:"
+msgid "percentage change in value during 6 months"
+msgstr "Prozentuale Wertänderung während 6 Monate"
+
+msgctxt "field:investment.asset,change_month12:"
+msgid "1 Year"
+msgstr "1 Jahr"
+
+msgctxt "help:investment.asset,change_month12:"
+msgid "percentage change in value during 1 year"
+msgstr "Prozentuale Wertänderung während 1 Jahr"
+
+msgctxt "field:investment.asset,change_symbol:"
+msgid "Symbol"
+msgstr "Symbol"
+
+msgctxt "field:investment.asset,updtdays:"
+msgid "Select days"
+msgstr "Tage wählen"
+
+msgctxt "selection:investment.asset,updtdays:"
+msgid "Mon - Fri"
+msgstr "Mo - Fr"
+
+msgctxt "selection:investment.asset,updtdays:"
+msgid "Mon - Sun"
+msgstr "Mo - So"
+
+msgctxt "field:investment.asset,updturl:"
+msgid "URL"
+msgstr "URL"
+
+msgctxt "help:investment.asset,updturl:"
+msgid "URL for data retrieval."
+msgstr "URL für Datenabruf."
+
+msgctxt "field:investment.asset,updturl_enable:"
+msgid "URL required"
+msgstr "URL required"
+
+
+###############################
+# investment.asset-source-rel #
+###############################
+msgctxt "model:investment.asset_source_rel,name:"
+msgid "Asset Source Relation"
+msgstr "Vermögenswert Quelle Verknüpfung"
+
+msgctxt "field:investment.asset_source_rel,source:"
+msgid "Source"
+msgstr "Quelle"
+
+msgctxt "field:investment.asset_source_rel,asset:"
+msgid "Asset"
+msgstr "Vermögenswert"
#####################
@@ -226,6 +410,10 @@ msgctxt "view:investment.source:"
msgid "Purely javascript-based websites do not work here."
msgstr "Rein javascriptbasierte Webseiten funktionieren hier nicht."
+msgctxt "view:investment.source:"
+msgid "Method: Extract from web page"
+msgstr "Methode: aus Webseite auslesen"
+
msgctxt "field:investment.source,name:"
msgid "Name"
msgstr "Name"
@@ -354,6 +542,22 @@ msgctxt "help:investment.source,fndident:"
msgid "Identifier found during test query."
msgstr "Bei der Testabfrage gefundener Bezeichner."
+msgctxt "field:investment.source,query_method:"
+msgid "Method"
+msgstr "Methode"
+
+msgctxt "help:investment.source,query_method:"
+msgid "Select the method to retrieve the data."
+msgstr "Wählen Sie die Methode zum Abruf der Daten."
+
+msgctxt "field:investment.source,fixed_url:"
+msgid "Fixed URL"
+msgstr "feste URL"
+
+msgctxt "help:investment.source,fixed_url:"
+msgid "URL must be defined at investment record."
+msgstr "Die URL muss im Investitionsdatensatz definiert werden."
+
###################
# investment.rate #
@@ -393,3 +597,15 @@ msgstr "Wertpapierkennnummer (WKN)"
msgctxt "selection:product.identifier,type:"
msgid "Stock market symbol"
msgstr "Börsensymbol"
+
+
+####################
+# diagram.graphdef #
+####################
+msgctxt "view:diagram.graphdef:"
+msgid "Asset"
+msgstr "Vermögenswert"
+
+msgctxt "field:diagram.graphdef,asset:"
+msgid "Asset"
+msgstr "Vermögenswert"
diff --git a/locale/en.po b/locale/en.po
index 2ef7e68..7d76175 100644
--- a/locale/en.po
+++ b/locale/en.po
@@ -22,6 +22,26 @@ msgctxt "model:ir.message,text:msg_currency_rate_positive"
msgid "A asset rate must be positive."
msgstr "A asset rate must be positive."
+msgctxt "model:ir.message,text:msg_import_err_date"
+msgid "failed to read column %(colnr)s of file, expected date (format: %(datefmt)s)"
+msgstr "failed to read column %(colnr)s of file, expected date (format: %(datefmt)s)"
+
+msgctxt "model:ir.message,text:msg_err_unknown_content"
+msgid "failed to identify row content: %(linetxt)s"
+msgstr "failed to identify row content: %(linetxt)s"
+
+msgctxt "model:ir.message,text:msg_querytype_web"
+msgid "Extract from web page"
+msgstr "Extract from web page"
+
+msgctxt "model:ir.message,text:msg_missing_url"
+msgid "URL for the online source '%(oname)s' is missing."
+msgstr "URL for the online source '%(oname)s' is missing."
+
+msgctxt "model:ir.message,text:msg_bug_in_regexquery"
+msgid "Error in regex code of field '%(fname)s': %(errmsg)s [%(code)s]"
+msgstr "Error in regex code of field '%(fname)s': %(errmsg)s [%(code)s]"
+
msgctxt "model:ir.ui.menu,name:menu_investment"
msgid "Investment"
msgstr "Investment"
@@ -46,6 +66,58 @@ msgctxt "model:ir.action,name:updt_source_wiz"
msgid "Update Source"
msgstr "Update Source"
+msgctxt "model:ir.action,name:act_import_wizard"
+msgid "Import CSV-File"
+msgstr "Import CSV-File"
+
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_current"
+msgid "Current"
+msgstr "Current"
+
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_inactive"
+msgid "Inactive"
+msgstr "Inactive"
+
+msgctxt "model:ir.action.act_window.domain,name:act_asset_domain_all"
+msgid "All"
+msgstr "All"
+
+msgctxt "model:investment.imp_wiz.start,name:"
+msgid "Import CSV-File"
+msgstr "Import CSV-File"
+
+msgctxt "field:investment.imp_wiz.start,asset:"
+msgid "Asset"
+msgstr "Asset"
+
+msgctxt "field:investment.imp_wiz.start,file_:"
+msgid "CSV-File"
+msgstr "CSV-File"
+
+msgctxt "field:investment.imp_wiz.start,dec_divider:"
+msgid "Decimal divider"
+msgstr "Decimal divider"
+
+msgctxt "field:investment.imp_wiz.start,date_fmt:"
+msgid "Date format"
+msgstr "Date format"
+
+msgctxt "field:investment.imp_wiz.start,field_delimiter:"
+msgid "Field delimiter"
+msgstr "Field delimiter"
+
+msgctxt "model:investment.imp_wiz,name:"
+msgid "Import CSV-File"
+msgstr "Import CSV-File"
+
+msgctxt "wizard_button:investment.imp_wiz,start,end:"
+msgid "Cancel"
+msgstr "Cancel"
+
+msgctxt "wizard_button:investment.imp_wiz,start,importf:"
+msgid "Import File"
+msgstr "Import File"
+
msgctxt "model:investment.source_update,name:"
msgid "Update Source"
msgstr "Update Source"
@@ -58,6 +130,10 @@ msgctxt "view:investment.asset:"
msgid "Currency and Units"
msgstr "Currency and Units"
+msgctxt "view:investment.asset:"
+msgid "Gain and Loss"
+msgstr "Gain and Loss"
+
msgctxt "view:investment.asset:"
msgid "Identifiers"
msgstr "Identifiers"
@@ -74,21 +150,13 @@ msgctxt "field:investment.asset,company:"
msgid "Company"
msgstr "Company"
-msgctxt "field:investment.asset,company_currency:"
-msgid "Company Currency"
-msgstr "Company Currency"
-
-msgctxt "field:investment.asset,company_currency_digits:"
-msgid "Currency Digits (Ref.)"
-msgstr "Currency Digits (Ref.)"
-
msgctxt "field:investment.asset,currency:"
msgid "Currency"
msgstr "Currency"
msgctxt "field:investment.asset,currency_digits:"
-msgid "Currency Digits"
-msgstr "Currency Digits"
+msgid "Digits"
+msgstr "Digits"
msgctxt "field:investment.asset,product:"
msgid "Product"
@@ -106,6 +174,14 @@ msgctxt "field:investment.asset,uom:"
msgid "UOM"
msgstr "UOM"
+msgctxt "field:investment.asset,symbol:"
+msgid "UOM"
+msgstr "UOM"
+
+msgctxt "field:investment.asset,asset_symbol:"
+msgid "Symbol"
+msgstr "Symbol"
+
msgctxt "field:investment.asset,wkn:"
msgid "NSIN"
msgstr "NSIN"
@@ -134,25 +210,117 @@ msgctxt "field:investment.asset,rates:"
msgid "Rates"
msgstr "Rates"
+msgctxt "field:investment.asset,name:"
+msgid "Name"
+msgstr "Name"
+
msgctxt "field:investment.asset,rate:"
msgid "Current Rate"
msgstr "Current Rate"
-msgctxt "field:investment.asset,updtsource:"
-msgid "Update Source"
-msgstr "Update Source"
+msgctxt "field:investment.asset,date:"
+msgid "Date"
+msgstr "Date"
-msgctxt "help:investment.asset,updtsource:"
-msgid "Select a source for the course update."
-msgstr "Select a source for the course update."
+msgctxt "help:investment.asset,date:"
+msgid "Date of current rate"
+msgstr "Date of current rate"
+
+msgctxt "field:investment.asset,updtsources:"
+msgid "Update Sources"
+msgstr "Update Sources"
+
+msgctxt "help:investment.asset,updtsources:"
+msgid "Select sources for the course update. The course sources are tried until a valid value has been read."
+msgstr "Select sources for the course update. The course sources are tried until a valid value has been read."
msgctxt "field:investment.asset,updttime:"
msgid "Time"
msgstr "Time"
-msgctxt "field:investment.asset,updtneeded:"
-msgid "Course update needed"
-msgstr "Course update needed"
+msgctxt "field:investment.asset,nextupdate:"
+msgid "Next Update"
+msgstr "Next Update"
+
+msgctxt "field:investment.asset,change_day1:"
+msgid "Previous Day"
+msgstr "Previous Day"
+
+msgctxt "help:investment.asset,change_day1:"
+msgid "percentage change in value compared to the previous day"
+msgstr "percentage change in value compared to the previous day"
+
+msgctxt "field:investment.asset,change_month1:"
+msgid "1 Month"
+msgstr "1 Month"
+
+msgctxt "help:investment.asset,change_month1:"
+msgid "percentage change in value compared to last month"
+msgstr "percentage change in value compared to last month"
+
+msgctxt "field:investment.asset,change_month3:"
+msgid "3 Months"
+msgstr "3 Months"
+
+msgctxt "help:investment.asset,change_month3:"
+msgid "percentage change in value during 3 months"
+msgstr "percentage change in value during 3 months"
+
+msgctxt "field:investment.asset,change_month6:"
+msgid "6 Months"
+msgstr "6 Months"
+
+msgctxt "help:investment.asset,change_month6:"
+msgid "percentage change in value during 6 months"
+msgstr "percentage change in value during 6 months"
+
+msgctxt "field:investment.asset,change_month12:"
+msgid "1 Year"
+msgstr "1 Year"
+
+msgctxt "help:investment.asset,change_month12:"
+msgid "percentage change in value during 1 year"
+msgstr "percentage change in value during 1 year"
+
+msgctxt "field:investment.asset,change_symbol:"
+msgid "Symbol"
+msgstr "Symbol"
+
+msgctxt "field:investment.asset,updtdays:"
+msgid "Select days"
+msgstr "Select days"
+
+msgctxt "selection:investment.asset,updtdays:"
+msgid "Mon - Fri"
+msgstr "Mon - Fri"
+
+msgctxt "selection:investment.asset,updtdays:"
+msgid "Mon - Sun"
+msgstr "Mon - Sun"
+
+msgctxt "field:investment.asset,updturl:"
+msgid "URL"
+msgstr "URL"
+
+msgctxt "help:investment.asset,updturl:"
+msgid "URL for data retrieval."
+msgstr "URL for data retrieval."
+
+msgctxt "field:investment.asset,updturl_enable:"
+msgid "URL required"
+msgstr "URL required"
+
+msgctxt "model:investment.asset_source_rel,name:"
+msgid "Asset Source Relation"
+msgstr "Asset Source Relation"
+
+msgctxt "field:investment.asset_source_rel,source:"
+msgid "Source"
+msgstr "Source"
+
+msgctxt "field:investment.asset_source_rel,asset:"
+msgid "Asset"
+msgstr "Asset"
msgctxt "model:investment.source,name:"
msgid "Online Source"
@@ -194,6 +362,10 @@ msgctxt "view:investment.source:"
msgid "Purely javascript-based websites do not work here."
msgstr "Purely javascript-based websites do not work here."
+msgctxt "view:investment.source:"
+msgid "Method: Extract from web page"
+msgstr "Method: Extract from web page"
+
msgctxt "field:investment.source,name:"
msgid "Name"
msgstr "Name"
@@ -322,6 +494,22 @@ msgctxt "help:investment.source,fndident:"
msgid "Identifier found during test query."
msgstr "Identifier found during test query."
+msgctxt "field:investment.source,query_method:"
+msgid "Method"
+msgstr "Method"
+
+msgctxt "help:investment.source,query_method:"
+msgid "Select the method to retrieve the data."
+msgstr "Select the method to retrieve the data."
+
+msgctxt "field:investment.source,fixed_url:"
+msgid "Fixed URL"
+msgstr "Fixed URL"
+
+msgctxt "help:investment.source,fixed_url:"
+msgid "URL must be defined at investment record."
+msgstr "URL must be defined at investment record."
+
msgctxt "model:investment.rate,name:"
msgid "Rate"
msgstr "Rate"
@@ -350,3 +538,11 @@ msgctxt "selection:product.identifier,type:"
msgid "National Securities Identifying Number (NSIN)"
msgstr "National Securities Identifying Number (NSIN)"
+msgctxt "selection:product.identifier,type:"
+msgid "Stock market symbol"
+msgstr "Stock market symbol"
+
+msgctxt "view:diagram.graphdef:"
+msgid "Asset"
+msgstr "Asset"
+
diff --git a/menu.xml b/menu.xml
index 2ab5feb..cc3b8ec 100644
--- a/menu.xml
+++ b/menu.xml
@@ -1,5 +1,5 @@
-
@@ -19,7 +19,7 @@ full copyright notices and license terms. -->
+ icon="mds-stockonline" parent="menu_investment"/>