monopyly 1.5.0__py3-none-any.whl → 1.5.2__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 (47) hide show
  1. monopyly/CHANGELOG.md +23 -0
  2. monopyly/README.md +2 -2
  3. monopyly/__init__.py +22 -27
  4. monopyly/_version.py +14 -2
  5. monopyly/auth/actions.py +12 -0
  6. monopyly/auth/routes.py +31 -22
  7. monopyly/auth/tools.py +1 -2
  8. monopyly/banking/accounts.py +3 -3
  9. monopyly/banking/banks.py +1 -1
  10. monopyly/banking/routes.py +1 -1
  11. monopyly/banking/transactions.py +14 -5
  12. monopyly/common/forms/utils.py +1 -2
  13. monopyly/common/transactions.py +17 -7
  14. monopyly/core/actions.py +0 -7
  15. monopyly/core/routes.py +0 -6
  16. monopyly/credit/accounts.py +1 -1
  17. monopyly/credit/cards.py +7 -3
  18. monopyly/credit/forms.py +1 -1
  19. monopyly/credit/routes.py +46 -25
  20. monopyly/credit/statements.py +1 -1
  21. monopyly/credit/transactions/_transactions.py +18 -5
  22. monopyly/credit/transactions/activity/parser.py +14 -6
  23. monopyly/credit/transactions/activity/reconciliation.py +20 -1
  24. monopyly/database/__init__.py +1 -56
  25. monopyly/database/models.py +181 -273
  26. monopyly/static/css/style.css +191 -11
  27. monopyly/static/js/create-balance-chart.js +1 -1
  28. monopyly/templates/auth/change_password.html +21 -0
  29. monopyly/templates/auth/login.html +3 -1
  30. monopyly/templates/auth/register.html +17 -7
  31. monopyly/templates/core/profile.html +2 -2
  32. monopyly/templates/credit/statement_reconciliation/statement_reconciliation_inquiry.html +1 -1
  33. monopyly/templates/credit/transaction_submission_page.html +64 -71
  34. monopyly/templates/credit/transactions_table/condensed_row_content.html +0 -1
  35. monopyly/templates/layout.html +2 -2
  36. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/METADATA +12 -13
  37. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/RECORD +41 -45
  38. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/WHEEL +1 -1
  39. monopyly-1.5.2.dist-info/entry_points.txt +2 -0
  40. monopyly/cli/apps.py +0 -108
  41. monopyly/cli/launch.py +0 -135
  42. monopyly/config/__init__.py +0 -1
  43. monopyly/config/default_settings.py +0 -56
  44. monopyly/config/settings.py +0 -59
  45. monopyly-1.5.0.dist-info/entry_points.txt +0 -2
  46. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/licenses/COPYING +0 -0
  47. {monopyly-1.5.0.dist-info → monopyly-1.5.2.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,10 @@
11
11
  --masthead-height: 50px;
12
12
  --masthead-color: #2b2b2b;
13
13
  --border-gray: #dddddd;
14
+ --button-background-base: #fbfbfb;
15
+ --button-background: linear-gradient(#fbfbfb, #efefef);
16
+ --button-block-background-base: #eeeeee;
17
+ --button-block-background: linear-gradient(#eeeeee, #e6e6e6);
14
18
  }
15
19
 
16
20
  html {
@@ -386,6 +390,35 @@ aside.sidebar {
386
390
  }
387
391
 
388
392
 
393
+ /*
394
+ * Provide styles for flashed messages
395
+ */
396
+ .flash.success,
397
+ .flash.warning,
398
+ .flash.error {
399
+ margin: 20px 0;
400
+ padding: 10px 20px;
401
+ border-radius: 10px;
402
+ box-shadow: 1px 1px 4px #ddd;
403
+ font-size: 0.9em;
404
+ }
405
+
406
+ .flash.success {
407
+ border: 1px solid #6fda6f;
408
+ background: linear-gradient(45deg, #e6efe1, #f5fef1);
409
+ }
410
+
411
+ .flash.warning {
412
+ border: 1px solid #dac96f;
413
+ background: linear-gradient(45deg, #efede1, #fefbf1);
414
+ }
415
+
416
+ .flash.error {
417
+ border: 1px solid #da6f6f;
418
+ background: linear-gradient(45deg, #f3e8e8, #fef1f1);
419
+ }
420
+
421
+
389
422
  /*
390
423
  * Provide styles for error pages
391
424
  */
@@ -459,8 +492,8 @@ aside.sidebar {
459
492
  border: 1px solid var(--border-gray);
460
493
  border-radius: 10px;
461
494
  box-shadow: 1px 1px 3px #bbbbbb;
462
- background-color: #eeeeee;
463
- background-image: linear-gradient(#eeeeee, #e6e6e6);
495
+ background-color: var(--button-block-background-base);
496
+ background-image: var(--button-block-background);
464
497
  color: inherit;
465
498
  }
466
499
 
@@ -546,15 +579,16 @@ aside.sidebar {
546
579
  left: 0;
547
580
  height: 100%;
548
581
  width: 100%;
582
+ display: flex;
583
+ flex-direction: column;
584
+ justify-content: center;
585
+ align-items: center;
549
586
  background-color: rgba(0, 0, 0, 0.75);
550
587
  z-index: 50; /* in front of content, but behind the header */
551
588
  }
552
589
 
553
590
  .modal {
554
- position: relative;
555
- top: 0%;
556
- left: 50%;
557
- transform: translate(-50%, 15%);
591
+ min-width: 200px;
558
592
  }
559
593
 
560
594
  .modal-box {
@@ -638,8 +672,8 @@ form input.button {
638
672
  align-items: center;
639
673
  margin-top: 20px;
640
674
  padding: 10px;
641
- background-color: #fbfbfb;
642
- background-image: linear-gradient(#fbfbfb, #efefef);
675
+ background-color: var(--button-background-base);
676
+ background-image: var(--button-background);
643
677
  color: #333333;
644
678
  font-weight:bold;
645
679
  text-align: center;
@@ -2564,6 +2598,21 @@ form .autocomplete-box .item.active {
2564
2598
  }
2565
2599
 
2566
2600
 
2601
+ /*
2602
+ * Customization for the 'Registration' page
2603
+ */
2604
+ .no-registration-notice {
2605
+ width: 60%;
2606
+ margin: 25px auto;
2607
+ padding: 10px;
2608
+ border: 1px solid var(--moneytree-leaves);
2609
+ border-radius: 10px;
2610
+ box-shadow: 0 0 10px var(--moneytree-leaves);
2611
+ background-color: #f5f5f5;
2612
+ text-align: center;
2613
+ }
2614
+
2615
+
2567
2616
  /*
2568
2617
  * Customization for the 'Profile' page
2569
2618
  */
@@ -3532,7 +3581,6 @@ form#pay #make-payment[type="submit"] #prompt {
3532
3581
  #statement-reconciliation.modal {
3533
3582
  width: 50%;
3534
3583
  min-width: 300px;
3535
- transform: translate(-50%, 100%);
3536
3584
  }
3537
3585
 
3538
3586
  #statement-reconciliation.modal .modal-box {
@@ -3604,13 +3652,145 @@ form#credit-transaction .merchant-field {
3604
3652
  /*
3605
3653
  * Customization for the transaction submission page
3606
3654
  */
3607
- #receipt {
3608
- width: 31%;
3655
+ #submission {
3656
+ display: flex;
3657
+ flex-direction: column;
3658
+ align-items: center;
3659
+ width: 50%;
3609
3660
  margin: auto;
3610
3661
  }
3662
+ @media screen and (max-width: 600px) {
3663
+ /* Mobile layout */
3664
+ #submission {
3665
+ width: 90%;
3666
+ }
3667
+ }
3668
+
3669
+ #submission-title {
3670
+ text-align: center;
3671
+ }
3672
+
3673
+ #receipt {
3674
+ width: 60%;
3675
+ min-width: 250px;
3676
+ max-width: 300px;
3677
+ margin: 40px auto;
3678
+ padding: 10px 20px 30px;
3679
+ box-shadow: 2px 2px 6px #bbbbbb;
3680
+ background-color: #fafafa;
3681
+ font-family: monospace;
3682
+ }
3683
+ @media screen and (max-width: 600px) {
3684
+ /* Mobile layout */
3685
+ #receipt {
3686
+ margin: 10px auto 30px;
3687
+ }
3688
+ }
3611
3689
 
3612
3690
  #receipt-title {
3691
+ margin-bottom: 20px;
3613
3692
  text-align: center;
3693
+ color: var(--silver-dollar);
3694
+ font-size: 10pt;
3695
+ font-weight: 400;
3696
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
3697
+ letter-spacing: 2px;
3698
+ text-transform: uppercase;
3699
+ }
3700
+
3701
+ #receipt .receipt-item {
3702
+ display: flex;
3703
+ margin: 15px 0;
3704
+ }
3705
+
3706
+ #receipt .receipt-item .receipt-key {
3707
+ font-weight: bold;
3708
+ }
3709
+
3710
+ #receipt #receipt-header {
3711
+ margin-bottom: 40px;
3712
+ }
3713
+
3714
+ #receipt #receipt-header #receipt-merchant {
3715
+ font-size: 14pt;
3716
+ justify-content: center;
3717
+ }
3718
+
3719
+ #receipt #receipt-header #receipt-date {
3720
+ font-size: 10pt;
3721
+ font-weight: normal;
3722
+ justify-content: center;
3723
+ }
3724
+
3725
+ #receipt .receipt-subtransaction {
3726
+ display: flex;
3727
+ gap: 10px 20px;
3728
+ margin: 10px 0;
3729
+ }
3730
+
3731
+ #receipt .receipt-subtransaction .note,
3732
+ #receipt #receipt-total .total {
3733
+ flex: 4;
3734
+ }
3735
+
3736
+ #receipt .receipt-subtransaction .amount,
3737
+ #receipt #receipt-total .amount {
3738
+ flex: 1;
3739
+ text-align: right;
3740
+ }
3741
+
3742
+ #receipt #receipt-card {
3743
+ margin-top: 40px;
3744
+ }
3745
+
3746
+ #receipt #receipt-card .receipt-item {
3747
+ flex-direction: column;
3748
+ }
3749
+
3750
+ #receipt #receipt-card .receipt-item .receipt-key {
3751
+ margin-bottom: 5px;
3752
+ }
3753
+
3754
+ #receipt #receipt-card .receipt-item .receipt-value {
3755
+ margin-left: 0;
3756
+ }
3757
+
3758
+ #submission-actions {
3759
+ display: flex;
3760
+ flex-direction: column;
3761
+ gap: 3px;
3762
+ align-items: center;
3763
+ width: 100%;
3764
+ max-width: 400px;
3765
+ }
3766
+ @media screen and (max-width: 600px) {
3767
+ /* Mobile layout */
3768
+ #submission-actions {
3769
+ gap: 5px;
3770
+ }
3771
+ }
3772
+
3773
+ #submission-actions .submission.button {
3774
+ width: 100%;
3775
+ padding: 3px 10px;
3776
+ border: 1px solid var(--border-gray);
3777
+ border-radius: 5px;
3778
+ background-color: var(--button-background-base);
3779
+ background-image: var(--button-background);;
3780
+ font-size: 12pt;
3781
+ text-decoration: none;
3782
+ box-sizing: border-box;
3783
+ }
3784
+ @media screen and (max-width: 600px) {
3785
+ /* Mobile layout */
3786
+ #submission-actions .submission.button {
3787
+ padding: 10px;
3788
+ font-size: 10pt;
3789
+ }
3790
+ }
3791
+
3792
+ #submission-actions .submission.button:hover {
3793
+ filter: brightness(0.98);
3614
3794
  }
