Compare commits

...

64 commits
6.0 ... main

Author SHA1 Message Date
Frederik Jaeckel
4a3d996600 optimize code 2024-01-21 19:37:48 +01:00
Frederik Jaeckel
ab6ab32d0a update online source 'www.sbroker.de' 2024-01-21 19:14:05 +01:00
Frederik Jaeckel
4a2135512f accept http 410 on server call 2024-01-21 19:08:41 +01:00
Frederik Jaeckel
9f74b8fbf7 check regex-code when save online-source 2024-01-21 18:55:21 +01:00
Frederik Jaeckel
c2df388692 asset: columns optional 2023-12-03 18:20:22 +01:00
Frederik Jaeckel
18ef1aaeb6 Tryton 7.0 2023-12-01 13:31:43 +01:00
Frederik Jaeckel
faefea3b5f formatting 2023-12-01 13:29:46 +01:00
Frederik Jaeckel
aed948fb8a asset-list: remove percent-symbol to speed up list view 2023-06-23 21:40:55 +02:00
Frederik Jaeckel
ff5893b451 diagram: simplify queries 2023-06-23 16:41:39 +02:00
Frederik Jaeckel
6e77058946 asset: avoid exceptions in get_identifiers() 2023-06-23 16:29:10 +02:00
Frederik Jaeckel
38e2c34a53 asset/rate: speed up percent-queries 2023-06-23 16:02:31 +02:00
Frederik Jaeckel
3b9de6c0bb asset: fixed possible exceptions 2023-06-22 17:28:58 +02:00
Frederik Jaeckel
ddffa302c4 remove: graph_form.xml 2023-06-07 21:57:27 +02:00
Frederik Jaeckel
1c5b31d2f1 Tryton 6.8: tests, indexes, info 2023-06-07 21:52:09 +02:00
Frederik Jaeckel
765738d9ee test: add more context 2023-06-07 20:06:36 +02:00
Frederik Jaeckel
ebb6d71be3 test: add context to product-create 2023-06-07 19:24:43 +02:00
Frederik Jaeckel
8f12741d76 formatting 2023-06-07 18:44:53 +02:00
Frederik Jaeckel
82f9d3a792 asset/onlinesource: add fixed url,
pre defned online sources: add FAZ.net
2023-04-21 16:55:51 +02:00
Frederik Jaeckel
edfa0424e5 online source: formatting 2023-04-21 15:40:07 +02:00
Frederik Jaeckel
26192ea831 add online-source www.finanzen.net - Aktie 2023-04-21 15:31:10 +02:00
Frederik Jaeckel
fbbb7bd96c setup.py 2023-02-28 09:13:29 +01:00
Frederik Jaeckel
6f1b3707bd readme updated 2023-02-28 09:05:52 +01:00
Frederik Jaeckel
23665ae6dc removed unused imports 2023-01-13 13:48:50 +01:00
Frederik Jaeckel
6789e63872 OnlineSource: Query method prepared for external extension 2023-01-13 13:08:52 +01:00
Frederik Jaeckel
40ef3ef192 remove caching, optimize rate/query 2023-01-10 19:54:32 +01:00
Frederik Jaeckel
0daf39b913 asset: cachezeit verkürzt 2023-01-09 22:21:03 +01:00
Frederik Jaeckel
8503fd3e32 caching für change_day1/month1/... 2023-01-08 20:56:27 +01:00
Frederik Jaeckel
8cfa54f693 asset: abfragezeit für prozente optimiert 2023-01-07 16:26:22 +01:00
Frederik Jaeckel
3dc79fc292 rate: optimize for speed 2023-01-05 22:25:16 +01:00
Frederik Jaeckel
8a4c8fa58f asset: Feld 'change_symbol' für Prozenzzeichen + test 2023-01-04 20:20:27 +01:00
Frederik Jaeckel
166a9e13a9 asset: spalte 'einheit' entfällt, Spalte 'Kurs' hat nun die Einheit 2023-01-04 17:12:46 +01:00
Frederik Jaeckel
d9403e4946 quellen korrigiert 2022-12-24 12:38:10 +01:00
Frederik Jaeckel
935be70112 setup - read_file 2022-12-20 12:31:36 +01:00
Frederik Jaeckel
347f09b49e test für wizard 2022-12-19 21:53:00 +01:00
Frederik Jaeckel
77d6e5f8ef import wizard 2022-12-19 21:13:32 +01:00
Frederik Jaeckel
94687139b1 onlinesource: sortierung, neues datumsformat 'dd.mm.yy',
neue quellen: finanzen.net/Stuttgard, s-broker
2022-12-18 13:40:13 +01:00
Frederik Jaeckel
94169fb416 asset: rekursion behoben, erlaubt aktion nach update 2022-12-16 14:17:28 +01:00
Frederik Jaeckel
ac0bced963 updates an asset nach online-aktion 2022-12-16 13:37:36 +01:00
Frederik Jaeckel
fcabafa080 asset: Feld 'symbol' in übersetzter form 2022-12-09 09:43:30 +01:00
Frederik Jaeckel
e30953a55c asset: prozentwerte mit einheit im form 2022-12-06 08:53:40 +01:00
Frederik Jaeckel
407569a684 asset: currency_digits limit --> 6 2022-12-05 22:34:51 +01:00
Frederik Jaeckel
9061039ec2 asset: bereichsgrenzen für currency_digits, farbe rot für tagesnegativ 2022-12-05 21:02:17 +01:00
Frederik Jaeckel
69afec62b0 import-script: add field-delimiter 2022-12-05 09:49:59 +01:00
Frederik Jaeckel
eb4af774c0 symbol fix 2022-12-03 00:08:55 +01:00
Frederik Jaeckel
e972d54f54 felder symbol, name, product_uom optimiert,
felder company_currency entfernt
2022-12-02 23:29:01 +01:00
Frederik Jaeckel
23d4790e3b icon 2022-12-02 14:58:54 +01:00
Frederik Jaeckel
71b52d106a asset: liste mit reiter für aktuell/inaktiv/alle 2022-12-02 12:56:14 +01:00
Frederik Jaeckel
f4310c596a asset: rec_name - einheit als währung/menge 2022-12-01 16:55:27 +01:00
Frederik Jaeckel
de99f0e473 asset: einheit als währung/menge 2022-12-01 16:30:50 +01:00
Frederik Jaeckel
c8c2afb190 icons 2022-11-30 21:50:31 +01:00
Frederik Jaeckel
78fc16d7e2 fix imports 2022-11-30 13:59:20 +01:00
Frederik Jaeckel
cc5a2fd164 setup.py 2022-11-30 13:35:21 +01:00
Frederik Jaeckel
9c5af080f5 importscript für historische kurse 2022-11-30 13:32:16 +01:00
Frederik Jaeckel
258c14e2b5 asset: online-quelle als liste 2022-11-29 21:54:27 +01:00
Frederik Jaeckel
7334e53f9f diagram: interpolator ergänzt,
asset: berechnung 'nextupdate' korrigiert
2022-11-28 23:15:28 +01:00
Frederik Jaeckel
d08cf19b60 asset: sortierung - neueste nach oben, farben - älter als 5 tage = gedimmt 2022-11-28 15:44:07 +01:00
Frederik Jaeckel
1a61b112e0 asset:tabellenzugriff optimiert,
diagram: darstellung in diagramm ergänzt
2022-11-26 22:42:02 +01:00
Frederik Jaeckel
83acfbb14b asset: suche in name, sortiert nach name, sortierer für wkn, isin, symbl 2022-11-25 22:46:32 +01:00
Frederik Jaeckel
82c451377c asset: spalten tag/monat/3monate... ok + test 2022-11-25 21:55:43 +01:00
Frederik Jaeckel
d70e674e58 abfrage der prozentwerte für tag/1 monat, 3 monate, 6 monate, 12 monate
test muß noch
2022-11-25 15:28:49 +01:00
Frederik Jaeckel
cba2117d13 asset: abfrage updatezeitpunkt optimiert, anzeige der vortagsprozente, farbe für zeilen 2022-11-25 11:00:03 +01:00
Frederik Jaeckel
34faf91476 prozentwerte begonnen 2022-11-24 23:17:44 +01:00
Frederik Jaeckel
f323334800 bug in updtneeded-suche, online-quellen erweitert 2022-11-23 22:22:22 +01:00
Frederik Jaeckel
1daa743631 asset: feld 'Datum' + 'Name' neu, rec_name optimiert, Test korrigiert 2022-11-23 10:44:09 +01:00
45 changed files with 3516 additions and 993 deletions

