Example #1
0
def test_delisting(prepare_db_fifo):
    create_stocks([(4, 'A', 'A SHARE')], currency_id=2)

    test_corp_actions = [(1622548800, CorporateAction.Delisting, 4, 100.0, 4,
                          0.0, 1.0, 'Delisting 100 A')]
    create_corporate_actions(1, test_corp_actions)

    test_trades = [(1619870400, 1619870400, 4, 100.0, 10.0, 0.0
                    )  # Buy  100 A x 10.00 01/05/2021
                   ]
    create_trades(1, test_trades)

    # Build ledger
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    assert readSQL("SELECT * FROM deals_ext WHERE asset_id=4") == [
        1, 'Inv. Account', 4, 'A', 1619870400, 1622548800, 10.0, 10.0, 100.0,
        0.0, 0.0, 0.0, -5
    ]
    assert readSQL(
        "SELECT * FROM ledger_totals WHERE asset_id=4 ORDER BY id DESC LIMIT 1"
    ) == [5, 5, 1, 1622548800, 4, 4, 1, 0.0, 0.0]
    assert readSQL("SELECT * FROM ledger WHERE book_account=1") == [
        6, 1622548800, 5, 1, 1, 2, 1, 1000.0, 0.0, 1000.0, 0.0, 1, 9, ''
    ]
Example #2
0
def test_symbol_change(prepare_db_fifo):
    # Prepare trades and corporate actions setup
    test_assets = [(4, 'A', 'A SHARE'), (5, 'B', 'B SHARE')]
    create_stocks(test_assets, currency_id=2)

    test_corp_actions = [(1622548800, CorporateAction.SymbolChange, 4, 100.0,
                          5, 100.0, 1.0, 'Symbol change 100 A -> 100 B')]
    create_corporate_actions(1, test_corp_actions)

    test_trades = [
        (1619870400, 1619870400, 4, 100.0, 10.0,
         0.0),  # Buy  100 A x 10.00 01/05/2021
        (1625140800, 1625140800, 5, -100.0, 20.0, 0.0
         )  # Sell 100 B x 20.00 01/07/2021
    ]
    create_trades(1, test_trades)

    # Build ledgerye
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    assert readSQL("SELECT * FROM deals_ext WHERE asset_id=4") == [
        1, 'Inv. Account', 4, 'A', 1619870400, 1622548800, 10.0, 10.0, 100.0,
        0.0, 0.0, 0.0, -3
    ]
    assert readSQL("SELECT * FROM deals_ext WHERE asset_id=5") == [
        1, 'Inv. Account', 5, 'B', 1622548800, 1625140800, 10.0, 20.0, 100.0,
        0.0, 1000.0, 100.0, 3
    ]
Example #3
0
def test_taxes_rus_bonds(tmp_path, project_root, data_path, prepare_db_taxes):
    with open(data_path + 'ibkr_bond.json', 'r') as json_file:
        statement = json.load(json_file)
    with open(data_path + 'taxes_bond_rus.json', 'r') as json_file:
        report = json.load(json_file)

    usd_rates = [
        (1632441600, 72.7245), (1629936000, 73.7428), (1631664000, 72.7171), (1622073600, 73.4737),
        (1621987200, 73.3963), (1621900800, 73.5266), (1621641600, 73.5803), (1632528000, 73.0081)
    ]
    create_quotes(2, 1, usd_rates)

    IBKR = StatementIBKR()
    IBKR.load(data_path + 'ibkr_bond.xml')
    assert IBKR._data == statement

    IBKR.validate_format()
    IBKR.match_db_ids()
    IBKR.import_into_db()

    ledger = Ledger()  # Build ledger to have FIFO deals table
    ledger.rebuild(from_timestamp=0)

    taxes = TaxesRus()
    tax_report = taxes.prepare_tax_report(2021, 1)
    assert tax_report == report
def test_symbol_change(prepare_db_fifo):
    # Prepare trades and corporate actions setup
    test_assets = [(4, 'A', 'A SHARE'), (5, 'B', 'B SHARE')]
    for asset in test_assets:
        assert executeSQL(
            "INSERT INTO assets (id, name, type_id, full_name) "
            "VALUES (:id, :name, :type, :full_name)",
            [(":id", asset[0]), (":name", asset[1]),
             (":type", PredefinedAsset.Stock), (":full_name", asset[2])],
            commit=True) is not None

    test_corp_actions = [(1, 1622548800, 3, 4, 100.0, 5, 100.0, 1.0,
                          'Symbol change 100 A -> 100 B')]
    for action in test_corp_actions:
        assert executeSQL(
            "INSERT INTO corp_actions "
            "(id, timestamp, account_id, type, asset_id, qty, asset_id_new, qty_new, basis_ratio, note) "
            "VALUES (:id, :timestamp, 1, :type, :a_o, :q_o, :a_n, :q_n, :ratio, :note)",
            [(":id", action[0]), (":timestamp", action[1]),
             (":type", action[2]), (":a_o", action[3]), (":q_o", action[4]),
             (":a_n", action[5]), (":q_n", action[6]), (":ratio", action[7]),
             (":note", action[8])],
            commit=True) is not None

    test_trades = [
        (1, 1619870400, 1619870400, 4, 100.0, 10.0,
         0.0),  # Buy  100 A x 10.00 01/05/2021
        (2, 1625140800, 1625140800, 5, -100.0, 20.0, 0.0
         )  # Sell 100 B x 20.00 01/07/2021
    ]
    for trade in test_trades:
        assert executeSQL(
            "INSERT INTO trades (id, timestamp, settlement, account_id, asset_id, qty, price, fee) "
            "VALUES (:id, :timestamp, :settlement, 1, :asset, :qty, :price, :fee)",
            [(":id", trade[0]), (":timestamp", trade[1]),
             (":settlement", trade[2]),
             (":asset", trade[3]), (":qty", trade[4]), (":price", trade[5]),
             (":fee", trade[6])]) is not None

    # Build ledgerye
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    assert readSQL("SELECT * FROM deals_ext WHERE asset_id=4") == [
        1, 'Inv. Account', 4, 'A', 1619870400, 1622548800, 10.0, 10.0, 100.0,
        0.0, 0.0, 0.0, -3
    ]
    assert readSQL("SELECT * FROM deals_ext WHERE asset_id=5") == [
        1, 'Inv. Account', 5, 'B', 1622548800, 1625140800, 10.0, 20.0, 100.0,
        0.0, 1000.0, 100.0, 3
    ]
Example #5
0
def test_spin_off(prepare_db_fifo):
    # Prepare trades and corporate actions setup
    test_assets = [(4, 'A', 'A SHARE'), (5, 'B', 'B SHARE')]
    create_stocks(test_assets, currency_id=2)

    test_corp_actions = [
        (1622548800, CorporateAction.SpinOff, 4, 100.0, 5, 5.0, 1.0,
         'Spin-off 5 B from 100 A'),  # 01/06/2021, cost basis 0.0
        (1627819200, CorporateAction.Split, 4, 104.0, 4, 13.0, 1.0,
         'Split A 104 -> 13')  # 01/08/2021
    ]
    create_corporate_actions(1, test_corp_actions)

    test_trades = [
        (1619870400, 1619870400, 4, 100.0, 14.0,
         0.0),  # Buy 100 A x 14.00 01/05/2021
        (1625140800, 1625140800, 4, 4.0, 13.0,
         0.0),  # Buy   4 A x 13.00 01/07/2021
        (1629047520, 1629047520, 4, -13.0, 150.0, 0.0
         )  # Sell 13 A x 150.00 15/08/2021
    ]
    create_trades(1, test_trades)

    create_quotes(2, 2, [(1614600000, 70.0)])
    create_quotes(4, 2, [(1617278400, 15.0)])
    create_quotes(5, 2, [(1617278400, 2.0)])
    create_quotes(4, 2, [(1628683200, 100.0)])

    # Build ledgerye
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Check ledger amounts before selling
    assert readSQL(
        "SELECT * FROM ledger WHERE asset_id=4 AND timestamp<1628615520 ORDER BY id DESC LIMIT 1"
    ) == [
        11, 1627819200, 5, 2, 4, 4, 1, 13.0, 1452.0, 13.0, 1452.0, '', '', ''
    ]
    assert readSQL(
        "SELECT * FROM ledger WHERE asset_id=5 AND timestamp<1628615520 ORDER BY id DESC LIMIT 1"
    ) == [7, 1622548800, 5, 1, 4, 5, 1, 5.0, 0.0, 5.0, 0.0, '', '', '']
    assert readSQL(
        "SELECT * FROM ledger WHERE book_account=3 AND timestamp<1628615520 ORDER BY id DESC LIMIT 1"
    ) == [8, 1625140800, 3, 2, 3, 2, 1, -52.0, 0.0, 8548.0, 0.0, '', '', '']
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE close_timestamp>=1629047520"
    ) == 498.0
