monopyly 1.5.1__py3-none-any.whl → 1.6.0__py3-none-any.whl

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 (97) hide show
  1. monopyly/CHANGELOG.md +27 -0
  2. monopyly/README.md +3 -3
  3. monopyly/__init__.py +22 -27
  4. monopyly/_version.py +2 -2
  5. monopyly/auth/blueprint.py +2 -0
  6. monopyly/auth/routes.py +2 -3
  7. monopyly/banking/accounts.py +7 -7
  8. monopyly/banking/actions.py +20 -17
  9. monopyly/banking/banks.py +1 -1
  10. monopyly/banking/blueprint.py +2 -0
  11. monopyly/banking/filters.py +6 -6
  12. monopyly/banking/forms.py +3 -5
  13. monopyly/banking/routes.py +72 -10
  14. monopyly/banking/transactions.py +15 -7
  15. monopyly/common/forms/__init__.py +8 -0
  16. monopyly/common/forms/_forms.py +1 -2
  17. monopyly/common/forms/fields.py +0 -2
  18. monopyly/common/forms/utils.py +1 -1
  19. monopyly/common/transactions.py +89 -14
  20. monopyly/core/actions.py +2 -8
  21. monopyly/core/blueprint.py +2 -0
  22. monopyly/core/filters.py +0 -2
  23. monopyly/core/routes.py +1 -1
  24. monopyly/credit/accounts.py +1 -1
  25. monopyly/credit/actions.py +4 -5
  26. monopyly/credit/blueprint.py +2 -0
  27. monopyly/credit/cards.py +7 -3
  28. monopyly/credit/forms.py +3 -5
  29. monopyly/credit/routes.py +65 -87
  30. monopyly/credit/statements.py +1 -1
  31. monopyly/credit/transactions/__init__.py +2 -0
  32. monopyly/credit/transactions/_transactions.py +18 -8
  33. monopyly/credit/transactions/activity/__init__.py +6 -0
  34. monopyly/credit/transactions/activity/parser.py +0 -1
  35. monopyly/credit/transactions/activity/reconciliation.py +25 -4
  36. monopyly/database/__init__.py +1 -59
  37. monopyly/database/models.py +198 -276
  38. monopyly/database/preloads.sql +6 -1
  39. monopyly/scripts/screenshot_application.py +100 -0
  40. monopyly/static/chartist-1.5.0.min.js +8 -0
  41. monopyly/static/css/style.css +35 -14
  42. monopyly/static/img/about/bank-account-details.png +0 -0
  43. monopyly/static/img/about/bank-account-summaries.png +0 -0
  44. monopyly/static/img/about/bank-accounts.png +0 -0
  45. monopyly/static/img/about/credit-account-details.png +0 -0
  46. monopyly/static/img/about/credit-statement-details.png +0 -0
  47. monopyly/static/img/about/credit-transactions.png +0 -0
  48. monopyly/static/img/about/homepage-user.png +0 -0
  49. monopyly/static/img/about/homepage.png +0 -0
  50. monopyly/static/jquery-3.7.1.min.js +2 -0
  51. monopyly/static/js/add-transfer.js +8 -9
  52. monopyly/static/js/bind-tag-actions.js +6 -0
  53. monopyly/static/js/create-balance-chart.js +2 -2
  54. monopyly/static/js/create-category-chart.js +1 -1
  55. monopyly/static/js/load-more-transactions.js +27 -0
  56. monopyly/static/js/modules/expand-transaction.js +7 -6
  57. monopyly/static/js/modules/update-display-ajax.js +20 -1
  58. monopyly/static/js/update-transactions-display.js +8 -2
  59. monopyly/templates/banking/account_page.html +15 -16
  60. monopyly/templates/banking/account_summaries.html +2 -2
  61. monopyly/templates/banking/account_summary.html +1 -1
  62. monopyly/templates/banking/accounts_page.html +2 -2
  63. monopyly/templates/banking/transactions_table/table.html +3 -0
  64. monopyly/templates/banking/transactions_table/transactions.html +0 -1
  65. monopyly/templates/common/tag_tree.html +25 -0
  66. monopyly/templates/{credit → common}/tags_page.html +7 -3
  67. monopyly/templates/common/transactions_table/linked_bank_transaction.html +2 -2
  68. monopyly/templates/common/transactions_table/table.html +6 -0
  69. monopyly/templates/common/transactions_table/transactions.html +9 -15
  70. monopyly/templates/core/index.html +112 -101
  71. monopyly/templates/core/profile.html +1 -1
  72. monopyly/templates/credit/statement_page.html +2 -2
  73. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +1 -1
  74. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +3 -3
  75. monopyly/templates/credit/statement_summary.html +2 -2
  76. monopyly/templates/credit/transaction_submission_page.html +3 -3
  77. monopyly/templates/credit/transactions_page.html +19 -3
  78. monopyly/templates/credit/transactions_table/condensed_row_content.html +2 -3
  79. monopyly/templates/credit/transactions_table/expanded_row_content.html +5 -5
  80. monopyly/templates/credit/transactions_table/table.html +3 -0
  81. monopyly/templates/credit/transactions_table/transactions.html +0 -1
  82. monopyly/templates/layout.html +9 -4
  83. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/METADATA +12 -13
  84. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/RECORD +88 -87
  85. monopyly-1.6.0.dist-info/entry_points.txt +3 -0
  86. monopyly/cli/apps.py +0 -108
  87. monopyly/cli/launch.py +0 -135
  88. monopyly/config/__init__.py +0 -1
  89. monopyly/config/default_settings.py +0 -56
  90. monopyly/config/settings.py +0 -59
  91. monopyly/static/jquery-3.7.0.min.js +0 -2
  92. monopyly/templates/credit/tag_tree/subtag_tree.html +0 -22
  93. monopyly/templates/credit/tag_tree/tag_tree.html +0 -13
  94. monopyly-1.5.1.dist-info/entry_points.txt +0 -2
  95. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/WHEEL +0 -0
  96. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/licenses/COPYING +0 -0
  97. {monopyly-1.5.1.dist-info → monopyly-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,10 +2,8 @@
2
2
  Tools for building a common transaction interface.
3
3
  """
4
4
 
5
- from abc import abstractmethod
6
-
7
- from authanor.database.handler import DatabaseHandler, DatabaseViewHandler
8
- from flask import current_app
5
+ from dry_foundation.database.handler import DatabaseHandler, DatabaseViewHandler
6
+ from flask import abort, current_app
9
7
 
10
8
  from ..database.models import (
11
9
  BankAccountTypeView,
@@ -31,17 +29,27 @@ class TransactionHandler(DatabaseViewHandler):
31
29
  """
32
30
 
33
31
  @classmethod
34
- def _customize_entries_query(cls, query, criteria, column_orders):
32
+ def _customize_entries_query(
33
+ cls, query, criteria, column_orders, offset=None, limit=None
34
+ ):
35
35
  # Group transactions and order by transaction date
36
36
  query = query.group_by(cls.model.id)
37
- return super()._customize_entries_query(query, criteria, column_orders)
37
+ return super()._customize_entries_query(
38
+ query, criteria, column_orders, offset=offset, limit=limit
39
+ )
38
40
 
39
41
  @classmethod
40
- def _get_transactions(cls, criteria=None, sort_order="DESC"):
42
+ def _get_transactions(
43
+ cls, criteria=None, sort_order="DESC", offset=None, limit=None
44
+ ):
41
45
  # Specify transaction order
42
46
  column_orders = {cls.model.transaction_date: sort_order}
43
47
  entries = cls.get_entries(
44
- entry_ids=None, criteria=criteria, column_orders=column_orders
48
+ entry_ids=None,
49
+ criteria=criteria,
50
+ column_orders=column_orders,
51
+ offset=offset,
52
+ limit=limit,
45
53
  )
46
54
  return entries
47
55
 
@@ -76,7 +84,26 @@ class TransactionHandler(DatabaseViewHandler):
76
84
 
77
85
  @classmethod
78
86
  def update_entry(cls, entry_id, **field_values):
79
- """Update a transaction in the database."""
87
+ """
88
+ Update a transaction in the database.
89
+
90
+ Accept a mapping relating given inputs to database fields. This
91
+ mapping is used to update an existing transaction in the
92
+ database. All fields are sanitized prior to updating, and any
93
+ subtransactions are identified for individual processing.
94
+
95
+ Parameters
96
+ ----------
97
+ entry_id : int
98
+ The ID of the transaction to be updated.
99
+ **field_values :
100
+ Values for the fields to update in the transaction.
101
+
102
+ Returns
103
+ -------
104
+ transaction : database.models.BankTransaction
105
+ The saved transaction.
106
+ """
80
107
  # Extend the default method to account for subtransactions
81
108
  subtransactions_data = field_values.pop("subtransactions", None)
82
109
  transaction = super().update_entry(entry_id, **field_values)
@@ -100,6 +127,30 @@ class TransactionHandler(DatabaseViewHandler):
100
127
  # Flush to the database after all subtransactions have been added
101
128
  cls._db.session.flush()
102
129
 
130
+ @classmethod
131
+ def delete_entry(cls, entry_id):
132
+ """
133
+ Delete a transaction in the database given its ID.
134
+
135
+ Parameters
136
+ ----------
137
+ entry_id : int
138
+ The ID of the transaction to be deleted.
139
+
140
+ Notes
141
+ -----
142
+ This will also delete any internal transactions associated with
143
+ this transaction, since the internal transaction link no longer
144
+ exists.
145
+ """
146
+ internal_transaction = cls.get_entry(entry_id).internal_transaction
147
+ super().delete_entry(entry_id)
148
+ if internal_transaction:
149
+ cls._db.session.refresh(internal_transaction)
150
+ if len(internal_transaction.transaction_views) <= 1:
151
+ cls._db.session.delete(internal_transaction)
152
+ cls._db.session.flush()
153
+
103
154
 
104
155
  def get_linked_transaction(transaction):
105
156
  """
@@ -232,10 +283,10 @@ class TransactionTagHandler(DatabaseHandler, model=TransactionTag):
232
283
  return tags
233
284
 
234
285
  @classmethod
235
- def _filter_entries(cls, query, criteria):
286
+ def _filter_entries(cls, query, criteria, offset, limit):
236
287
  # Only get distinct tag entries
237
288
  query = query.distinct()
238
- return super()._filter_entries(query, criteria)
289
+ return super()._filter_entries(query, criteria, offset, limit)
239
290
 
240
291
  @classmethod
241
292
  def get_subtags(cls, tag):
@@ -359,6 +410,25 @@ class TransactionTagHandler(DatabaseHandler, model=TransactionTag):
359
410
  tag = cls._db.session.execute(query).scalar_one_or_none()
360
411
  return tag
361
412
 
413
+ @classmethod
414
+ def delete_entry(cls, entry_id):
415
+ """
416
+ Delete the tag in the database given its ID.
417
+
418
+ Parameters
419
+ ----------
420
+ entry_id : int
421
+ The ID of the tag to be deleted.
422
+ """
423
+ super().delete_entry(entry_id)
424
+
425
+ @classmethod
426
+ def _retrieve_authorized_manipulable_entry(cls, entry_id):
427
+ tag = super()._retrieve_authorized_manipulable_entry(entry_id)
428
+ if tag.user_id != cls.user_id:
429
+ abort(403, "The current user is not authorized to manipulate this tag.")
430
+ return tag
431
+
362
432
 
363
433
  def categorize(transactions):
364
434
  """
@@ -402,6 +472,11 @@ class CategoryTree:
402
472
  """
403
473
  Store a tree of categories.
404
474
 
475
+ The category tree is a tree of categorized subtransactions. Each
476
+ leaf of the tree represents a transaction tag and the
477
+ subtransactions that have been categorized according to that tag
478
+ (the category).
479
+
405
480
  Parameters
406
481
  ----------
407
482
  category : database.models.TransactionTag, str
@@ -470,9 +545,9 @@ class RootCategoryTree(CategoryTree):
470
545
  Given a subtransaction, add that subtransaction to the category
471
546
  tree according to its tags. If multiple tags exist at the same
472
547
  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.
548
+ branches), the subtransaction is determined to be "uncategorizable"
549
+ and the tag is listed only as a member of the root tree and not
550
+ as a member of any other subcategory tree.
476
551
 
477
552
  Parameters
478
553
  ----------
monopyly/core/actions.py CHANGED
@@ -1,16 +1,10 @@
1
1
  """Module describing logical core actions (to be used in routes)."""
2
2
 
3
- from datetime import datetime
4
-
5
3
  import markdown
6
4
 
7
5
 
8
- def get_timestamp():
9
- """Get a timestamp for backup filenames."""
10
- return datetime.now().strftime("%Y%m%d_%H%M%S")
11
-
12
-
13
6
  class MarkdownConverter:
7
+ """An object to convert Markdown to HTML."""
14
8
 
15
9
  replacements = {
16
10
  "src": [
@@ -78,7 +72,7 @@ def convert_readme_to_html_template(readme_path):
78
72
  '<div class="resource-links">'
79
73
  " <h2>Links</h2>"
80
74
  ' <p><a href="{{ url_for("core.story") }}">Story</a></p>'
81
- ' <p><a href="{{ url_for("core.credits") }}">Credits</a></p>'
75
+ ' <p><a href="{{ url_for("core.application_credits") }}">Credits</a></p>'
82
76
  "</div>"
83
77
  ),
84
78
  )
@@ -9,3 +9,5 @@ bp = Blueprint("core", __name__)
9
9
 
10
10
  # Import routes after defining blueprint to avoid circular imports
11
11
  from . import context_processors, errors, filters, routes
12
+
13
+ __all__ = ["context_processors", "errors", "filters", "routes"]
monopyly/core/filters.py CHANGED
@@ -2,8 +2,6 @@
2
2
  Filters defined for the application.
3
3
  """
4
4
 
5
- from math import floor, log10
6
-
7
5
  from .blueprint import bp
8
6
 
9
7
 
monopyly/core/routes.py CHANGED
@@ -74,7 +74,7 @@ def story():
74
74
 
75
75
 
76
76
  @bp.route("/credits")
77
- def credits():
77
+ def application_credits():
78
78
  return render_template("core/credits.html")
79
79
 
80
80
 
@@ -2,7 +2,7 @@
2
2
  Tools for interacting with credit accounts in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseHandler
5
+ from dry_foundation.database.handler import DatabaseHandler
6
6
 
7
7
  from ..database.models import CreditAccount
8
8
 
@@ -94,9 +94,8 @@ def get_potential_preceding_card(card):
94
94
  card_ids=(other_card.id,),
95
95
  )
