monopyly 1.5.2__py3-none-any.whl → 1.6.1__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 (94) hide show
  1. monopyly/CHANGELOG.md +22 -0
  2. monopyly/README.md +1 -1
  3. monopyly/__init__.py +1 -1
  4. monopyly/_version.py +2 -2
  5. monopyly/auth/blueprint.py +2 -0
  6. monopyly/auth/routes.py +1 -2
  7. monopyly/banking/accounts.py +4 -4
  8. monopyly/banking/actions.py +20 -17
  9. monopyly/banking/blueprint.py +2 -0
  10. monopyly/banking/filters.py +6 -6
  11. monopyly/banking/forms.py +7 -5
  12. monopyly/banking/routes.py +71 -9
  13. monopyly/banking/transactions.py +2 -3
  14. monopyly/common/forms/__init__.py +8 -0
  15. monopyly/common/forms/_forms.py +6 -16
  16. monopyly/common/forms/fields.py +0 -11
  17. monopyly/common/forms/utils.py +4 -4
  18. monopyly/common/transactions.py +72 -7
  19. monopyly/core/actions.py +2 -1
  20. monopyly/core/blueprint.py +2 -0
  21. monopyly/core/context_processors.py +3 -21
  22. monopyly/core/filters.py +0 -2
  23. monopyly/core/routes.py +1 -1
  24. monopyly/credit/actions.py +4 -5
  25. monopyly/credit/blueprint.py +2 -0
  26. monopyly/credit/cards.py +1 -1
  27. monopyly/credit/forms.py +9 -7
  28. monopyly/credit/routes.py +37 -62
  29. monopyly/credit/transactions/__init__.py +2 -0
  30. monopyly/credit/transactions/_transactions.py +1 -4
  31. monopyly/credit/transactions/activity/__init__.py +6 -0
  32. monopyly/credit/transactions/activity/parser.py +0 -1
  33. monopyly/credit/transactions/activity/reconciliation.py +5 -3
  34. monopyly/database/__init__.py +0 -3
  35. monopyly/database/models.py +63 -49
  36. monopyly/database/preloads.sql +6 -1
  37. monopyly/scripts/screenshot_application.py +100 -0
  38. monopyly/static/chartist-1.5.0.min.js +8 -0
  39. monopyly/static/css/style.css +39 -14
  40. monopyly/static/img/about/bank-account-details.png +0 -0
  41. monopyly/static/img/about/bank-account-summaries.png +0 -0
  42. monopyly/static/img/about/bank-accounts.png +0 -0
  43. monopyly/static/img/about/credit-account-details.png +0 -0
  44. monopyly/static/img/about/credit-statement-details.png +0 -0
  45. monopyly/static/img/about/credit-transactions.png +0 -0
  46. monopyly/static/img/about/homepage-user.png +0 -0
  47. monopyly/static/img/about/homepage.png +0 -0
  48. monopyly/static/jquery-3.7.1.min.js +2 -0
  49. monopyly/static/js/add-transfer.js +8 -9
  50. monopyly/static/js/bind-tag-actions.js +6 -0
  51. monopyly/static/js/create-balance-chart.js +1 -1
  52. monopyly/static/js/create-category-chart.js +1 -1
  53. monopyly/static/js/load-more-transactions.js +27 -0
  54. monopyly/static/js/modules/expand-transaction.js +7 -6
  55. monopyly/static/js/modules/update-display-ajax.js +20 -1
  56. monopyly/static/js/update-transactions-display.js +8 -2
  57. monopyly/templates/banking/account_form/account_form_page_new.html +10 -8
  58. monopyly/templates/banking/account_page.html +32 -33
  59. monopyly/templates/banking/account_summaries.html +2 -2
  60. monopyly/templates/banking/account_summary.html +1 -1
  61. monopyly/templates/banking/accounts_page.html +10 -8
  62. monopyly/templates/banking/transactions_table/table.html +3 -0
  63. monopyly/templates/banking/transactions_table/transactions.html +0 -1
  64. monopyly/templates/common/tag_tree.html +25 -0
  65. monopyly/templates/{credit → common}/tags_page.html +17 -13
  66. monopyly/templates/common/transactions_table/linked_bank_transaction.html +2 -2
  67. monopyly/templates/common/transactions_table/table.html +6 -0
  68. monopyly/templates/common/transactions_table/transactions.html +9 -15
  69. monopyly/templates/core/index.html +124 -109
  70. monopyly/templates/core/profile.html +19 -20
  71. monopyly/templates/credit/account_page.html +17 -17
  72. monopyly/templates/credit/card_form/card_form_page_new.html +8 -5
  73. monopyly/templates/credit/statement_page.html +45 -46
  74. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +22 -22
  75. monopyly/templates/credit/statement_summary.html +2 -2
  76. monopyly/templates/credit/statements_page.html +13 -13
  77. monopyly/templates/credit/transaction_form/transaction_form_page_update.html +9 -9
  78. monopyly/templates/credit/transaction_submission_page.html +3 -3
  79. monopyly/templates/credit/transactions_page.html +38 -22
  80. monopyly/templates/credit/transactions_table/condensed_row_content.html +2 -2
  81. monopyly/templates/credit/transactions_table/expanded_row_content.html +5 -5
  82. monopyly/templates/credit/transactions_table/table.html +3 -0
  83. monopyly/templates/credit/transactions_table/transactions.html +0 -1
  84. monopyly/templates/layout.html +16 -9
  85. {monopyly-1.5.2.dist-info → monopyly-1.6.1.dist-info}/METADATA +8 -8
  86. {monopyly-1.5.2.dist-info → monopyly-1.6.1.dist-info}/RECORD +90 -84
  87. monopyly-1.6.1.dist-info/entry_points.txt +3 -0
  88. monopyly/static/jquery-3.7.0.min.js +0 -2
  89. monopyly/templates/credit/tag_tree/subtag_tree.html +0 -22
  90. monopyly/templates/credit/tag_tree/tag_tree.html +0 -13
  91. monopyly-1.5.2.dist-info/entry_points.txt +0 -2
  92. {monopyly-1.5.2.dist-info → monopyly-1.6.1.dist-info}/WHEEL +0 -0
  93. {monopyly-1.5.2.dist-info → monopyly-1.6.1.dist-info}/licenses/COPYING +0 -0
  94. {monopyly-1.5.2.dist-info → monopyly-1.6.1.dist-info}/licenses/LICENSE +0 -0
