diff --git a/book.py b/book.py index 3a9b64a..4e41f28 100644 --- a/book.py +++ b/book.py @@ -5,9 +5,14 @@ from trytond.model import fields, SymbolMixin from trytond.exceptions import UserError -from trytond.pool import PoolMeta +from trytond.pool import PoolMeta, Pool from trytond.pyson import Eval, Or, Len from trytond.modules.cashbook.book import STATES2, DEPENDS2 +from trytond.transaction import Transaction +from decimal import Decimal +from sql.functions import CurrentDate +from sql.aggregate import Sum +from sql.conditionals import Case, Coalesce class Book(SymbolMixin, metaclass=PoolMeta): @@ -55,6 +60,32 @@ class Book(SymbolMixin, metaclass=PoolMeta): }, depends=DEPENDS2+['feature', 'lines', 'asset_uomcat']) symbol = fields.Function(fields.Char(string='Symbol', readonly=True), 'on_change_with_symbol') + quantity = fields.Function(fields.Numeric(string='Quantity', + help='Quantity of assets until to date', readonly=True, + digits=(16, Eval('quantity_digits', 4)), + states={ + 'invisible': Eval('feature', '') != 'asset', + }, depends=['quantity_digits', 'feature']), + 'get_asset_quantity') + quantity_all = fields.Function(fields.Numeric(string='Total Quantity', + help='Total quantity of all assets', readonly=True, + digits=(16, Eval('quantity_digits', 4)), + states={ + 'invisible': Eval('feature', '') != 'asset', + }, depends=['quantity_digits', 'feature']), + 'get_asset_quantity') + current_value = fields.Function(fields.Numeric(string='Value', + readonly=True, digits=(16, Eval('currency_digits', 2)), + states={ + 'invisible': Eval('feature', '') != 'asset', + }, depends=['currency_digits', 'feature']), + 'get_asset_quantity') + current_value_ref = fields.Function(fields.Numeric(string='Value (Ref.)', + readonly=True, digits=(16, Eval('currency_digits', 2)), + states={ + 'invisible': Eval('feature', '') != 'asset', + }, depends=['currency_digits', 'feature']), + 'get_asset_quantity') @fields.depends('asset', 'quantity_uom') def on_change_asset(self): @@ -69,6 +100,85 @@ class Book(SymbolMixin, metaclass=PoolMeta): """ return 4 + @classmethod + def get_asset_quantity(cls, cashbooks, names): + """ get quantities + """ + pool = Pool() + CBook = pool.get('cashbook.book') + Line = pool.get('cashbook.line') + Asset = pool.get('investment.asset') + Currency = pool.get('currency.currency') + Uom = pool.get('product.uom') + tab_book = CBook.__table__() + tab_line = Line.__table__() + tab_cur = Currency.__table__() + tab_asset = Asset.__table__() + (tab_rate, tab2) = Asset.get_rate_data_sql() + cursor = Transaction().connection.cursor() + context = Transaction().context + + result = {x:{y.id: None for y in cashbooks} for x in names} + query_date = context.get('qdate', CurrentDate()) + company_currency = CBook.default_currency() + + query = tab_book.join(tab_line, + condition=(tab_book.id==tab_line.cashbook), + ).join(tab_cur, + condition=tab_book.currency==tab_cur.id, + ).join(tab_asset, + condition=tab_book.asset==tab_asset.id, + ).join(tab_rate, + condition=tab_book.asset==tab_rate.id, + type_ = 'LEFT OUTER', + ).select( + tab_book.id, # 0 + Coalesce(Sum(Case( + (tab_line.date <= query_date, tab_line.quantity), + else_ = Decimal('0.0'), + )), Decimal('0.0')).as_('quantity'), # 1 + Sum(tab_line.quantity).as_('quantity_all'), # 2 + Coalesce(tab_rate.rate, Decimal('0.0')).as_('rate'), # 3 + tab_book.currency, # 4 + tab_cur.digits.as_('currency_digits'), # 5 + tab_asset.uom, # 6 + tab_book.quantity_uom, # 7 + tab_asset.currency.as_('asset_currency'), #8 + group_by=[tab_book.id, tab_rate.rate, + tab_book.currency, tab_cur.digits, tab_asset.uom, + tab_book.quantity_uom, tab_asset.currency], + ) + cursor.execute(*query) + records = cursor.fetchall() + + for record in records: + # uom-factor + if record[6] == record[7]: + uom_factor = Decimal('1.0') + else : + uom_factor = Decimal( + Uom.compute_qty(Uom(record[6]), 1.0, Uom(record[7]), round=False) + ) + + values = { + 'quantity': record[1], + 'quantity_all': record[2], + 'current_value': Currency.compute( + record[8], + record[3] * record[1] / uom_factor, + record[4] + ), + 'current_value_ref': Currency.compute( + record[8], + record[3] * record[1] / uom_factor, + company_currency if company_currency is not None else record[8], + ), + } + + for name in names: + result[name][record[0]] = values[name] + return result + @fields.depends('quantity_uom', 'currency') def on_change_with_symbol(self, name=None): """ get symbol for asset diff --git a/locale/de.po b/locale/de.po index ada411c..7d9ce77 100644 --- a/locale/de.po +++ b/locale/de.po @@ -18,6 +18,14 @@ msgctxt "view:cashbook.book:" msgid "Asset" msgstr "Vermögenswert" +msgctxt "view:cashbook.book:" +msgid "Quantity" +msgstr "Anzahl" + +msgctxt "view:cashbook.book:" +msgid "Current value of the asset" +msgstr "aktueller Wert des Vermögenswertes" + msgctxt "field:cashbook.book,asset:" msgid "Asset" msgstr "Vermögenswert" @@ -34,7 +42,7 @@ msgctxt "help:cashbook.book,asset_uomcat:" msgid "UOM Category" msgstr "Einheitenkategorie" -msgctxt "help:cashbook.book,quantity_uom:" +msgctxt "field:cashbook.book,quantity_uom:" msgid "UOM" msgstr "Einheit" @@ -42,6 +50,38 @@ msgctxt "field:cashbook.book,symbol:" msgid "Symbol" msgstr "Symbol" +msgctxt "field:cashbook.book,quantity:" +msgid "Quantity" +msgstr "Anzahl" + +msgctxt "help:cashbook.book,quantity:" +msgid "Quantity of assets until to date" +msgstr "Menge der Vermögenswerte bis heute" + +msgctxt "field:cashbook.book,quantity_all:" +msgid "Total Quantity" +msgstr "Gesamtanzahl" + +msgctxt "help:cashbook.book,quantity_all:" +msgid "Total quantity of all assets" +msgstr "Gesamtmenge der Vermögenswerte" + +msgctxt "field:cashbook.book,current_value:" +msgid "Value" +msgstr "Wert" + +msgctxt "help:cashbook.book,current_value:" +msgid "Current Value in cashbook currency" +msgstr "aktueller Wert in der Kassenbuchwährung" + +msgctxt "field:cashbook.book,current_value_ref:" +msgid "Value (Ref)" +msgstr "Wert (Ref)" + +msgctxt "help:cashbook.book,current_value:" +msgid "Current Value in company currency" +msgstr "aktueller Wert in der Unternehmenswährung" + ################# # cashbook.line # diff --git a/locale/en.po b/locale/en.po index 9501c48..210e1a7 100644 --- a/locale/en.po +++ b/locale/en.po @@ -10,6 +10,14 @@ msgctxt "view:cashbook.book:" msgid "Asset" msgstr "Asset" +msgctxt "view:cashbook.book:" +msgid "Quantity" +msgstr "Quantity" + +msgctxt "view:cashbook.book:" +msgid "Value of the asset" +msgstr "Value of the asset" + msgctxt "field:cashbook.book,asset:" msgid "Asset" msgstr "Asset" @@ -26,7 +34,7 @@ msgctxt "help:cashbook.book,asset_uomcat:" msgid "UOM Category" msgstr "UOM Category" -msgctxt "help:cashbook.book,quantity_uom:" +msgctxt "field:cashbook.book,quantity_uom:" msgid "UOM" msgstr "UOM" @@ -34,6 +42,38 @@ msgctxt "field:cashbook.book,symbol:" msgid "Symbol" msgstr "Symbol" +msgctxt "field:cashbook.book,quantity:" +msgid "Quantity" +msgstr "Quantity" + +msgctxt "help:cashbook.book,quantity:" +msgid "Quantity of assets until to date" +msgstr "Quantity of assets until to date" + +msgctxt "field:cashbook.book,quantity_all:" +msgid "Total Quantity" +msgstr "Total Quantity" + +msgctxt "help:cashbook.book,quantity_all:" +msgid "Total quantity of all assets" +msgstr "Total quantity of all assets" + +msgctxt "field:cashbook.book,current_value:" +msgid "Value" +msgstr "Value" + +msgctxt "help:cashbook.book,current_value:" +msgid "Current Value in cashbook currency" +msgstr "Current Value in cashbook currency" + +msgctxt "field:cashbook.book,current_value_ref:" +msgid "Value (Ref)" +msgstr "Value (Ref)" + +msgctxt "help:cashbook.book,current_value:" +msgid "Current Value in company currency" +msgstr "Current Value in company currency" + msgctxt "field:cashbook.line,quantity_digits:" msgid "Digits" msgstr "Digits" diff --git a/tests/test_book.py b/tests/test_book.py index c8899a7..fcc3b01 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -56,6 +56,7 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): pool = Pool() Book = pool.get('cashbook.book') BType = pool.get('cashbook.type') + Asset = pool.get('investment.asset') types = self.prep_type() BType.write(*[ @@ -70,6 +71,22 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): 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') book, = Book.create([{ @@ -77,7 +94,7 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): 'name': 'Book 1', 'btype': types.id, 'company': company.id, - 'currency': company.currency.id, + 'currency': euro.id, 'number_sequ': self.prep_sequence().id, 'asset': asset.id, 'quantity_uom': asset.uom.id, @@ -90,22 +107,161 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): 'amount': Decimal('2.5'), 'party': party.id, 'quantity': Decimal('1.453'), + }, { + 'date': date(2022, 5, 10), + 'description': 'Text 2', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('4.0'), + 'party': party.id, + 'quantity': Decimal('3.3'), }], )], }]) self.assertEqual(book.name, 'Book 1') - self.assertEqual(book.rec_name, 'Book 1 | 2.50 usd | Open') + self.assertEqual(book.rec_name, 'Book 1 | 6.50 € | Open') self.assertEqual(book.state, 'open') self.assertEqual(book.feature, 'asset') self.assertEqual(book.quantity_digits, 3) - self.assertEqual(book.balance_all, Decimal('2.5')) - self.assertEqual(len(book.lines), 1) + self.assertEqual(book.balance_all, Decimal('6.5')) + self.assertEqual(len(book.lines), 2) + self.assertEqual(book.lines[0].amount, Decimal('2.5')) self.assertEqual(book.lines[0].quantity, Decimal('1.453')) self.assertEqual(book.lines[0].quantity_digits, 3) self.assertEqual(book.lines[0].quantity_uom.symbol, 'u') - self.assertEqual(book.symbol, 'usd/u') + + self.assertEqual(book.lines[1].amount, Decimal('4.0')) + self.assertEqual(book.lines[1].quantity, Decimal('3.3')) + self.assertEqual(book.lines[1].quantity_digits, 3) + self.assertEqual(book.lines[1].quantity_uom.symbol, 'u') + + self.assertEqual(book.symbol, '€/u') + self.assertEqual(book.asset.rec_name, 'Product 1 | 2.8000 usd/u | 05/02/2022') + + # check quantities at cashbook + with Transaction().set_context({ + 'qdate': date(2022, 5, 5), + 'company': company.id, + }): + book2, = Book.browse([book]) + self.assertEqual(book.asset.rate, Decimal('2.8')) # usd + self.assertEqual(book2.quantity, Decimal('1.453')) + self.assertEqual(book2.quantity_all, Decimal('4.753')) + # 2.8 / 1.05 * 1.453 = 3.87466 + self.assertEqual(book2.current_value, Decimal('3.87')) + self.assertEqual(book2.current_value_ref, Decimal('3.87')) + + with Transaction().set_context({ + 'qdate': date(2022, 5, 12), + 'company': company.id, + }): + book2, = Book.browse([book]) + self.assertEqual(book2.quantity, Decimal('4.753')) + self.assertEqual(book2.quantity_all, Decimal('4.753')) + # 2.8 / 1.05 * 4.753 = 12.67466 + self.assertEqual(book2.current_value, Decimal('12.67')) + self.assertEqual(book2.current_value_ref, Decimal('12.67')) + + @with_transaction() + def test_assetbook_check_uom_and_currency_convert(self): + """ asset in US$/Ounce, cashbook in EUR/Gram + """ + pool = Pool() + Book = pool.get('cashbook.book') + BType = pool.get('cashbook.type') + Asset = pool.get('investment.asset') + ProdTempl = pool.get('product.template') + Uom = pool.get('product.uom') + + types = self.prep_type() + BType.write(*[ + [types], + { + 'feature': 'asset', + }]) + category = self.prep_category(cattype='in') + + company = self.prep_company() + party = self.prep_party() + asset = self.prep_asset_item( + company=company, + product = self.prep_asset_product(name='Product 1')) + + # set product to ounce + ounce, = Uom.search([('symbol', '=', 'oz')]) + gram, = Uom.search([('symbol', '=', 'g')]) + + ProdTempl.write(*[ + [asset.product.template], + { + 'default_uom': ounce.id, + 'name': 'Aurum', + }]) + + Asset.write(*[ + [asset], + { + 'uom': ounce.id, + 'rates': [('create', [{ + 'date': date(2022, 5, 1), + 'rate': Decimal('1750.0'), + }, ])], + }]) + self.assertEqual(asset.rec_name, 'Aurum | 1,750.0000 usd/oz | 05/01/2022') + + (usd, euro) = self.prep_2nd_currency(company) + self.assertEqual(company.currency.rec_name, 'Euro') + self.assertEqual(asset.symbol, 'usd/oz') + + book, = Book.create([{ + 'start_date': date(2022, 4, 1), + 'name': 'Aurum-Storage', + 'btype': types.id, + 'company': company.id, + 'currency': euro.id, + 'number_sequ': self.prep_sequence().id, + 'asset': asset.id, + 'quantity_uom': gram.id, + 'quantity_digits': 3, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'store some metal', + 'category': category.id, + 'bookingtype': 'in', + 'amount': Decimal('1250.0'), + 'party': party.id, + 'quantity': Decimal('20.0'), + }], + )], + }]) + + self.assertEqual(book.rec_name, 'Aurum-Storage | 1,250.00 € | Open') + self.assertEqual(book.balance_all, Decimal('1250.0')) + self.assertEqual(len(book.lines), 1) + + self.assertEqual(book.lines[0].amount, Decimal('1250.0')) + self.assertEqual(book.lines[0].quantity, Decimal('20.0')) + self.assertEqual(book.lines[0].quantity_uom.symbol, 'g') + + self.assertEqual(book.symbol, '€/g') + self.assertEqual(book.asset.rec_name, 'Aurum | 1,750.0000 usd/oz | 05/01/2022') + + # check quantities at cashbook + with Transaction().set_context({ + 'qdate': date(2022, 5, 1), + 'company': company.id, + }): + book2, = Book.browse([book]) + self.assertEqual(book.asset.rate, Decimal('1750.0')) # usd + self.assertEqual(book2.quantity, Decimal('20.0')) + self.assertEqual(book2.quantity_all, Decimal('20.0')) + # usd --> eur: 1750 / 1.05 = 1666.666 + # 1 ounce --> 20 gram: 1666.666 * 20 / 28.3495 = 1175.7996 + # bette we use 'Troy Ounce': 1 oz.tr. = 31.1034768 gram + self.assertEqual(book2.current_value, Decimal('1175.80')) + self.assertEqual(book2.current_value_ref, Decimal('1175.80')) @with_transaction() def test_assetbook_book_uom(self): diff --git a/view/book_form.xml b/view/book_form.xml index 7a679d5..4b5a9e4 100644 --- a/view/book_form.xml +++ b/view/book_form.xml @@ -4,6 +4,21 @@ The COPYRIGHT file at the top level of this repository contains the full copyright notices and license terms. --> + + + +