View file

@ -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

View file

@ -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')

914
asset.py

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
@ -35,6 +35,29 @@ full copyright notices and license terms. -->
<field name="act_window" ref="act_asset_view"/>
</record>
<!-- domain view -->
<record model="ir.action.act_window.domain" id="act_asset_domain_current">
<field name="name">Current</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('date', '&gt;=', Date(delta_days=-5))]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_asset_view"/>
</record>
<record model="ir.action.act_window.domain" id="act_asset_domain_inactive">
<field name="name">Inactive</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('date', '&lt;', Date(delta_days=-5))]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_asset_view"/>
</record>
<record model="ir.action.act_window.domain" id="act_asset_domain_all">
<field name="name">All</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_asset_view"/>
</record>
<!-- permission -->
<!-- anon: deny all -->
<record model="ir.model.access" id="access_asset-anon">

8
const.py Normal file
View file

@ -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

View file

@ -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.

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>

148
diagram.py Normal file
View file

@ -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

16
diagram.xml Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data depends="diagram">
<!-- views -->
<record model="ir.ui.view" id="graph_view_form">
<field name="model">diagram.graphdef</field>
<field name="inherit" ref="diagram.graph_view_form"/>
<field name="name">graph_form</field>
</record>
</data>
</tryton>

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
<!-- 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. -->
<tryton>
@ -10,5 +10,15 @@ full copyright notices and license terms. -->
<field name="path">icon/gain_invest.svg</field>
</record>
<record model="ir.ui.icon" id="stockonline_icon">
<field name="name">mds-stockonline</field>
<field name="path">icon/stockonline.svg</field>
</record>
<record model="ir.ui.icon" id="asset_icon">
<field name="name">mds-asset</field>
<field name="path">icon/asset.svg</field>
</record>
</data>
</tryton>

