From c70d2fef7a784973c6c86805af9b864863230691 Mon Sep 17 00:00:00 2001 From: Frederik Jaeckel Date: Sun, 12 Feb 2023 00:09:56 +0100 Subject: [PATCH] line: add fields 'Fee' + 'Dividend' - todos --- __init__.py | 2 + assetsetting.py | 24 +++++ assetsetting.xml | 53 ++++++++++ line.py | 137 +++++++++++++++++++++++++ locale/de.po | 60 +++++++++++ locale/en.po | 36 +++++++ menu.xml | 18 ++++ tests/__init__.py | 2 + tests/test_yield.py | 215 ++++++++++++++++++++++++++++++++++++++++ tryton.cfg | 2 + view/assetconf_form.xml | 14 +++ view/line_form.xml | 6 ++ 12 files changed, 569 insertions(+) create mode 100644 assetsetting.py create mode 100644 assetsetting.xml create mode 100644 menu.xml create mode 100644 tests/test_yield.py create mode 100644 view/assetconf_form.xml 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 @@ + + +
+ +