Example #6
0
def test_ledger(prepare_db_ledger):
    actions = [(1638349200, 1, 1, [(5, -100.0)]),
               (1638352800, 1, 1, [(6, -30.0), (8, 55.0)]),
               (1638356400, 1, 1, [(7, 84.0)])]
    create_actions(actions)

    # Build ledger from scratch
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # validate book amounts
    expected_book_values = [None, 130.0, -139.0, 9.0, None, 0.0]
    query = executeSQL(
        "SELECT MAX(id) AS mid, book_account, amount_acc, value_acc "
        "FROM ledger GROUP BY book_account")
    while query.next():
        row = readSQLrecord(query, named=True)
        assert row['amount_acc'] == expected_book_values[row['book_account']]

    actions = [(1638360000, 1, 1, [(5, -34.0)]),
               (1638363600, 1, 1, [(7, 11.0)])]
    create_actions(actions)

    # Build ledger for recent transactions only
    ledger = Ledger()
    ledger.rebuild()

    # validate book amounts and values
    expected_book_amounts = [None, 164.0, -150.0, -0.0, None, -14.0]
    expected_book_values = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    query = executeSQL(
        "SELECT MAX(id) AS mid, book_account, amount_acc, value_acc "
        "FROM ledger GROUP BY book_account")
    while query.next():
        row = readSQLrecord(query, named=True)
        assert row['amount_acc'] == expected_book_amounts[row['book_account']]
        assert row['value_acc'] == expected_book_values[row['book_account']]

    # Re-build from the middle - validation should pass again
    ledger.rebuild(from_timestamp=1638352800)
    query = executeSQL(
        "SELECT MAX(id) AS mid, book_account, amount_acc, value_acc "
        "FROM ledger GROUP BY book_account")
    while query.next():
        row = readSQLrecord(query, named=True)
        assert row['amount_acc'] == expected_book_amounts[row['book_account']]
        assert row['value_acc'] == expected_book_values[row['book_account']]
