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.
Files changed (237) hide show
  1. {monopyly-1.4.8 → monopyly-1.5.0}/.gitignore +1 -0
  2. {monopyly-1.4.8 → monopyly-1.5.0}/PKG-INFO +2 -1
  3. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/CHANGELOG.md +7 -0
  4. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/__init__.py +2 -2
  5. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/_version.py +2 -2
  6. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/cli/apps.py +1 -1
  7. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/cli/launch.py +3 -0
  8. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/_forms.py +56 -2
  9. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/transactions.py +162 -0
  10. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/actions.py +29 -0
  11. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/forms.py +25 -0
  12. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/routes.py +97 -7
  13. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/transactions/_transactions.py +15 -0
  14. monopyly-1.5.0/monopyly/credit/transactions/activity/__init__.py +3 -0
  15. monopyly-1.5.0/monopyly/credit/transactions/activity/data.py +161 -0
  16. monopyly-1.5.0/monopyly/credit/transactions/activity/parser.py +274 -0
  17. monopyly-1.5.0/monopyly/credit/transactions/activity/reconciliation.py +456 -0
  18. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/models.py +6 -0
  19. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/css/style.css +1141 -263
  20. monopyly-1.5.0/monopyly/static/img/icons/statement-pair.png +0 -0
  21. monopyly-1.5.0/monopyly/static/img/icons/statement.png +0 -0
  22. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/bind-tag-actions.js +1 -1
  23. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/create-balance-chart.js +1 -1
  24. monopyly-1.5.0/monopyly/static/js/create-category-chart.js +27 -0
  25. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/define-filter.js +1 -1
  26. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/expand-transaction.js +10 -0
  27. monopyly-1.5.0/monopyly/static/js/highlight-discrepant-transactions.js +124 -0
  28. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/expand-transaction.js +12 -3
  29. monopyly-1.5.0/monopyly/static/js/modules/form-suggestions.js +60 -0
  30. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/manage-overlays.js +1 -3
  31. monopyly-1.5.0/monopyly/static/js/show-credit-activity-loader.js +29 -0
  32. monopyly-1.5.0/monopyly/static/js/toggle-navigation.js +35 -0
  33. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-card-status.js +1 -1
  34. monopyly-1.5.0/monopyly/static/js/use-suggested-amount.js +11 -0
  35. monopyly-1.5.0/monopyly/static/js/use-suggested-merchant.js +11 -0
  36. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_page.html +3 -1
  37. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries.html +1 -1
  38. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/accounts_page.html +11 -15
  39. monopyly-1.5.0/monopyly/templates/banking/transactions_table/expanded_row_content.html +50 -0
  40. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subtransaction_subform.html +10 -1
  41. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_bank_transaction.html +1 -1
  42. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_credit_transaction.html +1 -1
  43. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_condensed.html +2 -2
  44. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transaction_expanded.html +3 -3
  45. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/transactions.html +1 -1
  46. monopyly-1.5.0/monopyly/templates/core/credits.html +34 -0
  47. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/index.html +10 -0
  48. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/profile.html +1 -1
  49. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statement_page.html +33 -0
  50. monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/discrepant_records.html +25 -0
  51. monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +23 -0
  52. monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +86 -0
  53. monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/summary.html +45 -0
  54. monopyly-1.5.0/monopyly/templates/credit/statement_reconciliation/unrecorded_activities.html +24 -0
  55. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statement_summary.html +2 -2
  56. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statements.html +1 -1
  57. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form.html +9 -1
  58. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page.html +2 -0
  59. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_update.html +9 -0
  60. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_submission_page.html +8 -0
  61. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/expanded_row_content.html +18 -12
  62. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/layout.html +35 -27
  63. {monopyly-1.4.8 → monopyly-1.5.0}/pyproject.toml +1 -0
  64. monopyly-1.4.8/monopyly/static/img/icons/statement-pair.svg +0 -281
  65. monopyly-1.4.8/monopyly/static/img/icons/statement.svg +0 -294
  66. monopyly-1.4.8/monopyly/templates/banking/transactions_table/expanded_row_content.html +0 -52
  67. monopyly-1.4.8/monopyly/templates/core/credits.html +0 -32
  68. {monopyly-1.4.8 → monopyly-1.5.0}/COPYING +0 -0
  69. {monopyly-1.4.8 → monopyly-1.5.0}/LICENSE +0 -0
  70. {monopyly-1.4.8 → monopyly-1.5.0}/README.md +0 -0
  71. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/README.md +0 -0
  72. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/actions.py +0 -0
  73. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/blueprint.py +0 -0
  74. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/routes.py +0 -0
  75. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/auth/tools.py +0 -0
  76. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/accounts.py +0 -0
  77. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/actions.py +0 -0
  78. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/banks.py +0 -0
  79. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/blueprint.py +0 -0
  80. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/filters.py +0 -0
  81. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/forms.py +0 -0
  82. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/routes.py +0 -0
  83. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/banking/transactions.py +0 -0
  84. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/__init__.py +0 -0
  85. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/fields.py +0 -0
  86. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/utils.py +0 -0
  87. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/forms/validators.py +0 -0
  88. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/common/utils.py +0 -0
  89. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/__init__.py +0 -0
  90. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/default_settings.py +0 -0
  91. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/config/settings.py +0 -0
  92. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/actions.py +0 -0
  93. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/blueprint.py +0 -0
  94. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/context_processors.py +0 -0
  95. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/errors.py +0 -0
  96. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/filters.py +0 -0
  97. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/internal_transactions.py +0 -0
  98. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/core/routes.py +0 -0
  99. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/accounts.py +0 -0
  100. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/blueprint.py +0 -0
  101. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/cards.py +0 -0
  102. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/statements.py +0 -0
  103. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/credit/transactions/__init__.py +0 -0
  104. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/__init__.py +0 -0
  105. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/preloads.sql +0 -0
  106. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/schema.sql +0 -0
  107. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/database/views.sql +0 -0
  108. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/browserconfig.xml +0 -0
  109. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-114.png +0 -0
  110. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-120.png +0 -0
  111. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-144.png +0 -0
  112. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-150.png +0 -0
  113. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-152.png +0 -0
  114. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-16.png +0 -0
  115. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-160.png +0 -0
  116. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-180.png +0 -0
  117. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-192.png +0 -0
  118. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-310.png +0 -0
  119. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-32.png +0 -0
  120. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-57.png +0 -0
  121. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-60.png +0 -0
  122. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-64.png +0 -0
  123. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-70.png +0 -0
  124. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-72.png +0 -0
  125. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-76.png +0 -0
  126. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon-96.png +0 -0
  127. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon.ico +0 -0
  128. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/favicon/favicon.png +0 -0
  129. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-details.png +0 -0
  130. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-account-summaries.png +0 -0
  131. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/bank-accounts.png +0 -0
  132. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/credit-account-details.png +0 -0
  133. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/credit-transactions.png +0 -0
  134. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/homepage-user.png +0 -0
  135. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/homepage.png +0 -0
  136. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/about/statement-details.png +0 -0
  137. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/chase-card.png +0 -0
  138. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/discover-card.png +0 -0
  139. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/new-card.png +0 -0
  140. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/cards/template-card.png +0 -0
  141. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-down.png +0 -0
  142. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-left.png +0 -0
  143. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/arrow-up.png +0 -0
  144. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/checkmark.png +0 -0
  145. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange-thick.png +0 -0
  146. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-orange.png +0 -0
  147. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete-thick.png +0 -0
  148. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/delete.png +0 -0
  149. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/edit.png +0 -0
  150. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/link.png +0 -0
  151. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/minus-thick.png +0 -0
  152. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/minus.png +0 -0
  153. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/plus-thick.png +0 -0
  154. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/plus.png +0 -0
  155. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/refresh.png +0 -0
  156. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/save.png +0 -0
  157. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/sort-asc.png +0 -0
  158. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/sort-desc.png +0 -0
  159. /monopyly-1.4.8/monopyly/static/img/icons/statement.png → /monopyly-1.5.0/monopyly/static/img/icons/statement-thick.png +0 -0
  160. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/icons/x-thick.png +0 -0
  161. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/img/statement.png +0 -0
  162. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/jquery-3.7.0.min.js +0 -0
  163. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/add-subtransaction.js +0 -0
  164. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/add-transfer.js +0 -0
  165. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/autocomplete-transaction.js +0 -0
  166. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-account-type-inputs.js +0 -0
  167. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-bank-inputs.js +0 -0
  168. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/display-new-credit-account-inputs.js +0 -0
  169. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/expand-bank-account.js +0 -0
  170. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/expand-bank.js +0 -0
  171. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/flip-card.js +0 -0
  172. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/hide-homepage-block.js +0 -0
  173. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/infer-card.js +0 -0
  174. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/infer-statement.js +0 -0
  175. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/make-payment.js +0 -0
  176. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/ajax.js +0 -0
  177. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/autocomplete-input.js +0 -0
  178. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/expand-box-row.js +0 -0
  179. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/manage-acquisition-form.js +0 -0
  180. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/manage-subforms.js +0 -0
  181. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/update-database-widget.js +0 -0
  182. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/modules/update-display-ajax.js +0 -0
  183. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/show-linked-transaction.js +0 -0
  184. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-account-statement-parameters.js +0 -0
  185. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-bank-name.js +0 -0
  186. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-statement-parameters.js +0 -0
  187. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-statements-display.js +0 -0
  188. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/static/js/update-transactions-display.js +0 -0
  189. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/auth/login.html +0 -0
  190. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/auth/register.html +0 -0
  191. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form.html +0 -0
  192. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_form/account_form_page_new.html +0 -0
  193. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summaries_page.html +0 -0
  194. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/account_summary.html +0 -0
  195. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/bank_info_form.html +0 -0
  196. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form.html +0 -0
  197. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page.html +0 -0
  198. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_new.html +0 -0
  199. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transaction_form_page_update.html +0 -0
  200. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transaction_form/transfer_form.html +0 -0
  201. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/condensed_row_content.html +0 -0
  202. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transaction_field_titles.html +0 -0
  203. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/banking/transactions_table/transactions.html +0 -0
  204. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/form_page.html +0 -0
  205. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/subform.html +0 -0
  206. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transaction_form/transaction_form_page.html +0 -0
  207. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/linked_transaction_overlay.html +0 -0
  208. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/common/transactions_table/subtransactions.html +0 -0
  209. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/400.html +0 -0
  210. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/401.html +0 -0
  211. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/403.html +0 -0
  212. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/404.html +0 -0
  213. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/405.html +0 -0
  214. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/408.html +0 -0
  215. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/418.html +0 -0
  216. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/425.html +0 -0
  217. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/500.html +0 -0
  218. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/errors/error.html +0 -0
  219. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/core/story.html +0 -0
  220. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/account_page.html +0 -0
  221. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form.html +0 -0
  222. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/card_form_page_new.html +0 -0
  223. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_form/transfer_statement_inquiry.html +0 -0
  224. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_back.html +0 -0
  225. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_graphic/card_front.html +0 -0
  226. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/card_submission_page.html +0 -0
  227. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/cards.html +0 -0
  228. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/cards_page.html +0 -0
  229. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/statements_page.html +0 -0
  230. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/subtag_tree.html +0 -0
  231. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tag_tree/tag_tree.html +0 -0
  232. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/tags_page.html +0 -0
  233. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transaction_form/transaction_form_page_new.html +0 -0
  234. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_page.html +0 -0
  235. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/condensed_row_content.html +0 -0
  236. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/transaction_field_titles.html +0 -0
  237. {monopyly-1.4.8 → monopyly-1.5.0}/monopyly/templates/credit/transactions_table/transactions.html +0 -0
