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_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, '' ]
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 ]
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 ]
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 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']]
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
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
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
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
def test_empty_ledger(prepare_db_ledger): # Build ledger from scratch ledger = Ledger() ledger.rebuild(from_timestamp=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
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
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()
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
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]}" )
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)
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]}")