Example #7
0
def test_stock_dividend_change(prepare_db_fifo):
    # Prepare single stock
    create_stocks([(4, 'A', 'A SHARE')], currency_id=2)

    test_trades = [(1628852820, 1629158400, 4, 2.0, 53.13, 0.34645725),
                   (1628852820, 1629158400, 4, 8.0, 53.13, -0.0152),
                   (1643628654, 1643760000, 4, 5.0, 47.528, 0.35125725),
                   (1644351123, 1644523923, 4, -17.0, 60.0, 0.0)]
    create_trades(1, test_trades)

    # Insert a stock dividend between trades
    stock_dividends = [(1643907900, 1, 4, 2.0, 2, 54.0, 0.0,
                        'Stock dividend +2 A')]
    create_stock_dividends(stock_dividends)

    # insert action between trades and stock dividend to shift frontier
    create_actions([(1643746000, 1, 1, [(7, 100.0)])])

    # Build ledger
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Validate initial deal quantity
    assert readSQL("SELECT COUNT(*) FROM deals WHERE asset_id=4") == 4

    # Modify stock dividend
    executeSQL("UPDATE dividends SET amount=3.0 WHERE asset_id=4")

    # Re-build ledger from last actual data
    ledger.rebuild()

    # Check that deal quantity remains correct
    assert readSQL("SELECT COUNT(*) FROM deals WHERE asset_id=4") == 4

    # Put quotation back and rebuild
    create_quotes(4, 2, [(1643907900, 54.0)])

    # Re-build ledger from last actual data
    ledger.rebuild()

    # Check that deal quantity remains correct
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 4
Example #8
0
class MainWindow(QMainWindow, Ui_JAL_MainWindow):
    def __init__(self, language):
        QMainWindow.__init__(self, None)
        self.setupUi(self)

        self.currentLanguage = language
        self.current_index = None  # this is used in onOperationContextMenu() to track item for menu

        self.ledger = Ledger()
        self.downloader = QuoteDownloader()
        self.taxes = TaxesRus()
        self.statements = StatementLoader()
        self.backup = JalBackup(self, get_dbfilename(get_app_path()))
        self.estimator = None
        self.price_chart = None

        self.actionImportSlipRU.setEnabled(
            dependency_present(['pyzbar', 'PIL']))

        self.actionAbout = QAction(text=self.tr("About"), parent=self)
        self.MainMenu.addAction(self.actionAbout)

        self.langGroup = QActionGroup(self.menuLanguage)
        self.createLanguageMenu()

        self.statementGroup = QActionGroup(self.menuStatement)
        self.createStatementsImportMenu()

        # Set icons
        self.setWindowIcon(load_icon("jal.png"))
        self.NewOperationBtn.setIcon(load_icon("new.png"))
        self.CopyOperationBtn.setIcon(load_icon("copy.png"))
        self.DeleteOperationBtn.setIcon(load_icon("delete.png"))

        # Operations view context menu
        self.contextMenu = QMenu(self.OperationsTableView)
        self.actionReconcile = QAction(load_icon("reconcile.png"),
                                       self.tr("Reconcile"), self)
        self.actionCopy = QAction(load_icon("copy.png"), self.tr("Copy"), self)
        self.actionDelete = QAction(load_icon("delete.png"), self.tr("Delete"),
                                    self)
        self.contextMenu.addAction(self.actionReconcile)
        self.contextMenu.addSeparator()
        self.contextMenu.addAction(self.actionCopy)
        self.contextMenu.addAction(self.actionDelete)

        # Customize Status bar and logs
        self.ProgressBar = QProgressBar(self)
        self.StatusBar.addWidget(self.ProgressBar)
        self.ProgressBar.setVisible(False)
        self.ledger.setProgressBar(self, self.ProgressBar)
        self.NewLogEventLbl = QLabel(self)
        self.StatusBar.addWidget(self.NewLogEventLbl)
        self.Logs.setNotificationLabel(self.NewLogEventLbl)
        self.Logs.setFormatter(
            logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
        self.logger = logging.getLogger()
        self.logger.addHandler(self.Logs)
        log_level = os.environ.get('LOGLEVEL', 'INFO').upper()
        self.logger.setLevel(log_level)

        # Setup reports tab
        self.reports = Reports(self.ReportTableView, self.ReportTreeView)

        # Customize UI configuration
        self.balances_model = BalancesModel(self.BalancesTableView)
        self.BalancesTableView.setModel(self.balances_model)
        self.balances_model.configureView()

        self.holdings_model = HoldingsModel(self.HoldingsTableView)
        self.HoldingsTableView.setModel(self.holdings_model)
        self.holdings_model.configureView()
        self.HoldingsTableView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.operations_model = OperationsModel(self.OperationsTableView)
        self.OperationsTableView.setModel(self.operations_model)
        self.operations_model.configureView()
        self.OperationsTableView.setContextMenuPolicy(Qt.CustomContextMenu)

        self.connect_signals_and_slots()

        self.NewOperationMenu = QMenu()
        for i in range(self.OperationsTabs.count()):
            if hasattr(self.OperationsTabs.widget(i), "isCustom"):
                self.OperationsTabs.widget(i).dbUpdated.connect(
                    self.ledger.rebuild)
                self.OperationsTabs.widget(i).dbUpdated.connect(
                    self.operations_model.refresh)
                self.NewOperationMenu.addAction(
                    self.OperationsTabs.widget(i).name,
                    partial(self.createOperation, i))
        self.NewOperationBtn.setMenu(self.NewOperationMenu)

        # Setup balance and holdings parameters
        current_time = QDateTime.currentDateTime()
        current_time.setTimeSpec(
            Qt.UTC)  # We use UTC everywhere so need to force TZ info
        self.BalanceDate.setDateTime(current_time)
        self.BalancesCurrencyCombo.setIndex(
            JalSettings().getValue('BaseCurrency'))
        self.HoldingsDate.setDateTime(current_time)
        self.HoldingsCurrencyCombo.setIndex(
            JalSettings().getValue('BaseCurrency'))

        self.OperationsTabs.setCurrentIndex(TransactionType.NA)
        self.OperationsTableView.selectRow(0)
        self.OnOperationsRangeChange(0)

    def connect_signals_and_slots(self):
        self.actionExit.triggered.connect(QApplication.instance().quit)
        self.actionAbout.triggered.connect(self.showAboutWindow)
        self.langGroup.triggered.connect(self.onLanguageChanged)
        self.statementGroup.triggered.connect(self.statements.load)
        self.actionReconcile.triggered.connect(
            self.reconcileAtCurrentOperation)
        self.action_Load_quotes.triggered.connect(
            partial(self.downloader.showQuoteDownloadDialog, self))
        self.actionImportSlipRU.triggered.connect(self.importSlip)
        self.actionBackup.triggered.connect(self.backup.create)
        self.actionRestore.triggered.connect(self.backup.restore)
        self.action_Re_build_Ledger.triggered.connect(
            partial(self.ledger.showRebuildDialog, self))
        self.actionAccountTypes.triggered.connect(
            partial(self.onDataDialog, "account_types"))
        self.actionAccounts.triggered.connect(
            partial(self.onDataDialog, "accounts"))
        self.actionAssets.triggered.connect(
            partial(self.onDataDialog, "assets"))
        self.actionPeers.triggered.connect(partial(self.onDataDialog,
                                                   "agents"))
        self.actionCategories.triggered.connect(
            partial(self.onDataDialog, "categories"))
        self.actionTags.triggered.connect(partial(self.onDataDialog, "tags"))
        self.actionCountries.triggered.connect(
            partial(self.onDataDialog, "countries"))
        self.actionQuotes.triggered.connect(
            partial(self.onDataDialog, "quotes"))
        self.PrepareTaxForms.triggered.connect(
            partial(self.taxes.showTaxesDialog, self))
        self.BalanceDate.dateChanged.connect(
            self.BalancesTableView.model().setDate)
        self.HoldingsDate.dateChanged.connect(
            self.HoldingsTableView.model().setDate)
        self.BalancesCurrencyCombo.changed.connect(
            self.BalancesTableView.model().setCurrency)
        self.BalancesTableView.doubleClicked.connect(self.OnBalanceDoubleClick)
        self.HoldingsCurrencyCombo.changed.connect(
            self.HoldingsTableView.model().setCurrency)
        self.ReportRangeCombo.currentIndexChanged.connect(
            self.onReportRangeChange)
        self.RunReportBtn.clicked.connect(self.onRunReport)
        self.SaveReportBtn.clicked.connect(self.reports.saveReport)
        self.ShowInactiveCheckBox.stateChanged.connect(
            self.BalancesTableView.model().toggleActive)
        self.DateRangeCombo.currentIndexChanged.connect(
            self.OnOperationsRangeChange)
        self.ChooseAccountBtn.changed.connect(
            self.OperationsTableView.model().setAccount)
        self.SearchString.editingFinished.connect(self.updateOperationsFilter)
        self.HoldingsTableView.customContextMenuRequested.connect(
            self.onHoldingsContextMenu)
        self.OperationsTableView.selectionModel().selectionChanged.connect(
            self.OnOperationChange)
        self.OperationsTableView.customContextMenuRequested.connect(
            self.onOperationContextMenu)
        self.DeleteOperationBtn.clicked.connect(self.deleteOperation)
        self.actionDelete.triggered.connect(self.deleteOperation)
        self.CopyOperationBtn.clicked.connect(self.copyOperation)
        self.actionCopy.triggered.connect(self.copyOperation)
        self.downloader.download_completed.connect(self.balances_model.update)
        self.downloader.download_completed.connect(self.holdings_model.update)
        self.statements.load_completed.connect(self.onStatementImport)
        self.ledger.updated.connect(self.balances_model.update)
        self.ledger.updated.connect(self.holdings_model.update)

    @Slot()
    def closeEvent(self, event):
        self.logger.removeHandler(
            self.Logs
        )  # Removing handler (but it doesn't prevent exception at exit)
        logging.raiseExceptions = False  # Silencing logging module exceptions

    def createLanguageMenu(self):
        langPath = get_app_path() + Setup.LANG_PATH + os.sep

        langDirectory = QDir(langPath)
        for language_file in langDirectory.entryList(['*.qm']):
            language_code = language_file.split('.')[0]
            language = QLocale.languageToString(
                QLocale(language_code).language())
            language_icon = QIcon(langPath + language_code + '.png')
            action = QAction(language_icon, language, self)
            action.setCheckable(True)
            action.setData(language_code)
            self.menuLanguage.addAction(action)
            self.langGroup.addAction(action)

    @Slot()
    def onLanguageChanged(self, action):
        language_code = action.data()
        if language_code != self.currentLanguage:
            JalSettings().setValue('Language',
                                   JalDB().get_language_id(language_code))
            QMessageBox().information(
                self, self.tr("Restart required"),
                self.tr("Language was changed to ") +
                QLocale.languageToString(QLocale(language_code).language()) +
                "\n" +
                self.tr("You should restart application to apply changes\n"
                        "Application will be terminated now"), QMessageBox.Ok)
            self.close()

    # Create import menu for all known statements based on self.statements.sources values
    def createStatementsImportMenu(self):
        for i, source in enumerate(self.statements.sources):
            if 'icon' in source:
                source_icon = load_icon(source['icon'])
                action = QAction(source_icon, source['name'], self)
            else:
                action = QAction(source['name'], self)
            action.setData(i)
            self.menuStatement.addAction(action)
            self.statementGroup.addAction(action)

    @Slot()
    def showAboutWindow(self):
        about_box = QMessageBox(self)
        about_box.setAttribute(Qt.WA_DeleteOnClose)
        about_box.setWindowTitle(self.tr("About"))
        title = self.tr(
            "<h3>JAL</h3><p>Just Another Ledger, version {version}</p>".format(
                version=__version__))
        about_box.setText(title)
        about = self.tr(
            "<p>More information, manuals and problem reports are at "
            "<a href=https://github.com/titov-vv/jal>github home page</a></p>"
            "<p>Questions, comments, help or donations:</p>"
            "<p><a href=mailto:[email protected]>[email protected]</a></p>"
            "<p><a href=https://t.me/jal_support>Telegram</a></p>")
        about_box.setInformativeText(about)
        about_box.show()

    def showProgressBar(self, visible=False):
        self.ProgressBar.setVisible(visible)
        self.centralwidget.setEnabled(not visible)
        self.MainMenu.setEnabled(not visible)

    @Slot()
    def OnBalanceDoubleClick(self, index):
        self.ChooseAccountBtn.account_id = index.model().getAccountId(
            index.row())

    @Slot()
    def onReportRangeChange(self, range_index):
        report_ranges = {
            0: lambda: (0, 0),
            1: ManipulateDate.Last3Months,
            2: ManipulateDate.RangeYTD,
            3: ManipulateDate.RangeThisYear,
            4: ManipulateDate.RangePreviousYear
        }
        begin, end = report_ranges[range_index]()
        self.ReportFromDate.setDateTime(
            QDateTime.fromSecsSinceEpoch(begin, spec=Qt.UTC))
        self.ReportToDate.setDateTime(
            QDateTime.fromSecsSinceEpoch(end, spec=Qt.UTC))

    @Slot()
    def onRunReport(self):
        types = {
            0: ReportType.IncomeSpending,
            1: ReportType.ProfitLoss,
            2: ReportType.Deals,
            3: ReportType.ByCategory
        }
        report_type = types[self.ReportTypeCombo.currentIndex()]
        begin = self.ReportFromDate.dateTime().toSecsSinceEpoch()
        end = self.ReportToDate.dateTime().toSecsSinceEpoch()
        group_dates = 1 if self.ReportGroupCheck.isChecked() else 0
        if report_type == ReportType.ByCategory:
            self.reports.runReport(report_type, begin, end,
                                   self.ReportCategoryEdit.selected_id,
                                   group_dates)
        else:
            self.reports.runReport(report_type, begin, end,
                                   self.ReportAccountBtn.account_id,
                                   group_dates)

    @Slot()
    def OnOperationsRangeChange(self, range_index):
        view_ranges = {
            0: ManipulateDate.startOfPreviousWeek,
            1: ManipulateDate.startOfPreviousMonth,
            2: ManipulateDate.startOfPreviousQuarter,
            3: ManipulateDate.startOfPreviousYear,
            4: lambda: 0
        }
        self.OperationsTableView.model().setDateRange(
            view_ranges[range_index]())

    @Slot()
    def importSlip(self):
        dialog = ImportSlipDialog(self)
        dialog.finished.connect(self.onSlipImportFinished)
        dialog.open()

    @Slot()
    def onSlipImportFinished(self):
        self.ledger.rebuild()

    @Slot()
    def onHoldingsContextMenu(self, pos):
        index = self.HoldingsTableView.indexAt(pos)
        contextMenu = QMenu(self.HoldingsTableView)
        actionShowChart = QAction(text=self.tr("Show Price Chart"),
                                  parent=self.HoldingsTableView)
        actionShowChart.triggered.connect(
            partial(self.showPriceChart,
                    self.HoldingsTableView.viewport().mapToGlobal(pos), index))
        contextMenu.addAction(actionShowChart)
        actionEstimateTax = QAction(text=self.tr("Estimate Russian Tax"),
                                    parent=self.HoldingsTableView)
        actionEstimateTax.triggered.connect(
            partial(self.estimateRussianTax,
                    self.HoldingsTableView.viewport().mapToGlobal(pos), index))
        contextMenu.addAction(actionEstimateTax)
        contextMenu.popup(self.HoldingsTableView.viewport().mapToGlobal(pos))

    @Slot()
    def showPriceChart(self, position, index):
        model = index.model()
        account, asset, asset_qty = model.get_data_for_tax(index)
        self.price_chart = ChartWindow(account, asset, asset_qty, position)
        if self.price_chart.ready:
            self.price_chart.open()

    @Slot()
    def estimateRussianTax(self, position, index):
        model = index.model()
        account, asset, asset_qty = model.get_data_for_tax(index)
        self.estimator = TaxEstimator(account, asset, asset_qty, position)
        if self.estimator.ready:
            self.estimator.open()

    @Slot()
    def OnOperationChange(self, selected, _deselected):
        self.checkForUncommittedChanges()

        if len(self.OperationsTableView.selectionModel().selectedRows()) != 1:
            self.OperationsTabs.setCurrentIndex(TransactionType.NA)
        else:
            idx = selected.indexes()
            if idx:
                selected_row = idx[0].row()
                operation_type, operation_id = self.OperationsTableView.model(
                ).get_operation(selected_row)
                self.OperationsTabs.setCurrentIndex(operation_type)
                self.OperationsTabs.widget(operation_type).setId(operation_id)

    @Slot()
    def checkForUncommittedChanges(self):
        for i in range(self.OperationsTabs.count()):
            if hasattr(self.OperationsTabs.widget(i),
                       "isCustom") and self.OperationsTabs.widget(i).modified:
                reply = QMessageBox().warning(
                    None, self.tr("You have unsaved changes"),
                    self.OperationsTabs.widget(i).name + self.tr(
                        " has uncommitted changes,\ndo you want to save it?"),
                    QMessageBox.Yes, QMessageBox.No)
                if reply == QMessageBox.Yes:
                    self.OperationsTabs.widget(i).saveChanges()
                else:
                    self.OperationsTabs.widget(i).revertChanges()

    @Slot()
    def onOperationContextMenu(self, pos):
        self.current_index = self.OperationsTableView.indexAt(pos)
        if len(self.OperationsTableView.selectionModel().selectedRows()) != 1:
            self.actionReconcile.setEnabled(False)
            self.actionCopy.setEnabled(False)
        else:
            self.actionReconcile.setEnabled(True)
            self.actionCopy.setEnabled(True)
        self.contextMenu.popup(
            self.OperationsTableView.viewport().mapToGlobal(pos))

    @Slot()
    def reconcileAtCurrentOperation(self):
        idx = self.operations_model.index(
            self.current_index.row(),
            0)  # we need only row to address fields by name
        timestamp = self.operations_model.data(idx,
                                               Qt.UserRole,
                                               field="timestamp")
        account_id = self.operations_model.data(idx,
                                                Qt.UserRole,
                                                field="account_id")
        JalDB().reconcile_account(account_id, timestamp)
        self.operations_model.refresh()

    @Slot()
    def deleteOperation(self):
        if QMessageBox().warning(
                None, self.tr("Confirmation"),
                self.tr("Are you sure to delete selected transacion(s)?"),
                QMessageBox.Yes, QMessageBox.No) == QMessageBox.No:
            return
        rows = []
        for index in self.OperationsTableView.selectionModel().selectedRows():
            rows.append(index.row())
        self.operations_model.deleteRows(rows)
        self.ledger.rebuild()

    @Slot()
    def createOperation(self, operation_type):
        self.checkForUncommittedChanges()
        self.OperationsTabs.widget(operation_type).createNew(
            account_id=self.operations_model.getAccount())
        self.OperationsTabs.setCurrentIndex(operation_type)

    @Slot()
    def copyOperation(self):
        operation_type = self.OperationsTabs.currentIndex()
        if operation_type == TransactionType.NA:
            return
        self.checkForUncommittedChanges()
        self.OperationsTabs.widget(operation_type).copyNew()

    @Slot()
    def updateOperationsFilter(self):
        self.OperationsTableView.model().filterText(self.SearchString.text())

    @Slot()
    def onDataDialog(self, dlg_type):
        if dlg_type == "account_types":
            AccountTypeListDialog().exec()
        elif dlg_type == "accounts":
            AccountListDialog().exec()
        elif dlg_type == "assets":
            AssetListDialog().exec()
        elif dlg_type == "agents":
            PeerListDialog().exec()
        elif dlg_type == "categories":
            CategoryListDialog().exec()
        elif dlg_type == "tags":
            TagsListDialog().exec()
        elif dlg_type == "countries":
            CountryListDialog().exec()
        elif dlg_type == "quotes":
            QuotesListDialog().exec()
        else:
            assert False

    @Slot()
    def onStatementImport(self, timestamp, totals):
        self.ledger.rebuild()
        for account_id in totals:
            for asset_id in totals[account_id]:
                amount = JalDB().get_asset_amount(timestamp, account_id,
                                                  asset_id)
                if amount is not None:
                    if abs(totals[account_id][asset_id] -
                           amount) <= Setup.DISP_TOLERANCE:
                        JalDB().reconcile_account(account_id, timestamp)
                        self.balances_model.update(
                        )  # Update required to display reconciled
                    else:
                        account = JalDB().get_account_name(account_id)
                        asset = JalDB().get_asset_name(asset_id)
                        logging.warning(
                            self.tr(
                                "Statement ending balance doesn't match: ") +
                            f"{account} / {asset} / {amount} <> {totals[account_id][asset_id]}"
                        )
Example #9
0
def test_buy_sell_change(prepare_db_fifo):
    # Prepare single stock
    create_stocks([(4, 'A', 'A SHARE')], currency_id=2)

    test_trades = [(1, 1609567200, 1609653600, 4, 10.0, 100.0, 1.0),
                   (2, 1609729200, 1609815600, 4, -7.0, 200.0, 5.0)]
    for trade in test_trades:
        assert executeSQL(
            "INSERT INTO trades (id, timestamp, settlement, account_id, asset_id, qty, price, fee) "
            "VALUES (:id, :timestamp, :settlement, 1, :asset, :qty, :price, :fee)",
            [(":id", trade[0]), (":timestamp", trade[1]),
             (":settlement", trade[2]),
             (":asset", trade[3]), (":qty", trade[4]), (":price", trade[5]),
             (":fee", trade[6])]) is not None

    # insert action between trades to shift frontier
    create_actions([(1609642800, 1, 1, [(7, 100.0)])])

    # Build ledger
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Validate initial deal quantity
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 1
    assert readSQL("SELECT qty FROM deals WHERE asset_id=4") == 7.0

    # Modify closing deal quantity
    _ = executeSQL("UPDATE trades SET qty=-5 WHERE id=2")

    # Re-build ledger from last actual data
    ledger.rebuild()

    # Check that deal quantity remains correct
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 1
    assert readSQL("SELECT COUNT(*) FROM open_trades WHERE asset_id=4") == 1
    assert readSQL("SELECT qty FROM deals WHERE asset_id=4") == 5.0

    # Add one more trade
    assert executeSQL(
        "INSERT INTO trades (id, timestamp, settlement, account_id, asset_id, qty, price, fee) "
        "VALUES (3, 1609815600, 1609902000, 1, 4, -8, 150, 3.0)") is not None

    # Re-build ledger from last actual data
    ledger.rebuild()

    # Check that deal quantity remains correct
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 2

    assert readSQL("SELECT COUNT(*) FROM open_trades") == 2

    _ = executeSQL("DELETE FROM trades WHERE id=2", commit=True)

    assert readSQL("SELECT COUNT(*) FROM open_trades") == 1
    assert readSQL(
        "SELECT COUNT(*) FROM ledger WHERE timestamp>=1609729200") == 0
    # Re-build ledger from last actual data
    ledger.rebuild()

    # Check that deal quantity remains correct
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 1
    assert readSQL("SELECT qty FROM deals WHERE asset_id=4") == 8.0
Example #10
0
def test_fifo(prepare_db_fifo):
    # Prepare trades and corporate actions setup
    test_assets = [
        (4, 'A', 'A SHARE'),
        (5, 'B', 'B SHARE'),
        (6, 'C', 'C SHARE'),
        (7, 'D', 'D SHARE'),
        (8, 'E', 'E SHARE'),
        (9, 'F', 'F SHARE'),
        (10, 'G1', 'G SHARE BEFORE'),
        (11, 'G2', 'G SHARE AFTER'),
        (12, 'H', 'H SPIN-OFF FROM G'),
        (13, 'K', 'K SHARE'),
        (14, 'L', 'L SHARE'),
        (15, 'M', 'M SHARE'),
        (16, 'N', 'N WITH STOCK DIVIDEND'),
        (17, 'O', 'O SHARE'),
        (18, 'P', 'P SHARE'),
    ]
    create_stocks(test_assets, currency_id=2)

    test_corp_actions = [
        (1606899600, 3, 10, 100.0, 11, 100.0, 1.0, 'Symbol change G1 -> G2'),
        (1606986000, 2, 11, 100.0, 12, 20.0, 0.8, 'Spin-off H from G2'),
        (1607763600, 4, 14, 15.0, 14, 30.0, 1.0, 'Split L 15 -> 30'),
        (1607850000, 3, 13, 5.0, 15, 5.0, 1.0, 'Another symbol change K -> M'),
        (1607936412, 1, 14, 30.0, 15, 20.0, 1.0, 'Merger 30 L into 20 M'),
        (1608022800, 4, 15, 25.0, 15, 5.0, 1.0, 'Split M 25 -> 5')
    ]
    create_corporate_actions(1, test_corp_actions)

    stock_dividends = [(1608368400, 1, 16, 1.0, 2, 1050.0, 60.0,
                        'Stock dividend +1 N')]
    create_stock_dividends(stock_dividends)

    test_trades = [(1609567200, 1609653600, 4, 10.0, 100.0, 1.0),
                   (1609653600, 1609740000, 4, -10.0, 200.0, 5.0),
                   (1609653600, 1609740000, 5, 10.0, 100.0, 1.0),
                   (1609740000, 1609826400, 5, -3.0, 200.0, 2.0),
                   (1609740000, 1609826400, 5, -7.0, 50.0, 3.0),
                   (1609826400, 1609912800, 6, 2.0, 100.0, 2.0),
                   (1609912800, 1609999200, 6, 8.0, 200.0, 2.0),
                   (1609999200, 1610085600, 6, -10.0, 50.0, 2.0),
                   (1610085600, 1610172000, 7, -100.0, 1.0, 1.0),
                   (1610172000, 1610258400, 7, 50.0, 2.0, 1.0),
                   (1610258400, 1610344800, 7, 50.0, 1.5, 1.0),
                   (1610344800, 1610431200, 8, -1.3, 100.0, 1.0),
                   (1610431200, 1610517600, 8, -1.7, 200.0, 1.0),
                   (1610517600, 1610604000, 8, 3.0, 50.0, 1.0),
                   (1610604000, 1610690400, 9, 10.0, 100.0, 0.0),
                   (1610690400, 1610776800, 9, -7.0, 200.0, 0.0),
                   (1610776800, 1610863200, 9, -5.0, 200.0, 0.0),
                   (1610863200, 1610949600, 9, -10.0, 200.0, 0.0),
                   (1610949600, 1611036000, 9, -8.0, 200.0, 0.0),
                   (1611036000, 1611122400, 9, 40.0, 100.0, 0.0),
                   (1611122400, 1611208800, 9, -11.0, 200.0, 0.0),
                   (1611208800, 1611295200, 9, -18.0, 200.0, 0.0),
                   (1611295200, 1611381600, 9, 15.0, 300.0, 0.0),
                   (1611381600, 1611468000, 9, -3.0, 200.0, 0.0),
                   (1611468000, 1611554400, 9, -2.0, 200.0, 0.0),
                   (1611554400, 1611640800, 9, -1.0, 200.0, 0.0),
                   (1606813200, 1606856400, 10, 100.0, 10.0, 0.0),
                   (1607072400, 1607115600, 11, -100.0, 20.0, 0.0),
                   (1607158800, 1607202000, 12, -20.0, 10.0, 0.0),
                   (1607580000, 1607634000, 13, 5.0, 20.0, 0.0),
                   (1607666400, 1607720400, 14, 10.0, 25.0, 0.0),
                   (1607673600, 1607720400, 14, 10.0, 50.0, 0.0),
                   (1607680800, 1607720400, 14, -5.0, 40.0, 0.0),
                   (1608195600, 1608238800, 15, -5.0, 200.0, 1.0),
                   (1608282000, 1608325200, 16, 5.0, 1000.0, 0.0),
                   (1608454800, 1608498000, 16, -1.0, 1000.0, 0.0),
                   (1608541200, 1608584400, 16, -5.0, 1100.0, 0.0),
                   (1608616800, 1608670800, 17, 8.0, 130.0, 0.0),
                   (1608624000, 1608670800, 17, -8.0, 120.0, 0.0),
                   (1608620400, 1608670800, 17, 22.0, 110.0, 0.0),
                   (1608627600, 1608670800, 17, -22.0, 120.0, 0.0),
                   (1608703200, 1608757200, 18, 1.0, 1000.0, 0.0),
                   (1608706800, 1608757200, 18, -1.0, 2000.0, 0.0),
                   (1608710400, 1608757200, 18, -1.0, 1900.0, 0.0),
                   (1608714000, 1608757200, 18, 1.0, 2700.0, 0.0),
                   (1608717600, 1608757200, 18, -1.0, 3000.0, 0.0),
                   (1608721200, 1608757200, 18, -1.0, 2000.0, 0.0),
                   (1608724800, 1608757200, 18, 2.0, 2500.0, 0.0)]
    create_trades(1, test_trades)

    # Build ledger
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Check single deal
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4") == 1
    assert readSQL("SELECT SUM(profit) FROM deals_ext WHERE asset_id=4") == 994
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=4") == 6

    # One buy multiple sells
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=5") == 2
    assert readSQL("SELECT SUM(profit) FROM deals_ext WHERE asset_id=5") == -56
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=5") == 6

    # Multiple buy one sell
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=6") == 2
    assert readSQL(
        "SELECT SUM(profit) FROM deals_ext WHERE asset_id=6") == -1306
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=6") == 6

    # One sell multiple buys
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=7") == 2
    assert readSQL("SELECT SUM(profit) FROM deals_ext WHERE asset_id=7") == -78
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=7") == 3

    # Multiple sells one buy
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=8") == 2
    assert readSQL("SELECT SUM(profit) FROM deals_ext WHERE asset_id=8") == 317
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=8") == 3

    # Multiple buys and sells
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=9") == 11
    assert readSQL(
        "SELECT SUM(profit) FROM deals_ext WHERE asset_id=9") == 3500
    assert readSQL("SELECT SUM(fee) FROM deals_ext WHERE asset_id=9") == 0

    # Symbol change
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=10") == 1
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=11") == 1
    assert readSQL("SELECT profit FROM deals_ext WHERE asset_id=11") == 1200

    # Spin-off
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=12") == 1
    assert readSQL("SELECT profit FROM deals_ext WHERE asset_id=12") == approx(
        0)

    # Multiple corp actions
    assert readSQL(
        "SELECT COUNT(*) FROM deals_ext WHERE asset_id=13 AND corp_action IS NOT NULL"
    ) == 1
    assert readSQL("SELECT profit FROM deals_ext WHERE asset_id=13") == 0
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=14") == 3
    assert readSQL(
        "SELECT COUNT(*) FROM deals_ext WHERE asset_id=14 AND corp_action IS NOT NULL"
    ) == 2
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE asset_id=14 AND corp_action IS NULL"
    ) == 75
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE asset_id=14 AND corp_action IS NOT NULL"
    ) == 0
    assert readSQL(
        "SELECT COUNT(*) FROM deals_ext WHERE asset_id=15 AND corp_action IS NOT NULL"
    ) == 1
    assert readSQL("SELECT profit FROM deals_ext WHERE asset_id=15") == 274

    # Stock dividend
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=16") == 3
    assert readSQL(
        "SELECT SUM(profit) FROM deals_ext WHERE asset_id=16") == approx(450)
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE asset_id=16 AND close_timestamp=1608454800"
    ) == approx(0)
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE asset_id=16 AND open_timestamp=1608368400"
    ) == approx(50)

    # Order of buy/sell
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=17") == 2
    assert readSQL(
        "SELECT SUM(profit) FROM deals_ext WHERE asset_id=17") == 140
    assert readSQL("SELECT COUNT(*) FROM deals_ext WHERE asset_id=18") == 4
    assert readSQL("SELECT SUM(qty) FROM deals_ext WHERE asset_id=18") == -2
    assert readSQL(
        "SELECT SUM(profit) FROM deals_ext WHERE asset_id=18") == 200

    # totals
    assert readSQL("SELECT COUNT(*) FROM deals") == 41
    assert readSQL(
        "SELECT COUNT(*) FROM deals WHERE open_op_type=:trade AND close_op_type=:trade",
        [(":trade", LedgerTransaction.Trade)]) == 29
    assert readSQL(
        "SELECT COUNT(*) FROM deals WHERE open_op_type!=:corp_action OR close_op_type!=:corp_action",
        [(":corp_action", LedgerTransaction.CorporateAction)]) == 37
    assert readSQL(
        "SELECT COUNT(*) FROM deals WHERE open_op_type=:corp_action AND close_op_type=:corp_action",
        [(":corp_action", LedgerTransaction.CorporateAction)]) == 4

    # validate final amounts
    query = executeSQL(
        "SELECT MAX(id) AS mid, asset_id, amount_acc, value_acc FROM ledger "
        "WHERE book_account=:money OR book_account=:assets GROUP BY asset_id",
        [(":money", BookAccount.Money), (":assets", BookAccount.Assets)])
    while query.next():
        row = readSQLrecord(query, named=True)
        if row['asset_id'] == 2:  # Checking money amount
            assert row['amount_acc'] == 16700
        else:
            assert row['amount_acc'] == 0
        assert row['value_acc'] == 0
