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 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* *6.8.10 - 08.06.2023*
- compatibility to Tryton 6.8 - compatibility to Tryton 6.8

View file

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

View file

@ -3,8 +3,7 @@
# 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.
from trytond.pool import Pool, PoolMeta from trytond.pool import PoolMeta, Pool
CACHEKEY_ASSETRATE = 'assetrate-%s'
class AssetRate(metaclass=PoolMeta): class AssetRate(metaclass=PoolMeta):
@ -14,34 +13,52 @@ class AssetRate(metaclass=PoolMeta):
def create(cls, vlist): def create(cls, vlist):
""" update cache-value """ 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) 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 return records
@classmethod @classmethod
def write(cls, *args): def write(cls, *args):
""" update cache-value """ 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) super(AssetRate, cls).write(*args)
actions = iter(args) ValueStore.update_books(
for rates, values in zip(actions, actions): ValueStore.get_book_by_books(
for rate in rates: Cashbook.search([
MemCache.record_update(CACHEKEY_ASSETRATE % rate.asset.id, rate) ('asset', 'in', [
x.asset.id for x in all_rates])])))
@classmethod @classmethod
def delete(cls, records): def delete(cls, records):
""" set cache to None """ 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) super(AssetRate, cls).delete(records)
ValueStore.update_books(books)
# end # end

View file

@ -5,7 +5,6 @@
from trytond.model import ModelSingleton, ModelView, ModelSQL, fields from trytond.model import ModelSingleton, ModelView, ModelSQL, fields
from trytond.pool import Pool
class AssetSetting(ModelSingleton, ModelSQL, ModelView): class AssetSetting(ModelSingleton, ModelSQL, ModelView):
@ -25,13 +24,4 @@ class AssetSetting(ModelSingleton, ModelSQL, ModelView):
model_name='cashbook.book', ondelete='RESTRICT', model_name='cashbook.book', ondelete='RESTRICT',
help='Profit and loss on sale of assets are recorded in the cash book.') 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 # end AssetSetting

473
book.py
View file

