monopyly 1.5.2__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 (87) hide show
  1. monopyly/CHANGELOG.md +15 -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 +3 -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 +1 -2
  16. monopyly/common/forms/fields.py +0 -2
  17. monopyly/common/forms/utils.py +1 -1
  18. monopyly/common/transactions.py +72 -7
  19. monopyly/core/actions.py +2 -1
  20. monopyly/core/blueprint.py +2 -0
  21. monopyly/core/filters.py +0 -2
  22. monopyly/core/routes.py +1 -1
  23. monopyly/credit/actions.py +4 -5
  24. monopyly/credit/blueprint.py +2 -0
  25. monopyly/credit/forms.py +3 -5
  26. monopyly/credit/routes.py +37 -62
  27. monopyly/credit/transactions/__init__.py +2 -0
  28. monopyly/credit/transactions/_transactions.py +1 -4
  29. monopyly/credit/transactions/activity/__init__.py +6 -0
  30. monopyly/credit/transactions/activity/parser.py +0 -1
  31. monopyly/credit/transactions/activity/reconciliation.py +5 -3
  32. monopyly/database/__init__.py +0 -3
  33. monopyly/database/models.py +63 -49
  34. monopyly/database/preloads.sql +6 -1
  35. monopyly/scripts/screenshot_application.py +100 -0
  36. monopyly/static/chartist-1.5.0.min.js +8 -0
  37. monopyly/static/css/style.css +35 -14
  38. monopyly/static/img/about/bank-account-details.png +0 -0
  39. monopyly/static/img/about/bank-account-summaries.png +0 -0
  40. monopyly/static/img/about/bank-accounts.png +0 -0
  41. monopyly/static/img/about/credit-account-details.png +0 -0
  42. monopyly/static/img/about/credit-statement-details.png +0 -0
  43. monopyly/static/img/about/credit-transactions.png +0 -0
  44. monopyly/static/img/about/homepage-user.png +0 -0
  45. monopyly/static/img/about/homepage.png +0 -0
  46. monopyly/static/jquery-3.7.1.min.js +2 -0
  47. monopyly/static/js/add-transfer.js +8 -9
  48. monopyly/static/js/bind-tag-actions.js +6 -0
  49. monopyly/static/js/create-balance-chart.js +1 -1
  50. monopyly/static/js/create-category-chart.js +1 -1
  51. monopyly/static/js/load-more-transactions.js +27 -0
  52. monopyly/static/js/modules/expand-transaction.js +7 -6
  53. monopyly/static/js/modules/update-display-ajax.js +20 -1
  54. monopyly/static/js/update-transactions-display.js +8 -2
  55. monopyly/templates/banking/account_page.html +15 -16
  56. monopyly/templates/banking/account_summaries.html +2 -2
  57. monopyly/templates/banking/account_summary.html +1 -1
  58. monopyly/templates/banking/accounts_page.html +2 -2
  59. monopyly/templates/banking/transactions_table/table.html +3 -0
  60. monopyly/templates/banking/transactions_table/transactions.html +0 -1
  61. monopyly/templates/common/tag_tree.html +25 -0
  62. monopyly/templates/{credit → common}/tags_page.html +7 -3
  63. monopyly/templates/common/transactions_table/linked_bank_transaction.html +2 -2
  64. monopyly/templates/common/transactions_table/table.html +6 -0
  65. monopyly/templates/common/transactions_table/transactions.html +9 -15
  66. monopyly/templates/core/index.html +112 -101
  67. monopyly/templates/core/profile.html +1 -1
  68. monopyly/templates/credit/statement_page.html +2 -2
  69. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_page.html +3 -3
  70. monopyly/templates/credit/statement_summary.html +2 -2
  71. monopyly/templates/credit/transaction_submission_page.html +3 -3
  72. monopyly/templates/credit/transactions_page.html +19 -3
  73. monopyly/templates/credit/transactions_table/condensed_row_content.html +2 -2
  74. monopyly/templates/credit/transactions_table/expanded_row_content.html +5 -5
  75. monopyly/templates/credit/transactions_table/table.html +3 -0
  76. monopyly/templates/credit/transactions_table/transactions.html +0 -1
  77. monopyly/templates/layout.html +9 -4
  78. {monopyly-1.5.2.dist-info → monopyly-1.6.0.dist-info}/METADATA +5 -5
  79. {monopyly-1.5.2.dist-info → monopyly-1.6.0.dist-info}/RECORD +83 -77
  80. monopyly-1.6.0.dist-info/entry_points.txt +3 -0
  81. monopyly/static/jquery-3.7.0.min.js +0 -2
  82. monopyly/templates/credit/tag_tree/subtag_tree.html +0 -22
  83. monopyly/templates/credit/tag_tree/tag_tree.html +0 -13
  84. monopyly-1.5.2.dist-info/entry_points.txt +0 -2
  85. {monopyly-1.5.2.dist-info → monopyly-1.6.0.dist-info}/WHEEL +0 -0
  86. {monopyly-1.5.2.dist-info → monopyly-1.6.0.dist-info}/licenses/COPYING +0 -0
  87. {monopyly-1.5.2.dist-info → monopyly-1.6.0.dist-info}/licenses/LICENSE +0 -0
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,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)
@@ -1,9 +1,7 @@
1
1
  import datetime