5
icon/asset.svg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<g><path d="M729.7,990c-143.8,0-260.3-116.6-260.3-260.3c0-143.8,116.5-260.3,260.3-260.3S990,585.9,990,729.7C990,873.4,873.4,990,729.7,990L729.7,990z M729.7,530.6c-109.9,0-199.1,89.1-199.1,199.1c0,109.9,89.1,199.1,199.1,199.1s199.1-89.1,199.1-199.1C928.7,619.8,839.6,530.6,729.7,530.6L729.7,530.6z M852.2,775.6H699.1c-8.5,0-15.3-6.9-15.3-15.3V607.2c0-8.5,6.9-15.3,15.3-15.3h30.6c8.5,0,15.3,6.9,15.3,15.3v107.2h107.2c8.5,0,15.3,6.9,15.3,15.3v30.6C867.5,768.8,860.6,775.6,852.2,775.6L852.2,775.6z M836.9,101.9c0-16.9-13.7-30.6-30.6-30.6H101.9C85,71.3,71.2,85,71.2,101.9v796.3c0,16.9,13.7,30.6,30.6,30.6h306.2V990H71.2C37.4,990,10,962.5,10,928.7V71.2C10,37.4,37.4,10,71.2,10h765.6c33.8,0,61.3,27.4,61.3,61.3v275.6h-61.3V101.9L836.9,101.9z M239.7,683.7h122.5c8.4,0,15.3,6.9,15.3,15.3v30.6c0,8.4-6.9,15.3-15.3,15.3H239.7c-8.5,0-15.3-6.9-15.3-15.3v-30.6C224.4,690.6,231.2,683.7,239.7,683.7L239.7,683.7z M224.4,301v-30.6c0-8.5,6.8-15.3,15.3-15.3h459.4c8.5,0,15.3,6.9,15.3,15.3V301c0,8.4-6.9,15.3-15.3,15.3H239.7C231.2,316.3,224.4,309.4,224.4,301L224.4,301z M224.4,515.3v-30.6c0-8.5,6.8-15.3,15.3-15.3h214.4c8.5,0,15.3,6.9,15.3,15.3v30.6c0,8.4-6.9,15.3-15.3,15.3H239.7C231.2,530.6,224.4,523.7,224.4,515.3L224.4,515.3z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