3615
3795
 
3616
3796
 
@@ -50,7 +50,7 @@ function createBalanceChart(data) {
50
50
  const yLimit = Math.ceil(1.25*maxBalance/magnitudeOrder)*magnitudeOrder;
51
51
 
52
52
  let smoothLine = true;
53
- if (timestamps.length > 50) {
53
+ if (timestamps.length > 100) {
54
54
  smoothLine = false;
55
55
  }
56
56
 
@@ -0,0 +1,21 @@
1
+ {% extends 'layout.html' %}
2
+
3
+ {% block header %}
4
+ <h1>{% block title %}Change Password{% endblock %}</h1>
5
+ {% endblock %}
6
+
7
+ {% block content %}
8
+
9
+ <form id="change-password" method="post">
10
+ <p class="instructions">
11
+ Use this form to change the password for user: <b>{{ g.user.username }}</b>
12
+ </p>
13
+
14
+ <label for="password">Current Password</label>
15
+ <input type="password" name="current-password" id="current-password" required>
16
+ <label for="new-password">New Password</label>
17
+ <input type="password" name="new-password" id="new-password" required>
18
+ <input class="button" type="submit" value="Update" />
19
+ </form>
20
+
21
+ {% endblock %}
@@ -5,11 +5,13 @@
5
5
  {% endblock %}
6
6
 
7
7
  {% block content %}
8
- <form method="post">
8
+
9
+ <form id="log-in" method="post">
9
10
  <label for="username">Username</label>
10
11
  <input name="username" id="username" required>
11
12
  <label for="password">Password</label>
12
13
  <input type="password" name="password" id="password" required>
13
14
  <input class="button" type="submit" value="Log In" />
14
15
  </form>
16
+
15
17
  {% endblock %}
@@ -5,11 +5,21 @@
5
5
  {% endblock %}
6
6
 
7
7
  {% block content %}
8
- <form method="post">
9
- <label for="username">Username</label>
10
- <input name="username" id="username" required>
11
- <label for="password">Password</label>
12
- <input type="password" name="password" id="password" required>
13
- <input class="button" type="submit" value="Register" />
14
- </form>
8
+
9
+ {% if not config['REGISTRATION'] %}
10
+ <div class="no-registration-notice">
11
+ <b>Notice:</b> The app is not currently accepting new registrations.
12
+ </div>
13
+ {% endif %}
14
+
15
+ {% with input_status = 'required' if config['REGISTRATION'] else 'disabled' %}
16
+ <form id="register" method="post">
17
+ <label for="username">Username</label>
18
+ <input name="username" id="username" {{ input_status }}>
19
+ <label for="password">Password</label>
20
+ <input type="password" name="password" id="password" {{ input_status }}>
21
+ <input class="button" type="submit" value="Register" />
22
+ </form>
23
+ {% endwith %}
24
+
15
25
  {% endblock %}
@@ -25,8 +25,8 @@
25
25
  <div class=user-settings">
26
26
 
27
27
  <div class="password">
28
- <a style="color: gray;" href="{{ url_for('core.change_password') }}">
29
- Change password (coming soon)
28
+ <a style="color: gray;" href="{{ url_for('auth.change_password') }}">
29
+ Change password
30
30
  </a>
31
31
  </div>
32
32
 
@@ -8,7 +8,7 @@
8
8
  </div>
9
9
 
10
10
  <p class="instructions">
11
- Load a CSV file containing credit activity for comparison against this statement. The CSV file must be located in the <code>Downloads</code> directory on the machine running the Monopyly app.
11
+ Load a CSV file containing credit activity for comparison against this statement.
12
12
  </p>
13
13
 
14
14
  <form action="{{ url_for('credit.load_statement_reconciliation_details', statement_id=statement_id) }}" method="post" enctype="multipart/form-data">
@@ -12,83 +12,76 @@
12
12
 
13
13
  {% block content %}
14
14
 
15
- <div id="receipt">
15
+ <div id="submission">
16
16
 
17
- <p id="receipt-title">
17
+ <p id="submission-title">
18
18
  The transaction was saved successfully.
19
19
  </p>
20
- <br>
21
20
 
22
- <p>
23
- <b>Card:</b>
24
- {{ transaction.statement.card.account.bank.bank_name }} ****-{{ transaction.statement.card.last_four_digits }}
25
- </p>
26
-
27
- <p>
28
- <b>Date: </b>
29
- {{ transaction.transaction_date }}
30
- </p>
31
-
32
- <p>
33
- <b>Merchant: </b>
34
- {{ transaction.merchant }}
35
- </p>
36
-
37
- {% if subtransactions|length > 1 %}
38
- <p>
39
- <b>Total: </b>
40
- ${{ transaction.total|currency }}
41
- </p>
42
- {% endif %}
43
-
44
- {% for subtransaction in subtransactions %}
45
-
46
- <p>
47
- <b>Amount: </b>
48
- ${{ subtransaction['subtotal']|currency }}
49
- </p>
50
-
51
- <p>
52
- <b>Note: </b>
53
- {{ subtransaction['note'] }}
54
- </p>
55
-
56
- {% endfor %}
57
-
58
- <p>
59
- <b>Statement Date: </b>
60
- {{ transaction.statement.issue_date }}
61
- </p>
21
+ <div id="receipt">
22
+
23
+ <div id="receipt-header">
24
+ <h2 id="receipt-title">Transaction Submission</h2>
25
+ <h3 id="receipt-merchant" class="receipt-item">{{ transaction.merchant }}</h3>
26
+ <h4 id="receipt-date" class="receipt-item">{{ transaction.transaction_date }}</h4>
27
+ </div>
28
+
29
+ {% for subtransaction in subtransactions %}
30
+ <div class="receipt-subtransaction">
31
+ <div class="note">{{ subtransaction['note'] }}</div>
32
+ <div class="amount">${{ subtransaction['subtotal']|currency }}</div>
33
+ </div>
34
+ {% endfor %}
35
+
36
+ {% if subtransactions|length > 1 %}
37
+ <div id="receipt-total" class="receipt-item">
38
+ <div class="total receipt-key">Total:</div>
39
+ <div class="amount">${{ transaction.total|currency }}</div>
40
+ </div>
41
+ {% endif %}
42
+
43
+ <div id="receipt-card">
44
+ <div class="receipt-item">
45
+ <div class="receipt-key">Card:</div>
46
+ <div class="receipt-value">{{ transaction.statement.card.account.bank.bank_name }} ****-{{ transaction.statement.card.last_four_digits }}</div>
47
+ </div>
48
+ <div class="receipt-item">
49
+ <div class="receipt-key">Statement Date:</div>
50
+ <div class="receipt-value">{{ transaction.statement.issue_date }}</div>
51
+ </div>
52
+ </div>
53
+
54
+
55
+ </div>
62
56
 
63
57
  {% if g.user %}
64
- <br>
65
- <a href="{{ url_for('credit.load_statement_details', statement_id=transaction.statement_id) }}">
66
- See the statement for this transaction
67
- </a>
68
- <br>
69
- <a href="{{ url_for('credit.load_transactions') }}">
70
- See transaction history
71
- </a>
72
- <br>
73
- <a href="{{ url_for('credit.update_transaction', transaction_id=transaction.id) }}">
74
- Update this transaction
75
- </a>
76
- <br>
77
- <a href="{{ url_for('credit.add_transaction') }}">
78
- Create a new transaction
79
- </a>
80
- <br>
81
- <a href="{{ url_for('credit.add_transaction', card_id=transaction.statement.card_id, statement_id=transaction.statement_id) }}">
82
- Create a new transaction on this statement
83
- </a>
84
- {% with reconciliation_info = session.get('reconciliation_info', None) %}
85
- {% if reconciliation_info %}
86
- <br>
87
- <a href="{{ url_for('credit.load_statement_reconciliation_details', statement_id=reconciliation_info[0]) }}">
88
- Return to the in-progress statement reconciliation
89
- </a>
90
- {% endif %}
91
- {% endwith %}
58
+ <div id="submission-actions">
59
+ <a class="submission button" href="{{ url_for('credit.update_transaction', transaction_id=transaction.id) }}">
60
+ Update this transaction
61
+ </a>
62
+ <a class="submission button" href="{{ url_for('credit.add_transaction') }}">
63
+ Create a new transaction
64
+ </a>
65
+ <a class="submission button" href="{{ url_for('credit.add_transaction', card_id=transaction.statement.card_id, statement_id=transaction.statement_id) }}">
66
+ Create a new transaction on this statement
67
+ </a>
68
+ <a class="submission button" href="{{ url_for('credit.load_statement_details', statement_id=transaction.statement_id) }}">
69
+ See the statement for this transaction
70
+ </a>
71
+ <a class="submission button" href="{{ url_for('credit.load_statements') }}">
72
+ See statement history
73
+ </a>
74
+ <a class="submission button" href="{{ url_for('credit.load_transactions') }}">
75
+ See transaction history
76
+ </a>
77
+ {% with reconciliation_info = session.get('reconciliation_info', None) %}
78
+ {% if reconciliation_info %}
79
+ <a class="submission button" href="{{ url_for('credit.load_statement_reconciliation_details', statement_id=reconciliation_info[0]) }}">
80
+ Return to the in-progress statement reconciliation
81
+ </a>
82
+ {% endif %}
83
+ {% endwith %}
84
+ </div>
92
85
  {% endif %}
93
86
 
94
87
  </div>
@@ -23,4 +23,3 @@
23
23
  -<span class="digits">{{ transaction.statement.card.last_four_digits }}</span>
24
24
  </div>
25
25
  {% endif %}
26
-
@@ -91,8 +91,8 @@
91
91
  {% block header %}{% endblock %}
92
92
  </header>
93
93
 
94
- {% for message in get_flashed_messages() %}
95
- <div class="flash">{{ message }}</div>
94
+ {% for category, message in get_flashed_messages(with_categories=True) %}
95
+ <div class="flash {{ category }}">{{ message }}</div>
96
96
  {% endfor %}
97
97
 
98
98
  {% block content %}{% endblock %}
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: monopyly
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -23,16 +23,15 @@ Classifier: Topic :: Office/Business :: Financial
23
23
  Classifier: Topic :: Office/Business :: Financial :: Accounting
24
24
  Classifier: Topic :: Office/Business :: Financial :: Spreadsheet
25
25
  Requires-Python: <3.11,>=3.10
26
- Requires-Dist: authanor==1.1.0
27
- Requires-Dist: flask-wtf==1.2.1
28
- Requires-Dist: flask==3.0.3
29
- Requires-Dist: fuisce==1.0.2
30
- Requires-Dist: gunicorn==22.0.0
31
- Requires-Dist: markdown==3.6
32
- Requires-Dist: nltk==3.8.1
26
+ Requires-Dist: dry-foundation==1.3.0
27
+ Requires-Dist: flask-wtf==1.2.2
28
+ Requires-Dist: flask==3.1.2
29
+ Requires-Dist: gunicorn==23.0.0
30
+ Requires-Dist: markdown==3.9
31
+ Requires-Dist: nltk==3.9.1
33
32
  Requires-Dist: python-dateutil==2.9.0
34
- Requires-Dist: rich==13.7.1
35
- Requires-Dist: sqlalchemy==2.0.29
33
+ Requires-Dist: rich==14.1.0
34
+ Requires-Dist: sqlalchemy==2.0.43
36
35
  Description-Content-Type: text/markdown
37
36
 
38
37
  <div id="header">
@@ -64,10 +63,10 @@ The package requires a recent version of Python (3.10+).
64
63
 
65
64
  ## Getting started
66
65
 
67
- Once the package is properly installed, run the app in local mode from the command line (the default options should be sensible, but you may customize the host and port if necessary):
66
+ Once the package is properly installed, launch the app in local mode from the command line (the default options should be sensible, but you may customize the host and port if necessary):
68
67
 
69
68
  ```
70
- $ monopyly local --browser [--host HOST] [--port PORT]
69
+ $ monopyly launch local --browser [--host HOST] [--port PORT]
71
70
  ```
72
71
 
73
72
  Local mode indicates that the app is just going to be run using a locally hosted server, accessible to just your machine.