├── .gitignore ├── MANIFEST.in ├── designer_plugins ├── plugin_date_range_selector.py ├── plugin_db_lookup_combobox.py ├── plugin_log_viewer.py ├── plugin_tableview_with_footer.py └── plugin_treeview_with_footer.py ├── docs ├── FAQ.md ├── FAQ.ru.md ├── LICENSE ├── README.md ├── README.ru.md ├── error_description.md ├── error_description.ru.md ├── img │ ├── fns_auth_dialog.png │ ├── investment_portfolio_holdings.png │ ├── main_linux.png │ ├── main_windows.png │ ├── one_account_view.png │ ├── report_deals.png │ ├── report_income_spending.png │ ├── report_profit_loss.png │ ├── slip_import.png │ └── stocks_and_investment_account.png ├── ru-tax-3ndfl │ ├── 3ndfl_tax_report.xlsx │ ├── IBKR_flex_sample.xml │ ├── corporate_actions.md │ ├── corporate_actions.odg │ ├── cost_basis │ │ ├── 20190416_Form8937-NVS_ALC_SpinOff.pdf │ │ ├── 20201130_Form8937_PFE_VTRS_SpinOff.pdf │ │ ├── 20220411_Form8937_T_WBD_SpinOff.pdf │ │ ├── 20230103_Form8937_GE_GEHC_SpinOff.pdf │ │ └── 20240401_Form8937_GE_GEV_SpinOff.pdf │ ├── declaration.dc0 │ ├── declaration_empty.dc0 │ ├── img │ │ ├── account_selection.png │ │ ├── bank_account.png │ │ ├── corporate_actions.png │ │ ├── declaration_1.png │ │ ├── declaration_2.png │ │ ├── declaration_3.png │ │ ├── ibkr_account.png │ │ ├── ibkr_selection_example.png │ │ ├── import_log.png │ │ ├── main_window.png │ │ ├── menu_selection.png │ │ ├── report_1.png │ │ ├── report_2.png │ │ ├── report_3.png │ │ ├── report_4.png │ │ ├── report_5.png │ │ ├── report_params.png │ │ └── update_quotes.png │ └── taxes.md └── ru-tax-currency │ ├── CBR2TAX_currency.ipynb │ └── CurrencyRates.xml ├── jal ├── __init__.py ├── compile_ui ├── constants.py ├── create_pro ├── data_export │ ├── __init__.py │ ├── dlsg.py │ ├── tax_reports │ │ ├── __init__.py │ │ ├── portugal.json │ │ ├── portugal.py │ │ ├── russia.json │ │ └── russia.py │ ├── taxes.py │ ├── taxes_flow.py │ ├── templates │ │ ├── __init__.py │ │ ├── tax_prt_dividends.json │ │ ├── tax_prt_interests.json │ │ ├── tax_prt_shares.json │ │ ├── tax_rus_bonds.json │ │ ├── tax_rus_crypto.json │ │ ├── tax_rus_derivatives.json │ │ ├── tax_rus_dividends.json │ │ ├── tax_rus_fees.json │ │ ├── tax_rus_flow.json │ │ ├── tax_rus_interests.json │ │ └── tax_rus_trades.json │ └── xlsx.py ├── data_import │ ├── __init__.py │ ├── broker_statements │ │ ├── __init__.py │ │ ├── ibkr.py │ │ ├── just2trade.py │ │ ├── kit.py │ │ ├── open_portfolio.py │ │ ├── psb.py │ │ ├── tvoy.py │ │ └── vtb.py │ ├── category_recognizer.py │ ├── import_schema.json │ ├── receipt_api │ │ ├── __init__.py │ │ ├── eu_lidl_plus.py │ │ ├── pt_pingo_doce.py │ │ ├── receipt_api.py │ │ ├── receipts.py │ │ └── ru_fns.py │ ├── shop_receipt.py │ ├── statement.py │ ├── statement_xls.py │ ├── statement_xml.py │ └── statements.py ├── db │ ├── __init__.py │ ├── account.py │ ├── asset.py │ ├── backup_restore.py │ ├── balances_model.py │ ├── category.py │ ├── closed_trade.py │ ├── country.py │ ├── db.py │ ├── deposit.py │ ├── helpers.py │ ├── holdings_model.py │ ├── ledger.py │ ├── operations.py │ ├── operations_model.py │ ├── peer.py │ ├── reference_models.py │ ├── settings.py │ ├── tag.py │ ├── tax_estimator.py │ ├── trades_model.py │ ├── tree_model.py │ └── view_model.py ├── img │ ├── aux_ibkr.png │ ├── aux_j2t.png │ ├── aux_kit.png │ ├── aux_pof.png │ ├── aux_psb.ico │ ├── aux_tvoy.png │ ├── aux_vtb.ico │ ├── flag_en.png │ ├── flag_pt.png │ ├── flag_ru.png │ ├── tag_bank.ico │ ├── tag_card.ico │ ├── tag_cash.ico │ ├── tag_investing.ico │ ├── ui_add.ico │ ├── ui_add_child.ico │ ├── ui_amortization.ico │ ├── ui_buy.ico │ ├── ui_cancel.ico │ ├── ui_chart.ico │ ├── ui_clean.ico │ ├── ui_copy.ico │ ├── ui_coupon.ico │ ├── ui_delisting.ico │ ├── ui_deposit_account.ico │ ├── ui_deposit_close.ico │ ├── ui_deposit_open.ico │ ├── ui_details.ico │ ├── ui_dividend.ico │ ├── ui_dividend_stock.ico │ ├── ui_fee.ico │ ├── ui_interest.ico │ ├── ui_jal.png │ ├── ui_list.ico │ ├── ui_merger.ico │ ├── ui_minus.ico │ ├── ui_ok.ico │ ├── ui_plus.ico │ ├── ui_remove.ico │ ├── ui_renaming.ico │ ├── ui_sell.ico │ ├── ui_spinoff.ico │ ├── ui_split.ico │ ├── ui_tag.ico │ ├── ui_tax.ico │ ├── ui_total.ico │ ├── ui_transfer_asset_in.ico │ ├── ui_transfer_asset_out.ico │ ├── ui_transfer_in.ico │ ├── ui_transfer_out.ico │ ├── ui_vesting.ico │ └── ui_with_credit.ico ├── jal.pro ├── jal.py ├── jal_init.sql ├── languages │ ├── en.qm │ ├── en.ts │ ├── ru.qm │ └── ru.ts ├── net │ ├── __init__.py │ ├── downloader.py │ ├── moex.py │ └── web_request.py ├── pypi_description.md ├── reports │ ├── __init__.py │ ├── account_balance.py │ ├── assets_payments.py │ ├── category.py │ ├── deals.py │ ├── income_spending.py │ ├── operations_base.py │ ├── peer.py │ ├── portfolio.py │ ├── profit_loss.py │ ├── reports.py │ ├── tag.py │ └── term_deposits.py ├── run_designer ├── ui │ ├── __init__.py │ ├── asset_dlg.ui │ ├── flow_export_widget.ui │ ├── login_fns_dlg.ui │ ├── login_lidl_plus_dlg.ui │ ├── login_pingo_doce_dlg.ui │ ├── main_window.ui │ ├── operations_widget.ui │ ├── quotes_update.ui │ ├── rebuild_window.ui │ ├── receipt_import_dlg.ui │ ├── reference_data_dlg.ui │ ├── reports │ │ ├── __init__.py │ │ ├── account_balance_report.ui │ │ ├── assets_payments_report.ui │ │ ├── category_report.ui │ │ ├── deals_report.ui │ │ ├── income_spending_report.ui │ │ ├── peer_report.ui │ │ ├── portfolio_report.ui │ │ ├── profit_loss_report.ui │ │ ├── tag_report.ui │ │ ├── tax_estimation.ui │ │ ├── term_deposits_report.ui │ │ ├── ui_account_balance_report.py │ │ ├── ui_assets_payments_report.py │ │ ├── ui_category_report.py │ │ ├── ui_deals_report.py │ │ ├── ui_income_spending_report.py │ │ ├── ui_peer_report.py │ │ ├── ui_portfolio_report.py │ │ ├── ui_profit_loss_report.py │ │ ├── ui_tag_report.py │ │ ├── ui_tax_estimation.py │ │ └── ui_term_deposits_report.py │ ├── select_account_dlg.ui │ ├── select_reference_dlg.ui │ ├── tax_export_widget.ui │ ├── ui_asset_dlg.py │ ├── ui_flow_export_widget.py │ ├── ui_login_fns_dlg.py │ ├── ui_login_lidl_plus_dlg.py │ ├── ui_login_pingo_doce_dlg.py │ ├── ui_main_window.py │ ├── ui_operations_widget.py │ ├── ui_rebuild_window.py │ ├── ui_receipt_import_dlg.py │ ├── ui_reference_data_dlg.py │ ├── ui_select_account_dlg.py │ ├── ui_select_reference_dlg.py │ ├── ui_tax_export_widget.py │ ├── ui_update_quotes_window.py │ └── widgets │ │ ├── __init__.py │ │ ├── asset_payment_operation.ui │ │ ├── corporate_action_operation.ui │ │ ├── income_spending_operation.ui │ │ ├── term_deposit_operation.ui │ │ ├── trade_operation.ui │ │ ├── transfer_operation.ui │ │ ├── ui_asset_payment_operation.py │ │ ├── ui_corporate_action_operation.py │ │ ├── ui_income_spending_operation.py │ │ ├── ui_term_deposit_operation.py │ │ ├── ui_trade_operation.py │ │ └── ui_transfer_operation.py ├── universal_cache.py ├── updates │ ├── __init__.py │ ├── jal_delta_40.sql │ ├── jal_delta_41.sql │ ├── jal_delta_42.sql │ ├── jal_delta_43.sql │ ├── jal_delta_44.sql │ ├── jal_delta_45.sql │ ├── jal_delta_46.sql │ ├── jal_delta_47.sql │ ├── jal_delta_48.sql │ ├── jal_delta_49.sql │ ├── jal_delta_50.sql │ ├── jal_delta_51.sql │ ├── jal_delta_52.sql │ ├── jal_delta_53.sql │ ├── jal_delta_54.sql │ ├── jal_delta_55.sql │ ├── jal_delta_56.sql │ ├── jal_delta_57.sql │ ├── jal_delta_58.sql │ └── jal_delta_59.sql └── widgets │ ├── __init__.py │ ├── abstract_operation_details.py │ ├── account_select.py │ ├── asset_dialog.py │ ├── asset_payment_widget.py │ ├── corporate_action_widget.py │ ├── custom │ ├── __init__.py │ ├── date_range_selector.py │ ├── db_lookup_combobox.py │ ├── log_viewer.py │ ├── table_footer.py │ ├── tableview_with_footer.py │ └── treeview_with_footer.py │ ├── delegates.py │ ├── helpers.py │ ├── icons.py │ ├── income_spending_widget.py │ ├── main_window.py │ ├── mdi.py │ ├── operations_tabs.py │ ├── operations_widget.py │ ├── price_chart.py │ ├── qr_scanner.py │ ├── reference_data.py │ ├── reference_dialogs.py │ ├── reference_selector.py │ ├── selection_dialog.py │ ├── tax_widget.py │ ├── term_deposit_widget.py │ ├── trade_widget.py │ └── transfer_widget.py ├── register_designer_plugins.py ├── requirements.txt ├── run.py ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── fixtures.py ├── helpers.py ├── pytest.ini ├── test_corp_actions.py ├── test_data ├── 3ndfl_2020.dc0 ├── 3ndfl_2020_empty.dc0 ├── 3ndfl_2021.dc1 ├── 3ndfl_2021_empty.dc1 ├── 3ndfl_2022.dc2 ├── 3ndfl_2022_empty.dc2 ├── deals_set.tgz ├── ibkr.json ├── ibkr.xml ├── ibkr_bond.json ├── ibkr_bond.xml ├── ibkr_cfd.json ├── ibkr_cfd.xml ├── ibkr_corp_actions.json ├── ibkr_corp_actions.xml ├── ibkr_dividends.json ├── ibkr_dividends.xml ├── ibkr_merger_complex.json ├── ibkr_merger_complex.xml ├── ibkr_merger_spinoff.json ├── ibkr_merger_spinoff.xml ├── ibkr_rights.json ├── ibkr_rights.xml ├── ibkr_spinoff.json ├── ibkr_spinoff.xml ├── ibkr_warrants.json ├── ibkr_warrants.xml ├── ibkr_year0.xml ├── ibkr_year1.xml ├── invalid_backup.tgz ├── j2t.json ├── j2t.xlsx ├── kit.json ├── kit.xlsx ├── matched.json ├── pof.json ├── pof_converted.json ├── taxes_bond_rus.json ├── taxes_cfd_rus.json ├── taxes_flow.json ├── taxes_merger_complex_rus.json ├── taxes_merger_spinoff_rus.json ├── taxes_over_years_rus.json ├── taxes_rus.json ├── taxes_spinoff_rus.json ├── taxes_vesting_rus.json ├── tvoy.json ├── tvoy.zip ├── vtb.json └── vtb.xls ├── test_dlsg.py ├── test_downloaders.py ├── test_ibkr.py ├── test_json_import.py ├── test_ledger.py ├── test_main.py ├── test_statements.py └── test_taxes_rus.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | /venv 3 | /.idea 4 | /.vscode 5 | 6 | # unit testing 7 | *.prof 8 | .coverage 9 | 10 | # generated by app 11 | /jal/jal.sqlite 12 | /jal.egg-info 13 | *.log 14 | jal_faults.log 15 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include jal/languages/*.png jal/languages/*.qm 2 | include jal/img/*.png jal/img/*.ico 3 | include jal/*.sql jal/updates/*.sql 4 | include jal/pypi_description.md 5 | recursive-exclude tests * -------------------------------------------------------------------------------- /designer_plugins/plugin_date_range_selector.py: -------------------------------------------------------------------------------- 1 | # Here reference goes from PYSIDE_DESIGNER_PLUGINS directory 2 | from jal.widgets.custom.date_range_selector import DateRangeSelector 3 | 4 | from PySide6.QtGui import QIcon 5 | from PySide6.QtDesigner import (QDesignerCustomWidgetInterface) 6 | 7 | 8 | DOM_XML = """ 9 | 10 | 11 | 12 | 13 | 0 14 | 0 15 | 300 16 | 32 17 | 18 | 19 | 20 | 21 | """ 22 | 23 | 24 | class DateRangeSelectorPlugin(QDesignerCustomWidgetInterface): 25 | def __init__(self): 26 | super().__init__() 27 | self._initialized = False 28 | 29 | def createWidget(self, parent): 30 | t = DateRangeSelector(parent) 31 | return t 32 | 33 | def domXml(self): 34 | return DOM_XML 35 | 36 | def group(self): 37 | return '' 38 | 39 | def icon(self): 40 | return QIcon() 41 | 42 | def includeFile(self): 43 | return 'jal/widgets/custom/date_range_selector.h' 44 | 45 | def initialize(self, form_editor): 46 | if self._initialized: 47 | return 48 | self._initialized = True 49 | 50 | def isContainer(self): 51 | return False 52 | 53 | def isInitialized(self): 54 | return self._initialized 55 | 56 | def name(self): 57 | return 'DateRangeSelector' 58 | 59 | def toolTip(self): 60 | return 'Widget that allows selection of dates interval' 61 | 62 | def whatsThis(self): 63 | return self.toolTip() 64 | -------------------------------------------------------------------------------- /designer_plugins/plugin_db_lookup_combobox.py: -------------------------------------------------------------------------------- 1 | # Here reference goes from PYSIDE_DESIGNER_PLUGINS directory 2 | from jal.widgets.custom.db_lookup_combobox import DbLookupComboBox 3 | 4 | from PySide6.QtGui import QIcon 5 | from PySide6.QtDesigner import (QDesignerCustomWidgetInterface) 6 | 7 | 8 | DOM_XML = """ 9 | 10 | 11 | 12 | 13 | 0 14 | 0 15 | 300 16 | 32 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | """ 31 | 32 | 33 | class DbLookupComboBoxPlugin(QDesignerCustomWidgetInterface): 34 | def __init__(self): 35 | super().__init__() 36 | self._initialized = False 37 | 38 | def createWidget(self, parent): 39 | t = DbLookupComboBox(parent) 40 | return t 41 | 42 | def domXml(self): 43 | return DOM_XML 44 | 45 | def group(self): 46 | return '' 47 | 48 | def icon(self): 49 | return QIcon() 50 | 51 | def includeFile(self): 52 | return 'jal/widgets/custom/db_lookup_combobox.h' 53 | 54 | def initialize(self, form_editor): 55 | if self._initialized: 56 | return 57 | self._initialized = True 58 | 59 | def isContainer(self): 60 | return False 61 | 62 | def isInitialized(self): 63 | return self._initialized 64 | 65 | def name(self): 66 | return 'DbLookupComboBox' 67 | 68 | def toolTip(self): 69 | return 'ComboBox to select available values from database lookup table' 70 | 71 | def whatsThis(self): 72 | return self.toolTip() 73 | -------------------------------------------------------------------------------- /designer_plugins/plugin_log_viewer.py: -------------------------------------------------------------------------------- 1 | # Here reference goes from PYSIDE_DESIGNER_PLUGINS directory 2 | from jal.widgets.custom.log_viewer import LogViewer 3 | 4 | from PySide6.QtGui import QIcon 5 | from PySide6.QtDesigner import (QDesignerCustomWidgetInterface) 6 | 7 | 8 | DOM_XML = """ 9 | 10 | 11 | 12 | 13 | 0 14 | 0 15 | 300 16 | 200 17 | 18 | 19 | 20 | 21 | """ 22 | 23 | 24 | class LogViewerPlugin(QDesignerCustomWidgetInterface): 25 | def __init__(self): 26 | super().__init__() 27 | self._initialized = False 28 | 29 | def createWidget(self, parent): 30 | t = LogViewer(parent) 31 | return t 32 | 33 | def domXml(self): 34 | return DOM_XML 35 | 36 | def group(self): 37 | return '' 38 | 39 | def icon(self): 40 | return QIcon() 41 | 42 | def includeFile(self): 43 | return 'jal/widgets/custom/log_viewer.h' 44 | 45 | def initialize(self, form_editor): 46 | if self._initialized: 47 | return 48 | self._initialized = True 49 | 50 | def isContainer(self): 51 | return False 52 | 53 | def isInitialized(self): 54 | return self._initialized 55 | 56 | def name(self): 57 | return 'LogViewer' 58 | 59 | def toolTip(self): 60 | return 'Widget to display python logger messages in UI' 61 | 62 | def whatsThis(self): 63 | return self.toolTip() 64 | -------------------------------------------------------------------------------- /designer_plugins/plugin_tableview_with_footer.py: -------------------------------------------------------------------------------- 1 | from jal.widgets.custom.tableview_with_footer import TableViewWithFooter 2 | 3 | from PySide6.QtGui import QIcon 4 | from PySide6.QtDesigner import (QDesignerCustomWidgetInterface) 5 | 6 | 7 | DOM_XML = """ 8 | 9 | 10 | 11 | 12 | 0 13 | 0 14 | 400 15 | 300 16 | 17 | 18 | 19 | 20 | """ 21 | 22 | 23 | class TableViewWithFooterPlugin(QDesignerCustomWidgetInterface): 24 | def __init__(self): 25 | super().__init__() 26 | self._initialized = False 27 | 28 | def createWidget(self, parent): 29 | t = TableViewWithFooter(parent) 30 | return t 31 | 32 | def domXml(self): 33 | return DOM_XML 34 | 35 | def group(self): 36 | return '' 37 | 38 | def icon(self): 39 | return QIcon() 40 | 41 | def includeFile(self): 42 | return 'jal/widgets/custom/tableview_with_footer.h' 43 | 44 | def initialize(self, form_editor): 45 | if self._initialized: 46 | return 47 | self._initialized = True 48 | 49 | def isContainer(self): 50 | return False 51 | 52 | def isInitialized(self): 53 | return self._initialized 54 | 55 | def name(self): 56 | return 'TableViewWithFooter' 57 | 58 | def toolTip(self): 59 | return 'QTableView that has a footer' 60 | 61 | def whatsThis(self): 62 | return self.toolTip() 63 | -------------------------------------------------------------------------------- /designer_plugins/plugin_treeview_with_footer.py: -------------------------------------------------------------------------------- 1 | from jal.widgets.custom.treeview_with_footer import TreeViewWithFooter 2 | 3 | from PySide6.QtGui import QIcon 4 | from PySide6.QtDesigner import (QDesignerCustomWidgetInterface) 5 | 6 | 7 | DOM_XML = """ 8 | 9 | 10 | 11 | 12 | 0 13 | 0 14 | 400 15 | 300 16 | 17 | 18 | 19 | 20 | """ 21 | 22 | 23 | class TreeViewWithFooterPlugin(QDesignerCustomWidgetInterface): 24 | def __init__(self): 25 | super().__init__() 26 | self._initialized = False 27 | 28 | def createWidget(self, parent): 29 | t = TreeViewWithFooter(parent) 30 | return t 31 | 32 | def domXml(self): 33 | return DOM_XML 34 | 35 | def group(self): 36 | return '' 37 | 38 | def icon(self): 39 | return QIcon() 40 | 41 | def includeFile(self): 42 | return 'jal/widgets/custom/treeview_with_footer.h' 43 | 44 | def initialize(self, form_editor): 45 | if self._initialized: 46 | return 47 | self._initialized = True 48 | 49 | def isContainer(self): 50 | return False 51 | 52 | def isInitialized(self): 53 | return self._initialized 54 | 55 | def name(self): 56 | return 'TreeViewWithFooter' 57 | 58 | def toolTip(self): 59 | return 'QTreeView that has a footer' 60 | 61 | def whatsThis(self): 62 | return self.toolTip() 63 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | 1. *Which platforms/operation systems are supported by JAL*? 4 | JAL was created as a cross-platform appication based on *python* and GUI library *[Qt](https://www.qt.io/)*. 5 | Thus, theoretically, it should work on any platform that supports python and Qt installation. 6 | Installation and usage were tested on Linux, Windows and MacOS systems. 7 | But there are some problematic enviroments sometimes, like Windows with ARM (M1) CPU (*JAL* can't be installed due to *numpy* library compilation failure). 8 | 9 | 2. *Application does not start from python! We're write in it `>>>pip install jal`, and got SyntaxError*. 10 | JAL must run not from interactive python shell but, as any other python script, by passing a name of running program to python interpreter as an argument. It's shown in examples in Readme. 11 | 12 | 3. *How to start from begining, with clean database*? 13 | You may use menu _Data -> Clean All_ to start from scratch. 14 | Or you may delete JAL database file manually (don't forget to make backup if needed!). Database file is named `jal/jal.sqlite`. If you delete it (while programm is closed, sure), next run JAL creates new, clean database. 15 | -------------------------------------------------------------------------------- /docs/FAQ.ru.md: -------------------------------------------------------------------------------- 1 | # Часто задаваемые вопросы 2 | 3 | 1. *На каких платформах/операционных системах работает JAL*? 4 | JAL создавался как кросс-платформенное приложение на базе *python* и графической библиотеки *[Qt](https://www.qt.io/)*, 5 | поэтому теоретически запуск возможен на любой платформе, которая поддерживает их установку. 6 | Установка и использование были успешно проверены на Linux, Windows и MacOS. 7 | Иногда бывают проблемы с отдельными конфигурациями устройствами. Например, не поддерживается установка *JAL* на Windows с процессором ARM (M1), т.к. в репозитории PyPi нет бинарной сборки библиотеки *numpy* и сборщик пакета пытается собрать ее из исходного кода, а для этого нужен компилятор и целый ворох дополнительных библиотек. 8 | 9 | 2. *Приложение не запускается из python! Ввели в строке `>>>pip install jal` , получили ошибку SyntaxError*. 10 | JAL должен запускаться не из интерактивного интерпретатора python, а, как и любая программа на python, путем передачи имени запускаемого файла интерпретатору в качестве аргумента. Об этом достаточно подробно написано в Readme. 11 | 12 | 3. *Как очистить базу данных?* 13 | Вы можете использовать пункт меню _Данные -> Очистить Всё_ для того, чтобы начать всё сначала. 14 | Либо можно просто удалить файл базы данных вручную (не забудьте сделать резервную копию из самой программы или просто скопировав файл, если это необходимо!). Файл базы находится зесь: `jal/jal.sqlite`. Убедитесь, что программа не запущена, удалите файл и при следующем запуске JAL будет создана новая чистая база. 15 | -------------------------------------------------------------------------------- /docs/error_description.md: -------------------------------------------------------------------------------- 1 | # Error messages 2 | 3 | ### INFO 4 | Informational messages that may help to understand what JAL does but doesn't require any special attention. 5 | 6 | 1. *Tax adjustment for dividend: X.XX -> Y.YY (divided description)* 7 | Some brokers (e.g. Interactive Brokers) change dividend tax retrospectively sometimes. This message indicates when such an event happens during broker statement import. 8 | 9 | ### WARNING 10 | These are warnings that indicate something unexpected have happened. It doesn't necessarily a problem but good practice is to check and understand the reason. 11 | 12 | ### ERROR 13 | It indicates a problem that JAL was not able to cope with. You need to check and correct it. 14 | 15 | 1. *Results value of corporate action doesn't match 100% of initial asset value. Date: ...* 16 | If security undergo a corporate action then its cost basis should be fully distributed across results of corporate action according to Company's financial statement (e.g. Form 8937 for US securities). 17 | I.e. if stock *A* is converted into stocks *B* and *C* as result of corporate action then we need to declare X% of *A's* cost basis is allocated to *B* and Y% is allocated to *C*. While having X + Y equal to 100%. 18 | In order to make it you should locate corporate action in a list of operations and set shares of assets in results table. 19 | -------------------------------------------------------------------------------- /docs/img/fns_auth_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/fns_auth_dialog.png -------------------------------------------------------------------------------- /docs/img/investment_portfolio_holdings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/investment_portfolio_holdings.png -------------------------------------------------------------------------------- /docs/img/main_linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/main_linux.png -------------------------------------------------------------------------------- /docs/img/main_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/main_windows.png -------------------------------------------------------------------------------- /docs/img/one_account_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/one_account_view.png -------------------------------------------------------------------------------- /docs/img/report_deals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/report_deals.png -------------------------------------------------------------------------------- /docs/img/report_income_spending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/report_income_spending.png -------------------------------------------------------------------------------- /docs/img/report_profit_loss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/report_profit_loss.png -------------------------------------------------------------------------------- /docs/img/slip_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/slip_import.png -------------------------------------------------------------------------------- /docs/img/stocks_and_investment_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/img/stocks_and_investment_account.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/3ndfl_tax_report.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/3ndfl_tax_report.xlsx -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/corporate_actions.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/corporate_actions.odg -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/cost_basis/20190416_Form8937-NVS_ALC_SpinOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/cost_basis/20190416_Form8937-NVS_ALC_SpinOff.pdf -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/cost_basis/20201130_Form8937_PFE_VTRS_SpinOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/cost_basis/20201130_Form8937_PFE_VTRS_SpinOff.pdf -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/cost_basis/20220411_Form8937_T_WBD_SpinOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/cost_basis/20220411_Form8937_T_WBD_SpinOff.pdf -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/cost_basis/20230103_Form8937_GE_GEHC_SpinOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/cost_basis/20230103_Form8937_GE_GEHC_SpinOff.pdf -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/cost_basis/20240401_Form8937_GE_GEV_SpinOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/cost_basis/20240401_Form8937_GE_GEV_SpinOff.pdf -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/declaration.dc0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/declaration.dc0 -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/declaration_empty.dc0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/declaration_empty.dc0 -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/account_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/account_selection.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/bank_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/bank_account.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/corporate_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/corporate_actions.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/declaration_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/declaration_1.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/declaration_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/declaration_2.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/declaration_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/declaration_3.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/ibkr_account.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/ibkr_account.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/ibkr_selection_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/ibkr_selection_example.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/import_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/import_log.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/main_window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/main_window.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/menu_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/menu_selection.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_1.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_2.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_3.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_4.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_5.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/report_params.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/report_params.png -------------------------------------------------------------------------------- /docs/ru-tax-3ndfl/img/update_quotes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-3ndfl/img/update_quotes.png -------------------------------------------------------------------------------- /docs/ru-tax-currency/CurrencyRates.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/docs/ru-tax-currency/CurrencyRates.xml -------------------------------------------------------------------------------- /jal/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2025.2.3" -------------------------------------------------------------------------------- /jal/compile_ui: -------------------------------------------------------------------------------- 1 | #!/usr/bin/bash 2 | /usr/lib/qt6/uic -g python -o ./ui/ui_main_window.py ./ui/main_window.ui 3 | /usr/lib/qt6/uic -g python -o ./ui/ui_operations_widget.py ./ui/operations_widget.ui 4 | /usr/lib/qt6/uic -g python -o ./ui/ui_rebuild_window.py ./ui/rebuild_window.ui 5 | /usr/lib/qt6/uic -g python -o ./ui/ui_update_quotes_window.py ./ui/quotes_update.ui 6 | /usr/lib/qt6/uic -g python -o ./ui/ui_reference_data_dlg.py ./ui/reference_data_dlg.ui 7 | /usr/lib/qt6/uic -g python -o ./ui/ui_tax_export_widget.py ./ui/tax_export_widget.ui 8 | /usr/lib/qt6/uic -g python -o ./ui/ui_flow_export_widget.py ./ui/flow_export_widget.ui 9 | /usr/lib/qt6/uic -g python -o ./ui/ui_asset_dlg.py ./ui/asset_dlg.ui 10 | /usr/lib/qt6/uic -g python -o ./ui/ui_select_account_dlg.py ./ui/select_account_dlg.ui 11 | /usr/lib/qt6/uic -g python -o ./ui/ui_select_reference_dlg.py ./ui/select_reference_dlg.ui 12 | /usr/lib/qt6/uic -g python -o ./ui/ui_receipt_import_dlg.py ./ui/receipt_import_dlg.ui 13 | /usr/lib/qt6/uic -g python -o ./ui/ui_login_fns_dlg.py ./ui/login_fns_dlg.ui 14 | /usr/lib/qt6/uic -g python -o ./ui/ui_login_lidl_plus_dlg.py ./ui/login_lidl_plus_dlg.ui 15 | /usr/lib/qt6/uic -g python -o ./ui/ui_login_pingo_doce_dlg.py ./ui/login_pingo_doce_dlg.ui 16 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_income_spending_operation.py ./ui/widgets/income_spending_operation.ui 17 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_asset_payment_operation.py ./ui/widgets/asset_payment_operation.ui 18 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_trade_operation.py ./ui/widgets/trade_operation.ui 19 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_transfer_operation.py ./ui/widgets/transfer_operation.ui 20 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_corporate_action_operation.py ./ui/widgets/corporate_action_operation.ui 21 | /usr/lib/qt6/uic -g python -o ./ui/widgets/ui_term_deposit_operation.py ./ui/widgets/term_deposit_operation.ui 22 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_portfolio_report.py ./ui/reports/portfolio_report.ui 23 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_tax_estimation.py ./ui/reports/tax_estimation.ui 24 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_income_spending_report.py ./ui/reports/income_spending_report.ui 25 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_assets_payments_report.py ./ui/reports/assets_payments_report.ui 26 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_term_deposits_report.py ./ui/reports/term_deposits_report.ui 27 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_account_balance_report.py ./ui/reports/account_balance_report.ui 28 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_category_report.py ./ui/reports/category_report.ui 29 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_tag_report.py ./ui/reports/tag_report.ui 30 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_peer_report.py ./ui/reports/peer_report.ui 31 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_deals_report.py ./ui/reports/deals_report.ui 32 | /usr/lib/qt6/uic -g python -o ./ui/reports/ui_profit_loss_report.py ./ui/reports/profit_loss_report.ui 33 | 34 | /usr/lib/qt6/bin/lupdate -no-obsolete jal.pro 35 | -------------------------------------------------------------------------------- /jal/create_pro: -------------------------------------------------------------------------------- 1 | echo "SOURCES = "$(find . -type f -name "*.py" -not -path "./ui/*") > jal.pro 2 | echo "FORMS = "$(find ./ui -type f -name "*.ui") >> jal.pro 3 | echo "TRANSLATIONS = languages/en.ts languages/ru.ts" >> jal.pro -------------------------------------------------------------------------------- /jal/data_export/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/data_export/tax_reports/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/data_export/tax_reports/portugal.json: -------------------------------------------------------------------------------- 1 | { 2 | "2022": { 3 | "tax_treaty": ["ad", "ae","ao","at","bb", "be", "bg","bh", "br", "ca", "ch", "ci", "cl", "co", "cn", "cv", "cu", "cy", "cz", "de", "dk", "dz", "ee", "es", "et", "fr", "gb", "ge", "gr", "gw", "hk", "hr", "hu", "id", "ie", "il", "in", "is", "it", "jp", "ke", "kr", "kw", "lv", "lt", "lu", "ma", "md", "me", "mo", "mt", "mx", "mz", "nl", "no", "om", "pa", "pe", "pk", "pl", "qa", "ro", "ru", "sa", "se", "sg", "si", "sk", "sm", "sn", "st", "tl", "tn", "tr", "ua", "us", "uy", "ve", "vn", "za"] 4 | }, 5 | "2023": { 6 | "tax_treaty": ["ad", "ae","ao","at","bb", "be", "bg","bh", "br", "ca", "ch", "ci", "cl", "co", "cn", "cv", "cu", "cy", "cz", "de", "dk", "dz", "ee", "es", "et", "fr", "gb", "ge", "gr", "gw", "hk", "hr", "hu", "id", "ie", "il", "in", "is", "it", "jp", "ke", "kr", "kw", "lv", "lt", "lu", "ma", "md", "me", "mo", "mt", "mx", "mz", "nl", "no", "om", "pa", "pe", "pk", "pl", "qa", "ro", "ru", "sa", "se", "sg", "si", "sk", "sm", "sn", "st", "tl", "tn", "tr", "ua", "us", "uy", "ve", "vn", "za"] 7 | }, 8 | "2024": { 9 | "tax_treaty": ["ad", "ae","ao","at","bb", "be", "bg","bh", "br", "ca", "ch", "ci", "cl", "co", "cn", "cv", "cu", "cy", "cz", "de", "dk", "dz", "ee", "es", "et", "fr", "gb", "ge", "gr", "gw", "hk", "hr", "hu", "id", "ie", "il", "in", "is", "it", "jp", "ke", "kr", "kw", "lv", "lt", "lu", "ma", "md", "me", "mo", "mt", "mx", "mz", "nl", "no", "om", "pa", "pe", "pk", "pl", "qa", "ro", "ru", "sa", "se", "sg", "si", "sk", "sm", "sn", "st", "tl", "tn", "tr", "ua", "us", "uy", "ve", "vn", "za"] 10 | } 11 | } -------------------------------------------------------------------------------- /jal/data_export/tax_reports/russia.json: -------------------------------------------------------------------------------- 1 | { 2 | "2020": { 3 | "tax_treaty": ["al", "am", "ar", "at", "au", "az", "be", "bg", "bw", "by", "ca", "ch", "cl", "cn", "cu", "cy", "cz", "de", "dk", "dz", "eg", "es", "fi", "fr", "gb", "gr", "hr", "hu", "id", "ie", "il", "in", "ir", "is", "it", "jp", "kg", "kp", "kr", "kw", "kz", "lb", "lk", "lt", "lu", "lv", "ma", "md", "me", "mk", "ml", "mn", "mt", "mx", "my", "na", "nl", "no", "nz", "ph", "pl", "pt", "qa", "rs", "sa", "se", "sg", "si", "sk", "sy", "th", "tj", "tm", "tr", "ua", "us", "uz", "ve", "vn", "za"] 4 | }, 5 | "2021": { 6 | "tax_treaty": ["al", "am", "ar", "at", "au", "az", "be", "bg", "bw", "by", "ca", "ch", "cl", "cn", "cu", "cy", "cz", "de", "dk", "dz", "eg", "es", "fi", "fr", "gb", "gr", "hr", "hu", "id", "ie", "il", "in", "ir", "is", "it", "jp", "kg", "kp", "kr", "kw", "kz", "lb", "lk", "lt", "lu", "lv", "ma", "md", "me", "mk", "ml", "mn", "mt", "mx", "my", "na", "nl", "no", "nz", "ph", "pl", "pt", "qa", "rs", "sa", "se", "sg", "si", "sk", "sy", "th", "tj", "tm", "tr", "ua", "us", "uz", "ve", "vn", "za"] 7 | }, 8 | "2022": { 9 | "tax_treaty": ["al", "am", "ar", "at", "au", "az", "be", "bg", "bw", "by", "ca", "ch", "cl", "cn", "cu", "cy", "cz", "de", "dk", "dz", "eg", "es", "fi", "fr", "gb", "gr", "hr", "hu", "id", "ie", "il", "in", "ir", "is", "it", "jp", "kg", "kp", "kr", "kw", "kz", "lb", "lk", "lt", "lu", "ma", "md", "me", "mk", "ml", "mn", "mt", "mx", "my", "na", "no", "nz", "ph", "pl", "pt", "qa", "rs", "sa", "se", "sg", "si", "sk", "sy", "th", "tj", "tm", "tr", "us", "uz", "ve", "vn", "za"] 10 | }, 11 | "2023": { 12 | "tax_treaty": ["am", "ar", "az", "bw", "by", "cl", "cn", "cu", "dz", "eg", "id", "il", "in", "ir", "kg", "kp", "kw", "kz", "lb", "lk", "ma", "md", "ml", "mn", "mx", "my", "na", "ph", "qa", "rs", "sa", "sy", "th", "tj", "tm", "tr", "uz", "ve", "vn", "za"] 13 | }, 14 | "2024": { 15 | "tax_treaty": ["am", "ar", "az", "bw", "by", "cl", "cn", "cu", "dz", "eg", "id", "il", "in", "ir", "kg", "kp", "kw", "kz", "lb", "lk", "ma", "md", "ml", "mn", "mx", "my", "na", "ph", "qa", "rs", "sa", "sy", "th", "tj", "tm", "tr", "uz", "ve", "vn", "za"] 16 | } 17 | } -------------------------------------------------------------------------------- /jal/data_export/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/data_export/templates/__init__.py -------------------------------------------------------------------------------- /jal/data_export/templates/tax_prt_dividends.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Dividends", 3 | "title": "Dividends report", 4 | "headers": [ 5 | "Period: {parameters[period]}", 6 | "Account owner: ", 7 | "Account: {parameters[account]}" 8 | ], 9 | "columns": [ 10 | {"name": "Payment date", "width": 10}, 11 | {"name": "Stock symbol", "width": 8}, 12 | {"name": "ISIN", "width": 11}, 13 | {"name": "Payer name", "width": 40}, 14 | {"name": "EUR/{parameters[currency]} rate for payment date", "width": 16}, 15 | {"name": "Amount, {parameters[currency]}", "width": 12}, 16 | {"name": "Amount, EUR", "width": 12}, 17 | {"name": "Tax withheld, {parameters[currency]}", "width": 12}, 18 | {"name": "Tax withheld, EUR", "width": 12}, 19 | {"name": "Country", "width": 20}, 20 | {"name": "Tax treaty", "width": 7} 21 | ], 22 | "columns_numbered": true, 23 | "dividend": { 24 | "rows": [ 25 | ["payment_date", "symbol", "isin", "full_name", "rate", "amount", "amount_eur", "tax", "tax_eur", "country", "tax_treaty", "note"] 26 | ], 27 | "formats": [ 28 | ["D", "T", "T", "T", "N:4", "N:2", "N:2", "N:2", "N:2", "T", "T", "-"] 29 | ] 30 | }, 31 | "totals": { 32 | "rows": [ 33 | [null, null, null, null, "TOTAL", "amount", "amount_eur", "tax", "tax_eur", null, null] 34 | ], 35 | "formats": [ 36 | [null, null, null, null, "F", "F", "F", "F", "F", null, null] 37 | ] 38 | }, 39 | "footers": [] 40 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_prt_interests.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Interest", 3 | "title": "Interests report", 4 | "headers": [ 5 | "Period: {parameters[period]}", 6 | "Account owner: ", 7 | "Account: {parameters[account]}" 8 | ], 9 | "columns": [ 10 | {"name": "Description", "width": 50}, 11 | {"name": "Payment date", "width": 10}, 12 | {"name": "EUR/{parameters[currency]} rate for payment date", "width": 10}, 13 | {"name": "Amount, {parameters[currency]}", "width": 10}, 14 | {"name": "Amount, EUR", "width": 10} 15 | ], 16 | "columns_numbered": true, 17 | "interest": { 18 | "rows": [ 19 | ["note", "payment_date", "rate", "amount", "amount_eur"] 20 | ], 21 | "formats": [ 22 | ["T", "D", "N:4", "N:2", "N:2"] 23 | ] 24 | }, 25 | "totals": { 26 | "rows": [ 27 | [null, null, "ИТОГО", "amount", "amount_eur"] 28 | ], 29 | "formats": [ 30 | [null, null, "F", "F", "F"] 31 | ] 32 | }, 33 | "footers": [] 34 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_prt_shares.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Shares", 3 | "title": "Shares sales report", 4 | "headers": [ 5 | "Period: {parameters[period]}", 6 | "Account owner: ", 7 | "Account: {parameters[account]}" 8 | ], 9 | "columns": [ 10 | {"name": "Symbol", "width": 8}, 11 | {"name": "ISIN", "width": 11}, 12 | {"name": "Operation", "width": 8}, 13 | {"name": "Operation #", "width": 10}, 14 | {"name": "Operation date", "width": 10}, 15 | {"name": "Qty.", "width": 8}, 16 | {"name": "Price, {parameters[currency]}", "width": 12}, 17 | {"name": "Value, {parameters[currency]}", "width": 12}, 18 | {"name": "Fee, {parameters[currency]}", "width": 12}, 19 | {"name": "Profit/Loss, {parameters[currency]}", "width": 10}, 20 | {"name": "EUR/{parameters[currency]} rate", "width": 9}, 21 | {"name": "Value, EUR", "width": 12}, 22 | {"name": "Fee, EUR", "width": 12}, 23 | {"name": "Profit/Loss, EUR", "width": 10}, 24 | {"name": "Note", "width": 25} 25 | ], 26 | "columns_numbered": true, 27 | "trade": { 28 | "rows": [ 29 | ["symbol", "isin", "o_type", "o_number", "o_date", "qty", "o_price", "o_amount", "o_fee", "profit", "rate", "o_amount_eur", "o_fee_eur", "profit_eur", "note"], 30 | [null, null, "c_type", "c_number", "c_date", null, "c_price", "c_amount", "c_fee", null, null, "c_amount_eur", "c_fee_eur", null, null ] 31 | ], 32 | "formats": [ 33 | ["T", "N:0", "T", "T", "D", "T", "N:6", "N:2", "N:6", "N:2", "N:4", "N:2", "N:2", "N:2", "T" ], 34 | [null, null, "T", "T", "D", null, "N:6", "N:2", "N:6", null, null, "N:2", "N:2", null, null] 35 | ], 36 | "span": [ 37 | [{"h": 0, "v": 1}, {"h": 0, "v": 1}, null, null, null, {"h": 0, "v": 1}, null, null, null, {"h": 0, "v": 1}, {"h": 0, "v": 1}, null, null, {"h": 0, "v": 1}, {"h": 0, "v": 1}], 38 | [null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ] 39 | ] 40 | }, 41 | "totals": { 42 | "rows": [ 43 | [null, null, null, null, null, null, null, null, "TOTAL", "profit", " ", " ", "profit_eur", null, null] 44 | ], 45 | "formats": [ 46 | [null, null, null, null, null, null, null, null, "F", "F", "F", "F", "F", null, null] 47 | ] 48 | }, 49 | "footers": [] 50 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_rus_dividends.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Дивиденды", 3 | "title": "Отчет по дивидендам, полученным в отчетном периоде", 4 | "headers": [ 5 | "Документ-основание: ", 6 | "Период: {parameters[period]}", 7 | "ФИО: ", 8 | "Номер счета: {parameters[account]}" 9 | ], 10 | "columns": [ 11 | {"name": "Дата выплаты", "width": 10}, 12 | {"name": "Ценная бумага", "width": 8}, 13 | {"name": "ISIN", "width": 11}, 14 | {"name": "Полное наименование", "width": 40}, 15 | {"name": "Курс {parameters[currency]}/RUB на дату выплаты", "width": 16}, 16 | {"name": "Доход, {parameters[currency]}", "width": 12}, 17 | {"name": "Доход, RUB (код 1010)", "width": 12}, 18 | {"name": "Налог упл., {parameters[currency]}", "width": 12}, 19 | {"name": "Налог упл., RUB", "width": 12}, 20 | {"name": "Налог к уплате, RUB", "width": 12}, 21 | {"name": "Страна", "width": 20}, 22 | {"name": "СОИДН", "width": 7} 23 | ], 24 | "columns_numbered": true, 25 | "dividend": { 26 | "rows": [ 27 | ["payment_date", "symbol", "isin", "full_name", "rate", "amount", "amount_rub", "tax", "tax_rub", "tax2pay", "country", "tax_treaty", "note"] 28 | ], 29 | "formats": [ 30 | ["D", "T", "T", "T", "N:4", "N:2", "N:2", "N:2", "N:2", "N:2", "T", "T", "-"] 31 | ] 32 | }, 33 | "totals": { 34 | "rows": [ 35 | [null, null, null, null, "ИТОГО", "amount", "amount_rub", "tax", "tax_rub", "tax2pay", null, null] 36 | ], 37 | "formats": [ 38 | [null, null, null, null, "F", "F", "F", "F", "F", "F", null, null] 39 | ] 40 | }, 41 | "footers": [ 42 | "Описание данных в столбцах таблицы", 43 | "1 - Дата, в которую дивиденд был зачислен на счет согласно отчету брокера", 44 | "2 - Краткое наименование ценной бумаги", 45 | "3 - Международный идентификационный код ценной бумаги", 46 | "4 - Полное наименование ценной бумаги", 47 | "5 - Официальный курс валюты выплаты, установленный ЦБ РФ на дату выплаты дивиденда", 48 | "6 - Сумма выплаченного дивиденда в валюте счета", 49 | "7 - Сумма выплаченного дивиденда в рублях по курсу ЦБ РФ на дату выплаты (= Столбец 5 x Столбец 6)", 50 | "8 - Сумма налога, удержанная эмитентом, в валюте счета", 51 | "9 - Сумма налога, удержанная эмитентом, в рублях по курсу ЦБ РФ на дату удержания (= Столбец 5 x Столбец 8)", 52 | "10 - Сумма налога, подлежащая уплате в РФ (= 13% от Столбца 7 - Столбец 9)", 53 | "11 - Страна регистрации эмитента ценной бумаги ", 54 | "12 - Наличие у Российской Федерации договора об избежании двойного налогообложения со страной эмитента" 55 | ] 56 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_rus_fees.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Комиссии", 3 | "title": "Отчет по комиссиям и прочим платежам в отчетном периоде", 4 | "headers": [ 5 | "Документ-основание: ", 6 | "Период: {parameters[period]}", 7 | "ФИО: ", 8 | "Номер счета: {parameters[account]}" 9 | ], 10 | "columns": [ 11 | {"name": "Описание", "width": 50}, 12 | {"name": "Дата оплаты", "width": 10}, 13 | {"name": "Курс {parameters[currency]}/RUB на дату оплаты", "width": 10}, 14 | {"name": "Сумма, {parameters[currency]}", "width": 10}, 15 | {"name": "Сумма, RUB", "width": 10} 16 | ], 17 | "columns_numbered": true, 18 | "fee": { 19 | "rows": [ 20 | ["note", "payment_date", "rate", "amount", "amount_rub"] 21 | ], 22 | "formats": [ 23 | ["T", "D", "N:4", "N:2", "N:2"] 24 | ] 25 | }, 26 | "totals": { 27 | "rows": [ 28 | [null, null, "ИТОГО", "amount", "amount_rub"] 29 | ], 30 | "formats": [ 31 | [null, null, "F", "F", "F"] 32 | ] 33 | }, 34 | "footers": [ 35 | "Описание данных в столбцах таблицы", 36 | "1 - Описание платежа", 37 | "2 - Дата платежа", 38 | "3 - Официальный курс валюты, установленный ЦБ РФ на дату платежа", 39 | "4 - Сумма платежа в валюте счёта", 40 | "5 - Сумма платежа в рублях (= Столбец 2 * Столбец 4)" 41 | ] 42 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_rus_flow.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "ОоДДС", 3 | "title": "Отчет о движении средств и стоимости иных финансовых активов", 4 | "headers": [ 5 | "Период: {parameters[period]}" 6 | ], 7 | "columns": [ 8 | {"name": "Номер счёта", "width": 20}, 9 | {"name": "Название счёта", "width": 20}, 10 | {"name": "Валюта", "width": 20}, 11 | {"name": "Описание", "width": 20}, 12 | {"name": "Остаток начало", "width": 10}, 13 | {"name": "Зачислено", "width": 10}, 14 | {"name": "Списано", "width": 10}, 15 | {"name": "Остаток конец", "width": 10} 16 | ], 17 | "columns_numbered": true, 18 | "account_lines": { 19 | "rows": [ 20 | ["account", "account_name","currency", "money", "money_begin", "money_in", "money_out", "money_end"], 21 | [null, null, null, "assets", "assets_begin", "assets_in", "assets_out", "assets_end"] 22 | ], 23 | "formats": [ 24 | ["T", "T", "T", "T", "N:3", "N:3", "N:3", "N:3"], 25 | [null, null, null, "T", "N:3", "N:3", "N:3", "N:3"] 26 | ], 27 | "span": [ 28 | [{"h": 0, "v": 1}, {"h": 0, "v": 1}, {"h": 0, "v": 1}, null, null, null, null, null], 29 | [null, null, null, null, null, null, null, null] 30 | ] 31 | }, 32 | "footers": [ 33 | ] 34 | } -------------------------------------------------------------------------------- /jal/data_export/templates/tax_rus_interests.json: -------------------------------------------------------------------------------- 1 | { 2 | "page": "Проценты", 3 | "title": "Отчет по процентам и прочим выплатам, выплаченным в отчетном периоде", 4 | "headers": [ 5 | "Документ-основание: ", 6 | "Период: {parameters[period]}", 7 | "ФИО: ", 8 | "Номер счета: {parameters[account]}" 9 | ], 10 | "columns": [ 11 | {"name": "Описание", "width": 50}, 12 | {"name": "Дата выплаты", "width": 10}, 13 | {"name": "Курс {parameters[currency]}/RUB на дату выплаты", "width": 10}, 14 | {"name": "Сумма, {parameters[currency]}", "width": 10}, 15 | {"name": "Доход, RUB (код 1011)", "width": 10}, 16 | {"name": "Налог, RUB", "width": 10} 17 | ], 18 | "columns_numbered": true, 19 | "interest": { 20 | "rows": [ 21 | ["note", "payment_date", "rate", "amount", "amount_rub", "tax_rub"] 22 | ], 23 | "formats": [ 24 | ["T", "D", "N:4", "N:2", "N:2", "N:2"] 25 | ] 26 | }, 27 | "totals": { 28 | "rows": [ 29 | [null, null, "ИТОГО", "amount", "amount_rub", "tax_rub"] 30 | ], 31 | "formats": [ 32 | [null, null, "F", "F", "F", "F"] 33 | ] 34 | }, 35 | "footers": [ 36 | "Описание данных в столбцах таблицы", 37 | "1 - Описание платежа", 38 | "2 - Дата платежа", 39 | "3 - Официальный курс валюты, установленный ЦБ РФ на дату платежа", 40 | "4 - Сумма платежа в валюте счёта", 41 | "5 - Сумма дохода в рублях (= Столбец 3 * Столбец 4)", 42 | "6 - Сумма налога к уплате (13% от столбца 5)" 43 | ] 44 | } -------------------------------------------------------------------------------- /jal/data_import/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/data_import/broker_statements/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/data_import/broker_statements/__init__.py -------------------------------------------------------------------------------- /jal/data_import/import_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "period": { 5 | "type": "array", 6 | "minItems": 2, 7 | "maxItems": 2 8 | }, 9 | "accounts": { 10 | "type": "array" 11 | }, 12 | "assets": { 13 | "type": "array" 14 | }, 15 | "trades": { 16 | "type": "array" 17 | }, 18 | "income_spending": { 19 | "type": "array" 20 | }, 21 | "transfers": { 22 | "type": "array" 23 | }, 24 | "corporate_actions": { 25 | "type": "array" 26 | }, 27 | "asset_payments": { 28 | "type": "array" 29 | } 30 | }, 31 | "required": [ 32 | "period", 33 | "accounts", 34 | "assets" 35 | ] 36 | } -------------------------------------------------------------------------------- /jal/data_import/receipt_api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/data_import/receipt_api/__init__.py -------------------------------------------------------------------------------- /jal/data_import/receipt_api/receipt_api.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QObject, Signal, QDateTime 2 | from PySide6.QtWidgets import QApplication 3 | 4 | 5 | #----------------------------------------------------------------------------------------------------------------------- 6 | # Base parent class for various API used by JAL for downloading information about purchases (slips) 7 | class ReceiptAPI(QObject): 8 | slip_load_failed = Signal() 9 | slip_load_ok = Signal() 10 | 11 | def __init__(self): 12 | super().__init__() 13 | 14 | def tr(self, text): 15 | return QApplication.translate("ReceiptAPI", text) 16 | 17 | # Provides a list of parameters required for slip query if manual input is in use in form of dictionary 18 | # { "parameter_name" : "parameter_type" } 19 | @staticmethod 20 | def parameters_list() -> dict: 21 | raise NotImplementedError(f"parameters_list() shouldn't be called for ReceiptAPI class") 22 | 23 | # Method performs required actions to have active API session that may be used for queries 24 | # Returns True after successful activation and False otherwise 25 | def activate_session(self) -> bool: 26 | raise NotImplementedError(f"activate_session() method is not implemented in {type(self).__name__}") 27 | 28 | # Request slip data via API 29 | def query_slip(self): 30 | raise NotImplementedError(f"query_slip() method is not implemented in {type(self).__name__}") 31 | 32 | # Returns a list of purchased items (as dictionaries with name, price, amount, etc) 33 | def slip_lines(self) -> list: 34 | raise NotImplementedError(f"slip_lines() method is not implemented in {type(self).__name__}") 35 | 36 | # Returns a shop name where purchase was done (if it is possible to get) 37 | def shop_name(self) -> str: 38 | raise NotImplementedError(f"shop_name() method is not implemented in {type(self).__name__}") 39 | 40 | # Returns data/time of the operation from the receipt 41 | def datetime(self) -> QDateTime: 42 | raise NotImplementedError(f"datetime() method is not implemented in {type(self).__name__}") 43 | -------------------------------------------------------------------------------- /jal/data_import/receipt_api/receipts.py: -------------------------------------------------------------------------------- 1 | import re 2 | from PySide6.QtCore import QObject 3 | from jal.data_import.receipt_api.ru_fns import ReceiptRuFNS 4 | from jal.data_import.receipt_api.eu_lidl_plus import ReceiptEuLidlPlus 5 | from jal.data_import.receipt_api.pt_pingo_doce import ReceiptPtPingoDoce 6 | from jal.widgets.qr_scanner import ScanDialog, QRScanner 7 | 8 | 9 | # ---------------------------------------------------------------------------------------------------------------------- 10 | # Possible values that may be used by factory 11 | RU_FNS_API = 'RU_FNS' 12 | EU_LIDL_PLUS_API = 'EU_LIDL_PLUS' 13 | PT_PINGO_DOCE_API = 'PT_PINGO_DOCE' 14 | 15 | 16 | # ---------------------------------------------------------------------------------------------------------------------- 17 | class ReceiptAPIFactory(QObject): 18 | def __init__(self): 19 | super().__init__() 20 | self._apis = { 21 | RU_FNS_API: ReceiptRuFNS, 22 | EU_LIDL_PLUS_API: ReceiptEuLidlPlus, 23 | PT_PINGO_DOCE_API: ReceiptPtPingoDoce 24 | } 25 | self.supported_names = { 26 | RU_FNS_API: self.tr("Russian receipt"), 27 | EU_LIDL_PLUS_API: self.tr("European Lidl receipt"), 28 | PT_PINGO_DOCE_API: self.tr("Portuguese Pingo Doce receipt") 29 | } 30 | self._pt_nifs = { 31 | '503340855': EU_LIDL_PLUS_API, 32 | '500829993': PT_PINGO_DOCE_API 33 | } 34 | 35 | def get_api_parameters(self, api_type): 36 | api = self._apis.get(api_type) 37 | return api.parameters_list() 38 | 39 | # Selects required API class based on QR data pattern 40 | def get_api_for_qr(self, qr_text): 41 | extra_data='' 42 | api_type = self._detect_api_id_by_qr(qr_text) 43 | if api_type == EU_LIDL_PLUS_API: 44 | extra_data = ScanDialog.execute_scan(code_type=QRScanner.TYPE_ITF, 45 | message=self.tr("Please scan flat barcode from the receipt")) 46 | if extra_data is None: 47 | extra_data = '' # keep empty value to allow manual input 48 | api = self._apis.get(api_type) 49 | return api(qr_text=qr_text, aux_data=extra_data) 50 | 51 | def _detect_api_id_by_qr(self, qr_text): 52 | ru_fns_keys = ["i=", "n=", "s=", "t=", "fn=", "fp="] 53 | pt_at_pattern = r"A:(?P.{1,9})\*B:.{1,30}\*C:PT\*D:FS\*E:N\*F:\d{8}\*G:.{1,30}\*H:.{1,70}\*I1:PT\*.*" 54 | if all([x in qr_text for x in ru_fns_keys]): 55 | return RU_FNS_API 56 | parts = re.match(pt_at_pattern, qr_text) 57 | if parts is not None: 58 | NIF = parts.groupdict()['NIF'] 59 | try: 60 | return self._pt_nifs[NIF] 61 | except KeyError: 62 | raise ValueError(self.tr("Portuguese QR recognized but shop isn't supported, NIF: ") + f"{NIF}") 63 | raise ValueError(self.tr("No API found for QR data: ") + f"'{qr_text}'") 64 | 65 | def get_api_with_params(self, api_type, params): 66 | api = self._apis.get(api_type) 67 | return api(params=params) 68 | -------------------------------------------------------------------------------- /jal/db/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/db/country.py: -------------------------------------------------------------------------------- 1 | from jal.db.db import JalDB 2 | from jal.db.settings import JalSettings 3 | 4 | 5 | class JalCountry(JalDB): 6 | db_cache = [] 7 | 8 | def __init__(self, country_id: int = 0, data: dict = None, search=False) -> None: 9 | super().__init__(cached=True) 10 | if not JalCountry.db_cache: 11 | self._fetch_data() 12 | self._id = country_id 13 | if self._valid_data(data): 14 | if search: 15 | self._id = self._find_country(data) 16 | try: 17 | self._data = [x for x in self.db_cache if x['id'] == self._id][0] 18 | except IndexError: 19 | self._data = None 20 | self._name = self._data['name'] if self._data is not None else None 21 | self._code = self._data['code'] if self._data is not None else None 22 | self._iso_code = self._data['iso_code'] if self._data is not None else None 23 | 24 | def invalidate_cache(self): 25 | self._fetch_data() 26 | 27 | # JalCountry maintains single cache available for all instances 28 | @classmethod 29 | def class_cache(cls) -> True: 30 | return True 31 | 32 | def _fetch_data(self): 33 | JalCountry.db_cache = [] 34 | query = self._exec("SELECT * FROM countries_ext ORDER BY id") 35 | while query.next(): 36 | JalCountry.db_cache.append(self._read_record(query, named=True)) 37 | 38 | def id(self) -> int: 39 | return self._id 40 | 41 | # Returns country name in given language or in current interface language if no argument is given 42 | def name(self, language: str='') -> str: 43 | if not language: 44 | language = JalSettings().getLanguage() 45 | return self._read("SELECT c.name FROM country_names c LEFT JOIN languages l ON c.language_id=l.id " 46 | "WHERE country_id=:country_id AND l.language=:language", 47 | [(":country_id", self._id), (":language", language)]) 48 | 49 | def code(self) -> str: 50 | return self._code 51 | 52 | def iso_code(self) -> str: 53 | return self._iso_code 54 | 55 | def _valid_data(self, data: dict) -> bool: 56 | if data is None: 57 | return False 58 | if 'code' not in data: 59 | return False 60 | return True 61 | 62 | def _find_country(self, data: dict) -> int: 63 | country_id = self._read("SELECT id FROM countries WHERE code=:code", 64 | [(":code", data['code'])], check_unique=True) 65 | if country_id is None: 66 | return 0 67 | else: 68 | return country_id -------------------------------------------------------------------------------- /jal/db/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import auto 3 | from jal.db.db import JalDB 4 | from PySide6.QtCore import QStandardPaths, QFileInfo 5 | from jal.constants import Setup 6 | 7 | class FolderFor: 8 | Statement = auto() 9 | Report = auto() 10 | 11 | class JalSettings(JalDB): 12 | __RECENT_PREFIX = "RecentFolder_" 13 | __folders = { 14 | FolderFor.Statement: "Statement", 15 | FolderFor.Report: "Report" 16 | } 17 | 18 | def __init__(self): 19 | super().__init__() 20 | 21 | @staticmethod 22 | def path(path_type) -> str: 23 | app_path = JalDB.get_app_path() 24 | if path_type == JalDB.PATH_APP: 25 | return app_path 26 | if path_type == JalDB.PATH_DB_FILE: 27 | return JalDB.get_db_path() 28 | if path_type == JalDB.PATH_LANG: 29 | return app_path + Setup.LANG_PATH + os.sep 30 | if path_type == JalDB.PATH_ICONS: 31 | return app_path + Setup.ICONS_PATH + os.sep 32 | if path_type == JalDB.PATH_LANG_FILE: 33 | return app_path + Setup.LANG_PATH + os.sep + JalSettings().getLanguage() + '.qm' 34 | if path_type == JalDB.PATH_TAX_REPORT_TEMPLATE: 35 | return app_path + Setup.EXPORT_PATH + os.sep + Setup.TAX_REPORT_PATH + os.sep 36 | if path_type == JalDB.PATH_TEMPLATES: 37 | return app_path + Setup.EXPORT_PATH + os.sep + Setup.TEMPLATE_PATH + os.sep 38 | 39 | def getValue(self, key, default=None): 40 | value = self._read("SELECT value FROM settings WHERE name=:key", [(":key", key)]) 41 | if value is None: 42 | value = default 43 | try: 44 | return int(value) # Try to provide integer if conversion is possible 45 | except ValueError: 46 | return value 47 | 48 | def setValue(self, key, value): 49 | self._exec("INSERT OR REPLACE INTO settings(name, value) VALUES(:key, :value)", 50 | [(":key", key), (":value", value)], commit=True) 51 | 52 | # Returns 2-letter language code that corresponds to current 'Language' settings in DB 53 | def getLanguage(self): 54 | lang_id = self.getValue('Language', default=1) 55 | return self._read("SELECT language FROM languages WHERE id = :language_id", [(':language_id', lang_id)]) 56 | 57 | # Set 'Language' setting in DB that corresponds to given 2-letter language code 58 | def setLanguage(self, language_code): 59 | lang_id = self._read("SELECT id FROM languages WHERE language = :language_code", 60 | [(':language_code', language_code)]) 61 | self.setValue('Language', lang_id) 62 | 63 | def getRecentFolder(self, folder_type: int, default: str=''): 64 | folder = self.getValue(self.__RECENT_PREFIX + self.__folders[folder_type]) 65 | if not folder: 66 | folder = QStandardPaths.writableLocation(QStandardPaths.DocumentsLocation) 67 | if not folder: 68 | folder = default 69 | return folder 70 | 71 | def setRecentFolder(self, folder_type: int, folder: str): 72 | file_info = QFileInfo(folder) 73 | if file_info.isDir(): 74 | path = file_info.absoluteFilePath() 75 | else: 76 | path = file_info.absolutePath() 77 | self.setValue(self.__RECENT_PREFIX + self.__folders[folder_type], path) 78 | -------------------------------------------------------------------------------- /jal/db/tag.py: -------------------------------------------------------------------------------- 1 | from jal.db.db import JalDB 2 | from jal.constants import AssetData 3 | 4 | class JalTag(JalDB): 5 | db_cache = [] 6 | 7 | def __init__(self, tag_id: int = 0) -> None: 8 | super().__init__(cached=True) 9 | if not JalTag.db_cache: 10 | self._fetch_data() 11 | self._id = tag_id 12 | try: 13 | self._data = [x for x in self.db_cache if x['id'] == self._id][0] 14 | except IndexError: 15 | self._data = None 16 | self._name = self._data['tag'] if self._data is not None else '' 17 | self._iconfile = self._data['icon_file'] if self._data is not None else '' 18 | 19 | def invalidate_cache(self): 20 | self._fetch_data() 21 | 22 | # JalCountry maintains single cache available for all instances 23 | @classmethod 24 | def class_cache(cls) -> True: 25 | return True 26 | 27 | # Returns a dict {tag_id: 'icon_filename'} of all tags that have icons assigned 28 | @classmethod 29 | def icon_files(cls) -> dict: 30 | icons = {} 31 | query = cls._exec("SELECT id, icon_file FROM tags WHERE icon_file!=''") 32 | while query.next(): 33 | tag_id, filename = cls._read_record(query) 34 | icons[tag_id] = filename 35 | return icons 36 | 37 | def _fetch_data(self): 38 | JalTag.db_cache = [] 39 | query = self._exec("SELECT * FROM tags ORDER BY id") 40 | while query.next(): 41 | JalTag.db_cache.append(self._read_record(query, named=True)) 42 | 43 | def id(self) -> int: 44 | return self._id 45 | 46 | # Returns country name in given language or in current interface language if no argument is given 47 | def name(self) -> str: 48 | return self._name 49 | 50 | # Returns the name of icon file that is assigned to the tag 51 | def icon(self) -> str: 52 | return self._iconfile 53 | 54 | def replace_with(self, new_id): 55 | self._exec("UPDATE action_details SET tag_id=:new_id WHERE tag_id=:old_id", 56 | [(":new_id", new_id), (":old_id", self._id)]) 57 | self._exec("UPDATE asset_data SET value=:new_id WHERE datatype=:tag AND value=:old_id", 58 | [(":tag", AssetData.Tag), (":new_id", str(new_id)), (":old_id", self._id)]) 59 | self._exec("DELETE FROM tags WHERE id=:old_id", [(":old_id", self._id)], commit=True) 60 | JalDB().invalidate_cache() # Full DB as it impacts JalAsset cache also 61 | self._id = 0 62 | -------------------------------------------------------------------------------- /jal/db/view_model.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt 2 | from PySide6.QtGui import QFont 3 | from jal.db.db import JalModel 4 | 5 | 6 | class JalViewModel(JalModel): 7 | def __init__(self, parent_view, table_name): 8 | super().__init__(parent_view, table_name) 9 | self._key_field = 'id' # this is assumed to be an auto-increment field that us used to track rows existence 10 | self._columns = [] 11 | self.deleted = [] 12 | self._view = parent_view 13 | 14 | def headerData(self, section, orientation, role=Qt.DisplayRole): 15 | if orientation == Qt.Horizontal and role == Qt.DisplayRole: 16 | return self._columns[section] 17 | return None 18 | 19 | def footerData(self, section, role=Qt.DisplayRole): 20 | return None 21 | 22 | def removeRow(self, row, parent=None): 23 | if self.record(row).value(self._key_field): # New rows have 0 in index key field - we may not track this rows 24 | self.deleted.append(row) # as Qt will remove them completely on its own 25 | super().removeRow(row) 26 | 27 | def submitAll(self): 28 | result = super().submitAll() 29 | if result: 30 | self.deleted = [] 31 | return result 32 | 33 | def revertAll(self): 34 | self.deleted = [] 35 | super().revertAll() 36 | 37 | def data(self, index, role=Qt.DisplayRole): 38 | if not index.isValid(): 39 | return None 40 | if role == Qt.FontRole and (index.row() in self.deleted): 41 | font = QFont() 42 | font.setStrikeOut(True) 43 | return font 44 | return super().data(index, role) 45 | 46 | def row_is_deleted(self, row): 47 | if row in self.deleted: 48 | return True 49 | else: 50 | return False 51 | -------------------------------------------------------------------------------- /jal/img/aux_ibkr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_ibkr.png -------------------------------------------------------------------------------- /jal/img/aux_j2t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_j2t.png -------------------------------------------------------------------------------- /jal/img/aux_kit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_kit.png -------------------------------------------------------------------------------- /jal/img/aux_pof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_pof.png -------------------------------------------------------------------------------- /jal/img/aux_psb.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_psb.ico -------------------------------------------------------------------------------- /jal/img/aux_tvoy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_tvoy.png -------------------------------------------------------------------------------- /jal/img/aux_vtb.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/aux_vtb.ico -------------------------------------------------------------------------------- /jal/img/flag_en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/flag_en.png -------------------------------------------------------------------------------- /jal/img/flag_pt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/flag_pt.png -------------------------------------------------------------------------------- /jal/img/flag_ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/flag_ru.png -------------------------------------------------------------------------------- /jal/img/tag_bank.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/tag_bank.ico -------------------------------------------------------------------------------- /jal/img/tag_card.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/tag_card.ico -------------------------------------------------------------------------------- /jal/img/tag_cash.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/tag_cash.ico -------------------------------------------------------------------------------- /jal/img/tag_investing.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/tag_investing.ico -------------------------------------------------------------------------------- /jal/img/ui_add.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_add.ico -------------------------------------------------------------------------------- /jal/img/ui_add_child.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_add_child.ico -------------------------------------------------------------------------------- /jal/img/ui_amortization.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_amortization.ico -------------------------------------------------------------------------------- /jal/img/ui_buy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_buy.ico -------------------------------------------------------------------------------- /jal/img/ui_cancel.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_cancel.ico -------------------------------------------------------------------------------- /jal/img/ui_chart.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_chart.ico -------------------------------------------------------------------------------- /jal/img/ui_clean.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_clean.ico -------------------------------------------------------------------------------- /jal/img/ui_copy.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_copy.ico -------------------------------------------------------------------------------- /jal/img/ui_coupon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_coupon.ico -------------------------------------------------------------------------------- /jal/img/ui_delisting.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_delisting.ico -------------------------------------------------------------------------------- /jal/img/ui_deposit_account.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_deposit_account.ico -------------------------------------------------------------------------------- /jal/img/ui_deposit_close.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_deposit_close.ico -------------------------------------------------------------------------------- /jal/img/ui_deposit_open.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_deposit_open.ico -------------------------------------------------------------------------------- /jal/img/ui_details.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_details.ico -------------------------------------------------------------------------------- /jal/img/ui_dividend.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_dividend.ico -------------------------------------------------------------------------------- /jal/img/ui_dividend_stock.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_dividend_stock.ico -------------------------------------------------------------------------------- /jal/img/ui_fee.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_fee.ico -------------------------------------------------------------------------------- /jal/img/ui_interest.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_interest.ico -------------------------------------------------------------------------------- /jal/img/ui_jal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_jal.png -------------------------------------------------------------------------------- /jal/img/ui_list.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_list.ico -------------------------------------------------------------------------------- /jal/img/ui_merger.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_merger.ico -------------------------------------------------------------------------------- /jal/img/ui_minus.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_minus.ico -------------------------------------------------------------------------------- /jal/img/ui_ok.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_ok.ico -------------------------------------------------------------------------------- /jal/img/ui_plus.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_plus.ico -------------------------------------------------------------------------------- /jal/img/ui_remove.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_remove.ico -------------------------------------------------------------------------------- /jal/img/ui_renaming.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_renaming.ico -------------------------------------------------------------------------------- /jal/img/ui_sell.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_sell.ico -------------------------------------------------------------------------------- /jal/img/ui_spinoff.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_spinoff.ico -------------------------------------------------------------------------------- /jal/img/ui_split.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_split.ico -------------------------------------------------------------------------------- /jal/img/ui_tag.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_tag.ico -------------------------------------------------------------------------------- /jal/img/ui_tax.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_tax.ico -------------------------------------------------------------------------------- /jal/img/ui_total.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_total.ico -------------------------------------------------------------------------------- /jal/img/ui_transfer_asset_in.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_transfer_asset_in.ico -------------------------------------------------------------------------------- /jal/img/ui_transfer_asset_out.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_transfer_asset_out.ico -------------------------------------------------------------------------------- /jal/img/ui_transfer_in.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_transfer_in.ico -------------------------------------------------------------------------------- /jal/img/ui_transfer_out.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_transfer_out.ico -------------------------------------------------------------------------------- /jal/img/ui_vesting.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_vesting.ico -------------------------------------------------------------------------------- /jal/img/ui_with_credit.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/img/ui_with_credit.ico -------------------------------------------------------------------------------- /jal/jal.pro: -------------------------------------------------------------------------------- 1 | SOURCES = ./jal.py ./widgets/delegates.py ./widgets/qr_scanner.py ./widgets/mdi.py ./widgets/income_spending_widget.py ./widgets/reference_data.py ./widgets/term_deposit_widget.py ./widgets/tax_widget.py ./widgets/asset_dialog.py ./widgets/helpers.py ./widgets/asset_payment_widget.py ./widgets/__init__.py ./widgets/abstract_operation_details.py ./widgets/custom/log_viewer.py ./widgets/custom/treeview_with_footer.py ./widgets/custom/date_range_selector.py ./widgets/custom/db_lookup_combobox.py ./widgets/custom/__init__.py ./widgets/custom/table_footer.py ./widgets/custom/tableview_with_footer.py ./widgets/selection_dialog.py ./widgets/operations_widget.py ./widgets/operations_tabs.py ./widgets/corporate_action_widget.py ./widgets/reference_dialogs.py ./widgets/price_chart.py ./widgets/account_select.py ./widgets/trade_widget.py ./widgets/transfer_widget.py ./widgets/icons.py ./widgets/main_window.py ./widgets/reference_selector.py ./db/closed_trade.py ./db/deposit.py ./db/asset.py ./db/ledger.py ./db/settings.py ./db/account.py ./db/tax_estimator.py ./db/category.py ./db/backup_restore.py ./db/tag.py ./db/trades_model.py ./db/helpers.py ./db/__init__.py ./db/db.py ./db/country.py ./db/balances_model.py ./db/view_model.py ./db/tree_model.py ./db/operations.py ./db/reference_models.py ./db/operations_model.py ./db/holdings_model.py ./db/peer.py ./constants.py ./__init__.py ./updates/__init__.py ./data_export/templates/__init__.py ./data_export/taxes.py ./data_export/__init__.py ./data_export/tax_reports/russia.py ./data_export/tax_reports/portugal.py ./data_export/tax_reports/__init__.py ./data_export/dlsg.py ./data_export/taxes_flow.py ./data_export/xlsx.py ./net/web_request.py ./net/__init__.py ./net/downloader.py ./net/moex.py ./reports/deals.py ./reports/profit_loss.py ./reports/account_balance.py ./reports/category.py ./reports/tag.py ./reports/operations_base.py ./reports/__init__.py ./reports/portfolio.py ./reports/income_spending.py ./reports/assets_payments.py ./reports/term_deposits.py ./reports/reports.py ./reports/peer.py ./universal_cache.py ./data_import/broker_statements/ibkr.py ./data_import/broker_statements/tvoy.py ./data_import/broker_statements/__init__.py ./data_import/broker_statements/just2trade.py ./data_import/broker_statements/psb.py ./data_import/broker_statements/vtb.py ./data_import/broker_statements/open_portfolio.py ./data_import/broker_statements/kit.py ./data_import/receipt_api/pt_pingo_doce.py ./data_import/receipt_api/receipt_api.py ./data_import/receipt_api/eu_lidl_plus.py ./data_import/receipt_api/__init__.py ./data_import/receipt_api/receipts.py ./data_import/receipt_api/ru_fns.py ./data_import/statements.py ./data_import/statement_xml.py ./data_import/statement.py ./data_import/__init__.py ./data_import/shop_receipt.py ./data_import/category_recognizer.py ./data_import/statement_xls.py 2 | FORMS = ./ui/rebuild_window.ui ./ui/quotes_update.ui ./ui/widgets/trade_operation.ui ./ui/widgets/asset_payment_operation.ui ./ui/widgets/term_deposit_operation.ui ./ui/widgets/corporate_action_operation.ui ./ui/widgets/transfer_operation.ui ./ui/widgets/income_spending_operation.ui ./ui/reference_data_dlg.ui ./ui/login_pingo_doce_dlg.ui ./ui/asset_dlg.ui ./ui/login_lidl_plus_dlg.ui ./ui/flow_export_widget.ui ./ui/reports/tag_report.ui ./ui/reports/income_spending_report.ui ./ui/reports/term_deposits_report.ui ./ui/reports/account_balance_report.ui ./ui/reports/peer_report.ui ./ui/reports/assets_payments_report.ui ./ui/reports/category_report.ui ./ui/reports/tax_estimation.ui ./ui/reports/portfolio_report.ui ./ui/reports/deals_report.ui ./ui/reports/profit_loss_report.ui ./ui/select_account_dlg.ui ./ui/main_window.ui ./ui/operations_widget.ui ./ui/tax_export_widget.ui ./ui/select_reference_dlg.ui ./ui/login_fns_dlg.ui ./ui/receipt_import_dlg.ui 3 | TRANSLATIONS = languages/en.ts languages/ru.ts 4 | -------------------------------------------------------------------------------- /jal/jal.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import logging 4 | import traceback 5 | from PySide6.QtCore import Qt, QTranslator, qInstallMessageHandler, QtMsgType, qDebug 6 | from PySide6.QtWidgets import QApplication, QMessageBox 7 | from jal.widgets.main_window import MainWindow 8 | from jal.db.db import JalDB, JalDBError 9 | from jal.db.settings import JalSettings 10 | 11 | #----------------------------------------------------------------------------------------------------------------------- 12 | def exception_logger(exctype, value, tb): 13 | info = traceback.format_exception(exctype, value, tb) 14 | logging.fatal(f"EXCEPTION: {info}") 15 | sys.__excepthook__(exctype, value, tb) 16 | 17 | 18 | def make_error_window(error: JalDBError): 19 | window = QMessageBox() 20 | window.setAttribute(Qt.WA_DeleteOnClose) 21 | window.setWindowTitle("JAL: Start-up aborted") 22 | window.setIcon(QMessageBox.Critical) 23 | window.setText(error.message) 24 | window.setInformativeText(error.details) 25 | return window 26 | 27 | def setup_root_logging(): 28 | root_logger = logging.getLogger() 29 | log_level = os.environ.get('LOGLEVEL', 'INFO').upper() 30 | root_logger.setLevel(log_level) 31 | qInstallMessageHandler(systemWideQtLogHandler) 32 | 33 | def systemWideQtLogHandler(level, context, message): 34 | # Mapping Qt message levels to Python logging levels 35 | level_map = { 36 | QtMsgType.QtDebugMsg: logging.DEBUG, 37 | QtMsgType.QtInfoMsg: logging.INFO, 38 | QtMsgType.QtWarningMsg: logging.WARNING, 39 | QtMsgType.QtCriticalMsg: logging.ERROR, 40 | QtMsgType.QtFatalMsg: logging.CRITICAL, 41 | } 42 | 43 | category = context.category if hasattr(context, 'file') else "" 44 | ctx_str = f"{str(context)}:{category}" 45 | logging.log( 46 | level_map.get(level, logging.ERROR), 47 | f"[QT] {message} | {ctx_str}"[:250] 48 | ) 49 | 50 | #----------------------------------------------------------------------------------------------------------------------- 51 | def main(): 52 | setup_root_logging() 53 | translator_installed = False 54 | sys.excepthook = exception_logger 55 | app = QApplication([]) 56 | 57 | error = JalDB().init_db() 58 | translator = QTranslator(app) 59 | if translator.load(JalSettings.path(JalDB.PATH_LANG_FILE)): 60 | if app.installTranslator(translator): 61 | translator_installed = True 62 | if error.code == JalDBError.OutdatedDbSchema: 63 | error = JalDB().update_db_schema() # this call isn't a part of JalDB.init_db() intentionally - to provide translation of UI message 64 | if error.code != JalDBError.NoError: 65 | window = make_error_window(error) 66 | else: 67 | window = MainWindow(translator) 68 | window.show() 69 | app.exec() 70 | if translator_installed: 71 | app.removeTranslator(translator) 72 | 73 | 74 | #----------------------------------------------------------------------------------------------------------------------- 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /jal/languages/en.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/languages/en.qm -------------------------------------------------------------------------------- /jal/languages/ru.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/languages/ru.qm -------------------------------------------------------------------------------- /jal/net/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/net/web_request.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import platform 3 | from enum import auto 4 | import requests 5 | from requests.exceptions import ConnectTimeout, ConnectionError 6 | from PySide6.QtCore import QThread, QMutex 7 | from jal import __version__ 8 | 9 | 10 | # Class that executes web-requests in a separate thread 11 | # Request parameters are given in constructor and execution starts immediately after object creation 12 | # Result of execution is available via data() method after thread completion 13 | class WebRequest(QThread): 14 | GET = auto() # Execute HTTP GET method 15 | POST = auto() # Execute HTTP POST method with application/x-www-form-urlencoded 16 | POST_JSON = auto() # Execute HTTP POST with JSON 17 | 18 | def __init__(self, operation, url, params=None, headers=None, binary=False): 19 | super().__init__() 20 | self._mutex = QMutex() 21 | self._data = '' 22 | self._operation = operation 23 | self._url = url 24 | self._params = params 25 | self._headers = headers 26 | self._binary = binary 27 | if not self.isRunning(): 28 | self.start() 29 | 30 | def run(self): 31 | self._mutex.lock() 32 | url = self._url 33 | operation = self._operation 34 | params = self._params 35 | headers = self._headers 36 | binary = self._binary 37 | self._mutex.unlock() 38 | result = self._request(operation, url, params=params, headers=headers, binary=binary) 39 | self._mutex.lock() 40 | self._data = result 41 | self._mutex.unlock() 42 | 43 | def data(self): 44 | self._mutex.lock() 45 | data = self._data 46 | self._mutex.unlock() 47 | return data 48 | 49 | def _request(self, operation, url, params=None, headers=None, binary=False): 50 | session = requests.Session() 51 | session.headers['User-Agent'] = f"JAL/{__version__} ({platform.system()} {platform.release()})" 52 | if headers is not None: 53 | session.headers.update(headers) 54 | try: 55 | if operation == WebRequest.GET: 56 | response = session.get(url, params=params) 57 | elif operation == WebRequest.POST: 58 | response = session.post(url, data=params) 59 | elif operation == WebRequest.POST_JSON: 60 | response = session.post(url, json=params) 61 | else: 62 | assert False 63 | except ConnectTimeout: 64 | logging.error(self.tr("Timeout") + " URL {url}") 65 | return '' 66 | except ConnectionError as e: 67 | logging.error(self.tr("Error") + ", URL {url}\n{e}") 68 | return '' 69 | if response.status_code == 200: 70 | if binary: 71 | return response.content 72 | else: 73 | return response.text 74 | else: 75 | logging.error(self.tr("Failed") + f" [{response.status_code}] URL {url}\n{response.text}") 76 | return '' 77 | -------------------------------------------------------------------------------- /jal/pypi_description.md: -------------------------------------------------------------------------------- 1 | # JAL 2 | Just Another Ledger is a project for personal finance tracking. 3 | 4 | It was designed to keep records of personal incomes/spendings and investments with up-to-date information about account's balances and portfolio value. 5 | 6 | ### Main features 7 | - multiple accounts with different currencies (base currency is russian rouble but might be changed in future versions) 8 | - 5 types of transactions: 9 | 1. Generic income/spending operations that may be split into several categories 10 | 2. Asset and money transfers between accounts (with currency conversion if required) 11 | 3. Buy/Sell operation for securities (jal supports stocks, ETFs, options, partial support of bonds and futures) 12 | 4. Dividend for stocks and Interest payments for bonds 13 | 5. Corporate actions for stocks (Split, Symbol change, Merger, Spin-Off, Stock dividend) 14 | 6. Term deposits. 15 | - basic reports: 16 | 1. Daily history of account balance. 17 | 2. Portfolio asset allocation for a given date. 18 | 3. Monthly income/expenditure by category. 19 | 4. Investment profit/loss and history of payments for an assets. 20 | 5. Closed deals summary. 21 | - stock/ETF quotes updates for US (Yahoo), EU (Euronext), CA (TSX) and RU (MOEX) exchanges traded stocks 22 | - Broker statement import: 23 | 1. Russian: Uralsib broker (zipped xls), KIT Finance (xlsx), PSB broker (xls), Open broker (xml). 24 | 2. International: Interactive Brokers Flex statement (xml), Just2Trade (xls). 25 | - Investments report for tax declaration preparation for Russia and Portugal. 26 | Russian tax estimation for open positions. 27 | - *experimental* Download russian electronic slips from russian tax authority (FNS). This function requires authorization and `pyzbar` package installation for QR recognition. 28 | You may authorize via SMS, FNS personal account or ESIA/Gosuslugi. QR code may be scanned from camera, clipboard image or image file on disk. 29 | 30 | Full description is available at Github - *[English](https://github.com/titov-vv/jal/blob/master/docs/README.md), [Русский](https://github.com/titov-vv/jal/blob/master/docs/README.ru.md)* 31 | 32 | Support: [jal@gmx.ru](mailto:jal@gmx.ru?subject=%5BJAL%5D%20Help) or [Telegram](https://t.me/jal_support) -------------------------------------------------------------------------------- /jal/reports/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/reports/deals.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from PySide6.QtCore import Slot, QObject 4 | from jal.ui.reports.ui_deals_report import Ui_DealsReportWidget 5 | from jal.reports.reports import Reports 6 | from jal.widgets.mdi import MdiWidget 7 | from jal.db.trades_model import ClosedTradesModel 8 | 9 | JAL_REPORT_CLASS = "DealsReport" 10 | 11 | 12 | # ---------------------------------------------------------------------------------------------------------------------- 13 | class DealsReport(QObject): 14 | def __init__(self): 15 | super().__init__() 16 | self.name = self.name = self.tr("Deals by Account") 17 | self.window_class = "DealsReportWindow" 18 | 19 | 20 | # ---------------------------------------------------------------------------------------------------------------------- 21 | class DealsReportWindow(MdiWidget): 22 | def __init__(self, parent: Reports, settings: dict = None): 23 | super().__init__(parent.mdi_area()) 24 | self.ui = Ui_DealsReportWidget() 25 | self.ui.setupUi(self) 26 | self._parent = parent 27 | self.name = self.tr("Deals") 28 | 29 | # Add available groupings 30 | self.ui.GroupCombo.addItem(self.tr(""), "") 31 | self.ui.GroupCombo.addItem(self.tr("Asset"), "symbol") 32 | self.ui.GroupCombo.addItem(self.tr("Close"), "close_date") 33 | self.ui.GroupCombo.addItem(self.tr("Asset - Open - Close"), "symbol;open_date;close_date") 34 | self.ui.GroupCombo.addItem(self.tr("Open - Close"), "open_date;close_date") 35 | self.ui.GroupCombo.addItem(self.tr("Close - Open"), "close_date;open_date") 36 | 37 | self.trades_model = ClosedTradesModel(self.ui.ReportTreeView) 38 | self.ui.ReportTreeView.setModel(self.trades_model) 39 | 40 | self.connect_signals_and_slots() 41 | 42 | def connect_signals_and_slots(self): 43 | self.ui.ReportAccountButton.changed.connect(self.updateReport) 44 | self.ui.ReportRange.changed.connect(self.updateReport) 45 | self.ui.GroupCombo.currentIndexChanged.connect(self.updateReport) 46 | self.ui.SaveButton.pressed.connect(partial(self._parent.save_report, self.name, self.ui.ReportTreeView.model())) 47 | 48 | @Slot() 49 | def updateReport(self): 50 | self.ui.ReportTreeView.model().updateView(account_id=self.ui.ReportAccountButton.account_id, 51 | dates=self.ui.ReportRange.getRange(), 52 | grouping=self.ui.GroupCombo.currentData()) 53 | -------------------------------------------------------------------------------- /jal/run_designer: -------------------------------------------------------------------------------- 1 | export PYSIDE_DESIGNER_PLUGINS=.. 2 | pyside6-designer 3 | -------------------------------------------------------------------------------- /jal/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/ui/flow_export_widget.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MoneyFlowWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 458 10 | 158 11 | 12 | 13 | 14 | Money Flow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | Select file 27 | 28 | 29 | ... 30 | 31 | 32 | 33 | 34 | 35 | 36 | Save Report 37 | 38 | 39 | 40 | 41 | 42 | 43 | Excel file: 44 | 45 | 46 | 47 | 48 | 49 | 50 | Year: 51 | 52 | 53 | 54 | 55 | 56 | 57 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 58 | 59 | 60 | 61 | 62 | 63 | 2010 64 | 65 | 66 | 2030 67 | 68 | 69 | 2020 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 0 78 | 0 79 | 80 | 81 | 82 | File where to store tax report in Excel format 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /jal/ui/login_lidl_plus_dlg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | LoginLidlPlusDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 500 11 | 12 | 13 | 14 | Authorization Lidl Plus 15 | 16 | 17 | 18 | 6 19 | 20 | 21 | 2 22 | 23 | 24 | 2 25 | 26 | 27 | 2 28 | 29 | 30 | 2 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 39 | 40 | 41 | 42 | about:blank 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | QFrame::NoFrame 51 | 52 | 53 | QFrame::Plain 54 | 55 | 56 | 57 | 0 58 | 59 | 60 | 0 61 | 62 | 63 | 0 64 | 65 | 66 | 6 67 | 68 | 69 | 70 | 71 | Qt::Horizontal 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 0 80 | 0 81 | 82 | 83 | 84 | Close 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | QWebEngineView 96 | QWidget 97 |
QtWebEngineWidgets/QWebEngineView
98 |
99 |
100 | 101 | 102 | 103 | CloseBtn 104 | clicked() 105 | LoginLidlPlusDialog 106 | close() 107 | 108 | 109 | 199 110 | 477 111 | 112 | 113 | 199 114 | 249 115 | 116 | 117 | 118 | 119 |
120 | -------------------------------------------------------------------------------- /jal/ui/quotes_update.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | UpdateQuotesDlg 4 | 5 | 6 | Qt::ApplicationModal 7 | 8 | 9 | 10 | 0 11 | 0 12 | 256 13 | 308 14 | 15 | 16 | 17 | Update asset's quotes 18 | 19 | 20 | 21 | 22 | 23 | dd/MM/yyyy 24 | 25 | 26 | true 27 | 28 | 29 | Qt::UTC 30 | 31 | 32 | 33 | 34 | 35 | 36 | Qt::Horizontal 37 | 38 | 39 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 40 | 41 | 42 | 43 | 44 | 45 | 46 | dd/MM/yyyy 47 | 48 | 49 | true 50 | 51 | 52 | Qt::UTC 53 | 54 | 55 | 56 | 57 | 58 | 59 | End date 60 | 61 | 62 | 63 | 64 | 65 | 66 | Start date 67 | 68 | 69 | 70 | 71 | 72 | 73 | QAbstractItemView::EditKeyPressed 74 | 75 | 76 | true 77 | 78 | 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | Sources 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | buttonBox 96 | accepted() 97 | UpdateQuotesDlg 98 | accept() 99 | 100 | 101 | 248 102 | 254 103 | 104 | 105 | 157 106 | 274 107 | 108 | 109 | 110 | 111 | buttonBox 112 | rejected() 113 | UpdateQuotesDlg 114 | reject() 115 | 116 | 117 | 316 118 | 260 119 | 120 | 121 | 286 122 | 274 123 | 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /jal/ui/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/ui/reports/__init__.py -------------------------------------------------------------------------------- /jal/ui/reports/account_balance_report.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AccountBalanceHistoryReportWidget 4 | 5 | 6 | 7 | 0 8 | 0 9 | 769 10 | 345 11 | 12 | 13 | 14 | Account balance history chart 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 35 | QFrame::Panel 36 | 37 | 38 | QFrame::Sunken 39 | 40 | 41 | 42 | 2 43 | 44 | 45 | 2 46 | 47 | 48 | 2 49 | 50 | 51 | 2 52 | 53 | 54 | 6 55 | 56 | 57 | 58 | 59 | QTD;YTD;this_year;last_year 60 | 61 | 62 | 63 | 64 | 65 | 66 | Qt::Horizontal 67 | 68 | 69 | 70 | 40 71 | 20 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | Account: 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | Save... 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | DateRangeSelector 105 | QWidget 106 |
jal/widgets/custom/date_range_selector.h
107 | 1 108 |
109 | 110 | AccountButton 111 | QPushButton 112 |
jal/widgets/account_select.h
113 |
114 |
115 | 116 | 117 |
118 | -------------------------------------------------------------------------------- /jal/ui/select_account_dlg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SelectAccountDlg 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 141 11 | 12 | 13 | 14 | Please select account 15 | 16 | 17 | 18 | 19 | 20 | TextLabel 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Use the same account for given currency 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::Horizontal 38 | 39 | 40 | QDialogButtonBox::Ok 41 | 42 | 43 | 44 | 45 | 46 | 47 | Qt::Vertical 48 | 49 | 50 | 51 | 20 52 | 40 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | AccountSelector 62 | QWidget 63 |
jal/widgets/reference_selector.h
64 | 1 65 |
66 |
67 | 68 | 69 | 70 | buttonBox 71 | accepted() 72 | SelectAccountDlg 73 | close() 74 | 75 | 76 | 199 77 | 60 78 | 79 | 80 | 199 81 | 52 82 | 83 | 84 | 85 | 86 |
87 | -------------------------------------------------------------------------------- /jal/ui/select_reference_dlg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | SelectReferenceDlg 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 97 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Title 21 | 22 | 23 | 24 | 25 | 26 | Description 27 | 28 | 29 | 30 | 31 | 32 | 33 | QFrame::NoFrame 34 | 35 | 36 | QFrame::Plain 37 | 38 | 39 | 0 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | 0 47 | 48 | 49 | 0 50 | 51 | 52 | 0 53 | 54 | 55 | 0 56 | 57 | 58 | 0 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | Qt::Horizontal 67 | 68 | 69 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 70 | 71 | 72 | 73 | 74 | 75 | 76 | Qt::Vertical 77 | 78 | 79 | 80 | 20 81 | 40 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | buttonBox 92 | accepted() 93 | SelectReferenceDlg 94 | close() 95 | 96 | 97 | 199 98 | 60 99 | 100 | 101 | 199 102 | 52 103 | 104 | 105 | 106 | 107 | buttonBox 108 | rejected() 109 | SelectReferenceDlg 110 | reject() 111 | 112 | 113 | 199 114 | 51 115 | 116 | 117 | 199 118 | 48 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /jal/ui/ui_select_account_dlg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'select_account_dlg.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog, 19 | QDialogButtonBox, QLabel, QSizePolicy, QSpacerItem, 20 | QVBoxLayout, QWidget) 21 | 22 | from jal.widgets.reference_selector import AccountSelector 23 | 24 | class Ui_SelectAccountDlg(object): 25 | def setupUi(self, SelectAccountDlg): 26 | if not SelectAccountDlg.objectName(): 27 | SelectAccountDlg.setObjectName(u"SelectAccountDlg") 28 | SelectAccountDlg.resize(400, 141) 29 | self.verticalLayout = QVBoxLayout(SelectAccountDlg) 30 | self.verticalLayout.setObjectName(u"verticalLayout") 31 | self.DescriptionLbl = QLabel(SelectAccountDlg) 32 | self.DescriptionLbl.setObjectName(u"DescriptionLbl") 33 | 34 | self.verticalLayout.addWidget(self.DescriptionLbl) 35 | 36 | self.AccountWidget = AccountSelector(SelectAccountDlg) 37 | self.AccountWidget.setObjectName(u"AccountWidget") 38 | 39 | self.verticalLayout.addWidget(self.AccountWidget) 40 | 41 | self.ReuseAccount = QCheckBox(SelectAccountDlg) 42 | self.ReuseAccount.setObjectName(u"ReuseAccount") 43 | 44 | self.verticalLayout.addWidget(self.ReuseAccount) 45 | 46 | self.buttonBox = QDialogButtonBox(SelectAccountDlg) 47 | self.buttonBox.setObjectName(u"buttonBox") 48 | self.buttonBox.setOrientation(Qt.Horizontal) 49 | self.buttonBox.setStandardButtons(QDialogButtonBox.Ok) 50 | 51 | self.verticalLayout.addWidget(self.buttonBox) 52 | 53 | self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) 54 | 55 | self.verticalLayout.addItem(self.verticalSpacer) 56 | 57 | 58 | self.retranslateUi(SelectAccountDlg) 59 | self.buttonBox.accepted.connect(SelectAccountDlg.close) 60 | 61 | QMetaObject.connectSlotsByName(SelectAccountDlg) 62 | # setupUi 63 | 64 | def retranslateUi(self, SelectAccountDlg): 65 | SelectAccountDlg.setWindowTitle(QCoreApplication.translate("SelectAccountDlg", u"Please select account", None)) 66 | self.DescriptionLbl.setText(QCoreApplication.translate("SelectAccountDlg", u"TextLabel", None)) 67 | self.ReuseAccount.setText(QCoreApplication.translate("SelectAccountDlg", u"Use the same account for given currency", None)) 68 | # retranslateUi 69 | 70 | -------------------------------------------------------------------------------- /jal/ui/ui_select_reference_dlg.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'select_reference_dlg.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, 19 | QFrame, QLabel, QSizePolicy, QSpacerItem, 20 | QVBoxLayout, QWidget) 21 | 22 | class Ui_SelectReferenceDlg(object): 23 | def setupUi(self, SelectReferenceDlg): 24 | if not SelectReferenceDlg.objectName(): 25 | SelectReferenceDlg.setObjectName(u"SelectReferenceDlg") 26 | SelectReferenceDlg.resize(400, 97) 27 | sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred) 28 | sizePolicy.setHorizontalStretch(0) 29 | sizePolicy.setVerticalStretch(0) 30 | sizePolicy.setHeightForWidth(SelectReferenceDlg.sizePolicy().hasHeightForWidth()) 31 | SelectReferenceDlg.setSizePolicy(sizePolicy) 32 | SelectReferenceDlg.setWindowTitle(u"Title") 33 | self.WindowLayout = QVBoxLayout(SelectReferenceDlg) 34 | self.WindowLayout.setObjectName(u"WindowLayout") 35 | self.DescriptionLabel = QLabel(SelectReferenceDlg) 36 | self.DescriptionLabel.setObjectName(u"DescriptionLabel") 37 | self.DescriptionLabel.setText(u"Description") 38 | 39 | self.WindowLayout.addWidget(self.DescriptionLabel) 40 | 41 | self.SelectorFrame = QFrame(SelectReferenceDlg) 42 | self.SelectorFrame.setObjectName(u"SelectorFrame") 43 | self.SelectorFrame.setFrameShape(QFrame.NoFrame) 44 | self.SelectorFrame.setFrameShadow(QFrame.Plain) 45 | self.SelectorFrame.setLineWidth(0) 46 | self.SelectorFrame.setMidLineWidth(0) 47 | self.FrameLayout = QVBoxLayout(self.SelectorFrame) 48 | self.FrameLayout.setSpacing(0) 49 | self.FrameLayout.setObjectName(u"FrameLayout") 50 | self.FrameLayout.setContentsMargins(0, 0, 0, 0) 51 | 52 | self.WindowLayout.addWidget(self.SelectorFrame) 53 | 54 | self.buttonBox = QDialogButtonBox(SelectReferenceDlg) 55 | self.buttonBox.setObjectName(u"buttonBox") 56 | self.buttonBox.setOrientation(Qt.Horizontal) 57 | self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) 58 | 59 | self.WindowLayout.addWidget(self.buttonBox) 60 | 61 | self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) 62 | 63 | self.WindowLayout.addItem(self.verticalSpacer) 64 | 65 | 66 | self.retranslateUi(SelectReferenceDlg) 67 | self.buttonBox.accepted.connect(SelectReferenceDlg.close) 68 | self.buttonBox.rejected.connect(SelectReferenceDlg.reject) 69 | 70 | QMetaObject.connectSlotsByName(SelectReferenceDlg) 71 | # setupUi 72 | 73 | def retranslateUi(self, SelectReferenceDlg): 74 | pass 75 | # retranslateUi 76 | 77 | -------------------------------------------------------------------------------- /jal/ui/widgets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/ui/widgets/__init__.py -------------------------------------------------------------------------------- /jal/universal_cache.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | from typing import Any, Callable, Hashable, Tuple 3 | 4 | class UniversalCache: 5 | """ 6 | Universal cache for functions with hashable arguments. 7 | 8 | Features: 9 | - Cache key: tuple (function, hashable arguments) 10 | - Autovalidation of arguments (must be hashable) 11 | - Invalidate specific cache entries (by key) 12 | - Guaranteed unique cache key for each function and its arguments 13 | """ 14 | 15 | def __init__(self): 16 | self._cache = {} 17 | 18 | def get_data( 19 | self, 20 | func: Callable, 21 | args: Tuple[Hashable, ...] = () 22 | ) -> Any: 23 | """ 24 | Get data from cache or execute function if not cached. 25 | 26 | :param func: Function to cache/execute 27 | :param args: Function arguments (must be hashable) 28 | :return: Function result from cache or execution 29 | """ 30 | key = self._create_key(func, args) 31 | 32 | if key not in self._cache: 33 | self._cache[key] = func(*args) 34 | 35 | return self._cache[key] 36 | 37 | def update_data( 38 | self, 39 | func: Callable, 40 | args: Tuple[Hashable, ...] = () 41 | ) -> Any: 42 | """ 43 | Invalidates old cache entry and executes function to have new data cached. 44 | 45 | :param func: Function to cache/execute 46 | :param args: Function arguments (must be hashable) 47 | :return: Function result from new execution 48 | """ 49 | self.invalidate(func, args) 50 | return self.get_data(func, args) 51 | 52 | def invalidate(self, func: Callable, args: Tuple[Hashable, ...] = ()) -> None: 53 | """Delete a specific cache entry""" 54 | key = self._create_key(func, args) 55 | self._cache.pop(key, None) 56 | 57 | def clear_cache(self) -> None: 58 | """Clear the entire cache""" 59 | self._cache.clear() 60 | 61 | def _create_key(self, func: Callable, args: Tuple) -> Tuple: 62 | """Create a unique key for the cache""" 63 | self._validate_args_hashable(args) 64 | return ( 65 | func.__module__, 66 | func.__name__, 67 | args 68 | ) 69 | 70 | @staticmethod 71 | def _validate_args_hashable(args: Tuple) -> None: 72 | """Validate that all arguments are hashable""" 73 | try: 74 | hash(args) 75 | except TypeError: 76 | raise ValueError("All elements must be hashable") 77 | -------------------------------------------------------------------------------- /jal/updates/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/updates/jal_delta_40.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | -- Change db structure for base currency 6 | DELETE FROM settings WHERE id=2 AND name='BaseCurrency'; 7 | 8 | DROP TABLE IF EXISTS base_currency; 9 | CREATE TABLE base_currency ( 10 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 11 | since_timestamp INTEGER NOT NULL UNIQUE, 12 | currency_id INTEGER NOT NULL REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE 13 | ); 14 | -------------------------------------------------------------------------------- 15 | -- Ensure compatibility with previous behavior 16 | INSERT INTO base_currency(id, since_timestamp, currency_id) VALUES (1, 946684800, 1); 17 | -- Update data source name 18 | UPDATE data_sources SET name='Central banks' WHERE id=0; 19 | -------------------------------------------------------------------------------- 20 | -- Change db structure for currency handling 21 | CREATE TABLE temp_tickers AS SELECT * FROM asset_tickers; 22 | DROP TABLE asset_tickers; 23 | CREATE TABLE asset_tickers ( 24 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 25 | asset_id INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 26 | symbol TEXT NOT NULL, 27 | currency_id INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE, 28 | description TEXT NOT NULL DEFAULT (''), 29 | quote_source INTEGER REFERENCES data_sources (id) ON DELETE SET NULL ON UPDATE CASCADE DEFAULT ( -1), 30 | active INTEGER NOT NULL DEFAULT (1) 31 | ); 32 | INSERT INTO asset_tickers (id, asset_id, symbol, currency_id, description, quote_source, active) 33 | SELECT id, asset_id, symbol, currency_id, description, quote_source, active FROM temp_tickers; 34 | DROP TABLE temp_tickers; 35 | -- Set reference currency to NULL for currency symbols 36 | UPDATE asset_tickers SET currency_id=NULL WHERE asset_id IN (SELECT id FROM assets WHERE type_id=1); 37 | -- Index to prevent duplicates 38 | DROP INDEX IF EXISTS uniq_symbols; 39 | CREATE UNIQUE INDEX uniq_symbols ON asset_tickers (asset_id, symbol COLLATE NOCASE, currency_id); 40 | 41 | DROP TRIGGER IF EXISTS validate_ticker_currency_insert; 42 | CREATE TRIGGER validate_ticker_currency_insert 43 | BEFORE INSERT ON asset_tickers 44 | FOR EACH ROW 45 | WHEN IIF(NEW.currency_id IS NULL, 0, 1) = (SELECT IIF(type_id=1, 1, 0) FROM assets WHERE id=NEW.asset_id) 46 | BEGIN 47 | SELECT RAISE(ABORT, "JAL_SQL_MSG_0003"); 48 | END; 49 | 50 | DROP TRIGGER IF EXISTS validate_ticker_currency_update; 51 | CREATE TRIGGER validate_ticker_currency_update 52 | AFTER UPDATE OF currency_id ON asset_tickers 53 | FOR EACH ROW 54 | WHEN IIF(NEW.currency_id IS NULL, 0, 1) = (SELECT IIF(type_id=1, 1, 0) FROM assets WHERE id=NEW.asset_id) 55 | BEGIN 56 | SELECT RAISE(ABORT, "JAL_SQL_MSG_0003"); 57 | END; 58 | -------------------------------------------------------------------------------- 59 | PRAGMA foreign_keys = 1; 60 | -------------------------------------------------------------------------------- 61 | -- Set new DB schema version 62 | UPDATE settings SET value=40 WHERE name='SchemaVersion'; 63 | COMMIT; 64 | -------------------------------------------------------------------------------- /jal/updates/jal_delta_42.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | CREATE TABLE old_actions_table AS SELECT * FROM actions; 6 | DROP TABLE actions; 7 | CREATE TABLE actions ( 8 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 9 | op_type INTEGER NOT NULL DEFAULT (1), 10 | timestamp INTEGER NOT NULL, 11 | account_id INTEGER REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 12 | peer_id INTEGER REFERENCES agents (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 13 | alt_currency_id INTEGER REFERENCES assets (id) ON DELETE RESTRICT ON UPDATE CASCADE, 14 | note TEXT 15 | ); 16 | INSERT INTO actions (id, op_type, timestamp, account_id, peer_id, alt_currency_id) 17 | SELECT id, op_type, timestamp, account_id, peer_id, alt_currency_id FROM old_actions_table; 18 | DROP TABLE old_actions_table; 19 | -------------------------------------------------------------------------------- 20 | -- Restore triggers 21 | CREATE TRIGGER actions_after_delete 22 | AFTER DELETE ON actions 23 | FOR EACH ROW 24 | WHEN (SELECT value FROM settings WHERE id = 1) 25 | BEGIN 26 | DELETE FROM action_details WHERE pid = OLD.id; 27 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp; 28 | END; 29 | 30 | CREATE TRIGGER actions_after_insert 31 | AFTER INSERT ON actions 32 | FOR EACH ROW 33 | WHEN (SELECT value FROM settings WHERE id = 1) 34 | BEGIN 35 | DELETE FROM ledger WHERE timestamp >= NEW.timestamp; 36 | END; 37 | 38 | CREATE TRIGGER actions_after_update 39 | AFTER UPDATE OF timestamp, account_id, peer_id ON actions 40 | FOR EACH ROW 41 | WHEN (SELECT value FROM settings WHERE id = 1) 42 | BEGIN 43 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; 44 | END; 45 | -------------------------------------------------------------------------------- 46 | PRAGMA foreign_keys = 1; 47 | -------------------------------------------------------------------------------- 48 | -- Set new DB schema version 49 | UPDATE settings SET value=42 WHERE name='SchemaVersion'; 50 | COMMIT; -------------------------------------------------------------------------------- /jal/updates/jal_delta_43.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | -- Fix old data 6 | UPDATE dividends SET ex_date=0 WHERE ex_date IS NULL; 7 | UPDATE dividends SET number='' WHERE number IS NULL; 8 | UPDATE dividends SET tax='0' WHERE tax IS NULL; 9 | -------------------------------------------------------------------------------- 10 | -- Correct table structure 11 | CREATE TABLE dividends_old AS SELECT * FROM dividends; 12 | DROP TABLE dividends; 13 | CREATE TABLE dividends ( 14 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 15 | op_type INTEGER NOT NULL DEFAULT (2), 16 | timestamp INTEGER NOT NULL, 17 | ex_date INTEGER NOT NULL DEFAULT (0), 18 | number TEXT DEFAULT ('') NOT NULL, 19 | type INTEGER NOT NULL, 20 | account_id INTEGER REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 21 | asset_id INTEGER REFERENCES assets (id) ON DELETE RESTRICT ON UPDATE CASCADE NOT NULL, 22 | amount TEXT NOT NULL DEFAULT ('0'), 23 | tax TEXT NOT NULL DEFAULT ('0'), 24 | note TEXT 25 | ); 26 | INSERT INTO dividends (id, op_type, timestamp, ex_date, number, type, account_id, asset_id, amount, tax, note) 27 | SELECT id, op_type, timestamp, ex_date, number, type, account_id, asset_id, amount, tax, note FROM dividends_old; 28 | DROP TABLE dividends_old; 29 | 30 | DROP TRIGGER IF EXISTS dividends_after_delete; 31 | CREATE TRIGGER dividends_after_delete 32 | AFTER DELETE ON dividends 33 | FOR EACH ROW 34 | WHEN (SELECT value FROM settings WHERE id = 1) 35 | BEGIN 36 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp; 37 | END; 38 | 39 | DROP TRIGGER IF EXISTS dividends_after_insert; 40 | CREATE TRIGGER dividends_after_insert 41 | AFTER INSERT ON dividends 42 | FOR EACH ROW 43 | WHEN (SELECT value FROM settings WHERE id = 1) 44 | BEGIN 45 | DELETE FROM ledger WHERE timestamp >= NEW.timestamp; 46 | END; 47 | 48 | DROP TRIGGER IF EXISTS dividends_after_update; 49 | CREATE TRIGGER dividends_after_update 50 | AFTER UPDATE OF timestamp, account_id, asset_id, amount, tax ON dividends 51 | FOR EACH ROW 52 | WHEN (SELECT value FROM settings WHERE id = 1) 53 | BEGIN 54 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; 55 | END; 56 | -------------------------------------------------------------------------------- 57 | PRAGMA foreign_keys = 1; 58 | -------------------------------------------------------------------------------- 59 | -- Set new DB schema version 60 | UPDATE settings SET value=43 WHERE name='SchemaVersion'; 61 | COMMIT; -------------------------------------------------------------------------------- /jal/updates/jal_delta_44.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | -- Add new 'number' field into transfers table 6 | CREATE TABLE transfers_old AS SELECT * FROM transfers; 7 | DROP TABLE IF EXISTS transfers; 8 | CREATE TABLE transfers ( 9 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 10 | op_type INTEGER NOT NULL DEFAULT (4), 11 | withdrawal_timestamp INTEGER NOT NULL, 12 | withdrawal_account INTEGER NOT NULL REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE, 13 | withdrawal TEXT NOT NULL, 14 | deposit_timestamp INTEGER NOT NULL, 15 | deposit_account INTEGER NOT NULL REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE, 16 | deposit TEXT NOT NULL, 17 | fee_account INTEGER REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE, 18 | fee TEXT, 19 | number TEXT NOT NULL DEFAULT (''), 20 | asset INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE, 21 | note TEXT 22 | ); 23 | INSERT INTO transfers (id, op_type, withdrawal_timestamp, withdrawal_account, withdrawal, deposit_timestamp, deposit_account, deposit, fee_account, fee, asset, note) 24 | SELECT id, op_type, withdrawal_timestamp, withdrawal_account, withdrawal, deposit_timestamp, deposit_account, deposit, fee_account, fee, asset, note FROM transfers_old; 25 | DROP TABLE transfers_old; 26 | 27 | DROP TRIGGER IF EXISTS transfers_after_delete; 28 | CREATE TRIGGER transfers_after_delete 29 | AFTER DELETE ON transfers 30 | FOR EACH ROW 31 | WHEN (SELECT value FROM settings WHERE id = 1) 32 | BEGIN 33 | DELETE FROM ledger WHERE timestamp >= OLD.withdrawal_timestamp OR timestamp >= OLD.deposit_timestamp; 34 | END; 35 | 36 | DROP TRIGGER IF EXISTS transfers_after_insert; 37 | CREATE TRIGGER transfers_after_insert 38 | AFTER INSERT ON transfers 39 | FOR EACH ROW 40 | WHEN (SELECT value FROM settings WHERE id = 1) 41 | BEGIN 42 | DELETE FROM ledger WHERE timestamp >= NEW.withdrawal_timestamp OR timestamp >= NEW.deposit_timestamp; 43 | END; 44 | 45 | DROP TRIGGER IF EXISTS transfers_after_update; 46 | CREATE TRIGGER transfers_after_update 47 | AFTER UPDATE OF withdrawal_timestamp, deposit_timestamp, withdrawal_account, deposit_account, fee_account, 48 | withdrawal, deposit, fee, asset ON transfers 49 | FOR EACH ROW 50 | WHEN (SELECT value FROM settings WHERE id = 1) 51 | BEGIN 52 | DELETE FROM ledger WHERE timestamp >= OLD.withdrawal_timestamp OR timestamp >= OLD.deposit_timestamp OR 53 | timestamp >= NEW.withdrawal_timestamp OR timestamp >= NEW.deposit_timestamp; 54 | END; 55 | -------------------------------------------------------------------------------- 56 | PRAGMA foreign_keys = 1; 57 | -------------------------------------------------------------------------------- 58 | -- Set new DB schema version 59 | UPDATE settings SET value=44 WHERE name='SchemaVersion'; 60 | COMMIT; 61 | -------------------------------------------------------------------------------- /jal/updates/jal_delta_45.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | DELETE FROM settings WHERE id>10; 4 | INSERT INTO settings(id, name, value) VALUES (11, 'RecentFolder_Statement', '.'); 5 | INSERT INTO settings(id, name, value) VALUES (12, 'RecentFolder_Report', '.'); 6 | INSERT INTO settings(id, name, value) VALUES (13, 'CleanDB', 0); 7 | -------------------------------------------------------------------------------- 8 | -- Set new DB schema version 9 | UPDATE settings SET value=45 WHERE name='SchemaVersion'; 10 | COMMIT; -------------------------------------------------------------------------------- /jal/updates/jal_delta_46.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | -- Add tree-hierarchy to 'tags' table 6 | CREATE TABLE temp_tags AS SELECT * FROM tags; 7 | DROP TABLE tags; 8 | CREATE TABLE tags ( 9 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 10 | pid INTEGER NOT NULL DEFAULT (0), 11 | tag TEXT (64) NOT NULL UNIQUE 12 | ); 13 | INSERT INTO tags (id, tag) SELECT id, tag FROM temp_tags; 14 | DROP TABLE temp_tags; 15 | -------------------------------------------------------------------------------- 16 | PRAGMA foreign_keys = 1; 17 | -------------------------------------------------------------------------------- 18 | -- Set new DB schema version 19 | UPDATE settings SET value=46 WHERE name='SchemaVersion'; 20 | COMMIT; 21 | -- Reduce file size 22 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_47.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | INSERT INTO settings(id, name, value) VALUES (14, 'EuLidlClientSecret', 'TGlkbFBsdXNOYXRpdmVDbGllbnQ6c2VjcmV0'); 4 | INSERT INTO settings(id, name, value) VALUES (15, 'EuLidlAccessToken', ''); 5 | INSERT INTO settings(id, name, value) VALUES (16, 'EuLidlRefreshToken', ''); 6 | INSERT INTO settings(id, name, value) VALUES (17, 'PtPingoDoceAccessToken', ''); 7 | INSERT INTO settings(id, name, value) VALUES (18, 'PtPingoDoceRefreshToken', ''); 8 | INSERT INTO settings(id, name, value) VALUES (19, 'PtPingoDoceUserProfile', '{}'); 9 | -------------------------------------------------------------------------------- 10 | -- Set new DB schema version 11 | UPDATE settings SET value=47 WHERE name='SchemaVersion'; 12 | COMMIT; -------------------------------------------------------------------------------- /jal/updates/jal_delta_48.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | CREATE TABLE temp_tickers AS SELECT * FROM asset_tickers; 6 | DROP TABLE asset_tickers; 7 | CREATE TABLE asset_tickers ( 8 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 9 | asset_id INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 10 | symbol TEXT NOT NULL, 11 | currency_id INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE, 12 | description TEXT NOT NULL DEFAULT (''), 13 | quote_source INTEGER DEFAULT ( -1) NOT NULL, 14 | active INTEGER NOT NULL DEFAULT (1) 15 | ); 16 | INSERT INTO asset_tickers (id, asset_id, symbol, currency_id, description, quote_source, active) 17 | SELECT id, asset_id, symbol, currency_id, description, quote_source, active FROM temp_tickers; 18 | DROP TABLE temp_tickers; 19 | -- Index to prevent duplicates 20 | DROP INDEX IF EXISTS uniq_symbols; 21 | CREATE UNIQUE INDEX uniq_symbols ON asset_tickers (asset_id, symbol COLLATE NOCASE, currency_id); 22 | -- Create triggers to keep currency_id NULL for currencies and NOT NULL for other assets 23 | DROP TRIGGER IF EXISTS validate_ticker_currency_insert; 24 | CREATE TRIGGER validate_ticker_currency_insert 25 | BEFORE INSERT ON asset_tickers 26 | FOR EACH ROW 27 | WHEN IIF(NEW.currency_id IS NULL, 0, 1) = (SELECT IIF(type_id=1, 1, 0) FROM assets WHERE id=NEW.asset_id) 28 | BEGIN 29 | SELECT RAISE(ABORT, "JAL_SQL_MSG_0003"); 30 | END; 31 | 32 | DROP TRIGGER IF EXISTS validate_ticker_currency_update; 33 | CREATE TRIGGER validate_ticker_currency_update 34 | AFTER UPDATE OF currency_id ON asset_tickers 35 | FOR EACH ROW 36 | WHEN IIF(NEW.currency_id IS NULL, 0, 1) = (SELECT IIF(type_id=1, 1, 0) FROM assets WHERE id=NEW.asset_id) 37 | BEGIN 38 | SELECT RAISE(ABORT, "JAL_SQL_MSG_0003"); 39 | END; 40 | -- drop table with data sources 41 | DROP TABLE IF EXISTS data_sources; 42 | -------------------------------------------------------------------------------- 43 | PRAGMA foreign_keys = 1; 44 | -------------------------------------------------------------------------------- 45 | -- Set new DB schema version 46 | UPDATE settings SET value=48 WHERE name='SchemaVersion'; 47 | COMMIT; 48 | -- Reduce file size 49 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_49.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | UPDATE transfers SET deposit=(deposit-fee), fee_account=NULL, fee=NULL WHERE id IN 6 | (SELECT t.id FROM transfers t LEFT JOIN accounts fa ON fa.id = t.fee_account WHERE fa.organization_id IS NULL AND t.fee IS NOT NULL AND t.fee_account==t.deposit_account); 7 | UPDATE transfers SET withdrawal=(withdrawal+fee), fee_account=NULL, fee=NULL WHERE id IN 8 | (SELECT t.id FROM transfers t LEFT JOIN accounts fa ON fa.id = t.fee_account WHERE fa.organization_id IS NULL AND t.fee IS NOT NULL AND t.fee_account==t.withdrawal_account); 9 | -------------------------------------------------------------------------------- 10 | PRAGMA foreign_keys = 1; 11 | -------------------------------------------------------------------------------- 12 | -- Set new DB schema version 13 | UPDATE settings SET value=49 WHERE name='SchemaVersion'; 14 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (7, 'RebuildDB', 1); 15 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (10, 'MessageOnce', 16 | '{"en": "Database version was updated.\nTransfers handling was changed - fee account should now have organization assigned.\nYou may assign it in the Data->Accounts menu if a related error will appear in the log.", 17 | "ru": "Версия базы данных обновлена.\nОбработка переводов была изменена - счёт комиссии теперь должен иметь организацию-владельца.\nВы можете указать её в меню Данные->Счета, если будут в логе будет ошибка об этом."}'); 18 | COMMIT; 19 | -- Reduce file size 20 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_50.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | UPDATE transfers SET deposit='0' WHERE asset IS NOT NULL; 6 | -------------------------------------------------------------------------------- 7 | PRAGMA foreign_keys = 1; 8 | -------------------------------------------------------------------------------- 9 | -- Set new DB schema version 10 | UPDATE settings SET value=50 WHERE name='SchemaVersion'; 11 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (7, 'RebuildDB', 1); 12 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (10, 'MessageOnce', 13 | '{"en": "Database version was updated.\nAssets transfer handling was changed.\nSet a non-zero cost basis if you need to see a relevant open price for tranferred positions in another currency.", 14 | "ru": "Версия базы данных обновлена.\nОбработка переводов ЦБ была изменена.\nУкажите ненулевую стоимость позиции, если вы хотите видеть правильную цену покупки переведённых бумаг в другой валюте."}'); 15 | COMMIT; 16 | -- Reduce file size 17 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_51.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | DROP TRIGGER IF EXISTS on_closed_trade_delete; 6 | 7 | DROP INDEX IF EXISTS open_trades_by_operation_idx; 8 | CREATE INDEX open_trades_by_operation_idx ON trades_opened (timestamp, op_type, operation_id); 9 | 10 | DROP TRIGGER dividends_after_delete; 11 | 12 | DROP TRIGGER IF EXISTS dividends_after_delete; 13 | CREATE TRIGGER dividends_after_delete 14 | AFTER DELETE ON dividends 15 | FOR EACH ROW 16 | WHEN (SELECT value FROM settings WHERE id = 1) 17 | BEGIN 18 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp; 19 | DELETE FROM trades_opened WHERE timestamp >= OLD.timestamp; 20 | END; 21 | 22 | DROP TRIGGER IF EXISTS dividends_after_insert; 23 | CREATE TRIGGER dividends_after_insert 24 | AFTER INSERT ON dividends 25 | FOR EACH ROW 26 | WHEN (SELECT value FROM settings WHERE id = 1) 27 | BEGIN 28 | DELETE FROM ledger WHERE timestamp >= NEW.timestamp; 29 | DELETE FROM trades_opened WHERE timestamp >= NEW.timestamp; 30 | END; 31 | 32 | DROP TRIGGER IF EXISTS dividends_after_update; 33 | CREATE TRIGGER dividends_after_update 34 | AFTER UPDATE OF timestamp, type, account_id, asset_id, amount, tax ON dividends 35 | FOR EACH ROW 36 | WHEN (SELECT value FROM settings WHERE id = 1) 37 | BEGIN 38 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; 39 | DELETE FROM trades_opened WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; 40 | END; 41 | -------------------------------------------------------------------------------- 42 | PRAGMA foreign_keys = 1; 43 | -------------------------------------------------------------------------------- 44 | -- Set new DB schema version 45 | UPDATE settings SET value=51 WHERE name='SchemaVersion'; 46 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (7, 'RebuildDB', 1); 47 | COMMIT; 48 | -- Reduce file size 49 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_52.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = 0; 4 | -------------------------------------------------------------------------------- 5 | DROP TABLE IF EXISTS term_deposits; 6 | CREATE TABLE term_deposits ( 7 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 8 | op_type INTEGER NOT NULL DEFAULT (6), 9 | account_id INTEGER NOT NULL REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE, 10 | note TEXT 11 | ); 12 | DROP TABLE IF EXISTS deposit_actions; 13 | CREATE TABLE deposit_actions ( 14 | id INTEGER PRIMARY KEY UNIQUE NOT NULL, 15 | deposit_id INTEGER REFERENCES term_deposits (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 16 | timestamp INTEGER NOT NULL, 17 | action_type INTEGER NOT NULL, 18 | amount TEXT NOT NULL 19 | ); 20 | DROP INDEX IF EXISTS deposit_actions_idx; 21 | CREATE UNIQUE INDEX deposit_actions_idx ON deposit_actions (deposit_id, timestamp, action_type); 22 | 23 | 24 | DROP TRIGGER IF EXISTS deposit_action_after_delete; 25 | CREATE TRIGGER deposit_action_after_delete 26 | AFTER DELETE ON deposit_actions 27 | FOR EACH ROW 28 | WHEN (SELECT value FROM settings WHERE id = 1) 29 | BEGIN 30 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp; 31 | END; 32 | 33 | DROP TRIGGER IF EXISTS deposit_action_after_insert; 34 | CREATE TRIGGER deposit_action_after_insert 35 | AFTER INSERT ON deposit_actions 36 | FOR EACH ROW 37 | WHEN (SELECT value FROM settings WHERE id = 1) 38 | BEGIN 39 | DELETE FROM ledger WHERE timestamp >= NEW.timestamp; 40 | END; 41 | 42 | DROP TRIGGER IF EXISTS deposit_action_after_update; 43 | CREATE TRIGGER deposit_action_after_update 44 | AFTER UPDATE OF timestamp, account_id, type, asset_id, qty ON deposit_actions 45 | FOR EACH ROW 46 | WHEN (SELECT value FROM settings WHERE id = 1) 47 | BEGIN 48 | DELETE FROM ledger WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; 49 | END; 50 | 51 | 52 | DROP VIEW IF EXISTS operation_sequence; 53 | CREATE VIEW operation_sequence AS 54 | SELECT m.op_type, m.id, m.timestamp, m.account_id, subtype 55 | FROM 56 | ( 57 | SELECT op_type, 1 AS seq, id, timestamp, account_id, 0 AS subtype FROM actions 58 | UNION ALL 59 | SELECT op_type, 2 AS seq, id, timestamp, account_id, type AS subtype FROM dividends 60 | UNION ALL 61 | SELECT op_type, 3 AS seq, id, timestamp, account_id, type AS subtype FROM asset_actions 62 | UNION ALL 63 | SELECT op_type, 4 AS seq, id, timestamp, account_id, 0 AS subtype FROM trades 64 | UNION ALL 65 | SELECT op_type, 5 AS seq, id, withdrawal_timestamp AS timestamp, withdrawal_account AS account_id, -1 AS subtype FROM transfers 66 | UNION ALL 67 | SELECT op_type, 5 AS seq, id, withdrawal_timestamp AS timestamp, fee_account AS account_id, 0 AS subtype FROM transfers WHERE NOT fee IS NULL 68 | UNION ALL 69 | SELECT op_type, 5 AS seq, id, deposit_timestamp AS timestamp, deposit_account AS account_id, 1 AS subtype FROM transfers 70 | UNION ALL 71 | SELECT td.op_type, 6 AS seq, td.id, da.timestamp, td.account_id, da.id AS subtype FROM deposit_actions AS da LEFT JOIN term_deposits AS td ON da.deposit_id=td.id WHERE da.action_type<=100 72 | ) AS m 73 | ORDER BY m.timestamp, m.seq, m.subtype, m.id; 74 | -------------------------------------------------------------------------------- 75 | PRAGMA foreign_keys = 1; 76 | -------------------------------------------------------------------------------- 77 | -- Set new DB schema version 78 | UPDATE settings SET value=52 WHERE name='SchemaVersion'; 79 | COMMIT; 80 | -- Reduce file size 81 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_54.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | DELETE FROM quotes WHERE quote='NaN'; 4 | -------------------------------------------------------------------------------- 5 | -- Set new DB schema version 6 | UPDATE settings SET value=54 WHERE name='SchemaVersion'; 7 | COMMIT; 8 | -- Reduce file size 9 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_55.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (20, 'DlgGeometry_Accounts', ''); 4 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (21, 'DlgViewState_Accounts', ''); 5 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (22, 'DlgGeometry_Assets', ''); 6 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (23, 'DlgViewState_Assets', ''); 7 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (24, 'DlgGeometry_Peers', ''); 8 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (25, 'DlgViewState_Peers', ''); 9 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (26, 'DlgGeometry_Categories', ''); 10 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (27, 'DlgViewState_Categories', ''); 11 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (28, 'DlgGeometry_Tags', ''); 12 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (29, 'DlgViewState_Tags', ''); 13 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (30, 'DlgGeometry_Quotes', ''); 14 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (31, 'DlgViewState_Quotes', ''); 15 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (32, 'DlgGeometry_Base currency', ''); 16 | INSERT OR REPLACE INTO settings(id, name, value) VALUES (33, 'DlgViewState_Base currency', ''); 17 | -------------------------------------------------------------------------------- 18 | -- Set new DB schema version 19 | UPDATE settings SET value=55 WHERE name='SchemaVersion'; 20 | COMMIT; 21 | -- Reduce file size 22 | VACUUM; -------------------------------------------------------------------------------- /jal/updates/jal_delta_58.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | PRAGMA foreign_keys = OFF; 4 | -------------------------------------------------------------------------------- 5 | UPDATE trades SET number='' WHERE number IS NULL; 6 | UPDATE trades SET note='' WHERE note IS NULL; 7 | -- Forbid NULL settlement and fee 8 | CREATE TABLE temp_trades AS SELECT * FROM trades; 9 | DROP TABLE IF EXISTS trades; 10 | CREATE TABLE trades ( 11 | oid INTEGER PRIMARY KEY UNIQUE NOT NULL, 12 | otype INTEGER NOT NULL DEFAULT (3), 13 | timestamp INTEGER NOT NULL, 14 | settlement INTEGER NOT NULL DEFAULT (0), 15 | number TEXT NOT NULL DEFAULT (''), 16 | account_id INTEGER REFERENCES accounts (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 17 | asset_id INTEGER REFERENCES assets (id) ON DELETE CASCADE ON UPDATE CASCADE NOT NULL, 18 | qty TEXT NOT NULL DEFAULT ('0'), 19 | price TEXT NOT NULL DEFAULT ('0'), 20 | fee TEXT NOT NULL DEFAULT ('0'), 21 | note TEXT NOT NULL DEFAULT ('') 22 | ); 23 | INSERT INTO trades (oid, otype, timestamp, settlement, number, account_id, asset_id, qty, price, fee, note) 24 | SELECT oid, otype, timestamp, settlement, number, account_id, asset_id, qty, price, fee, note FROM temp_trades; 25 | DROP TABLE temp_trades; 26 | CREATE TRIGGER trades_after_delete AFTER DELETE ON trades FOR EACH ROW BEGIN DELETE FROM ledger WHERE timestamp >= OLD.timestamp; DELETE FROM trades_opened WHERE timestamp >= OLD.timestamp; END; 27 | CREATE TRIGGER trades_after_insert AFTER INSERT ON trades FOR EACH ROW BEGIN DELETE FROM ledger WHERE timestamp >= NEW.timestamp; DELETE FROM trades_opened WHERE timestamp >= NEW.timestamp; END; 28 | CREATE TRIGGER trades_after_update AFTER UPDATE OF timestamp, account_id, asset_id, qty, price, fee ON trades FOR EACH ROW BEGIN DELETE FROM ledger WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; DELETE FROM trades_opened WHERE timestamp >= OLD.timestamp OR timestamp >= NEW.timestamp; END; 29 | -------------------------------------------------------------------------------- 30 | DROP INDEX IF EXISTS ledger_by_operation; 31 | CREATE INDEX ledger_by_operation ON ledger (otype, oid, opart, book_account); 32 | DROP INDEX IF EXISTS ledger_by_time; 33 | CREATE INDEX ledger_by_time ON ledger (timestamp, asset_id, account_id); 34 | -------------------------------------------------------------------------------- 35 | -- Set new DB schema version 36 | UPDATE settings SET value=58 WHERE name='SchemaVersion'; 37 | INSERT OR REPLACE INTO settings(name, value) VALUES ('RebuildDB', 1); 38 | COMMIT; -------------------------------------------------------------------------------- /jal/updates/jal_delta_59.sql: -------------------------------------------------------------------------------- 1 | BEGIN TRANSACTION; 2 | -------------------------------------------------------------------------------- 3 | ALTER TABLE accounts ADD COLUMN credit TEXT DEFAULT ('0') NOT NULL; 4 | -------------------------------------------------------------------------------- 5 | INSERT OR REPLACE INTO settings(name, value) VALUES('ShowInactiveAccountBalances', 0); 6 | INSERT OR REPLACE INTO settings(name, value) VALUES('UseAccountCreditLimit', 1); 7 | -------------------------------------------------------------------------------- 8 | -- Set new DB schema version 9 | UPDATE settings SET value=59 WHERE name='SchemaVersion'; 10 | COMMIT; -------------------------------------------------------------------------------- /jal/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by find_packages() in setup.py -------------------------------------------------------------------------------- /jal/widgets/custom/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/jal/widgets/custom/__init__.py -------------------------------------------------------------------------------- /jal/widgets/custom/db_lookup_combobox.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, Property 2 | from PySide6.QtWidgets import QComboBox 3 | from jal.db.db import JalModel 4 | 5 | 6 | # Combobox to lookup in db tables: 7 | # It is mandatory to set up 'table', 'key_field' and 'field' properties at design time 8 | class DbLookupComboBox(QComboBox): 9 | def __init__(self, parent=None): 10 | super().__init__(parent=parent) 11 | self._model = None 12 | self._table = '' 13 | self._key_field = '' 14 | self._field = '' 15 | self._selected_id = -1 16 | 17 | def getKey(self): 18 | if self._model is None: 19 | return 0 20 | return self._model.get_value(self._key_field, self._field, self.currentText()) 21 | 22 | def setKey(self, selected_id): 23 | if self._selected_id == selected_id: 24 | return 25 | self._selected_id = selected_id 26 | value = self._model.get_value(self._field, self._key_field, selected_id) 27 | self.setCurrentIndex(self.findText(value)) 28 | 29 | def getTable(self): 30 | return self._table 31 | 32 | def setTable(self, table): 33 | if self._table == table: 34 | return 35 | self._table = table 36 | self.setupDb() 37 | 38 | def getKeyField(self): 39 | return self._key_field 40 | 41 | def setKeyField(self, field_name): 42 | if self._key_field == field_name: 43 | return 44 | self._key_field = field_name 45 | 46 | def getField(self): 47 | return self._field 48 | 49 | def setField(self, field_name): 50 | if self._field == field_name: 51 | return 52 | self._field = field_name 53 | self.setupDb() 54 | 55 | key = Property(int, getKey, setKey, user=True) 56 | db_table = Property(str, getTable, setTable) 57 | key_field = Property(str, getKeyField, setKeyField) 58 | db_field = Property(str, getField, setField) 59 | 60 | def setupDb(self): 61 | if not self._table or not self._field: 62 | return 63 | self._model = JalModel(self, self._table) 64 | field_idx = self._model.fieldIndex(self._field) 65 | self._model.setSort(field_idx, Qt.AscendingOrder) 66 | self._model.select() 67 | self.setModel(self._model) 68 | self.setModelColumn(field_idx) 69 | -------------------------------------------------------------------------------- /jal/widgets/custom/tableview_with_footer.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QAbstractItemModel 2 | from PySide6.QtWidgets import QTableView 3 | from PySide6.QtGui import QResizeEvent 4 | from .table_footer import FooterView 5 | 6 | # ---------------------------------------------------------------------------------------------------------------------- 7 | # File implements TableViewWithFooter class that is a descendant of QTableView class with footer. 8 | # Underlying model should support footerData(section, role) method in order to provide data for the footer. 9 | # The footer is implemented as FooterView class that is derived from QHeaderView. 10 | # ---------------------------------------------------------------------------------------------------------------------- 11 | class TableViewWithFooter(QTableView): 12 | def __init__(self, parent_view): 13 | self._parent_view = parent_view 14 | super().__init__(parent_view) 15 | self._footer = FooterView(self, self.horizontalHeader()) 16 | 17 | def footer(self) -> FooterView: 18 | return self._footer 19 | 20 | # Create a bottom margin for footer placement (mirror of a top header margin) 21 | def resizeEvent(self, event: QResizeEvent) -> None: 22 | super().resizeEvent(event) 23 | margins = self.viewportMargins() 24 | self.setViewportMargins(margins.left(), margins.top(), margins.right(), margins.top()) 25 | self._footer.on_header_geometry() 26 | 27 | def setModel(self, model: QAbstractItemModel) -> None: 28 | super().setModel(model) 29 | self._footer.setModel(model) 30 | 31 | def setColumnHidden(self, idx: int, hidden: bool) -> None: 32 | super().setColumnHidden(idx, hidden) 33 | self._footer.setSectionHidden(idx, hidden) 34 | -------------------------------------------------------------------------------- /jal/widgets/custom/treeview_with_footer.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QAbstractItemModel 2 | from PySide6.QtWidgets import QTreeView 3 | from PySide6.QtGui import QResizeEvent 4 | from .table_footer import FooterView 5 | 6 | # ---------------------------------------------------------------------------------------------------------------------- 7 | # File implements TableViewWithFooter class that is a descendant of QTableView class with footer. 8 | # Underlying model should support footerData(section, role) method in order to provide data for the footer. 9 | # The footer is implemented as FooterView class that is derived from QHeaderView. 10 | # ---------------------------------------------------------------------------------------------------------------------- 11 | class TreeViewWithFooter(QTreeView): 12 | def __init__(self, parent_view): 13 | self._parent_view = parent_view 14 | super().__init__(parent_view) 15 | self._footer = FooterView(self, self.header()) 16 | 17 | def footer(self) -> FooterView: 18 | return self._footer 19 | 20 | # Create a bottom margin for footer placement (mirror of a top header margin) 21 | def resizeEvent(self, event: QResizeEvent) -> None: 22 | super().resizeEvent(event) 23 | margins = self.viewportMargins() 24 | self.setViewportMargins(margins.left(), margins.top(), margins.right(), margins.top()) 25 | self._footer.on_header_geometry() 26 | 27 | def setModel(self, model: QAbstractItemModel) -> None: 28 | super().setModel(model) 29 | self._footer.setModel(model) 30 | 31 | def setColumnHidden(self, idx: int, hidden: bool) -> None: 32 | super().setColumnHidden(idx, hidden) 33 | self._footer.setSectionHidden(idx, hidden) 34 | -------------------------------------------------------------------------------- /jal/widgets/mdi.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Slot, Signal 2 | from PySide6.QtWidgets import QWidget, QMdiArea, QTabBar, QVBoxLayout 3 | 4 | 5 | # ---------------------------------------------------------------------------------------------------------------------- 6 | # Base class that is used for any other widget which should be displayed inside JAL MainWindow MDI 7 | # implemented as TabbedMdiArea() class (below) 8 | class MdiWidget(QWidget): 9 | onClose = Signal(QWidget) 10 | 11 | def __init__(self, parent=None): 12 | super().__init__(parent) 13 | 14 | @Slot() 15 | def closeEvent(self, event): 16 | self.onClose.emit(self.parent()) 17 | super().closeEvent(event) 18 | 19 | def refresh(self): 20 | pass 21 | 22 | 23 | # ---------------------------------------------------------------------------------------------------------------------- 24 | # Class that acts as QMdiArea in SubWindowView mode but has Tabs at the same time 25 | # Child windows should be derived from MdiWidget class for correct operation 26 | class TabbedMdiArea(QWidget): 27 | def __init__(self, parent=None): 28 | super().__init__(parent=parent) 29 | 30 | self.layout = QVBoxLayout(self) 31 | self.layout.setContentsMargins(0, 0, 0, 0) 32 | self.layout.setSpacing(0) 33 | self.setLayout(self.layout) 34 | 35 | self.mdi = QMdiArea(self) 36 | self.mdi.setOption(QMdiArea.DontMaximizeSubWindowOnActivation) 37 | self.layout.addWidget(self.mdi) 38 | 39 | self.tabs = QTabBar(self) 40 | self.tabs.setShape(QTabBar.RoundedSouth) 41 | self.tabs.setExpanding(False) 42 | self.tabs.setTabsClosable(True) 43 | self.layout.addWidget(self.tabs) 44 | 45 | self.mdi.subWindowActivated.connect(self.subWindowActivated) 46 | self.tabs.currentChanged.connect(self.tabClicked) 47 | self.tabs.tabCloseRequested.connect(self.tabClose) 48 | 49 | def subWindowList(self, order=QMdiArea.CreationOrder): 50 | return self.mdi.subWindowList(order) 51 | 52 | def addSubWindow(self, widget, maximized=False, size=None): 53 | sub_window = self.mdi.addSubWindow(widget) 54 | widget.onClose.connect(self.subWindowClosed) 55 | self.tabs.addTab(sub_window.windowTitle().replace('&', '&&')) # & -> && to prevent shortcut creation 56 | if maximized: 57 | sub_window.showMaximized() 58 | else: # show centered otherwise 59 | if size is None: 60 | w = sub_window.width() 61 | h = sub_window.height() 62 | else: 63 | w = size[0] 64 | h = size[1] 65 | x = self.mdi.x() + self.mdi.width() / 2 - w / 2 66 | y = self.mdi.y() + self.mdi.height() / 2 - h / 2 67 | sub_window.setGeometry(x, y, w, h) 68 | sub_window.show() 69 | return sub_window 70 | 71 | @Slot() 72 | def subWindowActivated(self, window): 73 | if window is not None: 74 | self.tabs.setCurrentIndex(self.mdi.subWindowList().index(window)) 75 | 76 | @Slot() 77 | def subWindowClosed(self, window): 78 | index = self.subWindowList().index(window) 79 | self.tabs.removeTab(index) 80 | 81 | @Slot() 82 | def tabClicked(self, index): 83 | try: 84 | sub_window = self.subWindowList()[index] 85 | except IndexError: 86 | return 87 | self.mdi.setActiveSubWindow(sub_window) 88 | 89 | @Slot() 90 | def tabClose(self, index): 91 | try: 92 | sub_window = self.subWindowList()[index] 93 | except KeyError: 94 | return 95 | self.mdi.removeSubWindow(sub_window) 96 | self.tabs.removeTab(index) 97 | -------------------------------------------------------------------------------- /jal/widgets/operations_tabs.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal, Slot 2 | from PySide6.QtWidgets import QWidget, QStackedWidget, QMessageBox 3 | 4 | from jal.widgets.corporate_action_widget import CorporateActionWidget 5 | from jal.widgets.asset_payment_widget import AssetPaymentWidget 6 | from jal.widgets.income_spending_widget import IncomeSpendingWidget 7 | from jal.widgets.trade_widget import TradeWidget 8 | from jal.widgets.transfer_widget import TransferWidget 9 | from jal.widgets.term_deposit_widget import TermDepositWidget 10 | from jal.db.operations import LedgerTransaction 11 | 12 | 13 | class JalOperationsTabs(QStackedWidget): 14 | dbUpdated = Signal() 15 | 16 | def __init__(self, parent): 17 | super().__init__(parent) 18 | self.widgets = {LedgerTransaction.NA: QWidget(self), 19 | LedgerTransaction.IncomeSpending: IncomeSpendingWidget(self), 20 | LedgerTransaction.AssetPayment: AssetPaymentWidget(self), 21 | LedgerTransaction.Trade: TradeWidget(self), 22 | LedgerTransaction.Transfer: TransferWidget(self), 23 | LedgerTransaction.CorporateAction: CorporateActionWidget(self), 24 | LedgerTransaction.TermDeposit: TermDepositWidget(self)} 25 | for key, widget in self.widgets.items(): 26 | if key != LedgerTransaction.NA: 27 | widget.dbUpdated.connect(self.dbUpdated) 28 | self.addWidget(widget) 29 | self.setCurrentIndex(0) 30 | 31 | # Returns a dictionary of {type, name} of operations that widget is able to handle 32 | def get_operations_list(self) -> dict: 33 | operations = {} 34 | for key, widget in self.widgets.items(): 35 | if hasattr(widget, "name"): 36 | operations[key] = widget.name 37 | return operations 38 | 39 | def _check_for_changes(self): 40 | for key, widget in self.widgets.items(): 41 | if key == LedgerTransaction.NA: 42 | continue 43 | if widget.modified: 44 | reply = QMessageBox().warning(self, self.tr("You have unsaved changes"), 45 | widget.name + 46 | self.tr(" has uncommitted changes,\ndo you want to save it?"), 47 | QMessageBox.Yes, QMessageBox.No) 48 | if reply == QMessageBox.Yes: 49 | widget.saveChanges() 50 | else: 51 | widget.revertChanges() 52 | 53 | def show_operation(self, otype, oid): 54 | self._check_for_changes() 55 | self.setCurrentIndex(otype) 56 | if otype != LedgerTransaction.NA: 57 | self.widgets[otype].set_id(oid) 58 | 59 | def new_operation(self, otype, account_id): 60 | self._check_for_changes() 61 | self.widgets[otype].createNew(account_id=account_id) 62 | self.setCurrentIndex(otype) 63 | 64 | @Slot() 65 | def copy_operation(self): 66 | otype = self.currentIndex() 67 | if otype == LedgerTransaction.NA: 68 | return 69 | self._check_for_changes() 70 | self.widgets[otype].copyNew() 71 | -------------------------------------------------------------------------------- /jal/widgets/selection_dialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Slot 2 | from PySide6.QtWidgets import QDialog, QMessageBox 3 | from jal.ui.ui_select_reference_dlg import Ui_SelectReferenceDlg 4 | from jal.widgets.helpers import center_window 5 | from jal.widgets.reference_selector import PeerSelector 6 | from jal.widgets.reference_selector import CategorySelector 7 | from jal.widgets.reference_selector import TagSelector 8 | 9 | 10 | #----------------------------------------------------------------------------------------------------------------------- 11 | # Common base GUI dialog class for selector dialogs. Takes window title and label comment to describe selection 12 | class SelectReferenceDialog(QDialog): 13 | def __init__(self, parent=None, title='', description=''): 14 | super().__init__(parent=parent) 15 | self.ui = Ui_SelectReferenceDlg() 16 | self.ui.setupUi(self) 17 | self.selected_id = 0 18 | self.setWindowTitle(title) 19 | self.ui.DescriptionLabel.setText(description) 20 | center_window(self) 21 | 22 | @Slot() 23 | def closeEvent(self, event): 24 | if self.selected_id == 0: 25 | QMessageBox().warning(None, self.tr("No selection"), self.tr("You should select something"), QMessageBox.Ok) 26 | event.ignore() 27 | return 28 | self.setResult(QDialog.Accepted) 29 | event.accept() 30 | 31 | 32 | #----------------------------------------------------------------------------------------------------------------------- 33 | # Dialog for peer selection 34 | # Constructor takes description to show and default_peer for initial choice 35 | class SelectPeerDialog(SelectReferenceDialog): 36 | def __init__(self, description, default_peer=0): 37 | super().__init__(title=self.tr("Please select peer"), description=description) 38 | self.PeerWidget = PeerSelector(self.ui.SelectorFrame) 39 | self.ui.FrameLayout.addWidget(self.PeerWidget) 40 | self.PeerWidget.selected_id = self.selected_id = default_peer 41 | 42 | @Slot() 43 | def closeEvent(self, event): 44 | self.selected_id = self.PeerWidget.selected_id 45 | super().closeEvent(event) 46 | 47 | 48 | #----------------------------------------------------------------------------------------------------------------------- 49 | # Dialog for category selection 50 | # Constructor takes description to show and default_category for initial choice 51 | class SelectCategoryDialog(SelectReferenceDialog): 52 | def __init__(self, parent=None, description='', default_category=0): 53 | super().__init__(parent, title=self.tr("Please select category"), description=description) 54 | self.CategoryWidget = CategorySelector(self.ui.SelectorFrame) 55 | self.ui.FrameLayout.addWidget(self.CategoryWidget) 56 | self.CategoryWidget.selected_id = self.selected_id = default_category 57 | 58 | @Slot() 59 | def closeEvent(self, event): 60 | self.selected_id = self.CategoryWidget.selected_id 61 | super().closeEvent(event) 62 | 63 | 64 | #----------------------------------------------------------------------------------------------------------------------- 65 | # Dialog for tag selection 66 | # Constructor takes description to show and default_tag for initial choice 67 | class SelectTagDialog(SelectReferenceDialog): 68 | def __init__(self, parent=None, description='', default_tag=0): 69 | super().__init__(parent, title=self.tr("Please select tag"), description=description) 70 | self.TagWidget = TagSelector(self.ui.SelectorFrame) 71 | self.ui.FrameLayout.addWidget(self.TagWidget) 72 | self.TagWidget.selected_id = self.selected_id = default_tag 73 | 74 | @Slot() 75 | def closeEvent(self, event): 76 | self.selected_id = self.TagWidget.selected_id 77 | super().closeEvent(event) 78 | -------------------------------------------------------------------------------- /jal/widgets/trade_widget.py: -------------------------------------------------------------------------------- 1 | from jal.ui.widgets.ui_trade_operation import Ui_TradeOperation 2 | from jal.widgets.abstract_operation_details import AbstractOperationDetails 3 | from jal.widgets.delegates import WidgetMapperDelegateBase 4 | from jal.db.operations import LedgerTransaction 5 | from jal.db.helpers import now_ts 6 | 7 | 8 | # ---------------------------------------------------------------------------------------------------------------------- 9 | class TradeWidgetDelegate(WidgetMapperDelegateBase): 10 | def __init__(self, parent=None): 11 | super().__init__(parent=parent) 12 | self.delegates = {'timestamp': self.timestamp_delegate, 13 | 'settlement': self.timestamp_delegate, 14 | 'asset_id': self.symbol_delegate, 15 | 'qty': self.decimal_long_delegate, 16 | 'price': self.decimal_long_delegate, 17 | 'fee': self.decimal_long_delegate} 18 | 19 | 20 | # ---------------------------------------------------------------------------------------------------------------------- 21 | class TradeWidget(AbstractOperationDetails): 22 | def __init__(self, parent=None): 23 | super().__init__(parent=parent, ui_class=Ui_TradeOperation) 24 | self.operation_type = LedgerTransaction.Trade 25 | super()._init_db("trades") 26 | self.ui.timestamp_editor.setFixedWidth(self.ui.timestamp_editor.fontMetrics().horizontalAdvance("00/00/0000 00:00:00") * 1.25) 27 | self.ui.settlement_editor.setFixedWidth(self.ui.settlement_editor.fontMetrics().horizontalAdvance("00/00/0000") * 1.5) 28 | 29 | self.mapper.setItemDelegate(TradeWidgetDelegate(self.mapper)) 30 | 31 | self.ui.account_widget.changed.connect(self.mapper.submit) 32 | self.ui.asset_widget.changed.connect(self.mapper.submit) 33 | 34 | self.mapper.addMapping(self.ui.timestamp_editor, self.model.fieldIndex("timestamp")) 35 | self.mapper.addMapping(self.ui.settlement_editor, self.model.fieldIndex("settlement")) 36 | self.mapper.addMapping(self.ui.account_widget, self.model.fieldIndex("account_id")) 37 | self.mapper.addMapping(self.ui.currency_price, self.model.fieldIndex("account_id")) 38 | self.mapper.addMapping(self.ui.currency_fee, self.model.fieldIndex("account_id")) 39 | self.mapper.addMapping(self.ui.asset_widget, self.model.fieldIndex("asset_id")) 40 | self.mapper.addMapping(self.ui.number, self.model.fieldIndex("number")) 41 | self.mapper.addMapping(self.ui.qty_edit, self.model.fieldIndex("qty")) 42 | self.mapper.addMapping(self.ui.price_edit, self.model.fieldIndex("price")) 43 | self.mapper.addMapping(self.ui.fee_edit, self.model.fieldIndex("fee")) 44 | self.mapper.addMapping(self.ui.note, self.model.fieldIndex("note")) 45 | 46 | self.model.select() 47 | 48 | def prepareNew(self, account_id): 49 | new_record = super().prepareNew(account_id) 50 | new_record.setValue("timestamp", now_ts()) 51 | new_record.setValue("settlement", now_ts()) 52 | new_record.setValue("number", '') 53 | new_record.setValue("account_id", account_id) 54 | new_record.setValue("asset_id", 0) 55 | new_record.setValue("qty", '0') 56 | new_record.setValue("price", '0') 57 | new_record.setValue("fee", '0') 58 | new_record.setValue("note", None) 59 | return new_record 60 | 61 | def copyToNew(self, row): 62 | new_record = self.model.record(row) 63 | new_record.setNull("oid") 64 | new_record.setValue("timestamp", now_ts()) 65 | new_record.setValue("settlement", now_ts()) 66 | new_record.setValue("number", '') 67 | return new_record -------------------------------------------------------------------------------- /register_designer_plugins.py: -------------------------------------------------------------------------------- 1 | # Environment variable PYSIDE_DESIGNER_PLUGINS should contain path to this file 2 | 3 | from designer_plugins.plugin_date_range_selector import DateRangeSelectorPlugin 4 | from designer_plugins.plugin_db_lookup_combobox import DbLookupComboBoxPlugin 5 | from designer_plugins.plugin_log_viewer import LogViewerPlugin 6 | from designer_plugins.plugin_tableview_with_footer import TableViewWithFooterPlugin 7 | from designer_plugins.plugin_treeview_with_footer import TreeViewWithFooterPlugin 8 | 9 | from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection 10 | 11 | if __name__ == '__main__': 12 | QPyDesignerCustomWidgetCollection.addCustomWidget(DateRangeSelectorPlugin()) 13 | QPyDesignerCustomWidgetCollection.addCustomWidget(DbLookupComboBoxPlugin()) 14 | QPyDesignerCustomWidgetCollection.addCustomWidget(LogViewerPlugin()) 15 | QPyDesignerCustomWidgetCollection.addCustomWidget(TableViewWithFooterPlugin()) 16 | QPyDesignerCustomWidgetCollection.addCustomWidget(TreeViewWithFooterPlugin()) 17 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml>=4.5.0 2 | pandas>=1.1.1 3 | PySide6>=6.5.1 4 | requests>=2.24.0 5 | XlsxWriter>=1.3.3 6 | pytest 7 | jsonschema 8 | sqlparse 9 | oauthlib 10 | requests-oauthlib 11 | setuptools 12 | packaging -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import faulthandler 3 | from jal.jal import main 4 | 5 | if __name__ == "__main__": 6 | log_file_fd = open("jal_faults.log", "a") 7 | faulthandler.enable(log_file_fd) 8 | main() 9 | 10 | # Below code is the same but initiates application via entry point defined in already installed package 11 | # from importlib.metadata import distribution 12 | # 13 | # main_entry = distribution('jal').entry_points[0].load() 14 | # 15 | # main_entry() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | 5 | def read(rel_path: str) -> str: 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | with open(os.path.join(here, rel_path), 'r', encoding='utf-8') as fp: 8 | return fp.read() 9 | 10 | 11 | def get_version(rel_path: str) -> str: 12 | for line in read(rel_path).splitlines(): 13 | if line.startswith('__version__'): 14 | quote_char = '"' if '"' in line else "'" 15 | return line.split(quote_char)[1] 16 | raise RuntimeError("Unable to find version string.") 17 | 18 | 19 | setup( 20 | name="jal", 21 | version=get_version("jal/__init__.py"), 22 | author_email="jal@gmx.ru", 23 | description="Just Another Ledger - project to track personal financial records", 24 | long_description_content_type='text/markdown', 25 | long_description=read('jal/pypi_description.md'), 26 | packages=find_packages(), 27 | package_dir={'jal': 'jal'}, 28 | python_requires=">=3.8.1", 29 | url="https://github.com/titov-vv/jal", 30 | classifiers=[ 31 | "Development Status :: 3 - Alpha", 32 | "Topic :: Office/Business", 33 | "Topic :: Office/Business :: Financial", 34 | "Topic :: Office/Business :: Financial :: Accounting", 35 | "Topic :: Office/Business :: Financial :: Investment", 36 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python" 39 | ], 40 | install_requires=["lxml", "pandas", "PySide6>=6.5.1", "requests>=2.24", "XlsxWriter>=1.3.3", "jsonschema", "sqlparse", "oauthlib", "requests-oauthlib", "setuptools"], 41 | entry_points={ 42 | 'console_scripts': ['jal=jal.jal:main', ] 43 | }, 44 | include_package_data=True, 45 | package_data={ 46 | '': ['*.sql', '*.json', 'languages/*.qm', 'languages/*.png', 'pypi_description.md', 'img/*.ico', 'img/*.png'] 47 | } 48 | ) 49 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Dummy __init__.py file to facilitate package recognition by pytest -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # 2 | # conftest.py 3 | import sys 4 | from os.path import dirname 5 | from os.path import abspath, join 6 | root_dir = dirname(dirname(abspath(__file__))) 7 | root_dir = join(root_dir, 'jal') 8 | sys.path.append(root_dir) -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | log_cli=true 3 | qt_api=pyside6 4 | -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2020.dc0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2020.dc0 -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2020_empty.dc0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2020_empty.dc0 -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2021.dc1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2021.dc1 -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2021_empty.dc1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2021_empty.dc1 -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2022.dc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2022.dc2 -------------------------------------------------------------------------------- /tests/test_data/3ndfl_2022_empty.dc2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/3ndfl_2022_empty.dc2 -------------------------------------------------------------------------------- /tests/test_data/deals_set.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/deals_set.tgz -------------------------------------------------------------------------------- /tests/test_data/ibkr_bond.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1609459200, 1640995199], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321", "currency": 1, "precision": 10, "cash_begin": 12.345, "cash_end": 1234567.249966388, "cash_end_settled": 1234567.89} 5 | ], 6 | "assets": [ 7 | {"id": 1, "type": "money", "name": "USD"}, 8 | {"id": 2, "type": "bond", "name": "X 6 1/4 03/15/26", "isin": "US912909AN84"}, 9 | {"id": 3, "type": "bond", "name": "X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26", "isin": "US912CALAN84"}, 10 | {"id": 4, "type": "stock", "name": "APPLE INC", "isin": "US0378331005"} 11 | ], 12 | "symbols": [ 13 | {"id": 1, "asset": 1, "symbol": "USD"}, 14 | {"id": 2, "asset": 2, "symbol": "X 6 1/4 03/15/26", "currency": 1}, 15 | {"id": 3, "asset": 3, "symbol": "X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26", "currency": 1}, 16 | {"id": 4, "asset": 4, "symbol": "AAPL", "currency": 1,"note": "NASDAQ"} 17 | ], 18 | "assets_data": [ 19 | {"id": 1, "asset": 2, "reg_number": "912909AN8", "expiry": 1773532800, "principal": 1000}, 20 | {"id": 2, "asset": 3, "reg_number": "912CALAN8", "expiry": 1773532800, "principal": 1000}, 21 | {"id": 3, "asset": 4, "reg_number": "037833100"} 22 | ], 23 | "trades": [ 24 | {"id": 1, "number": "2882737839", "timestamp": 1622131065, "settlement": 1622246400, "account": 1, "asset": 2, "quantity": 2.0, "price": 637.09, "fee": 2.0}, 25 | {"id": 2, "number": "TEST_ID", "timestamp": 1631709966, "settlement": 1631836800, "account": 1, "asset": 4, "quantity": 1.0, "price": 123.45, "fee": -0.12}, 26 | {"id": 3, "number": "", "timestamp": 1640781240, "settlement": 1640908800, "account": 1, "asset": 2, "quantity": 2.0, "price": 637.09, "fee": 2.0}, 27 | {"id": 4, "number": "17849517124", "timestamp": 1632515100, "settlement": 1632515100, "account": 1, "asset": 3, "quantity": -2.0, "price": 1031.25, "fee": 0.0, "note": "(US912CALAN84) BOND MATURITY FOR USD 1.03125 PER BOND (X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26, X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26, US912CALAN84)"} 28 | ], 29 | "corporate_actions": [ 30 | {"id": 1, "type": "merger", "account": 1, "timestamp": 1630007100, "number": "17569476329", "asset": 2, "quantity": 2.0, 31 | "outcome": [{"asset": 3, "quantity": 2.0, "share": 0.0}],"description": "X 6 1/4 03/15/26(US912909AN84) TENDERED TO US912CALAN84 1 FOR 1 (X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26, X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26, US912CALAN84)"} 32 | ], 33 | "asset_payments": [ 34 | {"id": 1, "type": "interest", "account": 1, "timestamp": 1622131065, "number": "2882737839", "asset": 2, "amount": -25.69, "description": "PURCHASE ACCRUED INT X 6 1/4 03/15/26"}, 35 | {"id": 2, "type": "interest", "account": 1, "timestamp": 1631664000, "number": "", "asset": 2, "amount": 62.0, "description": "BOND COUPON PAYMENT (X 6 1/4 03/15/26)"}, 36 | {"id": 3, "type": "interest", "account": 1, "timestamp": 1631664000, "number": "TEST_ID", "asset": 2, "amount": 0.5, "description": "BOND COUPON PAYMENT (X 6 1/4 03/15/26)"}, 37 | {"id": 4, "type": "interest", "account": 1, "timestamp": 1632700800, "number": "", "asset": 3, "amount": 3.82, "description": "BOND COUPON PAYMENT (X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26)"} 38 | ], 39 | "income_spending": [], 40 | "transfers": [] 41 | } -------------------------------------------------------------------------------- /tests/test_data/ibkr_cfd.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1641168000, 1672703999], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321F", "currency": 1, "precision": 10, "cash_begin": 12.345, "cash_end": 1234567.249966388, "cash_end_settled": 1234567.89} 5 | ], 6 | "assets": [ 7 | {"id": 1, "name": "USD", "type": "money"}, 8 | {"id": 2, "name": "USD TSLA", "type": "cfd"}, 9 | {"id": 3, "name": "USD MU", "type": "cfd"} 10 | ], 11 | "symbols": [ 12 | {"id": 1, "asset": 1, "symbol": "USD"}, 13 | {"id": 2, "asset": 2, "currency": 1, "symbol": "TSLAn"}, 14 | {"id": 3, "asset": 3, "currency": 1, "symbol": "MUn"} 15 | ], 16 | "assets_data": [ 17 | {"id": 1, "asset": 2, "expiry": 2093212800}, 18 | {"id": 2, "asset": 3, "expiry": 2093212800} 19 | ], 20 | "trades": [ 21 | {"id": 1, "number": "200000523", "timestamp": 1645695065, "settlement": 1645833600, "account": 1, "asset": 2, "quantity": -1.0, "price": 901.4, "fee": 1.0}, 22 | {"id": 2, "number": "211321719", "timestamp": 1657106512, "settlement": 1657238400, "account": 1, "asset": 3, "quantity": -37.0, "price": 60.074, "fee": 1.0}, 23 | {"id": 3, "number": "211326559", "timestamp": 1657110438, "settlement": 1657238400, "account": 1, "asset": 3, "quantity": -1.0, "price": 59.5, "fee": 1.0}, 24 | {"id": 4, "number": "211380318", "timestamp": 1657186655, "settlement": 1657497600, "account": 1, "asset": 3, "quantity": 38.0, "price": 58.035, "fee": 1.0} 25 | ], 26 | "corporate_actions": [ 27 | {"id": 1, "type": "split", "account": 1, "timestamp": 1661372700, "number": "1031017435", "asset": 2, "quantity": -1.0, 28 | "outcome": [{"asset": 2, "quantity": -3.0, "share": 1.0}], "description": "TSLAn(131067756) SPLIT 3 FOR 1 (TSLAn, USD TSLA, )"} 29 | ], 30 | "asset_payments": [ 31 | {"id": 1, "type": "dividend", "account": 1, "timestamp": 1658866800, "ex_date": 1657238400, "number": "", "asset": 3, "amount": -4.37, "description": "MUn(120549262) PAYMENT IN LIEU OF DIVIDEND (ICFDUK Dividend)"} 32 | ], 33 | "income_spending": [ 34 | {"id": 1, "timestamp": 1657152000, "account": 1, "peer": 0, "lines": [{"amount": -0.28, "category": -5, "description": "LONG CFD INTEREST FOR 07-JUL-2022"}]}, 35 | {"id": 2, "timestamp": 1657065600, "account": 1, "peer": 0, "lines": [{"amount": -0.02, "category": -5, "description": "CFD BORROW FEE FOR MU for 06-JUL-2022"}]}, 36 | {"id": 3, "timestamp": 1657065600, "account": 1, "peer": 0, "lines": [{"amount": -0.06, "category": -5, "description": "SHORT CFD INTEREST FOR 06-JUL-2022"}]} 37 | ], 38 | "transfers": [] 39 | } -------------------------------------------------------------------------------- /tests/test_data/ibkr_corp_actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1672531200, 1704067199], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321", "currency": 1, "precision": 10, "cash_begin": 12.345, "cash_end": 1234567.249966388, "cash_end_settled": 1234567.89}], 5 | "assets": [ 6 | {"id": 1, "type": "money", "name": "USD"}, 7 | {"id": 2, "type": "stock", "name": "TWO HANDS CORP", "isin": "US90187E5015"}, 8 | {"id": 3, "type": "stock", "name": "TWO HANDS CORP", "isin": "US90187E4026"}, 9 | {"id": 4, "type": "bond", "name": "B 01/25/24", "isin": "US912796ZY88"} 10 | ], 11 | "symbols": [ 12 | {"id": 1, "asset": 1, "symbol": "USD"}, 13 | {"id": 2, "asset": 2, "symbol": "TWOHD", "currency": 1, "note": "PINK"}, 14 | {"id": 3, "asset": 3, "symbol": "TWOHD", "currency": 1, "note": "PINK"}, 15 | {"id": 4, "asset": 4, "symbol": "912796ZY8", "currency": 1} 16 | ], 17 | "assets_data": [ 18 | {"id": 1, "asset": 2, "reg_number": "90187E501"}, 19 | {"id": 2, "asset": 3, "reg_number": "90187E402"}, 20 | {"id": 3, "asset": 4, "principal": 1000, "expiry": 1706140800, "reg_number": "912796ZY8"} 21 | ], 22 | "asset_payments": [], 23 | "corporate_actions": [ 24 | {"id": 1, "type": "split", "account": 1, "timestamp": 1695932700, "number": "25164201871", "asset": 3, "quantity": 35.0, 25 | "outcome": [{"asset": 2, "quantity": 0.035, "share": 1.0}], "description": "TWOH(US90187E4026) SPLIT 1 FOR 1000 (TWOHD, TWO HANDS CORP, US90187E5015)"} 26 | ], 27 | "income_spending": [], 28 | "trades": [ 29 | {"id": 1, "number": "26300000000", "timestamp": 1706127900, "settlement": 1706127900, "account": 1, "asset": 4, "quantity": -5.0, "price": 1000.0, "fee": 0.0, "note": "(US912796ZY88) TBILL MATURITY (912796ZY8, B 01/25/24, US912796ZY88)"} 30 | ], 31 | "transfers": [] 32 | } -------------------------------------------------------------------------------- /tests/test_data/ibkr_merger_complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1609459200, 1640995199], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321", "currency": 1, "precision": 10, "cash_begin": 0.0, "cash_end": -935.800878369, "cash_end_settled": -935.800878369} 5 | ], 6 | "assets": [ 7 | {"id": 1, "type": "money", "name": "USD"}, 8 | {"id": 2, "type": "warrant", "name": "DNA 31DEC27 11.5 C", "isin": "US37611X1182"}, 9 | {"id": 3, "type": "stock", "name": "GINKGO BIOWORKS HOLDINGS INC", "isin": "US37611X1000"}, 10 | {"id": 4, "type": "stock", "name": "SOARING EAGLE ACQUISITION CO", "isin": "KYG8354H1002" } 11 | ], 12 | "symbols": [ 13 | {"id": 1, "asset": 1, "symbol": "USD"}, 14 | {"id": 2, "asset": 2, "currency": 1, "symbol": "DNA WS", "note": "NYSE"}, 15 | {"id": 3, "asset": 3, "currency": 1, "symbol": "DNA", "note": "NYSE"}, 16 | {"id": 4, "asset": 4, "currency": 1, "symbol": "SRNGU"} 17 | ], 18 | "assets_data": [ 19 | {"id": 1, "asset": 2, "expiry": 1830211200}, 20 | {"id": 2, "asset": 3, "reg_number": "37611X100"} 21 | ], 22 | "corporate_actions": [ 23 | {"id": 1, "type": "merger", "account": 1, "timestamp": 1631823900, "number": "17747623336", "asset": 4, "quantity": 111.0, 24 | "outcome": [{"asset": 1, "quantity": 136.53, "share": 0.0}, {"asset": 2, "quantity": 22.0, "share": 0.0}, {"asset": 3, "quantity": 111.0, "share": 0.0}], 25 | "description": "SRNGU(G8354H100) CASH and STOCK MERGER (Acquisition) DNA WS 1 FOR 5, US37611X1000 1 FOR 1 AND USD 1.23 (DNA WS, DNA 01AUG26 11.5 C, US37611X1182)"} 26 | ], 27 | "trades": [ 28 | {"id": 1, "account": 1, "timestamp": 1631008683, "settlement": 1631145600, "number": "4057396877", "asset": 4, "quantity": 61.0, "price": 10.388, "fee": 0.36245725}, 29 | {"id": 2, "account": 1, "timestamp": 1631534345, "settlement": 1631664000, "number": "4072660868", "asset": 4, "quantity": 50.0, "price": 9.86, "fee": 0.36025725}, 30 | {"id": 3, "account": 1, "timestamp": 1632483314, "settlement": 1632787200, "number": "4103255040", "asset": 2, "quantity": -22.0, "price": 3.94, "fee": 0.423717318}, 31 | {"id": 4, "account": 1, "timestamp": 1633533335, "settlement": 1633651200, "number": "4131958719", "asset": 3, "quantity": -60.0, "price": 10.05, "fee": 0.20447255}, 32 | {"id": 5, "account": 1, "timestamp": 1634655039, "settlement": 1634774400, "number": "4160926315", "asset": 3, "quantity": -51.0, "price": 14.35, "fee": 0.309058685} 33 | ], 34 | "income_spending": [], 35 | "asset_payments": [], 36 | "transfers": [] 37 | } -------------------------------------------------------------------------------- /tests/test_data/ibkr_rights.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1609459200, 1640995199], 3 | "accounts": [ 4 | {"id": 1, "number": "U1234567", "currency": 1, "precision": 10, "cash_begin": 0.0, "cash_end": 10659.073152, "cash_end_settled": -81667.430698}, 5 | {"id": 2, "number": "U1234567", "currency": 2, "precision": 10, "cash_begin": 0.0, "cash_end": -935.800878369, "cash_end_settled": -935.800878369} 6 | ], 7 | "assets": [{"id": 1, "name": "HKD", "type": "money"}, 8 | {"id": 2, "name": "USD", "type": "money"}, 9 | {"id": 3, "type": "stock", "name": "TONGDA GROUP HOLDINGS LTD", "isin": "KYG8917X1218"}, 10 | {"id": 5, "type": "stock", "name": "INTERACTIVE BROKERS GRO-CL A", "isin": "US45841N1072" }, 11 | {"id": 6, "type": "stock", "isin": "KYG8917X1RTS"} 12 | ], 13 | "assets_data": [ 14 | {"asset": 5, "id": 1, "reg_number": "45841N107"} 15 | ], 16 | "symbols": [{"id": 1, "asset": 1, "symbol": "HKD"}, 17 | {"id": 2, "asset": 2, "symbol": "USD"}, 18 | {"id": 3, "asset": 3, "currency": 1, "symbol": "698", "note": "SEHK"}, 19 | {"id": 5, "asset": 5, "currency": 2, "symbol": "IBKR", "note": "NASDAQ"}, 20 | {"id": 6, "asset": 6, "currency": 1, "symbol": "2965", "note": "CORPACT"} 21 | ], 22 | "asset_payments": [ 23 | ], 24 | "corporate_actions": [ 25 | ], 26 | "income_spending": [], 27 | "trades": [], 28 | "transfers": []} -------------------------------------------------------------------------------- /tests/test_data/ibkr_spinoff.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1609459200, 1640995199], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321", "currency": 1, "precision": 10, "cash_begin": 0.0, "cash_end": -935.800878369, "cash_end_settled": -935.800878369} 5 | ], 6 | "assets": [ 7 | {"id": 1, "type": "money", "name": "USD"}, 8 | {"id": 2, "type": "stock", "name": "GENERAL ELECTRIC CO", "isin": "US3696041033"}, 9 | {"id": 3, "type": "stock", "name": "WABTEC CORP"} 10 | ], 11 | "symbols": [ 12 | {"id": 1, "asset": 1, "symbol": "USD"}, 13 | {"id": 2, "asset": 2, "currency": 1, "symbol": "GE", "note": "NYSE"}, 14 | {"id": 3, "asset": 3, "currency": 1, "symbol": "WAB", "note": "NYSE"} 15 | ], 16 | "assets_data": [ 17 | {"id": 1, "asset": 2, "reg_number": "369604103"}, 18 | {"id": 2, "asset": 3, "reg_number": "929740108"} 19 | ], 20 | "corporate_actions": [ 21 | {"id": 1, "type": "spin-off", "account": 1, "timestamp": 1637353500, "number": "10302900848", "asset": 2, "quantity": 100, 22 | "outcome": [{"asset": 2, "quantity": 100, "share": 0.0}, {"asset": 3, "quantity": 0.5371, "share": 0.0}], 23 | "description": "GE(US3696041033) SPINOFF 5371 FOR 1000000 (WAB, WABTEC CORP, 929740108)"} 24 | ], 25 | "trades": [ 26 | {"id": 1, "account": 1, "timestamp": 1635760683, "settlement": 1635897600, "number": "11111", "asset": 2, "quantity": 100.0, "price": 8.325, "fee": 0.35}, 27 | {"id": 2, "account": 1, "timestamp": 1637679039, "settlement": 1637798400, "number": "22222", "asset": 3, "quantity": -0.5371, "price": 1.2, "fee": 0.0}, 28 | {"id": 3, "account": 1, "timestamp": 1638370239, "settlement": 1638489600, "number": "33333", "asset": 2, "quantity": -100.0, "price": 7.95, "fee": 0.3} 29 | ], 30 | "income_spending": [], 31 | "asset_payments": [], 32 | "transfers": [] 33 | } -------------------------------------------------------------------------------- /tests/test_data/ibkr_warrants.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1577836800, 1609459199], 3 | "accounts": [ 4 | {"id": 1, "number": "U7654321", "currency": 1, "precision": 10, "cash_begin": 12.345, "cash_end": 1234567.249966388, "cash_end_settled": 1234567.89} 5 | ], 6 | "assets": [ 7 | {"id": 1, "name": "USD", "type": "money"}, 8 | {"id": 2, "type": "warrant", "name": "DNA 31DEC27 11.5 C", "isin": "US37611X1182"}, 9 | {"id": 3, "type": "stock", "name": "GINKGO BIOWORKS HOLDINGS INC", "isin": "US37611X1000"}, 10 | {"id": 4, "type": "stock", "name": "GINKGO BIOWORKS HOLDINGS INC", "isin": "KYG8354H1002"} 11 | ], 12 | "symbols": [ 13 | {"id": 1, "asset": 1, "symbol": "USD"}, 14 | {"id": 2, "asset": 2, "currency": 1, "symbol": "DNA WS", "note": "NYSE"}, 15 | {"id": 3, "asset": 3, "currency": 1, "symbol": "DNA", "note": "NYSE"}, 16 | {"id": 4, "asset": 4, "currency": 1, "symbol": "SRNGU"} 17 | ], 18 | "assets_data": [ 19 | {"id": 1, "asset": 2, "expiry": 1830211200}, 20 | {"id": 2, "asset": 3, "reg_number": "37611X100"} 21 | ], 22 | "trades": [], 23 | "corporate_actions": [ 24 | {"id": 1, "type": "merger", "account": 1, "timestamp": 1631823900, "number": "17747623336", "asset": 4, "quantity": 111.0, 25 | "outcome": [{"asset": 2, "quantity": 22.0, "share": 0.0}, {"asset": 3, "quantity": 111.0, "share": 0.0}], 26 | "description": "SRNGU(G8354H100) MERGED(Acquisition) WITH DNA WS 1 FOR 5, US37611X1000 1 FOR 1 (DNA WS, DNA 01AUG26 11.5 C, US37611X1182)"} 27 | ], 28 | "asset_payments": [], 29 | "income_spending": [], 30 | "transfers": [] 31 | } -------------------------------------------------------------------------------- /tests/test_data/invalid_backup.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/invalid_backup.tgz -------------------------------------------------------------------------------- /tests/test_data/j2t.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/j2t.xlsx -------------------------------------------------------------------------------- /tests/test_data/kit.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1617235200, 1621555199], 3 | "accounts": [ 4 | {"id": 1, "currency": 1, "number": "12345"}, 5 | {"id": 2, "currency": 2, "number": "12345"} 6 | ], 7 | "assets": [ 8 | {"id": 1, "type": "money", "name": "RUB"}, 9 | {"id": 2, "type": "money", "name": "USD"}, 10 | {"id": 3, "type": "stock", "name": "Аэрофлот-росс.авиалин(ПАО)ао", "isin": "RU0009062285"}, 11 | {"id": 4, "type": "etf", "name": "FinEx Gold ETF USD", "isin": "IE00B8XB7377"}, 12 | {"id": 5, "type": "bond", "name": "АО \"Тинькофф Банк\" БО-07", "isin": "RU000A0JWM31"}, 13 | {"id": 6, "type": "stock", "name": "Polymetal International plc", "isin": "JE00B6T5S470"} 14 | ], 15 | "symbols": [ 16 | {"id": 1, "asset": 1, "symbol": "RUB"}, 17 | {"id": 2, "asset": 2, "symbol": "USD"}, 18 | {"id": 3, "asset": 3, "symbol": "AFLT", "currency": 1, "note": "MOEX"}, 19 | {"id": 4, "asset": 4, "symbol": "FXGD", "currency": 1, "note": "MOEX"}, 20 | {"id": 5, "asset": 5, "symbol": "ТинькоффБ7", "currency": 1, "note": "MOEX"}, 21 | {"id": 6, "asset": 6, "symbol": "POLY", "currency": 1, "note": "MOEX"} 22 | ], 23 | "assets_data": [ 24 | {"id": 1, "asset": 3, "reg_number": "1-01-00010-A", "principal": 1.0}, 25 | {"id": 2, "asset": 5, "reg_number": "4B020702673B", "principal": 1000.0, "expiry": 1624492800}, 26 | {"id": 3, "asset": 6, "principal": 0.03} 27 | ], 28 | "trades": [ 29 | {"id": 1, "number": "3312348787", "timestamp": 1615647620, "settlement": 1615766400, "account": 1, "asset": 4, "quantity": 10, "price": 987.65, "fee": 6.9}, 30 | {"id": 2, "number": "3720000652", "timestamp": 1615909088, "settlement": 1615939200, "account": 1, "asset": 5, "quantity": 1, "price": 1005.7, "fee": 0.5}, 31 | {"id": 3, "number": "3725000743", "timestamp": 1615976923, "settlement": 1616025600, "account": 1, "asset": 5, "quantity": -1, "price": 1004.1, "fee": 0.5}, 32 | {"id": 4, "number": "3720123978", "timestamp": 1616770797, "settlement": 1616889600, "account": 1, "asset": 6, "quantity": 10, "price": 1532.1, "fee": 8.66} 33 | ], 34 | "income_spending": [ 35 | {"id": 1, "timestamp": 1618617600, "account": 1, "peer": 0, "lines": [{"amount": 43.21, "category": -8, "description": "Проценты по договору займа ЦБ ДЗ/16042021-1"}]}, 36 | {"id": 2, "timestamp": 1622332800, "account": 1, "peer": 0, "lines": [{"amount": -17.6, "category": -5, "description": "Счет НКО АО НРД за хранение ЦБ №H000/02923468 от 28.05.2021 г."}]}, 37 | {"id": 3, "timestamp": 1617148800, "account": 1, "peer": 0, "lines": [{"amount": -200, "category": -5, "description": "Абонентская плата за обслуживание счета 11249"}]} 38 | ], 39 | "transfers": [ 40 | {"id": 1, "account": [0, 1, 0], "asset": [1, 1], "timestamp": 1615766400, "withdrawal": 123, "deposit": 123, "fee": 0.0, "description": "зачисление - неторговое поручение от 15.03.2021, НП/в/А/150321/74123"} 41 | ], 42 | "corporate_actions": [], 43 | "asset_payments": [ 44 | {"id": 1, "type": "interest", "account": 1, "timestamp": 1615909088, "number": "3720000652", "asset": 5, "amount": -16.83, "description": "НКД"}, 45 | {"id": 2, "type": "interest", "account": 1, "timestamp": 1615976923, "number": "3725000743", "asset": 5, "amount": 17.03, "description": "НКД"} 46 | ] 47 | } -------------------------------------------------------------------------------- /tests/test_data/kit.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/kit.xlsx -------------------------------------------------------------------------------- /tests/test_data/pof_converted.json: -------------------------------------------------------------------------------- 1 | { 2 | "period": [1528528500, 1656742002], 3 | "accounts": [ 4 | {"id": 10, "currency": 0, "type": "investment", "number": "Imported #1"}, 5 | {"id": 11, "currency": 69, "number": "Imported #1"}, 6 | {"id": 12, "currency": 1, "number": "Imported #1"} 7 | ], 8 | "assets": [ 9 | {"id": 0, "type": "money", "name": "Рубль"}, 10 | {"id": 1, "type": "money", "name": "Доллар"}, 11 | {"id": 44, "type": "stock", "name": "Сбербанк", "isin": "RU0009029540"}, 12 | {"id": 45, "type": "stock", "name": "ГАЗПРОМ ао", "isin": "RU0007661625"}, 13 | {"id": 46, "type": "bond", "name": "ВЭБ ПБО1Р9", "isin": "RU000A0JXU71"}, 14 | {"id": 52, "type": "stock", "name": "Ross Stores Inc", "isin": "US7782961038"}, 15 | {"id": 14, "type": "commodity", "name": "GLDRUB_TOM"}, 16 | {"id": 69, "type": "money", "name": "HKD"} 17 | ], 18 | "symbols": [ 19 | {"id": 1, "asset": 0, "note": "", "symbol": "RUB"}, 20 | {"id": 2, "asset": 1, "note": "", "symbol": "USD"}, 21 | {"id": 3, "asset": 44, "note": "MOEX", "symbol": "SBER", "currency": 0}, 22 | {"id": 4, "asset": 45, "note": "MOEX", "symbol": "GAZP", "currency": 0}, 23 | {"id": 5, "asset": 46, "note": "MOEX", "symbol": "RU000A0JXU71", "currency": 0}, 24 | {"id": 6, "asset": 52, "note": "SPBEX", "symbol": "ROST", "currency": 0}, 25 | {"id": 7, "asset": 14, "note": "MOEX", "symbol": "GLDRUB_TOM", "currency": 0}, 26 | {"id": 10, "asset": 69, "symbol": "HKD"} 27 | ], 28 | "assets_data": [ 29 | 30 | ], 31 | "trades": [ 32 | {"id": 1, "number": "1313074529", "timestamp": 1539637200, "settlement": 0, "account": 10, "asset": 45, "quantity": 150, "price": 167.58000183105, "fee": 0,"note": "\"Газпром\" (ПАО) ао"}, 33 | {"id": 2, "number": "3407729597", "timestamp": 1582578000, "settlement": 0, "account": 10, "asset": 44, "quantity": -50, "price": 245, "fee": 0, "note": "Сбербанк России ПАО ао"}, 34 | {"id": 3, "number": "465e9b6f2c683de80b4d06480f49b1f1", "timestamp": 1593723600, "settlement": 0, "account": 10, "asset": 44, "quantity": 30, "price": 210.85, "fee": 0, "note": "Сбербанк России ПАО ао"}, 35 | {"id": 4, "number": "38159322496", "timestamp": 1722866873, "settlement": 0, "account": 10, "asset": 14, "quantity": 1, "price": 6629.6, "fee": 59.67, "note": "GLD/RUB_TOM - GLD/РУБ"} 36 | ], 37 | "income_spending": [ 38 | {"id": 1, "timestamp": 1548104400, "account": 10, "peer": 0, "lines": [{"amount": -149, "category": -5, "description": "Списание комиссий"}]}, 39 | {"id": 2, "timestamp": 1562014800, "account": 10, "peer": 0, "lines": [{"amount": 13000, "category": -1, "description": "Налоговый вычет"}]} 40 | ], 41 | "transfers": [ 42 | {"id": 1, "account": [10, 11, 10], "number": "PP230319000000069703", "asset": [0, 69], "timestamp": 1723219200, "withdrawal": 1692.18, "deposit": 152, "fee": 0, "description": "HKD/RUB_TOM - HKD/РУБ"}, 43 | {"id": 2, "account": [10, 12, 10], "number": "33918678504", "asset": [0, 1], "timestamp": 1698306811, "withdrawal": 4670.5, "deposit": 50, "fee": 18.68, "description": "USDRUB_TOM - USD/РУБ"} 44 | ] 45 | } -------------------------------------------------------------------------------- /tests/test_data/taxes_bond_rus.json: -------------------------------------------------------------------------------- 1 | { 2 | "Акции":[ 3 | 4 | ], 5 | "Дивиденды":[ 6 | 7 | ], 8 | "Комиссии":[ 9 | 10 | ], 11 | "Криптовалюты":[ 12 | 13 | ], 14 | "Облигации":[ 15 | { 16 | "c_amount":2062.5, 17 | "c_amount_rub":149994.28, 18 | "c_date":1632515100, 19 | "c_fee":0.0, 20 | "c_fee_rub":0.0, 21 | "c_int":0.0, 22 | "c_int_rub":0.0, 23 | "c_isin":"US912CALAN84", 24 | "c_number":"17849517124", 25 | "c_price":103.125, 26 | "c_qty":2.0, 27 | "c_rate":72.7245, 28 | "c_symbol":"X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26", 29 | "c_type":"Продажа", 30 | "country_iso":"840", 31 | "cs_date":1632515100, 32 | "cs_rate":72.7245, 33 | "income":2062.5, 34 | "income_rub":149994.28, 35 | "o_amount":1274.18, 36 | "o_amount_rub":93618.72, 37 | "o_date":1622131065, 38 | "o_fee":2.0, 39 | "o_fee_rub":146.95, 40 | "o_int":25.69, 41 | "o_int_rub":1887.54, 42 | "o_isin":"US912909AN84", 43 | "o_number":"2882737839", 44 | "o_price":63.709, 45 | "o_qty":2.0, 46 | "o_rate":73.4737, 47 | "o_symbol":"X 6 1/4 03/15/26", 48 | "o_type":"Покупка", 49 | "os_date":1622246400, 50 | "os_rate":73.4737, 51 | "principal":1000.0, 52 | "profit":760.63, 53 | "profit_rub":54341.07, 54 | "report_template":"bond_trade", 55 | "spending_rub":95653.21 56 | }, 57 | { 58 | "country_iso":"840", 59 | "empty":"", 60 | "income_rub":4508.46, 61 | "interest":62.0, 62 | "interest_rub":4508.46, 63 | "isin":"US912909AN84", 64 | "number":"", 65 | "o_date":1631664000, 66 | "profit":62.0, 67 | "profit_rub":4508.46, 68 | "rate":72.7171, 69 | "report_template":"bond_interest", 70 | "spending_rub":0.0, 71 | "symbol":"X 6 1/4 03/15/26", 72 | "type":"Купон" 73 | }, 74 | { 75 | "country_iso":"840", 76 | "empty":"", 77 | "income_rub":36.36, 78 | "interest":0.5, 79 | "interest_rub":36.36, 80 | "isin":"US912909AN84", 81 | "number":"TEST_ID", 82 | "o_date":1631664000, 83 | "profit":0.5, 84 | "profit_rub":36.36, 85 | "rate":72.7171, 86 | "report_template":"bond_interest", 87 | "spending_rub":0.0, 88 | "symbol":"X 6 1/4 03/15/26", 89 | "type":"Купон" 90 | }, 91 | { 92 | "country_iso":"840", 93 | "empty":"", 94 | "income_rub":278.89, 95 | "interest":3.82, 96 | "interest_rub":278.89, 97 | "isin":"US912CALAN84", 98 | "number":"", 99 | "o_date":1632700800, 100 | "profit":3.82, 101 | "profit_rub":278.89, 102 | "rate":73.0081, 103 | "report_template":"bond_interest", 104 | "spending_rub":0.0, 105 | "symbol":"X 6 1/4 03/15/26 - PARTIAL CALL RED DATE 9/26", 106 | "type":"Купон" 107 | }, 108 | { 109 | "income_rub":154817.99, 110 | "profit":826.95, 111 | "profit_rub":59164.78, 112 | "report_template":"totals", 113 | "spending_rub":95653.21 114 | } 115 | ], 116 | "ПФИ":[ 117 | 118 | ], 119 | "Проценты":[ 120 | 121 | ] 122 | } -------------------------------------------------------------------------------- /tests/test_data/taxes_cfd_rus.json: -------------------------------------------------------------------------------- 1 | { 2 | "Акции":[ 3 | 4 | ], 5 | "Дивиденды":[ 6 | 7 | ], 8 | "Комиссии":[ 9 | { 10 | "amount":0.28, 11 | "amount_rub":17.62, 12 | "note":"LONG CFD INTEREST FOR 07-JUL-2022", 13 | "payment_date":1657152000, 14 | "rate":62.911, 15 | "report_template":"fee" 16 | }, 17 | { 18 | "amount":0.02, 19 | "amount_rub":1.17, 20 | "note":"CFD BORROW FEE FOR MU for 06-JUL-2022", 21 | "payment_date":1657065600, 22 | "rate":58.5118, 23 | "report_template":"fee" 24 | }, 25 | { 26 | "amount":0.06, 27 | "amount_rub":3.51, 28 | "note":"SHORT CFD INTEREST FOR 06-JUL-2022", 29 | "payment_date":1657065600, 30 | "rate":58.5118, 31 | "report_template":"fee" 32 | }, 33 | { 34 | "amount":0.36, 35 | "amount_rub":22.3, 36 | "report_template":"totals" 37 | } 38 | ], 39 | "Криптовалюты":[ 40 | 41 | ], 42 | "Облигации":[ 43 | 44 | ], 45 | "ПФИ":[ 46 | { 47 | "c_amount":2147.3, 48 | "c_amount_rub":131557.03, 49 | "c_date":1657186655, 50 | "c_fee":0.9736842105263158, 51 | "c_fee_rub":61.26, 52 | "c_isin":"", 53 | "c_number":"211380318", 54 | "c_price":58.035, 55 | "c_qty":-37.0, 56 | "c_rate":62.911, 57 | "c_symbol":"MUn", 58 | "c_type":"Покупка", 59 | "cost_basis":100.0, 60 | "country_iso":"000", 61 | "cs_date":1657497600, 62 | "cs_rate":61.2664, 63 | "income":2222.74, 64 | "income_rub":140349.68, 65 | "note":"Удержан дивиденд: 252.51 RUB (4.37 USD)\n", 66 | "o_amount":2222.74, 67 | "o_amount_rub":140349.68, 68 | "o_date":1657106512, 69 | "o_fee":1.0, 70 | "o_fee_rub":58.51, 71 | "o_isin":"", 72 | "o_number":"211321719", 73 | "o_price":60.074, 74 | "o_qty":-37.0, 75 | "o_rate":58.5118, 76 | "o_symbol":"MUn", 77 | "o_type":"Продажа", 78 | "os_date":1657238400, 79 | "os_rate":63.1427, 80 | "profit":69.1, 81 | "profit_rub":8420.372223, 82 | "report_template":"trade", 83 | "spending_rub":131929.307777 84 | }, 85 | { 86 | "c_amount":58.04, 87 | "c_amount_rub":3555.6, 88 | "c_date":1657186655, 89 | "c_fee":0.02631578947368421, 90 | "c_fee_rub":1.66, 91 | "c_isin":"", 92 | "c_number":"211380318", 93 | "c_price":58.035, 94 | "c_qty":-1.0, 95 | "c_rate":62.911, 96 | "c_symbol":"MUn", 97 | "c_type":"Покупка", 98 | "cost_basis":100.0, 99 | "country_iso":"000", 100 | "cs_date":1657497600, 101 | "cs_rate":61.2664, 102 | "income":59.5, 103 | "income_rub":3756.99, 104 | "note":"", 105 | "o_amount":59.5, 106 | "o_amount_rub":3756.99, 107 | "o_date":1657110438, 108 | "o_fee":1.0, 109 | "o_fee_rub":58.51, 110 | "o_isin":"", 111 | "o_number":"211326559", 112 | "o_price":59.5, 113 | "o_qty":-1.0, 114 | "o_rate":58.5118, 115 | "o_symbol":"MUn", 116 | "o_type":"Продажа", 117 | "os_date":1657238400, 118 | "os_rate":63.1427, 119 | "profit":0.43, 120 | "profit_rub":141.22, 121 | "report_template":"trade", 122 | "spending_rub":3615.77 123 | }, 124 | { 125 | "income_rub":144106.67, 126 | "profit":69.53, 127 | "profit_rub":8561.592223, 128 | "report_template":"totals", 129 | "spending_rub":135545.077777 130 | } 131 | ], 132 | "Проценты":[ 133 | 134 | ] 135 | } -------------------------------------------------------------------------------- /tests/test_data/taxes_flow.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "report_template": "account_lines", 4 | "account": "U7654321", 5 | "currency": "USD (840)", 6 | "money": "Денежные средства", 7 | "account_name": "Inv. Account", 8 | "assets": "Финансовые активы", 9 | "money_begin": -0.278304, 10 | "money_in": 3.336481, 11 | "money_out": 2.532967000000000098225427791, 12 | "money_end": 0.52521, 13 | "assets_begin": 0.0, 14 | "assets_in": 2.719063000000000101863406599, 15 | "assets_out": 2.779543000000000120053300634, 16 | "assets_end": 0.1068 17 | } 18 | ] -------------------------------------------------------------------------------- /tests/test_data/taxes_merger_spinoff_rus.json: -------------------------------------------------------------------------------- 1 | { 2 | "Акции":[ 3 | { 4 | "c_amount":3.12, 5 | "c_amount_rub":198.25, 6 | "c_date":1652905500, 7 | "c_fee":0.0, 8 | "c_fee_rub":0.0, 9 | "c_isin":"US87167T2015", 10 | "c_number":"", 11 | "c_price":6.24, 12 | "c_qty":0.5, 13 | "c_rate":63.5428, 14 | "c_symbol":"SNPXD", 15 | "c_type":"Продажа", 16 | "cost_basis":0.0, 17 | "country_iso":"840", 18 | "cs_date":1652905500, 19 | "cs_rate":63.5428, 20 | "income":3.12, 21 | "income_rub":198.25, 22 | "note":"Spin-off: NTRP(US64129T2078) SPINOFF 1 FOR 5 (SNPX, SYNAPTOGENIX INC, US87167T1025)\nSYNAPTOGENIX INC (0.0 %)\nMerger: NTRP(US64129T2078) MERGED(Acquisition) WITH US71678J1007 1 FOR 5 (PTPI, PETROS PHARMACEUTICALS INC, US71678J1007)\nPETROS PHARMACEUTICALS INC\nSplit: SNPX(US87167T1025) SPLIT 1 FOR 4 (SNPXD, SYNAPTOGENIX INC, US87167T2015)\nSYNAPTOGENIX INC\nSplit: PTPI(US71678J1007) SPLIT 1 FOR 10 (PTPI, PETROS PHARMACEUTICALS INC, US71678J2096)\nPETROS PHARMACEUTICALS INC\n", 23 | "o_amount":47.4, 24 | "o_amount_rub":3481.16, 25 | "o_date":1630844624, 26 | "o_fee":0.1, 27 | "o_fee_rub":7.29, 28 | "o_isin":"US64129T2078", 29 | "o_number":"2543065342", 30 | "o_price":4.74, 31 | "o_qty":10.0, 32 | "o_rate":72.8545, 33 | "o_symbol":"NTRP", 34 | "o_type":"Покупка", 35 | "os_date":1631145600, 36 | "os_rate":73.4421, 37 | "profit":3.12, 38 | "profit_rub":198.25, 39 | "report_template":"corporate_action", 40 | "spending_rub":0.0 41 | }, 42 | { 43 | "c_amount":0.82, 44 | "c_amount_rub":50.22, 45 | "c_date":1669839900, 46 | "c_fee":0.0, 47 | "c_fee_rub":0.0, 48 | "c_isin":"US71678J2096", 49 | "c_number":"", 50 | "c_price":4.111, 51 | "c_qty":0.2, 52 | "c_rate":61.0742, 53 | "c_symbol":"PTPI", 54 | "c_type":"Продажа", 55 | "cost_basis":100.0, 56 | "country_iso":"840", 57 | "cs_date":1669839900, 58 | "cs_rate":61.0742, 59 | "income":0.82, 60 | "income_rub":50.22, 61 | "note":"Spin-off: NTRP(US64129T2078) SPINOFF 1 FOR 5 (SNPX, SYNAPTOGENIX INC, US87167T1025)\nSYNAPTOGENIX INC (0.0 %)\nMerger: NTRP(US64129T2078) MERGED(Acquisition) WITH US71678J1007 1 FOR 5 (PTPI, PETROS PHARMACEUTICALS INC, US71678J1007)\nPETROS PHARMACEUTICALS INC\nSplit: SNPX(US87167T1025) SPLIT 1 FOR 4 (SNPXD, SYNAPTOGENIX INC, US87167T2015)\nSYNAPTOGENIX INC\nSplit: PTPI(US71678J1007) SPLIT 1 FOR 10 (PTPI, PETROS PHARMACEUTICALS INC, US71678J2096)\nPETROS PHARMACEUTICALS INC\n", 62 | "o_amount":47.4, 63 | "o_amount_rub":3481.16, 64 | "o_date":1630844624, 65 | "o_fee":0.1, 66 | "o_fee_rub":7.29, 67 | "o_isin":"US64129T2078", 68 | "o_number":"2543065342", 69 | "o_price":4.74, 70 | "o_qty":10.0, 71 | "o_rate":72.8545, 72 | "o_symbol":"NTRP", 73 | "o_type":"Покупка", 74 | "os_date":1631145600, 75 | "os_rate":73.4421, 76 | "profit":-46.68, 77 | "profit_rub":-3438.23, 78 | "report_template":"corporate_action", 79 | "spending_rub":3488.45 80 | }, 81 | { 82 | "income_rub":248.47, 83 | "profit":-43.56, 84 | "profit_rub":-3239.98, 85 | "report_template":"totals", 86 | "spending_rub":3488.45 87 | } 88 | ], 89 | "Дивиденды":[ 90 | 91 | ], 92 | "Комиссии":[ 93 | 94 | ], 95 | "Криптовалюты":[ 96 | 97 | ], 98 | "Облигации":[ 99 | 100 | ], 101 | "ПФИ":[ 102 | 103 | ], 104 | "Проценты":[ 105 | 106 | ] 107 | } -------------------------------------------------------------------------------- /tests/test_data/taxes_spinoff_rus.json: -------------------------------------------------------------------------------- 1 | { 2 | "Акции": [ 3 | { 4 | "c_isin": "", 5 | "c_symbol": "WAB", 6 | "c_qty": 0.5371, 7 | "c_amount": 0.64, 8 | "c_amount_rub": 46.87, 9 | "c_date": 1637679039, 10 | "c_fee": 0.0, 11 | "c_fee_rub": 0.0, 12 | "c_number": "22222", 13 | "c_price": 1.2, 14 | "c_rate": 72.76, 15 | "c_type": "Продажа", 16 | "cost_basis": 10.0, 17 | "country_iso": "840", 18 | "cs_date": 1637798400, 19 | "cs_rate": 72.7171, 20 | "income": 0.64, 21 | "income_rub": 46.87, 22 | "note": "Spin-off: GE(US3696041033) SPINOFF 5371 FOR 1000000 (WAB, WABTEC CORP, 929740108)\nWABTEC CORP (10.0 %)\n", 23 | "o_isin": "US3696041033", 24 | "o_symbol": "GE", 25 | "o_qty": 100.0, 26 | "o_amount": 832.50, 27 | "o_amount_rub": 61140.55, 28 | "o_date": 1635760683, 29 | "o_fee": 0.35, 30 | "o_fee_rub": 25.53, 31 | "o_number": "11111", 32 | "o_price": 8.325, 33 | "o_rate": 72.9538, 34 | "o_type": "Покупка", 35 | "os_date": 1635897600, 36 | "os_rate": 73.4421, 37 | "profit": -82.65, 38 | "profit_rub": -6069.73, 39 | "report_template": "corporate_action", 40 | "spending_rub": 6116.6 41 | }, 42 | { 43 | "c_isin": "US3696041033", 44 | "c_symbol": "GE", 45 | "c_qty": 100.0, 46 | "c_amount": 795.0, 47 | "c_amount_rub": 57765.73, 48 | "c_date": 1638370239, 49 | "c_fee": 0.3, 50 | "c_fee_rub": 21.82, 51 | "c_number": "33333", 52 | "c_price": 7.95, 53 | "c_rate": 72.7245, 54 | "c_type": "Продажа", 55 | "cost_basis": 90.0, 56 | "country_iso": "840", 57 | "cs_date": 1638489600, 58 | "cs_rate": 72.6613, 59 | "income": 795.0, 60 | "income_rub": 57765.73, 61 | "note": "Spin-off: GE(US3696041033) SPINOFF 5371 FOR 1000000 (WAB, WABTEC CORP, 929740108)\nWABTEC CORP (10.0 %)\n", 62 | "o_symbol": "GE", 63 | "o_isin": "US3696041033", 64 | "o_qty": 100.0, 65 | "o_amount": 832.5, 66 | "o_amount_rub": 61140.55, 67 | "o_date": 1635760683, 68 | "o_fee": 0.35, 69 | "o_fee_rub": 25.53, 70 | "o_number": "11111", 71 | "o_price": 8.325, 72 | "o_rate": 72.9538, 73 | "o_type": "Покупка", 74 | "os_date": 1635897600, 75 | "os_rate": 73.4421, 76 | "profit": 45.13, 77 | "profit_rub": 2694.44, 78 | "report_template": "corporate_action", 79 | "spending_rub": 55071.29 80 | }, 81 | { 82 | "income_rub": 57812.6, 83 | "profit": -37.52, 84 | "profit_rub": -3375.29, 85 | "report_template": "totals", 86 | "spending_rub": 61187.89 87 | } 88 | ], 89 | "Дивиденды": [], 90 | "Комиссии": [], 91 | "Криптовалюты": [], 92 | "Облигации": [], 93 | "ПФИ": [], 94 | "Проценты": [] 95 | } -------------------------------------------------------------------------------- /tests/test_data/taxes_vesting_rus.json: -------------------------------------------------------------------------------- 1 | { 2 | "Акции": [ 3 | { 4 | "c_isin": "US92556V1061", 5 | "c_symbol": "VTRS", 6 | "c_qty": 11.0, 7 | "cost_basis": 100.0, 8 | "c_amount": 298.54, 9 | "c_amount_rub": 21950.63, 10 | "c_date": 1632528000, 11 | "c_fee": 1.0, 12 | "c_fee_rub": 73.4, 13 | "c_number": "", 14 | "c_price": 27.14, 15 | "c_rate": 73.3963, 16 | "c_type": "Продажа", 17 | "country_iso": "840", 18 | "cs_date": 1632700800, 19 | "cs_rate": 73.5266, 20 | "income": 298.54, 21 | "income_rub": 21950.63, 22 | "o_isin": "US92556V1061", 23 | "o_symbol": "VTRS", 24 | "o_qty": 11.0, 25 | "o_amount": 247.83, 26 | "o_amount_rub": 18235.41, 27 | "o_date": 1621641600, 28 | "o_fee": 0.0, 29 | "o_fee_rub": 0.0, 30 | "o_number": "", 31 | "o_price": 22.53, 32 | "o_rate": 73.5803, 33 | "o_type": "Покупка", 34 | "os_date": 1621641600, 35 | "os_rate": 73.5803, 36 | "profit": 49.71, 37 | "profit_rub": 3641.82, 38 | "report_template": "trade", 39 | "note": "Stock vesting\n\n", 40 | "spending_rub": 18308.81 41 | }, 42 | { 43 | "income_rub": 21950.63, 44 | "profit": 49.71, 45 | "profit_rub": 3641.82, 46 | "report_template": "totals", 47 | "spending_rub": 18308.81 48 | } 49 | ], 50 | "Дивиденды": [ 51 | { 52 | "amount": 247.83, 53 | "amount_rub": 18235.41, 54 | "country": "н/д", 55 | "country_iso": "000", 56 | "full_name": "VIATRIS INC", 57 | "isin": "US92556V1061", 58 | "note": "Доход получен в натуральной форме (ценными бумагами)", 59 | "payment_date": 1621641600, 60 | "rate": 73.5803, 61 | "report_template": "dividend", 62 | "symbol": "VTRS", 63 | "tax": 0.0, 64 | "tax2pay": 2370.60, 65 | "tax_rub": 0.0, 66 | "tax_treaty": "Нет" 67 | }, 68 | { 69 | "amount": 247.83, 70 | "amount_rub": 18235.41, 71 | "report_template": "totals", 72 | "tax": 0.0, 73 | "tax2pay": 2370.60, 74 | "tax_rub": 0.0 75 | } 76 | ], 77 | "Комиссии": [], 78 | "Криптовалюты": [], 79 | "Облигации": [], 80 | "ПФИ": [], 81 | "Проценты": [] 82 | } -------------------------------------------------------------------------------- /tests/test_data/tvoy.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/tvoy.zip -------------------------------------------------------------------------------- /tests/test_data/vtb.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/titov-vv/jal/a3469c00f341083c7abe3114beceb7bd83b7dbb8/tests/test_data/vtb.xls -------------------------------------------------------------------------------- /tests/test_dlsg.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import filecmp 4 | from jal.data_export.dlsg import DLSG 5 | from tests.fixtures import project_root, data_path 6 | 7 | 8 | def test_empty_dlsg(tmp_path, project_root, data_path): 9 | test_tax_files_empty = { 10 | 2020: "3ndfl_2020_empty.dc0", 11 | 2021: "3ndfl_2021_empty.dc1", 12 | 2022: "3ndfl_2022_empty.dc2" 13 | } 14 | 15 | for year in test_tax_files_empty: 16 | tax_form = DLSG(year) 17 | assert tax_form._year == year 18 | 19 | # Validate saving of empty tax form file 20 | tax_form.save(str(tmp_path) + os.sep + test_tax_files_empty[year]) 21 | assert filecmp.cmp(data_path + test_tax_files_empty[year], str(tmp_path) + os.sep + test_tax_files_empty[year]) 22 | os.remove(str(tmp_path) + os.sep + test_tax_files_empty[year]) 23 | 24 | 25 | def test_full_dlsg(tmp_path, project_root, data_path): 26 | test_tax_files_full = { 27 | 2020: "3ndfl_2020.dc0", 28 | 2021: "3ndfl_2021.dc1", 29 | 2022: "3ndfl_2022.dc2" 30 | } 31 | 32 | for year in test_tax_files_full: 33 | tax_form = DLSG(year) 34 | assert tax_form._year == year 35 | 36 | with open(data_path + 'taxes_rus.json', 'r', encoding='utf-8') as json_file: 37 | tax_report = json.load(json_file) 38 | tax_form.update_taxes(tax_report, {"currency": "USD", "broker_name": "IBKR", "broker_iso_country": "840"}) 39 | 40 | # Validate saving of tax form file with data from tax report 41 | tax_form.save(str(tmp_path) + os.sep + test_tax_files_full[year]) 42 | assert filecmp.cmp(data_path + test_tax_files_full[year], str(tmp_path) + os.sep + test_tax_files_full[year]) 43 | os.remove(str(tmp_path) + os.sep + test_tax_files_full[year]) 44 | --------------------------------------------------------------------------------