diff --git a/book.py b/book.py index fe83a22..6cdcd40 100644 --- a/book.py +++ b/book.py @@ -127,7 +127,9 @@ class Book(SymbolMixin, metaclass=PoolMeta): string='Purchase Amount', help='Total purchase amount, from shares and fees.', readonly=True, digits=(16, Eval('currency_digits', 2)), - states={'invisible': Eval('feature', '') != 'asset'}, + states={'invisible': ~Or( + Eval('feature', '') == 'asset', + ~Bool(Eval('feature')))}, depends=['currency_digits', 'feature']), 'get_asset_quantity', searcher='search_asset_quantity') @@ -555,19 +557,83 @@ class Book(SymbolMixin, metaclass=PoolMeta): where=(tab_type.feature == 'asset')) return (query, tab_book) + @classmethod + def get_asset_amounts_sub_sql(cls): + """ get table of asset and its values for + subordered cashbooks + """ + (tab_quantity, tab_book) = cls.get_asset_quantity_sql() + tab_subids = sub_ids_hierarchical('cashbook.book') + + query = tab_book.join( + tab_subids, + condition=tab_book.id == tab_subids.parent, + ).join( + tab_quantity, + condition=tab_quantity.id == AnyInArray(tab_subids.subids), + ).select( + tab_book.id, + tab_quantity.id.as_('id_subbook'), + tab_quantity.quantity, + tab_quantity.quantity_all, + tab_quantity.rate, + tab_quantity.currency, + tab_quantity.currency_digits, + tab_quantity.asset_currency, + tab_quantity.purchase_amount, + tab_quantity.quantity_uom.as_('book_uom'), + tab_quantity.uom.as_('asset_uom')) + return (query, tab_book) + + @classmethod + def get_generic_amounts_sub_sql(cls): + """ query to get amounts of current and subordered + non-asset cashbooks grouped by currency + """ + pool = Pool() + BType = pool.get('cashbook.type') + tab_btype = BType.__table__() + tab_book1 = cls.__table__() + tab_book2 = cls.__table__() + + subids_book = sub_ids_hierarchical('cashbook.book') + (query_amounts, tab_line) = cls.get_balance_of_cashbook_sql() + + query = tab_book1.join( + subids_book, + condition=subids_book.parent == tab_book1.id, + ).join( + tab_book2, + condition=tab_book2.id == AnyInArray(subids_book.subids), + ).join( + tab_btype, + condition=( + tab_btype.id == tab_book2.btype) & ( + tab_btype.feature != 'asset'), + ).join( + query_amounts, + condition=query_amounts.cashbook == tab_book2.id, + ).select( + tab_book1.id, + Sum(query_amounts.balance).as_('balance'), + Sum(query_amounts.balance_all).as_('balance_all'), + query_amounts.currency, + group_by=[tab_book1.id, query_amounts.currency]) + return (query, tab_book1) + @classmethod def get_asset_quantity_values(cls, cashbooks, names): """ get quantities field: quantity, quantity_all, current_value, current_value_ref, diff_amount, diff_percent, - current_rate, purchase_amount + current_rate, purchase_amount, + include subordered cashbooks for cashbooks w/o btype """ pool = Pool() CBook = pool.get('cashbook.book') Uom = pool.get('product.uom') Currency = pool.get('currency.currency') cursor = Transaction().connection.cursor() - (query, tab_book) = cls.get_asset_quantity_sql() company_currency = CBook.default_currency() result = { @@ -575,157 +641,187 @@ class Book(SymbolMixin, metaclass=PoolMeta): for x in [ 'quantity', 'quantity_all', 'current_value', 'current_value_ref', 'diff_amount', 'diff_percent', - 'current_rate', 'purchase_amount', 'purchase_amount_ref', - 'digits'] + 'current_rate', 'purchase_amount', 'digits'] } def values_from_record(rdata): """ compute values for record """ # uom-factor - if rdata[6] == rdata[7]: + if rdata['asset_uom'] == rdata['book_uom']: uom_factor = Decimal('1.0') else: uom_factor = Decimal( Uom.compute_qty( - Uom(rdata[6]), 1.0, - Uom(rdata[7]), round=False)) + Uom(rdata['asset_uom']), 1.0, + Uom(rdata['book_uom']), round=False)) current_value = Currency.compute( - rdata[8], - rdata[3] * rdata[1] / uom_factor, - rdata[4]) - return (record[0], { - 'quantity': rdata[1], - 'quantity_all': rdata[2], + rdata['asset_currency'], + rdata['rate'] * rdata['quantity'] / uom_factor, + rdata['book_currency']) + return (rdata['id'], { + 'quantity': rdata['quantity'], + 'quantity_all': rdata['quantity_all'], 'current_value': current_value, 'current_value_ref': Currency.compute( - rdata[8], - rdata[3] * rdata[1] / uom_factor, + rdata['asset_currency'], + rdata['rate'] * rdata['quantity'] / uom_factor, company_currency - if company_currency is not None else rdata[8]), - 'diff_amount': current_value - rdata[9], + if company_currency is not None + else rdata['asset_currency']), + 'diff_amount': current_value - rdata['purchase_amount'], 'diff_percent': ( Decimal('100.0') * current_value / - rdata[9] - Decimal('100.0') - ).quantize(Decimal(str(1/10**rdata[5]))) - if rdata[9] != Decimal('0.0') else None, + rdata['purchase_amount'] - Decimal('100.0') + ).quantize(Decimal(str(1/10**rdata['digits']))) + if rdata['purchase_amount'] != Decimal('0.0') else None, 'current_rate': ( - current_value / rdata[1] - ).quantize(Decimal(str(1/10**rdata[5]))) - if rdata[1] != Decimal('0.0') else None, - 'purchase_amount': record[9].quantize( - Decimal(str(1/10**rdata[5]))), - 'purchase_amount_ref': Currency.compute( - rdata[4], - record[9], - company_currency - if company_currency is not None else rdata[4]), + current_value / rdata['quantity'] + ).quantize(Decimal(str(1/10**rdata['digits']))) + if rdata['quantity'] != Decimal('0.0') else None, + 'purchase_amount': rdata['purchase_amount'].quantize( + Decimal(str(1/10**rdata['digits']))), }) - ids_assetbooks = [] - 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) + view_cashbook_ids = list({ + x.id for x in cashbooks if x.feature is None}) + asset_cashbook_ids = list({ + x.id for x in cashbooks if x.feature == 'asset'}) + generic_cashbooks = list({ + x for x in cashbooks if x.feature == 'gen'}) - # get values of asset-cashbooks in 'cashbooks' of type=asset - if len(ids_assetbooks) > 0: - query.where &= tab_book.id.in_(ids_assetbooks) + # check skipped cashbooks + assert list({ + x.id + for x in cashbooks + if x.feature not in ['asset', 'gen', None] + }) == [], 'unknown feature of cashbook' + + # get values of asset-cashbooks in 'cashbooks' of type=asset, + # values of current cashbook + if asset_cashbook_ids: + (query, tab_book) = cls.get_asset_quantity_sql() + query.where &= tab_book.id.in_(asset_cashbook_ids) cursor.execute(*query) records = cursor.fetchall() for record in records: - (book_id, values) = values_from_record(record) + (book_id, values) = values_from_record({ + 'id': record[0], + 'quantity': record[1], + 'quantity_all': record[2], + 'rate': record[3], + 'book_currency': record[4], + 'digits': record[5], + 'asset_uom': record[6], + 'book_uom': record[7], + 'asset_currency': record[8], + 'purchase_amount': record[9]}) for name in values.keys(): result[name][book_id] = values[name] - # add aggregated values of cashbooks without type - aggr_names = [ + enable_byfields = set({ 'current_value', 'current_value_ref', - 'diff_amount', 'diff_percent'] - queried_names = list(set(aggr_names).intersection(set(names))) + 'purchase_amount'}).intersection(set(names)) - if (len(queried_names) > 0) and (len(ids_nonebtypes) > 0): - # query all subordered asset-cashbooks to get values for - # cashbooks without type + # add values of current generic-cashbooks + if generic_cashbooks and enable_byfields: + fnames = [ + ('current_value', 'balance'), + ('current_value_ref', 'balance_ref'), + ('purchase_amount', 'balance')] + for generic_cashbook in generic_cashbooks: + for fname in fnames: + (fn_to, fn_from) = fname + if fn_to in names: + result[fn_to][generic_cashbook.id] = getattr( + generic_cashbook, fn_from) - (tab_quantity, tab_book) = cls.get_asset_quantity_sql() - tab_subids = sub_ids_hierarchical('cashbook.book') - query = tab_book.join( - tab_subids, - condition=tab_book.id == tab_subids.parent, - ).join( - tab_quantity, - condition=tab_quantity.id == AnyInArray(tab_subids.subids), - ).select( - tab_book.id, - Sum(tab_quantity.quantity), # 1 - Sum(tab_quantity.quantity_all), # 2 - tab_quantity.rate, # 3 - tab_quantity.currency, # 4 - tab_quantity.currency_digits, # 5 - tab_quantity.uom, # 6 - tab_quantity.quantity_uom, # 7 - tab_quantity.asset_currency, # 8 - Sum(tab_quantity.purchase_amount), # 9 - tab_book.currency.as_('currency_book'), # 10 - where=tab_book.id.in_(ids_nonebtypes), - group_by=[ - tab_book.id, tab_quantity.rate, - tab_quantity.currency, tab_quantity.currency_digits, - tab_quantity.uom, tab_quantity.quantity_uom, - tab_quantity.asset_currency, tab_book.currency]) - cursor.execute(*query) + # add amounts of non-asset cashbooks, + if view_cashbook_ids and enable_byfields: + + # sum amounts of subordered generic-cashbooks + (query_nonasset, tab_book) = cls.get_generic_amounts_sub_sql() + query_nonasset.where = tab_book.id.in_(view_cashbook_ids) + cursor.execute(*query_nonasset) records = cursor.fetchall() for record in records: - (book_id, values) = values_from_record(record) + cbook = CBook(record[0]) + rdata = { + 'id': record[0], + 'quantity': Decimal('1.0'), + 'quantity_all': None, + 'rate': record[1], # balance + 'book_currency': cbook.currency.id, + 'digits': cbook.currency.digits, + 'asset_uom': 0, + 'book_uom': 0, + 'asset_currency': record[3], + 'purchase_amount': record[1]} + (book_id, values) = values_from_record(rdata) for name in [ - 'current_value', 'diff_amount', - 'current_value_ref', 'purchase_amount_ref']: + ('current_value', 'current_value'), + ('current_value_ref', 'current_value_ref'), + ('purchase_amount', 'current_value')]: + (fn_to, fn_from) = name + + if fn_to in names: + if result[fn_to][book_id] is None: + result[fn_to][book_id] = Decimal('0.0') + result[fn_to][book_id] += values[fn_from] + + # sum amounts of subordered asset-cashbooks + (query_subbooks, tab_book) = cls.get_asset_amounts_sub_sql() + query_subbooks.where = tab_book.id.in_(view_cashbook_ids) + cursor.execute(*query_subbooks) + records = cursor.fetchall() + + for record in records: + cbook = CBook(record[0]) + (book_id, values) = values_from_record({ + 'id': record[0], + 'quantity': record[2], + 'quantity_all': record[3], + 'rate': record[4], + 'book_currency': record[5], + 'digits': record[6], + 'asset_uom': record[10], + 'book_uom': record[9], + 'asset_currency': record[7], + 'purchase_amount': record[8]}) + + for x in ['current_value', 'purchase_amount']: + values[x] = Currency.compute( + record[5], values[x], cbook.currency.id) + + for name in [ + 'current_value', 'current_value_ref', + 'purchase_amount']: if result[name][book_id] is None: result[name][book_id] = Decimal('0.0') - - value = Decimal('0.0') - if name == 'current_value': - value = Currency.compute( - company_currency - if company_currency is not None else record[4], - values['current_value_ref'], - record[10]) - elif name == 'diff_amount': - value = Currency.compute( - company_currency - if company_currency is not None else record[4], - values['current_value_ref'] - - values['purchase_amount_ref'], - record[10]) - elif name in ['current_value_ref', 'purchase_amount_ref']: - value = values[name] - result['digits'][book_id] = record[5] - result[name][book_id] += value + result[name][book_id] += values[name] # diff_percent - for id_book in ids_nonebtypes: - c_val = result['current_value_ref'][id_book] - p_amount = result['purchase_amount_ref'][id_book] - digits = result['digits'][id_book] + for id_book in view_cashbook_ids: + c_val = result['current_value'][id_book] + p_amount = result['purchase_amount'][id_book] + digits = result['digits'][id_book] or 2 if (p_amount == Decimal('0.0')) or \ (p_amount is None) or (c_val is None): continue + result['diff_amount'][id_book] = ( + c_val - p_amount).quantize(Decimal(str(1/10 ** digits))) result['diff_percent'][id_book] = ( Decimal('100.0') * c_val / p_amount - Decimal('100.0') ).quantize(Decimal(str(1/10 ** digits))) result['digits'][id_book] = None + return {x: result[x] for x in names} @classmethod diff --git a/tests/book.py b/tests/book.py index 3c89723..e4b0ae6 100644 --- a/tests/book.py +++ b/tests/book.py @@ -49,7 +49,7 @@ class CbInvTestCase(object): self.assertEqual(book.show_performance, True) # run sorter - books = Book.search( + Book.search( [], order=[ ('current_value', 'ASC'), @@ -103,6 +103,12 @@ class CbInvTestCase(object): asset.rec_name, 'Product 1 | 12.5000 usd/u | 05/02/2022') (usd, euro) = self.prep_2nd_currency(company) + self.assertEqual(len(usd.rates), 1) + self.assertEqual(usd.rates[0].rate, Decimal('1.05')) + self.assertEqual(usd.rates[0].date, date(2022, 5, 2)) + self.assertEqual(euro.rates[0].rate, Decimal('1.0')) + self.assertEqual(euro.rates[0].date, date(2022, 5, 2)) + self.assertEqual(company.currency.rec_name, 'Euro') BType.write(*[ @@ -182,26 +188,35 @@ class CbInvTestCase(object): }])], }])], }]) + self.prep_valstore_run_worker() + 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')) + # current_value: + # +15€ (15.00€ - L1-Euro-Cash) + # +15$ / 1.05 (14.29€ - L1-USD-Cash) + # +12.5$/1.05 (11.90€ - L1-Euro-Depot) + # +12.5$/1.05 (11.90€ - L1-USD-Depot) + # = 53.09€ + self.assertEqual(books[0].current_value, Decimal('53.09')) + self.assertEqual(books[0].current_value_ref, Decimal('53.09')) + self.assertEqual(books[0].purchase_amount, Decimal('58.58')) 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('-9.37')) # searcher self.assertEqual(Book.search_count([ - ('current_value', '=', Decimal('23.8'))]), 1) + ('current_value', '=', Decimal('53.09'))]), 1) self.assertEqual(Book.search_count([ - ('current_value_ref', '=', Decimal('23.8'))]), 1) + ('current_value_ref', '=', Decimal('53.09'))]), 1) self.assertEqual(Book.search_count([ ('diff_amount', '=', Decimal('-5.49'))]), 1) self.assertEqual(Book.search_count([ - ('diff_percent', '=', Decimal('-18.74'))]), 1) + ('diff_percent', '=', Decimal('-9.37'))]), 1) self.assertEqual(Book.search_count([ ('quantity', '=', Decimal('1.0'))]), 2) self.assertEqual(Book.search_count([ @@ -209,15 +224,17 @@ class CbInvTestCase(object): self.assertEqual(Book.search_count([ ('current_rate', '=', Decimal('11.9'))]), 1) self.assertEqual(Book.search_count([ - ('purchase_amount', '=', Decimal('15.0'))]), 2) + ('purchase_amount', '=', Decimal('15.0'))]), 4) 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].current_value, Decimal('15.0')) + self.assertEqual( + books[0].childs[0].current_value_ref, Decimal('15.0')) self.assertEqual(books[0].childs[0].diff_amount, None) self.assertEqual(books[0].childs[0].diff_percent, None) @@ -227,6 +244,7 @@ class CbInvTestCase(object): self.assertEqual( books[0].childs[1].asset.rec_name, 'Product 1 | 12.5000 usd/u | 05/02/2022') + # asset: usd, rate 12.50 usd/u @ 2022-05-02 self.assertEqual( books[0].childs[1].current_value, Decimal('11.9')) self.assertEqual( @@ -239,8 +257,10 @@ class CbInvTestCase(object): 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].current_value, Decimal('15.0')) + self.assertEqual( + books[0].childs[2].current_value_ref, Decimal('14.29')) self.assertEqual(books[0].childs[2].diff_amount, None) self.assertEqual(books[0].childs[2].diff_percent, None) diff --git a/view/book_list.xml b/view/book_list.xml index f874535..6ef98ee 100644 --- a/view/book_list.xml +++ b/view/book_list.xml @@ -6,6 +6,7 @@ full copyright notices and license terms. --> + diff --git a/view/book_tree.xml b/view/book_tree.xml index ce3eddc..c822229 100644 --- a/view/book_tree.xml +++ b/view/book_tree.xml @@ -6,6 +6,7 @@ full copyright notices and license terms. --> +