96
96
  latest_statement = statements.first()
97
- if latest_statement:
98
- if latest_statement.balance > 0:
99
- return other_card
97
+ if latest_statement and latest_statement.balance > 0:
98
+ return other_card
100
99
  # Card does not meet all of these conditions
101
100
  return None
102
101
 
@@ -110,8 +109,8 @@ def transfer_credit_card_statement(form, card_id, prior_card_id):
110
109
  statements = CreditStatementHandler.get_statements(card_ids=(prior_card_id,))
111
110
  latest_statement = statements.first()
112
111
  CreditStatementHandler.update_entry(latest_statement.id, card_id=card_id)
113
- # Deactivate the old card
114
- prior_card = CreditCardHandler.get_entry(prior_card_id)
112
+ # Deactivate the old card (after ensuring it exists and is accessible)
113
+ CreditCardHandler.get_entry(prior_card_id)
115
114
  CreditCardHandler.update_entry(prior_card_id, active=0)
116
115
 
117
116
 
@@ -9,3 +9,5 @@ bp = Blueprint("credit", __name__, url_prefix="/credit")
9
9
 
10
10
  # Import routes after defining blueprint to avoid circular imports
11
11
  from . import routes
12
+
13
+ __all__ = ["routes"]
monopyly/credit/cards.py CHANGED
@@ -2,7 +2,7 @@
2
2
  Tools for interacting with credit cards in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseHandler
