Compare commits

...

14 commits

Author SHA1 Message Date
Frederik Jaeckel
bad0a9b372 Version 6.8.12 2023-12-31 11:03:58 +01:00
Frederik Jaeckel
ac2e3c31dd add sorting 2023-12-30 11:44:33 +01:00
Frederik Jaeckel
298d3079ce add columns to cashbook tree/list-view 2023-12-30 11:18:49 +01:00
Frederik Jaeckel
9b0a3691a4 add update of cashbooks on update of asset-rates 2023-12-30 10:57:17 +01:00
Frederik Jaeckel
4f87242548 cashbook: optimize for speed for checking rows 2023-12-29 23:09:00 +01:00
Frederik Jaeckel
029c4150d0 add worker-based precalculation of cashbook-values 2023-12-29 23:07:39 +01:00
Frederik Jaeckel
40dbbd1a6b remove caching 2023-12-23 10:44:55 +01:00
Frederik Jaeckel
7f49602518 Etikett ver 6.8.11 zum Änderungssatz 0dbf8e66e1dc hinzugefügt 2023-12-06 21:52:04 +01:00
Frederik Jaeckel
327337dcc8 Version 6.8.11 2023-12-06 21:51:46 +01:00
Frederik Jaeckel
9ccacfaae6 columns optional 2023-12-06 20:04:52 +01:00
Frederik Jaeckel
5b5c9a32a4 tests: formatting 2023-12-03 17:37:43 +01:00
Frederik Jaeckel
53f020b4d0 formatting 2023-12-03 17:31:42 +01:00
Frederik Jaeckel
41e04482d4 Etikett ver 6.8.10 zum Änderungssatz 41c35869a26f hinzugefügt 2023-06-08 17:15:00 +02:00
Frederik Jaeckel
49f5d2d728 Version 6.8.10 2023-06-08 17:14:42 +02:00
21 changed files with 999 additions and 397 deletions

View file

@ -23,6 +23,16 @@ You can monitor trading fees, dividends and sales profits.
Changes
=======
*6.8.12 - 31.12.2023*
- remove caching
- add worker-based precalculation of cashbook-values
- add columns to cashbook list/tree view
*6.8.11 - 06.12.2023*
- add: columns optional
*6.8.10 - 08.06.2023*
- compatibility to Tryton 6.8

View file

@ -11,6 +11,7 @@ from .line import Line
from .splitline import SplitLine
from .assetsetting import AssetSetting
from .asset import AssetRate
from .valuestore import ValueStore
def register():
@ -22,4 +23,5 @@ def register():
SplitLine,
Reconciliation,
AssetSetting,
ValueStore,
module='cashbook_investment', type_='model')

View file