5
icon/stockonline.svg Normal file
View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<g><g><g><path d="M636.1,146.1c-34.8,0-68.3,5.1-100.1,14.5c-2.7,6.1-5.3,12.2-7.8,18.3l0,0c-3.4,8.3-6.8,16.6-9.9,25c23.8-18.5,50.1-31.5,77.9-38c-17.1,17.7-32.3,46.5-44.7,83.7c-18.2,3.3-35.8,7.5-52.7,12.7c-1.7,5.8-3.2,11.6-4.8,17.4c16.8-5.6,34.4-10.2,52.5-13.8c-10.2,34.9-18.1,75.9-23.1,121.3c-18.3,2-35.9,4.4-52.6,7.3c-0.7,5.2-1.4,10.4-2,15.6c16.7-3,34.5-5.6,53.1-7.6c-1.4,14.7-2.4,29.8-3.2,45.2c1.2-0.1,2.4-0.3,3.7-0.3h11.4c0.8-16,1.9-31.6,3.3-46.5c30.8-2.9,63.9-4.6,99.1-4.6c35.2,0,68.3,1.7,99.1,4.6c2.9,30.8,4.6,63.9,4.6,99.1c0,35.2-1.6,68.3-4.6,99.1c-30.8,2.9-63.9,4.6-99.1,4.6s-68.3-1.7-99.1-4.6c-1.8-18.7-3-38.4-3.8-58.7l-13.6,30.4c0.6,9,1.3,18,2.1,26.7c-4.5-0.5-8.9-1.1-13.3-1.6l-6.4,14.3c7,0.9,14,1.8,21.2,2.6c5,45.4,12.9,86.5,23.1,121.3c-29.1-5.7-56.6-14.2-82.1-25c0.9,5.9,1.9,11.7,3,17.5c26.2,10.4,54.4,18.4,84.1,23.7c12.4,37.3,27.7,66.1,44.8,83.8c-47-10.9-89.5-40.5-123.3-83.4c3.4,12.2,7.5,24.2,12,35.8c13.2,13.2,27.4,24.7,42.4,34.2c-11.5-3.9-22.7-8.4-33.6-13.5c3.1,6.9,6.2,13.8,9.4,20.6c41.1,16.7,85.9,26,132.9,26c195.2,0,354-158.8,354-354C990,304.8,831.2,146.1,636.1,146.1z M889.2,432.1c54.6,19.3,85.7,44,85.7,67.9c0,23.9-31.2,48.6-85.7,67.9c3.1-22,4.8-44.7,4.8-67.9C894,476.8,892.3,454.1,889.2,432.1z M886.5,415.3c-6.4-35.7-16.7-69.2-30.4-99.7C915.4,352,957,402.7,970.3,460.2C952.6,443,923.8,427.8,886.5,415.3z M750.4,402.5c47.5,5.3,88.8,13.8,122.7,24.2c3.8,23.6,5.8,48.1,5.8,73.3c0,25.1-2,49.6-5.8,73.3c-33.9,10.5-75.2,18.9-122.7,24.2c2.9-30.9,4.4-63.5,4.4-97.5C754.9,466,753.3,433.4,750.4,402.5z M956.9,391.3c-25.5-39.7-64.4-74-112.5-99.6c-25.6-48.1-59.8-87-99.6-112.5C844.3,213,923.1,291.8,956.9,391.3z M820.5,280c-30.5-13.6-64.1-23.9-99.7-30.3c-12.4-37.3-27.7-66.1-44.8-83.8C733.4,179.1,784.1,220.7,820.5,280z M636.1,161.2c23.9,0,48.6,31.2,67.9,85.7c-22-3.1-44.7-4.8-67.9-4.8c-23.2,0-46,1.7-67.9,4.8C587.4,192.3,612.2,161.2,636.1,161.2z M636.1,381.3c-34,0-66.6,1.6-97.5,4.5c5.3-47.5,13.8-88.9,24.3-122.7c23.6-3.8,48.1-5.8,73.2-5.8c25.2,0,49.7,2,73.3,5.8c10.5,33.9,18.9,75.2,24.2,122.7C702.7,382.8,670,381.3,636.1,381.3z M725.8,265.9c39.1,7.7,75.4,20.3,107.5,36.9c16.5,32.1,29.1,68.4,36.8,107.5c-34.9-10.2-75.9-18.1-121.3-23.1C743.9,341.8,736,300.8,725.8,265.9z M748.9,612.8c45.4-5,86.4-12.9,121.3-23.1c-7.7,39.1-20.3,75.4-36.8,107.5c-32.1,16.5-68.4,29.1-107.5,36.9C736,699.3,743.9,658.2,748.9,612.8z M538.6,614.3c30.9,2.9,63.5,4.4,97.5,4.4c34,0,66.6-1.6,97.5-4.4c-5.3,47.5-13.8,88.8-24.2,122.7c-23.6,3.7-48.1,5.8-73.3,5.8c-25.2,0-49.7-2.1-73.3-5.8C552.3,703.1,543.9,661.8,538.6,614.3z M636.1,838.8c-23.9,0-48.6-31.1-67.9-85.7c22,3.1,44.7,4.8,67.9,4.8c23.2,0,45.9-1.7,67.9-4.8C684.7,807.7,660,838.8,636.1,838.8z M720.8,750.4c35.7-6.4,69.2-16.7,99.7-30.3c-36.4,59.3-87.1,100.8-144.6,114.2C693.1,816.6,708.3,787.7,720.8,750.4z M744.8,820.8c39.7-25.5,74-64.4,99.6-112.5c48.1-25.6,87-59.9,112.5-99.6C923.1,708.2,844.3,787,744.8,820.8z M856.1,684.4c13.6-30.5,23.9-64.1,30.4-99.7c37.3-12.4,66.1-27.7,83.8-44.9C957,597.3,915.4,648.1,856.1,684.4z"/><path d="M460,612.9L512,496.6h136.3c8.3,0,15.1-6.8,15.1-15.1c0-8.3-6.8-15.1-15.1-15.1H502.1c-6,0-11.4,3.5-13.8,9l-56.7,127.1c-0.6,0-1.1-0.1-1.7-0.1c-1.5,0-3,0.1-4.5,0.2l-90-252.2c11.4-8.9,18.8-22.7,18.8-38.2c0-26.7-21.7-48.4-48.4-48.4c-26.7,0-48.4,21.7-48.4,48.4c0,12.6,4.9,24.1,12.9,32.8l-58.9,105.8H104.4c-6.4-19.3-24.6-33.3-46-33.3c-26.7,0-48.4,21.7-48.4,48.4c0,26.7,21.7,48.4,48.4,48.4c21.5,0,39.7-14,46-33.4h115.7c5.5,0,10.5-3,13.2-7.8l63.2-113.5c3,0.6,6,0.9,9.2,0.9c0.4,0,0.8-0.1,1.2-0.1l90.7,254.2c-9.9,8.9-16.2,21.7-16.2,36.1c0,26.7,21.7,48.4,48.4,48.4c26.7,0,48.4-21.7,48.4-48.4C478.4,635.5,471.2,621.8,460,612.9z M58.4,506.8c-22.5,0-40.9-18.3-40.9-40.9c0-22.5,18.3-40.9,40.9-40.9c17.2,0,31.9,10.7,37.9,25.7H58.4c-8.3,0-15.1,6.8-15.1,15.1c0,8.3,6.8,15.1,15.1,15.1h38C90.4,496,75.7,506.8,58.4,506.8z M264.8,312.2c0-22.5,18.3-40.9,40.9-40.9c22.5,0,40.9,18.3,40.9,40.9c0,12.2-5.4,23.1-13.9,30.6l-12.8-35.7c-2-5.6-7.1-9.5-13-10c-5.9-0.5-11.6,2.5-14.4,7.7L274,338C268.3,330.9,264.8,322,264.8,312.2z M300.4,352.7l2.2-4l1.5,4.3C303,352.9,301.7,352.8,300.4,352.7z M428.2,610l-0.1,0.2l-0.1-0.2C428.1,610,428.2,610,428.2,610z M429.9,691.7c-22.5,0-40.9-18.3-40.9-40.9c0-11,4.4-20.9,11.4-28.3l11.9,33.4c2.1,5.8,7.5,9.8,13.7,10c0.2,0,0.4,0,0.6,0c5.9,0,11.4-3.5,13.8-9l16.4-36.8c8.5,7.5,14,18.5,14,30.7C470.8,673.3,452.5,691.7,429.9,691.7z"/></g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g><g></g></g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View file