5
+ from dry_foundation.database.handler import DatabaseHandler
6
6
 
7
7
  from ..common.forms.utils import execute_on_form_validation
8
8
  from ..database.models import Bank, CreditAccount, CreditCard
@@ -99,8 +99,12 @@ class CreditCardHandler(DatabaseHandler, model=CreditCard):
99
99
  return card
100
100
 
101
101
  @classmethod
102
- def _customize_entries_query(cls, query, filters, sort_order):
103
- query = super()._customize_entries_query(query, filters, sort_order)
102
+ def _customize_entries_query(
103
+ cls, query, filters, sort_order, offset=None, limit=None
104
+ ):
105
+ query = super()._customize_entries_query(
106
+ query, filters, sort_order, offset=offset, limit=limit
107
+ )
104
108
  # Order cards by active status (active cards first)
105
109
  query = query.order_by(cls.model.active.desc())
106
110
  return query
monopyly/credit/forms.py CHANGED
@@ -2,20 +2,18 @@
2
2
  Generate credit card forms for the user to complete.
3
3
  """
4
4
 
5
- from werkzeug.exceptions import abort
5
+ from flask import abort
6
6
  from wtforms.fields import (
7
7
  BooleanField,
8
8
  FieldList,
9
9
  FormField,
10
10
  IntegerField,
11
11
  RadioField,
12
- StringField,
13
12
  SubmitField,
14
13
  )
15
14
  from wtforms.validators import DataRequired, Optional
16
15
 
17
- from ..banking.banks import BankHandler
18
- from ..banking.forms import BankSelectField, BankSubform
16
+ from ..banking.forms import BankSubform
19
17
  from ..common.forms import AcquisitionSubform, EntryForm, EntrySubform, TransactionForm
20
18
  from ..common.forms.fields import (
21
19
  CustomChoiceSelectField,
@@ -335,7 +333,7 @@ class CreditTransactionForm(TransactionForm):
335
333
  """Gather data for the form from the given database entry."""
336
334
  if isinstance(entry, CreditTransactionView):
337
335
  data = self._gather_transaction_data(entry)
338
- statement_info = entry.statement
336
+ statement_info = entry.statement_view
339
337
  elif isinstance(entry, (CreditCard, CreditStatementView)):
340
338
  data = {}
341
339
  statement_info = entry
monopyly/credit/routes.py CHANGED
@@ -2,11 +2,10 @@
2
2
  Routes for credit card financials.
3
3
  """