@ -3,8 +3,7 @@
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import Pool, PoolMeta
CACHEKEY_ASSETRATE = 'assetrate-%s'
from trytond.pool import PoolMeta, Pool
class AssetRate(metaclass=PoolMeta):
@ -14,34 +13,52 @@ class AssetRate(metaclass=PoolMeta):
def create(cls, vlist):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
records = super(AssetRate, cls).create(vlist)
for rate in records:
MemCache.record_update(CACHEKEY_ASSETRATE % rate.asset.id, rate)
ValueStore.update_books(
ValueStore.get_book_by_books(
Cashbook.search([
('asset', 'in', [
x.asset.id for x in records])])))
return records
@classmethod
def write(cls, *args):
""" update cache-value
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
actions = iter(args)
all_rates = []
for rates, values in zip(actions, actions):
all_rates.extend(rates)
super(AssetRate, cls).write(*args)
actions = iter(args)
for rates, values in zip(actions, actions):
for rate in rates:
MemCache.record_update(CACHEKEY_ASSETRATE % rate.asset.id, rate)
ValueStore.update_books(
ValueStore.get_book_by_books(
Cashbook.search([
('asset', 'in', [
x.asset.id for x in all_rates])])))
@classmethod
def delete(cls, records):
""" set cache to None
"""
MemCache = Pool().get('cashbook.memcache')
pool = Pool()
Cashbook = pool.get('cashbook.book')
ValueStore = pool.get('cashbook.values')
books = ValueStore.get_book_by_books(Cashbook.search([
('asset', 'in', [x.asset.id for x in records])]))
for record in records:
MemCache.record_update(CACHEKEY_ASSETRATE % record.asset.id, None)
super(AssetRate, cls).delete(records)
ValueStore.update_books(books)
# end

View file

@ -5,7 +5,6 @@
from trytond.model import ModelSingleton, ModelView, ModelSQL, fields
from trytond.pool import Pool
class AssetSetting(ModelSingleton, ModelSQL, ModelView):
@ -25,13 +24,4 @@ class AssetSetting(ModelSingleton, ModelSQL, ModelView):
model_name='cashbook.book', ondelete='RESTRICT',
help='Profit and loss on sale of assets are recorded in the cash book.')
@classmethod
def write(cls, *args):
""" clear cache-values
"""
MemCache = Pool().get('cashbook.memcache')
MemCache._cashbook_value_cache.clear_all()
super(AssetSetting, cls).write(*args)
# end AssetSetting

473
book.py
View file

@ -5,37 +5,20 @@
from trytond.model import fields, SymbolMixin, Index
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval, Or, Len, Bool, If
from trytond.pyson import Eval, Or, Bool, If
from trytond.modules.cashbook.book import STATES2, DEPENDS2
from trytond.transaction import Transaction
from trytond.report import Report
from trytond.config import config
from trytond.exceptions import UserError
from trytond.i18n import gettext
from decimal import Decimal
from datetime import timedelta
from sql import Literal
from sql.functions import CurrentDate
from sql.aggregate import Sum
from sql.conditionals import Case, Coalesce
from trytond.modules.cashbook.model import sub_ids_hierarchical,\
CACHEKEY_CURRENCY, AnyInArray
from .asset import CACHEKEY_ASSETRATE
# enable/disable caching of cachekey for 'currency.rate'
if config.get(
'cashbook', 'cache_currency',
default='yes').lower() in ['yes', '1', 'true']:
ENA_CURRKEY = True
else:
ENA_CURRKEY = False
# enable/disable caching of cachekey for 'currency.rate'
if config.get(
'cashbook', 'cache_asset',
default='yes').lower() in ['yes', '1', 'true']:
ENA_ASSETKEY = True
else:
ENA_ASSETKEY = False
from trytond.modules.cashbook.model import (
sub_ids_hierarchical, AnyInArray)
class Book(SymbolMixin, metaclass=PoolMeta):
@ -49,23 +32,20 @@ class Book(SymbolMixin, metaclass=PoolMeta):
'invisible': Eval('feature', '') != 'asset',
'readonly': Or(
STATES2['readonly'],
Len(Eval('lines')) > 0,
),
}, depends=DEPENDS2+['feature', 'lines'])
Eval('has_lines', False))},
depends=DEPENDS2+['feature', 'has_lines'])
quantity_digits = fields.Integer(
string='Digits', help='Quantity Digits',
domain=[
('quantity_digits', '>=', 0),
('quantity_digits', '<=', 6),
],
('quantity_digits', '<=', 6)],
states={
'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset',
'readonly': Or(
STATES2['readonly'],
Len(Eval('lines')) > 0,
),
}, depends=DEPENDS2+['feature', 'lines'])
Eval('has_lines', False))},
depends=DEPENDS2+['feature', 'has_lines'])
asset_uomcat = fields.Function(fields.Many2One(
string='UOM Category', readonly=True,
model_name='product.uom.category',
@ -73,17 +53,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
quantity_uom = fields.Many2One(
string='UOM',
model_name='product.uom', ondelete='RESTRICT',
domain=[
('category.id', '=', Eval('asset_uomcat', -1)),
],
domain=[('category.id', '=', Eval('asset_uomcat', -1))],
states={
'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset',
'readonly': Or(
STATES2['readonly'],
Len(Eval('lines')) > 0,
),
}, depends=DEPENDS2+['feature', 'lines', 'asset_uomcat'])
STATES2['readonly'],
Eval('has_lines', False))},
depends=DEPENDS2+['feature', 'asset_uomcat', 'has_lines'])
symbol = fields.Function(fields.Char(
string='Symbol', readonly=True), 'on_change_with_symbol')
asset_symbol = fields.Function(fields.Many2One(
@ -92,26 +69,23 @@ class Book(SymbolMixin, metaclass=PoolMeta):
quantity = fields.Function(fields.Numeric(
string='Quantity', help='Quantity of assets until to date',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['quantity_digits', 'feature']),
'get_asset_quantity')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['quantity_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
quantity_all = fields.Function(fields.Numeric(
string='Total Quantity', help='Total quantity of all assets',
readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['quantity_digits', 'feature']),
'get_asset_quantity')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['quantity_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
current_value = fields.Function(fields.Numeric(
string='Value',
help='Valuation of the investment based on the current ' +
'stock market price.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('show_performance', False) == False,
}, depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity', searcher='search_asset_quantity')
current_value_ref = fields.Function(fields.Numeric(
string='Value (Ref.)',
help='Valuation of the investment based on the current stock' +
@ -119,97 +93,91 @@ class Book(SymbolMixin, metaclass=PoolMeta):
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Or(
Eval('show_performance', False) == False,
~Bool(Eval('company_currency', -1)),
),
}, depends=['currency_digits', 'show_performance', 'company_currency']),
'get_asset_quantity')
~Eval('show_performance', False),
~Bool(Eval('company_currency', -1)))},
depends=['currency_digits', 'show_performance', 'company_currency']),
'get_asset_quantity', searcher='search_asset_quantity')
# performance
diff_amount = fields.Function(fields.Numeric(
string='Difference',
help='Difference between acquisition value and current value',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('show_performance', False) == False,
}, depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity', searcher='search_asset_quantity')
diff_percent = fields.Function(fields.Numeric(
string='Percent',
help='percentage performance since acquisition',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('show_performance', False) == False,
}, depends=['currency_digits', 'show_performance']),
'get_asset_quantity')
states={'invisible': ~Eval('show_performance', False)},
depends=['currency_digits', 'show_performance']),
'get_asset_quantity', searcher='search_asset_quantity')
show_performance = fields.Function(fields.Boolean(
string='Performance', readonly=True),
'on_change_with_show_performance')
string='Performance', readonly=True), 'on_change_with_show_performance')
current_rate = fields.Function(fields.Numeric(
string='Rate',
help='Rate per unit of investment based on current stock ' +
'exchange price.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_asset_quantity')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
purchase_amount = fields.Function(fields.Numeric(
string='Purchase Amount',
help='Total purchase amount, from shares and fees.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
'get_asset_quantity')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_asset_quantity', searcher='search_asset_quantity')
# yield
yield_dividend_total = fields.Function(fields.Numeric(
string='Dividend', help='Total dividends received',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_dividend_12m = fields.Function(fields.Numeric(
string='Dividend 1y',
help='Dividends received in the last twelve months',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_fee_total = fields.Function(fields.Numeric(
string='Trade Fee', help='Total trade fees payed',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_fee_12m = fields.Function(fields.Numeric(
string='Trade Fee 1y',
help='Trade fees payed in the last twelve month',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_sales = fields.Function(fields.Numeric(
string='Sales', help='Total profit or loss on sale of shares.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_sales_12m = fields.Function(fields.Numeric(
string='Sales 1y',
help='Total profit or loss on sale of shares in the last twelve month.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']), 'get_yield_data')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_quantity')
yield_balance = fields.Function(fields.Numeric(
string='Total Yield',
help='Total income: price gain + dividends + sales gains - fees',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
'on_change_with_yield_balance')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'on_change_with_yield_balance', searcher='search_asset_quantity')
@classmethod
def __setup__(cls):
@ -228,7 +196,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
def view_attributes(cls):
return super(Book, cls).view_attributes() + [
('/tree', 'visual',
If(Eval('show_performance', False) == True,
If(Eval('show_performance', False),
If(Eval('diff_percent', 0) < 0, 'danger',
If(Eval('diff_percent', 0) > 0,
'success', '')), '')),
@ -247,6 +215,109 @@ class Book(SymbolMixin, metaclass=PoolMeta):
}
return recname
@classmethod
def work_order_assets(cls, tables, field_name):
""" get order-query
"""
Book2 = Pool().get('cashbook.book')
if 'date' in Transaction().context:
raise UserError(gettext(
'cashbook.msg_nosearch_with_date',
fname=field_name, model=Book2.__name__))
return Book2.work_order_balance(tables, field_name)
@staticmethod
def order_current_value(tables):
""" order by current_value
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'current_value')
@staticmethod
def order_purchase_amount(tables):
""" order by purchase_amount
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'purchase_amount')
@staticmethod
def order_diff_amount(tables):
""" order by diff_amount
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'diff_amount')
@staticmethod
def order_yield_balance(tables):
""" order by yield_balance
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_balance')
@staticmethod
def order_diff_percent(tables):
""" order by diff_percent
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'diff_percent')
@staticmethod
def order_quantity(tables):
""" order by quantity
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'quantity')
@staticmethod
def order_quantity_all(tables):
""" order by quantity_all
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'quantity_all')
@staticmethod
def order_yield_sales(tables):
""" order by yield_sales
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_sales')
@staticmethod
def order_yield_sales_12m(tables):
""" order by yield_sales_12m
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_sales_12m')
@staticmethod
def order_yield_dividend_total(tables):
""" order by yield_dividend_total
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_dividend_total')
@staticmethod
def order_yield_dividend_12m(tables):
""" order by yield_dividend_12m
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_dividend_12m')
@staticmethod
def order_yield_fee_total(tables):
""" order by yield_fee_total
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_fee_total')
@staticmethod
def order_yield_fee_12m(tables):
""" order by yield_fee_12m
"""
Book2 = Pool().get('cashbook.book')
return Book2.work_order_assets(tables, 'yield_fee_12m')
@fields.depends('asset', 'quantity_uom')
def on_change_asset(self):
""" get uom from asset
@ -308,17 +379,34 @@ class Book(SymbolMixin, metaclass=PoolMeta):
Sum(tab_line_gainloss.gainloss).as_('gainloss'),
tab_cur.digits.as_('currency_digits'),
group_by=[tab_book.id, tab_cur.digits],
where=where
)
where=where)
return (tab_book, query)
@classmethod
def get_yield_data(cls, cashbooks, names):
""" get yield data - stored or computed
"""
context = Transaction().context
result = {x: {y.id: Decimal('0.0') for y in cashbooks} for x in names}
# return computed values if 'date' is in context
query_date = context.get('date', None)
if query_date is not None:
return cls.get_yield_values(cashbooks, names)
for cashbook in cashbooks:
for value in cashbook.value_store:
if value.field_name in names:
result[value.field_name][cashbook.id] = value.numvalue
return result
@classmethod
def get_yield_values(cls, cashbooks, names):
""" collect yield data
"""
pool = Pool()
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor()
context = Transaction().context
result = {
@ -336,40 +424,13 @@ class Book(SymbolMixin, metaclass=PoolMeta):
).quantize(Decimal(str(1/10 ** digits)))
query_date = context.get('date', IrDate.today())
cache_keys = {
x.id: MemCache.get_key_by_record(
name='get_yield_data',
record=x,
query=[{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
'cachekey' if ENA_CURRKEY else 'disabled':
CACHEKEY_CURRENCY % x.currency.id,
}, {
'model': 'investment.rate',
'query': [('asset.id', '=', x.asset.id)],
'cachekey' if ENA_ASSETKEY else 'disabled':
CACHEKEY_ASSETRATE % x.asset.id,
} if x.asset is not None else {}],
addkeys=[query_date.isoformat()])
for x in cashbooks
}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
# results for 'total'
records_total = []
records_12m = []
if len(todo_cashbook) > 0:
if cashbooks:
(tab_book1, query_total) = cls.get_yield_data_sql()
query_total.where &= tab_book1.id.in_([x.id for x in todo_cashbook])
query_total.where &= tab_book1.id.in_([x.id for x in cashbooks])
cursor.execute(*query_total)
records_total = cursor.fetchall()
@ -378,7 +439,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
date_to=query_date,
date_from=query_date - timedelta(days=365),
)
query_12m.where &= tab_book2.id.in_([x.id for x in todo_cashbook])
query_12m.where &= tab_book2.id.in_([x.id for x in cashbooks])
cursor.execute(*query_12m)
records_12m = cursor.fetchall()
@ -397,9 +458,6 @@ class Book(SymbolMixin, metaclass=PoolMeta):
record[2], record[4])
result['yield_sales_12m'][record[0]] = quantize_val(
record[3], record[4])
# store to cache
MemCache.store_result(cashbooks, cache_keys, result, todo_cashbook)
return {x: result[x] for x in names}
@classmethod
@ -422,7 +480,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
(tab_line_yield, query_yield) = Line.get_yield_data_sql()
context = Transaction().context
query_date = context.get('qdate', CurrentDate())
query_date = context.get('date', CurrentDate())
query = tab_book.join(
tab_line,
condition=(tab_book.id == tab_line.cashbook),
@ -470,12 +528,11 @@ class Book(SymbolMixin, metaclass=PoolMeta):
tab_book.currency, tab_cur.digits, tab_asset.uom,
tab_book.quantity_uom, tab_asset.currency,
tab_balance.balance],
where=(tab_type.feature == 'asset'),
)
where=(tab_type.feature == 'asset'))
return (query, tab_book)
@classmethod
def get_asset_quantity(cls, cashbooks, names):
def get_asset_quantity_values(cls, cashbooks, names):
""" get quantities
field: quantity, quantity_all, current_value,
current_value_ref, diff_amount, diff_percent,
@ -485,10 +542,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
CBook = pool.get('cashbook.book')
Uom = pool.get('product.uom')
Currency = pool.get('currency.currency')
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor()
context = Transaction().context
(query, tab_book) = cls.get_asset_quantity_sql()
company_currency = CBook.default_currency()
@ -501,37 +555,6 @@ class Book(SymbolMixin, metaclass=PoolMeta):
'digits']
}
cache_keys = {
x.id: MemCache.get_key_by_record(
name='get_asset_quantity',
record=x,
query=[{
'model': 'cashbook.line',
'query': [('cashbook.parent', 'child_of', [x.id])],
}, {
'model': 'currency.currency.rate',
'query': [('currency.id', '=', x.currency.id)],
'cachekey' if ENA_CURRKEY else 'disabled':
CACHEKEY_CURRENCY % x.currency.id,
}, {
'model': 'investment.rate',
'query': [('asset.id', '=', x.asset.id)],
'cachekey' if ENA_ASSETKEY else 'disabled':
CACHEKEY_ASSETRATE % x.asset.id,
} if x.asset is not None else {}],
addkeys=[
str(company_currency),
str(context.get('qdate', IrDate.today()).toordinal()),
])
for x in cashbooks
}
# read from cache
(todo_cashbook, result) = MemCache.read_from_cache(
cashbooks, cache_keys, names, result)
if len(todo_cashbook) == 0:
return result
def values_from_record(rdata):
""" compute values for record
"""
@ -542,14 +565,12 @@ class Book(SymbolMixin, metaclass=PoolMeta):
uom_factor = Decimal(
Uom.compute_qty(
Uom(rdata[6]), 1.0,
Uom(rdata[7]), round=False)
)
Uom(rdata[7]), round=False))
current_value = Currency.compute(
rdata[8],
rdata[3] * rdata[1] / uom_factor,
rdata[4]
)
rdata[4])
return (record[0], {
'quantity': rdata[1],
'quantity_all': rdata[2],
@ -558,8 +579,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
rdata[8],
rdata[3] * rdata[1] / uom_factor,
company_currency
if company_currency is not None else rdata[8],
),
if company_currency is not None else rdata[8]),
'diff_amount': current_value - rdata[9],
'diff_percent': (
Decimal('100.0') * current_value /
@ -576,12 +596,18 @@ class Book(SymbolMixin, metaclass=PoolMeta):
rdata[4],
record[9],
company_currency
if company_currency is not None else rdata[4],
),
if company_currency is not None else rdata[4]),
})
ids_assetbooks = [x.id for x in cashbooks if x.btype is not None]
ids_nonebtypes = [x.id for x in cashbooks if x.btype is None]
ids_assetbooks = []
ids_nonebtypes = []
for x in cashbooks:
if x.btype is None:
if x.id not in ids_nonebtypes:
ids_nonebtypes.append(x.id)
else:
if x.id not in ids_assetbooks:
ids_assetbooks.append(x.id)
# get values of asset-cashbooks in 'cashbooks' of type=asset
if len(ids_assetbooks) > 0:
@ -630,8 +656,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
tab_book.id, tab_quantity.rate,
tab_quantity.currency, tab_quantity.currency_digits,
tab_quantity.uom, tab_quantity.quantity_uom,
tab_quantity.asset_currency, tab_book.currency],
)
tab_quantity.asset_currency, tab_book.currency])
cursor.execute(*query)
records = cursor.fetchall()
@ -650,16 +675,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
company_currency
if company_currency is not None else record[4],
values['current_value_ref'],
record[10],
)
record[10])
elif name == 'diff_amount':
value = Currency.compute(
company_currency
if company_currency is not None else record[4],
values['current_value_ref'] -
values['purchase_amount_ref'],
record[10],
)
record[10])
elif name in ['current_value_ref', 'purchase_amount_ref']:
value = values[name]
result['digits'][book_id] = record[5]
@ -679,11 +702,84 @@ class Book(SymbolMixin, metaclass=PoolMeta):
Decimal('100.0') * c_val / p_amount - Decimal('100.0')
).quantize(Decimal(str(1/10 ** digits)))
result['digits'][id_book] = None
# store to cache
MemCache.store_result(cashbooks, cache_keys, result, todo_cashbook)
return {x: result[x] for x in names}
@classmethod
def get_asset_quantity(cls, cashbooks, names):
""" get quantities - stored or computed
"""
context = Transaction().context
result = {x: {y.id: Decimal('0.0') for y in cashbooks} for x in names}
# return computed values if 'date' is in context
query_date = context.get('date', None)
if query_date is not None:
return cls.get_asset_quantity_values(cashbooks, names)
for cashbook in cashbooks:
for value in cashbook.value_store:
if value.field_name in names:
result[value.field_name][cashbook.id] = value.numvalue
return result
@classmethod
def search_asset_quantity(cls, name, clause):
""" search in stored data
"""
ValueStore = Pool().get('cashbook.values')
context = Transaction().context
query_date = context.get('date', None)
if query_date is not None:
raise UserError(gettext(
'cashbook.msg_nosearch_with_date',
fname=name, model=cls.__name__))
else:
value_query = ValueStore.search([
('field_name', '=', clause[0]),
('numvalue',) + tuple(clause[1:]),
],
query=True)
return [('value_store', 'in', value_query)]
@classmethod
def valuestore_fields(cls):
""" field to update
"""
result = super(Book, cls).valuestore_fields()
# get_asset_quantity_values
result.extend([
'quantity', 'quantity_all', 'current_value', 'current_value_ref',
'diff_amount', 'diff_percent', 'current_rate', 'purchase_amount',
'yield_fee_total', 'yield_dividend_total', 'yield_sales',
'yield_fee_12m', 'yield_dividend_12m', 'yield_sales_12m'])
return result
@classmethod
def valuestore_update_records(cls, records):
""" compute current values of records,
store to global storage
"""
ValStore = Pool().get('cashbook.values')
super(Book, cls).valuestore_update_records(records)
if records:
ValStore.update_values(
cls.get_asset_quantity_values(
records, [
'quantity', 'quantity_all', 'current_value',
'current_value_ref', 'diff_amount', 'diff_percent',
'current_rate', 'purchase_amount']))
ValStore.update_values(
cls.get_yield_values(
records, [
'yield_fee_total', 'yield_dividend_total',
'yield_sales', 'yield_fee_12m', 'yield_dividend_12m',
'yield_sales_12m']))
@fields.depends('id')
def on_change_with_show_performance(self, name=None):
""" return True if current or subordered cashbooks
@ -710,8 +806,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
"""
return '%(currency)s/%(unit)s' % {
'currency': getattr(self.currency, 'symbol', '-'),
'unit': getattr(self.quantity_uom, 'symbol', '-'),
}
'unit': getattr(self.quantity_uom, 'symbol', '-')}
@fields.depends('asset', '_parent_asset.uom')
def on_change_with_asset_uomcat(self, name=None):

184
line.py
View file

@ -14,21 +14,19 @@ from trytond.i18n import gettext
from trytond.report import Report
from trytond.transaction import Transaction
from trytond.modules.cashbook.line import STATES, DEPENDS
from trytond.modules.cashbook.const import DEF_NONE
from .mixin import SecondUomMixin
STATESQ1 = {
'invisible': And(
Eval('feature', '') != 'asset',
Eval('booktransf_feature', '') != 'asset',
),
Eval('booktransf_feature', '') != 'asset'),
'required': Or(
Eval('feature', '') == 'asset',
Eval('booktransf_feature', '') == 'asset',
),
Eval('booktransf_feature', '') == 'asset'),
'readonly': Or(
STATES['readonly'],
Eval('bookingtype', '').in_(['spin', 'spout']),
),
Eval('bookingtype', '').in_(['spin', 'spout'])),
}
DEPENDSQ1 = ['feature', 'booktransf_feature', 'quantity_digits', 'bookingtype']
DEPENDSQ1.extend(DEPENDS)
@ -38,14 +36,12 @@ STATESQ1B.update(STATESQ1)
STATESQ1B['invisible'] = And(
Eval('feature', '') != 'asset',
Eval('booktransf_feature', '') != 'asset',
Eval('splitline_has_quantity', False) == False,
)
~Eval('splitline_has_quantity', False))
STATESQ2 = {
'invisible': Eval('feature', '') != 'asset',
'required': Eval('feature', '') == 'asset',
}
'required': Eval('feature', '') == 'asset'}
DEPENDSQ2 = ['feature', 'quantity_digits', 'bookingtype']
@ -66,30 +62,26 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
states=STATESQ2, depends=DEPENDSQ2)
quantity_digits = fields.Function(fields.Integer(
string='Digits',
readonly=True, states={'invisible': True}),
string='Digits', readonly=True, states={'invisible': True}),
'on_change_with_quantity_digits')
quantity_uom = fields.Function(fields.Many2One(
string='Symbol',
readonly=True, model_name='product.uom'),
string='Symbol', readonly=True, model_name='product.uom'),
'on_change_with_quantity_uom')
asset_rate = fields.Function(fields.Numeric(
string='Rate', readonly=True,
digits=(16, If(
Eval('currency_digits', 2) > Eval('quantity_digits', 2),
Eval('currency_digits', 2), Eval('quantity_digits', 2))),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'quantity_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'quantity_digits', 'feature']),
'on_change_with_asset_rate')
quantity_balance = fields.Function(fields.Numeric(
string='Quantity',
digits=(16, Eval('quantity_digits', 4)), readonly=True,
help='Number of shares in the cashbook up to the current ' +
'row if the default sort applies.',
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['quantity_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['quantity_digits', 'feature']),
'on_change_with_quantity_balance')
splitline_has_quantity = fields.Function(fields.Boolean(
string='has quantity', readonly=True, states={'invisible': True}),
@ -101,50 +93,42 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
help='Valuation of the investment based on the current ' +
'stock market price.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'on_change_with_current_value')
diff_amount = fields.Function(fields.Numeric(
string='Difference',
help='Difference between acquisition value and current value',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
'on_change_with_diff_amount')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']), 'on_change_with_diff_amount')
diff_percent = fields.Function(fields.Numeric(
string='Percent',
help='percentage performance since acquisition',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
'on_change_with_diff_percent')
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']), 'on_change_with_diff_percent')
trade_fee = fields.Function(fields.Numeric(
string='Fee',
help='Trading fee for the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_trade_fee')
asset_dividend = fields.Function(fields.Numeric(
string='Dividend',
help='Dividend received at the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_dividend')
asset_gainloss = fields.Function(fields.Numeric(
string='Profit/Loss',
help='Profit or loss on sale on the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)),
states={
'invisible': Eval('feature', '') != 'asset',
}, depends=['currency_digits', 'feature']),
states={'invisible': Eval('feature', '') != 'asset'},
depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_gainloss')
@classmethod
@ -176,7 +160,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mvsp_counterpart,
# [MV-SP] transfer booking,
# select counterpart [1] - a split-booking
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) & \
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) &
((tab_line.reference == tab_mvsp_counterpart.id) |
(tab_line.id == tab_mvsp_counterpart.reference)) &
(tab_mvsp_counterpart.bookingtype.in_(['spin', 'spout'])),
@ -185,9 +169,9 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mv_spline,
# [MV-SP] line is linked to split-booking-line
# of counterpart [1]
condition=(tab_mv_spline.line == tab_mvsp_counterpart.id) & \
(tab_mv_spline.splittype == 'tr') & \
(tab_mv_spline.booktransf != None) & \
condition=(tab_mv_spline.line == tab_mvsp_counterpart.id) &
(tab_mv_spline.splittype == 'tr') &
(tab_mv_spline.booktransf != DEF_NONE) &
(tab_mv_spline.booktransf == gainloss_book),
type_='LEFT OUTER',
@ -195,20 +179,20 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_spmv_counterpart,
# [SP-MV] split booking, select counterpart [1]
# - a transfer-booking
condition=tab_line.bookingtype.in_(['spin', 'spout']) & \
((tab_line.reference == tab_spmv_counterpart.id) | \
(tab_line.id == tab_spmv_counterpart.reference)) & \
tab_spmv_counterpart.bookingtype.in_(['mvin', 'mvout']) & \
condition=tab_line.bookingtype.in_(['spin', 'spout']) &
((tab_line.reference == tab_spmv_counterpart.id) |
(tab_line.id == tab_spmv_counterpart.reference)) &
tab_spmv_counterpart.bookingtype.in_(['mvin', 'mvout']) &
(tab_spmv_counterpart.cashbook == gainloss_book),
type_='LEFT OUTER',
).join(
tab_mvmv_counterpart,
# [MV-MV] transfer booking
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) & \
((tab_mvmv_counterpart.reference == tab_line.id) | \
(tab_mvmv_counterpart.id == tab_line.reference)) & \
tab_mvmv_counterpart.bookingtype.in_(['mvin', 'mvout']) & \
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) &
((tab_mvmv_counterpart.reference == tab_line.id) |
(tab_mvmv_counterpart.id == tab_line.reference)) &
tab_mvmv_counterpart.bookingtype.in_(['mvin', 'mvout']) &
(tab_mvmv_counterpart.cashbook == gainloss_book),
type_='LEFT OUTER',
).select(
@ -218,17 +202,14 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
Case(
(tab_line.bookingtype == 'mvin', tab_mv_spline.amount),
(tab_line.bookingtype == 'mvout',
tab_mv_spline.amount * Decimal('-1.0')),
),
tab_mv_spline.amount * Decimal('-1.0'))),
Case(
(tab_mvsp_counterpart.cashbook == gainloss_book,
tab_line.debit - tab_line.credit),
),
tab_line.debit - tab_line.credit)),
tab_spmv_counterpart.credit - tab_spmv_counterpart.debit,
Decimal('0.0'),
) * Decimal('-1.0')).as_('gainloss'),
tab_line.cashbook,
)
tab_line.cashbook)
return (tab_line, query)
@classmethod
@ -262,17 +243,17 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
).join(
tab_inout_fee,
# [INOUT] fee, local booked
condition=(tab_inout_fee.id == tab_line.id) & \
tab_inout_fee.bookingtype.in_(['in', 'out']) & \
(tab_inout_fee.category != None) & \
condition=(tab_inout_fee.id == tab_line.id) &
tab_inout_fee.bookingtype.in_(['in', 'out']) &
(tab_inout_fee.category != DEF_NONE) &
(tab_inout_fee.category == fee_category),
type_='LEFT OUTER',
).join(
tab_inout_divi,
# [INOUT] dividend, local booked
condition=(tab_inout_divi.id == tab_line.id) & \
tab_inout_divi.bookingtype.in_(['in', 'out']) & \
(tab_inout_divi.category != None) & \
condition=(tab_inout_divi.id == tab_line.id) &
tab_inout_divi.bookingtype.in_(['in', 'out']) &
(tab_inout_divi.category != DEF_NONE) &
(tab_inout_divi.category == dividend_category),
type_='LEFT OUTER',
@ -280,46 +261,46 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mv_counterpart,
# [MV] transfer booking, select counterpart [1]
# - a split-booking
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) & \
((tab_line.reference == tab_mv_counterpart.id) | \
(tab_line.id == tab_mv_counterpart.reference)) & \
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) &
((tab_line.reference == tab_mv_counterpart.id) |
(tab_line.id == tab_mv_counterpart.reference)) &
(tab_mv_counterpart.bookingtype.in_(['spin', 'spout'])),
type_='LEFT OUTER',
).join(
tab_mv_spline_fee,
# [MV] fee-line is linked to split-booking-line
# of counterpart [1]
condition=(tab_mv_spline_fee.line == tab_mv_counterpart.id) & \
(tab_mv_spline_fee.splittype == 'cat') & \
(tab_mv_spline_fee.category != None) & \
condition=(tab_mv_spline_fee.line == tab_mv_counterpart.id) &
(tab_mv_spline_fee.splittype == 'cat') &
(tab_mv_spline_fee.category != DEF_NONE) &
(tab_mv_spline_fee.category == fee_category),
type_='LEFT OUTER',
).join(
tab_mv_spline_divi,
# [MV] dividend-line is linked to split-booking-line
# of counterpart [1]
condition=(tab_mv_spline_divi.line == tab_mv_counterpart.id) & \
(tab_mv_spline_divi.splittype == 'cat') & \
(tab_mv_spline_divi.category != None) & \
condition=(tab_mv_spline_divi.line == tab_mv_counterpart.id) &
(tab_mv_spline_divi.splittype == 'cat') &
(tab_mv_spline_divi.category != DEF_NONE) &
(tab_mv_spline_divi.category == dividend_category),
type_='LEFT OUTER',
).join(
tab_spline_fee,
# [SP] fee, split booking
condition=(tab_spline_fee.line == tab_line.id) & \
tab_line.bookingtype.in_(['spin', 'spout']) & \
(tab_spline_fee.splittype == 'cat') & \
(tab_spline_fee.category != None) & \
condition=(tab_spline_fee.line == tab_line.id) &
tab_line.bookingtype.in_(['spin', 'spout']) &
(tab_spline_fee.splittype == 'cat') &
(tab_spline_fee.category != DEF_NONE) &
(tab_spline_fee.category == fee_category),
type_='LEFT OUTER',
).join(
tab_spline_divi,
# [SP] dividend, split booking
condition=(tab_spline_divi.line == tab_line.id) & \
tab_line.bookingtype.in_(['spin', 'spout']) & \
(tab_spline_divi.splittype == 'cat') & \
(tab_spline_divi.category != None) & \
condition=(tab_spline_divi.line == tab_line.id) &
tab_line.bookingtype.in_(['spin', 'spout']) &
(tab_spline_divi.splittype == 'cat') &
(tab_spline_divi.category != DEF_NONE) &
(tab_spline_divi.category == dividend_category),
type_='LEFT OUTER',
).select(
@ -350,8 +331,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
Decimal('0.0'),
)).as_('dividend'),
tab_line.cashbook,
group_by=[tab_line.id, tab_line.cashbook],
)
group_by=[tab_line.id, tab_line.cashbook])
return (tab_line, query)
@classmethod
@ -363,8 +343,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select(
tab_query.id,
where=Operator(tab_query.fee, clause[2]),
)
where=Operator(tab_query.fee, clause[2]))
return [('id', 'in', query)]
@classmethod
@ -376,8 +355,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select(
tab_query.id,
where=Operator(tab_query.dividend, clause[2]),
)
where=Operator(tab_query.dividend, clause[2]))
return [('id', 'in', query)]
@classmethod
@ -389,8 +367,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select(
tab_query.id,
where=Operator(tab_query.gainloss, clause[2]),
)
where=Operator(tab_query.gainloss, clause[2]))
return [('id', 'in', query)]
@classmethod
@ -421,8 +398,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
line = Line2(record[0])
values = {
'trade_fee': quantize_val(record[1], line),
'asset_dividend': quantize_val(record[2], line),
}
'asset_dividend': quantize_val(record[2], line)}
for name in list(name_set):
result[name][record[0]] = values[name]
@ -452,8 +428,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
'quantity': Report.format_number(
credit - debit,
lang=None, digits=self.quantity_digits),
'uom_symbol': getattr(self.quantity_uom, 'symbol', '-'),
}
'uom_symbol': getattr(self.quantity_uom, 'symbol', '-')}
return recname
@classmethod
@ -516,8 +491,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
result = super(Line, cls).get_counterpart_values(
line,
splitline=splitline,
values=values
)
values=values)
line_uom = getattr(line.quantity_uom, 'id', None)
booktransf_uom = getattr(getattr(
@ -543,27 +517,23 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
if (asset_books == 2) and (diff_uom is True)
else splitline.quantity,
'quantity_2nd_uom': splitline.quantity
if (asset_books == 2) and (diff_uom is True) else None,
})
if (asset_books == 2) and (diff_uom is True) else None})
elif sum([1 if booktransf_uom is not None else 0,
1 if line_uom is not None else 0]) == 1:
# one of the related cashbooks only is asset-type
result.update({
'quantity': line.quantity,
'quantity_2nd_uom': None,
})
'quantity_2nd_uom': None})
elif sum([1 if booktransf_uom is not None else 0,
1 if line_uom is not None else 0]) == 2:
if line_uom == booktransf_uom:
result.update({
'quantity': line.quantity,
'quantity_2nd_uom': None,
})
'quantity_2nd_uom': None})
else:
result.update({
'quantity': line.quantity_2nd_uom,
'quantity_2nd_uom': line.quantity,
})
'quantity_2nd_uom': line.quantity})
return result
@fields.depends('amount', 'splitlines', 'quantity')
@ -728,8 +698,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
(line.quantity_debit is None):
raise UserError(gettext(
'cashbook_investment.msg_line_quantity_not_set',
linetxt=line.rec_name,
))
linetxt=line.rec_name))
# quantity and amount must with same sign
if (line.amount != Decimal('0.0')) and \
@ -739,8 +708,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
if amount_sign != quantity_sign:
raise UserError(gettext(
'cashbook_investment.msg_line_sign_mismatch',
linetxt=line.rec_name,
))
linetxt=line.rec_name))
@classmethod
def update_values_by_splitlines(cls, lines):
@ -753,7 +721,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
quantity = sum([
x.quantity or Decimal('0.0') for x in line.splitlines])
if (cnt1 > 0) and (quantity != line.quantity):
to_write.extend([[line], {'quantity': quantity, }])
to_write.extend([[line], {'quantity': quantity}])
return to_write
@classmethod

View file

@ -28,12 +28,11 @@ class SecondUomMixin(object):
digits=(16, Eval('quantity2nd_digits', 4)),
states={
'readonly': Or(
STATESQ['readonly'],
~Bool(Eval('quantity2nd'))
),
STATESQ['readonly'],
~Bool(Eval('quantity2nd'))),
'required': Bool(Eval('quantity2nd')),
'invisible': ~Bool(Eval('quantity2nd')),
}, depends=DEPENDSQ+['quantity2nd_digits', 'quantity2nd'])
'invisible': ~Bool(Eval('quantity2nd'))},
depends=DEPENDSQ+['quantity2nd_digits', 'quantity2nd'])
factor_2nd_uom = fields.Function(fields.Numeric(
string='Conversion factor',
help='Conversion factor between the units of the ' +
@ -41,12 +40,11 @@ class SecondUomMixin(object):
digits=uom_conversion_digits,
states={
'readonly': Or(
STATESQ['readonly'],
~Bool(Eval('quantity2nd'))
),
STATESQ['readonly'],
~Bool(Eval('quantity2nd'))),
'required': Bool(Eval('quantity2nd')),
'invisible': ~Bool(Eval('quantity2nd')),
}, depends=DEPENDSQ+['quantity2nd_digits', 'quantity2nd']),
'invisible': ~Bool(Eval('quantity2nd'))},
depends=DEPENDSQ+['quantity2nd_digits', 'quantity2nd']),
'on_change_with_factor_2nd_uom', setter='set_factor_2nd_uom')
quantity2nd = fields.Function(fields.Many2One(
@ -86,8 +84,7 @@ class SecondUomMixin(object):
raise UserError(gettext(
'cashbook_investment.msg_uomcat_mismatch',
cat1=from_uom.category.rec_name,
cat2=booktransf.quantity_uom.category.rec_name,
))
cat2=booktransf.quantity_uom.category.rec_name))
values['quantity_2nd_uom'] = Decimal(UOM.compute_qty(
from_uom,
@ -96,8 +93,7 @@ class SecondUomMixin(object):
round=False,
)).quantize(Decimal(
Decimal(1) /
10 ** booktransf.quantity_digits)
)
10 ** booktransf.quantity_digits))
return values
@classmethod
@ -175,8 +171,7 @@ class SecondUomMixin(object):
self.quantity_uom,
float(self.quantity),
self.booktransf.quantity_uom,
round=False,
))
round=False))
if self.quantity != Decimal('0.0'):
self.factor_2nd_uom = (
self.quantity_2nd_uom / self.quantity

View file

@ -39,15 +39,18 @@ class Reconciliation(metaclass=PoolMeta):
"""
recname = super(Reconciliation, self).get_rec_name(name)
if self.cashbook.feature == 'asset':
recname += ' | %(start_quantity)s %(uom_symbol)s - %(end_quantity)s %(uom_symbol)s' % {
'start_quantity': Report.format_number(
recname += ' '.join([
' |',
Report.format_number(
self.start_quantity or 0.0, None,
digits=self.quantity_digits),
'end_quantity': Report.format_number(
getattr(self.quantity_uom, 'symbol', '-'),
'-',
Report.format_number(
self.end_quantity or 0.0, None,
digits=self.quantity_digits),
'uom_symbol': getattr(self.quantity_uom, 'symbol', '-'),
}
getattr(self.quantity_uom, 'symbol', '-')
])
return recname
@fields.depends('cashbook', '_parent_cashbook.quantity_uom')
@ -82,8 +85,7 @@ class Reconciliation(metaclass=PoolMeta):
values = super(Reconciliation, cls).get_values_wfedit(reconciliation)
values.update({
'start_quantity': Decimal('0.0'),
'end_quantity': Decimal('0.0'),
})
'end_quantity': Decimal('0.0')})
return values
@classmethod
@ -116,8 +118,7 @@ class Reconciliation(metaclass=PoolMeta):
# add quantities of already linked lines
values['end_quantity'] += sum([
x.quantity_credit - x.quantity_debit
for x in reconciliation.lines
])
for x in reconciliation.lines])
return values