2
- from typing import List, Optional
3
2
 
4
3
  from dry_foundation.database.models import AuthorizedAccessMixin, Model
5
- from sqlalchemy import Column, Date, Float, ForeignKey, Integer, String, Table
6
- from sqlalchemy.ext.hybrid import hybrid_property
4
+ from sqlalchemy import Column, ForeignKey, Integer, Table
7
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
8
6
 
9
7
 
@@ -15,7 +13,7 @@ class User(Model):
15
13
  password: Mapped[str]
16
14
  # Relationships
17
15
  banks: Mapped["Bank"] = relationship(back_populates="user", cascade="all, delete")
18
- bank_account_types: Mapped["BankAccountTypeView"] = relationship(
16
+ bank_account_type_views: Mapped[list["BankAccountTypeView"]] = relationship(
19
17
  back_populates="user", viewonly=True
20
18
  )
21
19
 
@@ -25,16 +23,23 @@ class InternalTransaction(Model):
25
23
  # Columns
26
24
  id: Mapped[int] = mapped_column(primary_key=True)
27
25
  # Relationships
28
- bank_transactions: Mapped[List["BankTransactionView"]] = relationship(
26
+ bank_transactions: Mapped[list["BankTransaction"]] = relationship(
29
27
  back_populates="internal_transaction"
30
28
  )
31
- credit_transactions: Mapped[List["CreditTransactionView"]] = relationship(
29
+ bank_transaction_views: Mapped[list["BankTransactionView"]] = relationship(
30
+ back_populates="internal_transaction", viewonly=True
31
+ )
32
+ credit_transactions: Mapped[list["CreditTransaction"]] = relationship(
32
33
  back_populates="internal_transaction"
33
34
  )
35
+ credit_transaction_views: Mapped[list["CreditTransactionView"]] = relationship(
36
+ back_populates="internal_transaction", viewonly=True
37
+ )
34
38
 
39
+ @property
40
+ def transaction_views(self):
41
+ return self.bank_transaction_views + self.credit_transaction_views
35
42
 
36
- # Not sure why these tables are necessary, because they should already be reflected;
37
- # perhaps explore or wait until sqlalchemy 2.0
38
43
 
39
44
  bank_tag_link_table = Table(
40
45
  "bank_tag_links",
@@ -62,20 +67,21 @@ credit_tag_link_table = Table(
62
67
 
63
68
  class TransactionTag(AuthorizedAccessMixin, Model):
64
69
  __tablename__ = "transaction_tags"
70
+ _alt_authorized_ids = (0,)
65
71
  # Columns
66
72
  id: Mapped[int] = mapped_column(primary_key=True)
67
73
  user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
68
- parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("transaction_tags.id"))
74
+ parent_id: Mapped[int | None] = mapped_column(ForeignKey("transaction_tags.id"))
69
75
  tag_name: Mapped[str]
70
76
  # Relationships
71
77
  parent: Mapped["TransactionTag"] = relationship(
72
78
  back_populates="children", remote_side=[id]
73
79
  )
74
- children: Mapped[List["TransactionTag"]] = relationship(back_populates="parent")
75
- bank_subtransactions: Mapped[List["BankSubtransaction"]] = relationship(
80
+ children: Mapped[list["TransactionTag"]] = relationship(back_populates="parent")
81
+ bank_subtransactions: Mapped[list["BankSubtransaction"]] = relationship(
76
82
  back_populates="tags", secondary=bank_tag_link_table
77
83
  )
78
- credit_subtransactions: Mapped[List["CreditSubtransaction"]] = relationship(
84
+ credit_subtransactions: Mapped[list["CreditSubtransaction"]] = relationship(
79
85
  back_populates="tags", secondary=credit_tag_link_table
80
86
  )
81
87
 
@@ -95,10 +101,10 @@ class Bank(AuthorizedAccessMixin, Model):
95
101
  bank_name: Mapped[str]
96
102
  # Relationships
97
103
  user: Mapped["User"] = relationship(back_populates="banks")
98
- bank_accounts: Mapped[List["BankAccountView"]] = relationship(
104
+ bank_account_views: Mapped[list["BankAccountView"]] = relationship(
99
105
  back_populates="bank", cascade="all, delete", viewonly=True
100
106
  )
101
- credit_accounts: Mapped[List["CreditAccount"]] = relationship(
107
+ credit_accounts: Mapped[list["CreditAccount"]] = relationship(
102
108
  back_populates="bank", cascade="all, delete"
103
109
  )
104
110
 
@@ -110,7 +116,7 @@ class BankAccountType(AuthorizedAccessMixin, Model):
110
116
  id: Mapped[int] = mapped_column(primary_key=True)
111
117
  user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
112
118
  type_name: Mapped[str]
113
- type_abbreviation: Mapped[Optional[str]]
119
+ type_abbreviation: Mapped[str | None]
114
120
  # Relationships
115
121
  view: Mapped["BankAccountTypeView"] = relationship(
116
122
  back_populates="account_type", uselist=False, viewonly=True
@@ -124,13 +130,13 @@ class BankAccountTypeView(AuthorizedAccessMixin, Model):
124
130
  id = mapped_column(Integer, ForeignKey("bank_account_types.id"), primary_key=True)
125
131
  user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
126
132
  type_name: Mapped[str]
127
- type_abbreviation: Mapped[Optional[str]]
133
+ type_abbreviation: Mapped[str | None]
128
134
  type_common_name: Mapped[str]
129
135
  # Relationships
130
136
  account_type: Mapped["BankAccountType"] = relationship(back_populates="view")
131
- user: Mapped["User"] = relationship(back_populates="bank_account_types")
132
- accounts: Mapped[List["BankAccountView"]] = relationship(
133
- back_populates="account_type", viewonly=True
137
+ user: Mapped["User"] = relationship(back_populates="bank_account_type_views")
138
+ account_views: Mapped[list["BankAccountView"]] = relationship(
139
+ back_populates="account_type_view", viewonly=True
134
140
  )
135
141
 
136
142
 
@@ -166,12 +172,12 @@ class BankAccountView(AuthorizedAccessMixin, Model):
166
172
  projected_balance: Mapped[float]
167
173
  # Relationships
168
174
  account: Mapped["BankAccount"] = relationship(back_populates="view")
169
- bank: Mapped["Bank"] = relationship(back_populates="bank_accounts")
170
- account_type: Mapped["BankAccountTypeView"] = relationship(
171
- back_populates="accounts", viewonly=True
175
+ bank: Mapped["Bank"] = relationship(back_populates="bank_account_views")
176
+ account_type_view: Mapped["BankAccountTypeView"] = relationship(
177
+ back_populates="account_views", viewonly=True
172
178
  )
173
- transactions: Mapped[List["BankTransactionView"]] = relationship(
174
- back_populates="account", viewonly=True
179
+ transaction_views: Mapped[list["BankTransactionView"]] = relationship(
180
+ back_populates="account_view", viewonly=True
175
181
  )
176
182
 
177
183
 
@@ -182,16 +188,19 @@ class BankTransaction(AuthorizedAccessMixin, Model):
182
188
  subtype = "bank"
183
189
  # Columns
184
190
  id: Mapped[int] = mapped_column(primary_key=True)
185
- internal_transaction_id: Mapped[Optional[int]] = mapped_column(
191
+ internal_transaction_id: Mapped[int | None] = mapped_column(
186
192
  ForeignKey("internal_transactions.id")
187
193
  )
188
194
  account_id: Mapped[int] = mapped_column(ForeignKey("bank_accounts_view.id"))
189
195
  transaction_date: Mapped[datetime.date]
190
- merchant: Mapped[Optional[str]]
196
+ merchant: Mapped[str | None]
191
197
  # Relationships
192
198
  view: Mapped["BankTransactionView"] = relationship(
193
199
  back_populates="transaction", uselist=False, viewonly=True
194
200
  )
201
+ internal_transaction: Mapped["InternalTransaction"] = relationship(
202
+ back_populates="bank_transactions"
203
+ )
195
204
 
196
205
 
197
206
  class BankTransactionView(AuthorizedAccessMixin, Model):
@@ -203,23 +212,25 @@ class BankTransactionView(AuthorizedAccessMixin, Model):
203
212
  id: Mapped[int] = mapped_column(
204
213
  ForeignKey("bank_transactions.id"), primary_key=True
205
214
  )
206
- internal_transaction_id: Mapped[Optional[int]] = mapped_column(
215
+ internal_transaction_id: Mapped[int | None] = mapped_column(
207
216
  ForeignKey("internal_transactions.id")
208
217
  )
209
218
  account_id: Mapped[int] = mapped_column(ForeignKey("bank_accounts_view.id"))
210
219
  transaction_date: Mapped[datetime.date]
211
- merchant: Mapped[Optional[str]]
220
+ merchant: Mapped[str | None]
212
221
  total: Mapped[float]
213
- notes: Mapped[Optional[str]]
222
+ notes: Mapped[str | None]
214
223
  balance: Mapped[float]
215
224
  # Relationships
216
225
  transaction: Mapped["BankTransaction"] = relationship(back_populates="view")
217
226
  internal_transaction: Mapped["InternalTransaction"] = relationship(
218
- back_populates="bank_transactions"
227
+ back_populates="bank_transaction_views"
228
+ )
229
+ account_view: Mapped["BankAccountView"] = relationship(
230
+ back_populates="transaction_views", viewonly=True
219
231
  )
220
- account: Mapped["BankAccountView"] = relationship(back_populates="transactions")
221
- subtransactions: Mapped[List["BankSubtransaction"]] = relationship(
222
- back_populates="transaction", lazy="selectin"
232
+ subtransactions: Mapped[list["BankSubtransaction"]] = relationship(
233
+ back_populates="transaction_view", lazy="selectin", cascade="all, delete"
223
234
  )
224
235
 
225
236
 
@@ -232,10 +243,10 @@ class BankSubtransaction(AuthorizedAccessMixin, Model):
232
243
  subtotal: Mapped[float]
233
244
  note: Mapped[str]
234
245
  # Relationships
235
- transaction: Mapped["BankTransactionView"] = relationship(
246
+ transaction_view: Mapped["BankTransactionView"] = relationship(
236
247
  back_populates="subtransactions", viewonly=True
237
248
  )
238
- tags: Mapped[List["TransactionTag"]] = relationship(
249
+ tags: Mapped[list["TransactionTag"]] = relationship(
239
250
  back_populates="bank_subtransactions",
240
251
  secondary=bank_tag_link_table,
241
252
  lazy="selectin",
@@ -253,7 +264,7 @@ class CreditAccount(AuthorizedAccessMixin, Model):
253
264
  # ((Should probably have an 'active' field))
254
265
  # Relationships
255
266
  bank: Mapped["Bank"] = relationship(back_populates="credit_accounts")
256
- cards: Mapped[List["CreditCard"]] = relationship(
267
+ cards: Mapped[list["CreditCard"]] = relationship(
257
268
  back_populates="account", cascade="all, delete"
258
269
  )
259
270
 
@@ -268,7 +279,7 @@ class CreditCard(AuthorizedAccessMixin, Model):
268
279
  active: Mapped[int]
269
280
  # Relationships
270
281
  account: Mapped["CreditAccount"] = relationship(back_populates="cards")
271
- statements: Mapped[List["CreditStatementView"]] = relationship(
282
+ statement_views: Mapped[list["CreditStatementView"]] = relationship(
272
283
  back_populates="card", viewonly=True
273
284
  )
274
285
 
@@ -301,9 +312,9 @@ class CreditStatementView(AuthorizedAccessMixin, Model):
301
312
  payment_date: Mapped[datetime.date]
302
313
  # Relationships
303
314
  statement: Mapped["CreditStatement"] = relationship(back_populates="view")
304
- card: Mapped["CreditCard"] = relationship(back_populates="statements")
305
- transactions: Mapped[List["CreditTransactionView"]] = relationship(
306
- back_populates="statement", viewonly=True
315
+ card: Mapped["CreditCard"] = relationship(back_populates="statement_views")
316
+ transaction_views: Mapped[list["CreditTransactionView"]] = relationship(
317
+ back_populates="statement_view", viewonly=True
307
318
  )
308
319
 
309
320
 
@@ -314,7 +325,7 @@ class CreditTransaction(AuthorizedAccessMixin, Model):
314
325
  subtype = "credit"
315
326
  # Columns
316
327
  id: Mapped[int] = mapped_column(primary_key=True)
317
- internal_transaction_id: Mapped[Optional[int]] = mapped_column(
328
+ internal_transaction_id: Mapped[int | None] = mapped_column(
318
329
  ForeignKey("internal_transactions.id")
319
330
  )
320
331
  statement_id: Mapped[int] = mapped_column(ForeignKey("credit_statements_view.id"))
@@ -324,6 +335,9 @@ class CreditTransaction(AuthorizedAccessMixin, Model):
324
335
  view: Mapped["CreditTransactionView"] = relationship(
325
336
  back_populates="transaction", uselist=False, viewonly=True
326
337
  )
338
+ internal_transaction: Mapped["InternalTransaction"] = relationship(
339
+ back_populates="credit_transactions"
340
+ )
327
341
 
328
342
 
329
343
  class CreditTransactionView(AuthorizedAccessMixin, Model):
@@ -335,7 +349,7 @@ class CreditTransactionView(AuthorizedAccessMixin, Model):
335
349
  id: Mapped[int] = mapped_column(
336
350
  ForeignKey("credit_transactions.id"), primary_key=True
337
351
  )
338
- internal_transaction_id: Mapped[Optional[int]] = mapped_column(
352
+ internal_transaction_id: Mapped[int | None] = mapped_column(
339
353
  ForeignKey("internal_transactions.id")
340
354
  )
341
355
  statement_id: Mapped[int] = mapped_column(ForeignKey("credit_statements_view.id"))
@@ -348,13 +362,13 @@ class CreditTransactionView(AuthorizedAccessMixin, Model):
348
362
  back_populates="view", uselist=False
349
363
  )
350
364
  internal_transaction: Mapped["InternalTransaction"] = relationship(
351
- back_populates="credit_transactions"
365
+ back_populates="credit_transaction_views"
352
366
  )
353
- statement: Mapped["CreditStatementView"] = relationship(
354
- back_populates="transactions", viewonly=True
367
+ statement_view: Mapped["CreditStatementView"] = relationship(
368
+ back_populates="transaction_views", viewonly=True
355
369
  )
356
- subtransactions: Mapped[List["CreditSubtransaction"]] = relationship(
357
- back_populates="transaction", lazy="selectin"
370
+ subtransactions: Mapped[list["CreditSubtransaction"]] = relationship(
371
+ back_populates="transaction_view", lazy="selectin", cascade="all, delete"
358
372
  )
359
373
 
360
374
 
@@ -375,10 +389,10 @@ class CreditSubtransaction(AuthorizedAccessMixin, Model):
375
389
  subtotal: Mapped[float]
376
390
  note: Mapped[str]
377
391
  # Relationships
378
- transaction: Mapped["CreditTransactionView"] = relationship(
392
+ transaction_view: Mapped["CreditTransactionView"] = relationship(
379
393
  back_populates="subtransactions", viewonly=True
380
394
  )
381
- tags: Mapped[List["TransactionTag"]] = relationship(
395
+ tags: Mapped[list["TransactionTag"]] = relationship(
382
396
  back_populates="credit_subtransactions",
383
397
  secondary=credit_tag_link_table,
384
398
  lazy="selectin",
@@ -4,7 +4,12 @@ INSERT INTO users
4
4
  VALUES
5
5
  (0, 'global', 'n/a');
6
6
 
7
- /* Set some default account types (user_id=0 indicates the "global" user) */
7
+ /* Set a default transaction tag for credit payments */
8
+ INSERT INTO transaction_tags
9
+ (user_id, parent_id, tag_name)
10
+ VALUES (0, NULL, 'Credit payments');
11
+
12
+ /* Set some default account types */
8
13
  INSERT INTO bank_account_types
9
14
  (user_id, type_name, type_abbreviation)
10
15
  VALUES