Example #11
0
def test_empty_ledger(prepare_db_ledger):
    # Build ledger from scratch
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)
Example #12
0
def test_spin_off(prepare_db_fifo):
    # Prepare trades and corporate actions setup
    test_assets = [(4, 'A', 'A SHARE'), (5, 'B', 'B SHARE')]
    for asset in test_assets:
        assert executeSQL(
            "INSERT INTO assets (id, name, type_id, full_name) "
            "VALUES (:id, :name, :type, :full_name)",
            [(":id", asset[0]), (":name", asset[1]),
             (":type", PredefinedAsset.Stock), (":full_name", asset[2])],
            commit=True) is not None

    test_corp_actions = [
        (1, 1622548800, 2, 4, 100.0, 5, 5.0, 1.0,
         'Spin-off 5 B from 100 A'),  # 01/06/2021, cost basis 0.0
        (2, 1627819200, 4, 4, 104.0, 4, 13.0, 1.0, 'Split A 104 -> 13'
         )  # 01/08/2021
    ]
    for action in test_corp_actions:
        assert executeSQL(
            "INSERT INTO corp_actions "
            "(id, timestamp, account_id, type, asset_id, qty, asset_id_new, qty_new, basis_ratio, note) "
            "VALUES (:id, :timestamp, 1, :type, :a_o, :q_o, :a_n, :q_n, :ratio, :note)",
            [(":id", action[0]), (":timestamp", action[1]),
             (":type", action[2]), (":a_o", action[3]), (":q_o", action[4]),
             (":a_n", action[5]), (":q_n", action[6]), (":ratio", action[7]),
             (":note", action[8])],
            commit=True) is not None

    test_trades = [
        (1, 1619870400, 1619870400, 4, 100.0, 14.0,
         0.0),  # Buy 100 A x 14.00 01/05/2021
        (2, 1625140800, 1625140800, 4, 4.0, 13.0,
         0.0),  # Buy   4 A x 13.00 01/07/2021
        (3, 1629047520, 1629047520, 4, -13.0, 150.0, 0.0
         )  # Sell 13 A x 150.00 15/08/2021
    ]
    for trade in test_trades:
        assert executeSQL(
            "INSERT INTO trades (id, timestamp, settlement, account_id, asset_id, qty, price, fee) "
            "VALUES (:id, :timestamp, :settlement, 1, :asset, :qty, :price, :fee)",
            [(":id", trade[0]), (":timestamp", trade[1]),
             (":settlement", trade[2]),
             (":asset", trade[3]), (":qty", trade[4]), (":price", trade[5]),
             (":fee", trade[6])]) is not None

    quotes = [(2, 1614600000, 2, 70.0), (3, 1617278400, 4, 15.0),
              (4, 1617278400, 5, 2.0), (5, 1628683200, 4, 100.0)]
    for quote in quotes:
        assert executeSQL(
            "INSERT INTO quotes (id, timestamp, asset_id, quote) "
            "VALUES (:id, :timestamp, :asset, :quote)",
            [(":id", quote[0]), (":timestamp", quote[1]), (":asset", quote[2]),
             (":quote", quote[3])]) is not None

    # Build ledgerye
    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Check ledger amounts before selling
    assert readSQL(
        "SELECT * FROM ledger_sums WHERE asset_id=4 AND timestamp<1628615520 ORDER BY timestamp DESC LIMIT 1"
    ) == [5, 1627819200, 4, 4, 1, 13.0, 1452.0]
    assert readSQL(
        "SELECT * FROM ledger_sums WHERE asset_id=5 AND timestamp<1628615520 ORDER BY timestamp DESC LIMIT 1"
    ) == [3, 1622548800, 4, 5, 1, 5.0, 0.0]
    assert readSQL(
        "SELECT * FROM ledger_sums WHERE book_account=3 AND timestamp<1628615520 ORDER BY timestamp DESC LIMIT 1"
    ) == [4, 1625140800, 3, 2, 1, 8548.0, 0.0]
    assert readSQL(
        "SELECT profit FROM deals_ext WHERE close_timestamp>=1629047520"
    ) == 498.0
