Repository: rotki/rotki

Issue:
Exporting PnL CSV for ACB exports different result to the one shown
## Problem Definition

Exporting a PnL report to CSV when having the ACB cost basis method selected seems to export a result different to the one shown in the report. It seems to actually show the calculated result in ACB but export it in FiFo when exporting in CSV

## Task

Find out why and fix.


Discussion:

Resolving patch:
diff --git a/rotkehlchen/accounting/export/csv.py b/rotkehlchen/accounting/export/csv.py
--- a/rotkehlchen/accounting/export/csv.py
+++ b/rotkehlchen/accounting/export/csv.py
@@ -10,7 +10,7 @@
 from rotkehlchen.constants.misc import ZERO
 from rotkehlchen.fval import FVal
 from rotkehlchen.logging import RotkehlchenLogsAdapter
-from rotkehlchen.types import Timestamp
+from rotkehlchen.types import CostBasisMethod, Timestamp
 from rotkehlchen.utils.mixins.customizable_date import CustomizableDateMixin
 from rotkehlchen.utils.version_check import get_current_version
 
@@ -138,19 +138,25 @@ def _add_pnl_type(
         dict_event[f'pnl_{name}'] = equation
         cost_basis = ''
         if event.cost_basis is not None:
-            for acquisition in event.cost_basis.matched_acquisitions:
-                if name == 'taxable' and acquisition.taxable is False:
-                    continue
-                if name == 'free' and acquisition.taxable is True:
-                    continue
-
-                index = acquisition.event.index + CSV_INDEX_OFFSET
-                if cost_basis == '':
-                    cost_basis = '='
-                else:
-                    cost_basis += '+'
-
-                cost_basis += f'{str(acquisition.amount)}*H{index}'
+            if self.settings.cost_basis_method == CostBasisMethod.ACB:
+                # ACB doesn't have matched acquisitions so we use pure numbers.
+                cost_basis = event.cost_basis.taxable_bought_cost if name == 'taxable' else event.cost_basis.taxfree_bought_cost  # type: ignore[assignment]  # noqa: E501
+                cost_basis = str(cost_basis) if cost_basis != 0 else ''
+            else:  # FIFO, LIFO, HIFO (methods that do have matched acquisitions)
+                cost_basis = ''
+                for acquisition in event.cost_basis.matched_acquisitions:
+                    if name == 'taxable' and acquisition.taxable is False:
+                        continue
+                    if name == 'free' and acquisition.taxable is True:
+                        continue
+
+                    index = acquisition.event.index + CSV_INDEX_OFFSET
+                    if cost_basis == '':
+                        cost_basis = '='
+                    else:
+                        cost_basis += '+'
+
+                    cost_basis += f'{str(acquisition.amount)}*H{index}'
 
         dict_event[f'cost_basis_{name}'] = cost_basis
 


Test patch:
diff --git a/rotkehlchen/tests/unit/accounting/test_basic.py b/rotkehlchen/tests/unit/accounting/test_basic.py
--- a/rotkehlchen/tests/unit/accounting/test_basic.py
+++ b/rotkehlchen/tests/unit/accounting/test_basic.py
@@ -17,17 +17,29 @@
 from rotkehlchen.tests.utils.constants import A_CHF, A_XMR
 from rotkehlchen.tests.utils.history import prices
 from rotkehlchen.tests.utils.messages import no_message_errors
-from rotkehlchen.types import Location, Timestamp, TradeType
+from rotkehlchen.types import CostBasisMethod, Location, Timestamp, TradeType
 
 
+@pytest.mark.parametrize(('db_settings', 'expected_pnls'), [
+    (
+        {'cost_basis_method': CostBasisMethod.FIFO},
+        PnlTotals({
+            AccountingEventType.TRADE: PNL(taxable=FVal('559.6947154'), free=ZERO),
+            AccountingEventType.FEE: PNL(taxable=FVal('-0.23886813'), free=ZERO),
+        }),
+    ),
+    (
+        {'cost_basis_method': CostBasisMethod.ACB},
+        PnlTotals({
+            AccountingEventType.TRADE: PNL(taxable=FVal('551.2588760825750541683933333'), free=ZERO),  # noqa: E501
+            AccountingEventType.FEE: PNL(taxable=FVal('-0.2640127597017956302424120028'), free=ZERO),  # noqa: E501
+        }),
+    ),
+])
 @pytest.mark.parametrize('mocked_price_queries', [prices])
-def test_simple_accounting(accountant, google_service):
+def test_simple_accounting(accountant, google_service, expected_pnls):
     accounting_history_process(accountant, 1436979735, 1495751688, history1)
     no_message_errors(accountant.msg_aggregator)
-    expected_pnls = PnlTotals({
-        AccountingEventType.TRADE: PNL(taxable=FVal('559.6947154'), free=ZERO),
-        AccountingEventType.FEE: PNL(taxable=FVal('-0.23886813'), free=ZERO),
-    })
     check_pnls_and_csv(accountant, expected_pnls, google_service)
 
 
diff --git a/rotkehlchen/tests/unit/test_cost_basis.py b/rotkehlchen/tests/unit/test_cost_basis.py
--- a/rotkehlchen/tests/unit/test_cost_basis.py
+++ b/rotkehlchen/tests/unit/test_cost_basis.py
@@ -1,6 +1,12 @@
+import csv
+import tempfile
+from itertools import zip_longest
+from pathlib import Path
+
 import pytest
 
 from rotkehlchen.accounting.cost_basis import AssetAcquisitionEvent
+from rotkehlchen.accounting.export.csv import FILENAME_ALL_CSV
 from rotkehlchen.accounting.mixins.event import AccountingEventType
 from rotkehlchen.accounting.pnl import PNL, PnlTotals
 from rotkehlchen.accounting.structures.balance import Balance
@@ -16,6 +22,8 @@
 from rotkehlchen.tests.utils.factories import make_evm_address, make_random_bytes
 from rotkehlchen.types import CostBasisMethod, Location, Timestamp, make_evm_tx_hash
 
+EXAMPLE_TIMESTAMP = Timestamp(1675483017)
+
 
 def add_acquisition(pot, amount, asset=A_ETH, price=ONE, taxable=False):
     """
@@ -27,7 +35,7 @@ def add_acquisition(pot, amount, asset=A_ETH, price=ONE, taxable=False):
         event_type=AccountingEventType.TRANSACTION_EVENT,
         notes='Test',
         location=Location.BLOCKCHAIN,
-        timestamp=Timestamp(1675483017),
+        timestamp=EXAMPLE_TIMESTAMP,
         asset=asset,
         amount=amount,
         taxable=taxable,
@@ -45,7 +53,7 @@ def add_spend(pot, amount, asset=A_ETH, price=ONE, taxable=True):
         event_type=AccountingEventType.TRANSACTION_EVENT,
         notes='Test',
         location=Location.BLOCKCHAIN,
-        timestamp=Timestamp(1675483017),
+        timestamp=EXAMPLE_TIMESTAMP,
         asset=asset,
         amount=amount,
         taxable=taxable,
@@ -822,12 +830,14 @@ def test_missing_acquisitions(accountant):
     assert cost_basis.missing_acquisitions == expected_missing_acquisitions
 
 
+@pytest.mark.parametrize('db_settings', [
+    {'cost_basis_method': CostBasisMethod.ACB},
+])
 def test_accounting_average_cost_basis(accountant):
     """Test various scenarios in average cost basis calculation"""
     pot = accountant.pots[0]
     events = pot.processed_events
     cost_basis = pot.cost_basis
-    cost_basis.reset(DBSettings(cost_basis_method=CostBasisMethod.ACB))
     manager = cost_basis.get_events(A_ETH).acquisitions_manager
 
     # Step 1. Add an acquisition
@@ -924,6 +934,28 @@ def test_accounting_average_cost_basis(accountant):
     assert manager.current_total_acb == ZERO
     assert manager.current_amount == ZERO
 
+    # Also check that the values in the exported csv match the processed events list.
+    # In case of ACB export cost basis values are pure numbers, with no formulas, so we can check
+    # like this.
+    with tempfile.TemporaryDirectory() as tmpdir:
+        path_dir = Path(tmpdir)
+        accountant.csvexporter.export(
+            events=pot.processed_events,
+            pnls=pot.pnls,
+            directory=path_dir,
+        )
+        with open(path_dir / FILENAME_ALL_CSV) as f:
+            for event, row in zip_longest(events, csv.DictReader(f)):
+                if event.cost_basis is None:
+                    assert row['cost_basis_taxable'] == ''
+                else:
+                    if row['cost_basis_taxable'] == '':
+                        value = ZERO
+                    else:
+                        value = FVal(row['cost_basis_taxable'])
+
+                    assert value == event.cost_basis.taxable_bought_cost
+
 
 @pytest.mark.parametrize('mocked_price_queries', [{
     A_ETH: {A_EUR: {1469020840: ONE}},