diff --git a/book.py b/book.py index 2858b37..83c8f22 100644 --- a/book.py +++ b/book.py @@ -81,18 +81,18 @@ class Book(SymbolMixin, metaclass=PoolMeta): help='Valuation of the investment based on the current stock market price.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ - 'invisible': Eval('feature', '') != 'asset', - }, depends=['currency_digits', 'feature']), + 'invisible': Eval('show_performance', False) == False, + }, depends=['currency_digits', 'show_performance']), 'get_asset_quantity') current_value_ref = fields.Function(fields.Numeric(string='Value (Ref.)', help='Valuation of the investment based on the current stock exchange price, converted into the company currency.', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ 'invisible': Or( - Eval('feature', '') != 'asset', + Eval('show_performance', False) == False, ~Bool(Eval('company_currency', -1)), ), - }, depends=['currency_digits', 'feature', 'company_currency']), + }, depends=['currency_digits', 'show_performance', 'company_currency']), 'get_asset_quantity') # performance @@ -100,14 +100,16 @@ class Book(SymbolMixin, metaclass=PoolMeta): help='Difference between acquisition value and current value', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ - 'invisible': Eval('feature', '') != 'asset', - }, depends=['currency_digits', 'feature']), 'get_asset_quantity') + 'invisible': Eval('show_performance', False) == False, + }, depends=['currency_digits', 'show_performance']), 'get_asset_quantity') diff_percent = fields.Function(fields.Numeric(string='Percent', help='percentage performance since acquisition', readonly=True, digits=(16, Eval('currency_digits', 2)), states={ - 'invisible': Eval('feature', '') != 'asset', - }, depends=['currency_digits', 'feature']), 'get_asset_quantity') + 'invisible': Eval('show_performance', False) == False, + }, depends=['currency_digits', 'show_performance']), 'get_asset_quantity') + show_performance = fields.Function(fields.Boolean(string='Performance', + readonly=True), 'on_change_with_show_performance') current_rate = fields.Function(fields.Numeric(string='Rate', help='Rate per unit of investment based on current stock exchange price.', readonly=True, digits=(16, Eval('currency_digits', 2)), @@ -119,7 +121,7 @@ class Book(SymbolMixin, metaclass=PoolMeta): def view_attributes(cls): return super(Book, cls).view_attributes() + [ ('/tree', 'visual', - If(Eval('feature', '') == 'asset', + If(Eval('show_performance', False) == True, If(Eval('diff_percent', 0) < 0, 'danger', If(Eval('diff_percent', 0) > 0, 'success', '') ), '') @@ -220,54 +222,143 @@ class Book(SymbolMixin, metaclass=PoolMeta): context = Transaction().context (query, tab_book) = cls.get_asset_quantity_sql() - result = {x:{y.id: None for y in cashbooks} for x in names} company_currency = CBook.default_currency() + result = {x:{y.id: None for y in cashbooks} for x in names} - query.where &= tab_book.id.in_([x.id for x in cashbooks]) & \ - (tab_book.btype != None) - cursor.execute(*query) - records = cursor.fetchall() - - for record in records: + def values_from_record(rdata): + """ compute values for record + """ # uom-factor - if record[6] == record[7]: + if rdata[6] == rdata[7]: uom_factor = Decimal('1.0') else : uom_factor = Decimal( - Uom.compute_qty(Uom(record[6]), 1.0, Uom(record[7]), round=False) + Uom.compute_qty(Uom(rdata[6]), 1.0, Uom(rdata[7]), round=False) ) current_value = Currency.compute( - record[8], - record[3] * record[1] / uom_factor, - record[4] + rdata[8], + rdata[3] * rdata[1] / uom_factor, + rdata[4] ) - - values = { - 'quantity': record[1], - 'quantity_all': record[2], + return (record[0], { + 'quantity': rdata[1], + 'quantity_all': rdata[2], 'current_value': current_value, 'current_value_ref': Currency.compute( - record[8], - record[3] * record[1] / uom_factor, - company_currency if company_currency is not None else record[8], + rdata[8], + rdata[3] * rdata[1] / uom_factor, + company_currency if company_currency is not None else rdata[8], ), - 'diff_amount': current_value - record[9], + 'diff_amount': current_value - rdata[9], 'diff_percent': ( Decimal('100.0') * current_value / \ - record[9] - Decimal('100.0') - ).quantize(Decimal(str(1/10**record[5]))) \ - if record[9] != Decimal('0.0') else None, + rdata[9] - Decimal('100.0') + ).quantize(Decimal(str(1/10**rdata[5]))) \ + if rdata[9] != Decimal('0.0') else None, 'current_rate': ( - current_value / record[1] - ).quantize(Decimal(str(1/10**record[5]))) \ - if record[1] != Decimal('0.0') else None, - } + current_value / rdata[1] + ).quantize(Decimal(str(1/10**rdata[5]))) \ + if rdata[1] != Decimal('0.0') else None, + }) + + result_cache = {} + ids_assetbooks = [x.id for x in cashbooks if x.btype is not None] + ids_nonebtypes = [x.id for x in cashbooks if x.btype is None] + + # get values of asset-cashbooks in 'cashbooks' of type=asset + if len(ids_assetbooks) > 0: + query.where &= tab_book.id.in_(ids_assetbooks) + cursor.execute(*query) + records = cursor.fetchall() + + for record in records: + (book_id, values) = values_from_record(record) + result_cache[book_id] = values + result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref + + for name in names: + result[name][book_id] = values[name] + + # add aggregated values of cashbooks without type + aggr_names = ['current_value', 'current_value_ref', + 'diff_amount', 'diff_percent'] + queried_names = list(set(aggr_names).intersection(set(names))) + + if len(queried_names) > 0: + # query all subordered asset-cashbooks for + # btype=None-cashbooks + query1 = [('btype.feature', '=', 'asset'), + ('parent', 'child_of', ids_nonebtypes)] + if len(result_cache.keys()) > 0: + query1.append(('id', 'not in', result_cache.keys())) + books_query = CBook.search(query1, query=True) + + # add results to cache + (query, tab_book) = cls.get_asset_quantity_sql() + query.where &= tab_book.id.in_(books_query) + cursor.execute(*query) + records = cursor.fetchall() + + for record in records: + (book_id, values) = values_from_record(record) + result_cache[book_id] = values + result_cache[book_id]['balance_ref'] = CBook(book_id).balance_ref + + # aggregate sub-cashbooks to requested cashbooks from cache + for id_none in ids_nonebtypes: + records = CBook.search([ + ('btype.feature', '=', 'asset'), + ('parent', 'child_of', [id_none]) + ]) + + values = {x:Decimal('0.0') for x in aggr_names+['balance_ref']} + for record in records: + for name in aggr_names+['balance_ref']: + values[name] += \ + result_cache.get(record.id, {}).get(name, Decimal('0.0')) + + # convert current-value-ref in company-currency to + # currency of current cashbook + cbook = CBook(id_none) + values['current_value'] = Currency.compute( + company_currency if company_currency is not None else cbook.currency, + values['current_value_ref'], + cbook.currency, + ) + + values['diff_amount'] = Currency.compute( + company_currency if company_currency is not None else cbook.currency, + values['current_value_ref'] - values['balance_ref'], + cbook.currency, + ) + + values['diff_percent'] = \ + (Decimal('100.0') * values['current_value_ref'] / \ + values['balance_ref'] - Decimal('100.0') + ).quantize( + Decimal(str(1/10**cbook.currency_digits)) + ) if values['balance_ref'] != Decimal('0.0') else None + + for name in queried_names: + result[name][id_none] = values[name] - for name in names: - result[name][record[0]] = values[name] return result + @fields.depends('id') + def on_change_with_show_performance(self, name=None): + """ return True if current or subordered cashbooks + are of type=asset + """ + Book2 = Pool().get('cashbook.book') + + if Book2.search_count([ + ('btype.feature', '=', 'asset'), + ('parent', 'child_of', [self.id]), + ]) > 0: + return True + return False + @fields.depends('id') def on_change_with_asset_symbol(self, name=None): """ get current cashbook to enable usage of 'symbol' diff --git a/tests/test_book.py b/tests/test_book.py index dc69b01..d3dc2c3 100644 --- a/tests/test_book.py +++ b/tests/test_book.py @@ -48,6 +48,168 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): self.assertEqual(book.state_string, 'Open') self.assertEqual(book.feature, 'asset') self.assertEqual(book.quantity_digits, 4) + self.assertEqual(book.show_performance, True) + + @with_transaction() + def test_assetbook_aggregated_values(self): + """ create cashbooks with hierarchy, add lines, + check values at non-type-books + """ + pool = Pool() + Book = pool.get('cashbook.book') + BType = pool.get('cashbook.type') + Asset = pool.get('investment.asset') + + company = self.prep_company() + type_depot = self.prep_type('Depot', 'D') + type_cash = self.prep_type('Cash', 'C') + category_in = self.prep_category(cattype='in') + + asset = self.prep_asset_item( + company=company, + product = self.prep_asset_product(name='Product 1')) + self.assertEqual(asset.symbol, 'usd/u') + + Asset.write(*[ + [asset], + { + 'rates': [('create', [{ + 'date': date(2022, 5, 1), + 'rate': Decimal('10.0'), + }, { + 'date': date(2022, 5, 2), + 'rate': Decimal('12.5'), + }])], + }]) + self.assertEqual(asset.rec_name, 'Product 1 | 12.5000 usd/u | 05/02/2022') + + (usd, euro) = self.prep_2nd_currency(company) + self.assertEqual(company.currency.rec_name, 'Euro') + + BType.write(*[ + [type_depot], + { + 'feature': 'asset', + }]) + + with Transaction().set_context({ + 'company': company.id, + }): + books = Book.create([{ + 'name': 'L0-Euro-None', + 'btype': None, + 'company': company.id, + 'currency': euro.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'childs': [('create', [{ + 'name': 'L1-Euro-Cash', + 'btype': type_cash.id, + 'company': company.id, + 'currency': euro.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Cat In', + 'category': category_in.id, + 'bookingtype': 'in', + 'amount': Decimal('15.0'), + }])], + }, { + 'name': 'L1-USD-Cash', + 'btype': type_cash.id, + 'company': company.id, + 'currency': usd.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Cat In', + 'category': category_in.id, + 'bookingtype': 'in', + 'amount': Decimal('15.0'), # 14.29 € + }])], + }, { + 'name': 'L1-Euro-Depot', + 'btype': type_depot.id, + 'company': company.id, + 'currency': euro.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'asset': asset.id, + 'quantity_uom': asset.uom.id, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Cat In', + 'category': category_in.id, + 'bookingtype': 'in', + 'amount': Decimal('15.0'), + 'quantity': Decimal('1.0'), + }])], + }, { + 'name': 'L1-USD-Depot', + 'btype': type_depot.id, + 'company': company.id, + 'currency': usd.id, + 'number_sequ': self.prep_sequence().id, + 'start_date': date(2022, 5, 1), + 'asset': asset.id, + 'quantity_uom': asset.uom.id, + 'lines': [('create', [{ + 'date': date(2022, 5, 1), + 'description': 'Cat In', + 'category': category_in.id, + 'bookingtype': 'in', + 'amount': Decimal('15.0'), # 14.29 € + 'quantity': Decimal('1.0'), + }])], + }])], + }]) + self.assertEqual(len(books), 1) + self.assertEqual(books[0].rec_name, 'L0-Euro-None') + self.assertEqual(books[0].balance, Decimal('58.57')) + self.assertEqual(books[0].balance_ref, Decimal('58.57')) + # balance of asset-books: 29,286 € + # value of asset-books: 11.9€ + 12.5USD/1.05 = 23.8€ + self.assertEqual(books[0].current_value, Decimal('23.8')) + self.assertEqual(books[0].current_value_ref, Decimal('23.8')) + self.assertEqual(books[0].diff_amount, Decimal('-5.49')) + self.assertEqual(books[0].diff_percent, Decimal('-18.74')) + + self.assertEqual(len(books[0].childs), 4) + + self.assertEqual(books[0].childs[0].rec_name, + 'L0-Euro-None/L1-Euro-Cash | 15.00 € | Open') + self.assertEqual(books[0].childs[0].current_value, None) + self.assertEqual(books[0].childs[0].current_value_ref, None) + self.assertEqual(books[0].childs[0].diff_amount, None) + self.assertEqual(books[0].childs[0].diff_percent, None) + + self.assertEqual(books[0].childs[1].rec_name, + 'L0-Euro-None/L1-Euro-Depot | 15.00 € | Open | 1.0000 u') + self.assertEqual(books[0].childs[1].asset.rec_name, + 'Product 1 | 12.5000 usd/u | 05/02/2022') + self.assertEqual(books[0].childs[1].current_value, Decimal('11.9')) + self.assertEqual(books[0].childs[1].current_value_ref, Decimal('11.9')) + self.assertEqual(books[0].childs[1].diff_amount, Decimal('-3.1')) + self.assertEqual(books[0].childs[1].diff_percent, Decimal('-20.67')) + + self.assertEqual(books[0].childs[2].rec_name, + 'L0-Euro-None/L1-USD-Cash | 15.00 usd | Open') + self.assertEqual(books[0].childs[2].current_value, None) + self.assertEqual(books[0].childs[2].current_value_ref, None) + self.assertEqual(books[0].childs[2].diff_amount, None) + self.assertEqual(books[0].childs[2].diff_percent, None) + + self.assertEqual(books[0].childs[3].rec_name, + 'L0-Euro-None/L1-USD-Depot | 15.00 usd | Open | 1.0000 u') + self.assertEqual(books[0].childs[3].asset.rec_name, + 'Product 1 | 12.5000 usd/u | 05/02/2022') + self.assertEqual(books[0].childs[3].current_value, Decimal('12.5')) + self.assertEqual(books[0].childs[3].current_value_ref, Decimal('11.9')) + self.assertEqual(books[0].childs[3].diff_amount, Decimal('-2.5')) + self.assertEqual(books[0].childs[3].diff_percent, Decimal('-16.67')) @with_transaction() def test_assetbook_create_line(self): @@ -576,6 +738,7 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): 'number_sequ': self.prep_sequence().id, 'start_date': date(2022, 5, 1), }]) + self.assertEqual(book2.show_performance, True) book, = Book.create([{ 'name': 'Book 1', @@ -594,6 +757,7 @@ class CbInvTestCase(CashbookTestCase, InvestmentTestCase): 'quantity': Decimal('1.5'), }])], }]) + self.assertEqual(book.show_performance, False) self.assertEqual(book.rec_name, 'Book 1 | -1.00 usd | Open') self.assertEqual(len(book.lines), 1) self.assertEqual(book.lines[0].quantity, Decimal('1.5'))