Example #13
0
def test_statement_ibkr(tmp_path, project_root, data_path, prepare_db_taxes):
    #  Import first year
    ibkr_statement0 = StatementIBKR()
    ibkr_statement0.load(data_path + 'ibkr_year0.xml')
    ibkr_statement0.validate_format()
    ibkr_statement0.match_db_ids()
    ibkr_statement0.import_into_db()

    # validate assets
    test_assets = [[1, PredefinedAsset.Money, 'Российский Рубль', '', 0, ''],
                   [2, PredefinedAsset.Money, 'Доллар США', '', 0, ''],
                   [3, PredefinedAsset.Money, 'Евро', '', 0, ''],
                   [
                       4, PredefinedAsset.Stock, 'PACIFIC ETHANOL INC',
                       'US69423U3059', 0, ''
                   ]]
    assert readSQL("SELECT COUNT(*) FROM assets") == len(test_assets)
    for i, asset in enumerate(test_assets):
        assert readSQL("SELECT * FROM assets WHERE id=:id",
                       [(":id", i + 1)]) == asset

    # validate assets
    test_symbols = [[1, 1, 'RUB', 1, 'Российский Рубль', -1, 1],
                    [2, 2, 'USD', 1, 'Доллар США (Банк России)', 0, 1],
                    [3, 3, 'EUR', 1, 'Евро (Банк России)', 0, 1],
                    [4, 4, 'PEIX', 2, 'NASDAQ', 2, 1]]
    assert readSQL("SELECT COUNT(*) FROM asset_tickers") == len(test_symbols)
    for i, symbol in enumerate(test_symbols):
        assert readSQL("SELECT * FROM asset_tickers WHERE id=:id",
                       [(":id", i + 1)]) == symbol

    # validate assets
    test_data = [[1, 4, 1, '69423U305']]
    assert readSQL("SELECT COUNT(*) FROM asset_data") == len(test_data)
    for i, data in enumerate(test_data):
        assert readSQL("SELECT * FROM asset_data WHERE id=:id",
                       [(":id", i + 1)]) == data

    # validate trades
    test_trades = [[
        1, 3, 1606471692, 1606780800, '3256333343', 1, 4, 70.0, 6.898,
        0.36425725, ''
    ],
                   [
                       2, 3, 1606821387, 1606953600, '3264444280', 1, 4, 70.0,
                       6.08, 0.32925725, ''
                   ]]
    assert readSQL("SELECT COUNT(*) FROM trades") == len(test_trades)
    for i, trade in enumerate(test_trades):
        assert readSQL("SELECT * FROM trades WHERE id=:id",
                       [(":id", i + 1)]) == trade

    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Import second year
    ibkr_statement1 = StatementIBKR()
    ibkr_statement1.load(data_path + 'ibkr_year1.xml')
    ibkr_statement1.validate_format()
    ibkr_statement1.match_db_ids()
    ibkr_statement1.import_into_db()

    ledger.rebuild(from_timestamp=0)

    # validate assets
    test_assets = [[1, PredefinedAsset.Money, 'Российский Рубль', '', 0, ''],
                   [2, PredefinedAsset.Money, 'Доллар США', '', 0, ''],
                   [3, PredefinedAsset.Money, 'Евро', '', 0, ''],
                   [
                       4, PredefinedAsset.Stock, 'PACIFIC ETHANOL INC',
                       'US69423U3059', 0, ''
                   ],
                   [
                       5, PredefinedAsset.Stock, 'ALTO INGREDIENTS INC',
                       'US0215131063', 0, ''
                   ]]
    assert readSQL("SELECT COUNT(*) FROM assets") == len(test_assets)
    for i, asset in enumerate(test_assets):
        assert readSQL("SELECT * FROM assets WHERE id=:id",
                       [(":id", i + 1)]) == asset

    # validate assets
    test_symbols = [[1, 1, 'RUB', 1, 'Российский Рубль', -1, 1],
                    [2, 2, 'USD', 1, 'Доллар США (Банк России)', 0, 1],
                    [3, 3, 'EUR', 1, 'Евро (Банк России)', 0, 1],
                    [4, 4, 'PEIX', 2, 'NASDAQ', 2, 1],
                    [5, 5, 'ALTO', 2, 'NASDAQ', 2, 0],
                    [6, 5, 'PEIX', 2, 'NASDAQ', 2, 1]]
    assert readSQL("SELECT COUNT(*) FROM asset_tickers") == len(test_symbols)
    for i, symbol in enumerate(test_symbols):
        assert readSQL("SELECT * FROM asset_tickers WHERE id=:id",
                       [(":id", i + 1)]) == symbol

    # validate assets
    test_data = [[1, 4, 1, '69423U305'], [2, 5, 1, '021513106']]
    assert readSQL("SELECT COUNT(*) FROM asset_data") == len(test_data)
    for i, data in enumerate(test_data):
        assert readSQL("SELECT * FROM asset_data WHERE id=:id",
                       [(":id", i + 1)]) == data

    # validate trades
    test_trades = [[
        1, 3, 1606471692, 1606780800, '3256333343', 1, 4, 70.0, 6.898,
        0.36425725, ''
    ],
                   [
                       2, 3, 1606821387, 1606953600, '3264444280', 1, 4, 70.0,
                       6.08, 0.32925725, ''
                   ],
                   [
                       3, 3, 1610625615, 1611014400, '3381623127', 1, 5, -70.0,
                       7.42, 0.23706599, ''
                   ],
                   [
                       4, 3, 1612871230, 1613001600, '3480222427', 1, 5, -70.0,
                       7.71, 0.23751462, ''
                   ]]
    assert readSQL("SELECT COUNT(*) FROM trades") == len(test_trades)
    for i, trade in enumerate(test_trades):
        assert readSQL("SELECT * FROM trades WHERE id=:id",
                       [(":id", i + 1)]) == trade

    # validate corp actions
    test_corp_actons = [[
        1, 5, 1610569500, '14909999818', 1, 3, 4, 140.0, 5, 140.0, 1.0,
        'PEIX(US69423U3059) CUSIP/ISIN CHANGE TO (US0215131063) (PEIX, ALTO INGREDIENTS INC, US0215131063)'
    ]]
    assert readSQL("SELECT COUNT(*) FROM corp_actions") == len(
        test_corp_actons)
    for i, action in enumerate(test_corp_actons):
        assert readSQL("SELECT * FROM corp_actions WHERE id=:id",
                       [(":id", i + 1)]) == action

    # Check that there are no remainders
    assert readSQL(
        "SELECT amount_acc, value_acc FROM ledger_totals WHERE asset_id=4 ORDER BY id DESC LIMIT 1"
    ) == [0.0, 0.0]
    assert readSQL(
        "SELECT amount_acc, value_acc FROM ledger_totals WHERE asset_id=5 ORDER BY id DESC LIMIT 1"
    ) == [0.0, 0.0]
    assert readSQL(
        "SELECT amount_acc, value_acc FROM ledger WHERE asset_id=4 ORDER BY id DESC LIMIT 1"
    ) == [0.0, 0.0]
    assert readSQL(
        "SELECT amount_acc, value_acc FROM ledger WHERE asset_id=5 ORDER BY id DESC LIMIT 1"
    ) == [0.0, 0.0]

    # Check correct number of deals
    assert readSQL("SELECT COUNT(*) FROM deals_ext") == 4