4
4
 
5
- from itertools import islice
6
-
5
+ from dry_foundation.database import db_transaction
7
6
  from flask import (
7
+ abort,
8
8
  flash,
9
- g,
10
9
  jsonify,
11
10
  redirect,
12
11
  render_template,
@@ -14,23 +13,18 @@ from flask import (
14
13
  session,
15
14
  url_for,
16
15
  )
17
- from fuisce.database import db_transaction
18
16
  from sqlalchemy.exc import MultipleResultsFound
19
- from werkzeug.exceptions import abort
20
- from wtforms.validators import ValidationError
21
17
 
22
18
  from ..auth.tools import login_required
23
19
  from ..banking.accounts import BankAccountHandler
24
20
  from ..banking.banks import BankHandler
25
- from ..banking.transactions import BankTransactionHandler
26
- from ..common.forms import form_err_msg
27
21
  from ..common.forms.utils import extend_field_list_for_ajax
28
22
  from ..common.transactions import (
29
23
  categorize,
30
24
  get_linked_transaction,
31
25
  highlight_unmatched_transactions,
32
26
  )
33
- from ..common.utils import dedelimit_float, parse_date, sort_by_frequency
27
+ from ..common.utils import dedelimit_float, parse_date
34
28
  from .accounts import CreditAccountHandler
35
29
  from .actions import (
36
30
  get_card_statement_grouping,
@@ -44,13 +38,16 @@ from .blueprint import bp
44
38
  from .cards import CreditCardHandler, save_card
45
39
  from .forms import CardStatementTransferForm, CreditCardForm, CreditTransactionForm
46
40
  from .statements import CreditStatementHandler
47
- from .transactions import CreditTagHandler, CreditTransactionHandler, save_transaction
41
+ from .transactions import CreditTransactionHandler, save_transaction
48
42
  from .transactions.activity import (
49
43
  ActivityMatchmaker,
50
44
  TransactionActivities,
51
45
  parse_transaction_activity_file,
52
46
  )
53
47
 
48
+ # Set a limit on the number of transactions loaded at one time for certain routes
49
+ TRANSACTION_LIMIT = 100
50
+
54
51
 
55
52
  @bp.route("/cards")
56
53
  @login_required
@@ -206,7 +203,7 @@ def load_statement_details(statement_id):
206
203
  return render_template(
207
204
  "credit/statement_page.html",
208
205
  statement=statement,
209
- statement_transactions=transactions,
206
+ transactions=transactions,
210
207
  bank_accounts=bank_accounts,
211
208
  chart_data=categories.assemble_chart_data(exclude=["Credit payments"]),
212
209
  )
@@ -261,7 +258,7 @@ def pay_credit_card(card_id, statement_id):
261
258
  bank_accounts=bank_accounts,
262
259
  )
263
260
  transactions_table_template = render_template(
264
- "credit/transactions_table/transactions.html",
261
+ "credit/transactions_table/table.html",
265
262
  transactions=transactions,
266
263
  )
267
264
  return jsonify((summary_template, transactions_table_template))
@@ -299,7 +296,7 @@ def load_statement_reconciliation_details(statement_id):
299
296
  return render_template(
300
297
  "credit/statement_reconciliation/statement_reconciliation_page.html",
301
298
  statement=statement,
302
- statement_transactions=transactions,
299
+ transactions=transactions,
303
300
  discrepant_records=matchmaker.match_discrepancies,
304
301
  discrepant_amount=abs(statement_transaction_balance - activities.total),
305
302
  unrecorded_activities=matchmaker.unmatched_activities,
@@ -321,6 +318,7 @@ def clear_reconciliation_info():
321
318
  "credit.update_transaction",
322
319
  "credit.infer_statement",
323
320
  "credit.suggest_transaction_autocomplete",
321
+ "credit.add_subtransaction_fields",
324
322
  "credit.delete_transaction",
325
323
  "static",
326
324
  None,
@@ -344,15 +342,57 @@ def load_transactions(card_id):
344
342
  # Get all of the user's transactions for the selected cards
345
343
  sort_order = "DESC"
346
344
  transactions = CreditTransactionHandler.get_transactions(
347
- card_ids=selected_card_ids,
348
- sort_order=sort_order,
349
- )
345
+ card_ids=selected_card_ids, sort_order=sort_order
346
+ ).all()
350
347
  return render_template(
351
348
  "credit/transactions_page.html",
352
349
  filter_cards=cards,
353
350
  selected_card_ids=selected_card_ids,
354
351
  sort_order=sort_order,
355
- transactions=islice(transactions, 100),
352
+ transactions=transactions[:TRANSACTION_LIMIT],
353
+ total_transactions=len(transactions),
354
+ )
355
+
356
+
357
+ @bp.route("/_extra_transactions", methods=("POST",))
358
+ @login_required
359
+ def load_more_transactions():
360
+ # Get info about the transactions being displayed from the AJAX request
361
+ post_args = request.get_json()
362
+ selected_card_ids = map(int, post_args["selected_card_ids"])
363
+ sort_order = "ASC" if post_args["sort_order"] == "asc" else "DESC"
364
+ block_index = post_args["block_count"] - 1
365
+ full_view = post_args["full_view"]
366
+ # Get a subset of the remaining transactions to load
367
+ more_transactions = CreditTransactionHandler.get_transactions(
368
+ card_ids=selected_card_ids,
369
+ sort_order=sort_order,
370
+ offset=block_index * TRANSACTION_LIMIT,
371
+ limit=TRANSACTION_LIMIT,
372
+ )
373
+ return render_template(
374
+ "credit/transactions_table/transactions.html",
375
+ transactions=more_transactions,
376
+ full_view=full_view,
377
+ )
378
+
379
+
380
+ @bp.route("/_update_transactions_display", methods=("POST",))
381
+ @login_required
382
+ def update_transactions_display():
383
+ # Separate the arguments of the POST method
384
+ post_args = request.get_json()
385
+ card_ids = map(int, post_args["card_ids"])
386
+ sort_order = "ASC" if post_args["sort_order"] == "asc" else "DESC"
387
+ # Filter selected transactions from the database
388
+ transactions = CreditTransactionHandler.get_transactions(
389
+ card_ids=card_ids, sort_order=sort_order, limit=100
390
+ )
391
+ return render_template(
392
+ "credit/transactions_table/table.html",
393
+ sort_order=sort_order,
394
+ transactions=transactions,
395
+ full_view=True,
356
396
  )
357
397
 
358
398
 
@@ -385,26 +425,6 @@ def show_linked_transaction():
385
425
  )
