monopyly 1.4.8__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.8 → monopyly-1.5.0}/.gitignore +1 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/PKG-INFO +2 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/CHANGELOG.md +7 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/__init__.py +2 -2
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/_version.py +2 -2
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/cli/apps.py +1 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/cli/launch.py +3 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/_forms.py +56 -2
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/transactions.py +162 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/actions.py +29 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/forms.py +25 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/routes.py +97 -7
- {monopyly-1.4.8 → 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.8 → monopyly-1.5.0}/monopyly/database/models.py +6 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/css/style.css +1141 -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.8 → monopyly-1.5.0}/monopyly/static/js/bind-tag-actions.js +1 -1
- {monopyly-1.4.8 → 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.8 → monopyly-1.5.0}/monopyly/static/js/define-filter.js +1 -1
- {monopyly-1.4.8 → 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.8 → 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.8 → 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.8 → 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.8 → monopyly-1.5.0}/monopyly/templates/banking/account_page.html +3 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries.html +1 -1
- {monopyly-1.4.8 → 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.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subtransaction_subform.html +10 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_bank_transaction.html +1 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_credit_transaction.html +1 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_condensed.html +2 -2
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_expanded.html +3 -3
- {monopyly-1.4.8 → 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.8 → monopyly-1.5.0}/monopyly/templates/core/index.html +10 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/profile.html +1 -1
- {monopyly-1.4.8 → 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.8 → monopyly-1.5.0}/monopyly/templates/credit/statement_summary.html +2 -2
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statements.html +1 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form.html +9 -1
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page.html +2 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_update.html +9 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_submission_page.html +8 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/expanded_row_content.html +18 -12
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/layout.html +35 -27
- {monopyly-1.4.8 → monopyly-1.5.0}/pyproject.toml +1 -0
- monopyly-1.4.8/monopyly/static/img/icons/statement-pair.svg +0 -281
- monopyly-1.4.8/monopyly/static/img/icons/statement.svg +0 -294
- monopyly-1.4.8/monopyly/templates/banking/transactions_table/expanded_row_content.html +0 -52
- monopyly-1.4.8/monopyly/templates/core/credits.html +0 -32
- {monopyly-1.4.8 → monopyly-1.5.0}/COPYING +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/LICENSE +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/README.md +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/README.md +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/actions.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/blueprint.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/routes.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/tools.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/accounts.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/actions.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/banks.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/blueprint.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/filters.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/forms.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/routes.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/transactions.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/__init__.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/fields.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/utils.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/validators.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/utils.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/__init__.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/default_settings.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/settings.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/actions.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/blueprint.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/context_processors.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/errors.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/filters.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/internal_transactions.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/routes.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/accounts.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/blueprint.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/cards.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/statements.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/transactions/__init__.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/__init__.py +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/preloads.sql +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/schema.sql +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/views.sql +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/browserconfig.xml +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-114.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-120.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-144.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-150.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-152.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-16.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-160.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-180.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-192.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-310.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-32.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-57.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-60.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-64.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-70.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-72.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-76.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-96.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon.ico +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-details.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-summaries.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-accounts.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/credit-account-details.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/credit-transactions.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/homepage-user.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/homepage.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/statement-details.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/chase-card.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/discover-card.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/new-card.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/template-card.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-down.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-left.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-up.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/checkmark.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/edit.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/link.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/minus-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/minus.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/plus-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/plus.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/refresh.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/save.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/sort-asc.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/sort-desc.png +0 -0
- /monopyly-1.4.8/monopyly/static/img/icons/statement.png → /monopyly-1.5.0/monopyly/static/img/icons/statement-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/x-thick.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/statement.png +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/jquery-3.7.0.min.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/add-subtransaction.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/add-transfer.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/autocomplete-transaction.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-account-type-inputs.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-bank-inputs.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-credit-account-inputs.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/expand-bank-account.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/expand-bank.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/flip-card.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/hide-homepage-block.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/infer-card.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/infer-statement.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/make-payment.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/ajax.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/autocomplete-input.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/expand-box-row.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/manage-acquisition-form.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/manage-subforms.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/update-database-widget.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/update-display-ajax.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/show-linked-transaction.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-account-statement-parameters.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-bank-name.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-statement-parameters.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-statements-display.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-transactions-display.js +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/auth/login.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/auth/register.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form_page_new.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summary.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/bank_info_form.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_new.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_update.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transfer_form.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/condensed_row_content.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transaction_field_titles.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transactions.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/form_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subform.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/transaction_form_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_transaction_overlay.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/subtransactions.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/400.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/401.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/403.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/404.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/405.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/408.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/418.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/425.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/500.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/error.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/story.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/account_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form_page_new.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/transfer_statement_inquiry.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_back.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_front.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_submission_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/cards.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/cards_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statements_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/subtag_tree.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/tag_tree.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tags_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_new.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_page.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/condensed_row_content.html +0 -0
- {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/transaction_field_titles.html +0 -0
- {monopyly-1.4.8 → 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
|
|
@@ -192,4 +192,11 @@
|
|
|
192
192
|
- Fix bug in the ordering of balances in the bank account balance charts for transactions on duplicate dates
|
|
193
193
|
|
|
194
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
|
+
|
|
195
202
|
<a name="bottom" id="bottom"></a>
|
|
@@ -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)
|
|
@@ -31,7 +31,7 @@ class LocalApplication:
|
|
|
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()
|
|
34
|
+
self.application = create_app(debug=self._debug)
|
|
35
35
|
|
|
36
36
|
def run(self):
|
|
37
37
|
"""Run the Monopyly application in development mode."""
|
|
@@ -16,6 +16,9 @@ from rich.console import Console
|
|
|
16
16
|
|
|
17
17
|
from .apps import DevelopmentApplication, LocalApplication, ProductionApplication
|
|
18
18
|
|
|
19
|
+
# Set the Flask environment variable (to specify the app to use)
|
|
20
|
+
os.environ["FLASK_APP"] = "monopyly"
|
|
21
|
+
|
|
19
22
|
|
|
20
23
|
def main(mode, host=None, port=None, backup=False, browser=False):
|
|
21
24
|
app_launcher = Launcher(mode, host=host, port=port)
|
|
@@ -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
|
|
@@ -36,6 +36,8 @@ from ..database.models import (
|
|
|
36
36
|
from .accounts import CreditAccountHandler
|
|
37
37
|
from .cards import CreditCardHandler
|
|
38
38
|
from .statements import CreditStatementHandler
|
|
39
|
+
from .transactions import CreditTransactionHandler
|
|
40
|
+
from .transactions.activity.reconciliation import ActivityMatchmaker
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
class CreditAccountSelectField(CustomChoiceSelectField):
|
|
@@ -297,6 +299,29 @@ class CreditTransactionForm(TransactionForm):
|
|
|
297
299
|
statement = self.get_transaction_statement()
|
|
298
300
|
return self._prepare_transaction_data(statement)
|
|
299
301
|
|
|
302
|
+
def _extract_merchant_suggestion(self, data):
|
|
303
|
+
# Use the merchant transaction data as a suggestion source
|
|
304
|
+
if merchant := self._extract_suggestion(data, "merchant"):
|
|
305
|
+
merchant_tokens = ActivityMatchmaker.tokenize(merchant)
|
|
306
|
+
# Suggest a known merchant with the closest distance to the activity merchant
|
|
307
|
+
score_records = []
|
|
308
|
+
for potential_merchant in CreditTransactionHandler.get_merchants():
|
|
309
|
+
test_tokens = ActivityMatchmaker.tokenize(potential_merchant)
|
|
310
|
+
score = ActivityMatchmaker.score_tokens(merchant_tokens, test_tokens)
|
|
311
|
+
# Only consider scores that have some similarity at all
|
|
312
|
+
if score < 1:
|
|
313
|
+
score_records.append((score, potential_merchant))
|
|
314
|
+
suggested_merchant = min(score_records)[1] if score_records else None
|
|
315
|
+
else:
|
|
316
|
+
suggested_merchant = None
|
|
317
|
+
return suggested_merchant
|
|
318
|
+
|
|
319
|
+
def _extract_amount_suggestion(self, data):
|
|
320
|
+
# Use the first subtransaction subtotal as a suggestion
|
|
321
|
+
subtransactions = self._extract_suggestion(data, "subtransactions")
|
|
322
|
+
suggested_amount = subtransactions[0]["subtotal"] if subtransactions else None
|
|
323
|
+
return suggested_amount
|
|
324
|
+
|
|
300
325
|
def get_transaction_statement(self):
|
|
301
326
|
"""Get the credit card statement associated with the transaction."""
|
|
302
327
|
return self.statement_info.get_statement(self.transaction_date.data)
|
|
@@ -4,7 +4,16 @@ Routes for credit card financials.
|
|
|
4
4
|
|
|
5
5
|
from itertools import islice
|
|
6
6
|
|
|
7
|
-
from flask import
|
|
7
|
+
from flask import (
|
|
8
|
+
flash,
|
|
9
|
+
g,
|
|
10
|
+
jsonify,
|
|
11
|
+
redirect,
|
|
12
|
+
render_template,
|
|
13
|
+
request,
|
|
14
|
+
session,
|
|
15
|
+
url_for,
|
|
16
|
+
)
|
|
8
17
|
from fuisce.database import db_transaction
|
|
9
18
|
from sqlalchemy.exc import MultipleResultsFound
|
|
10
19
|
from werkzeug.exceptions import abort
|
|
@@ -16,7 +25,11 @@ from ..banking.banks import BankHandler
|
|
|
16
25
|
from ..banking.transactions import BankTransactionHandler
|
|
17
26
|
from ..common.forms import form_err_msg
|
|
18
27
|
from ..common.forms.utils import extend_field_list_for_ajax
|
|
19
|
-
from ..common.transactions import
|
|
28
|
+
from ..common.transactions import (
|
|
29
|
+
categorize,
|
|
30
|
+
get_linked_transaction,
|
|
31
|
+
highlight_unmatched_transactions,
|
|
32
|
+
)
|
|
20
33
|
from ..common.utils import dedelimit_float, parse_date, sort_by_frequency
|
|
21
34
|
from .accounts import CreditAccountHandler
|
|
22
35
|
from .actions import (
|
|
@@ -24,6 +37,7 @@ from .actions import (
|
|
|
24
37
|
get_potential_preceding_card,
|
|
25
38
|
get_statement_and_transactions,
|
|
26
39
|
make_payment,
|
|
40
|
+
parse_request_transaction_data,
|
|
27
41
|
transfer_credit_card_statement,
|
|
28
42
|
)
|
|
29
43
|
from .blueprint import bp
|
|
@@ -31,6 +45,11 @@ from .cards import CreditCardHandler, save_card
|
|
|
31
45
|
from .forms import CardStatementTransferForm, CreditCardForm, CreditTransactionForm
|
|
32
46
|
from .statements import CreditStatementHandler
|
|
33
47
|
from .transactions import CreditTagHandler, CreditTransactionHandler, save_transaction
|
|
48
|
+
from .transactions.activity import (
|
|
49
|
+
ActivityMatchmaker,
|
|
50
|
+
TransactionActivities,
|
|
51
|
+
parse_transaction_activity_file,
|
|
52
|
+
)
|
|
34
53
|
|
|
35
54
|
|
|
36
55
|
@bp.route("/cards")
|
|
@@ -179,6 +198,7 @@ def update_statements_display():
|
|
|
179
198
|
@login_required
|
|
180
199
|
def load_statement_details(statement_id):
|
|
181
200
|
statement, transactions = get_statement_and_transactions(statement_id)
|
|
201
|
+
categories = categorize(transactions)
|
|
182
202
|
# Get bank accounts for potential payments
|
|
183
203
|
bank_accounts = BankAccountHandler.get_accounts()
|
|
184
204
|
return render_template(
|
|
@@ -186,6 +206,7 @@ def load_statement_details(statement_id):
|
|
|
186
206
|
statement=statement,
|
|
187
207
|
statement_transactions=transactions,
|
|
188
208
|
bank_accounts=bank_accounts,
|
|
209
|
+
chart_data=categories.assemble_chart_data(exclude=["Credit payments"]),
|
|
189
210
|
)
|
|
190
211
|
|
|
191
212
|
|
|
@@ -232,6 +253,68 @@ def pay_credit_card(card_id, statement_id):
|
|
|
232
253
|
return jsonify((summary_template, transactions_table_template))
|
|
233
254
|
|
|
234
255
|
|
|
256
|
+
@bp.route("/_reconcile_activity/<int:statement_id>")
|
|
257
|
+
@login_required
|
|
258
|
+
def reconcile_activity(statement_id):
|
|
259
|
+
return render_template(
|
|
260
|
+
"credit/statement_reconciliation/statement_reconciliation_inquiry.html",
|
|
261
|
+
statement_id=statement_id,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@bp.route("/reconciliation/<int:statement_id>", methods=("GET", "POST"))
|
|
266
|
+
@login_required
|
|
267
|
+
def load_statement_reconciliation_details(statement_id):
|
|
268
|
+
if request.method == "POST":
|
|
269
|
+
activity_file = request.files.get("activity-file")
|
|
270
|
+
# Parse the data and match transactions to activities
|
|
271
|
+
if activities := parse_transaction_activity_file(activity_file):
|
|
272
|
+
session["reconciliation_info"] = (statement_id, activities.jsonify())
|
|
273
|
+
else:
|
|
274
|
+
activity_data = session.get("reconciliation_info", (None, []))[1]
|
|
275
|
+
activities = TransactionActivities(activity_data)
|
|
276
|
+
if activities:
|
|
277
|
+
statement, transactions = get_statement_and_transactions(statement_id)
|
|
278
|
+
matchmaker = ActivityMatchmaker(transactions, activities)
|
|
279
|
+
non_matches = matchmaker.unmatched_transactions
|
|
280
|
+
transactions = list(highlight_unmatched_transactions(transactions, non_matches))
|
|
281
|
+
# Calculate the amount charged/refunded during this statement timeframe
|
|
282
|
+
prior_statement = CreditStatementHandler.get_prior_statement(statement)
|
|
283
|
+
prior_statement_balance = prior_statement.balance if prior_statement else 0
|
|
284
|
+
statement_transaction_balance = statement.balance - prior_statement_balance
|
|
285
|
+
return render_template(
|
|
286
|
+
"credit/statement_reconciliation/statement_reconciliation_page.html",
|
|
287
|
+
statement=statement,
|
|
288
|
+
statement_transactions=transactions,
|
|
289
|
+
discrepant_records=matchmaker.match_discrepancies,
|
|
290
|
+
discrepant_amount=abs(statement_transaction_balance - activities.total),
|
|
291
|
+
unrecorded_activities=matchmaker.unmatched_activities,
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
flash("ERROR")
|
|
295
|
+
return redirect(
|
|
296
|
+
url_for("credit.load_statement_details", statement_id=statement_id)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
@bp.before_app_request
|
|
301
|
+
def clear_reconciliation_info():
|
|
302
|
+
exempt_endpoints = (
|
|
303
|
+
"credit.reconcile_activity",
|
|
304
|
+
"credit.load_statement_reconciliation_details",
|
|
305
|
+
"credit.expand_transaction",
|
|
306
|
+
"credit.add_transaction",
|
|
307
|
+
"credit.update_transaction",
|
|
308
|
+
"credit.infer_statement",
|
|
309
|
+
"credit.suggest_transaction_autocomplete",
|
|
310
|
+
"credit.delete_transacton",
|
|
311
|
+
"static",
|
|
312
|
+
None,
|
|
313
|
+
)
|
|
314
|
+
if request.endpoint not in exempt_endpoints:
|
|
315
|
+
session.pop("reconciliation_info", None)
|
|
316
|
+
|
|
317
|
+
|
|
235
318
|
@bp.route("/transactions", defaults={"card_id": None})
|
|
236
319
|
@bp.route("/transactions/<int:card_id>")
|
|
237
320
|
@login_required
|
|
@@ -333,12 +416,16 @@ def add_transaction(card_id, statement_id):
|
|
|
333
416
|
update=False,
|
|
334
417
|
)
|
|
335
418
|
else:
|
|
419
|
+
transaction_data = parse_request_transaction_data(request.args)
|
|
336
420
|
if statement_id:
|
|
337
|
-
|
|
338
|
-
form = form.prepopulate(statement)
|
|
421
|
+
entry = CreditStatementHandler.get_entry(statement_id)
|
|
339
422
|
elif card_id:
|
|
340
|
-
|
|
341
|
-
|
|
423
|
+
entry = CreditCardHandler.get_entry(card_id)
|
|
424
|
+
else:
|
|
425
|
+
entry = None
|
|
426
|
+
form = form.prepopulate(
|
|
427
|
+
entry, data=transaction_data, suggestion_fields=["merchant"]
|
|
428
|
+
)
|
|
342
429
|
# Display the form for accepting user input
|
|
343
430
|
return render_template(
|
|
344
431
|
"credit/transaction_form/transaction_form_page_new.html", form=form
|
|
@@ -360,8 +447,11 @@ def update_transaction(transaction_id):
|
|
|
360
447
|
update=True,
|
|
361
448
|
)
|
|
362
449
|
else:
|
|
450
|
+
transaction_data = parse_request_transaction_data(request.args)
|
|
363
451
|
transaction = CreditTransactionHandler.get_entry(transaction_id)
|
|
364
|
-
form = form.prepopulate(
|
|
452
|
+
form = form.prepopulate(
|
|
453
|
+
transaction, data=transaction_data, suggestion_fields=["amount"]
|
|
454
|
+
)
|
|
365
455
|
# Display the form for accepting user input
|
|
366
456
|
return render_template(
|
|
367
457
|
"credit/transaction_form/transaction_form_page_update.html",
|
|
@@ -81,6 +81,21 @@ class CreditTransactionHandler(
|
|
|
81
81
|
)
|
|
82
82
|
return transactions
|
|
83
83
|
|
|
84
|
+
@classmethod
|
|
85
|
+
@DatabaseViewHandler.view_query
|
|
86
|
+
def get_merchants(cls):
|
|
87
|
+
"""
|
|
88
|
+
Get a credit card merchants from the database.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
merchants : sqlalchemy.engine.ScalarResult
|
|
93
|
+
All known credit card transaction merchants from the
|
|
94
|
+
database.
|
|
95
|
+
"""
|
|
96
|
+
query = cls.model.select_for_user(cls.model.merchant).distinct()
|
|
97
|
+
return cls._db.session.scalars(query)
|
|
98
|
+
|
|
84
99
|
@classmethod
|
|
85
100
|
def add_entry(cls, **field_values):
|
|
86
101
|
"""
|