@ -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

211
import_wiz.py Normal file
View file

@ -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

31
import_wiz.xml Normal file
View file

@ -0,0 +1,31 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data>
<record model="ir.ui.view" id="imp_wiz_start_form">
<field name="model">investment.imp_wiz.start</field>
<field name="type">form</field>
<field name="name">import_start_form</field>
</record>
<record model="ir.action.wizard" id="act_import_wizard">
<field name="name">Import CSV-File</field>
<field name="wiz_name">investment.imp_wiz</field>
</record>
<record model="ir.action-res.group"
id="act_import_wizard-group_investment_admin">
<field name="action" ref="act_import_wizard"/>
<field name="group" ref="group_investment_admin"/>
</record>
<record model="ir.action.keyword" id="act_import_wizard-cat-keyword">
<field name="keyword">form_action</field>
<field name="model">investment.asset,-1</field>
<field name="action" ref="act_import_wizard"/>
</record>
</data>
</tryton>

View file

@ -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"

View file

@ -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"

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
@ -19,7 +19,7 @@ full copyright notices and license terms. -->
<!-- menu: /Investment/Online Source -->
<menuitem id="menu_onlinesource" action="act_source_view" sequence="10"
icon="tryton-list" parent="menu_investment"/>
icon="mds-stockonline" parent="menu_investment"/>
<record model="ir.ui.menu-res.group" id="menu_onlinesource-group_investment_admin">
<field name="menu" ref="menu_onlinesource"/>
<field name="group" ref="group_investment_admin"/>
@ -27,7 +27,7 @@ full copyright notices and license terms. -->
<!-- menu: /Investment/Asset -->
<menuitem id="menu_asset" action="act_asset_view" sequence="20"
icon="tryton-list" parent="menu_investment"/>
icon="mds-asset" parent="menu_investment"/>
<record model="ir.ui.menu-res.group" id="menu_asset-group_investment">
<field name="menu" ref="menu_asset"/>
<field name="group" ref="group_investment"/>

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
@ -11,6 +11,21 @@ full copyright notices and license terms. -->
<record model="ir.message" id="msg_rate_positive">
<field name="text">A asset rate must be positive.</field>
</record>
<record model="ir.message" id="msg_import_err_date">
<field name="text">failed to read column %(colnr)s of file, expected date (format: %(datefmt)s)</field>
</record>
<record model="ir.message" id="msg_err_unknown_content">
<field name="text">failed to identify row content: %(linetxt)s</field>
</record>
<record model="ir.message" id="msg_querytype_web">
<field name="text">Extract from web page</field>
</record>
<record model="ir.message" id="msg_missing_url">
<field name="text">URL for the online source '%(oname)s' is missing.</field>
</record>
<record model="ir.message" id="msg_bug_in_regexquery">
<field name="text">Error in regex code of field '%(fname)s': %(errmsg)s [%(code)s]</field>
</record>
</data>
</tryton>