386
426
 
387
427
 
388
- @bp.route("/_update_transactions_display", methods=("POST",))
389
- @login_required
390
- def update_transactions_display():
391
- # Separate the arguments of the POST method
392
- post_args = request.get_json()
393
- card_ids = map(int, post_args["card_ids"])
394
- sort_order = "ASC" if post_args["sort_order"] == "asc" else "DESC"
395
- # Filter selected transactions from the database
396
- transactions = CreditTransactionHandler.get_transactions(
397
- card_ids=card_ids,
398
- sort_order=sort_order,
399
- )
400
- return render_template(
401
- "credit/transactions_table/transactions.html",
402
- sort_order=sort_order,
403
- transactions=islice(transactions, 100),
404
- full_view=True,
405
- )
406
-
407
-
408
428
  @bp.route(
409
429
  "/add_transaction",
410
430
  defaults={"card_id": None, "statement_id": None},
@@ -497,59 +517,17 @@ def add_subtransaction_fields():
497
517
  @db_transaction
498
518
  def delete_transaction(transaction_id):
499
519
  CreditTransactionHandler.delete_entry(transaction_id)
500
- if statement_id := session.pop("statement_focus", None):
501
- return redirect(
502
- url_for("credit.load_statement_details", statement_id=statement_id)
503
- )
520
+ if (statement_id := session.pop("statement_focus", None)) is not None:
521
+ statement = CreditStatementHandler.get_entry(statement_id)
522
+ # Delete the statement if it has no more transactions
523
+ if statement.balance is not None:
524
+ return redirect(
525
+ url_for("credit.load_statement_details", statement_id=statement.id)
526
+ )
527
+ CreditStatementHandler.delete_entry(statement.id)
504
528
  return redirect(url_for("credit.load_transactions"))
505
529
 
506
530
 
507
- @bp.route("/tags")
508
- @login_required
509
- def load_tags():
510
- # Get the tag hierarchy from the database
511
- hierarchy = CreditTagHandler.get_hierarchy()
512
- return render_template("credit/tags_page.html", tags_hierarchy=hierarchy)
513
-
514
-
515
- @bp.route("/_add_tag", methods=("POST",))
516
- @login_required
517
- @db_transaction
518
- def add_tag():
519
- # Get the new tag (and potentially parent category) from the AJAX request
520
- post_args = request.get_json()
521
- tag_name = post_args["tag_name"]
522
- parent_name = post_args.get("parent")
523
- # Check that the tag name does not already exist
524
- if CreditTagHandler.get_tags(tag_names=(tag_name,)):
525
- raise ValueError("The given tag name already exists. Tag names must be unique.")
526
- if parent_name:
527
- parent_id = CreditTagHandler.find_tag(parent_name).id
528
- else:
529
- parent_id = None
530
- tag = CreditTagHandler.add_entry(
531
- parent_id=parent_id,
532
- user_id=g.user.id,
533
- tag_name=tag_name,
534
- )
535
- return render_template(
536
- "credit/tag_tree/subtag_tree.html", tag=tag, tags_hierarchy={}
537
- )
538
-
539
-
540
- @bp.route("/_delete_tag", methods=("POST",))
541
- @login_required
542
- @db_transaction
543
- def delete_tag():
544
- # Get the tag to be deleted from the AJAX request
545
- post_args = request.get_json()
546
- tag_name = post_args["tag_name"]
547
- tag = CreditTagHandler.find_tag(tag_name)
548
- # Remove the tag from the database
549
- CreditTagHandler.delete_entry(tag.id)
550
- return ""
551
-
552
-
553
531
  @bp.route("/_suggest_transaction_autocomplete", methods=("POST",))
554
532
  @login_required
555
533
  def suggest_transaction_autocomplete():
@@ -2,8 +2,8 @@
2
2
  Tools for interacting with the credit statements in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseViewHandler
6
5
  from dateutil.relativedelta import relativedelta
6
+ from dry_foundation.database.handler import DatabaseViewHandler
7
7
 
8
8
  from ..common.utils import get_next_occurrence_of_day
9
9
  from ..database.models import (
@@ -3,3 +3,5 @@ Tools for interacting with the credit transactions in the database.
3
3
  """
4
4
 
5
5
  from ._transactions import CreditTagHandler, CreditTransactionHandler, save_transaction
6
+
7
+ __all__ = ["CreditTagHandler", "CreditTransactionHandler", "save_transaction"]
@@ -2,15 +2,12 @@
2
2
  Tools for interacting with the credit transactions in the database.
3
3
  """
4
4
 
5
- from authanor.database.handler import DatabaseHandler, DatabaseViewHandler
5
+ from dry_foundation.database.handler import DatabaseViewHandler
6
6
 
7
7
  from ...common.forms.utils import execute_on_form_validation
8
8
  from ...common.transactions import TransactionHandler, TransactionTagHandler
9
9
  from ...database.models import (
10
- Bank,
11
- CreditAccount,
12
10
  CreditCard,
13
- CreditStatementView,
14
11
  CreditSubtransaction,
15
12
  CreditTransaction,
16
13
  CreditTransactionView,
@@ -38,7 +35,13 @@ class CreditTransactionHandler(
38
35
  @classmethod
39
36
  @DatabaseViewHandler.view_query
40
37
  def get_transactions(
41
- cls, statement_ids=None, card_ids=None, active=None, sort_order="DESC"
38
+ cls,
39
+ statement_ids=None,
40
+ card_ids=None,
41
+ active=None,
42
+ sort_order="DESC",
43
+ offset=None,
44
+ limit=None,
42
45
  ):
43
46
  """
44
47
  Get credit card transactions from the database.
@@ -66,6 +69,13 @@ class CreditTransactionHandler(
66
69
  An indicator of whether the transactions should be ordered
67
70
  in ascending (oldest at top) or descending (newest at top)
68
71
  order.
72
+ offset : int, optional
73
+ The number of transactions by which to offset the results
74
+ returned by this query. The default is `None`, in which case
75
+ no offset will be added.
76
+ limit : int, optional
77
+ A limit on the number of transactions retrieved from the
78
+ database.
69
79
 
70
80
  Returns
71
81
  -------
@@ -77,7 +87,7 @@ class CreditTransactionHandler(
77
87
  criteria.add_match_filter(CreditCard, "id", card_ids)
78
88
  criteria.add_match_filter(CreditCard, "active", active)
79
89
  transactions = super()._get_transactions(
80
- criteria=criteria, sort_order=sort_order
90
+ criteria=criteria, sort_order=sort_order, offset=offset, limit=limit
81
91
  )
82
92
  return transactions
83
93
 
@@ -196,7 +206,7 @@ class CreditTagHandler(TransactionTagHandler, model=TransactionTagHandler.model)
196
206
  return tags
197
207
 
198
208
  @classmethod
199
- def _filter_entries(cls, query, criteria):
209
+ def _filter_entries(cls, query, criteria, offset, limit):
200
210
  # Add a join to enable filtering by transaction ID or subtransaction ID
201
211
  join_transaction = CreditTransactionView in criteria.discriminators
202
212
  join_subtransaction = (
@@ -206,7 +216,7 @@ class CreditTagHandler(TransactionTagHandler, model=TransactionTagHandler.model)
206
216
  query = query.join(credit_tag_link_table).join(CreditSubtransaction)
207
217
  if join_transaction:
208
218
  query = query.join(CreditTransactionView)
209
- return super()._filter_entries(query, criteria)
219
+ return super()._filter_entries(query, criteria, offset, limit)
210
220
 
211
221
 
212
222
  @execute_on_form_validation
@@ -1,3 +1,9 @@
1
1
  from .data import TransactionActivities
2
2
  from .parser import parse_transaction_activity_file
3
3
  from .reconciliation import ActivityMatchmaker
4
+
5
+ __all__ = [
6
+ "TransactionActivities",
7
+ "parse_transaction_activity_file",
8
+ "ActivityMatchmaker",
9
+ ]
@@ -5,7 +5,6 @@ from abc import ABC, abstractmethod
5
5
  from pathlib import Path
6
6
 
7
7
  from flask import abort, current_app
8
- from werkzeug.utils import secure_filename
9
8
 
10
9
  from ....common.utils import parse_date
11
10
  from .data import ActivityLoadingError, TransactionActivities, TransactionActivityLoader