@@ -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"]
@@ -2,8 +2,7 @@
2
2
  Filters defined for the application.
3
3
  """
4
4
 
5
- from datetime import date
6
- from importlib import import_module
5
+ from dry_foundation.utils import define_basic_template_global_variables
7
6
 
8
7
  from .actions import determine_summary_balance_svg_viewbox_width
9
8
  from .blueprint import bp
@@ -11,13 +10,8 @@ from .blueprint import bp
11
10
 
12
11
  @bp.app_context_processor
13
12
  def inject_global_template_variables():
14
- """Inject template variablees globally into the template context."""
15
- template_globals = {
16
- "app_version": _display_version(),
17
- "copyright_statement": f"© {date.today().year}",
18
- "date_today": date.today(),
19
- }
20
- return template_globals
13
+ """Inject template variables globally into the template context."""
14
+ return define_basic_template_global_variables("monopyly._version")
21
15
 
22
16
 
23
17
  @bp.app_context_processor
@@ -27,15 +21,3 @@ def inject_utility_functions():
27
21
  "calculate_summary_balance_width": determine_summary_balance_svg_viewbox_width,
28
22
  }
29
23
  return utility_functions
30
-
31
-
32
- def _display_version():
33
- """Show the version (without commit information)."""
34
- try:
35
- version = import_module("monopyly._version").version
36
- except ModuleNotFoundError:
37
- # Fallback action in case Hatch VCS fails
38
- display_version = ""
39
- else:
40
- display_version = version.split("+")[0]
41
- return display_version
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
 
@@ -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
@@ -137,7 +137,7 @@ def save_card(form, card_id=None):
137
137
 
138
138
  Parameters
139
139
  ----------
140
- form : flask_wtf.FlaskForm
140
+ form : CreditCardForm
141
141
  The form beign used to provide the data being saved.
142
142
  card_id : int
143
143
  The ID of hte card to be saved. If provided, the named card will
monopyly/credit/forms.py CHANGED
@@ -2,25 +2,23 @@
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
+ DateField,
8
9
  FieldList,
9
10
  FormField,
10
11
  IntegerField,
11
12
  RadioField,
12
- StringField,
13
13
  SubmitField,
14
14
  )
15
15
  from wtforms.validators import DataRequired, Optional
16
16
 
17
- from ..banking.banks import BankHandler
18
- from ..banking.forms import BankSelectField, BankSubform
17
+ from ..banking.forms import BankSubform
19
18
  from ..common.forms import AcquisitionSubform, EntryForm, EntrySubform, TransactionForm
20
19
  from ..common.forms.fields import (
21
20
  CustomChoiceSelectField,
22
21
  LastFourDigitsField,
23
- OptionalDateField,
24
22
  StringField,
25
23
  )
26
24
  from ..common.forms.utils import Autocompleter
@@ -154,6 +152,10 @@ class CardStatementTransferForm(EntryForm):
154
152
  transfer = RadioField("transfer", choices=[("yes", "Yes"), ("no", "No")])
155
153
  submit = SubmitField("Continue")
156
154
 
155
+ def gather_entry_data(self, entry):
156
+ """Gather data for the form from the given database entry."""
157
+ raise NotImplementedError
158
+
157
159
 
158
160
  class CreditTransactionForm(TransactionForm):
159
161
  """Form to input/edit credit card transactions."""
@@ -195,7 +197,7 @@ class CreditTransactionForm(TransactionForm):
195
197
  # Fields to identify the card/bank information for the statement
196
198
  card_info = FormField(CardSubform)
197
199
  # Fields pertaining to the statement
198
- issue_date = OptionalDateField("Statement Date")
200
+ issue_date = DateField("Statement Date", [Optional()])
199
201
 
200
202
  def get_statement(self, transaction_date):
201
203
  """Get the credit card statement described by the form data."""
@@ -335,7 +337,7 @@ class CreditTransactionForm(TransactionForm):
335
337
  """Gather data for the form from the given database entry."""
336
338
  if isinstance(entry, CreditTransactionView):
337
339
  data = self._gather_transaction_data(entry)
338
- statement_info = entry.statement
340
+ statement_info = entry.statement_view
339
341
  elif isinstance(entry, (CreditCard, CreditStatementView)):
340
342
  data = {}
341
343
  statement_info = entry
monopyly/credit/routes.py CHANGED
@@ -2,12 +2,10 @@
2
2
  Routes for credit card financials.
3
3
  """