View file

@ -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,
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,7 +231,7 @@ 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
"""
@ -178,7 +240,13 @@ class OnlineSource(ModelSQL, ModelView):
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,
)
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 '',
})
'symbol': symbol if symbol is not None else ''})
@classmethod
def update_rate(cls, asset):
@ -221,17 +324,20 @@ 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,
for updtsource in asset.updtsources:
rate_data = cls.run_query_method(
updtsource,
isin=asset.isin,
nsin=asset.wkn,
symbol=asset.secsymb,
)
url=asset.updturl)
if len(asset.updtsource.rgxident or '') > 0:
if len(updtsource.rgxident or '') > 0:
# check result - same code?
code = rate_data.get('code', None)
if code:
@ -239,42 +345,54 @@ class OnlineSource(ModelSQL, ModelView):
'isin': 'isin',
'nsin': 'wkn',
'symbol': 'secsymb',
}[asset.updtsource.rgxidtype])
}[updtsource.rgxidtype])
if (asset_code or '').lower() != code.lower():
# fail
logger.warning(
'update_rate: got wrong code "%(wrong)s" - expected "%(exp)s"' % {
'update_rate: got wrong code ' +
'"%(wrong)s" - expected "%(exp)s"' % {
'exp': asset_code,
'wrong': code,
})
return False
'wrong': code})
continue
to_create = {
'date': rate_data.get('date', None),
'rate': rate_data.get('rate', None),
'asset': asset.id,
}
'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:
('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
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:
result = search_result.group(1)
@ -285,20 +403,23 @@ class OnlineSource(ModelSQL, ModelView):
dec_sep = [',', '.']
dec_sep.remove(self.rgxdecimal)
result = result.replace(dec_sep[0], '').replace(self.rgxdecimal, '.')
result = result.replace(
dec_sep[0], '').replace(self.rgxdecimal, '.')
try:
result = Decimal(result)
except :
except Exception:
result = None
elif field_name == 'rgxdate':
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 = {}
@ -312,7 +433,7 @@ class OnlineSource(ModelSQL, ModelView):
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
@ -342,7 +463,9 @@ class OnlineSource(ModelSQL, ModelView):
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]' % {
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]),

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>

104
rate.py
View file

@ -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

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>

2
scripts/__init__.py Normal file
View file

@ -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.

View file

@ -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()

View file

@ -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:
@ -39,7 +39,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
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,23 +51,27 @@ 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']))
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']))
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, < %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',
@ -87,6 +91,7 @@ setup(name='%s_%s' % (PREFIX, MODULE),
'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,9 +100,9 @@ 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',
'trytond.modules.%s' % MODULE: (info.get('xml', []) + [
'tryton.cfg', 'locale/*.po', 'tests/*.py',
'view/*.xml', 'icon/*.svg', 'scripts/*.py',
'versiondep.txt', 'README.rst']),
},
@ -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),
)

View file

@ -1,19 +1,93 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>
<data>
<record model="investment.source" id="web_finanzen_net">
<field name="name">www.finanzen.net - ETF</field>
<field name="name">www.finanzen.net - ETF (Tradegate)</field>
<field name="url">https://www.finanzen.net/etf/${isin}/tgt</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\nKurszeit\s+(\d+\.\d+\.\d+)\s+\d{2}:\d{2}:\d{2}.*\n</field>
<field name="rgxdatefmt">%d.%m.%Y</field>
<field name="rgxrate">\nKurs\s+(\d+,\d+)\s+EUR.*\n</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">WKN:.* ISIN:\s+([A-Z,0-9]+).*</field>
<field name="rgxidtype">isin</field>
</record>
<record model="investment.source" id="web_finanzen_net_stu">
<field name="name">www.finanzen.net - ETF (Stuttgard)</field>
<field name="url">https://www.finanzen.net/etf/${isin}/stu</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\nKurszeit\s+(\d+\.\d+\.\d+)\s+\d{2}:\d{2}:\d{2}.*\n</field>
<field name="rgxdatefmt">%d.%m.%Y</field>
<field name="rgxrate">\nKurs\s+(\d+,\d+)\s+EUR.*\n</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">WKN:.* ISIN: ([A-Z,0-9]+).*</field>
<field name="rgxidtype">isin</field>
</record>
<record model="investment.source" id="web_finanzen_fonds">
<field name="name">www.finanzen.net - Fonds</field>
<field name="url">https://www.finanzen.net/fonds/${isin}/tgt</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\n\*\*Kursdatum\*\*\s+(\d+\.\d+\.\d+).*\n</field>
<field name="rgxdatefmt">%d.%m.%Y</field>
<field name="rgxrate">\n\*\*Kurs\*\*\s+(\d+,\d+)\s+EUR.*\n</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">WKN:.*\s+ISIN:\s+([A-Z,0-9]+).*</field>
<field name="rgxidtype">isin</field>
</record>
<record model="investment.source" id="web_finanzen_rohstoffe">
<field name="name">www.finanzen.net - Rohstoffe</field>
<field name="url">https://www.finanzen.net/rohstoffe/${symbol}</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\nKurszeit\s+(\d+\.\d+\.\d+)\s+\d+:\d+:\d+.*\n</field>
<field name="rgxdatefmt">%d.%m.%Y</field>
<field name="rgxrate">\nKurs\s+([\d+\.]*\d+,\d+)\s+USD\s*\n</field>
<field name="rgxdecimal">,</field>
</record>
<record model="investment.source" id="web_finanzen_aktie">
<field name="name">www.finanzen.net - Aktie</field>
<field name="url">https://www.finanzen.net/aktien/${symbol}</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\nKurszeit(\d+\.\d+\.\d+) \d{2}:\d{2}:\d{2}.*\n</field>
<field name="rgxdatefmt">%d.%m.%Y</field>
<field name="rgxrate">\nKurs(\d+,\d+) EUR.*\n</field>
<field name="rgxident">\nISIN([A-Z,0-9]+).*\n</field>
<field name="rgxidtype">isin</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">WKN:.* ISIN: ([A-Z,0-9]+).*</field>
</record>
<record model="investment.source" id="web_markets_ft_com">
<field name="name">Financial Times UK</field>
<field name="url">https://markets.ft.com/data/etfs/tearsheet/summary?s=${symbol}</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">Data delayed at least 15 minutes, as of (.*) \d+:\d+ GMT\.</field>
<field name="rgxdatefmt">%b %d %Y</field>
<field name="rgxrate">Price\D+([\d,]*\d+\.\d+)</field>
<field name="rgxdecimal">.</field>
</record>
<record model="investment.source" id="web_sbroker">
<field name="name">www.sbroker.de</field>
<field name="url">https://www.sbroker.de/sbl/mdaten_analyse/dksuche_a?SEARCH_VALUE=${isin}</field>
<field name="nohtml" eval="True"/>
<field name="rgxdate">\nDatum\s*(?:\/ Uhrzeit)*: (\d+\.\d+\.\d+)\s*(?:\/ \d+:\d+)*\s*\n</field>
<field name="rgxdatefmt">%d.%m.%y</field>
<field name="rgxrate">(?:Kurs aktuell|Rucknahmepreis)\s+[()\w\./\[\]!]+\s+(\d+,\d+)\s+(EUR|€)</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">\nWKN / ISIN: [A-Z,0-9]+ / ([A-Z,0-9]+)\s+\n</field>
<field name="rgxidtype">isin</field>
</record>
<record model="investment.source" id="web_faz">
<field name="name">FAZ</field>
<field name="url">https://www.faz.net/aktuell/finanzen/kurs/etf/</field>
<field name="nohtml" eval="True"/>
<field name="fixed_url" eval="True"/>
<field name="rgxdate">(\d{2}\.\d{2}\.\d{4})</field>
<field name="rgxdatefmt">%d.%m.%y</field>
<field name="rgxrate">\nKurs(\d+.\d+)\s+€\s+\n</field>
<field name="rgxdecimal">,</field>
<field name="rgxident">\nWKN:.*\sISIN: ([A-Z,0-9]+).*\n</field>
<field name="rgxidtype">isin</field>
</record>

View file

@ -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

770
tests/asset.py Normal file
View file

@ -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

50
tests/rate.py Normal file
View file

@ -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

185
tests/source.py Normal file
View file

@ -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 = """<html><body>Response from finance-server
Course Date 14.08.2022 Today
High 34,87 EUR
</body></html>""".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

View file

@ -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

26
tests/test_module.py Normal file
View file

@ -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

View file

@ -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

View file

@ -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 = """<html><body>Response from finance-server
Course Date 14.08.2022 Today
High 34,87 EUR
</body></html>""".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

78
tests/wizard.py Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,5 +1,5 @@
<?xml version="1.0"?>
<!-- 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. -->
<tryton>

View file

@ -2,24 +2,46 @@
<!-- 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. -->
<form col="4">
<form col="6">
<label name="product" />
<field name="product" />
<field name="product" colspan="3"/>
<label name="rate" />
<field name="rate" symbol="currency"/>
<field name="rate" symbol="asset_symbol"/>
<separator id="sepunits" colspan="4" string="Currency and Units"/>
<label id="labdate" string=" "/>
<label name="nextupdate" colspan="2"/>
<field name="nextupdate"/>
<label name="date" />
<field name="date"/>
<separator id="sepperc" colspan="6" string="Gain and Loss"/>
<label name="change_day1" />
<field name="change_day1" symbol="change_symbol"/>
<label name="change_month1" />
<field name="change_month1" symbol="change_symbol"/>
<label name="change_month3" />
<field name="change_month3" symbol="change_symbol"/>
<label name="change_month6" />
<field name="change_month6" symbol="change_symbol"/>
<label name="change_month12" />
<field name="change_month12" symbol="change_symbol"/>
<newline/>
<separator id="sepunits" colspan="6" string="Currency and Units"/>
<label name="currency" />
<field name="currency" />
<label name="currency_digits" />
<field name="currency_digits" />
<newline/>
<label name="uom" />
<field name="uom" />
<label name="product_uom" />
<field name="product_uom" />
<newline/>
<notebook>
<notebook colspan="6">
<page id="pgids" col="4" string="Identifiers">
<label name="wkn" />
<field name="wkn" />
@ -33,10 +55,17 @@ full copyright notices and license terms. -->
<field name="rates" mode="tree,form,graph"/>
</page>
<page id="pgupdate" col="4" string="Course Update">
<label name="updtsource"/>
<field name="updtsource"/>
<label name="updttime"/>
<field name="updttime"/>
<label name="updtdays"/>
<field name="updtdays"/>
<label name="updturl"/>
<field name="updturl" colspan="3"/>
<field name="updturl_enable"/>
<newline/>
<field name="updtsources" colspan="4"/>
</page>
</notebook>

View file

@ -3,11 +3,13 @@
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree>
<field name="rec_name"/>
<field name="isin"/>
<field name="secsymb"/>
<field name="wkn"/>
<field name="rate"/>
<field name="currency"/>
<field name="uom"/>
<field name="name" expand="1"/>
<field name="change_day1" optional="0"/>
<field name="change_month1" optional="0"/>
<field name="change_month3" optional="0"/>
<field name="change_month6" optional="0"/>
<field name="date" optional="0"/>
<field name="rate" symbol="asset_symbol" optional="0"/>
<field name="isin" optional="0"/>
<field name="wkn" optional="0"/>
</tree>

View file

@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- 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. -->
<form col="4">
<label name="asset"/>
<field name="asset" colspan="3"/>
<label name="dec_divider"/>
<field name="dec_divider"/>
<label name="date_fmt"/>
<field name="date_fmt"/>
<label name="field_delimiter"/>
<field name="field_delimiter"/>
<newline/>
<label name="file_"/>
<field name="file_"/>
</form>

View file

@ -5,16 +5,24 @@ full copyright notices and license terms. -->
<form col="6">
<label name="name"/>
<field name="name"/>
<label name="query_method"/>
<field name="query_method"/>
<newline/>
<separator colspan="6" name="url" string="Method: Extract from web page"/>
<label name="url"/>
<field name="url" colspan="3"/>
<label name="nohtml"/>
<field name="nohtml"/>
<label id="labtp1" string=" "/>
<label xalign="0.0" colspan="3" id="labtempl"
string="URL parameter placeholders: ${isin}, ${nsin}, ${symbol}"/>
<separator colspan="6" id="seprgx" string="Regular expressions to find data"/>
<label id="labtp1" string=" "/>
<label xalign="0.0" colspan="3" name="nohtml"
string="URL parameter placeholders: ${isin}, ${nsin}, ${symbol}"/>
<label name="fixed_url"/>
<field name="fixed_url"/>
<newline/>
<separator colspan="6" name="url" string="Regular expressions to find data"/>
<label name="rgxdate"/>
<field name="rgxdate"/>
<label name="rgxdatefmt"/>

View file

@ -4,5 +4,6 @@ The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tree>
<field name="name"/>
<field name="query_method"/>
<field name="url"/>
</tree>