Example #14
0
def test_fifo(tmp_path, project_root):
    # Prepare environment
    src_path = project_root + os.sep + 'jal' + os.sep + Setup.INIT_SCRIPT_PATH
    target_path = str(tmp_path) + os.sep + Setup.INIT_SCRIPT_PATH
    copyfile(src_path, target_path)
    init_and_check_db(str(tmp_path) + os.sep)
    db_file_name = get_dbfilename(str(tmp_path) + os.sep)
    backup = JalBackup(None, db_file_name)
    backup.backup_name = project_root + os.sep + "tests" + os.sep + "test_data" + os.sep + "deals_set.tgz"
    backup.do_restore()

    error = init_and_check_db(str(tmp_path) + os.sep)

    assert error.code == LedgerInitError.DbInitSuccess

    ledger = Ledger()
    ledger.rebuild(from_timestamp=0)

    # Check single deal
    db_file_name = get_dbfilename(str(tmp_path) + os.sep)
    db = sqlite3.connect(db_file_name)
    cursor = db.cursor()

    # Check single deal
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=4")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=4")
    assert cursor.fetchone()[0] == 994
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=4")
    assert cursor.fetchone()[0] == 6

    # One buy multiple sells
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=5")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=5")
    assert cursor.fetchone()[0] == -56
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=5")
    assert cursor.fetchone()[0] == 6

    # Multiple buy one sell
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=6")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=6")
    assert cursor.fetchone()[0] == -1306
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=6")
    assert cursor.fetchone()[0] == 6

    # One sell multiple buys
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=7")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=7")
    assert cursor.fetchone()[0] == -78
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=7")
    assert cursor.fetchone()[0] == 3

    # Multiple sells one buy
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=8")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=8")
    assert cursor.fetchone()[0] == 317
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=8")
    assert cursor.fetchone()[0] == 3

    # Multiple buys and sells
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=9")
    assert cursor.fetchone()[0] == 11
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=9")
    assert cursor.fetchone()[0] == 3500
    cursor.execute("SELECT SUM(fee) FROM deals_ext WHERE asset_id=9")
    assert cursor.fetchone()[0] == 0

    # Symbol change
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=10")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=11")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=11")
    assert cursor.fetchone()[0] == 1200

    # Spin-off
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=12")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=12")
    assert cursor.fetchone()[0] == 0

    # Multiple corp actions
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=13 AND corp_action IS NOT NULL")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=13")
    assert cursor.fetchone()[0] == 0
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=14")
    assert cursor.fetchone()[0] == 3
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=14 AND corp_action IS NOT NULL")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=14 AND corp_action IS NULL")
    assert cursor.fetchone()[0] == 75
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=14 AND corp_action IS NOT NULL")
    assert cursor.fetchone()[0] == 0
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=15 AND corp_action IS NOT NULL")
    assert cursor.fetchone()[0] == 1
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=15")
    assert cursor.fetchone()[0] == 274

    # Stock dividend
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=16")
    assert cursor.fetchone()[0] == 3
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=16")
    assert cursor.fetchone()[0] == approx(1500)
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=16 AND close_timestamp=1608454800")
    assert cursor.fetchone()[0] == approx(166.666667)
    cursor.execute("SELECT profit FROM deals_ext WHERE asset_id=16 AND close_timestamp=1608541200")
    assert cursor.fetchone()[0] == approx(1333.333333)

    # Order of buy/sell
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=17")
    assert cursor.fetchone()[0] == 2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=17")
    assert cursor.fetchone()[0] == 140
    cursor.execute("SELECT COUNT(*) FROM deals_ext WHERE asset_id=18")
    assert cursor.fetchone()[0] == 4
    cursor.execute("SELECT SUM(qty) FROM deals_ext WHERE asset_id=18")
    assert cursor.fetchone()[0] == -2
    cursor.execute("SELECT SUM(profit) FROM deals_ext WHERE asset_id=18")
    assert cursor.fetchone()[0] == 200

    # totals
    cursor.execute("SELECT COUNT(*) FROM deals AS d "
                   "LEFT JOIN sequence as os ON os.id = d.open_sid "
                   "LEFT JOIN sequence as cs ON cs.id = d.close_sid")
    assert cursor.fetchone()[0] == 41
    cursor.execute("SELECT COUNT(*) FROM deals AS d "
                   "LEFT JOIN sequence as os ON os.id = d.open_sid "
                   "LEFT JOIN sequence as cs ON cs.id = d.close_sid "
                   "WHERE os.type==3 AND cs.type==3")
    assert cursor.fetchone()[0] == 27
    cursor.execute("SELECT COUNT(*) FROM deals AS d "
                   "LEFT JOIN sequence as os ON os.id = d.open_sid "
                   "LEFT JOIN sequence as cs ON cs.id = d.close_sid "
                   "WHERE os.type!=5 OR cs.type!=5")
    assert cursor.fetchone()[0] == 37
    cursor.execute("SELECT COUNT(*) FROM deals AS d "
                   "LEFT JOIN sequence as os ON os.id = d.open_sid "
                   "LEFT JOIN sequence as cs ON cs.id = d.close_sid "
                   "WHERE os.type==5 AND cs.type==5")
    assert cursor.fetchone()[0] == 4

    # validate final amounts
    cursor.execute("SELECT MAX(sid), asset_id, sum_amount, sum_value FROM ledger_sums "
                   "GROUP BY asset_id")
    ledger_sums = cursor.fetchall()
    for row in ledger_sums:
        if row[1] == 1:    # Checking money amount
            assert row[2] == 16760
        else:
            assert row[2] == 0
        assert row[3] == 0

    db.close()
