Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4ae5601c04 | ||
![]() |
31c76dfb48 | ||
![]() |
57cb06d60e | ||
![]() |
5587bfea3a | ||
![]() |
f919d9e290 | ||
![]() |
e8614b1242 | ||
![]() |
a71bc0a79a | ||
![]() |
f1d9b3b1dd | ||
![]() |
4947b495c2 | ||
![]() |
aeb949cc20 | ||
![]() |
916d73ef12 |
10 changed files with 543 additions and 124 deletions
18
README.rst
18
README.rst
|
@ -14,6 +14,24 @@ Requires
|
||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
*6.0.4 - 25.11.2022*
|
||||||
|
|
||||||
|
- add: assets - colors for percentual success/loss
|
||||||
|
- updt: optimize timestamp for next online-update
|
||||||
|
|
||||||
|
*6.0.3 - 23.11.2022*
|
||||||
|
|
||||||
|
- fix: bug in searcher
|
||||||
|
- add: online-sources
|
||||||
|
|
||||||
|
*6.0.2 - 23.11.2022*
|
||||||
|
|
||||||
|
- asset: add field 'date', optimized 'rec_name'
|
||||||
|
|
||||||
|
*6.0.1 - 22.11.2022*
|
||||||
|
|
||||||
|
- works
|
||||||
|
|
||||||
*6.0.0 - 09.11.2022*
|
*6.0.0 - 09.11.2022*
|
||||||
|
|
||||||
- init
|
- init
|
||||||
|
|
313
asset.py
313
asset.py
|
@ -6,17 +6,21 @@
|
||||||
from trytond.model import ModelView, ModelSQL, fields
|
from trytond.model import ModelView, ModelSQL, fields
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from trytond.pool import Pool
|
from trytond.pool import Pool
|
||||||
from trytond.pyson import Eval, Bool, And
|
from trytond.pyson import Eval, Bool, And, If
|
||||||
|
from trytond.report import Report
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import time
|
from datetime import time
|
||||||
from sql.functions import CurrentTime
|
from sql.functions import CurrentDate, CurrentTimestamp
|
||||||
from sql.conditionals import Case
|
from sql.conditionals import Case, Coalesce
|
||||||
|
from sql import Literal
|
||||||
|
|
||||||
|
|
||||||
class Asset(ModelSQL, ModelView):
|
class Asset(ModelSQL, ModelView):
|
||||||
'Asset'
|
'Asset'
|
||||||
__name__ = 'investment.asset'
|
__name__ = 'investment.asset'
|
||||||
|
|
||||||
|
name = fields.Function(fields.Char(string='Name', readonly=True),
|
||||||
|
'on_change_with_name')
|
||||||
company = fields.Many2One(string='Company', model_name='company.company',
|
company = fields.Many2One(string='Company', model_name='company.company',
|
||||||
required=True, ondelete="RESTRICT")
|
required=True, ondelete="RESTRICT")
|
||||||
product = fields.Many2One(string='Product', required=True,
|
product = fields.Many2One(string='Product', required=True,
|
||||||
|
@ -38,7 +42,9 @@ class Asset(ModelSQL, ModelView):
|
||||||
model_name='investment.rate')
|
model_name='investment.rate')
|
||||||
rate = fields.Function(fields.Numeric(string='Current Rate',
|
rate = fields.Function(fields.Numeric(string='Current Rate',
|
||||||
readonly=True, digits=(16, Eval('currency_digits', 4)),
|
readonly=True, digits=(16, Eval('currency_digits', 4)),
|
||||||
depends=['currency_digits']), 'on_change_with_rate')
|
depends=['currency_digits']), 'get_rate_data')
|
||||||
|
date = fields.Function(fields.Date(string='Date', readonly=True,
|
||||||
|
help='Date of current rate'), 'get_rate_data')
|
||||||
|
|
||||||
company_currency = fields.Function(fields.Many2One(readonly=True,
|
company_currency = fields.Function(fields.Many2One(readonly=True,
|
||||||
string='Company Currency', states={'invisible': True},
|
string='Company Currency', states={'invisible': True},
|
||||||
|
@ -70,9 +76,40 @@ class Asset(ModelSQL, ModelView):
|
||||||
states={
|
states={
|
||||||
'readonly': ~Bool(Eval('updtsource')),
|
'readonly': ~Bool(Eval('updtsource')),
|
||||||
}, depends=['updtsource'])
|
}, depends=['updtsource'])
|
||||||
updtneeded = fields.Function(fields.Boolean(string='Course update needed',
|
nextupdtate = fields.Function(fields.DateTime(string='Next Update',
|
||||||
readonly=True),
|
readonly=True),
|
||||||
'on_change_with_updtneeded', searcher='search_updtneeded')
|
'get_nextupdtates', searcher='search_nextupdtate')
|
||||||
|
|
||||||
|
# percentage change
|
||||||
|
change_today = fields.Function(fields.Numeric(string='Previous Day',
|
||||||
|
help='percentage change in value compared to the previous day',
|
||||||
|
readonly=True, digits=(16,1)),
|
||||||
|
'get_percentage_change')
|
||||||
|
change_month = fields.Function(fields.Numeric(string='1 Month',
|
||||||
|
help='percentage change in value compared to last month',
|
||||||
|
readonly=True, digits=(16,1)),
|
||||||
|
'get_percentage_change')
|
||||||
|
change_3month = fields.Function(fields.Numeric(string='3 Months',
|
||||||
|
help='percentage change in value during 3 months',
|
||||||
|
readonly=True, digits=(16,1)),
|
||||||
|
'get_percentage_change')
|
||||||
|
change_6month = fields.Function(fields.Numeric(string='6 Months',
|
||||||
|
help='percentage change in value during 6 months',
|
||||||
|
readonly=True, digits=(16,1)),
|
||||||
|
'get_percentage_change')
|
||||||
|
change_12month = fields.Function(fields.Numeric(string='1 Year',
|
||||||
|
help='percentage change in value during 1 year',
|
||||||
|
readonly=True, digits=(16,1)),
|
||||||
|
'get_percentage_change')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def view_attributes(cls):
|
||||||
|
return super().view_attributes() + [
|
||||||
|
('/tree', 'visual',
|
||||||
|
If(Eval('change_today', 0) < 0, 'warning',
|
||||||
|
If(Eval('change_today', 0) > 0, 'success', '')
|
||||||
|
)),
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default_currency(cls):
|
def default_currency(cls):
|
||||||
|
@ -96,6 +133,142 @@ class Asset(ModelSQL, ModelView):
|
||||||
"""
|
"""
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_percentage_sql(cls, table_asset):
|
||||||
|
""" get table for percentages and dates
|
||||||
|
"""
|
||||||
|
pool = Pool()
|
||||||
|
Rate = pool.get('investment.rate')
|
||||||
|
tab_rate_today = Rate.__table__()
|
||||||
|
tab_rate_1day = Rate.__table__()
|
||||||
|
tab_rate_1month = Rate.__table__()
|
||||||
|
tab_rate_3month = Rate.__table__()
|
||||||
|
context = Transaction().context
|
||||||
|
|
||||||
|
query_date = context.get('qdate', CurrentDate())
|
||||||
|
query_today = table_asset.join(tab_rate_today,
|
||||||
|
condition=table_asset.id==tab_rate_today.asset,
|
||||||
|
).select(
|
||||||
|
table_asset.id,
|
||||||
|
tab_rate_today.date,
|
||||||
|
tab_rate_today.rate,
|
||||||
|
distinct_on=[table_asset.id],
|
||||||
|
order_by=[table_asset.id, tab_rate_today.date.desc],
|
||||||
|
where=tab_rate_today.date <= query_date,
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query_today.join(tab_rate_1day,
|
||||||
|
# select newest date from yesterday until 3 days old
|
||||||
|
condition=(query_today.id==tab_rate_1day.asset) & \
|
||||||
|
(query_today.date > tab_rate_1day.date) & \
|
||||||
|
(query_today.date < tab_rate_1day.date + Literal(3)),
|
||||||
|
type_ = 'LEFT OUTER',
|
||||||
|
).join(tab_rate_1month,
|
||||||
|
# select newest date from 1 month ago until +3 days old
|
||||||
|
condition=(query_today.id==tab_rate_1month.asset) & \
|
||||||
|
(query_today.date > tab_rate_1month.date + Literal(30)) & \
|
||||||
|
(query_today.date < tab_rate_1month.date + Literal(33)),
|
||||||
|
type_ = 'LEFT OUTER',
|
||||||
|
).select(
|
||||||
|
query_today.id,
|
||||||
|
query_today.date,
|
||||||
|
Case(
|
||||||
|
((tab_rate_1day.rate != None) & (query_today.rate != None) & \
|
||||||
|
(tab_rate_1day.rate != Literal(0.0)),
|
||||||
|
query_today.rate * Literal(100.0) / tab_rate_1day.rate - Literal(100.0)),
|
||||||
|
else_ = None,
|
||||||
|
).as_('day1'),
|
||||||
|
Case(
|
||||||
|
((tab_rate_1month.rate != None) & (query_today.rate != None) & \
|
||||||
|
(tab_rate_1month.rate != Literal(0.0)),
|
||||||
|
query_today.rate * Literal(100.0) / tab_rate_1month.rate - Literal(100.0)),
|
||||||
|
else_ = None,
|
||||||
|
).as_('month1'),
|
||||||
|
distinct_on=[query_today.id],
|
||||||
|
order_by=[
|
||||||
|
query_today.id,
|
||||||
|
tab_rate_1day.date.desc,
|
||||||
|
tab_rate_1month.date.desc,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_percentage_change(cls, assets, names):
|
||||||
|
""" get percentage per period
|
||||||
|
"""
|
||||||
|
pool = Pool()
|
||||||
|
Asset = pool.get('investment.asset')
|
||||||
|
tab_asset = Asset.__table__()
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
|
||||||
|
tab_percent = cls.get_percentage_sql(tab_asset)
|
||||||
|
query = tab_percent.select(
|
||||||
|
tab_percent.id,
|
||||||
|
tab_percent.day1,
|
||||||
|
tab_percent.month1,
|
||||||
|
where=tab_percent.id.in_([x.id for x in assets]),
|
||||||
|
)
|
||||||
|
cursor.execute(*query)
|
||||||
|
records = cursor.fetchall()
|
||||||
|
|
||||||
|
result = {x:{y.id: None for y in assets} for x in names}
|
||||||
|
for record in records:
|
||||||
|
values = {
|
||||||
|
'change_today': record[1].quantize(Decimal('0.1')) \
|
||||||
|
if record[1] is not None else None,
|
||||||
|
'change_month': record[2].quantize(Decimal('0.1')) \
|
||||||
|
if record[2] is not None else None,
|
||||||
|
'change_3month': None,
|
||||||
|
'change_6month': None,
|
||||||
|
'change_12month': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
result[name][record[0]] = values[name]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_rate_data(cls, assets, names):
|
||||||
|
""" get date and rate of asset
|
||||||
|
"""
|
||||||
|
pool = Pool()
|
||||||
|
Asset = pool.get('investment.asset')
|
||||||
|
Rate = pool.get('investment.rate')
|
||||||
|
tab_asset = Asset.__table__()
|
||||||
|
tab_rate = Rate.__table__()
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
|
||||||
|
query = tab_asset.join(tab_rate,
|
||||||
|
condition=tab_asset.id==tab_rate.asset
|
||||||
|
).select(
|
||||||
|
tab_asset.id,
|
||||||
|
tab_rate.rate,
|
||||||
|
tab_rate.date,
|
||||||
|
distinct_on=[tab_asset.id],
|
||||||
|
order_by=[tab_asset.id, tab_rate.date.desc],
|
||||||
|
where=tab_asset.id.in_([x.id for x in assets]),
|
||||||
|
)
|
||||||
|
cursor.execute(*query)
|
||||||
|
records = cursor.fetchall()
|
||||||
|
|
||||||
|
result = {x:{y.id: None for y in assets} for x in names}
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
(id1, rate1, date1) = record
|
||||||
|
|
||||||
|
asset = Asset(id1)
|
||||||
|
exp = Decimal(Decimal(1) / 10 ** (asset.currency_digits or 4))
|
||||||
|
|
||||||
|
values = {'rate': record[1].quantize(exp), 'date': record[2]}
|
||||||
|
|
||||||
|
for name in names:
|
||||||
|
result[name][record[0]] = values[name]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@fields.depends('updtsource', 'updttime')
|
@fields.depends('updtsource', 'updttime')
|
||||||
def on_change_updtsource(self):
|
def on_change_updtsource(self):
|
||||||
""" clear time-fields
|
""" clear time-fields
|
||||||
|
@ -105,23 +278,6 @@ class Asset(ModelSQL, ModelView):
|
||||||
else :
|
else :
|
||||||
self.updttime = time(11, 30)
|
self.updttime = time(11, 30)
|
||||||
|
|
||||||
@fields.depends('id', 'currency_digits')
|
|
||||||
def on_change_with_rate(self, name=None):
|
|
||||||
""" get current rate
|
|
||||||
"""
|
|
||||||
pool = Pool()
|
|
||||||
Rate = pool.get('investment.rate')
|
|
||||||
IrDate = pool.get('ir.date')
|
|
||||||
|
|
||||||
if self.id:
|
|
||||||
rates = Rate.search([
|
|
||||||
('date', '<=', IrDate.today()),
|
|
||||||
('asset.id', '=', self.id),
|
|
||||||
], order=[('date', 'DESC')], limit=1)
|
|
||||||
if len(rates) > 0:
|
|
||||||
exp = Decimal(Decimal(1) / 10 ** (self.currency_digits or 4))
|
|
||||||
return rates[0].rate.quantize(exp)
|
|
||||||
|
|
||||||
@fields.depends('product', 'uom')
|
@fields.depends('product', 'uom')
|
||||||
def on_change_product(self):
|
def on_change_product(self):
|
||||||
""" update unit by product
|
""" update unit by product
|
||||||
|
@ -138,6 +294,13 @@ class Asset(ModelSQL, ModelView):
|
||||||
if self.currency:
|
if self.currency:
|
||||||
self.currency_digits = self.currency.digits
|
self.currency_digits = self.currency.digits
|
||||||
|
|
||||||
|
@fields.depends('product')
|
||||||
|
def on_change_with_name(self, name=None):
|
||||||
|
""" get name of product
|
||||||
|
"""
|
||||||
|
if self.product:
|
||||||
|
return self.product.name
|
||||||
|
|
||||||
@fields.depends('product')
|
@fields.depends('product')
|
||||||
def on_change_with_product_uom(self, name=None):
|
def on_change_with_product_uom(self, name=None):
|
||||||
""" get category of product-uom
|
""" get category of product-uom
|
||||||
|
@ -164,51 +327,72 @@ class Asset(ModelSQL, ModelView):
|
||||||
if self.company.currency.id != self.currency.id:
|
if self.company.currency.id != self.currency.id:
|
||||||
return self.company.currency.id
|
return self.company.currency.id
|
||||||
|
|
||||||
@fields.depends('id')
|
|
||||||
def on_change_with_updtneeded(self, name=None):
|
|
||||||
""" get state of update
|
|
||||||
"""
|
|
||||||
Asset2 = Pool().get('investment.asset')
|
|
||||||
|
|
||||||
if self.id:
|
|
||||||
if Asset2.search_count([
|
|
||||||
('updtneeded', '=', True),
|
|
||||||
('id', '=', self.id)
|
|
||||||
]) == 1:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def search_updtneeded(cls, names, clause):
|
def get_next_update_datetime_sql(cls):
|
||||||
""" search for assets to update
|
""" get sql for datetime of next planned update
|
||||||
"""
|
"""
|
||||||
pool = Pool()
|
pool = Pool()
|
||||||
Asset2 = pool.get('investment.asset')
|
Asset = pool.get('investment.asset')
|
||||||
Rate = pool.get('investment.rate')
|
Rate = pool.get('investment.rate')
|
||||||
IrDate = pool.get('ir.date')
|
tab_asset = Asset.__table__()
|
||||||
tab_asset = Asset2.__table__()
|
|
||||||
tab_rate = Rate.__table__()
|
tab_rate = Rate.__table__()
|
||||||
|
context = Transaction().context
|
||||||
|
|
||||||
|
query_date = context.get('qdate', CurrentDate() - Literal(1))
|
||||||
|
query = tab_asset.join(tab_rate,
|
||||||
|
condition=tab_asset.id == tab_rate.asset,
|
||||||
|
type_ = 'LEFT OUTER',
|
||||||
|
).select(
|
||||||
|
tab_asset.id,
|
||||||
|
((Coalesce(tab_rate.date, query_date) + Literal(1)) + \
|
||||||
|
tab_asset.updttime).as_('updttime'),
|
||||||
|
distinct_on = [tab_asset.id],
|
||||||
|
order_by = [tab_asset.id, tab_rate.date.desc],
|
||||||
|
where=(tab_asset.updtsource != None),
|
||||||
|
)
|
||||||
|
return query
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_nextupdtates(cls, assets, names):
|
||||||
|
""" get timestamp of next update
|
||||||
|
"""
|
||||||
|
Asset2 = Pool().get('investment.asset')
|
||||||
|
tab_updt = Asset2.get_next_update_datetime_sql()
|
||||||
|
cursor = Transaction().connection.cursor()
|
||||||
|
|
||||||
|
query = tab_updt.select(
|
||||||
|
tab_updt.id,
|
||||||
|
tab_updt.updttime,
|
||||||
|
where=tab_updt.id.in_([x.id for x in assets]),
|
||||||
|
)
|
||||||
|
cursor.execute(*query)
|
||||||
|
records = cursor.fetchall()
|
||||||
|
|
||||||
|
result = {x:{y.id: None for y in assets} for x in names}
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
(id1, updt) = record
|
||||||
|
r1 = {'nextupdtate': updt}
|
||||||
|
|
||||||
|
for n in names:
|
||||||
|
result[n][id1] = r1[n]
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def search_nextupdtate(cls, names, clause):
|
||||||
|
""" search for assets to update
|
||||||
|
"""
|
||||||
|
Asset2 = Pool().get('investment.asset')
|
||||||
|
tab_updt = Asset2.get_next_update_datetime_sql()
|
||||||
|
|
||||||
Operator = fields.SQL_OPERATORS[clause[1]]
|
Operator = fields.SQL_OPERATORS[clause[1]]
|
||||||
context = Transaction().context
|
context = Transaction().context
|
||||||
|
|
||||||
query_date = context.get('qdate', IrDate.today())
|
query = tab_updt.select(
|
||||||
query_time = context.get('qtime', CurrentTime())
|
tab_updt.id,
|
||||||
|
where=Operator(tab_updt.updttime, clause[2]),
|
||||||
query = tab_asset.join(tab_rate,
|
|
||||||
condition=(tab_asset.id==tab_rate.asset) & \
|
|
||||||
(tab_rate.date == query_date),
|
|
||||||
type_ = 'LEFT OUTER',
|
|
||||||
).select(tab_asset.id,
|
|
||||||
where=Operator(
|
|
||||||
Case(
|
|
||||||
((tab_rate.id == None) & \
|
|
||||||
(tab_asset.updtsource != None) & \
|
|
||||||
(tab_asset.updttime <= query_time), True),
|
|
||||||
default_ = False,
|
|
||||||
),
|
|
||||||
clause[2]),
|
|
||||||
)
|
)
|
||||||
return [('id', '=', query)]
|
return [('id', 'in', query)]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_identifier_sql(cls, tab_asset):
|
def get_identifier_sql(cls, tab_asset):
|
||||||
|
@ -294,10 +478,14 @@ class Asset(ModelSQL, ModelView):
|
||||||
def get_rec_name(self, name):
|
def get_rec_name(self, name):
|
||||||
""" record name
|
""" record name
|
||||||
"""
|
"""
|
||||||
return '%(prod)s [%(curr)s/%(unit)s]' % {
|
return '%(prod)s - %(rate)s %(curr)s/%(unit)s [%(date)s]' % {
|
||||||
'prod': getattr(self.product, 'rec_name', '-'),
|
'prod': getattr(self.product, 'rec_name', '-'),
|
||||||
'curr': getattr(self.currency, 'rec_name', '-'),
|
'curr': getattr(self.currency, 'symbol', '-'),
|
||||||
'unit': getattr(self.uom, 'rec_name', '-'),
|
'unit': getattr(self.uom, 'rec_name', '-'),
|
||||||
|
'rate': Report.format_number(self.rate, lang=None,
|
||||||
|
digits=self.currency_digits or 4) \
|
||||||
|
if self.rate is not None else '-',
|
||||||
|
'date': Report.format_date(self.date) if self.date is not None else '-',
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -314,8 +502,9 @@ class Asset(ModelSQL, ModelView):
|
||||||
Asset2 = pool.get('investment.asset')
|
Asset2 = pool.get('investment.asset')
|
||||||
OnlineSource = pool.get('investment.source')
|
OnlineSource = pool.get('investment.source')
|
||||||
|
|
||||||
|
query_time = context.get('qdatetime', CurrentTimestamp())
|
||||||
for asset in Asset2.search([
|
for asset in Asset2.search([
|
||||||
('updtneeded', '=', True),
|
('updttime', '<=', query_time),
|
||||||
]):
|
]):
|
||||||
OnlineSource.update_rate(asset)
|
OnlineSource.update_rate(asset)
|
||||||
|
|
||||||
|
|
58
locale/de.po
58
locale/de.po
|
@ -162,10 +162,22 @@ msgctxt "field:investment.asset,rates:"
|
||||||
msgid "Rates"
|
msgid "Rates"
|
||||||
msgstr "Kurse"
|
msgstr "Kurse"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,name:"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,rate:"
|
msgctxt "field:investment.asset,rate:"
|
||||||
msgid "Current Rate"
|
msgid "Current Rate"
|
||||||
msgstr "aktueller Kurs"
|
msgstr "aktueller Kurs"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,date:"
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "Datum"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,date:"
|
||||||
|
msgid "Date of current rate"
|
||||||
|
msgstr "Datum des aktuellen Kurses"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,updtsource:"
|
msgctxt "field:investment.asset,updtsource:"
|
||||||
msgid "Update Source"
|
msgid "Update Source"
|
||||||
msgstr "Kursquelle"
|
msgstr "Kursquelle"
|
||||||
|
@ -178,9 +190,49 @@ msgctxt "field:investment.asset,updttime:"
|
||||||
msgid "Time"
|
msgid "Time"
|
||||||
msgstr "Zeitpunkt"
|
msgstr "Zeitpunkt"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,updtneeded:"
|
msgctxt "field:investment.asset,nextupdtate:"
|
||||||
msgid "Course update needed"
|
msgid "Next Update"
|
||||||
msgstr "Kursaktualisierung nötig"
|
msgstr "nächste Aktualisierung"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_today:"
|
||||||
|
msgid "Previous Day"
|
||||||
|
msgstr "Vortag"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_today:"
|
||||||
|
msgid "percentage change in value compared to the previous day"
|
||||||
|
msgstr "prozentuale Wertänderung zum Vortag"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_month:"
|
||||||
|
msgid "1 Month"
|
||||||
|
msgstr "1 Monat"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_month:"
|
||||||
|
msgid "percentage change in value compared to last month"
|
||||||
|
msgstr "prozentuale Wertänderung zum letzten Monat"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_3month:"
|
||||||
|
msgid "3 Months"
|
||||||
|
msgstr "3 Monate"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_3month:"
|
||||||
|
msgid "percentage change in value during 3 months"
|
||||||
|
msgstr "Prozentuale Wertänderung während 3 Monate"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_6month:"
|
||||||
|
msgid "6 Months"
|
||||||
|
msgstr "6 Monate"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_6month:"
|
||||||
|
msgid "percentage change in value during 6 months"
|
||||||
|
msgstr "Prozentuale Wertänderung während 6 Monate"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_12month:"
|
||||||
|
msgid "1 Year"
|
||||||
|
msgstr "1 Jahr"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_12month:"
|
||||||
|
msgid "percentage change in value during 1 year"
|
||||||
|
msgstr "Prozentuale Wertänderung während 1 Jahr"
|
||||||
|
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
|
|
58
locale/en.po
58
locale/en.po
|
@ -134,10 +134,22 @@ msgctxt "field:investment.asset,rates:"
|
||||||
msgid "Rates"
|
msgid "Rates"
|
||||||
msgstr "Rates"
|
msgstr "Rates"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,name:"
|
||||||
|
msgid "Name"
|
||||||
|
msgstr "Name"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,rate:"
|
msgctxt "field:investment.asset,rate:"
|
||||||
msgid "Current Rate"
|
msgid "Current Rate"
|
||||||
msgstr "Current Rate"
|
msgstr "Current Rate"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,date:"
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "Date"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,date:"
|
||||||
|
msgid "Date of current rate"
|
||||||
|
msgstr "Date of current rate"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,updtsource:"
|
msgctxt "field:investment.asset,updtsource:"
|
||||||
msgid "Update Source"
|
msgid "Update Source"
|
||||||
msgstr "Update Source"
|
msgstr "Update Source"
|
||||||
|
@ -150,9 +162,49 @@ msgctxt "field:investment.asset,updttime:"
|
||||||
msgid "Time"
|
msgid "Time"
|
||||||
msgstr "Time"
|
msgstr "Time"
|
||||||
|
|
||||||
msgctxt "field:investment.asset,updtneeded:"
|
msgctxt "field:investment.asset,nextupdtate:"
|
||||||
msgid "Course update needed"
|
msgid "Next Update"
|
||||||
msgstr "Course update needed"
|
msgstr "Next Update"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_today:"
|
||||||
|
msgid "Previous Day"
|
||||||
|
msgstr "Previous Day"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_today:"
|
||||||
|
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_month:"
|
||||||
|
msgid "1 Month"
|
||||||
|
msgstr "1 Month"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_month:"
|
||||||
|
msgid "percentage change in value compared to last month"
|
||||||
|
msgstr "percentage change in value compared to last month"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_3month:"
|
||||||
|
msgid "3 Months"
|
||||||
|
msgstr "3 Months"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_3month:"
|
||||||
|
msgid "percentage change in value during 3 months"
|
||||||
|
msgstr "percentage change in value during 3 months"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_6month:"
|
||||||
|
msgid "6 Months"
|
||||||
|
msgstr "6 Months"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_6month:"
|
||||||
|
msgid "percentage change in value during 6 months"
|
||||||
|
msgstr "percentage change in value during 6 months"
|
||||||
|
|
||||||
|
msgctxt "field:investment.asset,change_12month:"
|
||||||
|
msgid "1 Year"
|
||||||
|
msgstr "1 Year"
|
||||||
|
|
||||||
|
msgctxt "help:investment.asset,change_12month:"
|
||||||
|
msgid "percentage change in value during 1 year"
|
||||||
|
msgstr "percentage change in value during 1 year"
|
||||||
|
|
||||||
msgctxt "model:investment.source,name:"
|
msgctxt "model:investment.source,name:"
|
||||||
msgid "Online Source"
|
msgid "Online Source"
|
||||||
|
|
|
@ -30,6 +30,7 @@ sel_rgxdatefmt = [
|
||||||
('%d.%m.%Y', 'dd.mm.yyyy'),
|
('%d.%m.%Y', 'dd.mm.yyyy'),
|
||||||
('%m/%d/%Y', 'mm/dd/yyyy'),
|
('%m/%d/%Y', 'mm/dd/yyyy'),
|
||||||
('%Y-%m-%d', 'yyyy-mm-dd'),
|
('%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', \
|
||||||
|
|
|
@ -16,6 +16,35 @@ full copyright notices and license terms. -->
|
||||||
<field name="rgxident">WKN:.* ISIN: ([A-Z,0-9]+).*</field>
|
<field name="rgxident">WKN:.* ISIN: ([A-Z,0-9]+).*</field>
|
||||||
<field name="rgxidtype">isin</field>
|
<field name="rgxidtype">isin</field>
|
||||||
</record>
|
</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\*\* (\d+\.\d+\.\d+).*\n</field>
|
||||||
|
<field name="rgxdatefmt">%d.%m.%Y</field>
|
||||||
|
<field name="rgxrate">\n\*\*Kurs\*\* (\d+,\d+) 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_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 (\d+\.\d+\.\d+) \d+:\d+:\d+.*\n</field>
|
||||||
|
<field name="rgxdatefmt">%d.%m.%Y</field>
|
||||||
|
<field name="rgxrate">\nKurs ([\d+\.]*\d+,\d+) USD\n</field>
|
||||||
|
<field name="rgxdecimal">,</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>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</tryton>
|
</tryton>
|
||||||
|
|
|
@ -8,7 +8,7 @@ from trytond.pool import Pool
|
||||||
from trytond.modules.company.tests import create_company
|
from trytond.modules.company.tests import create_company
|
||||||
from trytond.transaction import Transaction
|
from trytond.transaction import Transaction
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from datetime import time, date
|
from datetime import time, date, datetime
|
||||||
|
|
||||||
|
|
||||||
class AssetTestCase(ModuleTestCase):
|
class AssetTestCase(ModuleTestCase):
|
||||||
|
@ -61,7 +61,7 @@ class AssetTestCase(ModuleTestCase):
|
||||||
'currency_digits': 4,
|
'currency_digits': 4,
|
||||||
'uom': product.default_uom.id,
|
'uom': product.default_uom.id,
|
||||||
}])
|
}])
|
||||||
self.assertEqual(asset.rec_name, '%s [usd/%s]' % (
|
self.assertEqual(asset.rec_name, '%s - - usd/%s [-]' % (
|
||||||
product.rec_name,
|
product.rec_name,
|
||||||
asset.uom.rec_name,
|
asset.uom.rec_name,
|
||||||
))
|
))
|
||||||
|
@ -84,6 +84,81 @@ class AssetTestCase(ModuleTestCase):
|
||||||
company=company,
|
company=company,
|
||||||
product = product)
|
product = product)
|
||||||
|
|
||||||
|
@with_transaction()
|
||||||
|
def test_asset_rec_name(self):
|
||||||
|
""" create asset
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.assertEqual(asset.rec_name, 'Product 1 - - usd/Unit [-]')
|
||||||
|
|
||||||
|
Asset.write(*[
|
||||||
|
[asset],
|
||||||
|
{
|
||||||
|
'rates': [('create', [{
|
||||||
|
'date': date(2022, 5, 15),
|
||||||
|
'rate': Decimal('2.45'),
|
||||||
|
}])],
|
||||||
|
}])
|
||||||
|
self.assertEqual(asset.rec_name, 'Product 1 - 2.4500 usd/Unit [05/15/2022]')
|
||||||
|
|
||||||
|
@with_transaction()
|
||||||
|
def test_asset_percentges(self):
|
||||||
|
""" create asset, add rates, check percentages
|
||||||
|
"""
|
||||||
|
Asset = Pool().get('investment.asset')
|
||||||
|
|
||||||
|
company = self.prep_asset_company()
|
||||||
|
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/Unit [-]')
|
||||||
|
self.assertEqual(asset2.rec_name, 'Product 1 - - usd/Unit [-]')
|
||||||
|
|
||||||
|
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/Unit [05/16/2022]')
|
||||||
|
self.assertEqual(asset2.rec_name, 'Product 1 - 5.2500 usd/Unit [05/15/2022]')
|
||||||
|
self.assertEqual(asset1.change_today, Decimal('6.1'))
|
||||||
|
self.assertEqual(asset2.change_today, Decimal('-8.7'))
|
||||||
|
|
||||||
@with_transaction()
|
@with_transaction()
|
||||||
def test_asset_check_onlinesource_onoff(self):
|
def test_asset_check_onlinesource_onoff(self):
|
||||||
""" create asset, switch online-source on/off
|
""" create asset, switch online-source on/off
|
||||||
|
@ -146,28 +221,23 @@ class AssetTestCase(ModuleTestCase):
|
||||||
'updtsource': o_source.id,
|
'updtsource': o_source.id,
|
||||||
'updttime': time(10, 45),
|
'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({
|
with Transaction().set_context({
|
||||||
'qdate': date(2022, 10, 15),
|
'qdate': date(2022, 10, 14),
|
||||||
'qtime': time(10, 30),
|
|
||||||
}):
|
}):
|
||||||
# no rates exists - wait for 10:45
|
# re-read to make context work
|
||||||
self.assertEqual(asset.updtneeded, True)
|
asset2, = Asset.browse([asset.id])
|
||||||
|
|
||||||
|
self.assertEqual(asset2.updtsource.rec_name, 'Source 1')
|
||||||
|
self.assertEqual(asset2.updttime, time(10, 45))
|
||||||
|
self.assertEqual(len(asset2.rates), 0)
|
||||||
|
self.assertEqual(asset2.nextupdtate, datetime(2022, 10, 15, 10, 45))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Asset.search_count([('updtneeded', '=', True)]),
|
Asset.search_count([('nextupdtate', '<', datetime(2022, 10, 15, 10, 45))]),
|
||||||
0)
|
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(
|
self.assertEqual(
|
||||||
Asset.search_count([('updtneeded', '=', True)]),
|
Asset.search_count([('nextupdtate', '>=', datetime(2022, 10, 15, 10, 45))]),
|
||||||
1)
|
1)
|
||||||
|
|
||||||
# add rate at yesterday
|
# add rate at yesterday
|
||||||
|
@ -181,24 +251,18 @@ class AssetTestCase(ModuleTestCase):
|
||||||
}])
|
}])
|
||||||
self.assertEqual(len(asset.rates), 1)
|
self.assertEqual(len(asset.rates), 1)
|
||||||
|
|
||||||
with Transaction().set_context({
|
asset2, = Asset.browse([asset.id])
|
||||||
'qdate': date(2022, 10, 15),
|
self.assertEqual(asset.updtsource.rec_name, 'Source 1')
|
||||||
'qtime': time(10, 30),
|
self.assertEqual(asset.updttime, time(10, 45))
|
||||||
}):
|
self.assertEqual(len(asset.rates), 1)
|
||||||
# 1x rate exists - run at 10:30
|
self.assertEqual(asset.rates[0].date, date(2022, 10, 14))
|
||||||
self.assertEqual(asset.updtneeded, True)
|
self.assertEqual(asset.nextupdtate, datetime(2022, 10, 15, 10, 45))
|
||||||
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(
|
self.assertEqual(
|
||||||
Asset.search_count([('updtneeded', '=', True)]),
|
Asset.search_count([('nextupdtate', '<', datetime(2022, 10, 15, 10, 45))]),
|
||||||
|
0)
|
||||||
|
self.assertEqual(
|
||||||
|
Asset.search_count([('nextupdtate', '>=', datetime(2022, 10, 15, 10, 45))]),
|
||||||
1)
|
1)
|
||||||
|
|
||||||
# add rate at today
|
# add rate at today
|
||||||
|
@ -212,15 +276,19 @@ class AssetTestCase(ModuleTestCase):
|
||||||
}])
|
}])
|
||||||
self.assertEqual(len(asset.rates), 2)
|
self.assertEqual(len(asset.rates), 2)
|
||||||
|
|
||||||
with Transaction().set_context({
|
asset2, = Asset.browse([asset.id])
|
||||||
'qdate': date(2022, 10, 15),
|
self.assertEqual(asset2.updtsource.rec_name, 'Source 1')
|
||||||
'qtime': time(10, 47),
|
self.assertEqual(asset2.updttime, time(10, 45))
|
||||||
}):
|
self.assertEqual(len(asset2.rates), 2)
|
||||||
# 1x rate exists today - run at 10:47
|
self.assertEqual(asset2.rates[0].date, date(2022, 10, 15))
|
||||||
self.assertEqual(asset.updtneeded, True)
|
self.assertEqual(asset2.nextupdtate, datetime(2022, 10, 16, 10, 45))
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Asset.search_count([('updtneeded', '=', True)]),
|
Asset.search_count([('nextupdtate', '<', datetime(2022, 10, 15, 10, 45))]),
|
||||||
0)
|
0)
|
||||||
|
self.assertEqual(
|
||||||
|
Asset.search_count([('nextupdtate', '>=', datetime(2022, 10, 15, 10, 45))]),
|
||||||
|
1)
|
||||||
|
|
||||||
@with_transaction()
|
@with_transaction()
|
||||||
def test_asset_indentifiers(self):
|
def test_asset_indentifiers(self):
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[tryton]
|
[tryton]
|
||||||
version=6.0.0
|
version=6.0.4
|
||||||
depends:
|
depends:
|
||||||
ir
|
ir
|
||||||
res
|
res
|
||||||
|
|
|
@ -2,24 +2,32 @@
|
||||||
<!-- This file is part of the investment-module from m-ds for Tryton.
|
<!-- 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
|
The COPYRIGHT file at the top level of this repository contains the
|
||||||
full copyright notices and license terms. -->
|
full copyright notices and license terms. -->
|
||||||
<form col="4">
|
<form col="6">
|
||||||
<label name="product" />
|
<label name="product" />
|
||||||
<field name="product" />
|
<field name="product" colspan="3"/>
|
||||||
<label name="rate" />
|
<label name="rate" />
|
||||||
<field name="rate" symbol="currency"/>
|
<field name="rate" symbol="currency"/>
|
||||||
|
|
||||||
<separator id="sepunits" colspan="4" string="Currency and Units"/>
|
<label id="labdate" colspan="2" string=" "/>
|
||||||
|
<label name="nextupdtate" />
|
||||||
|
<field name="nextupdtate"/>
|
||||||
|
<label name="date" />
|
||||||
|
<field name="date"/>
|
||||||
|
|
||||||
|
<separator id="sepunits" colspan="6" string="Currency and Units"/>
|
||||||
<label name="currency" />
|
<label name="currency" />
|
||||||
<field name="currency" />
|
<field name="currency" />
|
||||||
<label name="currency_digits" />
|
<label name="currency_digits" />
|
||||||
<field name="currency_digits" />
|
<field name="currency_digits" />
|
||||||
|
<newline/>
|
||||||
|
|
||||||
<label name="uom" />
|
<label name="uom" />
|
||||||
<field name="uom" />
|
<field name="uom" />
|
||||||
<label name="product_uom" />
|
<label name="product_uom" />
|
||||||
<field name="product_uom" />
|
<field name="product_uom" />
|
||||||
|
<newline/>
|
||||||
|
|
||||||
<notebook>
|
<notebook colspan="6">
|
||||||
<page id="pgids" col="4" string="Identifiers">
|
<page id="pgids" col="4" string="Identifiers">
|
||||||
<label name="wkn" />
|
<label name="wkn" />
|
||||||
<field name="wkn" />
|
<field name="wkn" />
|
||||||
|
|
|
@ -3,11 +3,13 @@
|
||||||
The COPYRIGHT file at the top level of this repository contains the
|
The COPYRIGHT file at the top level of this repository contains the
|
||||||
full copyright notices and license terms. -->
|
full copyright notices and license terms. -->
|
||||||
<tree>
|
<tree>
|
||||||
<field name="rec_name"/>
|
<field name="name" expand="2"/>
|
||||||
<field name="isin"/>
|
<field name="isin" expand="1"/>
|
||||||
<field name="secsymb"/>
|
<field name="wkn" expand="1"/>
|
||||||
<field name="wkn"/>
|
<field name="date" expand="1"/>
|
||||||
<field name="rate"/>
|
<field name="rate" expand="1"/>
|
||||||
<field name="currency"/>
|
<field name="currency"/>
|
||||||
<field name="uom"/>
|
<field name="uom" />
|
||||||
|
<field name="change_today" />
|
||||||
|
|
||||||
</tree>
|
</tree>
|
||||||
|
|
Loading…
Reference in a new issue