@ -5,37 +5,20 @@
from trytond.model import fields, SymbolMixin, Index from trytond.model import fields, SymbolMixin, Index
from trytond.pool import PoolMeta, Pool 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.modules.cashbook.book import STATES2, DEPENDS2
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.report import Report 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 decimal import Decimal
from datetime import timedelta from datetime import timedelta
from sql import Literal from sql import Literal
from sql.functions import CurrentDate from sql.functions import CurrentDate
from sql.aggregate import Sum from sql.aggregate import Sum
from sql.conditionals import Case, Coalesce from sql.conditionals import Case, Coalesce
from trytond.modules.cashbook.model import sub_ids_hierarchical,\ from trytond.modules.cashbook.model import (
CACHEKEY_CURRENCY, AnyInArray sub_ids_hierarchical, 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
class Book(SymbolMixin, metaclass=PoolMeta): class Book(SymbolMixin, metaclass=PoolMeta):
@ -49,23 +32,20 @@ class Book(SymbolMixin, metaclass=PoolMeta):
'invisible': Eval('feature', '') != 'asset', 'invisible': Eval('feature', '') != 'asset',
'readonly': Or( 'readonly': Or(
STATES2['readonly'], STATES2['readonly'],
Len(Eval('lines')) > 0, Eval('has_lines', False))},
), depends=DEPENDS2+['feature', 'has_lines'])
}, depends=DEPENDS2+['feature', 'lines'])
quantity_digits = fields.Integer( quantity_digits = fields.Integer(
string='Digits', help='Quantity Digits', string='Digits', help='Quantity Digits',
domain=[ domain=[
('quantity_digits', '>=', 0), ('quantity_digits', '>=', 0),
('quantity_digits', '<=', 6), ('quantity_digits', '<=', 6)],
],
states={ states={
'required': Eval('feature', '') == 'asset', 'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset', 'invisible': Eval('feature', '') != 'asset',
'readonly': Or( 'readonly': Or(
STATES2['readonly'], STATES2['readonly'],
Len(Eval('lines')) > 0, Eval('has_lines', False))},
), depends=DEPENDS2+['feature', 'has_lines'])
}, depends=DEPENDS2+['feature', 'lines'])
asset_uomcat = fields.Function(fields.Many2One( asset_uomcat = fields.Function(fields.Many2One(
string='UOM Category', readonly=True, string='UOM Category', readonly=True,
model_name='product.uom.category', model_name='product.uom.category',
@ -73,17 +53,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
quantity_uom = fields.Many2One( quantity_uom = fields.Many2One(
string='UOM', string='UOM',
model_name='product.uom', ondelete='RESTRICT', model_name='product.uom', ondelete='RESTRICT',
domain=[ domain=[('category.id', '=', Eval('asset_uomcat', -1))],
('category.id', '=', Eval('asset_uomcat', -1)),
],
states={ states={
'required': Eval('feature', '') == 'asset', 'required': Eval('feature', '') == 'asset',
'invisible': Eval('feature', '') != 'asset', 'invisible': Eval('feature', '') != 'asset',
'readonly': Or( 'readonly': Or(
STATES2['readonly'], STATES2['readonly'],
Len(Eval('lines')) > 0, Eval('has_lines', False))},
), depends=DEPENDS2+['feature', 'asset_uomcat', 'has_lines'])
}, depends=DEPENDS2+['feature', 'lines', 'asset_uomcat'])
symbol = fields.Function(fields.Char( symbol = fields.Function(fields.Char(
string='Symbol', readonly=True), 'on_change_with_symbol') string='Symbol', readonly=True), 'on_change_with_symbol')
asset_symbol = fields.Function(fields.Many2One( asset_symbol = fields.Function(fields.Many2One(
@ -92,26 +69,23 @@ class Book(SymbolMixin, metaclass=PoolMeta):
quantity = fields.Function(fields.Numeric( quantity = fields.Function(fields.Numeric(
string='Quantity', help='Quantity of assets until to date', string='Quantity', help='Quantity of assets until to date',
readonly=True, digits=(16, Eval('quantity_digits', 4)), readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['quantity_digits', 'feature']),
}, depends=['quantity_digits', 'feature']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
quantity_all = fields.Function(fields.Numeric( quantity_all = fields.Function(fields.Numeric(
string='Total Quantity', help='Total quantity of all assets', string='Total Quantity', help='Total quantity of all assets',
readonly=True, digits=(16, Eval('quantity_digits', 4)), readonly=True, digits=(16, Eval('quantity_digits', 4)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['quantity_digits', 'feature']),
}, depends=['quantity_digits', 'feature']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
current_value = fields.Function(fields.Numeric( current_value = fields.Function(fields.Numeric(
string='Value', string='Value',
help='Valuation of the investment based on the current ' + help='Valuation of the investment based on the current ' +
'stock market price.', 'stock market price.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': ~Eval('show_performance', False)},
'invisible': Eval('show_performance', False) == False, depends=['currency_digits', 'show_performance']),
}, depends=['currency_digits', 'show_performance']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
current_value_ref = fields.Function(fields.Numeric( current_value_ref = fields.Function(fields.Numeric(
string='Value (Ref.)', string='Value (Ref.)',
help='Valuation of the investment based on the current stock' + 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)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={
'invisible': Or( 'invisible': Or(
Eval('show_performance', False) == False, ~Eval('show_performance', False),
~Bool(Eval('company_currency', -1)), ~Bool(Eval('company_currency', -1)))},
), depends=['currency_digits', 'show_performance', 'company_currency']),
}, depends=['currency_digits', 'show_performance', 'company_currency']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
# performance # performance
diff_amount = fields.Function(fields.Numeric( diff_amount = fields.Function(fields.Numeric(
string='Difference', string='Difference',
help='Difference between acquisition value and current value', help='Difference between acquisition value and current value',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': ~Eval('show_performance', False)},
'invisible': Eval('show_performance', False) == False, depends=['currency_digits', 'show_performance']),
}, depends=['currency_digits', 'show_performance']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
diff_percent = fields.Function(fields.Numeric( diff_percent = fields.Function(fields.Numeric(
string='Percent', string='Percent',
help='percentage performance since acquisition', help='percentage performance since acquisition',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': ~Eval('show_performance', False)},
'invisible': Eval('show_performance', False) == False, depends=['currency_digits', 'show_performance']),
}, depends=['currency_digits', 'show_performance']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
show_performance = fields.Function(fields.Boolean( show_performance = fields.Function(fields.Boolean(
string='Performance', readonly=True), string='Performance', readonly=True), 'on_change_with_show_performance')
'on_change_with_show_performance')
current_rate = fields.Function(fields.Numeric( current_rate = fields.Function(fields.Numeric(
string='Rate', string='Rate',
help='Rate per unit of investment based on current stock ' + help='Rate per unit of investment based on current stock ' +
'exchange price.', 'exchange price.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_asset_quantity') 'get_asset_quantity', searcher='search_asset_quantity')
purchase_amount = fields.Function(fields.Numeric( purchase_amount = fields.Function(fields.Numeric(
string='Purchase Amount', string='Purchase Amount',
help='Total purchase amount, from shares and fees.', help='Total purchase amount, from shares and fees.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_asset_quantity', searcher='search_asset_quantity')
'get_asset_quantity')
# yield # yield
yield_dividend_total = fields.Function(fields.Numeric( yield_dividend_total = fields.Function(fields.Numeric(
string='Dividend', help='Total dividends received', string='Dividend', help='Total dividends received',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_dividend_12m = fields.Function(fields.Numeric( yield_dividend_12m = fields.Function(fields.Numeric(
string='Dividend 1y', string='Dividend 1y',
help='Dividends received in the last twelve months', help='Dividends received in the last twelve months',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_fee_total = fields.Function(fields.Numeric( yield_fee_total = fields.Function(fields.Numeric(
string='Trade Fee', help='Total trade fees payed', string='Trade Fee', help='Total trade fees payed',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_fee_12m = fields.Function(fields.Numeric( yield_fee_12m = fields.Function(fields.Numeric(
string='Trade Fee 1y', string='Trade Fee 1y',
help='Trade fees payed in the last twelve month', help='Trade fees payed in the last twelve month',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_sales = fields.Function(fields.Numeric( yield_sales = fields.Function(fields.Numeric(
string='Sales', help='Total profit or loss on sale of shares.', string='Sales', help='Total profit or loss on sale of shares.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_sales_12m = fields.Function(fields.Numeric( yield_sales_12m = fields.Function(fields.Numeric(
string='Sales 1y', string='Sales 1y',
help='Total profit or loss on sale of shares in the last twelve month.', help='Total profit or loss on sale of shares in the last twelve month.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'get_yield_data') 'get_yield_data', searcher='search_asset_quantity')
yield_balance = fields.Function(fields.Numeric( yield_balance = fields.Function(fields.Numeric(
string='Total Yield', string='Total Yield',
help='Total income: price gain + dividends + sales gains - fees', help='Total income: price gain + dividends + sales gains - fees',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']), 'on_change_with_yield_balance', searcher='search_asset_quantity')
'on_change_with_yield_balance')
@classmethod @classmethod
def __setup__(cls): def __setup__(cls):
@ -228,7 +196,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
def view_attributes(cls): def view_attributes(cls):
return super(Book, cls).view_attributes() + [ return super(Book, cls).view_attributes() + [
('/tree', 'visual', ('/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, 'danger',
If(Eval('diff_percent', 0) > 0, If(Eval('diff_percent', 0) > 0,
'success', '')), '')), 'success', '')), '')),
@ -247,6 +215,109 @@ class Book(SymbolMixin, metaclass=PoolMeta):
} }
return recname 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') @fields.depends('asset', 'quantity_uom')
def on_change_asset(self): def on_change_asset(self):
""" get uom from asset """ get uom from asset
@ -308,17 +379,34 @@ class Book(SymbolMixin, metaclass=PoolMeta):
Sum(tab_line_gainloss.gainloss).as_('gainloss'), Sum(tab_line_gainloss.gainloss).as_('gainloss'),
tab_cur.digits.as_('currency_digits'), tab_cur.digits.as_('currency_digits'),
group_by=[tab_book.id, tab_cur.digits], group_by=[tab_book.id, tab_cur.digits],
where=where where=where)
)
return (tab_book, query) return (tab_book, query)
@classmethod @classmethod
def get_yield_data(cls, cashbooks, names): 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 """ collect yield data
""" """
pool = Pool() pool = Pool()
IrDate = pool.get('ir.date') IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor() cursor = Transaction().connection.cursor()
context = Transaction().context context = Transaction().context
result = { result = {
@ -336,40 +424,13 @@ class Book(SymbolMixin, metaclass=PoolMeta):
).quantize(Decimal(str(1/10 ** digits))) ).quantize(Decimal(str(1/10 ** digits)))
query_date = context.get('date', IrDate.today()) 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' # results for 'total'
records_total = [] records_total = []
records_12m = [] records_12m = []
if len(todo_cashbook) > 0: if cashbooks:
(tab_book1, query_total) = cls.get_yield_data_sql() (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) cursor.execute(*query_total)
records_total = cursor.fetchall() records_total = cursor.fetchall()
@ -378,7 +439,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
date_to=query_date, date_to=query_date,
date_from=query_date - timedelta(days=365), 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) cursor.execute(*query_12m)
records_12m = cursor.fetchall() records_12m = cursor.fetchall()
@ -397,9 +458,6 @@ class Book(SymbolMixin, metaclass=PoolMeta):
record[2], record[4]) record[2], record[4])
result['yield_sales_12m'][record[0]] = quantize_val( result['yield_sales_12m'][record[0]] = quantize_val(
record[3], record[4]) record[3], record[4])
# store to cache
MemCache.store_result(cashbooks, cache_keys, result, todo_cashbook)
return {x: result[x] for x in names} return {x: result[x] for x in names}
@classmethod @classmethod
@ -422,7 +480,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
(tab_line_yield, query_yield) = Line.get_yield_data_sql() (tab_line_yield, query_yield) = Line.get_yield_data_sql()
context = Transaction().context context = Transaction().context
query_date = context.get('qdate', CurrentDate()) query_date = context.get('date', CurrentDate())
query = tab_book.join( query = tab_book.join(
tab_line, tab_line,
condition=(tab_book.id == tab_line.cashbook), 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.currency, tab_cur.digits, tab_asset.uom,
tab_book.quantity_uom, tab_asset.currency, tab_book.quantity_uom, tab_asset.currency,
tab_balance.balance], tab_balance.balance],
where=(tab_type.feature == 'asset'), where=(tab_type.feature == 'asset'))
)
return (query, tab_book) return (query, tab_book)
@classmethod @classmethod
def get_asset_quantity(cls, cashbooks, names): def get_asset_quantity_values(cls, cashbooks, names):
""" get quantities """ get quantities
field: quantity, quantity_all, current_value, field: quantity, quantity_all, current_value,
current_value_ref, diff_amount, diff_percent, current_value_ref, diff_amount, diff_percent,
@ -485,10 +542,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
CBook = pool.get('cashbook.book') CBook = pool.get('cashbook.book')
Uom = pool.get('product.uom') Uom = pool.get('product.uom')
Currency = pool.get('currency.currency') Currency = pool.get('currency.currency')
IrDate = pool.get('ir.date')
MemCache = pool.get('cashbook.memcache')
cursor = Transaction().connection.cursor() cursor = Transaction().connection.cursor()
context = Transaction().context
(query, tab_book) = cls.get_asset_quantity_sql() (query, tab_book) = cls.get_asset_quantity_sql()
company_currency = CBook.default_currency() company_currency = CBook.default_currency()
@ -501,37 +555,6 @@ class Book(SymbolMixin, metaclass=PoolMeta):
'digits'] '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): def values_from_record(rdata):
""" compute values for record """ compute values for record
""" """
@ -542,14 +565,12 @@ class Book(SymbolMixin, metaclass=PoolMeta):
uom_factor = Decimal( uom_factor = Decimal(
Uom.compute_qty( Uom.compute_qty(
Uom(rdata[6]), 1.0, Uom(rdata[6]), 1.0,
Uom(rdata[7]), round=False) Uom(rdata[7]), round=False))
)
current_value = Currency.compute( current_value = Currency.compute(
rdata[8], rdata[8],
rdata[3] * rdata[1] / uom_factor, rdata[3] * rdata[1] / uom_factor,
rdata[4] rdata[4])
)
return (record[0], { return (record[0], {
'quantity': rdata[1], 'quantity': rdata[1],
'quantity_all': rdata[2], 'quantity_all': rdata[2],
@ -558,8 +579,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
rdata[8], rdata[8],
rdata[3] * rdata[1] / uom_factor, rdata[3] * rdata[1] / uom_factor,
company_currency 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_amount': current_value - rdata[9],
'diff_percent': ( 'diff_percent': (
Decimal('100.0') * current_value / Decimal('100.0') * current_value /
@ -576,12 +596,18 @@ class Book(SymbolMixin, metaclass=PoolMeta):
rdata[4], rdata[4],
record[9], record[9],
company_currency 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_assetbooks = []
ids_nonebtypes = [x.id for x in cashbooks if x.btype is None] 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 # get values of asset-cashbooks in 'cashbooks' of type=asset
if len(ids_assetbooks) > 0: if len(ids_assetbooks) > 0:
@ -630,8 +656,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
tab_book.id, tab_quantity.rate, tab_book.id, tab_quantity.rate,
tab_quantity.currency, tab_quantity.currency_digits, tab_quantity.currency, tab_quantity.currency_digits,
tab_quantity.uom, tab_quantity.quantity_uom, tab_quantity.uom, tab_quantity.quantity_uom,
tab_quantity.asset_currency, tab_book.currency], tab_quantity.asset_currency, tab_book.currency])
)
cursor.execute(*query) cursor.execute(*query)
records = cursor.fetchall() records = cursor.fetchall()
@ -650,16 +675,14 @@ class Book(SymbolMixin, metaclass=PoolMeta):
company_currency company_currency
if company_currency is not None else record[4], if company_currency is not None else record[4],
values['current_value_ref'], values['current_value_ref'],
record[10], record[10])
)
elif name == 'diff_amount': elif name == 'diff_amount':
value = Currency.compute( value = Currency.compute(
company_currency company_currency
if company_currency is not None else record[4], if company_currency is not None else record[4],
values['current_value_ref'] - values['current_value_ref'] -
values['purchase_amount_ref'], values['purchase_amount_ref'],
record[10], record[10])
)
elif name in ['current_value_ref', 'purchase_amount_ref']: elif name in ['current_value_ref', 'purchase_amount_ref']:
value = values[name] value = values[name]
result['digits'][book_id] = record[5] 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') Decimal('100.0') * c_val / p_amount - Decimal('100.0')
).quantize(Decimal(str(1/10 ** digits))) ).quantize(Decimal(str(1/10 ** digits)))
result['digits'][id_book] = None 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} 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') @fields.depends('id')
def on_change_with_show_performance(self, name=None): def on_change_with_show_performance(self, name=None):
""" return True if current or subordered cashbooks """ return True if current or subordered cashbooks
@ -710,8 +806,7 @@ class Book(SymbolMixin, metaclass=PoolMeta):
""" """
return '%(currency)s/%(unit)s' % { return '%(currency)s/%(unit)s' % {
'currency': getattr(self.currency, 'symbol', '-'), 'currency': getattr(self.currency, 'symbol', '-'),
'unit': getattr(self.quantity_uom, 'symbol', '-'), 'unit': getattr(self.quantity_uom, 'symbol', '-')}
}
@fields.depends('asset', '_parent_asset.uom') @fields.depends('asset', '_parent_asset.uom')
def on_change_with_asset_uomcat(self, name=None): 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.report import Report
from trytond.transaction import Transaction from trytond.transaction import Transaction
from trytond.modules.cashbook.line import STATES, DEPENDS from trytond.modules.cashbook.line import STATES, DEPENDS
from trytond.modules.cashbook.const import DEF_NONE
from .mixin import SecondUomMixin from .mixin import SecondUomMixin
STATESQ1 = { STATESQ1 = {
'invisible': And( 'invisible': And(
Eval('feature', '') != 'asset', Eval('feature', '') != 'asset',
Eval('booktransf_feature', '') != 'asset', Eval('booktransf_feature', '') != 'asset'),
),
'required': Or( 'required': Or(
Eval('feature', '') == 'asset', Eval('feature', '') == 'asset',
Eval('booktransf_feature', '') == 'asset', Eval('booktransf_feature', '') == 'asset'),
),
'readonly': Or( 'readonly': Or(
STATES['readonly'], STATES['readonly'],
Eval('bookingtype', '').in_(['spin', 'spout']), Eval('bookingtype', '').in_(['spin', 'spout'])),
),
} }
DEPENDSQ1 = ['feature', 'booktransf_feature', 'quantity_digits', 'bookingtype'] DEPENDSQ1 = ['feature', 'booktransf_feature', 'quantity_digits', 'bookingtype']
DEPENDSQ1.extend(DEPENDS) DEPENDSQ1.extend(DEPENDS)
@ -38,14 +36,12 @@ STATESQ1B.update(STATESQ1)
STATESQ1B['invisible'] = And( STATESQ1B['invisible'] = And(
Eval('feature', '') != 'asset', Eval('feature', '') != 'asset',
Eval('booktransf_feature', '') != 'asset', Eval('booktransf_feature', '') != 'asset',
Eval('splitline_has_quantity', False) == False, ~Eval('splitline_has_quantity', False))
)
STATESQ2 = { STATESQ2 = {
'invisible': Eval('feature', '') != 'asset', 'invisible': Eval('feature', '') != 'asset',
'required': Eval('feature', '') == 'asset', 'required': Eval('feature', '') == 'asset'}
}
DEPENDSQ2 = ['feature', 'quantity_digits', 'bookingtype'] DEPENDSQ2 = ['feature', 'quantity_digits', 'bookingtype']
@ -66,30 +62,26 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
states=STATESQ2, depends=DEPENDSQ2) states=STATESQ2, depends=DEPENDSQ2)
quantity_digits = fields.Function(fields.Integer( quantity_digits = fields.Function(fields.Integer(
string='Digits', string='Digits', readonly=True, states={'invisible': True}),
readonly=True, states={'invisible': True}),
'on_change_with_quantity_digits') 'on_change_with_quantity_digits')
quantity_uom = fields.Function(fields.Many2One( quantity_uom = fields.Function(fields.Many2One(
string='Symbol', string='Symbol', readonly=True, model_name='product.uom'),
readonly=True, model_name='product.uom'),
'on_change_with_quantity_uom') 'on_change_with_quantity_uom')
asset_rate = fields.Function(fields.Numeric( asset_rate = fields.Function(fields.Numeric(
string='Rate', readonly=True, string='Rate', readonly=True,
digits=(16, If( digits=(16, If(
Eval('currency_digits', 2) > Eval('quantity_digits', 2), Eval('currency_digits', 2) > Eval('quantity_digits', 2),
Eval('currency_digits', 2), Eval('quantity_digits', 2))), Eval('currency_digits', 2), Eval('quantity_digits', 2))),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'quantity_digits', 'feature']),
}, depends=['currency_digits', 'quantity_digits', 'feature']),
'on_change_with_asset_rate') 'on_change_with_asset_rate')
quantity_balance = fields.Function(fields.Numeric( quantity_balance = fields.Function(fields.Numeric(
string='Quantity', string='Quantity',
digits=(16, Eval('quantity_digits', 4)), readonly=True, digits=(16, Eval('quantity_digits', 4)), readonly=True,
help='Number of shares in the cashbook up to the current ' + help='Number of shares in the cashbook up to the current ' +
'row if the default sort applies.', 'row if the default sort applies.',
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['quantity_digits', 'feature']),
}, depends=['quantity_digits', 'feature']),
'on_change_with_quantity_balance') 'on_change_with_quantity_balance')
splitline_has_quantity = fields.Function(fields.Boolean( splitline_has_quantity = fields.Function(fields.Boolean(
string='has quantity', readonly=True, states={'invisible': True}), 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 ' + help='Valuation of the investment based on the current ' +
'stock market price.', 'stock market price.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']),
'on_change_with_current_value') 'on_change_with_current_value')
diff_amount = fields.Function(fields.Numeric( diff_amount = fields.Function(fields.Numeric(
string='Difference', string='Difference',
help='Difference between acquisition value and current value', help='Difference between acquisition value and current value',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']), 'on_change_with_diff_amount')
}, depends=['currency_digits', 'feature']),
'on_change_with_diff_amount')
diff_percent = fields.Function(fields.Numeric( diff_percent = fields.Function(fields.Numeric(
string='Percent', string='Percent',
help='percentage performance since acquisition', help='percentage performance since acquisition',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']), 'on_change_with_diff_percent')
}, depends=['currency_digits', 'feature']),
'on_change_with_diff_percent')
trade_fee = fields.Function(fields.Numeric( trade_fee = fields.Function(fields.Numeric(
string='Fee', string='Fee',
help='Trading fee for the current booking line.', help='Trading fee for the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_trade_fee') 'get_yield_data', searcher='search_trade_fee')
asset_dividend = fields.Function(fields.Numeric( asset_dividend = fields.Function(fields.Numeric(
string='Dividend', string='Dividend',
help='Dividend received at the current booking line.', help='Dividend received at the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_dividend') 'get_yield_data', searcher='search_asset_dividend')
asset_gainloss = fields.Function(fields.Numeric( asset_gainloss = fields.Function(fields.Numeric(
string='Profit/Loss', string='Profit/Loss',
help='Profit or loss on sale on the current booking line.', help='Profit or loss on sale on the current booking line.',
readonly=True, digits=(16, Eval('currency_digits', 2)), readonly=True, digits=(16, Eval('currency_digits', 2)),
states={ states={'invisible': Eval('feature', '') != 'asset'},
'invisible': Eval('feature', '') != 'asset', depends=['currency_digits', 'feature']),
}, depends=['currency_digits', 'feature']),
'get_yield_data', searcher='search_asset_gainloss') 'get_yield_data', searcher='search_asset_gainloss')
@classmethod @classmethod
@ -176,7 +160,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mvsp_counterpart, tab_mvsp_counterpart,
# [MV-SP] transfer booking, # [MV-SP] transfer booking,
# select counterpart [1] - a split-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.reference == tab_mvsp_counterpart.id) |
(tab_line.id == tab_mvsp_counterpart.reference)) & (tab_line.id == tab_mvsp_counterpart.reference)) &
(tab_mvsp_counterpart.bookingtype.in_(['spin', 'spout'])), (tab_mvsp_counterpart.bookingtype.in_(['spin', 'spout'])),
@ -185,9 +169,9 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mv_spline, tab_mv_spline,
# [MV-SP] line is linked to split-booking-line # [MV-SP] line is linked to split-booking-line
# of counterpart [1] # of counterpart [1]
condition=(tab_mv_spline.line == tab_mvsp_counterpart.id) & \ condition=(tab_mv_spline.line == tab_mvsp_counterpart.id) &
(tab_mv_spline.splittype == 'tr') & \ (tab_mv_spline.splittype == 'tr') &
(tab_mv_spline.booktransf != None) & \ (tab_mv_spline.booktransf != DEF_NONE) &
(tab_mv_spline.booktransf == gainloss_book), (tab_mv_spline.booktransf == gainloss_book),
type_='LEFT OUTER', type_='LEFT OUTER',
@ -195,20 +179,20 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_spmv_counterpart, tab_spmv_counterpart,
# [SP-MV] split booking, select counterpart [1] # [SP-MV] split booking, select counterpart [1]
# - a transfer-booking # - a transfer-booking
condition=tab_line.bookingtype.in_(['spin', 'spout']) & \ condition=tab_line.bookingtype.in_(['spin', 'spout']) &
((tab_line.reference == tab_spmv_counterpart.id) | \ ((tab_line.reference == tab_spmv_counterpart.id) |
(tab_line.id == tab_spmv_counterpart.reference)) & \ (tab_line.id == tab_spmv_counterpart.reference)) &
tab_spmv_counterpart.bookingtype.in_(['mvin', 'mvout']) & \ tab_spmv_counterpart.bookingtype.in_(['mvin', 'mvout']) &
(tab_spmv_counterpart.cashbook == gainloss_book), (tab_spmv_counterpart.cashbook == gainloss_book),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_mvmv_counterpart, tab_mvmv_counterpart,
# [MV-MV] transfer booking # [MV-MV] transfer booking
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) & \ condition=tab_line.bookingtype.in_(['mvin', 'mvout']) &
((tab_mvmv_counterpart.reference == tab_line.id) | \ ((tab_mvmv_counterpart.reference == tab_line.id) |
(tab_mvmv_counterpart.id == tab_line.reference)) & \ (tab_mvmv_counterpart.id == tab_line.reference)) &
tab_mvmv_counterpart.bookingtype.in_(['mvin', 'mvout']) & \ tab_mvmv_counterpart.bookingtype.in_(['mvin', 'mvout']) &
(tab_mvmv_counterpart.cashbook == gainloss_book), (tab_mvmv_counterpart.cashbook == gainloss_book),
type_='LEFT OUTER', type_='LEFT OUTER',
).select( ).select(
@ -218,17 +202,14 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
Case( Case(
(tab_line.bookingtype == 'mvin', tab_mv_spline.amount), (tab_line.bookingtype == 'mvin', tab_mv_spline.amount),
(tab_line.bookingtype == 'mvout', (tab_line.bookingtype == 'mvout',
tab_mv_spline.amount * Decimal('-1.0')), tab_mv_spline.amount * Decimal('-1.0'))),
),
Case( Case(
(tab_mvsp_counterpart.cashbook == gainloss_book, (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, tab_spmv_counterpart.credit - tab_spmv_counterpart.debit,
Decimal('0.0'), Decimal('0.0'),
) * Decimal('-1.0')).as_('gainloss'), ) * Decimal('-1.0')).as_('gainloss'),
tab_line.cashbook, tab_line.cashbook)
)
return (tab_line, query) return (tab_line, query)
@classmethod @classmethod
@ -262,17 +243,17 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
).join( ).join(
tab_inout_fee, tab_inout_fee,
# [INOUT] fee, local booked # [INOUT] fee, local booked
condition=(tab_inout_fee.id == tab_line.id) & \ condition=(tab_inout_fee.id == tab_line.id) &
tab_inout_fee.bookingtype.in_(['in', 'out']) & \ tab_inout_fee.bookingtype.in_(['in', 'out']) &
(tab_inout_fee.category != None) & \ (tab_inout_fee.category != DEF_NONE) &
(tab_inout_fee.category == fee_category), (tab_inout_fee.category == fee_category),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_inout_divi, tab_inout_divi,
# [INOUT] dividend, local booked # [INOUT] dividend, local booked
condition=(tab_inout_divi.id == tab_line.id) & \ condition=(tab_inout_divi.id == tab_line.id) &
tab_inout_divi.bookingtype.in_(['in', 'out']) & \ tab_inout_divi.bookingtype.in_(['in', 'out']) &
(tab_inout_divi.category != None) & \ (tab_inout_divi.category != DEF_NONE) &
(tab_inout_divi.category == dividend_category), (tab_inout_divi.category == dividend_category),
type_='LEFT OUTER', type_='LEFT OUTER',
@ -280,46 +261,46 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
tab_mv_counterpart, tab_mv_counterpart,
# [MV] transfer booking, select counterpart [1] # [MV] transfer booking, select counterpart [1]
# - a split-booking # - a split-booking
condition=tab_line.bookingtype.in_(['mvin', 'mvout']) & \ condition=tab_line.bookingtype.in_(['mvin', 'mvout']) &
((tab_line.reference == tab_mv_counterpart.id) | \ ((tab_line.reference == tab_mv_counterpart.id) |
(tab_line.id == tab_mv_counterpart.reference)) & \ (tab_line.id == tab_mv_counterpart.reference)) &
(tab_mv_counterpart.bookingtype.in_(['spin', 'spout'])), (tab_mv_counterpart.bookingtype.in_(['spin', 'spout'])),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_mv_spline_fee, tab_mv_spline_fee,
# [MV] fee-line is linked to split-booking-line # [MV] fee-line is linked to split-booking-line
# of counterpart [1] # of counterpart [1]
condition=(tab_mv_spline_fee.line == tab_mv_counterpart.id) & \ condition=(tab_mv_spline_fee.line == tab_mv_counterpart.id) &
(tab_mv_spline_fee.splittype == 'cat') & \ (tab_mv_spline_fee.splittype == 'cat') &
(tab_mv_spline_fee.category != None) & \ (tab_mv_spline_fee.category != DEF_NONE) &
(tab_mv_spline_fee.category == fee_category), (tab_mv_spline_fee.category == fee_category),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_mv_spline_divi, tab_mv_spline_divi,
# [MV] dividend-line is linked to split-booking-line # [MV] dividend-line is linked to split-booking-line
# of counterpart [1] # of counterpart [1]
condition=(tab_mv_spline_divi.line == tab_mv_counterpart.id) & \ condition=(tab_mv_spline_divi.line == tab_mv_counterpart.id) &
(tab_mv_spline_divi.splittype == 'cat') & \ (tab_mv_spline_divi.splittype == 'cat') &
(tab_mv_spline_divi.category != None) & \ (tab_mv_spline_divi.category != DEF_NONE) &
(tab_mv_spline_divi.category == dividend_category), (tab_mv_spline_divi.category == dividend_category),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_spline_fee, tab_spline_fee,
# [SP] fee, split booking # [SP] fee, split booking
condition=(tab_spline_fee.line == tab_line.id) & \ condition=(tab_spline_fee.line == tab_line.id) &
tab_line.bookingtype.in_(['spin', 'spout']) & \ tab_line.bookingtype.in_(['spin', 'spout']) &
(tab_spline_fee.splittype == 'cat') & \ (tab_spline_fee.splittype == 'cat') &
(tab_spline_fee.category != None) & \ (tab_spline_fee.category != DEF_NONE) &
(tab_spline_fee.category == fee_category), (tab_spline_fee.category == fee_category),
type_='LEFT OUTER', type_='LEFT OUTER',
).join( ).join(
tab_spline_divi, tab_spline_divi,
# [SP] dividend, split booking # [SP] dividend, split booking
condition=(tab_spline_divi.line == tab_line.id) & \ condition=(tab_spline_divi.line == tab_line.id) &
tab_line.bookingtype.in_(['spin', 'spout']) & \ tab_line.bookingtype.in_(['spin', 'spout']) &
(tab_spline_divi.splittype == 'cat') & \ (tab_spline_divi.splittype == 'cat') &
(tab_spline_divi.category != None) & \ (tab_spline_divi.category != DEF_NONE) &
(tab_spline_divi.category == dividend_category), (tab_spline_divi.category == dividend_category),
type_='LEFT OUTER', type_='LEFT OUTER',
).select( ).select(
@ -350,8 +331,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
Decimal('0.0'), Decimal('0.0'),
)).as_('dividend'), )).as_('dividend'),
tab_line.cashbook, tab_line.cashbook,
group_by=[tab_line.id, tab_line.cashbook], group_by=[tab_line.id, tab_line.cashbook])
)
return (tab_line, query) return (tab_line, query)
@classmethod @classmethod
@ -363,8 +343,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select( query = tab_query.select(
tab_query.id, tab_query.id,
where=Operator(tab_query.fee, clause[2]), where=Operator(tab_query.fee, clause[2]))
)
return [('id', 'in', query)] return [('id', 'in', query)]
@classmethod @classmethod
@ -376,8 +355,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select( query = tab_query.select(
tab_query.id, tab_query.id,
where=Operator(tab_query.dividend, clause[2]), where=Operator(tab_query.dividend, clause[2]))
)
return [('id', 'in', query)] return [('id', 'in', query)]
@classmethod @classmethod
@ -389,8 +367,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
query = tab_query.select( query = tab_query.select(
tab_query.id, tab_query.id,
where=Operator(tab_query.gainloss, clause[2]), where=Operator(tab_query.gainloss, clause[2]))
)
return [('id', 'in', query)] return [('id', 'in', query)]
@classmethod @classmethod
@ -421,8 +398,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
line = Line2(record[0]) line = Line2(record[0])
values = { values = {
'trade_fee': quantize_val(record[1], line), '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): for name in list(name_set):
result[name][record[0]] = values[name] result[name][record[0]] = values[name]
@ -452,8 +428,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
'quantity': Report.format_number( 'quantity': Report.format_number(
credit - debit, credit - debit,
lang=None, digits=self.quantity_digits), lang=None, digits=self.quantity_digits),
'uom_symbol': getattr(self.quantity_uom, 'symbol', '-'), 'uom_symbol': getattr(self.quantity_uom, 'symbol', '-')}
}
return recname return recname
@classmethod @classmethod
@ -516,8 +491,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
result = super(Line, cls).get_counterpart_values( result = super(Line, cls).get_counterpart_values(
line, line,
splitline=splitline, splitline=splitline,
values=values values=values)
)
line_uom = getattr(line.quantity_uom, 'id', None) line_uom = getattr(line.quantity_uom, 'id', None)
booktransf_uom = getattr(getattr( booktransf_uom = getattr(getattr(
@ -543,27 +517,23 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
if (asset_books == 2) and (diff_uom is True) if (asset_books == 2) and (diff_uom is True)
else splitline.quantity, else splitline.quantity,
'quantity_2nd_uom': 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, elif sum([1 if booktransf_uom is not None else 0,
1 if line_uom is not None else 0]) == 1: 1 if line_uom is not None else 0]) == 1:
# one of the related cashbooks only is asset-type # one of the related cashbooks only is asset-type
result.update({ result.update({
'quantity': line.quantity, 'quantity': line.quantity,
'quantity_2nd_uom': None, 'quantity_2nd_uom': None})
})
elif sum([1 if booktransf_uom is not None else 0, elif sum([1 if booktransf_uom is not None else 0,
1 if line_uom is not None else 0]) == 2: 1 if line_uom is not None else 0]) == 2:
if line_uom == booktransf_uom: if line_uom == booktransf_uom:
result.update({ result.update({
'quantity': line.quantity, 'quantity': line.quantity,
'quantity_2nd_uom': None, 'quantity_2nd_uom': None})
})
else: else:
result.update({ result.update({
'quantity': line.quantity_2nd_uom, 'quantity': line.quantity_2nd_uom,
'quantity_2nd_uom': line.quantity, 'quantity_2nd_uom': line.quantity})
})
return result return result
@fields.depends('amount', 'splitlines', 'quantity') @fields.depends('amount', 'splitlines', 'quantity')
@ -728,8 +698,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
(line.quantity_debit is None): (line.quantity_debit is None):
raise UserError(gettext( raise UserError(gettext(
'cashbook_investment.msg_line_quantity_not_set', 'cashbook_investment.msg_line_quantity_not_set',
linetxt=line.rec_name, linetxt=line.rec_name))
))
# quantity and amount must with same sign # quantity and amount must with same sign
if (line.amount != Decimal('0.0')) and \ if (line.amount != Decimal('0.0')) and \
@ -739,8 +708,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
if amount_sign != quantity_sign: if amount_sign != quantity_sign:
raise UserError(gettext( raise UserError(gettext(
'cashbook_investment.msg_line_sign_mismatch', 'cashbook_investment.msg_line_sign_mismatch',
linetxt=line.rec_name, linetxt=line.rec_name))
))
@classmethod @classmethod
def update_values_by_splitlines(cls, lines): def update_values_by_splitlines(cls, lines):
@ -753,7 +721,7 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
quantity = sum([ quantity = sum([
x.quantity or Decimal('0.0') for x in line.splitlines]) x.quantity or Decimal('0.0') for x in line.splitlines])
if (cnt1 > 0) and (quantity != line.quantity): if (cnt1 > 0) and (quantity != line.quantity):
to_write.extend([[line], {'quantity': quantity, }]) to_write.extend([[line], {'quantity': quantity}])
return to_write return to_write
@classmethod @classmethod

View file

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

View file

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

View file

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

View file

@ -48,6 +48,25 @@ class CbInvTestCase(object):
self.assertEqual(book.quantity_digits, 4) self.assertEqual(book.quantity_digits, 4)
self.assertEqual(book.show_performance, True) 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() @with_transaction()
def test_assetbook_aggregated_values(self): def test_assetbook_aggregated_values(self):
""" create cashbooks with hierarchy, add lines, """ 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_amount, Decimal('-5.49'))
self.assertEqual(books[0].diff_percent, Decimal('-18.74')) 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(len(books[0].childs), 4)
self.assertEqual( self.assertEqual(
@ -327,10 +364,11 @@ class CbInvTestCase(object):
# wf --> check # wf --> check
Line.wfcheck(book.lines) Line.wfcheck(book.lines)
self.prep_valstore_run_worker()
# check quantities at cashbook # check quantities at cashbook
with Transaction().set_context({ with Transaction().set_context({
'qdate': date(2022, 5, 5)}): 'date': date(2022, 5, 5)}):
book2, = Book.browse([book]) book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('2.8')) # usd self.assertEqual(book.asset.rate, Decimal('2.8')) # usd
self.assertEqual(book2.quantity, Decimal('1.453')) self.assertEqual(book2.quantity, Decimal('1.453'))
@ -340,7 +378,7 @@ class CbInvTestCase(object):
self.assertEqual(book2.current_value_ref, Decimal('3.87')) self.assertEqual(book2.current_value_ref, Decimal('3.87'))
with Transaction().set_context({ with Transaction().set_context({
'qdate': date(2022, 5, 12)}): 'date': date(2022, 5, 12)}):
book2, = Book.browse([book]) book2, = Book.browse([book])
self.assertEqual(book2.quantity, Decimal('4.753')) self.assertEqual(book2.quantity, Decimal('4.753'))
self.assertEqual(book2.quantity_all, 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') 'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company) (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(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz') self.assertEqual(asset.symbol, 'usd/oz')
@ -441,7 +490,7 @@ class CbInvTestCase(object):
# check quantities at cashbook # check quantities at cashbook
with Transaction().set_context({ with Transaction().set_context({
'qdate': date(2022, 5, 1)}): 'date': date(2022, 5, 1)}):
book2, = Book.browse([book]) book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0')) self.assertEqual(book2.quantity, Decimal('20.0'))
@ -508,6 +557,16 @@ class CbInvTestCase(object):
'Aurum | 1,750.0000 usd/oz | 05/01/2022') 'Aurum | 1,750.0000 usd/oz | 05/01/2022')
(usd, euro) = self.prep_2nd_currency(company) (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(company.currency.rec_name, 'Euro')
self.assertEqual(asset.symbol, 'usd/oz') self.assertEqual(asset.symbol, 'usd/oz')
chf, = Currency.create([{ chf, = Currency.create([{
@ -562,14 +621,16 @@ class CbInvTestCase(object):
# check quantities at cashbook # check quantities at cashbook
with Transaction().set_context({ with Transaction().set_context({
'qdate': date(2022, 5, 1)}): 'date': date(2022, 5, 1)}):
book2, = Book.browse([book]) book2, = Book.browse([book])
self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd
self.assertEqual(book2.quantity, Decimal('20.0')) self.assertEqual(book2.quantity, Decimal('20.0'))
self.assertEqual(book2.quantity_all, Decimal('20.0')) self.assertEqual(book2.quantity_all, Decimal('20.0'))
# usd --> chf: 1750 US$ * 0.95 / 1.05 = 1583.333 € # usd --> chf: 1750 US$ * 0.95 / 1.05 = 1583.333 €
# 1 ounce --> 20 gram: 1583.333 CHF * 20 / 28.3495 = 1117.0097 CHF # 1 ounce --> 20 gram:
self.assertEqual(book2.current_value, Decimal('1117.01')) # CHF # 1583.333 CHF * 20 / 28.3495 = 1117.0097 CHF
self.assertEqual(
book2.current_value, Decimal('1117.01')) # CHF
self.assertEqual( self.assertEqual(
book2.current_value_ref, Decimal('1175.80')) # EUR book2.current_value_ref, Decimal('1175.80')) # EUR
self.assertEqual(book2.diff_amount, Decimal('-132.99')) self.assertEqual(book2.diff_amount, Decimal('-132.99'))
@ -698,7 +759,8 @@ class CbInvTestCase(object):
}]) }])
self.assertEqual( self.assertEqual(
books[1].lines[1].rec_name, 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) self.assertEqual(books[1].lines[1].quantity_digits, 3)
@with_transaction() @with_transaction()
@ -736,7 +798,8 @@ class CbInvTestCase(object):
}]) }])
self.assertEqual(book.name, 'Book 1') 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.btype.rec_name, 'A - Asset')
self.assertEqual(book.state, 'open') self.assertEqual(book.state, 'open')
self.assertEqual(book.feature, 'asset') self.assertEqual(book.feature, 'asset')
@ -846,7 +909,8 @@ class CbInvTestCase(object):
}]) }])
self.prep_category(cattype='in') 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() self.prep_party()
asset = self.prep_asset_item( asset = self.prep_asset_item(
@ -919,6 +983,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart # set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines)) Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open') self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open')
self.assertEqual(len(book.lines), 1) self.assertEqual(len(book.lines), 1)
@ -1009,7 +1074,8 @@ class CbInvTestCase(object):
'feature': 'asset', '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() self.prep_party()
asset = self.prep_asset_item( asset = self.prep_asset_item(
@ -1063,6 +1129,7 @@ class CbInvTestCase(object):
self.assertEqual(len(book2.lines), 0) self.assertEqual(len(book2.lines), 0)
Line.wfcheck(list(book1.lines)) Line.wfcheck(list(book1.lines))
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book1.rec_name, book1.rec_name,
@ -1100,7 +1167,8 @@ class CbInvTestCase(object):
}]) }])
self.prep_category(cattype='in') 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() self.prep_party()
asset = self.prep_asset_item( asset = self.prep_asset_item(
@ -1160,6 +1228,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart # set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines)) Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual(book.rec_name, 'Book 1 | 1.00 usd | Open') self.assertEqual(book.rec_name, 'Book 1 | 1.00 usd | Open')
self.assertEqual(len(book.lines), 1) self.assertEqual(len(book.lines), 1)
@ -1240,7 +1309,8 @@ class CbInvTestCase(object):
}]) }])
self.prep_category(cattype='in') 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() self.prep_party()
asset = self.prep_asset_item( asset = self.prep_asset_item(
@ -1306,6 +1376,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart # set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines)) Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book.rec_name, book.rec_name,
@ -1385,7 +1456,8 @@ class CbInvTestCase(object):
}]) }])
self.prep_category(cattype='in') 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() self.prep_party()
asset1 = self.prep_asset_item( asset1 = self.prep_asset_item(
@ -1489,6 +1561,7 @@ class CbInvTestCase(object):
# set line to 'checked', this creates the counterpart # set line to 'checked', this creates the counterpart
Line.wfcheck(list(book.lines)) Line.wfcheck(list(book.lines))
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book.rec_name, book.rec_name,
@ -1573,7 +1646,8 @@ class CbInvTestCase(object):
}]) }])
self.prep_category(cattype='in') 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() self.prep_party()
asset1 = self.prep_asset_item( asset1 = self.prep_asset_item(
@ -1633,7 +1707,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex( self.assertRaisesRegex(
UserError, 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.write,
*[ *[
[book], [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(book.balance_all, Decimal('11.0'))
self.assertEqual(len(book.lines), 1) self.assertEqual(len(book.lines), 1)
@ -1723,10 +1799,12 @@ class CbInvTestCase(object):
self.assertEqual(len(book.lines[0].splitlines), 2) self.assertEqual(len(book.lines[0].splitlines), 2)
self.assertEqual(book.lines[0].splitlines[0].splittype, 'cat') 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].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].splittype, 'cat')
self.assertEqual(book.lines[0].splitlines[1].amount, Decimal('6.0')) 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() @with_transaction()
def test_assetbook_split_in_category_and_assetbook(self): 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(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].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( self.assertEqual(
books[0].lines[0].splitlines[0].quantity, books[0].lines[0].splitlines[0].quantity,
Decimal('1.5')) Decimal('1.5'))
@ -1823,7 +1902,8 @@ class CbInvTestCase(object):
'Cat1') 'Cat1')
self.assertEqual(books[0].lines[0].splitlines[0].booktransf, None) 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].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( self.assertEqual(
books[0].lines[0].splitlines[1].quantity, books[0].lines[0].splitlines[1].quantity,
Decimal('2.5')) Decimal('2.5'))
@ -1833,11 +1913,13 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].references), 0) self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None) 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(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]]) Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
books[0].rec_name, books[0].rec_name,
@ -1855,7 +1937,8 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].splitlines), 2) 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].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( self.assertEqual(
books[0].lines[0].splitlines[0].quantity, books[0].lines[0].splitlines[0].quantity,
Decimal('1.5')) Decimal('1.5'))
@ -1864,7 +1947,8 @@ class CbInvTestCase(object):
'Cat1') 'Cat1')
self.assertEqual(books[0].lines[0].splitlines[0].booktransf, None) 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].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( self.assertEqual(
books[0].lines[0].splitlines[1].quantity, books[0].lines[0].splitlines[1].quantity,
Decimal('2.5')) Decimal('2.5'))
@ -1985,11 +2069,13 @@ class CbInvTestCase(object):
self.assertEqual(len(books[0].lines[0].references), 0) self.assertEqual(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None) 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(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]]) Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
books[0].rec_name, books[0].rec_name,
@ -2172,6 +2258,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]]) Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
books[0].rec_name, books[0].rec_name,
@ -2269,7 +2356,8 @@ class CbInvTestCase(object):
self.assertEqual( self.assertEqual(
asset1.rec_name, asset1.rec_name,
'Aurum | 1,750.0000 usd/oz | 05/01/2022') '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) (usd, euro) = self.prep_2nd_currency(company)
self.assertEqual(company.currency.rec_name, 'Euro') self.assertEqual(company.currency.rec_name, 'Euro')
@ -2310,7 +2398,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex( self.assertRaisesRegex(
UserError, 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, Book.write,
*[ *[
[books[0]], [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(book.balance_all, Decimal('-11.0'))
self.assertEqual(len(book.lines), 1) self.assertEqual(len(book.lines), 1)
self.assertEqual(book.lines[0].splitline_has_quantity, False) 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(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None) 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(books[1].balance_all, Decimal('0.0'))
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]]) Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
books[0].rec_name, books[0].rec_name,
@ -2527,7 +2619,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[1].rec_name, books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book 2 | 6.00 usd | Open' + 'Exp/Sp|6.00 usd|to cashbook [Book 2 | 6.00 usd | Open' +
' | 2.50 u]|2.50 u') ' | 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( self.assertEqual(
books[0].lines[0].splitlines[1].amount_2nd_currency, books[0].lines[0].splitlines[1].amount_2nd_currency,
None) None)
@ -2545,7 +2638,8 @@ class CbInvTestCase(object):
' | Open | -4.00 u]|2.50 u') ' | Open | -4.00 u]|2.50 u')
self.assertEqual(books[0].lines[0].reference, None) 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(books[1].balance_all, Decimal('6.0'))
self.assertEqual(len(books[1].lines), 1) self.assertEqual(len(books[1].lines), 1)
self.assertEqual( self.assertEqual(
@ -2642,7 +2736,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[0].rec_name, books[0].lines[0].splitlines[0].rec_name,
'Exp/Sp|5.00 usd|to category [Cat1]') '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, 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( self.assertEqual(
books[0].lines[0].splitlines[1].rec_name, books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book Asset | 0.00 usd | ' + 'Exp/Sp|6.00 usd|to cashbook [Book Asset | 0.00 usd | ' +
@ -2650,7 +2745,8 @@ class CbInvTestCase(object):
self.assertEqual( self.assertEqual(
books[0].lines[0].splitlines[1].quantity, books[0].lines[0].splitlines[1].quantity,
Decimal('2.5')) 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(len(books[0].lines[0].references), 0)
self.assertEqual(books[0].lines[0].reference, None) self.assertEqual(books[0].lines[0].reference, None)
@ -2662,6 +2758,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].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].rec_name, 'Book Cash | -11.00 usd | Open')
self.assertEqual(books[0].balance_all, Decimal('-11.0')) self.assertEqual(books[0].balance_all, Decimal('-11.0'))
@ -2684,7 +2781,8 @@ class CbInvTestCase(object):
books[0].lines[0].splitlines[1].rec_name, books[0].lines[0].splitlines[1].rec_name,
'Exp/Sp|6.00 usd|to cashbook [Book Asset | 6.00 usd' + 'Exp/Sp|6.00 usd|to cashbook [Book Asset | 6.00 usd' +
' | Open | 2.50 u]') ' | 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( self.assertEqual(
books[0].lines[0].splitlines[1].amount_2nd_currency, books[0].lines[0].splitlines[1].amount_2nd_currency,
None) None)
@ -2853,6 +2951,7 @@ class CbInvTestCase(object):
self.assertEqual(len(books[1].lines), 0) self.assertEqual(len(books[1].lines), 0)
Line.wfcheck([books[0].lines[0]]) Line.wfcheck([books[0].lines[0]])
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
books[0].rec_name, books[0].rec_name,
@ -2993,7 +3092,8 @@ class CbInvTestCase(object):
self.assertRaisesRegex( self.assertRaisesRegex(
UserError, 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, Book.write,
*[ *[
[books[0]], [books[0]],

View file

@ -10,9 +10,11 @@ from trytond.modules.investment.tests.test_module import InvestmentTestCase
from .yieldtest import YieldTestCase from .yieldtest import YieldTestCase
from .book import CbInvTestCase from .book import CbInvTestCase
from .reconciliation import ReconTestCase from .reconciliation import ReconTestCase
from .valuestore import ValueStoreTestCase
class CashbookInvestmentTestCase( class CashbookInvestmentTestCase(
ValueStoreTestCase,
CashbookTestCase, CashbookTestCase,
InvestmentTestCase, InvestmentTestCase,
CbInvTestCase, 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) self.assertEqual(len(lines), 1)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -257,6 +258,7 @@ class YieldTestCase(object):
}]) }])
Line.wfcheck(book_cash.lines) Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book_asset.rec_name, book_asset.rec_name,
@ -366,6 +368,7 @@ class YieldTestCase(object):
}]) }])
Line.wfcheck(book_asset.lines) Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book_asset.rec_name, book_asset.rec_name,
@ -451,6 +454,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -532,6 +536,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -631,6 +636,7 @@ class YieldTestCase(object):
}]) }])
Line.wfcheck(book_cash.lines) Line.wfcheck(book_cash.lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book_asset.rec_name, book_asset.rec_name,
@ -743,6 +749,7 @@ class YieldTestCase(object):
}]) }])
Line.wfcheck(book_asset.lines) Line.wfcheck(book_asset.lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
book_asset.rec_name, book_asset.rec_name,
@ -879,6 +886,7 @@ class YieldTestCase(object):
lines[0].rec_name, lines[0].rec_name,
'05/02/2022|Exp/Sp|-23.50 usd|all out (1) [-]|-3.0000 u') '05/02/2022|Exp/Sp|-23.50 usd|all out (1) [-]|-3.0000 u')
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -1007,6 +1015,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2) self.assertEqual(len(lines), 2)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -1150,6 +1159,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 1) self.assertEqual(len(lines), 1)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -1301,6 +1311,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 3) self.assertEqual(len(lines), 3)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,
@ -1438,6 +1449,7 @@ class YieldTestCase(object):
self.assertEqual(len(lines), 2) self.assertEqual(len(lines), 2)
Line.wfcheck(lines) Line.wfcheck(lines)
self.prep_valstore_run_worker()
self.assertEqual( self.assertEqual(
lines[0].rec_name, lines[0].rec_name,

View file

@ -1,5 +1,5 @@
[tryton] [tryton]
version=6.8.10 version=6.8.12
depends: depends:
cashbook cashbook
investment investment
@ -11,3 +11,4 @@ xml:
splitline.xml splitline.xml
assetsetting.xml assetsetting.xml
menu.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> <data>
<xpath expr="/tree/field[@name='balance']" position="after"> <xpath expr="/tree/field[@name='balance']" position="after">
<field name="current_value" symbol="currency"/> <field name="current_value" symbol="currency" optional="0"/>
<field name="diff_amount" symbol="currency"/> <field name="purchase_amount" symbol="currency" optional="1"/>
<field name="diff_percent"/> <field name="diff_amount" symbol="currency" optional="0"/>
<field name="quantity" symbol="quantity_uom"/> <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> </xpath>
</data> </data>

View file

@ -5,8 +5,19 @@ full copyright notices and license terms. -->
<data> <data>
<xpath expr="/tree/field[@name='balance']" position="after"> <xpath expr="/tree/field[@name='balance']" position="after">
<field name="diff_amount" symbol="currency"/> <field name="current_value" symbol="currency" optional="1"/>
<field name="diff_percent"/> <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> </xpath>
</data> </data>

View file

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

View file

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

View file

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