Example #15
0
def test_taxes_rus(tmp_path, data_path, prepare_db_taxes):
    with open(data_path + 'taxes_rus.json', 'r') as json_file:
        report = json.load(json_file)

    assets = [
        (4, "GE", "General Electric Company", "US3696043013", 2, PredefinedAsset.Stock, 2),
        (5, "TLT", "iShares 20+ Year Treasury Bond ETF", "US4642874329", 2, PredefinedAsset.ETF, 2),
        (6, "ERIC", "Telefonaktiebolaget LM Ericsson B ADR", "US2948216088", 2, PredefinedAsset.ETF, 7),
        (7, "GOLD", "Barrick Gold Corp", "CA0679011084", 2, PredefinedAsset.ETF, 6),
        (8, "TEVA", "Teva Pharma Industries Ltd ADR", "US8816242098", 2, PredefinedAsset.Stock, 0),
        (9, "X 6 1/4 03/15/26", "X 6 1/4 03/15/26", "US912909AN84", 2, PredefinedAsset.Bond, 2),
        (10, "AAL   210115C00030000", "AAL 15JAN21 30.0 C", "", 2, PredefinedAsset.Derivative, 0),
        (11, "MYL", "MYLAN NV", "NL0011031208", 2, PredefinedAsset.Stock, 0),
        (12, "VTRS", "VIATRIS INC", "US92556V1061", 2, PredefinedAsset.Stock, 0),
        (13, "DEL", "DELISTED STOCK", "US12345X0000", 2, PredefinedAsset.Stock, 0)
    ]
    create_assets(assets)
    usd_rates = [
        (1569456000, 64.1873), (1569369600, 63.706), (1569283200, 63.9453), (1569024000, 63.8487),
        (1582243200, 63.7413), (1582156800, 63.6873), (1582070400, 63.7698), (1581984000, 63.3085),
        (1554163200, 65.4176), (1553904000, 64.7347), (1553817600, 64.8012), (1553731200, 64.5925),
        (1550016000, 65.7147), (1549929600, 65.6517), (1549670400, 66.0628), (1549584000, 66.0199),
        (1579132800, 61.4328), (1579046400, 61.414), (1578960000, 60.9474), (1578700800, 61.2632),
        (1582934400, 66.9909), (1582848000, 65.6097), (1582761600, 65.5177), (1582675200, 64.9213),
        (1590710400, 71.1012), (1590624000, 71.0635), (1590537600, 71.1408), (1590451200, 71.5962),
        (1604448000, 80.0006), (1604361600, 80.5749), (1604102400, 79.3323), (1604016000, 78.8699),
        (1593820800, 70.4999), (1593734400, 70.5198), (1593648000, 70.4413), (1593561600, 70.4413),
        (1608163200, 73.4201), (1608076800, 73.4453), (1607990400, 72.9272), (1607731200, 73.1195),
        (1579910400, 61.8031),
        (1581033600, 62.7977),
        (1587513600, 76.2562),
        (1605039600, 76.9515),
        (1600128000, 74.7148),
        (1591142400, 68.9831),
        (1593129600, 69.4660)
    ]
    create_quotes(2, 1, usd_rates)
    dividends = [
        (1580156400, 1, 4, 1.0, 0.1, "GE(US3696041033) CASH DIVIDEND USD 0.01 PER SHARE (Ordinary Dividend)"),
        (1581106800, 1, 5, 16.94, 0, "TLT(US4642874329) CASH DIVIDEND USD 0.241966 PER SHARE (Ordinary Dividend)"),
        (1587586800, 1, 6, 3.74, 1.12, "ERIC(US2948216088) CASH DIVIDEND USD 0.074728 PER SHARE (Ordinary Dividend)")
    ]
    create_dividends(dividends)
    stock_dividends = [
        (1593205200, 1, 4, 2.0, 2, 53.4, 10.68, 'GE (US3696041033) Stock Dividend US3696041033 196232339 for 10000000000')
    ]
    create_stock_dividends(stock_dividends)
    coupons = [
        (1590587855, 1, 9, -25.69, 0, "PURCHASE ACCRUED INT X 6 1/4 03/15/26", "2881234567"),
        (1600128000, 1, 9, 62.5, 0, "BOND COUPON PAYMENT (X 6 1/4 03/15/26)", ""),
        (1604319194, 1, 9, 16.89, 0, "SALE ACCRUED INT X 6 1/4 03/15/26", "2881234589")
    ]
    create_coupons(coupons)
    trades = [
        (1569334259, 1569456000, 7, 10, 18.74, 0.32825725, "0001"),
        (1582116724, 1582243200, 7, -10, 20.95, 0.3370772, "0002"),
        (1549881381, 1550016000, 8, 4, 18.52, 0.34625725, "0000000003"),
        (1553861071, 1554163200, 8, 1, 15.8, 0.34995725, "0000000004"),
        (1582117021, 1582243200, 8, -5, 13.23, 0.33137108, "0000000005"),
        (1579097059, 1579132800, 10, -100, 2.94, 0.8018858, "2661844383"),
        (1582886521, 1583107200, 10, 100, 1.31, 1.0938, "2716375310"),
        (1590587855, 1590710400, 9, 2, 639.07, 2, "2881234567"),
        (1604319194, 1604448000, 9, -2, 800, 2, "2881234589"),
        (1593604800, 1593993600, 11, 50, 15.9, 0.35, "1118233222"),
        (1608044400, 1608163200, 12, -50, 17.71, 0.35, "2227095222"),
        (1593604800, 1593993600, 5, 10, 10.0, 0.35, "A"),
        (1608044400, 1608163200, 5, -25, 3.0, 0.35, "B1"),
        (1608044400, 1608163200, 5, -25, 2.5, 0.35, "B2"),
        (1607871600, 1608044400, 13, 10, 11, 0.5, "1000000001"),
    ]
    create_trades(1, trades)

    # insert fees and interest
    operations = [
        (1604343555, 1, 1, [(5, -10.0, "BALANCE OF MONTHLY MINIMUM FEE FOR OCT 2020")]),
        (1605039600, 1, 1, [(5, -1.0, "ERIC(294821608) ADR Fee USD 0.02 PER SHARE - FEE")]),
        (1591142400, 1, 1, [(8, 1.5, "RUB CREDIT INT FOR MAY-2020")])
    ]
    create_actions(operations)

    corporate_actions = [
        (1605528000, CorporateAction.SymbolChange, 11, 50, 12, 50, 1, "Symbol change MYL->VTRS"),
        (1604448000, CorporateAction.Split, 5, 10, 5, 50, 1, "Split 5:1 of TLT"),
        (1608217200, CorporateAction.Delisting, 13, 10, 13, 0, 1, "Delisting of DEL")
    ]
    create_corporate_actions(1, corporate_actions)

    ledger = Ledger()    # Build ledger to have FIFO deals table
    ledger.rebuild(from_timestamp=0)

    taxes = TaxesRus()
    tax_report = taxes.prepare_tax_report(2020, 1)
    assert tax_report == report