View file

@ -45,8 +45,7 @@ class SplitLine(SecondUomMixin, metaclass=PoolMeta):
'quantity': Report.format_number(
self.quantity or 0.0, None,
digits=self.quantity_digits),
'uom_symbol': self.quantity_uom.symbol,
}
'uom_symbol': self.quantity_uom.symbol}
return recname
@fields.depends(
@ -76,7 +75,6 @@ class SplitLine(SecondUomMixin, metaclass=PoolMeta):
if self.booktransf:
if self.booktransf.feature == 'asset':
return self.booktransf.quantity_digits
return 4
@classmethod

View file

@ -48,6 +48,25 @@ class CbInvTestCase(object):
self.assertEqual(book.quantity_digits, 4)
self.assertEqual(book.show_performance, True)
# run sorter
books = Book.search(
[],
order=[
('current_value', 'ASC'),
('purchase_amount', 'ASC'),
('diff_amount', 'ASC'),
('yield_balance', 'ASC'),
('diff_percent', 'ASC'),
('quantity', 'ASC'),
('quantity_all', 'ASC'),
('yield_sales', 'ASC'),
('yield_sales_12m', 'ASC'),
('yield_dividend_total', 'ASC'),
('yield_dividend_12m', 'ASC'),
('yield_fee_total', 'ASC'),
('yield_fee_12m', 'ASC'),
])
@with_transaction()
def test_assetbook_aggregated_values(self):
""" create cashbooks with hierarchy, add lines,
@ -174,6 +193,24 @@ class CbInvTestCase(object):
self.assertEqual(books[0].diff_amount, Decimal('-5.49'))
self.assertEqual(books[0].diff_percent, Decimal('-18.74'))
# searcher
self.assertEqual(Book.search_count([
('current_value', '=', Decimal('23.8'))]), 1)
self.assertEqual(Book.search_count([
('current_value_ref', '=', Decimal('23.8'))]), 1)
self.assertEqual(Book.search_count([
('diff_amount', '=', Decimal('-5.49'))]), 1)
self.assertEqual(Book.search_count([
('diff_percent', '=', Decimal('-18.74'))]), 1)
self.assertEqual(Book.search_count([
('quantity', '=', Decimal('1.0'))]), 2)
self.assertEqual(Book.search_count([
('quantity_all', '=', Decimal('1.0'))]), 2)
self.assertEqual(Book.search_count([
('current_rate', '=', Decimal('11.9'))]), 1)
self.assertEqual(Book.search_count([
('purchase_amount', '=', Decimal('15.0'))]), 2)
self.assertEqual(len(books[0].childs), 4)
self.assertEqual(
@ -327,10 +364,11 @@ class CbInvTestCase(object):
# wf --> check
Line.wfcheck(book.lines)
self.prep_valstore_run_worker()
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 5)}):
'date': date(2022, 5, 5)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('2.8')) # usd
self.assertEqual(book2.quantity, Decimal('1.453'))
@ -340,7 +378,7 @@ class CbInvTestCase(object):
self.assertEqual(book2.current_value_ref, Decimal('3.87'))
with Transaction().set_context({
'qdate': date(2022, 5, 12)}):
'date': date(2022, 5, 12)}):
book2, = Book.browse([book])
self.assertEqual(book2.quantity, Decimal('4.753'))
self.assertEqual(book2.quantity_all, Decimal('4.753'))
@ -399,6 +437,17 @@ class CbInvTestCase(object):
'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company)
usd.rates[0].date = date(2022, 5, 1)
usd.rates[0].save()
self.assertEqual(len(usd.rates), 1)
self.assertEqual(usd.rates[0].date, date(2022, 5, 1))
euro.rates[0].date = date(2022, 5, 1)
euro.rates[0].save()
self.assertEqual(len(euro.rates), 1)
self.assertEqual(euro.rates[0].date, date(2022, 5, 1))
self.assertEqual(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz')
@ -441,7 +490,7 @@ class CbInvTestCase(object):
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 1)}):
'date': date(2022, 5, 1)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0'))
@ -508,6 +557,16 @@ class CbInvTestCase(object):
'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company)
usd.rates[0].date = date(2022, 5, 1)
usd.rates[0].save()
self.assertEqual(len(usd.rates), 1)
self.assertEqual(usd.rates[0].date, date(2022, 5, 1))
euro.rates[0].date = date(2022, 5, 1)
euro.rates[0].save()
self.assertEqual(len(euro.rates), 1)
self.assertEqual(euro.rates[0].date, date(2022, 5, 1))
self.assertEqual(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz')
chf, = Currency.create([{
@ -562,14 +621,16 @@ class CbInvTestCase(object):
# check quantities at cashbook
with Transaction().set_context({
'qdate': date(2022, 5, 1)}):
'date': date(2022, 5, 1)}):
book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0'))
self.assertEqual(book2.quantity_all, Decimal('20.0'))
# usd --> chf: 1750 US$ * 0.95 / 1.05 = 1583.333 €
# 1 ounce --> 20 gram: 1583.333 CHF * 20 / 28.3495 = 1117.0097 CHF
self.assertEqual(book2.current_value, Decimal('1117.01')) # CHF
# 1 ounce --> 20 gram:
# 1583.333 CHF * 20 / 28.3495 = 1117.0097 CHF
self.assertEqual(
book2.current_value, Decimal('1117.01')) # CHF
self.assertEqual(
book2.current_value_ref, Decimal('1175.80')) # EUR
self.assertEqual(book2.diff_amount, Decimal('-132.99'))
@ -698,7 +759,8 @@ class CbInvTestCase(object):
}])
self.assertEqual(
books[1].lines[1].rec_name,
'05/01/2022|from|1.00 usd|- [Book 1 | 1.00 usd | Open | 1.000 u]')
'05/01/2022|from|1.00 usd|- [Book 1 | ' +
'1.00 usd | Open | 1.000 u]')
self.assertEqual(books[1].lines[1].quantity_digits, 3)
@with_transaction()
@ -736,7 +798,8 @@ class CbInvTestCase(object):
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.rec_name, 'Book 1 | 0.00 usd | Open | 0.0000 u')
self.assertEqual(
book.rec_name, 'Book 1 | 0.00 usd | Open | 0.0000 u')
self.assertEqual(book.btype.rec_name, 'A - Asset')
self.assertEqual(book.state, 'open')
self.assertEqual(book.feature, 'asset')
@ -846,7 +909,8 @@ class CbInvTestCase(object):
}])
self.prep_category(cattype='in')
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset = self.prep_asset_item(
@ -919,6 +983,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
@ -1009,7 +1074,8 @@ class CbInvTestCase(object):
'feature': 'asset',
}])
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset = self.prep_asset_item(
@ -1063,6 +1129,7 @@ class CbInvTestCase(object):
self.assertEqual(len(book2.lines), 0)
Line.wfcheck(list(book1.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book1.rec_name,
@ -1100,7 +1167,8 @@ class CbInvTestCase(object):
}])
self.prep_category(cattype='in')
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset = self.prep_asset_item(
@ -1160,6 +1228,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | 1.00 usd | Open')
self.assertEqual(len(book.lines), 1)
@ -1240,7 +1309,8 @@ class CbInvTestCase(object):
}])
self.prep_category(cattype='in')
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset = self.prep_asset_item(
@ -1306,6 +1376,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book.rec_name,
@ -1385,7 +1456,8 @@ class CbInvTestCase(object):
}])
self.prep_category(cattype='in')
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset1 = self.prep_asset_item(
@ -1489,6 +1561,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(
book.rec_name,
@ -1573,7 +1646,8 @@ class CbInvTestCase(object):
}])
self.prep_category(cattype='in')
category_out = self.prep_category(name='Out Category', cattype='out')
category_out = self.prep_category(
name='Out Category', cattype='out')
self.prep_party()
asset1 = self.prep_asset_item(
@ -1633,7 +1707,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex(
UserError,
r'Cannot transfer quantities between cashbooks with different unit-categories \(Time != Weight\).',
r'Cannot transfer quantities between cashbooks with ' +
r'different unit-categories \(Time != Weight\).',
Book.write,
*[
[book],
@ -1708,7 +1783,8 @@ class CbInvTestCase(object):
}])],
}])
self.assertEqual(book.rec_name, 'Book 1 | 11.00 usd | Open | 4.00 u')
self.assertEqual(
book.rec_name, 'Book 1 | 11.00 usd | Open | 4.00 u')
self.assertEqual(book.balance_all, Decimal('11.0'))
self.assertEqual(len(book.lines), 1)
@ -1723,10 +1799,12 @@ class CbInvTestCase(object):
self.assertEqual(len(book.lines[0].splitlines), 2)
self.assertEqual(book.lines[0].splitlines[0].splittype, 'cat')
self.assertEqual(book.lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(book.lines[0].splitlines[0].quantity, Decimal('1.5'))
self.assertEqual(
book.lines[0].splitlines[0].quantity, Decimal('1.5'))
self.assertEqual(book.lines[0].splitlines[1].splittype, 'cat')
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(book.lines[0].splitlines[1].quantity, Decimal('2.5'))
self.assertEqual(
book.lines[0].splitlines[1].quantity, Decimal('2.5'))
@with_transaction()
def test_assetbook_split_in_category_and_assetbook(self):
@ -1814,7 +1892,8 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].splitlines), 2)
self.assertEqual(books[0].lines[0].splitlines[0].splittype, 'cat')
self.assertEqual(books[0].lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(
books[0].lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(
books[0].lines[0].splitlines[0].quantity,
Decimal('1.5'))
@ -1823,7 +1902,8 @@ class CbInvTestCase(object):
'Cat1')
self.assertEqual(books[0].lines[0].splitlines[0].booktransf, None)
self.assertEqual(books[0].lines[0].splitlines[1].splittype, 'tr')
self.assertEqual(books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].quantity,
Decimal('2.5'))
@ -1833,11 +1913,13 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(
books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -1855,7 +1937,8 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].splitlines), 2)
self.assertEqual(books[0].lines[0].splitlines[0].splittype, 'cat')
self.assertEqual(books[0].lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(
books[0].lines[0].splitlines[0].amount, Decimal('5.0'))
self.assertEqual(
books[0].lines[0].splitlines[0].quantity,
Decimal('1.5'))
@ -1864,7 +1947,8 @@ class CbInvTestCase(object):
'Cat1')
self.assertEqual(books[0].lines[0].splitlines[0].booktransf, None)
self.assertEqual(books[0].lines[0].splitlines[1].splittype, 'tr')
self.assertEqual(books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].quantity,
Decimal('2.5'))
@ -1985,11 +2069,13 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(
books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2172,6 +2258,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2269,7 +2356,8 @@ class CbInvTestCase(object):
self.assertEqual(
asset1.rec_name,
'Aurum | 1,750.0000 usd/oz | 05/01/2022')
self.assertEqual(asset2.rec_name, 'Liquid | 10.0000 usd/l | 05/01/2022')
self.assertEqual(
asset2.rec_name, 'Liquid | 10.0000 usd/l | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
@ -2310,7 +2398,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex(
UserError,
r"Cannot transfer quantities between cashbooks with different unit-categories \(Weight != Volume\).",
r"Cannot transfer quantities between cashbooks with " +
r"different unit-categories \(Weight != Volume\).",
Book.write,
*[
[books[0]],
@ -2393,7 +2482,8 @@ class CbInvTestCase(object):
}])],
}])
self.assertEqual(book.rec_name, 'Book 1 | -11.00 usd | Open | -4.00 u')
self.assertEqual(
book.rec_name, 'Book 1 | -11.00 usd | Open | -4.00 u')
self.assertEqual(book.balance_all, Decimal('-11.0'))
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].splitline_has_quantity, False)
@ -2502,11 +2592,13 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(
books[1].rec_name, 'Book 2 | 0.00 usd | Open | 0.00 u')
self.assertEqual(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2527,7 +2619,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book 2 | 6.00 usd | Open' +
' | 2.50 u]|2.50 u')
self.assertEqual(books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount_2nd_currency,
None)
@ -2545,7 +2638,8 @@ class CbInvTestCase(object):
' | Open | -4.00 u]|2.50 u')
self.assertEqual(books[0].lines[0].reference, None)
self.assertEqual(books[1].rec_name, 'Book 2 | 6.00 usd | Open | 2.50 u')
self.assertEqual(
books[1].rec_name, 'Book 2 | 6.00 usd | Open | 2.50 u')
self.assertEqual(books[1].balance_all, Decimal('6.0'))
self.assertEqual(len(books[1].lines), 1)
self.assertEqual(
@ -2642,7 +2736,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[0].rec_name,
'Exp/Sp|5.00 usd|to category [Cat1]')
self.assertEqual(books[0].lines[0].splitlines[0].quantity, None)
self.assertEqual(books[0].lines[0].splitlines[0].quantity_2nd_uom, None)
self.assertEqual(
books[0].lines[0].splitlines[0].quantity_2nd_uom, None)
self.assertEqual(
books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book Asset | 0.00 usd | ' +
@ -2650,7 +2745,8 @@ class CbInvTestCase(object):
self.assertEqual(
books[0].lines[0].splitlines[1].quantity,
Decimal('2.5'))
self.assertEqual(books[0].lines[0].splitlines[1].quantity_2nd_uom, None)
self.assertEqual(
books[0].lines[0].splitlines[1].quantity_2nd_uom, None)
self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None)
@ -2662,6 +2758,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(books[0].rec_name, 'Book Cash | -11.00 usd | Open')
self.assertEqual(books[0].balance_all, Decimal('-11.0'))
@ -2684,7 +2781,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book Asset | 6.00 usd' +
' | Open | 2.50 u]')
self.assertEqual(books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount, Decimal('6.0'))
self.assertEqual(
books[0].lines[0].splitlines[1].amount_2nd_currency,
None)
@ -2853,6 +2951,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual(
books[0].rec_name,
@ -2993,7 +3092,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex(
UserError,
r"Cannot transfer quantities between cashbooks with different unit-categories \(Weight != Volume\).",
r"Cannot transfer quantities between cashbooks with " +
r"different unit-categories \(Weight != Volume\).",
Book.write,
*[
[books[0]],

View file

@ -10,9 +10,11 @@ from trytond.modules.investment.tests.test_module import InvestmentTestCase
from .yieldtest import YieldTestCase
from .book import CbInvTestCase
from .reconciliation import ReconTestCase
from .valuestore import ValueStoreTestCase
class CashbookInvestmentTestCase(
ValueStoreTestCase,
CashbookTestCase,
InvestmentTestCase,
CbInvTestCase,

365
tests/valuestore.py Normal file
View file

@ -0,0 +1,365 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.tests.test_tryton import with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from datetime import date
from decimal import Decimal
class ValueStoreTestCase(object):
""" test update of cashbooks on update of asset
"""
@with_transaction()
def test_valstore_update_asset_rate(self):
""" update rate of asset, should update cashbook
"""
pool = Pool()
Book = pool.get('cashbook.book')
BType = pool.get('cashbook.type')
Asset = pool.get('investment.asset')
Queue = pool.get('ir.queue')
ValStore = pool.get('cashbook.values')
company = self.prep_company()
with Transaction().set_context({'company': company.id}):
types = self.prep_type()
BType.write(*[
[types],
{
'feature': 'asset',
}])
category = self.prep_category(cattype='in')
party = self.prep_party()
asset = self.prep_asset_item(
company=company,
product=self.prep_asset_product(name='Product 1'))
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(
asset.rec_name, 'Product 1 | 2.8000 usd/u | 05/02/2022')
(usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/u')
self.assertEqual(Queue.search_count([]), 0)
book, = Book.create([{
'start_date': date(2022, 4, 1),
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': euro.id,
'number_sequ': self.prep_sequence().id,
'asset': asset.id,
'quantity_uom': asset.uom.id,
'quantity_digits': 3,
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('2.5'),
'party': party.id,
'quantity': Decimal('1.5'),
}])],
}])
# run worker
self.assertEqual(
ValStore.search_count([]),
len(Book.valuestore_fields()))
self.prep_valstore_run_worker()
self.assertEqual(book.name, 'Book 1')
self.assertEqual(book.rec_name, 'Book 1 | 2.50 € | Open | 1.500 u')
self.assertEqual(len(book.lines), 1)
self.assertEqual(
book.lines[0].rec_name,
'05/01/2022|Rev|2.50 €|Text 1 [Cat1]|1.500 u')
values = ValStore.search([
('cashbook', '=', book.id)], order=[('field_name', 'ASC')])
self.assertEqual(len(values), len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance|2.50|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_all|2.50|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_ref|2.50|2')
self.assertEqual(
values[3].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_rate|2.67|2')
self.assertEqual(
values[4].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value|4.00|2')
self.assertEqual(
values[5].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value_ref|4.00|2')
self.assertEqual(
values[6].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_amount|1.50|2')
self.assertEqual(
values[7].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_percent|60.00|2')
self.assertEqual(
values[8].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|purchase_amount|2.50|2')
self.assertEqual(
values[9].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity|1.500|3')
self.assertEqual(
values[10].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity_all|1.500|3')
self.assertEqual(
values[11].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_12m|0.00|2')
self.assertEqual(
values[12].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_total|0.00|2')
self.assertEqual(
values[13].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_12m|0.00|2')
self.assertEqual(
values[14].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_total|0.00|2')
self.assertEqual(
values[15].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales|2.50|2')
self.assertEqual(
values[16].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales_12m|0.00|2')
# add rate
Asset.write(*[
[asset],
{
'rates': [('create', [{
'date': date(2022, 5, 10),
'rate': Decimal('3.0'),
}])],
}])
self.assertEqual(
asset.rec_name, 'Product 1 | 3.0000 usd/u | 05/10/2022')
# run worker
self.prep_valstore_run_worker()
values = ValStore.search([
('cashbook', '=', book.id)], order=[('field_name', 'ASC')])
self.assertEqual(len(values), len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance|2.50|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_all|2.50|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_ref|2.50|2')
self.assertEqual(
values[3].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_rate|2.86|2')
self.assertEqual(
values[4].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value|4.29|2')
self.assertEqual(
values[5].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value_ref|4.29|2')
self.assertEqual(
values[6].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_amount|1.79|2')
self.assertEqual(
values[7].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_percent|71.60|2')
self.assertEqual(
values[8].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|purchase_amount|2.50|2')
self.assertEqual(
values[9].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity|1.500|3')
self.assertEqual(
values[10].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity_all|1.500|3')
self.assertEqual(
values[11].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_12m|0.00|2')
self.assertEqual(
values[12].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_total|0.00|2')
self.assertEqual(
values[13].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_12m|0.00|2')
self.assertEqual(
values[14].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_total|0.00|2')
self.assertEqual(
values[15].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales|2.50|2')
self.assertEqual(
values[16].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales_12m|0.00|2')
# update rate
self.assertEqual(asset.rates[0].rate, Decimal('3.0'))
self.assertEqual(asset.rates[0].date, date(2022, 5, 10))
Asset.write(*[
[asset],
{
'rates': [('write', [asset.rates[0]], {
'rate': Decimal('3.5'),
})],
}])
self.assertEqual(
asset.rec_name, 'Product 1 | 3.5000 usd/u | 05/10/2022')
# run worker
self.prep_valstore_run_worker()
values = ValStore.search([
('cashbook', '=', book.id)], order=[('field_name', 'ASC')])
self.assertEqual(len(values), len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance|2.50|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_all|2.50|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_ref|2.50|2')
self.assertEqual(
values[3].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_rate|3.33|2')
self.assertEqual(
values[4].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value|5.00|2')
self.assertEqual(
values[5].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value_ref|5.00|2')
self.assertEqual(
values[6].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_amount|2.50|2')
self.assertEqual(
values[7].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_percent|100.00|2')
self.assertEqual(
values[8].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|purchase_amount|2.50|2')
self.assertEqual(
values[9].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity|1.500|3')
self.assertEqual(
values[10].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity_all|1.500|3')
self.assertEqual(
values[11].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_12m|0.00|2')
self.assertEqual(
values[12].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_total|0.00|2')
self.assertEqual(
values[13].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_12m|0.00|2')
self.assertEqual(
values[14].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_total|0.00|2')
self.assertEqual(
values[15].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales|2.50|2')
self.assertEqual(
values[16].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales_12m|0.00|2')
# delete rate
self.assertEqual(asset.rates[0].rate, Decimal('3.5'))
self.assertEqual(asset.rates[0].date, date(2022, 5, 10))
Asset.write(*[
[asset],
{
'rates': [('delete', [asset.rates[0].id])],
}])
self.assertEqual(
asset.rec_name, 'Product 1 | 2.8000 usd/u | 05/02/2022')
# run worker
self.prep_valstore_run_worker()
values = ValStore.search([
('cashbook', '=', book.id)], order=[('field_name', 'ASC')])
self.assertEqual(len(values), len(Book.valuestore_fields()))
self.assertEqual(
values[0].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance|2.50|2')
self.assertEqual(
values[1].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_all|2.50|2')
self.assertEqual(
values[2].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|balance_ref|2.50|2')
self.assertEqual(
values[3].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_rate|2.67|2')
self.assertEqual(
values[4].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value|4.00|2')
self.assertEqual(
values[5].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|current_value_ref|4.00|2')
self.assertEqual(
values[6].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_amount|1.50|2')
self.assertEqual(
values[7].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|diff_percent|60.00|2')
self.assertEqual(
values[8].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|purchase_amount|2.50|2')
self.assertEqual(
values[9].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity|1.500|3')
self.assertEqual(
values[10].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|quantity_all|1.500|3')
self.assertEqual(
values[11].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_12m|0.00|2')
self.assertEqual(
values[12].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_dividend_total|0.00|2')
self.assertEqual(
values[13].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_12m|0.00|2')
self.assertEqual(
values[14].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_fee_total|0.00|2')
self.assertEqual(
values[15].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales|2.50|2')
self.assertEqual(
values[16].rec_name,
'[Book 1 | 2.50 € | Open | 1.500 u]|yield_sales_12m|0.00|2')
# end ValueStoreTestCase

View file

@ -152,6 +152,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -257,6 +258,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -366,6 +368,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -451,6 +454,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -532,6 +536,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -631,6 +636,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -743,6 +749,7 @@ class YieldTestCase(object):
}])
Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual(
book_asset.rec_name,
@ -879,6 +886,7 @@ class YieldTestCase(object):
lines[0].rec_name,
'05/02/2022|Exp/Sp|-23.50 usd|all out (1) [-]|-3.0000 u')
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1007,6 +1015,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1150,6 +1159,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1301,6 +1311,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 3)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,
@ -1438,6 +1449,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2)
Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual(
lines[0].rec_name,

View file

@ -1,5 +1,5 @@
[tryton]
version=6.8.10
version=6.8.12
depends:
cashbook
investment
@ -11,3 +11,4 @@ xml:
splitline.xml
assetsetting.xml
menu.xml

26
valuestore.py Normal file
View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from trytond.pool import PoolMeta
class ValueStore(metaclass=PoolMeta):
__name__ = 'cashbook.values'
@classmethod
def _maintenance_fields(cls):
""" add fields to update job
"""
result = super(ValueStore, cls)._maintenance_fields()
result.extend([
'quantity', 'current_value', 'current_value_ref',
'diff_amount', 'diff_percent', 'current_rate',
'purchase_amount', 'yield_fee_total', 'yield_dividend_total',
'yield_sales', 'yield_fee_12m', 'yield_dividend_12m',
'yield_sales_12m'])
return result
# end ValueStore

View file

@ -1 +1 @@
cashbook;6.8.28;6.8.999;mds
cashbook;6.8.32;6.8.999;mds

View file

@ -5,10 +5,19 @@ full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='balance']" position="after">
<field name="current_value" symbol="currency"/>
<field name="diff_amount" symbol="currency"/>
<field name="diff_percent"/>
<field name="quantity" symbol="quantity_uom"/>
<field name="current_value" symbol="currency" optional="0"/>
<field name="purchase_amount" symbol="currency" optional="1"/>
<field name="diff_amount" symbol="currency" optional="0"/>
<field name="yield_balance" symbol="currency" optional="1"/>
<field name="diff_percent" optional="0"/>
<field name="quantity" symbol="quantity_uom" optional="0"/>
<field name="quantity_all" symbol="quantity_uom" optional="1"/>
<field name="yield_sales" symbol="currency" optional="1"/>
<field name="yield_sales_12m" symbol="currency" optional="1"/>
<field name="yield_dividend_total" symbol="currency" optional="1"/>
<field name="yield_dividend_12m" symbol="currency" optional="1"/>
<field name="yield_fee_total" symbol="currency" optional="1"/>
<field name="yield_fee_12m" symbol="currency" optional="1"/>
</xpath>
</data>

View file

@ -5,8 +5,19 @@ full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='balance']" position="after">
<field name="diff_amount" symbol="currency"/>
<field name="diff_percent"/>
<field name="current_value" symbol="currency" optional="1"/>
<field name="purchase_amount" symbol="currency" optional="1"/>
<field name="diff_amount" symbol="currency" optional="0"/>
<field name="yield_balance" symbol="currency" optional="1"/>
<field name="diff_percent" optional="0"/>
<field name="quantity" symbol="quantity_uom" optional="1"/>
<field name="quantity_all" symbol="quantity_uom" optional="1"/>
<field name="yield_sales" symbol="currency" optional="1"/>
<field name="yield_sales_12m" symbol="currency" optional="1"/>
<field name="yield_dividend_total" symbol="currency" optional="1"/>
<field name="yield_dividend_12m" symbol="currency" optional="1"/>
<field name="yield_fee_total" symbol="currency" optional="1"/>
<field name="yield_fee_12m" symbol="currency" optional="1"/>
</xpath>
</data>

View file

@ -5,9 +5,9 @@ full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='balance']" position="after">
<field name="quantity_credit"/>
<field name="quantity_debit"/>
<field name="quantity_balance"/>
<field name="quantity_credit" optional="0"/>
<field name="quantity_debit" optional="0"/>
<field name="quantity_balance" optional="0"/>
</xpath>
</data>

View file

@ -5,7 +5,7 @@ full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='end_amount']" position="after">
<field name="start_quantity"/>
<field name="end_quantity"/>
<field name="start_quantity" optional="0"/>
<field name="end_quantity" optional="0"/>
</xpath>
</data>

View file

@ -5,8 +5,8 @@ full copyright notices and license terms. -->
<data>
<xpath expr="/tree/field[@name='amount_2nd_currency']" position="after">
<field name="quantity" symbol="quantity_uom"/>
<field name="quantity_2nd_uom" symbol="quantity2nd"/>
<field name="quantity" symbol="quantity_uom" optional="0"/>
<field name="quantity_2nd_uom" symbol="quantity2nd" optional="0"/>
</xpath>
</data>