4
4
 
5
- from itertools import islice
6
-
7
5
  from dry_foundation.database import db_transaction
8
6
  from flask import (
7
+ abort,
9
8
  flash,
10
- g,
11
9
  jsonify,
12
10
  redirect,
13
11
  render_template,
@@ -16,21 +14,17 @@ from flask import (
16
14
  url_for,
17
15
  )
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,
@@ -345,14 +342,38 @@ def load_transactions(card_id):
345
342
  # Get all of the user's transactions for the selected cards
346
343
  sort_order = "DESC"
347
344
  transactions = CreditTransactionHandler.get_transactions(
348
- card_ids=selected_card_ids, sort_order=sort_order, limit=100
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=transactions,
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,
356
377
  )
357
378
 
358
379
 
@@ -368,7 +389,7 @@ def update_transactions_display():
368
389
  card_ids=card_ids, sort_order=sort_order, limit=100
369
390
  )
370
391
  return render_template(
371
- "credit/transactions_table/transactions.html",
392
+ "credit/transactions_table/table.html",
372
393
  sort_order=sort_order,
373
394
  transactions=transactions,
374
395
  full_view=True,
@@ -507,52 +528,6 @@ def delete_transaction(transaction_id):
507
528
  return redirect(url_for("credit.load_transactions"))
508
529
 
509
530
 
510
- @bp.route("/tags")
511
- @login_required
512
- def load_tags():
513
- # Get the tag hierarchy from the database
514
- hierarchy = CreditTagHandler.get_hierarchy()
515
- return render_template("credit/tags_page.html", tags_hierarchy=hierarchy)
516
-
517
-
518
- @bp.route("/_add_tag", methods=("POST",))
519
- @login_required
520
- @db_transaction
521
- def add_tag():
522
- # Get the new tag (and potentially parent category) from the AJAX request
523
- post_args = request.get_json()
524
- tag_name = post_args["tag_name"]
525
- parent_name = post_args.get("parent")
526
- # Check that the tag name does not already exist
527
- if CreditTagHandler.get_tags(tag_names=(tag_name,)):
528
- raise ValueError("The given tag name already exists. Tag names must be unique.")
529
- if parent_name:
530
- parent_id = CreditTagHandler.find_tag(parent_name).id
531
- else:
532
- parent_id = None
533
- tag = CreditTagHandler.add_entry(
534
- parent_id=parent_id,
535
- user_id=g.user.id,
536
- tag_name=tag_name,
537
- )
538
- return render_template(
539
- "credit/tag_tree/subtag_tree.html", tag=tag, tags_hierarchy={}
540
- )
541
-
542
-
543
- @bp.route("/_delete_tag", methods=("POST",))
544
- @login_required
545
- @db_transaction
546
- def delete_tag():
547
- # Get the tag to be deleted from the AJAX request
548
- post_args = request.get_json()
549
- tag_name = post_args["tag_name"]
550
- tag = CreditTagHandler.find_tag(tag_name)
551
- # Remove the tag from the database
552
- CreditTagHandler.delete_entry(tag.id)
553
- return ""
554
-
555
-
556
531
  @bp.route("/_suggest_transaction_autocomplete", methods=("POST",))
557
532
  @login_required
558
533
  def suggest_transaction_autocomplete():
@@ -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 dry_foundation.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,
@@ -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
@@ -113,9 +113,11 @@ class _Matchmaker(ABC):
113
113
 
114
114
  def includes_activity(self, activity):
115
115
  for value in self.values():
116
- if activity == value:
117
- return True
118
- elif isinstance(value, TransactionActivityGroup) and activity in value:
116
+ same_activity = activity == value
117
+ same_group = (
118
+ isinstance(value, TransactionActivityGroup) and activity in value
119
+ )
120
+ if same_activity or same_group:
119
121
  return True
120
122
  return False
121
123
 
@@ -41,6 +41,3 @@ class SQLAlchemy(_SQLAlchemy):
41
41
  raw_conn.close()
42
42
  # Top level initialization does not overwrite tables, so it goes at the end
43
43
  super().initialize(app)
44
-
45
-
46
- SQLAlchemy.create_default_interface(echo_engine=False)