Example #16
0
class MainWindow(QMainWindow, Ui_JAL_MainWindow):
    def __init__(self, language):
        QMainWindow.__init__(self, None)
        self.running = False
        self.setupUi(self)
        self.restoreGeometry(base64.decodebytes(JalSettings().getValue('WindowGeometry', '').encode('utf-8')))
        self.restoreState(base64.decodebytes(JalSettings().getValue('WindowState', '').encode('utf-8')))

        self.ledger = Ledger()

        # Customize Status bar and logs
        self.ProgressBar = QProgressBar(self)
        self.StatusBar.addPermanentWidget(self.ProgressBar)
        self.ProgressBar.setVisible(False)
        self.ledger.setProgressBar(self, self.ProgressBar)
        self.Logs.setStatusBar(self.StatusBar)
        self.logger = logging.getLogger()
        self.logger.addHandler(self.Logs)
        log_level = os.environ.get('LOGLEVEL', 'INFO').upper()
        self.logger.setLevel(log_level)

        self.currentLanguage = language

        self.downloader = QuoteDownloader()
        self.statements = Statements(self)
        self.reports = Reports(self, self.mdiArea)
        self.backup = JalBackup(self, get_dbfilename(get_app_path()))
        self.estimator = None
        self.price_chart = None

        self.actionImportSlipRU.setEnabled(dependency_present(['pyzbar', 'PIL']))

        self.actionAbout = QAction(text=self.tr("About"), parent=self)
        self.MainMenu.addAction(self.actionAbout)

        self.langGroup = QActionGroup(self.menuLanguage)
        self.createLanguageMenu()

        self.statementGroup = QActionGroup(self.menuStatement)
        self.createStatementsImportMenu()

        self.reportsGroup = QActionGroup(self.menuReports)
        self.createReportsMenu()

        self.setWindowIcon(load_icon("jal.png"))

        self.connect_signals_and_slots()

        self.actionOperations.trigger()

    def connect_signals_and_slots(self):
        self.actionExit.triggered.connect(QApplication.instance().quit)
        self.actionOperations.triggered.connect(self.createOperationsWindow)
        self.actionAbout.triggered.connect(self.showAboutWindow)
        self.langGroup.triggered.connect(self.onLanguageChanged)
        self.statementGroup.triggered.connect(self.statements.load)
        self.reportsGroup.triggered.connect(self.reports.show)
        self.action_LoadQuotes.triggered.connect(partial(self.downloader.showQuoteDownloadDialog, self))
        self.actionImportSlipRU.triggered.connect(self.importSlip)
        self.actionBackup.triggered.connect(self.backup.create)
        self.actionRestore.triggered.connect(self.backup.restore)
        self.action_Re_build_Ledger.triggered.connect(partial(self.ledger.showRebuildDialog, self))
        self.actionAccountTypes.triggered.connect(partial(self.onDataDialog, "account_types"))
        self.actionAccounts.triggered.connect(partial(self.onDataDialog, "accounts"))
        self.actionAssets.triggered.connect(partial(self.onDataDialog, "assets"))
        self.actionPeers.triggered.connect(partial(self.onDataDialog, "agents"))
        self.actionCategories.triggered.connect(partial(self.onDataDialog, "categories"))
        self.actionTags.triggered.connect(partial(self.onDataDialog, "tags"))
        self.actionCountries.triggered.connect(partial(self.onDataDialog, "countries"))
        self.actionQuotes.triggered.connect(partial(self.onDataDialog, "quotes"))
        self.PrepareTaxForms.triggered.connect(partial(self.mdiArea.addSubWindow, TaxWidget(self), maximized=True))
        self.downloader.download_completed.connect(self.updateWidgets)
        self.ledger.updated.connect(self.updateWidgets)
        self.statements.load_completed.connect(self.onStatementImport)

    @Slot()
    def showEvent(self, event):
        super().showEvent(event)
        if self.running:
            return
        self.running = True
        # Call slot via queued connection so it's called from the UI thread after the window has been shown
        QMetaObject().invokeMethod(self, "afterShowEvent", Qt.ConnectionType.QueuedConnection)

    @Slot()
    def afterShowEvent(self):
        if JalSettings().getValue('RebuildDB', 0) == 1:
            if QMessageBox().warning(self, self.tr("Confirmation"), self.tr("Ledger isn't complete. Rebuild it now?"),
                                     QMessageBox.Yes, QMessageBox.No) == QMessageBox.Yes:
                self.ledger.rebuild()

    @Slot()
    def closeEvent(self, event):
        JalSettings().setValue('WindowGeometry', base64.encodebytes(self.saveGeometry()).decode('utf-8'))
        JalSettings().setValue('WindowState', base64.encodebytes(self.saveState()).decode('utf-8'))
        self.logger.removeHandler(self.Logs)    # Removing handler (but it doesn't prevent exception at exit)
        logging.raiseExceptions = False         # Silencing logging module exceptions
        super().closeEvent(event)

    def createLanguageMenu(self):
        langPath = get_app_path() + Setup.LANG_PATH + os.sep

        langDirectory = QDir(langPath)
        for language_file in langDirectory.entryList(['*.qm']):
            language_code = language_file.split('.')[0]
            language = QLocale.languageToString(QLocale(language_code).language())
            language_icon = QIcon(langPath + language_code + '.png')
            action = QAction(language_icon, language, self)
            action.setCheckable(True)
            action.setData(language_code)
            self.menuLanguage.addAction(action)
            self.langGroup.addAction(action)

    @Slot()
    def onLanguageChanged(self, action):
        language_code = action.data()
        if language_code != self.currentLanguage:
            JalSettings().setValue('Language', JalDB().get_language_id(language_code))
            QMessageBox().information(self, self.tr("Restart required"),
                                      self.tr("Language was changed to ") +
                                      QLocale.languageToString(QLocale(language_code).language()) + "\n" +
                                      self.tr("You should restart application to apply changes\n"
                                           "Application will be terminated now"),
                                      QMessageBox.Ok)
            self.close()

    # Create import menu for all known statements based on self.statements.items values
    def createStatementsImportMenu(self):
        for i, statement in enumerate(self.statements.items):
            statement_name = statement['name'].replace('&', '&&')  # & -> && to prevent shortcut creation
            if statement['icon']:
                statement_icon = load_icon(statement['icon'])
                action = QAction(statement_icon, statement_name, self)
            else:
                action = QAction(statement_name, self)
            action.setData(i)
            self.menuStatement.addAction(action)
            self.statementGroup.addAction(action)

    # Create menu entry for all known reports based on self.reports.sources values
    def createReportsMenu(self):
        for i, report in enumerate(self.reports.items):
            action = QAction(report['name'].replace('&', '&&'), self)  # & -> && to prevent shortcut creation
            action.setData(i)
            self.menuReports.addAction(action)
            self.reportsGroup.addAction(action)

    @Slot()
    def createOperationsWindow(self):
        operations_window = self.mdiArea.addSubWindow(OperationsWidget(self), maximized=True)
        operations_window.widget().dbUpdated.connect(self.ledger.rebuild)

    @Slot()
    def showAboutWindow(self):
        about_box = QMessageBox(self)
        about_box.setAttribute(Qt.WA_DeleteOnClose)
        about_box.setWindowTitle(self.tr("About"))
        title = self.tr("<h3>JAL</h3><p>Just Another Ledger, version {version}</p>".format(version=__version__))
        about_box.setText(title)
        about = self.tr("<p>More information, manuals and problem reports are at "
                        "<a href=https://github.com/titov-vv/jal>github home page</a></p>"
                        "<p>Questions, comments, help or donations:</p>"
                        "<p><a href=mailto:[email protected]>[email protected]</a></p>"
                        "<p><a href=https://t.me/jal_support>Telegram</a></p>")
        about_box.setInformativeText(about)
        about_box.show()

    def showProgressBar(self, visible=False):
        self.ProgressBar.setVisible(visible)
        self.centralwidget.setEnabled(not visible)
        self.MainMenu.setEnabled(not visible)

    @Slot()
    def importSlip(self):
        dialog = ImportSlipDialog(self)
        dialog.finished.connect(self.onSlipImportFinished)
        dialog.open()

    @Slot()
    def onSlipImportFinished(self):
        self.ledger.rebuild()

    @Slot()
    def onDataDialog(self, dlg_type):
        if dlg_type == "account_types":
            AccountTypeListDialog().exec()
        elif dlg_type == "accounts":
            AccountListDialog().exec()
        elif dlg_type == "assets":
            AssetListDialog().exec()
        elif dlg_type == "agents":
            PeerListDialog().exec()
        elif dlg_type == "categories":
            CategoryListDialog().exec()
        elif dlg_type == "tags":
            TagsListDialog().exec()
        elif dlg_type == "countries":
            CountryListDialog().exec()
        elif dlg_type == "quotes":
            QuotesListDialog().exec()
        else:
            assert False

    @Slot()
    def updateWidgets(self):
        for window in self.mdiArea.subWindowList():
            window.widget().refresh()

    @Slot()
    def onStatementImport(self, timestamp, totals):
        self.ledger.rebuild()
        for account_id in totals:
            for asset_id in totals[account_id]:
                amount = JalDB().get_asset_amount(timestamp, account_id, asset_id)
                if amount is not None:
                    if abs(totals[account_id][asset_id] - amount) <= Setup.DISP_TOLERANCE:
                        JalDB().reconcile_account(account_id, timestamp)
                        self.updateWidgets()
                    else:
                        account = JalDB().get_account_name(account_id)
                        asset = JalDB().get_asset_name(asset_id)
                        logging.warning(self.tr("Statement ending balance doesn't match: ") +
                                        f"{account} / {asset} / {amount} <> {totals[account_id][asset_id]}")