monopyly 1.4.7__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {monopyly-1.4.7 → monopyly-1.5.0}/.gitignore +1 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/PKG-INFO +3 -2
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/CHANGELOG.md +15 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/README.md +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/__init__.py +3 -3
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/_version.py +2 -2
- monopyly-1.5.0/monopyly/auth/actions.py +13 -0
- monopyly-1.5.0/monopyly/banking/actions.py +85 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/routes.py +2 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/cli/apps.py +26 -24
- monopyly-1.4.7/monopyly/cli/run.py → monopyly-1.5.0/monopyly/cli/launch.py +19 -8
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/forms/_forms.py +56 -2
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/transactions.py +162 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/actions.py +29 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/forms.py +25 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/routes.py +97 -7
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/transactions/_transactions.py +15 -0
- monopyly-1.5.0/monopyly/credit/transactions/activity/__init__.py +3 -0
- monopyly-1.5.0/monopyly/credit/transactions/activity/data.py +161 -0
- monopyly-1.5.0/monopyly/credit/transactions/activity/parser.py +274 -0
- monopyly-1.5.0/monopyly/credit/transactions/activity/reconciliation.py +456 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/database/models.py +6 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/css/style.css +1146 -263
- monopyly-1.5.0/monopyly/static/img/icons/statement-pair.png +0 -0
- monopyly-1.5.0/monopyly/static/img/icons/statement.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/bind-tag-actions.js +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/create-balance-chart.js +1 -1
- monopyly-1.5.0/monopyly/static/js/create-category-chart.js +27 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/define-filter.js +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/expand-transaction.js +10 -0
- monopyly-1.5.0/monopyly/static/js/highlight-discrepant-transactions.js +124 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/expand-transaction.js +12 -3
- monopyly-1.5.0/monopyly/static/js/modules/form-suggestions.js +60 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/manage-overlays.js +1 -3
- monopyly-1.5.0/monopyly/static/js/show-credit-activity-loader.js +29 -0
- monopyly-1.5.0/monopyly/static/js/toggle-navigation.js +35 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-card-status.js +1 -1
- monopyly-1.5.0/monopyly/static/js/use-suggested-amount.js +11 -0
- monopyly-1.5.0/monopyly/static/js/use-suggested-merchant.js +11 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_page.html +3 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries.html +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/accounts_page.html +11 -15
- monopyly-1.5.0/monopyly/templates/banking/transactions_table/expanded_row_content.html +50 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subtransaction_subform.html +10 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_bank_transaction.html +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_credit_transaction.html +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_condensed.html +2 -2
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_expanded.html +3 -3
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transactions.html +1 -1
- monopyly-1.5.0/monopyly/templates/core/credits.html +34 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/index.html +10 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/profile.html +1 -1
- monopyly-1.5.0/monopyly/templates/core/story.html +62 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/statement_page.html +33 -0
- monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/discrepant_records.html +25 -0
- monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +23 -0
- monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +86 -0
- monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/summary.html +45 -0
- monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/unrecorded_activities.html +24 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/statement_summary.html +2 -2
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/statements.html +1 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form.html +9 -1
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page.html +2 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_update.html +9 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transaction_submission_page.html +8 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/expanded_row_content.html +18 -12
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/layout.html +46 -29
- {monopyly-1.4.7 → monopyly-1.5.0}/pyproject.toml +2 -1
- monopyly-1.4.7/monopyly/auth/actions.py +0 -8
- monopyly-1.4.7/monopyly/banking/actions.py +0 -44
- monopyly-1.4.7/monopyly/static/img/icons/statement-pair.svg +0 -281
- monopyly-1.4.7/monopyly/static/img/icons/statement.svg +0 -294
- monopyly-1.4.7/monopyly/templates/banking/transactions_table/expanded_row_content.html +0 -52
- monopyly-1.4.7/monopyly/templates/core/credits.html +0 -32
- monopyly-1.4.7/monopyly/templates/core/story.html +0 -47
- {monopyly-1.4.7 → monopyly-1.5.0}/COPYING +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/LICENSE +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/README.md +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/auth/blueprint.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/auth/routes.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/auth/tools.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/accounts.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/banks.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/blueprint.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/filters.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/forms.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/banking/transactions.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/forms/__init__.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/forms/fields.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/forms/utils.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/forms/validators.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/common/utils.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/config/__init__.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/config/default_settings.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/config/settings.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/actions.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/blueprint.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/context_processors.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/errors.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/filters.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/internal_transactions.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/core/routes.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/accounts.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/blueprint.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/cards.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/statements.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/credit/transactions/__init__.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/database/__init__.py +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/database/preloads.sql +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/database/schema.sql +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/database/views.sql +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/browserconfig.xml +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-114.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-120.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-144.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-150.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-152.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-16.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-160.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-180.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-192.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-310.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-32.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-57.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-60.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-64.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-70.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-72.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-76.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon-96.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon.ico +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/favicon/favicon.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-details.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-summaries.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/bank-accounts.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/credit-account-details.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/credit-transactions.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/homepage-user.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/homepage.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/about/statement-details.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/cards/chase-card.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/cards/discover-card.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/cards/new-card.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/cards/template-card.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-down.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-left.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-up.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/checkmark.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/delete-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/delete.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/edit.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/link.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/minus-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/minus.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/plus-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/plus.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/refresh.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/save.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/sort-asc.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/sort-desc.png +0 -0
- /monopyly-1.4.7/monopyly/static/img/icons/statement.png → /monopyly-1.5.0/monopyly/static/img/icons/statement-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/icons/x-thick.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/img/statement.png +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/jquery-3.7.0.min.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/add-subtransaction.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/add-transfer.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/autocomplete-transaction.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/display-new-account-type-inputs.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/display-new-bank-inputs.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/display-new-credit-account-inputs.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/expand-bank-account.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/expand-bank.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/flip-card.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/hide-homepage-block.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/infer-card.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/infer-statement.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/make-payment.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/ajax.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/autocomplete-input.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/expand-box-row.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/manage-acquisition-form.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/manage-subforms.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/update-database-widget.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/modules/update-display-ajax.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/show-linked-transaction.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-account-statement-parameters.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-bank-name.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-statement-parameters.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-statements-display.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/static/js/update-transactions-display.js +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/auth/login.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/auth/register.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form_page_new.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/account_summary.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/bank_info_form.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_new.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_update.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transfer_form.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/condensed_row_content.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transaction_field_titles.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transactions.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/form_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subform.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/transaction_form_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_transaction_overlay.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/subtransactions.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/400.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/401.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/403.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/404.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/405.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/408.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/418.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/425.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/500.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/core/errors/error.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/account_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form_page_new.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_form/transfer_statement_inquiry.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_back.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_front.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/card_submission_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/cards.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/cards_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/statements_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/subtag_tree.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/tag_tree.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/tags_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_new.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transactions_page.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/condensed_row_content.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/transaction_field_titles.html +0 -0
- {monopyly-1.4.7 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/transactions.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: monopyly
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: A homemade personal finance manager.
|
|
5
5
|
Project-URL: Download, https://pypi.org/project/monopyly
|
|
6
6
|
Project-URL: Homepage, http://monopyly.com
|
|
@@ -29,6 +29,7 @@ Requires-Dist: flask==3.0.3
|
|
|
29
29
|
Requires-Dist: fuisce==1.0.2
|
|
30
30
|
Requires-Dist: gunicorn==22.0.0
|
|
31
31
|
Requires-Dist: markdown==3.6
|
|
32
|
+
Requires-Dist: nltk==3.8.1
|
|
32
33
|
Requires-Dist: python-dateutil==2.9.0
|
|
33
34
|
Requires-Dist: rich==13.7.1
|
|
34
35
|
Requires-Dist: sqlalchemy==2.0.29
|
|
@@ -58,7 +59,7 @@ To install the app, simply run
|
|
|
58
59
|
$ pip install monopyly
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
The package requires a recent version of Python (3.
|
|
62
|
+
The package requires a recent version of Python (3.10+).
|
|
62
63
|
|
|
63
64
|
|
|
64
65
|
## Getting started
|
|
@@ -184,4 +184,19 @@
|
|
|
184
184
|
- Refresh the table of transactions after making a payment on a credit card statement
|
|
185
185
|
- Use SVG to handle long values in account/statement summary boxes; fixes bugs in page rendering (long value overflow) and hover actions not happening because of conflicting overlap with the sidebar
|
|
186
186
|
|
|
187
|
+
|
|
188
|
+
### 1.4.8
|
|
189
|
+
|
|
190
|
+
- Set username collection to be case insensitive
|
|
191
|
+
- Use Flask/Gunicorn APIs (rather than subprocess CLI calls) to launch the app
|
|
192
|
+
- Fix bug in the ordering of balances in the bank account balance charts for transactions on duplicate dates
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## 1.5.0
|
|
196
|
+
|
|
197
|
+
- Add categorical pie charts to credit card statement details
|
|
198
|
+
- Provide mobile layouts for the application
|
|
199
|
+
- Create a tool for reconciling credit card transactions with information collected from external resources (e.g., CSVs downloaded from a user's online credit card account)
|
|
200
|
+
|
|
201
|
+
|
|
187
202
|
<a name="bottom" id="bottom"></a>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Run
|
|
2
|
+
Run the Monopyly app.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from flask import Flask
|
|
@@ -9,7 +9,7 @@ from monopyly.core.errors import render_error_template
|
|
|
9
9
|
from monopyly.database import SQLAlchemy, register_db_cli_commands
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def create_app(test_config=None):
|
|
12
|
+
def create_app(test_config=None, debug=None):
|
|
13
13
|
# Create and configure the app
|
|
14
14
|
app = Flask(__name__, instance_relative_config=True)
|
|
15
15
|
|
|
@@ -18,7 +18,7 @@ def create_app(test_config=None):
|
|
|
18
18
|
config = test_config
|
|
19
19
|
else:
|
|
20
20
|
# Load the development/production config when not testing
|
|
21
|
-
if app.debug:
|
|
21
|
+
if app.debug or debug:
|
|
22
22
|
config = DevelopmentConfig.configure_for_instance(app.instance_path)
|
|
23
23
|
else:
|
|
24
24
|
config = ProductionConfig.configure_for_instance(app.instance_path)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Module describing logical authorization actions (to be used in routes)."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_username_and_password(form):
|
|
5
|
+
"""
|
|
6
|
+
Get username and password from a form.
|
|
7
|
+
|
|
8
|
+
Get the username and password from the given form. Username should
|
|
9
|
+
be case insensitive.
|
|
10
|
+
"""
|
|
11
|
+
username = form["username"].lower()
|
|
12
|
+
password = form["password"]
|
|
13
|
+
return username, password
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Module describing logical banking actions (to be used in routes)."""
|
|
2
|
+
|
|
3
|
+
from collections import UserList, namedtuple
|
|
4
|
+
|
|
5
|
+
from ..common.utils import convert_date_to_midnight_timestamp
|
|
6
|
+
from .accounts import BankAccountHandler, BankAccountTypeHandler
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_bank_account_type_grouping(bank):
|
|
10
|
+
"""Get a summary of accounts for the given bank, grouped by type."""
|
|
11
|
+
# Get a grouping (by account type) of accounts at the given bank
|
|
12
|
+
type_accounts = {}
|
|
13
|
+
for account_type in BankAccountTypeHandler.get_types_for_bank(bank.id):
|
|
14
|
+
# Get only accounts for the logged in user and the given bank
|
|
15
|
+
type_accounts[account_type] = BankAccountHandler.get_accounts(
|
|
16
|
+
bank_ids=(bank.id,),
|
|
17
|
+
account_type_ids=(account_type.id,),
|
|
18
|
+
)
|
|
19
|
+
return type_accounts
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_balance_chart_data(transactions):
|
|
23
|
+
"""
|
|
24
|
+
Build a dataset to be passed to a `chartist.js` chart constructor.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
transactions : list
|
|
29
|
+
A list of transactions to be used for generating the chart data.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
chart_data : list
|
|
34
|
+
A list containing (x, y) pairs, each consisting of the Unix
|
|
35
|
+
timestamp (in milliseconds) and the bank account balance.
|
|
36
|
+
"""
|
|
37
|
+
return list(_BalanceChartData(transactions))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class _BalanceChartData(UserList):
|
|
41
|
+
"""
|
|
42
|
+
A list of balances to be passed to a `chartist.js` chart constructor.
|
|
43
|
+
|
|
44
|
+
A special list-like object containing transaction data formatted for
|
|
45
|
+
use in a balance chart created by the `chartist.js` library. This
|
|
46
|
+
converts each transaction into an (x, y) pair consisting of a Unix
|
|
47
|
+
timestamp (in milleseconds) and a corresponding bank account
|
|
48
|
+
balance. For transactions occurring on the same day (the finest
|
|
49
|
+
granularity recorded by the Monopyly app), a slight offset is
|
|
50
|
+
added to each timestamp to guarantee a smooth representation in the
|
|
51
|
+
rendered chart.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
transactions : list
|
|
56
|
+
A list of transactions to be used for generating the chart data.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
_DAILY_MILLISECONDS = 86_400_000
|
|
60
|
+
offset = 1
|
|
61
|
+
point = namedtuple("DataPoint", ["timestamp", "balance"])
|
|
62
|
+
|
|
63
|
+
def __init__(self, transactions):
|
|
64
|
+
super().__init__()
|
|
65
|
+
transaction_groups = self._group_transactions_by_date(transactions)
|
|
66
|
+
self._prepare_chart_data(transaction_groups)
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _group_transactions_by_date(transactions):
|
|
70
|
+
date_groups = {}
|
|
71
|
+
for transaction in transactions:
|
|
72
|
+
group = date_groups.setdefault(transaction.transaction_date, [])
|
|
73
|
+
group.append(transaction)
|
|
74
|
+
return date_groups
|
|
75
|
+
|
|
76
|
+
def _prepare_chart_data(self, transaction_groups):
|
|
77
|
+
# Assign chart data to the list as tuples, adding offsets for duplicated dates
|
|
78
|
+
for transaction_date, transaction_group in transaction_groups.items():
|
|
79
|
+
base_timestamp = convert_date_to_midnight_timestamp(
|
|
80
|
+
transaction_date, milliseconds=True
|
|
81
|
+
)
|
|
82
|
+
offset = self._DAILY_MILLISECONDS / len(transaction_group)
|
|
83
|
+
for i, transaction in enumerate(transaction_group):
|
|
84
|
+
adjusted_timestamp = base_timestamp + (i * offset)
|
|
85
|
+
self.data.append((adjusted_timestamp, transaction.balance))
|
|
@@ -79,7 +79,8 @@ def load_account_details(account_id):
|
|
|
79
79
|
"banking/account_page.html",
|
|
80
80
|
account=account,
|
|
81
81
|
account_transactions=transactions[:100],
|
|
82
|
-
|
|
82
|
+
# Reverse the chart transactions to be chronologically ascending
|
|
83
|
+
chart_data=get_balance_chart_data(reversed(transactions)),
|
|
83
84
|
)
|
|
84
85
|
|
|
85
86
|
|
|
@@ -21,25 +21,25 @@ class LocalApplication:
|
|
|
21
21
|
|
|
22
22
|
mode_name = "local"
|
|
23
23
|
default_port = "5001"
|
|
24
|
-
|
|
24
|
+
_debug = None
|
|
25
25
|
|
|
26
26
|
def __init__(self, host=None, port=None, **options):
|
|
27
27
|
"""Initialize the application in development mode."""
|
|
28
28
|
self._host = host
|
|
29
|
-
self._port = port
|
|
29
|
+
self._port = port or self.default_port
|
|
30
30
|
if options:
|
|
31
31
|
raise NotImplementedError(
|
|
32
32
|
f"Options besides `host` and `port` are not handled in {self.mode_name} mode."
|
|
33
33
|
)
|
|
34
|
+
self.application = create_app(debug=self._debug)
|
|
34
35
|
|
|
35
36
|
def run(self):
|
|
36
37
|
"""Run the Monopyly application in development mode."""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
server = subprocess.Popen(instruction)
|
|
38
|
+
self.application.run(
|
|
39
|
+
host=self._host,
|
|
40
|
+
port=self._port,
|
|
41
|
+
debug=self._debug,
|
|
42
|
+
)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class DevelopmentApplication(LocalApplication):
|
|
@@ -52,8 +52,8 @@ class DevelopmentApplication(LocalApplication):
|
|
|
52
52
|
"""
|
|
53
53
|
|
|
54
54
|
mode_name = "development"
|
|
55
|
-
default_port =
|
|
56
|
-
|
|
55
|
+
default_port = None # traditionally 5000
|
|
56
|
+
_debug = True
|
|
57
57
|
|
|
58
58
|
|
|
59
59
|
class ProductionApplication(BaseApplication):
|
|
@@ -64,31 +64,33 @@ class ProductionApplication(BaseApplication):
|
|
|
64
64
|
Gunicorn server instead of the built-in Python server.
|
|
65
65
|
"""
|
|
66
66
|
|
|
67
|
-
default_port =
|
|
67
|
+
default_port = None # traditionally 8000
|
|
68
68
|
_default_worker_count = (multiprocessing.cpu_count() * 2) + 1
|
|
69
69
|
|
|
70
70
|
def __init__(self, host=None, port=None, **options):
|
|
71
71
|
"""Initialize the application in production mode."""
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
if port and not host:
|
|
73
|
+
raise ValueError("A host must be specified when the port is given.")
|
|
74
|
+
self._host = host
|
|
75
|
+
self._port = port or self.default_port
|
|
74
76
|
self.options = options
|
|
77
|
+
self.options["bind"] = self._determine_binding(options.get("bind"))
|
|
78
|
+
self.options.setdefault("workers", self._default_worker_count)
|
|
75
79
|
self.application = create_app()
|
|
76
80
|
super().__init__()
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
def _parse_binding(host, port, bind_option):
|
|
82
|
+
def _determine_binding(self, bind_option):
|
|
80
83
|
# Parse any socket binding options
|
|
81
|
-
if
|
|
84
|
+
if self._host and bind_option:
|
|
82
85
|
raise ValueError(
|
|
83
|
-
"
|
|
84
|
-
"`bind` option is given."
|
|
86
|
+
"The `host` may not be specified directly if the `bind` option is used."
|
|
85
87
|
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return
|
|
88
|
+
if self._host:
|
|
89
|
+
bind_values = [self._host]
|
|
90
|
+
if self._port:
|
|
91
|
+
bind_values.append(self._port)
|
|
92
|
+
bind_option = ":".join(bind_values)
|
|
93
|
+
return bind_option
|
|
92
94
|
|
|
93
95
|
def load_config(self):
|
|
94
96
|
config = {
|
|
@@ -16,26 +16,37 @@ from rich.console import Console
|
|
|
16
16
|
|
|
17
17
|
from .apps import DevelopmentApplication, LocalApplication, ProductionApplication
|
|
18
18
|
|
|
19
|
-
# Set the Flask environment variable
|
|
19
|
+
# Set the Flask environment variable (to specify the app to use)
|
|
20
20
|
os.environ["FLASK_APP"] = "monopyly"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
def main():
|
|
24
|
-
|
|
25
|
-
app_launcher = Launcher(args.mode, host=args.host, port=args.port)
|
|
23
|
+
def main(mode, host=None, port=None, backup=False, browser=False):
|
|
24
|
+
app_launcher = Launcher(mode, host=host, port=port)
|
|
26
25
|
# Initialize the database and run the app
|
|
27
26
|
app_launcher.initialize_database()
|
|
28
|
-
if
|
|
27
|
+
if backup:
|
|
29
28
|
app_launcher.backup_database()
|
|
30
29
|
app_launcher.launch()
|
|
31
|
-
if
|
|
30
|
+
if mode in ("development", "local"):
|
|
32
31
|
# Enable browser viewing in development mode
|
|
33
|
-
if
|
|
32
|
+
if browser:
|
|
34
33
|
app_launcher.open_browser(delay=1)
|
|
35
34
|
# Wait for the exit command to stop
|
|
36
35
|
app_launcher.wait_for_exit()
|
|
37
36
|
|
|
38
37
|
|
|
38
|
+
def main_cli():
|
|
39
|
+
"""Run the app as a command line program."""
|
|
40
|
+
args = parse_arguments()
|
|
41
|
+
main(
|
|
42
|
+
args.mode,
|
|
43
|
+
host=args.host,
|
|
44
|
+
port=args.port,
|
|
45
|
+
backup=args.backup,
|
|
46
|
+
browser=args.browser,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
39
50
|
def parse_arguments():
|
|
40
51
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
41
52
|
parser.add_argument("--host", help="the host address where the app will be run")
|
|
@@ -121,4 +132,4 @@ class Launcher:
|
|
|
121
132
|
|
|
122
133
|
|
|
123
134
|
if __name__ == "__main__":
|
|
124
|
-
|
|
135
|
+
main_cli()
|
|
@@ -31,7 +31,7 @@ class EntryForm(FlaskForm, metaclass=AbstractEntryFormMixinMeta):
|
|
|
31
31
|
that follows the same naming schema.
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
def prepopulate(self, entry):
|
|
34
|
+
def prepopulate(self, entry, data=None):
|
|
35
35
|
"""
|
|
36
36
|
Generate a duplicate prepopulated form.
|
|
37
37
|
|
|
@@ -40,6 +40,11 @@ class EntryForm(FlaskForm, metaclass=AbstractEntryFormMixinMeta):
|
|
|
40
40
|
entry : database.models.Model
|
|
41
41
|
A database entry from which to pull information for
|
|
42
42
|
prepopulating the form.
|
|
43
|
+
data : dict, optional
|
|
44
|
+
A dictionary containing extra data that will be joined with
|
|
45
|
+
data gathered from the given database entry. Fields defined
|
|
46
|
+
in this data dictionary will supersed fields defined on the
|
|
47
|
+
entry.
|
|
43
48
|
|
|
44
49
|
Returns
|
|
45
50
|
-------
|
|
@@ -57,7 +62,9 @@ class EntryForm(FlaskForm, metaclass=AbstractEntryFormMixinMeta):
|
|
|
57
62
|
can not be used as a replacement for populating an existing
|
|
58
63
|
form.
|
|
59
64
|
"""
|
|
60
|
-
|
|
65
|
+
entry_data = self.gather_entry_data(entry) if entry else {}
|
|
66
|
+
# Merge data parsed from the entry with any data provided directly
|
|
67
|
+
data = entry_data | (data or {})
|
|
61
68
|
return self.__class__(data=data)
|
|
62
69
|
|
|
63
70
|
@abstractmethod
|
|
@@ -175,6 +182,53 @@ class TransactionForm(EntryForm):
|
|
|
175
182
|
# Define an autocompleter for the form (in a sublcass)
|
|
176
183
|
_autocompleter = None
|
|
177
184
|
|
|
185
|
+
def __init__(self, *args, **kwargs):
|
|
186
|
+
super().__init__(*args, **kwargs)
|
|
187
|
+
self.suggestions = {}
|
|
188
|
+
|
|
189
|
+
def prepopulate(self, entry, data=None, suggestion_fields=()):
|
|
190
|
+
"""
|
|
191
|
+
Generate a duplicate prepopulated form.
|
|
192
|
+
|
|
193
|
+
Parameters
|
|
194
|
+
----------
|
|
195
|
+
entry : database.models.Model
|
|
196
|
+
A database entry from which to pull information for
|
|
197
|
+
prepopulating the form.
|
|
198
|
+
data : dict, optional
|
|
199
|
+
A dictionary containing extra data that will be joined with
|
|
200
|
+
data gathered from the given database entry. Fields defined
|
|
201
|
+
in this data dictionary will supersed fields defined on the
|
|
202
|
+
entry.
|
|
203
|
+
suggestion_fields : list, optional
|
|
204
|
+
A list with form-specific parameters defining how the form
|
|
205
|
+
will extract and process a suggestion from the dictionary of
|
|
206
|
+
data which will be added to the form.
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
form : TransactionForm
|
|
211
|
+
A duplicate form, prepopulated with the collected database
|
|
212
|
+
information.
|
|
213
|
+
"""
|
|
214
|
+
self.suggestions |= self._extract_suggestions(data, suggestion_fields)
|
|
215
|
+
form = super().prepopulate(entry, data=data)
|
|
216
|
+
form.suggestions = self.suggestions
|
|
217
|
+
return form
|
|
218
|
+
|
|
219
|
+
def _extract_suggestions(self, data, suggestion_fields):
|
|
220
|
+
# Extract suggestion field values from the given data as specified
|
|
221
|
+
suggestions = {}
|
|
222
|
+
for field in suggestion_fields:
|
|
223
|
+
extraction_method = getattr(self, f"_extract_{field}_suggestion")
|
|
224
|
+
suggestions[field] = extraction_method(data)
|
|
225
|
+
return suggestions
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _extract_suggestion(data, field):
|
|
229
|
+
# Pop the field value to provide a suggestion, rather than a deduction
|
|
230
|
+
return data.pop(field, None)
|
|
231
|
+
|
|
178
232
|
def _prepare_transaction_data(self):
|
|
179
233
|
subtransactions_data = [
|
|
180
234
|
subform.subtransaction_data for subform in self["subtransactions"]
|
|
@@ -158,6 +158,15 @@ def _get_linked_credit_transaction(transaction_id, internal_transaction_id):
|
|
|
158
158
|
return transaction
|
|
159
159
|
|
|
160
160
|
|
|
161
|
+
def highlight_unmatched_transactions(transactions, unmatched_transactions):
|
|
162
|
+
"""Highlight transactions that are unmatched."""
|
|
163
|
+
unmatched_transaction_ids = [_.id for _ in unmatched_transactions]
|
|
164
|
+
for transaction in transactions:
|
|
165
|
+
if transaction.id in unmatched_transaction_ids:
|
|
166
|
+
transaction.highlight = True
|
|
167
|
+
yield transaction
|
|
168
|
+
|
|
169
|
+
|
|
161
170
|
class TransactionTagHandler(DatabaseHandler, model=TransactionTag):
|
|
162
171
|
"""
|
|
163
172
|
A database handler for managing transaction tags.
|
|
@@ -349,3 +358,156 @@ class TransactionTagHandler(DatabaseHandler, model=TransactionTag):
|
|
|
349
358
|
query = cls.model.select_for_user().where(*criteria)
|
|
350
359
|
tag = cls._db.session.execute(query).scalar_one_or_none()
|
|
351
360
|
return tag
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def categorize(transactions):
|
|
364
|
+
"""
|
|
365
|
+
Categorize subtransactions into a tree of categories and subcategories.
|
|
366
|
+
|
|
367
|
+
Given a list of transactions, this function places each transaction
|
|
368
|
+
(technically each individual subtransaction of the transaction) into
|
|
369
|
+
categories based on its assigned tags. When a category is ambiguous
|
|
370
|
+
(e.g., multiple tags have been assigned from different branches in
|
|
371
|
+
the tag tree), the subtransaction is left uncategorized.
|
|
372
|
+
|
|
373
|
+
Parameters
|
|
374
|
+
----------
|
|
375
|
+
transactions : list
|
|
376
|
+
The transactions (and corresponding subtransactions) to be
|
|
377
|
+
categorized.
|
|
378
|
+
|
|
379
|
+
Returns
|
|
380
|
+
-------
|
|
381
|
+
categories : CategoryTree
|
|
382
|
+
A tree-like structure of transaction categories, including
|
|
383
|
+
nested subcategories and subtotals at each level.
|
|
384
|
+
"""
|
|
385
|
+
# Assign the subtransactions to categories
|
|
386
|
+
categories = RootCategoryTree()
|
|
387
|
+
for subtransaction in get_subtransactions(transactions):
|
|
388
|
+
categories.categorize_subtransaction(subtransaction)
|
|
389
|
+
return categories
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def get_subtransactions(transactions):
|
|
393
|
+
"""Given a list of transactions, return all the corresponding subtransactions."""
|
|
394
|
+
return [
|
|
395
|
+
subtransaction
|
|
396
|
+
for transaction in transactions
|
|
397
|
+
for subtransaction in transaction.subtransactions
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class CategoryTree:
|
|
402
|
+
"""
|
|
403
|
+
Store a tree of categories.
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
category : database.models.TransactionTag, str
|
|
408
|
+
The (root) category that this tree will represent.
|
|
409
|
+
subtransactions : list
|
|
410
|
+
A list of subtransactions that belong to this category, but
|
|
411
|
+
which are not included in any subcategory of this category.
|
|
412
|
+
|
|
413
|
+
Attributes
|
|
414
|
+
----------
|
|
415
|
+
category : database.models.TransactionTag, str
|
|
416
|
+
The (root) category that this tree represents.
|
|
417
|
+
subtransactions : list
|
|
418
|
+
The subtransactions that belong to this category, but which are
|
|
419
|
+
not included in any subcategory of this category.
|
|
420
|
+
subcategories : dict
|
|
421
|
+
A mapping of subcategory names and trees that comprise this
|
|
422
|
+
category.
|
|
423
|
+
subtotal : float
|
|
424
|
+
The subtotal of all transactions in this category and all of its
|
|
425
|
+
subcategories.
|
|
426
|
+
"""
|
|
427
|
+
|
|
428
|
+
def __init__(self, category, subtransactions=None):
|
|
429
|
+
self.category = category
|
|
430
|
+
self.subtransactions = subtransactions or []
|
|
431
|
+
self.subcategories = {}
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def subtotal(self):
|
|
435
|
+
subcategories = list(self.subcategories.values())
|
|
436
|
+
return sum(item.subtotal for item in self.subtransactions + subcategories)
|
|
437
|
+
|
|
438
|
+
def add_subcategory(self, tag):
|
|
439
|
+
"""
|
|
440
|
+
Add a subcategory to the tree based on the given tag.
|
|
441
|
+
|
|
442
|
+
Add a subcategory tree to the mapping of subcategories based
|
|
443
|
+
on the given tag and return it. If the tag already has a
|
|
444
|
+
subcategory tree mapped to it, return that subcategory tree
|
|
445
|
+
instead.
|
|
446
|
+
|
|
447
|
+
Parameters
|
|
448
|
+
----------
|
|
449
|
+
tag : database.models.TransactionTag
|
|
450
|
+
The tag for which a subcategory will be added.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
subcategory : CategoryTree
|
|
455
|
+
The subcategory tree matching the given tag.
|
|
456
|
+
"""
|
|
457
|
+
return self.subcategories.setdefault(tag.tag_name, CategoryTree(tag))
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
class RootCategoryTree(CategoryTree):
|
|
461
|
+
"""A special class of category tree that forms the root of a categorization."""
|
|
462
|
+
|
|
463
|
+
def __init__(self, subtransactions=None):
|
|
464
|
+
super().__init__("root", subtransactions=subtransactions)
|
|
465
|
+
|
|
466
|
+
def categorize_subtransaction(self, subtransaction):
|
|
467
|
+
"""
|
|
468
|
+
Add a subtransaction to the tree of nested categories by tag.
|
|
469
|
+
|
|
470
|
+
Given a subtransaction, add that subtransaction to the category
|
|
471
|
+
tree according to its tags. If multiple tags exist at the same
|
|
472
|
+
level of the tree (i.e., a subtransaction with tags in diverging
|
|
473
|
+
branches), the tag is determined to be "uncategorizable" and the
|
|
474
|
+
tag is listed only as a member of the root tree and not as a
|
|
475
|
+
member of any other subcategory tree.
|
|
476
|
+
|
|
477
|
+
Parameters
|
|
478
|
+
----------
|
|
479
|
+
subtransaction :
|
|
480
|
+
The subtransaction to be categorized.
|
|
481
|
+
"""
|
|
482
|
+
tree = self
|
|
483
|
+
if subtransaction.categorizable:
|
|
484
|
+
# Collect all the tags for the subtransaction (ordered by tag depth)
|
|
485
|
+
tags = sorted(subtransaction.tags, key=lambda tag: tag.depth)
|
|
486
|
+
for tag in tags:
|
|
487
|
+
tree = tree.add_subcategory(tag)
|
|
488
|
+
tree.subtransactions.append(subtransaction)
|
|
489
|
+
|
|
490
|
+
def assemble_chart_data(self, exclude=()):
|
|
491
|
+
"""
|
|
492
|
+
Create a dataset of categories and subtotals that can be used in a chart.
|
|
493
|
+
|
|
494
|
+
Parameters
|
|
495
|
+
----------
|
|
496
|
+
exclude : ...
|
|
497
|
+
"""
|
|
498
|
+
labels, subtotals = [], []
|
|
499
|
+
# Add chart data for categorical information
|
|
500
|
+
for name, subcategory in self.subcategories.items():
|
|
501
|
+
if name not in exclude and subcategory.subtotal > 0:
|
|
502
|
+
labels.append(name)
|
|
503
|
+
subtotals.append(subcategory.subtotal)
|
|
504
|
+
# Add chart data for uncategorized transactions
|
|
505
|
+
if (other_subtotal := sum(_.subtotal for _ in self.subtransactions)) > 0:
|
|
506
|
+
labels.append("")
|
|
507
|
+
subtotals.append(other_subtotal)
|
|
508
|
+
# Return the data in a format similar to what is required by the chart app
|
|
509
|
+
subtotal_labels = zip(subtotals, labels, strict=True)
|
|
510
|
+
return {
|
|
511
|
+
"labels": [label for _, label in sorted(subtotal_labels, reverse=True)],
|
|
512
|
+
"subtotals": sorted(subtotals, reverse=True),
|
|
513
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from ..banking.transactions import record_new_transfer
|
|
4
4
|
from ..common.forms.utils import execute_on_form_validation
|
|
5
|
+
from ..common.utils import parse_date
|
|
5
6
|
from .cards import CreditCardHandler
|
|
6
7
|
from .statements import CreditStatementHandler
|
|
7
8
|
from .transactions import CreditTransactionHandler
|
|
@@ -174,3 +175,31 @@ def make_payment(card_id, payment_account_id, payment_date, payment_amount):
|
|
|
174
175
|
],
|
|
175
176
|
}
|
|
176
177
|
CreditTransactionHandler.add_entry(**credit_mapping)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def parse_request_transaction_data(request_args):
|
|
181
|
+
"""
|
|
182
|
+
Parse transaction data given as arguments on the request.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
request_args : dict
|
|
187
|
+
A dictionary of URL arguments provided by the request.
|
|
188
|
+
|
|
189
|
+
Returns
|
|
190
|
+
-------
|
|
191
|
+
transaction_data : dict
|
|
192
|
+
A dictionary of transaction data parsed from the request
|
|
193
|
+
arguments.
|
|
194
|
+
"""
|
|
195
|
+
if request_args:
|
|
196
|
+
transaction_data = {
|
|
197
|
+
"transaction_date": parse_date(request_args.get("transaction_date")),
|
|
198
|
+
}
|
|
199
|
+
if (subtotal := request_args.get("total")) is not None:
|
|
200
|
+
transaction_data["subtransactions"] = [{"subtotal": float(subtotal)}]
|
|
201
|
+
if (merchant := request_args.get("description")) is not None:
|
|
202
|
+
transaction_data["merchant"] = merchant
|
|
203
|
+
else:
|
|
204
|
+
transaction_data = {}
|
|
205
|
+
return transaction_data
|