diff --git a/__init__.py b/__init__.py
index a5cbea0..a11da2c 100644
--- a/__init__.py
+++ b/__init__.py
@@ -9,6 +9,7 @@ from .book import Book
from .reconciliation import Reconciliation
from .line import Line
from .splitline import SplitLine
+from .assetsetting import AssetSetting
def register():
@@ -18,4 +19,5 @@ def register():
Line,
SplitLine,
Reconciliation,
+ AssetSetting,
module='cashbook_investment', type_='model')
diff --git a/assetsetting.py b/assetsetting.py
new file mode 100644
index 0000000..f9846a8
--- /dev/null
+++ b/assetsetting.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# This file is part of the cashbook-module from m-ds.de for Tryton.
+# The COPYRIGHT file at the top level of this repository contains the
+# full copyright notices and license terms.
+
+
+from trytond.model import ModelSingleton, ModelView, ModelSQL, fields
+
+
+class AssetSetting(ModelSingleton, ModelSQL, ModelView):
+ 'Asset setting'
+ __name__ = 'cashbook.assetconf'
+
+ fee_category = fields.Many2One(string='Fee category',
+ model_name='cashbook.category', ondelete='RESTRICT',
+ help='Category for fees when trading assets.')
+ dividend_category = fields.Many2One(string='Dividend category',
+ model_name='cashbook.category', ondelete='RESTRICT',
+ help='Category for dividend paid out.')
+ gainloss_book = fields.Many2One(string='Profit/Loss Cashbook',
+ model_name='cashbook.book', ondelete='RESTRICT',
+ help='Profit and loss on sale of assets are recorded in the cash book.')
+
+# end AssetSetting
diff --git a/assetsetting.xml b/assetsetting.xml
new file mode 100644
index 0000000..5b3e284
--- /dev/null
+++ b/assetsetting.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ cashbook.assetconf
+ form
+ assetconf_form
+
+
+
+ Asset setting
+ cashbook.assetconf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/line.py b/line.py
index 8c7b862..3682edb 100644
--- a/line.py
+++ b/line.py
@@ -4,12 +4,14 @@
# full copyright notices and license terms.
from decimal import Decimal
+from sql.conditionals import Coalesce
from trytond.model import fields
from trytond.pool import PoolMeta, Pool
from trytond.pyson import Eval, Or, If, And
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.report import Report
+from trytond.transaction import Transaction
from trytond.modules.cashbook.line import STATES, DEPENDS
from .mixin import SecondUomMixin
@@ -108,6 +110,141 @@ class Line(SecondUomMixin, metaclass=PoolMeta):
}, depends=['currency_digits', 'feature']),
'on_change_with_diff_percent')
+ trade_fee = fields.Function(fields.Numeric(string='Fee',
+ readonly=True, digits=(16, Eval('currency_digits', 2)),
+ states = {
+ 'invisible': Eval('feature', '') != 'asset',
+ }, depends=['currency_digits', 'feature']),
+ 'get_yield_data')
+ asset_dividend = fields.Function(fields.Numeric(string='Dividend',
+ readonly=True, digits=(16, Eval('currency_digits', 2)),
+ states = {
+ 'invisible': Eval('feature', '') != 'asset',
+ }, depends=['currency_digits', 'feature']),
+ 'get_yield_data')
+ asset_gainloss = fields.Function(fields.Numeric(string='Profit/Loss',
+ readonly=True, digits=(16, Eval('currency_digits', 2)),
+ states = {
+ 'invisible': Eval('feature', '') != 'asset',
+ }, depends=['currency_digits', 'feature']),
+ 'get_yield_data')
+
+ @classmethod
+ def get_yield_data_sql(cls):
+ """ query for fee, dividend, gain/loss
+ """
+ pool = Pool()
+ AssetSetting = pool.get('cashbook.assetconf')
+ SplitLine = pool.get('cashbook.split')
+ tab_line = cls.__table__()
+ tab_line1 = cls.__table__()
+ tab_line2 = cls.__table__()
+ tab_line_fee = cls.__table__()
+ tab_line_divi = cls.__table__()
+ tab_spline_fee = SplitLine.__table__()
+ tab_spline_divi = SplitLine.__table__()
+
+ cfg1 = AssetSetting.get_singleton()
+
+ # local booked fee/dividend
+ tab_inout = cls.search([
+ ('cashbook.btype.feature', '=', 'asset'),
+ ('bookingtype', 'in', ['in', 'out']),
+ ('category', '!=', None),
+ ], query=True)
+ query_inout = tab_inout.join(tab_line_fee,
+ condition=(tab_line_fee.id==tab_inout.id) & \
+ (tab_line_fee.category == getattr(cfg1.fee_category, 'id', None)),
+ type_ = 'LEFT OUTER',
+ ).join(tab_line_divi,
+ condition=(tab_line_divi.id==tab_inout.id) & \
+ (tab_line_divi.category == getattr(cfg1.dividend_category, 'id', None)),
+ type_ = 'LEFT OUTER',
+ ).select(
+ tab_inout.id,
+ (tab_line_fee.credit - tab_line_fee.debit).as_('fee'),
+ (tab_line_divi.credit - tab_line_divi.debit).as_('dividend'),
+ )
+
+ # fee/dividend - in counterpart booking
+ tab_mvinout = cls.search([
+ ('cashbook.btype.feature', '=', 'asset'),
+ ('bookingtype', 'in', ['mvin', 'mvout']),
+ ], query=True)
+ query_mvinout = tab_mvinout.join(tab_line1,
+ condition=tab_mvinout.id==tab_line1.id,
+ ).join(tab_line2,
+ # current line is linked to split-booking-line of counterpart
+ condition=((tab_line1.reference == tab_line2.id) | \
+ (tab_line1.id == tab_line2.reference)) & \
+ (tab_line2.bookingtype.in_(['spin', 'spout'])),
+ ).join(tab_spline_fee,
+ # fee-line is linked to split-booking-line
+ condition=(tab_spline_fee.line == tab_line2.id) & \
+ (tab_spline_fee.splittype == 'cat') & \
+ (tab_spline_fee.category != None) & \
+ (tab_spline_fee.category == getattr(cfg1.fee_category, 'id', None)),
+ type_ = 'LEFT OUTER',
+ ).join(tab_spline_divi,
+ # dividend-line is linked to split-booking-line
+ condition=(tab_spline_divi.line == tab_line2.id) & \
+ (tab_spline_divi.splittype == 'cat') & \
+ (tab_spline_divi.category != None) & \
+ (tab_spline_divi.category == getattr(cfg1.dividend_category, 'id', None)),
+ type_ = 'LEFT OUTER',
+ ).select(
+ tab_line1.id,
+ tab_spline_fee.amount.as_('fee'),
+ tab_spline_divi.amount.as_('dividend'),
+ )
+
+ # together
+ query = tab_line.join(query_inout,
+ condition=query_inout.id==tab_line.id,
+ type_ = 'LEFT OUTER',
+ ).join(query_mvinout,
+ condition=query_mvinout.id==tab_line.id,
+ type_ = 'LEFT OUTER',
+ ).select(
+ tab_line.id,
+ Coalesce(query_inout.fee, query_mvinout.fee).as_('fee'),
+ Coalesce(query_inout.dividend, query_mvinout.dividend).as_('dividend'),
+ )
+ return (tab_line, query)
+
+ @classmethod
+ def get_yield_data(cls, lines, names):
+ """ collect data for fee, dividend, gain/loss per line
+ """
+ Line2 = Pool().get('cashbook.line')
+ (tab_line, query) = cls.get_yield_data_sql()
+ cursor = Transaction().connection.cursor()
+
+ def quantize_val(value, line):
+ """ quantize...
+ """
+ return (
+ value or Decimal('0.0')
+ ).quantize(Decimal(str(1/10**line.currency_digits)))
+
+ result = {x:{y.id: None for y in lines} for x in names}
+ query.where = tab_line.id.in_([x.id for x in lines])
+ cursor.execute(*query)
+ records = cursor.fetchall()
+
+ for record in records:
+ line = Line2(record[0])
+ values = {
+ 'trade_fee': quantize_val(record[1], line),
+ 'asset_dividend': quantize_val(record[2], line),
+ 'asset_gainloss': Decimal('0.0'), #quantize_val(record[3], line),
+ }
+
+ for name in names:
+ result[name][record[0]] = values[name]
+
+ return result
+
def get_rec_name(self, name):
""" add quantities - if its a asset-cashbook
"""
diff --git a/locale/de.po b/locale/de.po
index cc86f22..d23cc63 100644
--- a/locale/de.po
+++ b/locale/de.po
@@ -23,6 +23,22 @@ msgid "Cannot transfer quantities between cashbooks with different unit-categori
msgstr "Es können keine Mengen zwischen Kassenbüchern mit verschiedenen Einheitenkategorien (%(cat1)s != %(cat2)s) übertragen werden."
+##############
+# ir.ui.menu #
+##############
+msgctxt "model:ir.ui.menu,name:menu_assetconf"
+msgid "Asset setting"
+msgstr "Vermögenswert"
+
+
+#############
+# ir.action #
+#############
+msgctxt "model:ir.action,name:act_assetconf_form"
+msgid "Asset setting"
+msgstr "Vermögenswert-Einstellung"
+
+
#################
# cashbook.book #
#################
@@ -246,6 +262,18 @@ msgctxt "help:cashbook.line,diff_percent:"
msgid "percentage performance since acquisition"
msgstr "prozentuale Wertentwicklung seit Anschaffung"
+msgctxt "field:cashbook.line,trade_fee:"
+msgid "Fee"
+msgstr "Gebühr"
+
+msgctxt "field:cashbook.line,asset_dividend:"
+msgid "Dividend"
+msgstr "Dividende"
+
+msgctxt "field:cashbook.line,asset_gainloss:"
+msgid "Profit/Loss"
+msgstr "Gewinn/Verlust"
+
##################
# cashbook.recon #
@@ -265,3 +293,35 @@ msgstr "Anzahl-Dezimalstellen"
msgctxt "field:cashbook.recon,quantity_uom:"
msgid "Symbol"
msgstr "Symbol"
+
+
+######################
+# cashbook.assetconf #
+######################
+msgctxt "model:cashbook.assetconf,name:"
+msgid "Asset setting"
+msgstr "Vermögenswert-Einstellung"
+
+msgctxt "field:cashbook.assetconf,fee_category:"
+msgid "Fee category"
+msgstr "Gebührenkategorie"
+
+msgctxt "help:cashbook.assetconf,fee_category:"
+msgid "Category for fees when trading assets."
+msgstr "Kategorie für Gebühren beim Handel mit Vermögenswerten."
+
+msgctxt "field:cashbook.assetconf,dividend_category:"
+msgid "Dividend category"
+msgstr "Dividendenkategorie"
+
+msgctxt "help:cashbook.assetconf,dividend_category:"
+msgid "Category for dividend paid out."
+msgstr "Kategorie für ausgezahlte Dividenden."
+
+msgctxt "field:cashbook.assetconf,gainloss_book:"
+msgid "Profit/Loss Cashbook"
+msgstr "Gewinn/Verlust Kassenbuch"
+
+msgctxt "help:cashbook.assetconf,gainloss_book:"
+msgid "Profit and loss on sale of assets are recorded in the cash book."
+msgstr "Gewinn und Verlust bei Verkauf von Vermögenswerten werden auf das Kassenbuch gebucht."
diff --git a/locale/en.po b/locale/en.po
index ce29b4e..585c663 100644
--- a/locale/en.po
+++ b/locale/en.po
@@ -18,6 +18,14 @@ msgctxt "model:ir.message,text:msg_uomcat_mismatch"
msgid "Cannot transfer quantities between cashbooks with different unit-categories (%(cat1)s != %(cat2)s)."
msgstr "Cannot transfer quantities between cashbooks with different unit-categories (%(cat1)s != %(cat2)s)."
+msgctxt "model:ir.ui.menu,name:menu_assetconf"
+msgid "Asset setting"
+msgstr "Asset setting"
+
+msgctxt "model:ir.action,name:act_assetconf_form"
+msgid "Asset setting"
+msgstr "Asset setting"
+
msgctxt "view:cashbook.book:"
msgid "Asset"
msgstr "Asset"
@@ -242,3 +250,31 @@ msgctxt "field:cashbook.recon,quantity_digits:"
msgid "Quantity Digits"
msgstr "Quantity Digits"
+msgctxt "field:cashbook.recon,quantity_uom:"
+msgid "Symbol"
+msgstr "Symbol"
+
+msgctxt "model:cashbook.assetconf,name:"
+msgid "Asset setting"
+msgstr "Asset setting"
+
+msgctxt "field:cashbook.assetconf,fee_category:"
+msgid "Fee category"
+msgstr "Fee category"
+
+msgctxt "help:cashbook.assetconf,fee_category:"
+msgid "Category for fees when trading assets."
+msgstr "Category for fees when trading assets."
+
+msgctxt "field:cashbook.assetconf,dividend_category:"
+msgid "Dividend category"
+msgstr "Dividend category"
+
+msgctxt "help:cashbook.assetconf,dividend_category:"
+msgid "Category for dividend paid out."
+msgstr "Category for dividend paid out."
+
+msgctxt "field:cashbook.assetconf,gainloss_book:"
+msgid "Profit/Loss Cashbook"
+msgstr "Profit/Loss Cashbook"
+
diff --git a/menu.xml b/menu.xml
new file mode 100644
index 0000000..5a97d05
--- /dev/null
+++ b/menu.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/__init__.py b/tests/__init__.py
index 21885a8..97f40bc 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -6,6 +6,7 @@ import unittest
from trytond.modules.cashbook_investment.tests.test_book import CbInvTestCase
from trytond.modules.cashbook_investment.tests.test_reconciliation import ReconTestCase
+from trytond.modules.cashbook_investment.tests.test_yield import YieldTestCase
__all__ = ['suite']
@@ -14,6 +15,7 @@ __all__ = ['suite']
class CashbookInvestmentTestCase(\
CbInvTestCase,\
ReconTestCase,\
+ YieldTestCase,\
):
'Test cashbook-investment module'
module = 'cashbook_investment'
diff --git a/tests/test_yield.py b/tests/test_yield.py
new file mode 100644
index 0000000..2a8568a
--- /dev/null
+++ b/tests/test_yield.py
@@ -0,0 +1,215 @@
+# -*- 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 decimal import Decimal
+from datetime import date
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+from trytond.tests.test_tryton import ModuleTestCase, with_transaction
+
+
+class YieldTestCase(ModuleTestCase):
+ 'Test yield calculation module'
+ module = 'cashbook_investment'
+
+ def prep_yield_config(self, fee, dividend, gainloss, company):
+ """ add config for yield-calculation
+ fee: name of fee-category,
+ dividend: name of fee-category,
+ gainloss: name of cashbook for gain/loss booking
+ """
+ pool = Pool()
+ Category = pool.get('cashbook.category')
+ Cashbook = pool.get('cashbook.book')
+ AssetConf = pool.get('cashbook.assetconf')
+
+ fee_cat = Category.search([('name', '=', fee)])
+ if len(fee_cat) > 0:
+ fee_cat = fee_cat[0]
+ else :
+ fee_cat, = Category.create([{
+ 'name': fee,
+ 'company': company.id,
+ 'cattype': 'out',
+ }])
+
+ dividend_cat = Category.search([('name', '=', dividend)])
+ if len(dividend_cat) > 0:
+ dividend_cat = dividend_cat[0]
+ else :
+ dividend_cat, = Category.create([{
+ 'name': dividend,
+ 'company': company.id,
+ 'cattype': 'in',
+ }])
+
+ gainloss_book = Cashbook.search([('name', '=', gainloss)])
+ if len(gainloss_book) > 0:
+ gainloss_book = gainloss_book[0]
+ else :
+ types = self.prep_type()
+ gainloss_book, = Cashbook.create([{
+ 'name': gainloss,
+ 'btype': types.id,
+ 'company': company.id,
+ 'currency': company.currency.id,
+ 'number_sequ': self.prep_sequence().id,
+ }])
+
+ as_cfg = None
+ with Transaction().set_context({
+ 'company': company.id,
+ }):
+ as_cfg, = AssetConf.create([{
+ 'fee_category': fee_cat.id,
+ 'dividend_category': dividend_cat.id,
+ 'gainloss_book': gainloss_book.id,
+ }])
+ self.assertEqual(as_cfg.fee_category.rec_name, 'Fee')
+ self.assertEqual(as_cfg.fee_category.cattype, 'out')
+
+ self.assertEqual(as_cfg.dividend_category.rec_name, 'Dividend')
+ self.assertEqual(as_cfg.dividend_category.cattype, 'in')
+
+ self.assertEqual(as_cfg.gainloss_book.rec_name,
+ 'GainLoss | 0.00 usd | Open')
+ return as_cfg
+
+ @with_transaction()
+ def test_yield_config(self):
+ """ check config
+ """
+ pool = Pool()
+ AssetConf = pool.get('cashbook.assetconf')
+ company = self.prep_company()
+
+ as_cfg = self.prep_yield_config('Fee', 'Dividend', 'GainLoss', company)
+ self.assertEqual(as_cfg.fee_category.rec_name, 'Fee')
+ self.assertEqual(as_cfg.dividend_category.rec_name, 'Dividend')
+ self.assertEqual(as_cfg.gainloss_book.rec_name, 'GainLoss | 0.00 usd | Open')
+
+ @with_transaction()
+ def test_yield_fee_dividend_gainloss(self):
+ """ add cashbook, categories, bookings for
+ fees, dididend...
+ """
+ pool = Pool()
+ Cashbook = pool.get('cashbook.book')
+ BType = pool.get('cashbook.type')
+ Category = pool.get('cashbook.category')
+ Line = pool.get('cashbook.line')
+
+ company = self.prep_company()
+ as_cfg = self.prep_yield_config('Fee', 'Dividend', 'GainLoss', company)
+
+ type_depot = self.prep_type('Depot', 'D')
+ type_cash = self.prep_type('Cash', 'C')
+ BType.write(*[
+ [type_depot],
+ {
+ 'feature': 'asset',
+ }])
+
+ asset = self.prep_asset_item(
+ company=company,
+ product = self.prep_asset_product(name='Product 1'))
+ self.assertEqual(asset.symbol, 'usd/u')
+
+ category_office, = Category.create([{
+ 'name': 'Office',
+ 'company': company.id,
+ }])
+
+ book_asset, = Cashbook.create([{
+ 'name': 'Depot',
+ 'btype': type_depot.id,
+ 'company': company.id,
+ 'currency': company.currency.id,
+ 'number_sequ': self.prep_sequence().id,
+ 'asset': asset.id,
+ 'quantity_uom': asset.uom.id,
+ 'start_date': date(2022, 5, 1),
+ 'lines': [('create', [{
+ 'bookingtype': 'out',
+ 'date': date(2022, 5, 1),
+ 'amount': Decimal('2.0'),
+ 'quantity': Decimal('0.0'),
+ 'category': as_cfg.fee_category.id,
+ 'description': 'Fee',
+ }, {
+ 'bookingtype': 'in',
+ 'date': date(2022, 5, 1),
+ 'amount': Decimal('10.0'),
+ 'quantity': Decimal('1.0'),
+ 'category': as_cfg.dividend_category.id,
+ 'description': 'reinvested dividend',
+ }])],
+ }])
+
+ # dividend, incoming + link to depot-account
+ book_cash, = Cashbook.create([{
+ 'name': 'Cash',
+ 'btype': type_cash.id,
+ 'company': company.id,
+ 'currency': company.currency.id,
+ 'number_sequ': self.prep_sequence().id,
+ 'start_date': date(2022, 5, 1),
+ 'lines': [('create', [{
+ 'bookingtype': 'spin',
+ 'date': date(2022, 5, 1),
+ 'description': 'Dividend',
+ 'splitlines': [('create', [{
+ 'description': 'Dividend',
+ 'splittype': 'cat',
+ 'category': as_cfg.dividend_category.id,
+ 'amount': Decimal('5.0'),
+ }, {
+ 'description': 'Dividend',
+ 'splittype': 'tr',
+ 'booktransf': book_asset.id,
+ 'amount': Decimal('0.0'),
+ 'quantity': Decimal('0.0'),
+ }])],
+ }])],
+ }])
+ self.assertEqual(len(book_cash.lines), 1)
+ Line.wfcheck([book_cash.lines[0]])
+
+ self.assertEqual(book_cash.lines[0].rec_name,
+ '05/01/2022|Rev/Sp|5.00 usd|Dividend [-]')
+ self.assertEqual(book_cash.lines[0].reference, None)
+ self.assertEqual(len(book_cash.lines[0].references), 1)
+ self.assertEqual(book_cash.lines[0].references[0].rec_name,
+ '05/01/2022|to|0.00 usd|Dividend [Cash | 5.00 usd | Open]|0.0000 u')
+
+ self.assertEqual(book_asset.name, 'Depot')
+ self.assertEqual(book_asset.rec_name, 'Depot | 8.00 usd | Open | 1.0000 u')
+ self.assertEqual(book_asset.btype.rec_name, 'D - Depot')
+ self.assertEqual(book_asset.state, 'open')
+ self.assertEqual(book_asset.feature, 'asset')
+ self.assertEqual(len(book_asset.lines), 3)
+
+ # reference to dividend (1)
+ self.assertEqual(book_asset.lines[0].rec_name,
+ '05/01/2022|to|0.00 usd|Dividend [Cash | 5.00 usd | Open]|0.0000 u')
+ self.assertEqual(book_asset.lines[0].trade_fee, Decimal('0.0'))
+ self.assertEqual(book_asset.lines[0].asset_dividend, Decimal('5.0'))
+ self.assertEqual(book_asset.lines[0].asset_gainloss, Decimal('0.0'))
+
+ # fee payed from asset-account
+ self.assertEqual(book_asset.lines[1].rec_name,
+ '05/01/2022|Exp|-2.00 usd|Fee [Fee]|0.0000 u')
+ self.assertEqual(book_asset.lines[1].trade_fee, Decimal('-2.0'))
+ self.assertEqual(book_asset.lines[1].asset_dividend, Decimal('0.0'))
+ self.assertEqual(book_asset.lines[1].asset_gainloss, Decimal('0.0'))
+
+ # dividend (2) added to asset-account
+ self.assertEqual(book_asset.lines[2].rec_name,
+ '05/01/2022|Rev|10.00 usd|reinvested dividend [Dividend]|1.0000 u')
+ self.assertEqual(book_asset.lines[2].trade_fee, Decimal('0.0'))
+ self.assertEqual(book_asset.lines[2].asset_dividend, Decimal('10.0'))
+ self.assertEqual(book_asset.lines[2].asset_gainloss, Decimal('0.0'))
+
+# end YieldTestCase
diff --git a/tryton.cfg b/tryton.cfg
index dc3ee72..40fa87c 100644
--- a/tryton.cfg
+++ b/tryton.cfg
@@ -9,3 +9,5 @@ xml:
line.xml
reconciliation.xml
splitline.xml
+ assetsetting.xml
+ menu.xml
diff --git a/view/assetconf_form.xml b/view/assetconf_form.xml
new file mode 100644
index 0000000..c3b563a
--- /dev/null
+++ b/view/assetconf_form.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/view/line_form.xml b/view/line_form.xml
index 6ecafb8..0e00fa3 100644
--- a/view/line_form.xml
+++ b/view/line_form.xml
@@ -34,6 +34,12 @@ full copyright notices and license terms. -->
+
+
+
+
+
+