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"/> @@ -27,7 +27,7 @@ full copyright notices and license terms. --> + icon="mds-asset" parent="menu_investment"/> diff --git a/message.xml b/message.xml index a17315c..2d681ef 100644 --- a/message.xml +++ b/message.xml @@ -1,5 +1,5 @@ - @@ -11,6 +11,21 @@ full copyright notices and license terms. --> A asset rate must be positive. + + failed to read column %(colnr)s of file, expected date (format: %(datefmt)s) + + + failed to identify row content: %(linetxt)s + + + Extract from web page + + + URL for the online source '%(oname)s' is missing. + + + Error in regex code of field '%(fname)s': %(errmsg)s [%(code)s] + diff --git a/onlinesource.py b/onlinesource.py index cf9a858..abda8b4 100644 --- a/onlinesource.py +++ b/onlinesource.py @@ -1,16 +1,20 @@ # -*- 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 string import Template -import requests, logging, html2text, re +import requests +import logging +import html2text +import re from datetime import datetime from decimal import Decimal -from trytond.model import ModelView, ModelSQL, fields, Unique, Check -from trytond.transaction import Transaction +from trytond.model import ModelView, ModelSQL, fields from trytond.pool import Pool from trytond.pyson import Eval, Bool +from trytond.i18n import gettext +from trytond.exceptions import UserError logger = logging.getLogger(__name__) @@ -28,63 +32,115 @@ sel_rgxidtype = [ sel_rgxdatefmt = [ ('%d.%m.%Y', 'dd.mm.yyyy'), + ('%d.%m.%y', 'dd.mm.yy'), ('%m/%d/%Y', 'mm/dd/yyyy'), + ('%m/%d/%y', 'mm/dd/yy'), ('%Y-%m-%d', 'yyyy-mm-dd'), + ('%b %d %Y', 'mon dd yyyy'), ] -fields_check = ['url', 'nsin', 'isin', 'symbol', 'text', 'http_state', \ +fields_check = [ + 'url', 'nsin', 'isin', 'symbol', 'text', 'http_state', 'fnddate', 'fndrate', 'fndident'] +STATES_WEB = { + 'invisible': Eval('query_method', '') != 'web', + 'required': Eval('query_method', '') == 'web', + } +DEPENDS_WEB = ['query_method'] + + class OnlineSource(ModelSQL, ModelView): 'Online Source' __name__ = 'investment.source' name = fields.Char(string='Name', required=True) - url = fields.Char(string='URL', required=True) - nohtml = fields.Boolean(string='Remove HTML', - help='Removes HTML tags before the text is interpreted.') - rgxdate = fields.Char(string='Date', required=True, - help='Regex code to find the date in the downloaded HTML file.') - rgxdatefmt = fields.Selection(string='Date format', required=True, - selection=sel_rgxdatefmt) - rgxrate = fields.Char(string='Rate', required=True, - help='Regex code to find the rate in the downloaded HTML file.') - rgxdecimal = fields.Selection(string='Decimal Separator', required=True, - help='Decimal separator for converting the market value into a number.', - selection=sel_rgxdecimal) - rgxident = fields.Char(string='Identifier', - help='Regex code to find the identifier in the downloaded HTML file.') - rgxidtype = fields.Selection(string='ID-Type', selection=sel_rgxidtype, + query_method = fields.Selection( + string='Method', required=True, + help='Select the method to retrieve the data.', + selection='get_query_methods') + url = fields.Char(string='URL', states=STATES_WEB, depends=DEPENDS_WEB) + fixed_url = fields.Boolean( + string='Fixed URL', + states={'invisible': Eval('query_method', '') != 'web'}, + depends=DEPENDS_WEB, + help='URL must be defined at investment record.') + nohtml = fields.Boolean( + string='Remove HTML', + help='Removes HTML tags before the text is interpreted.', + states={'invisible': STATES_WEB['invisible']}, + depends=DEPENDS_WEB) + rgxdate = fields.Char( + string='Date', + help='Regex code to find the date in the downloaded HTML file.', + states=STATES_WEB, depends=DEPENDS_WEB) + rgxdatefmt = fields.Selection( + string='Date format', selection=sel_rgxdatefmt, + states=STATES_WEB, depends=DEPENDS_WEB) + rgxrate = fields.Char( + string='Rate', + help='Regex code to find the rate in the downloaded HTML file.', + states=STATES_WEB, depends=DEPENDS_WEB) + rgxdecimal = fields.Selection( + string='Decimal Separator', + help='Decimal separator for converting the market ' + + 'value into a number.', + selection=sel_rgxdecimal, states=STATES_WEB, depends=DEPENDS_WEB) + rgxident = fields.Char( + string='Identifier', + help='Regex code to find the identifier in the downloaded HTML file.', + states={'invisible': STATES_WEB['invisible']}, + depends=DEPENDS_WEB) + rgxidtype = fields.Selection( + string='ID-Type', selection=sel_rgxidtype, help='Type of identifier used to validate the result.', states={ 'required': Bool(Eval('rgxident', '')), - }, depends=['rgxident']) + 'invisible': STATES_WEB['invisible'], + }, depends=DEPENDS_WEB+['rgxident']) # field to test requests - used_url = fields.Function(fields.Char(string='Used URL', readonly=True, - help='This URL is used to retrieve the HTML file.'), + used_url = fields.Function(fields.Char( + string='Used URL', readonly=True, + help='This URL is used to retrieve the HTML file.', + states={'invisible': STATES_WEB['invisible']}, depends=DEPENDS_WEB), 'on_change_with_used_url') - nsin = fields.Function(fields.Char(string='NSIN'), - 'on_change_with_nsin', setter='set_test_value') - isin = fields.Function(fields.Char(string='ISIN'), - 'on_change_with_isin', setter='set_test_value') - symbol = fields.Function(fields.Char(string='Symbol'), - 'on_change_with_symbol', setter='set_test_value') - http_state = fields.Function(fields.Char(string='HTTP-State', + nsin = fields.Function(fields.Char( + string='NSIN'), 'on_change_with_nsin', setter='set_test_value') + isin = fields.Function(fields.Char( + string='ISIN'), 'on_change_with_isin', setter='set_test_value') + symbol = fields.Function(fields.Char( + string='Symbol'), 'on_change_with_symbol', setter='set_test_value') + http_state = fields.Function(fields.Char( + string='HTTP-State', readonly=True), 'on_change_with_http_state') - text = fields.Function(fields.Text(string='Result', - readonly=True), 'on_change_with_text') - fnddate = fields.Function(fields.Date(string='Date', readonly=True, + text = fields.Function(fields.Text( + string='Result', readonly=True), 'on_change_with_text') + fnddate = fields.Function(fields.Date( + string='Date', readonly=True, help='Date found during test query.'), 'on_change_with_fnddate') - fndrate = fields.Function(fields.Numeric(string='Rate', readonly=True, - help='Rate found during test query.', digits=(16,4)), + fndrate = fields.Function(fields.Numeric( + string='Rate', readonly=True, + help='Rate found during test query.', digits=(16, 4)), 'on_change_with_fndrate') - fndident = fields.Function(fields.Char(string='Identifier', readonly=True, + fndident = fields.Function(fields.Char( + string='Identifier', readonly=True, help='Identifier found during test query.'), 'on_change_with_fndident') + @classmethod + def __setup__(cls): + super(OnlineSource, cls).__setup__() + cls._order.insert(0, ('name', 'DESC')) + + @classmethod + def default_query_method(cls): + """ default: web + """ + return 'web' + @classmethod def default_url(cls): """ defaul-url @@ -127,6 +183,12 @@ class OnlineSource(ModelSQL, ModelView): """ return True + @classmethod + def default_fixed_url(cls): + """ default: False + """ + return False + @fields.depends(*fields_check) def on_change_nsin(self): """ run request @@ -169,16 +231,22 @@ class OnlineSource(ModelSQL, ModelView): def on_change_with_symbol(self, name=None): return '' - @fields.depends('url', 'isin', 'nsin', 'symbol') + @fields.depends('url', 'isin', 'nsin', 'symbol', 'fixed_url') def on_change_with_used_url(self, name=None): """ get url for testing """ if self.url: return self.get_url_with_parameter( - isin = self.isin, - nsin = self.nsin, - symbol = self.symbol, - ) + isin=self.isin, + nsin=self.nsin, + symbol=self.symbol, + url=self.url) + + @classmethod + def get_query_methods(cls): + """ get list of query-methods + """ + return [('web', gettext('investment.msg_querytype_web'))] @classmethod def set_test_value(cls, record, name, value): @@ -186,34 +254,69 @@ class OnlineSource(ModelSQL, ModelView): """ pass + @classmethod + def validate(cls, records): + """ check regex-code + """ + for record in records: + for x in ['rgxdate', 'rgxrate', 'rgxident']: + if x: + record.get_regex_result('', x) + + @classmethod + def run_query_method(cls, osource, isin, nsin, symbol, url, debug=False): + """ run selected query to retrive data + result: { + 'text': raw-text from query - for debug, + 'http_state': state of query, + 'date': date() if success, + 'rate': Decimal() if success, + 'code': identifier - isin/nsin/symbol + } + """ + OSourc = Pool().get('investment.source') + + if getattr(osource, 'query_method', None) == 'web': + return OSourc.read_from_website( + osource, + isin=isin, + nsin=nsin, + symbol=symbol, + debug=debug, + url=url) + def call_online_source(self): """ use updated values to call online-source, for testing parameters """ OSourc = Pool().get('investment.source') - result = OSourc.read_from_website( - self, - isin = self.isin, - nsin = self.nsin, - symbol = self.symbol, - debug = True, - ) - self.text = result.get('text', None) - self.http_state = result.get('http_state', None) - self.fnddate = result.get('date', None) - self.fndrate = result.get('rate', None) - self.fndident = result.get('code', None) + result = OSourc.run_query_method( + self, self.isin, self.nsin, self.url, + self.symbol, debug=True) + if result: + self.text = result.get('text', None) + self.http_state = result.get('http_state', None) + self.fnddate = result.get('date', None) + self.fndrate = result.get('rate', None) + self.fndident = result.get('code', None) - def get_url_with_parameter(self, isin=None, nsin=None, symbol=None): + def get_url_with_parameter( + self, isin=None, nsin=None, symbol=None, url=None): """ generate url """ + if self.fixed_url is True: + if not url: + raise UserError(gettext( + 'investment.msg_missing_url', + oname=self.rec_name)) + return url + if self.url: return Template(self.url).substitute({ - 'isin': isin if isin is not None else '', - 'nsin': nsin if nsin is not None else '', - 'symbol': symbol if symbol is not None else '', - }) + 'isin': isin if isin is not None else '', + 'nsin': nsin if nsin is not None else '', + 'symbol': symbol if symbol is not None else ''}) @classmethod def update_rate(cls, asset): @@ -221,62 +324,77 @@ class OnlineSource(ModelSQL, ModelView): """ pool = Pool() Rate = pool.get('investment.rate') + IrDate = pool.get('ir.date') - if asset.updtsource is None: + if len(asset.updtsources) == 0: return - rate_data = cls.read_from_website( - asset.updtsource, - isin = asset.isin, - nsin = asset.wkn, - symbol = asset.secsymb, - ) - if len(asset.updtsource.rgxident or '') > 0: - # check result - same code? - code = rate_data.get('code', None) - if code: - asset_code = getattr(asset, { - 'isin': 'isin', - 'nsin': 'wkn', - 'symbol': 'secsymb', - }[asset.updtsource.rgxidtype]) - if (asset_code or '').lower() != code.lower(): - # fail - logger.warning( - 'update_rate: got wrong code "%(wrong)s" - expected "%(exp)s"' % { - 'exp': asset_code, - 'wrong': code, - }) - return False + for updtsource in asset.updtsources: + rate_data = cls.run_query_method( + updtsource, + isin=asset.isin, + nsin=asset.wkn, + symbol=asset.secsymb, + url=asset.updturl) - to_create = { - 'date': rate_data.get('date', None), - 'rate': rate_data.get('rate', None), - 'asset': asset.id, - } - if (to_create['date'] is not None) and \ - (to_create['rate'] is not None): - # check if exists - if Rate.search_count([ - ('asset.id', '=', asset.id), - ('date', '=', to_create['date']), - ]) == 0: - Rate.create([to_create]) - return True + if len(updtsource.rgxident or '') > 0: + # check result - same code? + code = rate_data.get('code', None) + if code: + asset_code = getattr(asset, { + 'isin': 'isin', + 'nsin': 'wkn', + 'symbol': 'secsymb', + }[updtsource.rgxidtype]) + if (asset_code or '').lower() != code.lower(): + # fail + logger.warning( + 'update_rate: got wrong code ' + + '"%(wrong)s" - expected "%(exp)s"' % { + 'exp': asset_code, + 'wrong': code}) + continue + + to_create = { + 'date': rate_data.get('date', None), + 'rate': rate_data.get('rate', None), + 'asset': asset.id} + if (to_create['date'] is not None) and \ + (to_create['rate'] is not None): + # check if exists + if Rate.search_count([ + ('asset.id', '=', asset.id), + ('date', '=', to_create['date'])]) == 0: + Rate.create([to_create]) + return True + else: + # if we got a record for today - stop + # otherwise try next source + if to_create['date'] == IrDate.today(): + break return False def get_regex_result(self, html_text, field_name): """ run regex on html-text, convert result """ + OSource = Pool().get('investment.source') + rgxcode = getattr(self, field_name) or '' if len(rgxcode) == 0: return None - search_result = re.compile(rgxcode).search(html_text) - if search_result is None: - return None + try: + search_result = re.compile(rgxcode).search(html_text) + if search_result is None: + return None + except Exception as e1: + raise UserError(gettext( + 'investment.msg_bug_in_regexquery', + errmsg=str(e1), + fname=getattr(OSource, field_name).string, + code=rgxcode)) - try : + try: result = search_result.group(1) except IndexError: result = search_result.group(0) @@ -285,20 +403,23 @@ class OnlineSource(ModelSQL, ModelView): dec_sep = [',', '.'] dec_sep.remove(self.rgxdecimal) - result = result.replace(dec_sep[0], '').replace(self.rgxdecimal, '.') - try : + result = result.replace( + dec_sep[0], '').replace(self.rgxdecimal, '.') + try: result = Decimal(result) - except : + except Exception: result = None elif field_name == 'rgxdate': - try : + try: result = datetime.strptime(result, self.rgxdatefmt).date() - except : + except Exception: result = None return result @classmethod - def read_from_website(cls, updtsource, isin=None, nsin=None, symbol=None, debug=False): + def read_from_website( + cls, updtsource, isin=None, nsin=None, + symbol=None, url=None, debug=False): """ read from url, extract values """ result = {} @@ -309,10 +430,10 @@ class OnlineSource(ModelSQL, ModelView): res1 = requests.get( updtsource.get_url_with_parameter( - isin = isin, - nsin = nsin, - symbol = symbol, - ), + isin=isin, + nsin=nsin, + symbol=symbol, + url=url), allow_redirects=True, timeout=5.0) @@ -321,7 +442,7 @@ class OnlineSource(ModelSQL, ModelView): 'msg': res1.reason, } - if res1.status_code in [200, 204]: + if res1.status_code in [200, 204, 410]: html = res1.text # remove html-tags @@ -341,8 +462,10 @@ class OnlineSource(ModelSQL, ModelView): result['rate'] = updtsource.get_regex_result(html, 'rgxrate') result['date'] = updtsource.get_regex_result(html, 'rgxdate') result['code'] = updtsource.get_regex_result(html, 'rgxident') - else : - logger.error('read_from_website: %(code)s, url: %(url)s, redirects: [%(redirects)s]' % { + else: + logger.error( + 'read_from_website: ' + + '%(code)s, url: %(url)s, redirects: [%(redirects)s]' % { 'code': res1.status_code, 'url': res1.url, 'redirects': ', '.join([x.url for x in res1.history]), diff --git a/onlinesource.xml b/onlinesource.xml index 21e2433..045d883 100644 --- a/onlinesource.xml +++ b/onlinesource.xml @@ -1,5 +1,5 @@ - diff --git a/rate.py b/rate.py index f37194b..6379e6b 100644 --- a/rate.py +++ b/rate.py @@ -1,34 +1,37 @@ # -*- 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, Unique, Check +from trytond.model import ( + ModelView, ModelSQL, fields, Unique, Check, SymbolMixin, Index) from trytond.transaction import Transaction from trytond.pool import Pool -from trytond.pyson import Eval, Bool +from trytond.pyson import Eval -class Rate(ModelSQL, ModelView): +class Rate(SymbolMixin, ModelSQL, ModelView): 'Rate' __name__ = 'investment.rate' - asset = fields.Many2One(string='Asset', required=True, - select=True, ondelete='CASCADE', + asset = fields.Many2One( + string='Asset', required=True, ondelete='CASCADE', model_name='investment.asset') - date = fields.Date(string='Date', required=True, select=True) - rate = fields.Numeric(string='Rate', required=True, - digits=(16, Eval('asset_digits', 4)), - depends=['asset_digits']) + date = fields.Date(string='Date', required=True) + rate = fields.Numeric( + string='Rate', required=True, + digits=(16, Eval('asset_digits', 4)), depends=['asset_digits']) - asset_digits = fields.Function(fields.Integer(string='Digits', - readonly=True), 'on_change_with_asset_digits') - currency = fields.Function(fields.Many2One(string='Currency', - readonly=True, model_name='currency.currency'), - 'on_change_with_currency') - uom = fields.Function(fields.Many2One(string='Uom', - readonly=True, model_name='product.uom'), - 'on_change_with_uom') + asset_digits = fields.Function(fields.Integer( + string='Digits', readonly=True), 'get_rate_data') + currency = fields.Function(fields.Many2One( + string='Currency', readonly=True, model_name='currency.currency'), + 'get_rate_data') + uom = fields.Function(fields.Many2One( + string='Uom', readonly=True, model_name='product.uom'), + 'get_rate_data') + symbol = fields.Function(fields.Char( + string='Symbol', readonly=True), 'get_rate_data') @classmethod def __setup__(cls): @@ -43,6 +46,21 @@ class Rate(ModelSQL, ModelView): 'currency.msg_rate_positive'), ] cls._order.insert(0, ('date', 'DESC')) + cls._sql_indexes.update({ + Index( + t, + (t.date, Index.Range(order='DESC'))), + Index( + t, + (t.rate, Index.Range())), + Index( + t, + (t.asset, Index.Equality())), + Index( + t, + (t.asset, Index.Equality()), + (t.date, Index.Range(order='DESC'))), + }) @classmethod def default_date(cls): @@ -51,26 +69,40 @@ class Rate(ModelSQL, ModelView): IrDate = Pool().get('ir.date') return IrDate.today() - @fields.depends('asset', '_parent_asset.uom') - def on_change_with_uom(self, name=None): - """ get unit of asset + @classmethod + def get_rate_data(cls, rates, names): + """ speed up: get values for rate """ - if self.asset: - return self.asset.uom.id + pool = Pool() + Asset = pool.get('investment.asset') + tab_asset = Asset.__table__() + tab_rate = cls.__table__() + cursor = Transaction().connection.cursor() - @fields.depends('asset', '_parent_asset.currency') - def on_change_with_currency(self, name=None): - """ get currency - """ - if self.asset: - return self.asset.currency.id + query = tab_asset.join( + tab_rate, + condition=tab_asset.id == tab_rate.asset, + ).select( + tab_rate.id, + tab_asset.uom, + tab_asset.currency, + tab_asset.currency_digits, + where=tab_rate.id.in_([x.id for x in rates]), + ) + cursor.execute(*query) + records = cursor.fetchall() - @fields.depends('asset', '_parent_asset.currency_digits') - def on_change_with_asset_digits(self, name=None): - """ get digits for asset - """ - if self.asset: - return self.asset.currency_digits - return 4 + result = {x: {y.id: None for y in rates} for x in names} + for record in records: + r1 = { + 'symbol': '%', + 'uom': record[1], + 'currency': record[2], + 'asset_digits': record[3], + } + + for n in names: + result[n][record[0]] = r1[n] + return result # Rate diff --git a/rate.xml b/rate.xml index dacff3d..e84e042 100644 --- a/rate.xml +++ b/rate.xml @@ -1,5 +1,5 @@ - diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..4effdfa --- /dev/null +++ b/scripts/__init__.py @@ -0,0 +1,2 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. diff --git a/scripts/import_investment_historicalrates.py b/scripts/import_investment_historicalrates.py new file mode 100644 index 0000000..de11273 --- /dev/null +++ b/scripts/import_investment_historicalrates.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# This file is part of the currency_ecbrate-module from m-ds for Tryton. +# The COPYRIGHT file at the top level of this repository contains the +# full copyright notices and license terms. + +import csv, os, sys +from argparse import ArgumentParser +from datetime import datetime, date +from decimal import Decimal + + +try: + from proteus import Model, config +except ImportError: + prog = os.path.basename(sys.argv[0]) + sys.exit("proteus must be installed to use %s" % prog) + + +def read_csv_file(file_name, dec_devider, date_fmt, delimiter): + """ read file from csv + """ + result = [] + + del_chars = ['.', ','] + del_chars.remove(dec_devider) + min_date = None + max_date = None + min_rate = None + max_rate = None + + with open(file_name, 'r', encoding='latin1') as fhdl: + csv_lines = csv.DictReader(fhdl, dialect='excel', delimiter=delimiter) + + for line in csv_lines: + try : + date_val = datetime.strptime(line.get('date', None).strip(), date_fmt).date() + except : + raise ValueError('- failed to read column 1 of file, expected date (format: %s)' % date_fmt) + + try : + rate_val = line.get('rate', None).replace(del_chars[0], '').strip() + rate_val = Decimal(rate_val.replace(dec_devider, '.')) + except : + raise ValueError('- failed to read column 1 of file, expected date (format: %s)' % date_fmt) + + 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 ValueError('- failed to identify row content: %s' % line) + + print('- found %d records' % len(result)) + print('- dates from %s to %s' % ( + min_date.isoformat() if min_date is not None else '-', + max_date.isoformat() if max_date is not None else '-', + )) + print('- rates from %s to %s' % ( + str(min_rate) if min_rate is not None else '-', + str(max_rate) if max_rate is not None else '-', + )) + return (result, max_date, min_date) + + + +def upload_rates(isin, rates_list, max_date, min_date): + """ generate to_create for rates + """ + Rate = Model.get('investment.rate') + Asset = Model.get('investment.asset') + + # get id of asset by isin + assets = Asset.find([ + ('isin', '=', isin), + ]) + if len(assets) == 0: + print('- ISIN %s not found' % isin) + return + + # get rate in date-range + rates = Rate.find([ + ('asset.id', '=', assets[0].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': assets[0].id, + 'date': rate['date'], + 'rate': rate['rate'], + }) + done_dates.append(rate['date']) + + if len(to_create) > 0: + print('- upload %d historical rates...' % len(to_create)) + Rate.create(to_create, context={}) + print('- finished upload') + else : + print('- nothing to upload') + + +def do_import(csv_file, isin, dec_devider, date_fmt, delimiter): + """ run import + """ + print('\n--== Import historical asset rates ==--') + print('- file: %s' % csv_file) + print('- ISIN: %s' % isin) + print('- date-format: %s, decimal divider: "%s", delimiter: "%s"' % (date_fmt, dec_devider, delimiter)) + (lines, max_date, min_date) = read_csv_file(csv_file, dec_devider, date_fmt, delimiter) + upload_rates(isin, lines, max_date, min_date) + + print('--== finish import ==--') + + +def main(database, config_file, csv_file, dec_devider, date_fmt, isin, delimiter): + config.set_trytond(database, config_file=config_file) + with config.get_config().set_context(active_test=False): + do_import(csv_file, isin, dec_devider, date_fmt, delimiter) + + +def run(): + parser = ArgumentParser() + parser.add_argument('-d', '--database', dest='database', required=True) + parser.add_argument('-c', '--config', dest='config_file', help='the trytond config file') + parser.add_argument('-f', '--file', dest='csv_file', required=True, + help='CSV-file to import, should contain two columns: 1. date, 2. numeric, first line must have "date" and "rate"') + parser.add_argument('-p', '--decimal', default=',', dest='decimal_divider', + help='decimal divider, defaults to: ,') + parser.add_argument('-t', '--delimiter', default=';', dest='delimiter', + help='field delimiter for csv-table, defaults to: ;') + parser.add_argument('-a', '--dateformat', default='%d.%m.%Y', dest='date_format', + help='date format like %%d.%%m.%%Y or %%Y-%%m-%%d or similiar') + parser.add_argument('-i', '--isin', dest='isin', required=True, help='ISIN of the target asset') + + args = parser.parse_args() + main(args.database, args.config_file, args.csv_file, args.decimal_divider, \ + args.date_format, args.isin, args.delimiter) + + +if __name__ == '__main__': + run() diff --git a/setup.py b/setup.py index b84eb8a..29f2a96 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ """ # Always prefer setuptools over distutils -from setuptools import setup, find_packages +from setuptools import setup # To use a consistent encoding from codecs import open from os import path @@ -22,7 +22,7 @@ with open(path.join(here, 'README.rst'), encoding='utf-8') as f: # tryton.cfg einlesen config = ConfigParser() -config.readfp(open('tryton.cfg')) +config.read_file(open('tryton.cfg')) info = dict(config.items('tryton')) for key in ('depends', 'extras_depend', 'xml'): if key in info: @@ -36,10 +36,10 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f: l2 = i.strip().split(';') if len(l2) < 4: continue - modversion[l2[0]] = {'min':l2[1], 'max':l2[2], 'prefix':l2[3]} + modversion[l2[0]] = {'min': l2[1], 'max': l2[2], 'prefix': l2[3]} # tryton-version -major_version = 6 +major_version = 7 minor_version = 0 requires = ['requests>=2.26', 'html2text'] @@ -51,42 +51,47 @@ for dep in info.get('depends', []): prefix = modversion[dep]['prefix'] if len(modversion[dep]['max']) > 0: - requires.append('%s_%s >= %s, <= %s' % - (prefix, dep, modversion[dep]['min'], modversion[dep]['max'])) - else : - requires.append('%s_%s >= %s' % - (prefix, dep, modversion[dep]['min'])) - else : - requires.append('%s_%s >= %s.%s, < %s.%s' % - ('trytond', dep, major_version, minor_version, + requires.append('%s_%s >= %s, <= %s' % ( + prefix, dep, modversion[dep]['min'], + modversion[dep]['max'])) + else: + requires.append('%s_%s >= %s' % ( + prefix, dep, modversion[dep]['min'])) + else: + requires.append('%s_%s >= %s.%s, < %s.%s' % ( + 'trytond', dep, major_version, minor_version, major_version, minor_version + 1)) -requires.append('trytond >= %s.%s, < %s.%s' % - (major_version, minor_version, major_version, minor_version + 1)) +requires.append('trytond >= %s.%s, < %s.%s' % ( + major_version, minor_version, major_version, minor_version + 1)) -setup(name='%s_%s' % (PREFIX, MODULE), +setup( + name='%s_%s' % (PREFIX, MODULE), version=info.get('version', '0.0.1'), description='Tryton module to add investment items.', long_description=long_description, + long_description_content_type='text/x-rst', url='https://www.m-ds.de/', + download_url='https://scmdev.m-ds.de/Tryton/Extra/investment', author='martin-data services', author_email='service@m-ds.de', license='GPL-3', classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Plugins', - 'Framework :: Tryton', - 'Intended Audience :: Developers', - 'Intended Audience :: Customer Service', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Financial and Insurance Industry', - 'Topic :: Office/Business', - 'Topic :: Office/Business :: Financial :: Accounting', - 'Natural Language :: German', - 'Natural Language :: English', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: GNU General Public License (GPL)', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Plugins', + 'Framework :: Tryton', + 'Intended Audience :: Developers', + 'Intended Audience :: Customer Service', + 'Intended Audience :: Information Technology', + 'Intended Audience :: Financial and Insurance Industry', + 'Topic :: Office/Business', + 'Topic :: Office/Business :: Financial :: Accounting', + 'Natural Language :: German', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], keywords='tryton investment shares commodities', @@ -95,10 +100,10 @@ setup(name='%s_%s' % (PREFIX, MODULE), 'trytond.modules.%s' % MODULE, ], package_data={ - 'trytond.modules.%s' % MODULE: (info.get('xml', []) - + ['tryton.cfg', 'locale/*.po', 'tests/*.py', - 'view/*.xml', 'icon/*.svg', - 'versiondep.txt', 'README.rst']), + 'trytond.modules.%s' % MODULE: (info.get('xml', []) + [ + 'tryton.cfg', 'locale/*.po', 'tests/*.py', + 'view/*.xml', 'icon/*.svg', 'scripts/*.py', + 'versiondep.txt', 'README.rst']), }, install_requires=requires, @@ -106,5 +111,7 @@ setup(name='%s_%s' % (PREFIX, MODULE), entry_points=""" [trytond.modules] %s = trytond.modules.%s + [console_scripts] + trytond_import_investment_historicalrates = trytond.modules.investment.scripts.import_investment_historicalrates:run [data] """ % (MODULE, MODULE), ) diff --git a/sources_def.xml b/sources_def.xml index 25e148c..ff5959a 100644 --- a/sources_def.xml +++ b/sources_def.xml @@ -1,21 +1,95 @@ - - www.finanzen.net - ETF + www.finanzen.net - ETF (Tradegate) https://www.finanzen.net/etf/${isin}/tgt - \nKurszeit (\d+\.\d+\.\d+) \d{2}:\d{2}:\d{2}.*\n + \nKurszeit\s+(\d+\.\d+\.\d+)\s+\d{2}:\d{2}:\d{2}.*\n %d.%m.%Y - \nKurs (\d+,\d+) EUR.*\n + \nKurs\s+(\d+,\d+)\s+EUR.*\n + , + WKN:.* ISIN:\s+([A-Z,0-9]+).* + isin + + + www.finanzen.net - ETF (Stuttgard) + https://www.finanzen.net/etf/${isin}/stu + + \nKurszeit\s+(\d+\.\d+\.\d+)\s+\d{2}:\d{2}:\d{2}.*\n + %d.%m.%Y + \nKurs\s+(\d+,\d+)\s+EUR.*\n , WKN:.* ISIN: ([A-Z,0-9]+).* isin + + www.finanzen.net - Fonds + https://www.finanzen.net/fonds/${isin}/tgt + + \n\*\*Kursdatum\*\*\s+(\d+\.\d+\.\d+).*\n + %d.%m.%Y + \n\*\*Kurs\*\*\s+(\d+,\d+)\s+EUR.*\n + , + WKN:.*\s+ISIN:\s+([A-Z,0-9]+).* + isin + + + www.finanzen.net - Rohstoffe + https://www.finanzen.net/rohstoffe/${symbol} + + \nKurszeit\s+(\d+\.\d+\.\d+)\s+\d+:\d+:\d+.*\n + %d.%m.%Y + \nKurs\s+([\d+\.]*\d+,\d+)\s+USD\s*\n + , + + + www.finanzen.net - Aktie + https://www.finanzen.net/aktien/${symbol} + + \nKurszeit(\d+\.\d+\.\d+) \d{2}:\d{2}:\d{2}.*\n + %d.%m.%Y + \nKurs(\d+,\d+) EUR.*\n + \nISIN([A-Z,0-9]+).*\n + isin + , + + + Financial Times UK + https://markets.ft.com/data/etfs/tearsheet/summary?s=${symbol} + + Data delayed at least 15 minutes, as of (.*) \d+:\d+ GMT\. + %b %d %Y + Price\D+([\d,]*\d+\.\d+) + . + + + www.sbroker.de + https://www.sbroker.de/sbl/mdaten_analyse/dksuche_a?SEARCH_VALUE=${isin} + + \nDatum\s*(?:\/ Uhrzeit)*: (\d+\.\d+\.\d+)\s*(?:\/ \d+:\d+)*\s*\n + %d.%m.%y + (?:Kurs aktuell|Rucknahmepreis)\s+[()\w\./\[\]!]+\s+(\d+,\d+)\s+(EUR|€) + , + \nWKN / ISIN: [A-Z,0-9]+ / ([A-Z,0-9]+)\s+\n + isin + + + FAZ + https://www.faz.net/aktuell/finanzen/kurs/etf/ + + + (\d{2}\.\d{2}\.\d{4}) + %d.%m.%y + \nKurs(\d+.\d+)\s+€\s+\n + , + \nWKN:.*\sISIN: ([A-Z,0-9]+).*\n + isin + diff --git a/tests/__init__.py b/tests/__init__.py index 1d46b3f..4effdfa 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,28 +1,2 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. - -import trytond.tests.test_tryton -import unittest - -from trytond.modules.investment.tests.test_asset import AssetTestCase -from trytond.modules.investment.tests.test_rate import RateTestCase -from trytond.modules.investment.tests.test_source import SourceTestCase - - -__all__ = ['suite'] - - -class InvestmentTestCase(\ - SourceTestCase, \ - RateTestCase,\ - AssetTestCase,\ - ): - 'Test investment module' - module = 'investment' - -# end InvestmentTestCase - -def suite(): - suite = trytond.tests.test_tryton.suite() - suite.addTests(unittest.TestLoader().loadTestsFromTestCase(InvestmentTestCase)) - return suite diff --git a/tests/asset.py b/tests/asset.py new file mode 100644 index 0000000..d184fc3 --- /dev/null +++ b/tests/asset.py @@ -0,0 +1,770 @@ +# -*- coding: utf-8 -*- +# This file is part of the investment-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.tests.test_tryton import with_transaction +from trytond.pool import Pool +from trytond.modules.company.tests import create_company +from trytond.transaction import Transaction +from trytond.exceptions import UserError +from decimal import Decimal +from datetime import time, date, datetime + + +class AssetTestCase(object): + """ test asset + """ + def prep_asset_company(self): + """ get/create company + """ + Company = Pool().get('company.company') + + company = Company.search([]) + if len(company) > 0: + company = company[0] + else: + company = create_company(name='m-ds') + return company + + def prep_asset_product( + self, name='Product 1', description=None, unit='u', + unit_name='Units'): + """ create product + """ + pool = Pool() + Product = pool.get('product.template') + Uom = pool.get('product.uom') + + uom, = Uom.search([('symbol', '=', unit)]) + prod_templ, = Product.create([{ + 'name': name, + 'type': 'assets', + 'list_price': Decimal('1.0'), + 'default_uom': uom.id, + 'products': [('create', [{ + 'description': description, + }])], + }]) + self.assertEqual(prod_templ.default_uom.symbol, unit) + self.assertEqual(prod_templ.products[0].description, description) + return prod_templ.products[0] + + def prep_asset_item(self, company, product): + """ create asset + """ + pool = Pool() + Asset = pool.get('investment.asset') + + asset, = Asset.create([{ + 'company': company.id, + 'product': product.id, + 'currency': company.currency.id, + 'currency_digits': 4, + 'uom': product.default_uom.id, + }]) + self.assertEqual(asset.rec_name, '%s | - usd/%s | -' % ( + product.rec_name, + asset.uom.symbol, + )) + self.assertEqual(asset.currency.rec_name, 'usd') + self.assertEqual(asset.currency_digits, 4) + self.assertEqual(asset.product.rec_name, product.name) + self.assertEqual(asset.uom.symbol, product.default_uom.symbol) + return asset + + @with_transaction() + def test_asset_create(self): + """ create asset + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item( + company=company, + product=product) + self.assertEqual(asset.symbol, 'usd/u') + self.assertEqual(asset.asset_symbol.symbol, 'usd/u') + + # check ranges + Asset.write(*[ + [asset], + { + 'currency_digits': 1, + }]) + self.assertRaisesRegex( + UserError, + 'ss', + Asset.write, + *[[asset], { + 'currency_digits': -1, + }]) + + @with_transaction() + def test_asset_rec_name(self): + """ create asset + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item( + company=company, + product=product) + + self.assertEqual(asset.rec_name, 'Product 1 | - usd/u | -') + + Asset.write(*[ + [asset], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 15), + 'rate': Decimal('2.45'), + }])], + }]) + self.assertEqual( + asset.rec_name, + 'Product 1 | 2.4500 usd/u | 05/15/2022') + self.assertEqual( + Asset.search_count([('name', '=', 'Product 1')]), + 1) + + @with_transaction() + def test_asset_order_and_search_rate_and_date(self): + """ create asset, check order of rate + date + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product1 = self.prep_asset_product( + name='Product 1', + description='some asset') + product2 = self.prep_asset_product( + name='Product 2', + description='some asset') + + asset1 = self.prep_asset_item(company=company, product=product1) + asset2 = self.prep_asset_item(company=company, product=product2) + + Asset.write(*[ + [asset1], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 18), + 'rate': Decimal('3.5'), + }, { + 'date': date(2022, 5, 15), + 'rate': Decimal('2.45'), + }])], + }, + [asset2], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 17), + 'rate': Decimal('2.6'), + }, { + 'date': date(2022, 5, 14), + 'rate': Decimal('2.4'), + }])], + }, + ]) + self.assertEqual( + asset1.rec_name, + 'Product 1 | 3.5000 usd/u | 05/18/2022') + self.assertEqual( + asset2.rec_name, + 'Product 2 | 2.6000 usd/u | 05/17/2022') + + assets = Asset.search([], order=[('date', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].date, date(2022, 5, 17)) + self.assertEqual(assets[1].date, date(2022, 5, 18)) + + assets = Asset.search([], order=[('date', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].date, date(2022, 5, 18)) + self.assertEqual(assets[1].date, date(2022, 5, 17)) + + assets = Asset.search([], order=[('rate', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].rate, Decimal('2.6')) + self.assertEqual(assets[1].rate, Decimal('3.5')) + + assets = Asset.search([], order=[('rate', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].rate, Decimal('3.5')) + self.assertEqual(assets[1].rate, Decimal('2.6')) + + self.assertEqual(Asset.search_count([ + ('date', '=', date(2022, 5, 17)), + ]), 1) + self.assertEqual(Asset.search_count([ + ('date', '>=', date(2022, 5, 17)), + ]), 2) + self.assertEqual(Asset.search_count([ + ('date', '<', date(2022, 5, 17)), + ]), 0) + + @with_transaction() + def test_asset_percentages_dateselect1(self): + """ create asset, add rates, check selection of + specific date - fixed date + """ + Asset = Pool().get('investment.asset') + cursor = Transaction().connection.cursor() + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset1 = self.prep_asset_item(company=company, product=product) + self.assertEqual(asset1.rec_name, 'Product 1 | - usd/u | -') + + Asset.write(*[ + [asset1], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 15), + 'rate': Decimal('2.45'), + }, { + 'date': date(2022, 5, 16), + 'rate': Decimal('2.6'), + }, { + 'date': date(2022, 5, 12), + 'rate': Decimal('2.0'), + }, { + 'date': date(2022, 5, 3), + 'rate': Decimal('3.6'), + }])], + }, + ]) + self.assertEqual( + asset1.rec_name, + 'Product 1 | 2.6000 usd/u | 05/16/2022') + self.assertEqual(len(asset1.rates), 4) + self.assertEqual(asset1.rates[0].date, date(2022, 5, 16)) + self.assertEqual(asset1.rates[1].date, date(2022, 5, 15)) + self.assertEqual(asset1.rates[2].date, date(2022, 5, 12)) + self.assertEqual(asset1.rates[3].date, date(2022, 5, 3)) + + # query fixed date + tab_percent = Asset.get_percentage_sql(days=0) + with Transaction().set_context({ + 'qdate': date(2022, 5, 16)}): + query = tab_percent.select( + tab_percent.id, + tab_percent.date, + tab_percent.percent, + where=tab_percent.id == asset1.id, + ) + cursor.execute(*query) + records = cursor.fetchall() + + # there should be one record, three colums + self.assertEqual(len(records), 1) + self.assertEqual(len(records[0]), 3) + self.assertEqual(records[0][0], asset1.id) + self.assertEqual(records[0][1], date(2022, 5, 16)) + self.assertEqual( + records[0][2].quantize(Decimal('0.01')), + Decimal('6.12')) + + @with_transaction() + def test_asset_percentages_daterange(self): + """ create asset, add rates, check selection of + value + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset1 = self.prep_asset_item(company=company, product=product) + asset2 = self.prep_asset_item(company=company, product=product) + + self.assertEqual(asset1.rec_name, 'Product 1 | - usd/u | -') + self.assertEqual(asset2.rec_name, 'Product 1 | - usd/u | -') + + Asset.write(*[ + [asset1], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 15), + 'rate': Decimal('2.45'), + }, { + 'date': date(2022, 5, 16), + 'rate': Decimal('2.6'), + }])], + }, + [asset2], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 14), + 'rate': Decimal('5.75'), + }, { + 'date': date(2022, 5, 15), + 'rate': Decimal('5.25'), + }])], + }, + ]) + self.assertEqual( + asset1.rec_name, + 'Product 1 | 2.6000 usd/u | 05/16/2022') + self.assertEqual( + asset2.rec_name, + 'Product 1 | 5.2500 usd/u | 05/15/2022') + self.assertEqual(asset1.change_day1, Decimal('6.12')) + self.assertEqual(asset2.change_day1, Decimal('-8.7')) + self.assertEqual(asset1.change_month1, None) + self.assertEqual(asset2.change_month1, None) + self.assertEqual(asset1.change_month3, None) + self.assertEqual(asset2.change_month3, None) + self.assertEqual(asset1.change_month6, None) + self.assertEqual(asset2.change_month6, None) + self.assertEqual(asset1.change_month12, None) + self.assertEqual(asset2.change_month12, None) + + # check ordering + assets = Asset.search([ + ('change_day1', '!=', Decimal('0.0')), + ], order=[('change_day1', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].change_day1, Decimal('-8.7')) + self.assertEqual(assets[1].change_day1, Decimal('6.12')) + + assets = Asset.search([ + ('change_day1', '!=', Decimal('0.0')), + ], order=[('change_day1', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].change_day1, Decimal('6.12')) + self.assertEqual(assets[1].change_day1, Decimal('-8.7')) + + # check 5-day-range + # four days + Asset.write(*[ + [asset1], + { + 'rates': [('write', [asset1.rates[1]], { + 'date': date(2022, 5, 12), + })], + }]) + self.assertEqual(asset1.rates[0].date, date(2022, 5, 16)) + self.assertEqual(asset1.rates[1].date, date(2022, 5, 12)) + self.assertEqual(asset1.change_day1, Decimal('6.12')) + # five days + Asset.write(*[ + [asset1], + { + 'rates': [('write', [asset1.rates[1]], { + 'date': date(2022, 5, 11), + })], + }]) + self.assertEqual(asset1.rates[0].date, date(2022, 5, 16)) + self.assertEqual(asset1.rates[1].date, date(2022, 5, 11)) + self.assertEqual(asset1.change_day1, Decimal('6.12')) + # six days + Asset.write(*[ + [asset1], + { + 'rates': [('write', [asset1.rates[1]], { + 'date': date(2022, 5, 10), + })], + }]) + self.assertEqual(asset1.rates[0].date, date(2022, 5, 16)) + self.assertEqual(asset1.rates[1].date, date(2022, 5, 10)) + self.assertEqual(asset1.change_day1, None) + + @with_transaction() + def test_asset_percentges_values(self): + """ create asset, add rates, check percentages + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset1 = self.prep_asset_item(company=company, product=product) + + self.assertEqual(asset1.rec_name, 'Product 1 | - usd/u | -') + + Asset.write(*[ + [asset1], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 15), + 'rate': Decimal('2.45'), + }, { + 'date': date(2022, 5, 16), + 'rate': Decimal('2.6'), + }, { + 'date': date(2022, 4, 14), + 'rate': Decimal('2.2'), + }, { + 'date': date(2022, 2, 14), + 'rate': Decimal('2.8'), + },])], + }]) + self.assertEqual( + asset1.rec_name, + 'Product 1 | 2.6000 usd/u | 05/16/2022') + self.assertEqual(len(asset1.rates), 4) + self.assertEqual(asset1.rates[0].date, date(2022, 5, 16)) + self.assertEqual(asset1.rates[1].date, date(2022, 5, 15)) + self.assertEqual(asset1.rates[2].date, date(2022, 4, 14)) + self.assertEqual(asset1.rates[3].date, date(2022, 2, 14)) + + self.assertEqual(asset1.change_day1, Decimal('6.12')) + self.assertEqual(asset1.change_month1, Decimal('18.18')) + self.assertEqual(asset1.change_month3, Decimal('-7.14')) + self.assertEqual(asset1.change_month6, None) + self.assertEqual(asset1.change_month12, None) + + # call order-functions + Asset.search([], order=[('change_day1', 'ASC')]) + Asset.search([], order=[('change_month1', 'ASC')]) + Asset.search([], order=[('change_month3', 'ASC')]) + Asset.search([], order=[('change_month6', 'ASC')]) + Asset.search([], order=[('change_month12', 'ASC')]) + + # searcher + self.assertEqual( + Asset.search_count([('change_day1', '>', Decimal('6.1'))]), + 1) + self.assertEqual( + Asset.search_count([('change_day1', '>', Decimal('6.15'))]), + 0) + self.assertEqual( + Asset.search_count([('change_day1', '=', Decimal('6.12'))]), + 1) + + self.assertEqual( + Asset.search_count([('change_month1', '>', Decimal('18.0'))]), + 1) + self.assertEqual( + Asset.search_count([('change_month1', '>', Decimal('18.18'))]), + 0) + self.assertEqual( + Asset.search_count([('change_month1', '=', Decimal('18.18'))]), + 1) + + self.assertEqual( + Asset.search_count([('change_month3', '=', Decimal('-7.14'))]), + 1) + self.assertEqual( + Asset.search_count([('change_month6', '=', None)]), + 1) + + @with_transaction() + def test_asset_check_onlinesource_onoff(self): + """ create asset, switch online-source on/off + """ + pool = Pool() + OnlineSource = pool.get('investment.source') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item(company=company, product=product) + + o_source, = OnlineSource.create([{ + 'name': 'Source 1', + }]) + + self.assertEqual(len(asset.updtsources), 0) + self.assertEqual(asset.updttime, time(14, 0)) + + asset.updtsources = [o_source] + asset.updttime = time(10, 45) + asset.save() + self.assertEqual(len(asset.updtsources), 1) + self.assertEqual(asset.updtsources[0].rec_name, 'Source 1') + self.assertEqual(asset.updttime, time(10, 45)) + + asset.updtsources = [] + asset.on_change_updtsources() + self.assertEqual(len(asset.updtsources), 0) + self.assertEqual(asset.updttime, None) + + @with_transaction() + def test_asset_check_update_select(self): + """ create asset, add online-source, + check selection of assets to update + """ + pool = Pool() + OnlineSource = pool.get('investment.source') + Asset = pool.get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item(company=company, product=product) + + o_source, = OnlineSource.create([{ + 'name': 'Source 1', + }]) + Asset.write(*[ + [asset], + { + 'updtsources': [('add', [o_source.id])], + 'updttime': time(10, 45), + }]) + + with Transaction().set_context({'qdate': date(2022, 10, 14)}): + # re-read to make context work + asset2, = Asset.browse([asset.id]) + + self.assertEqual(len(asset2.updtsources), 1) + self.assertEqual(asset2.updtsources[0].rec_name, 'Source 1') + self.assertEqual(asset2.updttime, time(10, 45)) + self.assertEqual(len(asset2.rates), 0) + # qdate = 2022-10-14 simulates existence of record at this day + # next call would be the 15. - but its saturday, + # next-call-date is moved to 17. + self.assertEqual( + asset2.nextupdate, + datetime(2022, 10, 17, 10, 45)) + + self.assertEqual( + Asset.search_count([ + ('nextupdate', '<', datetime(2022, 10, 17, 10, 45))]), + 0) + self.assertEqual( + Asset.search_count([ + ('nextupdate', '>=', datetime(2022, 10, 17, 10, 45))]), + 1) + + # add rate at next monday + Asset.write(*[ + [asset], + { + 'rates': [('create', [{ + 'date': date(2022, 10, 17), # monday + 'rate': Decimal('1.5'), + }])], + }]) + self.assertEqual(len(asset.rates), 1) + + asset2, = Asset.browse([asset.id]) + self.assertEqual(asset.updtsources[0].rec_name, 'Source 1') + self.assertEqual(asset.updttime, time(10, 45)) + self.assertEqual(len(asset.rates), 1) + self.assertEqual(asset.rates[0].date, date(2022, 10, 17)) + self.assertEqual(asset.nextupdate, datetime(2022, 10, 18, 10, 45)) + + self.assertEqual( + Asset.search_count([ + ('nextupdate', '<', datetime(2022, 10, 18, 10, 45))]), + 0) + self.assertEqual( + Asset.search_count([ + ('nextupdate', '>=', datetime(2022, 10, 18, 10, 45))]), + 1) + + # add rate at today + Asset.write(*[ + [asset], + { + 'rates': [('create', [{ + 'date': date(2022, 10, 18), + 'rate': Decimal('1.5'), + }])], + }]) + self.assertEqual(len(asset.rates), 2) + + asset2, = Asset.browse([asset.id]) + self.assertEqual(asset2.updtsources[0].rec_name, 'Source 1') + self.assertEqual(asset2.updttime, time(10, 45)) + self.assertEqual(len(asset2.rates), 2) + self.assertEqual(asset2.rates[0].date, date(2022, 10, 18)) + self.assertEqual(asset2.nextupdate, datetime(2022, 10, 19, 10, 45)) + + self.assertEqual( + Asset.search_count([ + ('nextupdate', '<', datetime(2022, 10, 19, 10, 45))]), + 0) + self.assertEqual( + Asset.search_count([ + ('nextupdate', '>=', datetime(2022, 10, 19, 10, 45))]), + 1) + + @with_transaction() + def test_asset_indentifiers(self): + """ create asset, add identifiers + """ + pool = Pool() + Product = pool.get('product.product') + Asset = pool.get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product1 = self.prep_asset_product( + name='Product unit', unit='u') + product2 = self.prep_asset_product( + name='Product gram', unit='g') + + asset1 = self.prep_asset_item(company=company, product=product1) + asset2 = self.prep_asset_item(company=company, product=product2) + + Product.write(*[ + [product1], + { + 'identifiers': [('create', [{ + 'type': 'wkn', + 'code': '965515', + }, { + 'type': 'secsymb', + 'code': '1472977', + }, { + 'type': 'isin', + 'code': 'XC0009655157', + }, ])], + }, + [product2], + { + 'identifiers': [('create', [{ + 'type': 'wkn', + 'code': '965310', + }, { + 'type': 'secsymb', + 'code': '1431157', + }, { + 'type': 'isin', + 'code': 'XC0009653103', + }, ])], + }, + ]) + + self.assertEqual(asset1.wkn, '965515') + self.assertEqual(asset1.isin, 'XC0009655157') + self.assertEqual(asset1.secsymb, '1472977') + + self.assertEqual( + Asset.search_count([('wkn', '=', '965515')]), + 1) + self.assertEqual( + Asset.search_count([('isin', '=', 'XC0009655157')]), + 1) + self.assertEqual( + Asset.search_count([('secsymb', '=', '1472977')]), + 1) + + self.assertEqual( + Asset.search_count([('rec_name', '=', '965515')]), + 1) + self.assertEqual( + Asset.search_count([('rec_name', '=', 'XC0009655157')]), + 1) + self.assertEqual( + Asset.search_count([('rec_name', '=', '1472977')]), + 1) + + self.assertEqual( + Asset.search_count([('name', '=', '965515')]), + 1) + self.assertEqual( + Asset.search_count([('name', '=', 'XC0009655157')]), + 1) + self.assertEqual( + Asset.search_count([('name', '=', '1472977')]), + 1) + self.assertEqual( + Asset.search_count([('name', '=', 'Product unit')]), + 1) + + self.assertEqual(Asset.search_count([ + ('wkn', 'ilike', '9655%'), + ]), 1) + self.assertEqual(Asset.search_count([ + ('wkn', 'ilike', '965%'), + ]), 2) + + self.assertEqual(asset2.wkn, '965310') + self.assertEqual(asset2.isin, 'XC0009653103') + self.assertEqual(asset2.secsymb, '1431157') + + # order wkn + assets = Asset.search([], order=[('wkn', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].wkn, '965310') + self.assertEqual(assets[1].wkn, '965515') + assets = Asset.search([], order=[('wkn', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].wkn, '965515') + self.assertEqual(assets[1].wkn, '965310') + + # order isin + assets = Asset.search([], order=[('isin', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].isin, 'XC0009653103') + self.assertEqual(assets[1].isin, 'XC0009655157') + assets = Asset.search([], order=[('wkn', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].isin, 'XC0009655157') + self.assertEqual(assets[1].isin, 'XC0009653103') + + # order secsymb + assets = Asset.search([], order=[('secsymb', 'ASC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].secsymb, '1431157') + self.assertEqual(assets[1].secsymb, '1472977') + assets = Asset.search([], order=[('wkn', 'DESC')]) + self.assertEqual(len(assets), 2) + self.assertEqual(assets[0].secsymb, '1472977') + self.assertEqual(assets[1].secsymb, '1431157') + + @with_transaction() + def test_asset_check_product_update(self): + """ check update of product on asset + """ + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product1 = self.prep_asset_product( + name='Product unit', unit='u') + product2 = self.prep_asset_product( + name='Product gram', unit='g') + self.assertEqual(product2.default_uom.digits, 2) + + asset = self.prep_asset_item(company=company, product=product1) + + self.assertEqual(asset.product.rec_name, 'Product unit') + self.assertEqual(asset.product.default_uom.rec_name, 'Unit') + self.assertEqual(asset.uom.rec_name, 'Unit') + self.assertEqual(asset.currency_digits, 4) + + asset.product = product2 + asset.on_change_product() + asset.save() + + self.assertEqual(asset.product.rec_name, 'Product gram') + self.assertEqual(asset.product.default_uom.rec_name, 'Gram') + self.assertEqual(asset.uom.rec_name, 'Gram') + + asset.on_change_currency() + asset.save() + self.assertEqual(asset.currency_digits, 2) + +# end AssetTestCase diff --git a/tests/rate.py b/tests/rate.py new file mode 100644 index 0000000..1f61754 --- /dev/null +++ b/tests/rate.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# This file is part of the investment-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.tests.test_tryton import with_transaction +from trytond.transaction import Transaction +from trytond.pool import Pool +from decimal import Decimal +from datetime import date + + +class RateTestCase(object): + """ test rate + """ + @with_transaction() + def test_rate_create(self): + """ create rate + """ + Asset = Pool().get('investment.asset') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item(company=company, product=product) + + Asset.write(*[ + [asset], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 1), + 'rate': Decimal('2.5'), + }, { + 'date': date(2022, 5, 2), + 'rate': Decimal('2.8'), + }])], + }]) + self.assertEqual(len(asset.rates), 2) + self.assertEqual(asset.rates[0].date, date(2022, 5, 2)) + self.assertEqual(asset.rates[0].rate, Decimal('2.8')) + self.assertEqual(asset.rates[0].uom.rec_name, 'Unit') + self.assertEqual(asset.rates[0].asset_digits, 4) + self.assertEqual(asset.rates[0].currency.rec_name, 'usd') + self.assertEqual(asset.rates[0].symbol, '%') + self.assertEqual(asset.change_symbol.symbol, '%') + +# end RateTestCase diff --git a/tests/source.py b/tests/source.py new file mode 100644 index 0000000..43ac22b --- /dev/null +++ b/tests/source.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# This file is part of the investment-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.tests.test_tryton import with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from trytond.exceptions import UserError +from decimal import Decimal +from datetime import time, date, datetime +from unittest.mock import MagicMock +from requests import Response +import requests + + +class SourceTestCase(object): + """ test online source + """ + @with_transaction() + def test_waitlist_source_request(self): + """ create source, call server + """ + pool = Pool() + OSource = pool.get('investment.source') + Asset = pool.get('investment.asset') + Product = pool.get('product.product') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + osource, = OSource.create([{ + 'name': 'Source 1', + 'url': 'https://foo.bar/${isin}/${nsin}/${symbol}', + 'rgxdate': 'Course Date (\\d+.\\d+.\\d+) Today', + 'rgxdatefmt': '%d.%m.%Y', + 'rgxrate': 'High (\\d+,\\d+) EUR', + 'rgxdecimal': ',', + }]) + self.assertEqual(osource.rec_name, 'Source 1') + + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + Product.write(*[ + [product], + { + 'identifiers': [('create', [{ + 'type': 'wkn', + 'code': '965515', + }, { + 'type': 'secsymb', + 'code': '1472977', + }, { + 'type': 'isin', + 'code': 'XC0009655157', + }, ])], + }]) + + asset = self.prep_asset_item(company=company, product=product) + + Asset.write(*[ + [asset], + { + 'updtsources': [('add', [osource.id])], + }]) + + with Transaction().set_context({ + 'qdate': date(2022, 10, 1), # saturday + 'qdatetime': datetime(2022, 10, 2, 10, 0, 0)}): + asset2, = Asset.browse([asset]) + self.assertEqual(asset2.wkn, '965515') + self.assertEqual(asset2.isin, 'XC0009655157') + self.assertEqual(asset2.secsymb, '1472977') + self.assertEqual(asset2.updttime, time(14, 0)) + self.assertEqual(len(asset2.updtsources), 1) + self.assertEqual(asset2.updtsources[0].rec_name, 'Source 1') + self.assertEqual(asset2.updtdays, 'work') + self.assertEqual( + asset2.nextupdate, datetime(2022, 10, 3, 14, 0)) + self.assertEqual(len(asset.rates), 0) + + # fake server-response + resp1 = Response() + resp1._content = """Response from finance-server +Course Date 14.08.2022 Today +High 34,87 EUR +""".encode('utf8') + resp1.status_code = 200 + resp1.reason = 'OK' + requests.get = MagicMock(return_value=resp1) + + OSource.update_rate(asset) + self.assertEqual(len(asset.rates), 1) + self.assertEqual(asset.rates[0].date, date(2022, 8, 14)) + self.assertEqual(asset.rates[0].rate, Decimal('34.87')) + + @with_transaction() + def test_waitlist_source_check_regex(self): + """ create source, check convert + """ + pool = Pool() + OSource = pool.get('investment.source') + + osource, = OSource.create([{ + 'name': 'Source 1', + 'rgxdate': 'Course Date (\\d+.\\d+.\\d+) Today', + 'rgxdatefmt': '%d.%m.%Y', + 'rgxrate': 'High (\\d+,\\d+) EUR', + 'rgxdecimal': ',', + }]) + self.assertEqual(osource.rec_name, 'Source 1') + self.assertEqual(osource.get_regex_result( + 'The Course Date 14.03.2022 Today, High 13,43 EUR', + 'rgxdate' + ), date(2022, 3, 14)) + + self.assertEqual(osource.get_regex_result( + 'The Course Date 14.03.2022 Today, High 13,43 EUR', + 'rgxrate' + ), Decimal('13.43')) + + # iso-date + OSource.write(*[ + [osource], + { + 'rgxdate': 'Course Date (\\d+-\\d+-\\d+) Today', + 'rgxdatefmt': '%Y-%m-%d', + }]) + self.assertEqual(osource.get_regex_result( + 'The Course Date 2022-03-14 Today, High 13,43 EUR', + 'rgxdate' + ), date(2022, 3, 14)) + + @with_transaction() + def test_waitlist_source_check_regex_validate(self): + """ create source, check validation of regex-code + """ + pool = Pool() + OSource = pool.get('investment.source') + + self.assertRaisesRegex( + UserError, + r"Error in regex code of field 'Date': nothing to repeat " + + r"at position 0 \[\*+ multiple repeat\]", + OSource.create, + [{ + 'name': 'Check date', + 'rgxdate': '** multiple repeat', + 'rgxrate': 'rate -- multiple repeat', + 'rgxident': 'identifiert ** multiple repeat', + }]) + + self.assertRaisesRegex( + UserError, + r"Error in regex code of field 'Rate': multiple repeat " + + r"at position 6 \[rate \*+ multiple repeat\]", + OSource.create, + [{ + 'name': 'Check rate', + 'rgxdate': '-- multiple repeat', + 'rgxrate': 'rate ** multiple repeat', + 'rgxident': 'identifiert -- multiple repeat', + }]) + + self.assertRaisesRegex( + UserError, + r"Error in regex code of field 'Identifier': multiple " + + r"repeat at position 13 \[identifiert \*+ multiple repeat\]", + OSource.create, + [{ + 'name': 'Check rgxident', + 'rgxdate': '-- multiple repeat', + 'rgxrate': 'rate -- multiple repeat', + 'rgxident': 'identifiert ** multiple repeat', + }]) + + OSource.create([{ + 'name': 'Check rgxident', + 'rgxdate': '-- multiple repeat', + 'rgxrate': 'rate -- multiple repeat', + 'rgxident': 'identifiert -- multiple repeat', + }]) + +# end SourceTestCase diff --git a/tests/test_asset.py b/tests/test_asset.py deleted file mode 100644 index a7c791c..0000000 --- a/tests/test_asset.py +++ /dev/null @@ -1,326 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of the investment-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.tests.test_tryton import ModuleTestCase, with_transaction -from trytond.pool import Pool -from trytond.modules.company.tests import create_company -from trytond.transaction import Transaction -from decimal import Decimal -from datetime import time, date - - -class AssetTestCase(ModuleTestCase): - 'Test asset module' - module = 'investment' - - def prep_asset_company(self): - """ get/create company - """ - Company = Pool().get('company.company') - - company = Company.search([]) - if len(company) > 0: - company = company[0] - else : - company = create_company(name='m-ds') - return company - - def prep_asset_product(self, name='Product 1', description=None, unit='u', unit_name='Units'): - """ create product - """ - pool = Pool() - Product = pool.get('product.template') - Uom = pool.get('product.uom') - - uom, = Uom.search([('symbol', '=', unit)]) - prod_templ, = Product.create([{ - 'name': name, - 'type': 'assets', - 'list_price': Decimal('1.0'), - 'default_uom': uom.id, - 'products': [('create', [{ - 'description': description, - }])], - }]) - self.assertEqual(prod_templ.default_uom.symbol, unit) - self.assertEqual(prod_templ.products[0].description, description) - return prod_templ.products[0] - - def prep_asset_item(self, company, product): - """ create asset - """ - pool = Pool() - Asset = pool.get('investment.asset') - - asset, = Asset.create([{ - 'company': company.id, - 'product': product.id, - 'currency': company.currency.id, - 'currency_digits': 4, - 'uom': product.default_uom.id, - }]) - self.assertEqual(asset.rec_name, '%s [usd/%s]' % ( - product.rec_name, - asset.uom.rec_name, - )) - self.assertEqual(asset.currency.rec_name, 'usd') - self.assertEqual(asset.currency_digits, 4) - self.assertEqual(asset.product.rec_name, product.name) - self.assertEqual(asset.uom.symbol, product.default_uom.symbol) - return asset - - @with_transaction() - def test_asset_create(self): - """ create asset - """ - company = self.prep_asset_company() - product = self.prep_asset_product( - name='Product 1', - description='some asset') - - asset = self.prep_asset_item( - company=company, - product = product) - - @with_transaction() - def test_asset_check_onlinesource_onoff(self): - """ create asset, switch online-source on/off - """ - pool = Pool() - OnlineSource = pool.get('investment.source') - Asset = pool.get('investment.asset') - - company = self.prep_asset_company() - product = self.prep_asset_product( - name='Product 1', - description='some asset') - - asset = self.prep_asset_item( - company=company, - product = product) - - o_source, = OnlineSource.create([{ - 'name': 'Source 1', - }]) - - self.assertEqual(asset.updtsource, None) - self.assertEqual(asset.updttime, None) - - asset.updtsource = o_source - asset.updttime = time(10, 45) - asset.save() - self.assertEqual(asset.updtsource.rec_name, 'Source 1') - self.assertEqual(asset.updttime, time(10, 45)) - - asset.updtsource = None - asset.on_change_updtsource() - self.assertEqual(asset.updtsource, None) - self.assertEqual(asset.updttime, None) - - @with_transaction() - def test_asset_check_update_select(self): - """ create asset, add online-source, - check selection of assets to update - """ - pool = Pool() - OnlineSource = pool.get('investment.source') - Asset = pool.get('investment.asset') - - company = self.prep_asset_company() - product = self.prep_asset_product( - name='Product 1', - description='some asset') - - asset = self.prep_asset_item( - company=company, - product = product) - - o_source, = OnlineSource.create([{ - 'name': 'Source 1', - }]) - Asset.write(*[ - [asset], - { - 'updtsource': o_source.id, - 'updttime': time(10, 45), - }]) - self.assertEqual(asset.updtsource.rec_name, 'Source 1') - self.assertEqual(asset.updttime, time(10, 45)) - self.assertEqual(len(asset.rates), 0) - - with Transaction().set_context({ - 'qdate': date(2022, 10, 15), - 'qtime': time(10, 30), - }): - # no rates exists - wait for 10:45 - self.assertEqual(asset.updtneeded, True) - self.assertEqual( - Asset.search_count([('updtneeded', '=', True)]), - 0) - - with Transaction().set_context({ - 'qdate': date(2022, 10, 15), - 'qtime': time(10, 46), - }): - # no rates exists - run at 10:46 - self.assertEqual(asset.updtneeded, True) - self.assertEqual( - Asset.search_count([('updtneeded', '=', True)]), - 1) - - # add rate at yesterday - Asset.write(*[ - [asset], - { - 'rates': [('create', [{ - 'date': date(2022, 10, 14), - 'rate': Decimal('1.5'), - }])], - }]) - self.assertEqual(len(asset.rates), 1) - - with Transaction().set_context({ - 'qdate': date(2022, 10, 15), - 'qtime': time(10, 30), - }): - # 1x rate exists - run at 10:30 - self.assertEqual(asset.updtneeded, True) - self.assertEqual( - Asset.search_count([('updtneeded', '=', True)]), - 0) - - with Transaction().set_context({ - 'qdate': date(2022, 10, 15), - 'qtime': time(10, 46), - }): - # 1x rate exists yesterday - run at 10:46 - self.assertEqual(asset.updtneeded, True) - self.assertEqual( - Asset.search_count([('updtneeded', '=', True)]), - 1) - - # add rate at today - Asset.write(*[ - [asset], - { - 'rates': [('create', [{ - 'date': date(2022, 10, 15), - 'rate': Decimal('1.5'), - }])], - }]) - self.assertEqual(len(asset.rates), 2) - - with Transaction().set_context({ - 'qdate': date(2022, 10, 15), - 'qtime': time(10, 47), - }): - # 1x rate exists today - run at 10:47 - self.assertEqual(asset.updtneeded, True) - self.assertEqual( - Asset.search_count([('updtneeded', '=', True)]), - 0) - - @with_transaction() - def test_asset_indentifiers(self): - """ create asset, add identifiers - """ - pool = Pool() - Product = pool.get('product.product') - Asset = pool.get('investment.asset') - - company = self.prep_asset_company() - product1 = self.prep_asset_product( - name='Product unit', unit='u') - product2 = self.prep_asset_product( - name='Product gram', unit='g') - - asset1 = self.prep_asset_item( - company=company, - product = product1) - asset2 = self.prep_asset_item( - company=company, - product = product2) - - Product.write(*[ - [product1], - { - 'identifiers': [('create', [{ - 'type': 'wkn', - 'code': '965515', - }, { - 'type': 'secsymb', - 'code': '1472977', - }, { - 'type': 'isin', - 'code': 'XC0009655157', - }, ])], - }, - [product2], - { - 'identifiers': [('create', [{ - 'type': 'wkn', - 'code': '965310', - }, { - 'type': 'secsymb', - 'code': '1431157', - }, { - 'type': 'isin', - 'code': 'XC0009653103', - }, ])], - }, - ]) - - self.assertEqual(asset1.wkn, '965515') - self.assertEqual(asset1.isin, 'XC0009655157') - self.assertEqual(asset1.secsymb, '1472977') - - self.assertEqual(Asset.search_count([('wkn', '=', '965515')]), 1) - self.assertEqual(Asset.search_count([('isin', '=', 'XC0009655157')]), 1) - self.assertEqual(Asset.search_count([('secsymb', '=', '1472977')]), 1) - - self.assertEqual(Asset.search_count([ - ('wkn', 'ilike', '9655%'), - ]), 1) - self.assertEqual(Asset.search_count([ - ('wkn', 'ilike', '965%'), - ]), 2) - - self.assertEqual(asset2.wkn, '965310') - self.assertEqual(asset2.isin, 'XC0009653103') - self.assertEqual(asset2.secsymb, '1431157') - - @with_transaction() - def test_asset_check_product_update(self): - """ check update of product on asset - """ - company = self.prep_asset_company() - product1 = self.prep_asset_product( - name='Product unit', unit='u') - product2 = self.prep_asset_product( - name='Product gram', unit='g') - self.assertEqual(product2.default_uom.digits, 2) - - asset = self.prep_asset_item( - company=company, - product = product1) - - self.assertEqual(asset.product.rec_name, 'Product unit') - self.assertEqual(asset.product.default_uom.rec_name, 'Unit') - self.assertEqual(asset.uom.rec_name, 'Unit') - self.assertEqual(asset.currency_digits, 4) - - asset.product = product2 - asset.on_change_product() - asset.save() - - self.assertEqual(asset.product.rec_name, 'Product gram') - self.assertEqual(asset.product.default_uom.rec_name, 'Gram') - self.assertEqual(asset.uom.rec_name, 'Gram') - - asset.on_change_currency() - asset.save() - self.assertEqual(asset.currency_digits, 2) - -# end AssetTestCase diff --git a/tests/test_module.py b/tests/test_module.py new file mode 100644 index 0000000..fce11cd --- /dev/null +++ b/tests/test_module.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# This file is part of the investment-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.tests.test_tryton import ModuleTestCase +from .asset import AssetTestCase +from .rate import RateTestCase +from .source import SourceTestCase +from .wizard import WizardTestCase + + +class InvestmentTestCase( + WizardTestCase, + SourceTestCase, + RateTestCase, + AssetTestCase, + ModuleTestCase): + 'Test investment module' + module = 'investment' + +# end InvestmentTestCase + + +del ModuleTestCase diff --git a/tests/test_rate.py b/tests/test_rate.py deleted file mode 100644 index 548b952..0000000 --- a/tests/test_rate.py +++ /dev/null @@ -1,50 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of the investment-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.tests.test_tryton import ModuleTestCase, with_transaction -from trytond.pool import Pool -from trytond.modules.company.tests import create_company -from decimal import Decimal -from datetime import date - - -class RateTestCase(ModuleTestCase): - 'Test rate module' - module = 'investment' - - @with_transaction() - def test_rate_create(self): - """ create rate - """ - Asset = Pool().get('investment.asset') - - company = self.prep_asset_company() - product = self.prep_asset_product( - name='Product 1', - description='some asset') - - asset = self.prep_asset_item( - company=company, - product = product) - - Asset.write(*[ - [asset], - { - 'rates': [('create', [{ - 'date': date(2022, 5, 1), - 'rate': Decimal('2.5'), - }, { - 'date': date(2022, 5, 2), - 'rate': Decimal('2.8'), - }])], - }]) - self.assertEqual(len(asset.rates), 2) - self.assertEqual(asset.rates[0].date, date(2022, 5, 2)) - self.assertEqual(asset.rates[0].rate, Decimal('2.8')) - self.assertEqual(asset.rates[0].uom.rec_name, 'Unit') - self.assertEqual(asset.rates[0].asset_digits, 4) - self.assertEqual(asset.rates[0].currency.rec_name, 'usd') - -# end RateTestCase diff --git a/tests/test_source.py b/tests/test_source.py deleted file mode 100644 index e68361a..0000000 --- a/tests/test_source.py +++ /dev/null @@ -1,128 +0,0 @@ -# -*- coding: utf-8 -*- -# This file is part of the investment-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.tests.test_tryton import ModuleTestCase, with_transaction -from trytond.pool import Pool -from trytond.modules.company.tests import create_company -from trytond.transaction import Transaction -from decimal import Decimal -from datetime import time, date -from unittest.mock import MagicMock -from requests import Response -import requests - - -class SourceTestCase(ModuleTestCase): - 'Test online source module' - module = 'investment' - - @with_transaction() - def test_waitlist_source_request(self): - """ create source, call server - """ - pool = Pool() - OSource = pool.get('investment.source') - Asset = pool.get('investment.asset') - Product = pool.get('product.product') - - company = self.prep_asset_company() - osource, = OSource.create([{ - 'name': 'Source 1', - 'url': 'https://foo.bar/${isin}/${nsin}/${symbol}', - 'rgxdate': 'Course Date (\\d+.\\d+.\\d+) Today', - 'rgxdatefmt': '%d.%m.%Y', - 'rgxrate': 'High (\\d+,\\d+) EUR', - 'rgxdecimal': ',', - }]) - self.assertEqual(osource.rec_name, 'Source 1') - - product = self.prep_asset_product( - name='Product 1', - description='some asset') - - Product.write(*[ - [product], - { - 'identifiers': [('create', [{ - 'type': 'wkn', - 'code': '965515', - }, { - 'type': 'secsymb', - 'code': '1472977', - }, { - 'type': 'isin', - 'code': 'XC0009655157', - }, ])], - }]) - - asset = self.prep_asset_item( - company=company, - product = product) - - Asset.write(*[ - [asset], - { - 'updtsource': osource.id, - }]) - self.assertEqual(asset.wkn, '965515') - self.assertEqual(asset.isin, 'XC0009655157') - self.assertEqual(asset.secsymb, '1472977') - self.assertEqual(asset.updtsource.rec_name, 'Source 1') - self.assertEqual(len(asset.rates), 0) - - # fake server-response - resp1 = Response() - resp1._content = """Response from finance-server -Course Date 14.08.2022 Today -High 34,87 EUR -""".encode('utf8') - resp1.status_code = 200 - resp1.reason = 'OK' - requests.get = MagicMock(return_value=resp1) - - OSource.update_rate(asset) - self.assertEqual(len(asset.rates), 1) - self.assertEqual(asset.rates[0].date, date(2022, 8, 14)) - self.assertEqual(asset.rates[0].rate, Decimal('34.87')) - - @with_transaction() - def test_waitlist_source_check_regex(self): - """ create source, check convert - """ - pool = Pool() - OSource = pool.get('investment.source') - - osource, = OSource.create([{ - 'name': 'Source 1', - 'rgxdate': 'Course Date (\\d+.\\d+.\\d+) Today', - 'rgxdatefmt': '%d.%m.%Y', - 'rgxrate': 'High (\\d+,\\d+) EUR', - 'rgxdecimal': ',', - }]) - self.assertEqual(osource.rec_name, 'Source 1') - self.assertEqual(osource.get_regex_result( - 'The Course Date 14.03.2022 Today, High 13,43 EUR', - 'rgxdate' - ), date(2022, 3, 14)) - - self.assertEqual(osource.get_regex_result( - 'The Course Date 14.03.2022 Today, High 13,43 EUR', - 'rgxrate' - ), Decimal('13.43')) - - # iso-date - OSource.write(*[ - [osource], - { - 'rgxdate': 'Course Date (\\d+-\\d+-\\d+) Today', - 'rgxdatefmt': '%Y-%m-%d', - }]) - self.assertEqual(osource.get_regex_result( - 'The Course Date 2022-03-14 Today, High 13,43 EUR', - 'rgxdate' - ), date(2022, 3, 14)) - - -# end SourceTestCase diff --git a/tests/wizard.py b/tests/wizard.py new file mode 100644 index 0000000..2e15dd1 --- /dev/null +++ b/tests/wizard.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# This file is part of the investment-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.tests.test_tryton import with_transaction +from trytond.pool import Pool +from trytond.transaction import Transaction +from decimal import Decimal +from datetime import date + + +class WizardTestCase(object): + """ test import wizard + """ + @with_transaction() + def test_wiz_run_import(self): + """ run import wizard + """ + pool = Pool() + ImportWiz = pool.get('investment.imp_wiz', type='wizard') + + company = self.prep_asset_company() + with Transaction().set_context({'company': company.id}): + product = self.prep_asset_product( + name='Product 1', + description='some asset') + + asset = self.prep_asset_item(company=company, product=product) + self.assertEqual(len(asset.rates), 0) + + with Transaction().set_context({ + 'active_id': asset.id, + 'active_model': 'investment.asset'}): + (sess_id, start_state, end_state) = ImportWiz.create() + w_obj = ImportWiz(sess_id) + self.assertEqual(start_state, 'start') + self.assertEqual(end_state, 'end') + + # run start + result = ImportWiz.execute(sess_id, {}, start_state) + self.assertEqual(list(result.keys()), ['view']) + + self.assertEqual(result['view']['defaults']['asset'], asset.id) + self.assertEqual(result['view']['defaults']['dec_divider'], ',') + self.assertEqual( + result['view']['defaults']['date_fmt'], + '%d.%m.%Y') + self.assertEqual( + result['view']['defaults']['field_delimiter'], + ';') + + w_obj.start.asset = asset + w_obj.start.dec_divider = ',' + w_obj.start.date_fmt = '%d.%m.%Y' + w_obj.start.field_delimiter = ';' + + result = ImportWiz.execute(sess_id, {'start': { + 'asset': asset.id, + 'dec_divider': ',', + 'date_fmt': '%d.%m.%Y', + 'field_delimiter': ';', + 'file_': b'"date";"rate"\n"03.05.2022";"23,56"\n' + + b'"05.05.2022";"24,22"\n"06.05.2022";"25,43"', + }}, 'importf') + self.assertEqual(list(result.keys()), []) + # finish wizard + ImportWiz.delete(sess_id) + + self.assertEqual(len(asset.rates), 3) + self.assertEqual(asset.rates[0].date, date(2022, 5, 6)) + self.assertEqual(asset.rates[0].rate, Decimal('25.43')) + self.assertEqual(asset.rates[1].date, date(2022, 5, 5)) + self.assertEqual(asset.rates[1].rate, Decimal('24.22')) + self.assertEqual(asset.rates[2].date, date(2022, 5, 3)) + self.assertEqual(asset.rates[2].rate, Decimal('23.56')) + +# end WizardTestCase diff --git a/tryton.cfg b/tryton.cfg index cc07bb9..39f64d0 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -1,11 +1,13 @@ [tryton] -version=6.0.0 +version=7.0.0 depends: ir res company currency product +extras_depends: + diagram xml: icon.xml message.xml @@ -15,5 +17,7 @@ xml: sources_def.xml update_wiz.xml rate.xml + diagram.xml + import_wiz.xml menu.xml cron.xml diff --git a/update_wiz.py b/update_wiz.py index 0c16980..50a36a3 100644 --- a/update_wiz.py +++ b/update_wiz.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. @@ -24,8 +24,14 @@ class UpdateSoureWizard(Wizard): context = Transaction().context assets = Asset.browse(context.get('active_ids', [])) + to_run_activities = [] for asset in assets: - OnlineSource.update_rate(asset) + if OnlineSource.update_rate(asset): + to_run_activities.append(asset) + + if to_run_activities: + Asset.after_update_actions(to_run_activities) + return 'end' # UpdateSoureWizard diff --git a/update_wiz.xml b/update_wiz.xml index b84e96b..73ae6e6 100644 --- a/update_wiz.xml +++ b/update_wiz.xml @@ -1,5 +1,5 @@ - diff --git a/view/asset_form.xml b/view/asset_form.xml index 8ae2bdb..e2230bb 100644 --- a/view/asset_form.xml +++ b/view/asset_form.xml @@ -2,24 +2,46 @@ -
+