@@ -80,6 +80,7 @@ monopyly/_version.py
80
80
  monopyly/static/img/**/*.pptx
81
81
  monopyly/static/img/**/*.key
82
82
  monopyly/static/img/**/*.xcf
83
+ monopyly/static/img/**/*.svg
83
84
  monopyly/static/favicon/*.ai
84
85
 
85
86
  # Flask instance files/data
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: monopyly
3
- Version: 1.4.8
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)
@@ -1,4 +1,4 @@
1
1
  # file generated by setuptools_scm
2
2
  # don't change, don't track in version control
3
- __version__ = version = '1.4.8'
4
- __version_tuple__ = version_tuple = (1, 4, 8)
3
+ __version__ = version = '1.5.0'
4
+ __version_tuple__ = version_tuple = (1, 5, 0)
@@ -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
- data = self.gather_entry_data(entry)
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 flash, g, jsonify, redirect, render_template, request, url_for
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 get_linked_transaction
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
- statement = CreditStatementHandler.get_entry(statement_id)
338
- form = form.prepopulate(statement)
421
+ entry = CreditStatementHandler.get_entry(statement_id)
339
422
  elif card_id:
340
- card = CreditCardHandler.get_entry(card_id)
341
- form = form.prepopulate(card)
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(transaction)
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
  """
@@ -0,0 +1,3 @@
1
+ from .data import TransactionActivities
2
+ from .parser import parse_transaction_activity_file
3
+ from .reconciliation import ActivityMatchmaker