Compare commits

..

166 Commits

Author SHA1 Message Date
Herculino Trotta
74899f63ab Merge pull request #323
fix(locale): get_format doesn't override number formatting if use_l10n is None
2025-08-10 12:24:37 -03:00
Herculino Trotta
66a5e6d613 fix(locale): get_format doesn't override number formatting if use_l10n is None 2025-08-10 12:23:41 -03:00
Dimitri Decrock
e0ab32ec03 locale(Dutch): update translation
Currently translated at 100.0% (680 of 680 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-08-09 10:17:41 +00:00
eitchtee
a912e4a511 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-09 06:57:32 +00:00
Herculino Trotta
57ba672c91 Merge pull request #321
feat(accounts): add option for untracking accounts on a per user basis
2025-08-09 03:55:59 -03:00
Herculino Trotta
20c6989ffb fix(insights:emergency-fund): hide untracked accounts 2025-08-09 03:45:16 -03:00
Herculino Trotta
c6cd525c49 fix(insights): display untracked accounts on sankey by account 2025-08-09 03:42:21 -03:00
Herculino Trotta
55c4b920ee feat(accounts): add option for untracking accounts on a per user basis 2025-08-09 03:35:39 -03:00
google-labs-jules[bot]
7f8261b9cc refactor: Style transaction items for untracked accounts
This commit extends the "Untrack Account" feature by applying a special style to transaction items that belong to an untracked account.

- The transaction item template is modified to apply a "dimmed" style to transactions from untracked accounts.
- The styling follows the precedence: Account (untracked) > Category (muted) > Transaction (hidden).
- The dropdown menu for transaction items now shows "Controlled by account" if the transaction's account is untracked.
2025-08-09 05:47:18 +00:00
Herculino Trotta
9102654eab Merge pull request #320
style(swal): move swal prompt over tooltips
2025-08-08 19:01:54 -03:00
Herculino Trotta
1ff49a8a04 style(swal): move swal prompt over tooltips 2025-08-08 19:01:35 -03:00
Herculino Trotta
846dd1fd73 Merge remote-tracking branch 'origin/main' 2025-08-08 16:49:54 -03:00
Herculino Trotta
9eed3b6692 style(transactions): remove menu vertical positioning on smaller screens 2025-08-08 16:49:37 -03:00
Dimitri Decrock
b7c53a3c2d locale(Dutch): update translation
Currently translated at 99.7% (673 of 675 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-08-08 06:17:41 +00:00
Herculino Trotta
b378c8f6f7 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (675 of 675 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-08-08 04:17:41 +00:00
Herculino Trotta
ccc4deb1d8 Merge branch 'main' of https://github.com/eitchtee/WYGIWYH 2025-08-07 23:48:45 -03:00
Herculino Trotta
d3ecf55375 Merge remote-tracking branch 'weblate/main' 2025-08-07 23:43:25 -03:00
eitchtee
580f3e7345 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-08 02:41:32 +00:00
Herculino Trotta
0e5843094b Merge pull request #319
dev
2025-08-07 23:39:41 -03:00
Herculino Trotta
ed65945d19 feat(automatic-exchange-rates): rename automatic field 2025-08-07 23:39:21 -03:00
Herculino Trotta
18d8837c64 locale(Portuguese (Brazil)): update translation
Currently translated at 99.2% (671 of 676 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-08-08 02:37:06 +00:00
eitchtee
067d819077 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-08 02:37:02 +00:00
Herculino Trotta
bbaae4746a Merge pull request #318
feat(transactions:recurring): try to create transactions on update
2025-08-07 23:36:12 -03:00
Herculino Trotta
d2e5c1d6b3 feat(transactions:recurring): try to create transactions on update 2025-08-07 23:35:57 -03:00
eitchtee
ffef61d514 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-08 02:27:52 +00:00
Herculino Trotta
9020f6f972 Merge pull request #317
feat(automatic-exchange-rates): add "Single exchange rate" where only one exchange rate is added and updated to avoid db clutter
2025-08-07 23:26:10 -03:00
Herculino Trotta
540235c1b0 feat(automatic-exchange-rates): add "Single exchange rate" where only one exchange rate is added and updated to avoid db clutter 2025-08-07 23:25:51 -03:00
eitchtee
9070bc5705 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-07 20:04:56 +00:00
Herculino Trotta
ba5a6c9772 Merge pull request #316 from eitchtee/dev
feat(transactions): add menu itens for quickly changing transaction date
2025-08-07 17:04:11 -03:00
Herculino Trotta
2f33b5989f feat(transactions): add menu itens for quickly changing transaction date 2025-08-07 15:30:42 -03:00
Herculino Trotta
5f24d05540 Merge pull request #315
style(login): make login button take full width as open id login
2025-08-07 12:03:02 -03:00
Herculino Trotta
31cf62e277 style(login): make login button take full width as open id login 2025-08-07 12:02:41 -03:00
Dimitri Decrock
15d990007e locale(Dutch): update translation
Currently translated at 100.0% (670 of 670 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-08-07 06:17:41 +00:00
Herculino Trotta
3d5bc9cd3f Merge pull request #314
feat(tasks:check_for_updates): add env variable to disable checking
2025-08-06 16:59:15 -03:00
Herculino Trotta
a544dc4943 feat(tasks:check_for_updates): add env variable to disable checking 2025-08-06 16:58:58 -03:00
Herculino Trotta
b1178198e9 Merge remote-tracking branch 'origin/main' 2025-08-06 16:37:48 -03:00
Herculino Trotta
02a488bfff fix(sidebar): management menu gets hidden on my mobile by floating UI 2025-08-06 16:37:12 -03:00
Herculino Trotta
b05285947b locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (670 of 670 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-08-06 18:17:41 +00:00
eitchtee
d7b7dd28c7 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-06 16:21:44 +00:00
Herculino Trotta
9353d498ef Merge pull request #313
feat(transactions:recurring): allow to set how many future instances of a recurring transaction to create in advance
2025-08-06 13:20:58 -03:00
Herculino Trotta
4f6903e8e4 feat(transactions:recurring): Allow to set how many future instances of a recurring transaction to create in advance 2025-08-06 13:13:59 -03:00
eitchtee
7d3d6ea2fc chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-06 14:48:14 +00:00
Herculino Trotta
cce9c7a7a5 Merge pull request #312
refactor(transactions:all): reduce screen state by moving filter to top
2025-08-06 11:46:06 -03:00
Herculino Trotta
f80f74a01a refactor(transactions:all): reduce screen state by moving filter to top 2025-08-06 11:45:43 -03:00
Herculino Trotta
df47ffc49c Merge pull request #311
refactor(yearly): convert into a single page instead of two
2025-08-06 11:45:07 -03:00
eitchtee
4f35647a22 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-06 14:45:03 +00:00
Herculino Trotta
368342853f refactor(yearly): convert into a single page instead of two 2025-08-06 11:44:36 -03:00
Herculino Trotta
5a675f674d Merge pull request #310
refactor(networth): convert into a single page instead of two
2025-08-06 11:44:15 -03:00
Herculino Trotta
9ef8fdec49 refactor(networth): convert into a single page instead of two 2025-08-06 11:43:49 -03:00
Herculino Trotta
f29a8d8bc0 refactor: remove debug print from category overview 2025-08-06 11:42:53 -03:00
Herculino Trotta
8c43365ec0 Merge pull request #309
fix: bulk delete not working
2025-08-06 11:42:24 -03:00
Herculino Trotta
2cdcc4ee26 fix: bulk delete not working 2025-08-06 11:41:57 -03:00
eitchtee
84852012f9 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-06 14:36:50 +00:00
Herculino Trotta
edf0e2c66f Merge pull request #308
feat: replace navbar with sidebar
2025-08-06 11:35:58 -03:00
Herculino Trotta
f90a31f2b9 feat: add sidebar 2025-08-06 11:35:17 -03:00
Herculino Trotta
dd1f6a6ef2 feat: add sidebar 2025-08-04 22:10:44 -03:00
Herculino Trotta
57f98ba171 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (669 of 669 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-08-03 16:17:41 +00:00
eitchtee
f2e93f7df9 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-08-02 18:39:27 +00:00
Herculino Trotta
26cfa493b3 Merge pull request #305 from eitchtee/dev
fix(ui:transactions): transaction menu overflows screen on smaller screens
2025-08-02 15:37:30 -03:00
Herculino Trotta
c6e003ed86 fix(ui:transactions): transaction menu overflows screen on smaller screens 2025-08-02 15:36:57 -03:00
Herculino Trotta
c09ad0e49d Merge pull request #304
fix(ui:transactions): transaction menu overflows screen on smaller screens + alternative menu placement
2025-08-02 15:35:42 -03:00
Herculino Trotta
9250131396 fix(ui:transactions): transaction menu overflows screen on smaller screens + alternative menu placement 2025-08-02 15:35:20 -03:00
Herculino Trotta
5f503149ce Merge pull request #303
fix(mobile): tooltips show up on top of offcanvas
2025-08-02 15:34:18 -03:00
Herculino Trotta
d45b4f2942 fix(mobile): tooltips show up on top of offcanvas 2025-08-02 15:34:01 -03:00
Erwan Colin
4a8493c7d9 locale(French): update translation
Currently translated at 51.1% (342 of 669 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2025-07-28 22:17:41 +00:00
Dimitri Decrock
c39c3ccacb locale(Dutch): update translation
Currently translated at 100.0% (669 of 669 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-07-28 17:17:41 +00:00
Herculino Trotta
4bb8ef6582 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (669 of 669 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-07-28 04:17:41 +00:00
eitchtee
d711ccca69 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-28 02:21:41 +00:00
Herculino Trotta
76d59f1038 Merge pull request #302
refactor: properly name shareable objects generic fields
2025-07-27 23:20:01 -03:00
Herculino Trotta
5b6c123fa1 refactor: properly name shareable objects generic fields 2025-07-27 23:19:39 -03:00
Herculino Trotta
782ab11ae4 Merge pull request #301
feat: add quick transactions to django admin and properly name it
2025-07-27 23:19:01 -03:00
Herculino Trotta
8db885f47d feat: add quick transactions to django admin and properly name it
+ fix some wrongly assigned admin instances
2025-07-27 23:18:42 -03:00
Herculino Trotta
01bd8710d8 Merge pull request #300
feat(sharing): add action for turning shareable objects into public or private on Django Admin
2025-07-27 23:17:31 -03:00
Herculino Trotta
569d08711c feat(sharing): add action for turning shareable objects into public or private on Django Admin 2025-07-27 23:17:14 -03:00
eitchtee
a285f055e4 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-28 01:58:29 +00:00
Herculino Trotta
6aae9b1207 Merge pull request #299
feat(transactions): display more information about recurring, quick and installments
2025-07-27 22:56:26 -03:00
Herculino Trotta
9d2206f8a4 feat(transactions): display more information about recurring, quick and installments 2025-07-27 22:55:59 -03:00
Herculino Trotta
d7e3c50c2c Merge pull request #298
chore: bump python requirements
2025-07-27 22:55:27 -03:00
Herculino Trotta
789fd4eb80 chore: bump python requirements 2025-07-27 22:54:50 -03:00
Herculino Trotta
586b3a5d44 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (667 of 667 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-07-27 06:17:41 +00:00
eitchtee
9248e8bd77 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-27 04:41:52 +00:00
Herculino Trotta
c44247f6a5 Merge pull request #297
feat: automatically dismiss htmx error popups after 1 minute
2025-07-27 01:39:33 -03:00
Herculino Trotta
8ba89434f8 feat: automatically dismiss htmx error popups after 1 minute
this is mostly to avoid being greeted by a popup when you leave the app open and it is unable to auto-update every few minutes
2025-07-27 01:39:02 -03:00
eitchtee
f2f41981a3 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-27 04:29:40 +00:00
Herculino Trotta
1153fd6b0a Merge pull request #296
feat: check and notify users of new versions
2025-07-27 01:28:54 -03:00
Herculino Trotta
76822224a0 feat: check and notify users of new versions
checks are done against github's API with one request every 12 hours
2025-07-27 01:28:27 -03:00
Herculino Trotta
31b2b98eb9 Merge pull request #295 from eitchtee/dev
chore: bump python requirements
2025-07-27 01:26:51 -03:00
Herculino Trotta
d7a4e79321 chore: bump python requirements 2025-07-27 01:25:20 -03:00
Herculino Trotta
985f07e792 Merge pull request #294
fix(insights): filters not changing on click
2025-07-26 23:32:18 -03:00
Herculino Trotta
5465bb1eeb fix(insights): filters not changing on click 2025-07-26 23:31:54 -03:00
eitchtee
451a85a998 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-27 02:07:51 +00:00
Herculino Trotta
54c74e7c07 Merge pull request #293
fix(insights): filters not changing on click
2025-07-26 23:07:12 -03:00
Herculino Trotta
d6e9e123b7 fix(insights): filters not changing on click 2025-07-26 23:06:45 -03:00
seraphblade2010
80c9c43a02 locale(German): update translation
Currently translated at 98.0% (653 of 666 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/de/
2025-07-22 06:17:41 +00:00
afermar
3e34f088fc locale(Spanish): update translation
Currently translated at 19.6% (131 of 666 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2025-07-21 18:17:41 +00:00
afermar
5b9e5c6003 locale(Spanish): update translation
Currently translated at 19.5% (130 of 666 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2025-07-21 16:41:11 +00:00
eitchtee
c266b8809f chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-21 13:16:57 +00:00
Herculino Trotta
8cda4116bc Merge pull request #290
fix: FAB gets bellow hovered transaction
2025-07-21 10:16:14 -03:00
Herculino Trotta
c2510b2261 feat: allow to select if transfer transactions are muted or not on creation 2025-07-21 10:15:02 -03:00
Herculino Trotta
dcdaf756f9 fix: FAB gets bellow hovered transaction 2025-07-21 09:51:11 -03:00
Herculino Trotta
50ca08165a Merge pull request #289
fix: hover detection doesn't work with Firefox mobile
2025-07-21 09:39:13 -03:00
Herculino Trotta
f85618fa01 fix: hover detection doesn't work with Firefox mobile 2025-07-21 09:38:48 -03:00
Dimitri Decrock
635f87a8ad locale(Dutch): update translation
Currently translated at 100.0% (666 of 666 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-07-20 09:17:41 +00:00
Herculino Trotta
1a073ba53d Merge pull request #287
feat: improve ordering when searching existing transactions on DCA creation
2025-07-20 00:24:51 -03:00
Herculino Trotta
5412e5b12c feat: improve ordering when searching existing transactions on DCA creation 2025-07-20 00:24:31 -03:00
Herculino Trotta
2103ba1b38 Merge pull request #286
fix: DCA and other indicators too close together when there's more than one
2025-07-20 00:09:25 -03:00
Herculino Trotta
04fb15224c fix: DCA and other indicators too close together when there's more than one 2025-07-20 00:09:09 -03:00
Herculino Trotta
2fc526beac Merge pull request #285
docs: update README
2025-07-20 00:02:46 -03:00
Herculino Trotta
cc3ca4f4a3 docs: improve README 2025-07-20 00:02:11 -03:00
Herculino Trotta
8d3844c431 Merge pull request #284
feat: add FAB to networth and all transactions pages
2025-07-20 00:00:14 -03:00
Herculino Trotta
5e7e918085 feat: add FAB to networth and all transactions pages 2025-07-19 23:59:58 -03:00
Herculino Trotta
c3f02320b5 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (666 of 666 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-07-19 21:17:41 +00:00
eitchtee
da8bbbfb0b chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-19 19:24:50 +00:00
Herculino Trotta
e3f74538d2 Merge pull request #283
feat: quickly add an existing transaction as a model for quick transactions
2025-07-19 16:23:16 -03:00
Herculino Trotta
d8234950c6 feat: quickly add an existing transaction as a model for quick transactions
Use the dropdown menu on a transaction
2025-07-19 16:22:50 -03:00
Herculino Trotta
58f19ce1ca Merge pull request #282
feat: allow single transactions to be hidden from summaries

fix #274
2025-07-19 16:19:50 -03:00
Herculino Trotta
ef5f3580a0 feat: allow single transactions to be hidden from summaries
Control is done via the dropdown menu on a transaction item
Transfers are hidden by default

fix #274
2025-07-19 16:19:11 -03:00
eitchtee
efe0f99cb4 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-19 18:47:16 +00:00
Herculino Trotta
dccb5079ad Merge pull request #281
feat: add selected count to floating transactions bar
2025-07-19 15:45:55 -03:00
Herculino Trotta
6c90150661 feat: add selected count to floating transactions bar 2025-07-19 15:39:57 -03:00
eitchtee
c33d6fab69 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-19 17:48:14 +00:00
Herculino Trotta
c0c57a6d77 Merge pull request #280
feat: add dropdown to transaction menu and move duplication to there
2025-07-19 14:46:41 -03:00
Herculino Trotta
f19d58a2bd feat: add dropdown to transaction menu and move duplication to there 2025-07-19 14:46:23 -03:00
eitchtee
dfe99093e9 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-19 17:43:49 +00:00
Herculino Trotta
d737e573cc Merge pull request #279
refactor: improve transaction menu displaying
2025-07-19 14:43:10 -03:00
Herculino Trotta
805d3f419e refactor: improve transaction menu displaying
Use hover instead of hyperscript and events
2025-07-19 14:42:50 -03:00
Herculino Trotta
9777aac746 Merge pull request #278
feat: slightly opaque transactions if they're silenced
2025-07-18 23:35:20 -03:00
Herculino Trotta
61b782104d feat: slightly opaque transactions if they're silenced 2025-07-18 23:34:55 -03:00
eitchtee
79dec2b515 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-07-12 05:27:34 +00:00
Herculino Trotta
db23e162c4 Merge pull request #276
feat: auto-refresh overview and networth pages (10m for overview and 1h for networth)
2025-07-12 02:26:50 -03:00
Herculino Trotta
d81d89d9f6 feat: auto-refresh overview and networth pages (10m for overview and 1h for networth)
Useful if you want to leave WYGIWYH open in a fixed tab
2025-07-12 02:26:15 -03:00
Herculino Trotta
6826cfe79a Merge pull request #275
fix: backspacing on currency fields doesn't update mask
2025-07-09 20:27:40 -03:00
Herculino Trotta
0832ec75ca fix: visual bug when backspacing on the "amount" field 2025-07-09 20:24:27 -03:00
Dimitri Decrock
3090f632de locale(Dutch): update translation
Currently translated at 100.0% (661 of 661 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-06-30 06:17:41 +00:00
Herculino Trotta
6b4fbee7a6 Merge pull request #272
style: remove color from scrollbar
2025-06-29 17:39:12 -03:00
Herculino Trotta
e7fe6622cd style: remove color from scrollbar 2025-06-29 17:38:49 -03:00
Herculino Trotta
3017593ed5 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (661 of 661 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-06-29 19:34:02 +00:00
eitchtee
ceb8e9ea97 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-06-29 19:29:11 +00:00
Herculino Trotta
9b5b7683dd git: merge conflict with weblate 2025-06-29 16:27:15 -03:00
Herculino Trotta
514600e34a locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (661 of 661 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-06-29 17:42:29 +00:00
eitchtee
07dd805b07 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-06-29 17:42:26 +00:00
Herculino Trotta
905e9b4c54 Merge pull request #271
feat: allow loading any available datepicker languages
2025-06-29 14:41:03 -03:00
Herculino Trotta
60d367dec5 feat: allow loading any available datepicker languages
instead of a pre-configured list
2025-06-29 14:40:41 -03:00
Herculino Trotta
6e0842a697 Merge pull request #270
chore: bump npm dependencies
2025-06-29 14:11:03 -03:00
Herculino Trotta
858934b7c5 chore: bump npm dependencies 2025-06-29 14:10:38 -03:00
Herculino Trotta
47d9e4078c Merge pull request #269
chore: update npm dependencies
2025-06-29 12:11:37 -03:00
Herculino Trotta
fa6f3e87c0 chore: update npm dependencies 2025-06-29 12:11:17 -03:00
Herculino Trotta
5f101af879 Merge pull request #268
fix: broken distribution chart when number format is set to dot-comma
2025-06-29 01:32:07 -03:00
Herculino Trotta
b27633a28e fix: broken distribution chart when number format is set to dot-comma 2025-06-29 01:31:43 -03:00
eitchtee
7716eee0b3 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-06-29 04:15:12 +00:00
Herculino Trotta
37c447ae0a Merge pull request #267
style: improve the look of secondary navbar buttons (profile and calc)
2025-06-29 01:14:32 -03:00
Herculino Trotta
e544d7068b style: improve the look of secondary navbar buttons (profile and calc) 2025-06-29 01:14:06 -03:00
eitchtee
8d93da99c1 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-06-29 03:23:23 +00:00
Herculino Trotta
cc87477a2e Merge pull request #266
feat: add sounds volume control to user settings
2025-06-29 00:21:54 -03:00
Herculino Trotta
e86e0b8c08 feat: add sounds volume control to user settings 2025-06-29 00:21:32 -03:00
eitchtee
eb0c872c50 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2025-06-28 03:05:50 +00:00
Herculino Trotta
b4578df242 Merge pull request #265
feat: creating a quick transaction triggers the proper rule
2025-06-28 00:05:10 -03:00
Herculino Trotta
756de12835 feat: creating a quick transaction triggers the proper rule 2025-06-28 00:04:45 -03:00
ichi135
d573d02657 locale(Spanish): update translation
Currently translated at 17.6% (116 of 659 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2025-06-27 23:16:54 +00:00
Herculino Trotta
250b352217 Merge pull request #263
chore: update tailwind to v4
2025-06-21 16:13:06 -03:00
Herculino Trotta
b4e9446cf6 chore: update tailwind to v4
As is customary in the JS world EVERYTHING must break with each major version
2025-06-21 16:12:44 -03:00
Dimitri Decrock
90944f0179 locale(Dutch): update translation
Currently translated at 100.0% (659 of 659 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2025-06-21 16:16:54 +00:00
Herculino Trotta
008d34b1d0 Merge remote-tracking branch 'origin/main' 2025-06-21 10:55:56 -03:00
Herculino Trotta
46dfc7dad4 chore: update npm dependencies 2025-06-21 10:55:32 -03:00
Herculino Trotta
22900b5d9e locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (659 of 659 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2025-06-20 07:16:54 +00:00
Herculino Trotta
0c48e9fe3d docs: wrong OIDC callback url 2025-06-20 02:16:37 -03:00
152 changed files with 12646 additions and 8225 deletions

View File

@@ -29,15 +29,15 @@ Managing money can feel unnecessarily complex, but it doesnt have to be. WYGI
By sticking to this straightforward approach, you avoid dipping into your savings while still keeping tabs on where your money goes.
While this philosophy is simple, finding tools to make it work wasnt. I initially used a spreadsheet, which served me well for yearsuntil it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
While this philosophy is simple, finding tools to make it work wasnt. I initially used a spreadsheet, which served me well for years, until it became unwieldy as I started managing multiple currencies, accounts, and investments. I tried various financial management apps, but none met my key requirements:
1. **Multi-currency support** to track income and expenses in different currencies.
2. **Not a budgeting app** as I dislike budgeting constraints.
2. **Not a budgeting app** as I dislike budgeting constraints.
3. **Web app usability** (ideally with mobile support, though optional).
4. **Automation-ready API** to integrate with other tools and services.
5. **Custom transaction rules** for credit card billing cycles or similar quirks.
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH** an opinionated yet powerful tool that I believe will resonate with like-minded users.
Frustrated by the lack of comprehensive options, I set out to build **WYGIWYH**, an opinionated yet powerful tool that I believe will resonate with like-minded users.
# Key Features
@@ -140,9 +140,10 @@ To create the first user, open the container's console using Unraid's UI, by cli
| ENABLE_SOFT_DELETE | true\|false | false | Whether to enable transactions soft delete, if enabled, deleted transactions will remain in the database. Useful for imports and avoiding duplicate entries. |
| KEEP_DELETED_TRANSACTIONS_FOR | int | 365 | Time in days to keep soft deleted transactions for. If 0, will keep all transactions indefinitely. Only works if ENABLE_SOFT_DELETE is true. |
| TASK_WORKERS | int | 1 | How many workers to have for async tasks. One should be enough for most use cases |
| DEMO | true\|false | false | If demo mode is enabled. |
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
| DEMO | true\|false | false | If demo mode is enabled. |
| ADMIN_EMAIL | string | None | Automatically creates an admin account with this email. Must have `ADMIN_PASSWORD` also set. |
| ADMIN_PASSWORD | string | None | Automatically creates an admin account with this password. Must have `ADMIN_EMAIL` also set. |
| CHECK_FOR_UPDATES | bool | true | Check and notify users about new versions. The check is done by doing a single query to Github's API every 12 hours. |
## OIDC Configuration
@@ -165,7 +166,7 @@ To configure OIDC, you need to set the following environment variables:
When configuring your OIDC provider, you will need to provide a callback URL (also known as a Redirect URI). For WYGIWYH, the default callback URL is:
`https://your.wygiwyh.domain/daa/accounts/oidc/<OIDC_CLIENT_NAME>/login/callback/`
`https://your.wygiwyh.domain/auth/oidc/<OIDC_CLIENT_NAME>/login/callback/`
Replace `https://your.wygiwyh.domain` with the actual URL where your WYGIWYH instance is accessible. And `<OIDC_CLIENT_NAME>` with the slugfied value set in OIDC_CLIENT_NAME or the default `openid-connect` if you haven't set this variable.

View File

@@ -487,6 +487,8 @@ else:
CACHALOT_UNCACHABLE_TABLES = ("django_migrations", "procrastinate_jobs")
# Procrastinate
PROCRASTINATE_ON_APP_READY = "apps.common.procrastinate.on_app_ready"
# PWA
PWA_APP_NAME = SITE_TITLE
@@ -535,6 +537,7 @@ PWA_APP_SCREENSHOTS = [
PWA_SERVICE_WORKER_PATH = BASE_DIR / "templates" / "pwa" / "serviceworker.js"
ENABLE_SOFT_DELETE = os.getenv("ENABLE_SOFT_DELETE", "false").lower() == "true"
CHECK_FOR_UPDATES = os.getenv("CHECK_FOR_UPDATES", "true").lower() == "true"
KEEP_DELETED_TRANSACTIONS_FOR = int(os.getenv("KEEP_DELETED_ENTRIES_FOR", "365"))
APP_VERSION = os.getenv("APP_VERSION", "unknown")
DEMO = os.getenv("DEMO", "false").lower() == "true"

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.2.4 on 2025-07-28 02:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0014_alter_account_options_alter_accountgroup_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='account',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='account',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='account',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
migrations.AlterField(
model_name='accountgroup',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='accountgroup',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='accountgroup',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 5.2.4 on 2025-08-09 05:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0015_alter_account_owner_alter_account_shared_with_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='account',
name='untracked_by',
field=models.ManyToManyField(blank=True, related_name='untracked_accounts', to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -1,11 +1,11 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from apps.transactions.models import Transaction
from apps.common.middleware.thread_local import get_current_user
from apps.common.models import SharedObject, SharedObjectManager
from apps.transactions.models import Transaction
class AccountGroup(SharedObject):
@@ -62,6 +62,11 @@ class Account(SharedObject):
verbose_name=_("Archived"),
help_text=_("Archived accounts don't show up nor count towards your net worth"),
)
untracked_by = models.ManyToManyField(
settings.AUTH_USER_MODEL,
blank=True,
related_name="untracked_accounts",
)
objects = SharedObjectManager()
all_objects = models.Manager() # Unfiltered manager
@@ -75,6 +80,10 @@ class Account(SharedObject):
def __str__(self):
return self.name
def is_untracked_by(self):
user = get_current_user()
return self.untracked_by.filter(pk=user.pk).exists()
def clean(self):
super().clean()
if self.exchange_currency == self.currency:

View File

@@ -31,6 +31,11 @@ urlpatterns = [
views.account_take_ownership,
name="account_take_ownership",
),
path(
"account/<int:pk>/toggle-untracked/",
views.account_toggle_untracked,
name="account_toggle_untracked",
),
path("account-groups/", views.account_groups_index, name="account_groups_index"),
path("account-groups/list/", views.account_groups_list, name="account_groups_list"),
path("account-groups/add/", views.account_group_add, name="account_group_add"),

View File

@@ -155,6 +155,26 @@ def account_delete(request, pk):
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def account_toggle_untracked(request, pk):
account = get_object_or_404(Account, id=pk)
if account.is_untracked_by():
account.untracked_by.remove(request.user)
messages.success(request, _("Account is now tracked"))
else:
account.untracked_by.add(request.user)
messages.success(request, _("Account is now untracked"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "updated",
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])

View File

@@ -138,6 +138,7 @@ class RecurringTransactionSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
instance = super().update(instance, validated_data)
instance.update_unpaid_transactions()
instance.generate_upcoming_transactions()
return instance

View File

@@ -1,7 +1,26 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
@admin.action(description=_("Make public"))
def make_public(modeladmin, request, queryset):
queryset.update(visibility="public")
@admin.action(description=_("Make private"))
def make_private(modeladmin, request, queryset):
queryset.update(visibility="private")
class SharedObjectModelAdmin(admin.ModelAdmin):
actions = [make_public, make_private]
list_display = ("__str__", "visibility", "owner", "get_shared_with")
@admin.display(description=_("Shared with users"))
def get_shared_with(self, obj):
return ", ".join([p.email for p in obj.shared_with.all()])
def get_queryset(self, request):
# Use the all_objects manager to show all transactions, including deleted ones
return self.model.all_objects.all()

View File

@@ -1,4 +1,5 @@
from django.apps import AppConfig
from django.core.cache import cache
class CommonConfig(AppConfig):
@@ -18,3 +19,7 @@ class CommonConfig(AppConfig):
admin.site.unregister(SocialAccount)
admin.site.unregister(SocialApp)
admin.site.unregister(SocialToken)
# Delete the cache for update checks to prevent false-positives when the app is restarted
# this will be recreated by the check_for_updates task
cache.delete("update_check")

View File

@@ -5,7 +5,12 @@ from django.utils.formats import get_format as original_get_format
def get_format(format_type=None, lang=None, use_l10n=None):
user = get_current_user()
if user and user.is_authenticated and hasattr(user, "settings"):
if (
user
and user.is_authenticated
and hasattr(user, "settings")
and use_l10n is not False
):
user_settings = user.settings
if format_type == "THOUSAND_SEPARATOR":
number_format = getattr(user_settings, "number_format", None)

View File

@@ -36,12 +36,19 @@ class SharedObject(models.Model):
related_name="%(class)s_owned",
null=True,
blank=True,
verbose_name=_("Owner"),
)
visibility = models.CharField(
max_length=10, choices=Visibility.choices, default=Visibility.private
max_length=10,
choices=Visibility.choices,
default=Visibility.private,
verbose_name=_("Visibility"),
)
shared_with = models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name="%(class)s_shared", blank=True
settings.AUTH_USER_MODEL,
related_name="%(class)s_shared",
blank=True,
verbose_name=_("Shared with users"),
)
# Use as abstract base class

View File

@@ -0,0 +1,6 @@
import procrastinate
def on_app_ready(app: procrastinate.App):
"""This function is ran upon procrastinate initialization."""
...

View File

@@ -1,13 +1,17 @@
import logging
from packaging.version import parse as parse_version, InvalidVersion
from asgiref.sync import sync_to_async
from django.conf import settings
from django.core import management
from django.db import DEFAULT_DB_ALIAS
from django.core.cache import cache
from procrastinate import builtin_tasks
from procrastinate.contrib.django import app
import requests
logger = logging.getLogger(__name__)
@@ -79,3 +83,49 @@ def reset_demo_data(timestamp=None):
except Exception as e:
logger.exception(f"Error during daily demo data reset: {e}")
raise
@app.periodic(cron="0 */12 * * *") # Every 12 hours
@app.task(
name="check_for_updates",
)
def check_for_updates(timestamp=None):
if not settings.CHECK_FOR_UPDATES:
return "CHECK_FOR_UPDATES is disabled"
url = "https://api.github.com/repos/eitchtee/WYGIWYH/releases/latest"
try:
response = requests.get(url, timeout=60)
response.raise_for_status() # Raise an exception for bad status codes (4xx or 5xx)
data = response.json()
latest_version = data.get("tag_name")
if latest_version:
try:
current_v = parse_version(settings.APP_VERSION)
except InvalidVersion:
current_v = parse_version("0.0.0")
try:
latest_v = parse_version(latest_version)
except InvalidVersion:
latest_v = parse_version("0.0.0")
update_info = {
"update_available": False,
"current_version": str(current_v),
"latest_version": str(latest_v),
}
if latest_v > current_v:
update_info["update_available"] = True
# Cache the entire dictionary
cache.set("update_check", update_info, 60 * 60 * 25)
logger.info(f"Update check complete. Result: {update_info}")
else:
logger.warning("Could not find 'tag_name' in GitHub API response.")
except requests.exceptions.RequestException as e:
logger.error(f"Failed to fetch updates from GitHub: {e}")

View File

@@ -0,0 +1,17 @@
# core/templatetags/update_tags.py
from django import template
from django.core.cache import cache
register = template.Library()
@register.simple_tag
def get_update_check():
"""
Retrieves the update status dictionary from the cache.
Returns a default dictionary if nothing is found.
"""
return cache.get("update_check") or {
"update_available": False,
"latest_version": "N/A",
}

View File

@@ -37,7 +37,9 @@ class AirDatePickerInput(widgets.DateInput):
def _get_current_language():
"""Get current language code in format compatible with AirDatepicker"""
lang_code = translation.get_language()
# AirDatepicker uses simple language codes
# AirDatepicker uses simple language codes, except for pt-br
if lang_code.lower() == "pt-br":
return "pt-BR"
return lang_code.split("-")[0]
def _get_format(self):

View File

@@ -35,8 +35,8 @@ class ArbitraryDecimalDisplayNumberInput(forms.TextInput):
self.attrs.update(
{
"x-data": "",
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', "
f"'{get_format('THOUSAND_SEPARATOR')}', '30')",
"x-mask:dynamic": f"$money($input, '{get_format('DECIMAL_SEPARATOR')}', '{get_format('THOUSAND_SEPARATOR')}', '30')",
"x-on:keyup": "$el.dispatchEvent(new Event('input'))",
}
)

View File

@@ -203,21 +203,63 @@ class ExchangeRateFetcher:
if provider.rates_inverted:
# If rates are inverted, we need to swap currencies
ExchangeRate.objects.create(
from_currency=to_currency,
to_currency=from_currency,
rate=rate,
date=timezone.now(),
)
if service.singleton:
# Try to get the last automatically created exchange rate
exchange_rate = (
ExchangeRate.objects.filter(
automatic=True,
from_currency=to_currency,
to_currency=from_currency,
)
.order_by("-date")
.first()
)
else:
exchange_rate = None
if not exchange_rate:
ExchangeRate.objects.create(
automatic=True,
from_currency=to_currency,
to_currency=from_currency,
rate=rate,
date=timezone.now(),
)
else:
exchange_rate.rate = rate
exchange_rate.date = timezone.now()
exchange_rate.save()
processed_pairs.add((to_currency.id, from_currency.id))
else:
# If rates are not inverted, we can use them as is
ExchangeRate.objects.create(
from_currency=from_currency,
to_currency=to_currency,
rate=rate,
date=timezone.now(),
)
if service.singleton:
# Try to get the last automatically created exchange rate
exchange_rate = (
ExchangeRate.objects.filter(
automatic=True,
from_currency=from_currency,
to_currency=to_currency,
)
.order_by("-date")
.first()
)
else:
exchange_rate = None
if not exchange_rate:
ExchangeRate.objects.create(
automatic=True,
from_currency=from_currency,
to_currency=to_currency,
rate=rate,
date=timezone.now(),
)
else:
exchange_rate.rate = rate
exchange_rate.date = timezone.now()
exchange_rate.save()
processed_pairs.add((from_currency.id, to_currency.id))
service.last_fetch = timezone.now()

View File

@@ -114,6 +114,7 @@ class ExchangeRateServiceForm(forms.ModelForm):
"fetch_interval",
"target_currencies",
"target_accounts",
"singleton",
]
def __init__(self, *args, **kwargs):
@@ -126,6 +127,7 @@ class ExchangeRateServiceForm(forms.ModelForm):
"name",
"service_type",
Switch("is_active"),
Switch("singleton"),
"api_key",
Row(
Column("interval_type", css_class="form-group col-md-6"),

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.2.4 on 2025-08-08 02:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('currencies', '0014_alter_currency_options'),
]
operations = [
migrations.AddField(
model_name='exchangerate',
name='automatic',
field=models.BooleanField(default=False, verbose_name='Automatic'),
),
migrations.AddField(
model_name='exchangerateservice',
name='singleton',
field=models.BooleanField(default=False, help_text='Create one exchange rate and keep updating it. Avoids database clutter.', verbose_name='Single exchange rate'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.4 on 2025-08-08 02:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('currencies', '0015_exchangerate_automatic_exchangerateservice_singleton'),
]
operations = [
migrations.AlterField(
model_name='exchangerate',
name='automatic',
field=models.BooleanField(default=False, verbose_name='Auto'),
),
]

View File

@@ -70,6 +70,8 @@ class ExchangeRate(models.Model):
)
date = models.DateTimeField(verbose_name=_("Date and Time"))
automatic = models.BooleanField(verbose_name=_("Auto"), default=False)
class Meta:
verbose_name = _("Exchange Rate")
verbose_name_plural = _("Exchange Rates")
@@ -148,6 +150,14 @@ class ExchangeRateService(models.Model):
blank=True,
)
singleton = models.BooleanField(
verbose_name=_("Single exchange rate"),
default=False,
help_text=_(
"Create one exchange rate and keep updating it. Avoids database clutter."
),
)
class Meta:
verbose_name = _("Exchange Rate Service")
verbose_name_plural = _("Exchange Rate Services")

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.4 on 2025-07-28 02:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dca', '0003_dcastrategy_owner_dcastrategy_shared_with_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='dcastrategy',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='dcastrategy',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='dcastrategy',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
]

View File

@@ -13,7 +13,9 @@ from apps.insights.forms import (
)
def get_transactions(request, include_unpaid=True, include_silent=False):
def get_transactions(
request, include_unpaid=True, include_silent=False, include_untracked_accounts=False
):
transactions = Transaction.objects.all()
filter_type = request.GET.get("type", None)
@@ -91,6 +93,13 @@ def get_transactions(request, include_unpaid=True, include_silent=False):
transactions = transactions.filter(is_paid=True)
if not include_silent:
transactions = transactions.exclude(Q(category__mute=True) & ~Q(category=None))
transactions = transactions.exclude(
Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True)
)
if not include_untracked_accounts:
transactions = transactions.exclude(
account__in=request.user.untracked_accounts.all()
)
return transactions

View File

@@ -74,7 +74,7 @@ def index(request):
def sankey_by_account(request):
# Get filtered transactions
transactions = get_transactions(request)
transactions = get_transactions(request, include_untracked_accounts=True)
# Generate Sankey data
sankey_data = generate_sankey_data_by_account(transactions)
@@ -239,10 +239,14 @@ def late_transactions(request):
@login_required
@require_http_methods(["GET"])
def emergency_fund(request):
transactions_currency_queryset = Transaction.objects.filter(
is_paid=True, account__is_archived=False, account__is_asset=False
).order_by(
"account__currency__name",
transactions_currency_queryset = (
Transaction.objects.filter(
is_paid=True, account__is_archived=False, account__is_asset=False
)
.exclude(account__in=request.user.untracked_accounts.all())
.order_by(
"account__currency__name",
)
)
currency_net_worth = calculate_currency_totals(
transactions_queryset=transactions_currency_queryset, ignore_empty=False
@@ -260,7 +264,9 @@ def emergency_fund(request):
reference_date__gte=start_date,
reference_date__lte=end_date,
category__mute=False,
mute=False,
)
.exclude(account__in=request.user.untracked_accounts.all())
.values("reference_date", "account__currency")
.annotate(monthly_total=Sum("amount"))
)

View File

@@ -107,9 +107,15 @@ def transactions_list(request, month: int, year: int):
@require_http_methods(["GET"])
def monthly_summary(request, month: int, year: int):
# Base queryset with all required filters
base_queryset = Transaction.objects.filter(
reference_date__year=year, reference_date__month=month, account__is_asset=False
).exclude(Q(category__mute=True) & ~Q(category=None))
base_queryset = (
Transaction.objects.filter(
reference_date__year=year,
reference_date__month=month,
account__is_asset=False,
)
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
)
data = calculate_currency_totals(base_queryset, ignore_empty=True)
percentages = calculate_percentage_distribution(data)
@@ -143,7 +149,7 @@ def monthly_account_summary(request, month: int, year: int):
base_queryset = Transaction.objects.filter(
reference_date__year=year,
reference_date__month=month,
).exclude(Q(category__mute=True) & ~Q(category=None))
).exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
account_data = calculate_account_totals(transactions_queryset=base_queryset.all())
account_percentages = calculate_percentage_distribution(account_data)
@@ -165,10 +171,14 @@ def monthly_account_summary(request, month: int, year: int):
@require_http_methods(["GET"])
def monthly_currency_summary(request, month: int, year: int):
# Base queryset with all required filters
base_queryset = Transaction.objects.filter(
reference_date__year=year,
reference_date__month=month,
).exclude(Q(category__mute=True) & ~Q(category=None))
base_queryset = (
Transaction.objects.filter(
reference_date__year=year,
reference_date__month=month,
)
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
)
currency_data = calculate_currency_totals(base_queryset.all(), ignore_empty=True)
currency_percentages = calculate_percentage_distribution(currency_data)

View File

@@ -5,4 +5,5 @@ from . import views
urlpatterns = [
path("net-worth/current/", views.net_worth_current, name="net_worth_current"),
path("net-worth/projected/", views.net_worth_projected, name="net_worth_projected"),
path("net-worth/", views.net_worth, name="net_worth"),
]

View File

@@ -2,7 +2,7 @@ import json
from django.contrib.auth.decorators import login_required
from django.core.serializers.json import DjangoJSONEncoder
from django.shortcuts import render
from django.shortcuts import render, redirect
from django.views.decorators.http import require_http_methods
from apps.net_worth.utils.calculate_net_worth import (
@@ -18,18 +18,42 @@ from apps.transactions.utils.calculations import (
@login_required
@require_http_methods(["GET"])
def net_worth_current(request):
transactions_currency_queryset = Transaction.objects.filter(
is_paid=True, account__is_archived=False
).order_by(
"account__currency__name",
)
transactions_account_queryset = Transaction.objects.filter(
is_paid=True, account__is_archived=False
).order_by(
"account__group__name",
"account__name",
)
def net_worth(request):
if "view_type" in request.GET:
print(request.GET["view_type"])
view_type = request.GET["view_type"]
request.session["networth_view_type"] = view_type
else:
view_type = request.session.get("networth_view_type", "current")
if view_type == "current":
transactions_currency_queryset = (
Transaction.objects.filter(is_paid=True, account__is_archived=False)
.order_by(
"account__currency__name",
)
.exclude(account__in=request.user.untracked_accounts.all())
)
transactions_account_queryset = Transaction.objects.filter(
is_paid=True, account__is_archived=False
).order_by(
"account__group__name",
"account__name",
)
else:
transactions_currency_queryset = (
Transaction.objects.filter(account__is_archived=False)
.order_by(
"account__currency__name",
)
.exclude(account__in=request.user.untracked_accounts.all())
)
transactions_account_queryset = Transaction.objects.filter(
account__is_archived=False
).order_by(
"account__group__name",
"account__name",
)
currency_net_worth = calculate_currency_totals(
transactions_queryset=transactions_currency_queryset, deep_search=True
@@ -116,111 +140,22 @@ def net_worth_current(request):
"currencies": currencies,
"chart_data_accounts_json": chart_data_accounts_json,
"accounts": accounts,
"type": "current",
"type": view_type,
},
)
@login_required
@require_http_methods(["GET"])
def net_worth_current(request):
request.session["networth_view_type"] = "current"
return redirect("net_worth")
@login_required
@require_http_methods(["GET"])
def net_worth_projected(request):
transactions_currency_queryset = Transaction.objects.filter(
account__is_archived=False
).order_by(
"account__currency__name",
)
transactions_account_queryset = Transaction.objects.filter(
account__is_archived=False
).order_by(
"account__group__name",
"account__name",
)
request.session["networth_view_type"] = "projected"
currency_net_worth = calculate_currency_totals(
transactions_queryset=transactions_currency_queryset, deep_search=True
)
account_net_worth = calculate_account_totals(
transactions_queryset=transactions_account_queryset
)
historical_currency_net_worth = calculate_historical_currency_net_worth(
queryset=transactions_currency_queryset
)
labels = (
list(historical_currency_net_worth.keys())
if historical_currency_net_worth
else []
)
currencies = (
list(historical_currency_net_worth[labels[0]].keys())
if historical_currency_net_worth
else []
)
datasets = []
for i, currency in enumerate(currencies):
data = [
float(month_data[currency])
for month_data in historical_currency_net_worth.values()
]
datasets.append(
{
"label": currency,
"data": data,
"yAxisID": f"y{i}",
"fill": False,
"tension": 0.1,
}
)
chart_data_currency = {"labels": labels, "datasets": datasets}
chart_data_currency_json = json.dumps(chart_data_currency, cls=DjangoJSONEncoder)
historical_account_balance = calculate_historical_account_balance(
queryset=transactions_account_queryset
)
labels = (
list(historical_account_balance.keys()) if historical_account_balance else []
)
accounts = (
list(historical_account_balance[labels[0]].keys())
if historical_account_balance
else []
)
datasets = []
for i, account in enumerate(accounts):
data = [
float(month_data[account])
for month_data in historical_account_balance.values()
]
datasets.append(
{
"label": account,
"data": data,
"fill": False,
"tension": 0.1,
"yAxisID": f"y-axis-{i}", # Assign each dataset to its own Y-axis
}
)
chart_data_accounts = {"labels": labels, "datasets": datasets}
chart_data_accounts_json = json.dumps(chart_data_accounts, cls=DjangoJSONEncoder)
return render(
request,
"net_worth/net_worth.html",
{
"currency_net_worth": currency_net_worth,
"account_net_worth": account_net_worth,
"chart_data_currency_json": chart_data_currency_json,
"currencies": currencies,
"chart_data_accounts_json": chart_data_accounts_json,
"accounts": accounts,
"type": "projected",
},
)
return redirect("net_worth")

View File

@@ -0,0 +1,31 @@
# Generated by Django 5.2.4 on 2025-07-28 02:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('rules', '0013_transactionrule_on_delete'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='transactionrule',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='transactionrule',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='transactionrule',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
]

View File

@@ -7,6 +7,7 @@ from apps.transactions.models import (
InstallmentPlan,
RecurringTransaction,
TransactionEntity,
QuickTransaction,
)
from apps.common.admin import SharedObjectModelAdmin
@@ -49,19 +50,22 @@ class TransactionInline(admin.TabularInline):
@admin.register(InstallmentPlan)
class InstallmentPlanAdmin(SharedObjectModelAdmin):
class InstallmentPlanAdmin(admin.ModelAdmin):
inlines = [
TransactionInline,
]
@admin.register(RecurringTransaction)
class RecurringTransactionAdmin(SharedObjectModelAdmin):
class RecurringTransactionAdmin(admin.ModelAdmin):
inlines = [
TransactionInline,
]
admin.site.register(QuickTransaction)
@admin.register(TransactionCategory)
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
pass

View File

@@ -1,5 +1,5 @@
from crispy_bootstrap5.bootstrap5 import Switch, BS5Accordion
from crispy_forms.bootstrap import FormActions, AccordionGroup
from crispy_forms.bootstrap import FormActions, AccordionGroup, AppendedText
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
Layout,
@@ -290,11 +290,15 @@ class QuickTransactionForm(forms.ModelForm):
"category",
"tags",
"entities",
"mute",
]
widgets = {
"notes": forms.Textarea(attrs={"rows": 3}),
"account": TomSelect(clear_button=False, group_by="group"),
}
help_texts = {
"mute": _("Muted transactions won't be displayed on monthly summaries")
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -356,6 +360,7 @@ class QuickTransactionForm(forms.ModelForm):
css_class="form-row",
),
"notes",
Switch("mute"),
)
if self.instance and self.instance.pk:
@@ -494,6 +499,13 @@ class TransferForm(forms.Form):
label=_("Notes"),
)
mute = forms.BooleanField(
label=_("Mute"),
initial=True,
required=False,
help_text=_("Muted transactions won't be displayed on monthly summaries"),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -512,6 +524,7 @@ class TransferForm(forms.Form):
),
Field("description"),
Field("notes"),
Switch("mute"),
Row(
Column(
Row(
@@ -594,6 +607,8 @@ class TransferForm(forms.Form):
return cleaned_data
def save(self):
mute = self.cleaned_data["mute"]
from_account = self.cleaned_data["from_account"]
to_account = self.cleaned_data["to_account"]
from_amount = self.cleaned_data["from_amount"]
@@ -616,6 +631,7 @@ class TransferForm(forms.Form):
description=description,
category=from_category,
notes=notes,
mute=mute,
)
from_transaction.tags.set(self.cleaned_data.get("from_tags", []))
@@ -630,6 +646,7 @@ class TransferForm(forms.Form):
description=description,
category=to_category,
notes=notes,
mute=mute,
)
to_transaction.tags.set(self.cleaned_data.get("to_tags", []))
@@ -868,7 +885,7 @@ class TransactionCategoryForm(forms.ModelForm):
fields = ["name", "mute", "active"]
labels = {"name": _("Category name")}
help_texts = {
"mute": _("Muted categories won't count towards your monthly total")
"mute": _("Muted categories won't be displayed on monthly summaries")
}
def __init__(self, *args, **kwargs):
@@ -946,6 +963,7 @@ class RecurringTransactionForm(forms.ModelForm):
"notes",
"add_notes_to_transaction",
"entities",
"keep_at_most",
]
widgets = {
"reference_date": AirMonthYearPickerInput(),
@@ -1025,6 +1043,7 @@ class RecurringTransactionForm(forms.ModelForm):
Column("end_date", css_class="form-group col-md-4 mb-0"),
css_class="form-row",
),
AppendedText("keep_at_most", _("future transactions")),
)
self.fields["amount"].widget = ArbitraryDecimalDisplayNumberInput()
@@ -1066,5 +1085,6 @@ class RecurringTransactionForm(forms.ModelForm):
instance.create_upcoming_transactions()
else:
instance.update_unpaid_transactions()
instance.generate_upcoming_transactions()
return instance

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.11 on 2025-07-19 18:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0044_alter_quicktransaction_unique_together'),
]
operations = [
migrations.AddField(
model_name='transaction',
name='mute',
field=models.BooleanField(default=False, verbose_name='Mute'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.11 on 2025-07-19 18:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0045_transaction_mute'),
]
operations = [
migrations.AddField(
model_name='quicktransaction',
name='mute',
field=models.BooleanField(default=False, verbose_name='Mute'),
),
]

View File

@@ -0,0 +1,61 @@
# Generated by Django 5.2.4 on 2025-07-28 02:15
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0046_quicktransaction_mute'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='transactioncategory',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='transactioncategory',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='transactioncategory',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
migrations.AlterField(
model_name='transactionentity',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='transactionentity',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='transactionentity',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
migrations.AlterField(
model_name='transactiontag',
name='owner',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
),
migrations.AlterField(
model_name='transactiontag',
name='shared_with',
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL, verbose_name='Shared with users'),
),
migrations.AlterField(
model_name='transactiontag',
name='visibility',
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10, verbose_name='Visibility'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.2.4 on 2025-08-06 14:51
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0047_alter_transactioncategory_owner_and_more'),
]
operations = [
migrations.AddField(
model_name='recurringtransaction',
name='keep_at_most',
field=models.PositiveIntegerField(default=6, validators=[django.core.validators.MinValueValidator(1)], verbose_name='Keep at most'),
),
]

View File

@@ -299,6 +299,7 @@ class Transaction(OwnedObject):
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
date = models.DateField(verbose_name=_("Date"))
reference_date = MonthYearModelField(verbose_name=_("Reference Date"))
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
amount = models.DecimalField(
max_digits=42,
@@ -721,6 +722,9 @@ class RecurringTransaction(models.Model):
recurrence_interval = models.PositiveIntegerField(
verbose_name=_("Recurrence Interval"),
)
keep_at_most = models.PositiveIntegerField(
verbose_name=_("Keep at most"), default=6, validators=[MinValueValidator(1)]
)
last_generated_date = models.DateField(
verbose_name=_("Last Generated Date"), null=True, blank=True
@@ -758,8 +762,10 @@ class RecurringTransaction(models.Model):
current_date = self.start_date
reference_date = self.reference_date
end_date = min(
self.end_date or timezone.now().date() + (self.get_recurrence_delta() * 5),
timezone.now().date() + (self.get_recurrence_delta() * 5),
self.end_date
or timezone.now().date()
+ (self.get_recurrence_delta() * self.keep_at_most),
timezone.now().date() + (self.get_recurrence_delta() * self.keep_at_most),
)
while current_date <= end_date:
@@ -836,8 +842,16 @@ class RecurringTransaction(models.Model):
current_date = start_date
end_date = min(
recurring_transaction.end_date
or today + (recurring_transaction.get_recurrence_delta() * 6),
today + (recurring_transaction.get_recurrence_delta() * 6),
or today
+ (
recurring_transaction.get_recurrence_delta()
* recurring_transaction.keep_at_most
),
today
+ (
recurring_transaction.get_recurrence_delta()
* recurring_transaction.keep_at_most
),
)
logger.info(f"End date: {end_date}")
@@ -918,6 +932,7 @@ class QuickTransaction(OwnedObject):
verbose_name=_("Type"),
)
is_paid = models.BooleanField(default=True, verbose_name=_("Paid"))
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
amount = models.DecimalField(
max_digits=42,
@@ -974,3 +989,6 @@ class QuickTransaction(OwnedObject):
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return self.name

View File

@@ -28,23 +28,18 @@ def generate_recurring_transactions(timestamp=None):
@app.periodic(cron="10 1 * * *")
@app.task(name="cleanup_deleted_transactions")
def cleanup_deleted_transactions(timestamp=None):
with cachalot_disabled():
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
if settings.ENABLE_SOFT_DELETE and settings.KEEP_DELETED_TRANSACTIONS_FOR == 0:
return "KEEP_DELETED_TRANSACTIONS_FOR is 0, no cleanup performed."
if not settings.ENABLE_SOFT_DELETE:
# Hard delete all soft-deleted transactions
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
return (
f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
)
if not settings.ENABLE_SOFT_DELETE:
# Hard delete all soft-deleted transactions
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
return f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
# Calculate the cutoff date
cutoff_date = timezone.now() - timedelta(
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
)
invalidate()
# Calculate the cutoff date
cutoff_date = timezone.now() - timedelta(
days=settings.KEEP_DELETED_TRANSACTIONS_FOR
)
# Hard delete soft-deleted transactions older than the cutoff date
old_transactions = Transaction.userless_deleted_objects.filter(

View File

@@ -66,6 +66,21 @@ urlpatterns = [
views.transaction_pay,
name="transaction_pay",
),
path(
"transaction/<int:transaction_id>/mute/",
views.transaction_mute,
name="transaction_mute",
),
path(
"transaction/<int:transaction_id>/change-month/<str:change_type>/",
views.transaction_change_month,
name="transaction_change_month",
),
path(
"transaction/<int:transaction_id>/move-to-today/",
views.transaction_move_to_today,
name="transaction_move_to_today",
),
path(
"transaction/<int:transaction_id>/delete/",
views.transaction_delete,
@@ -342,4 +357,9 @@ urlpatterns = [
views.quick_transaction_add_as_transaction,
name="quick_transaction_add_as_transaction",
),
path(
"transactions/<int:transaction_id>/add-as-quick-transaction/",
views.quick_transaction_add_as_quick_transaction,
name="quick_transaction_add_as_quick_transaction",
),
]

View File

@@ -62,7 +62,7 @@ def bulk_unpay_transactions(request):
@login_required
def bulk_delete_transactions(request):
selected_transactions = request.GET.getlist("transactions", [])
transactions = Transaction.all_objects.filter(id__in=selected_transactions)
transactions = Transaction.objects.filter(id__in=selected_transactions)
count = transactions.count()
transactions.delete()

View File

@@ -9,7 +9,7 @@ from django.views.decorators.http import require_http_methods
from apps.common.decorators.htmx import only_htmx
from apps.transactions.forms import QuickTransactionForm
from apps.transactions.models import QuickTransaction
from apps.transactions.models import QuickTransaction, transaction_created
from apps.transactions.models import Transaction
@@ -129,7 +129,15 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
quick_transaction_data = model_to_dict(
quick_transaction,
exclude=["id", "name", "owner", "account", "category", "tags", "entities"],
exclude=[
"id",
"name",
"owner",
"account",
"category",
"tags",
"entities",
],
)
new_transaction = Transaction(**quick_transaction_data)
@@ -142,6 +150,8 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
new_transaction.tags.set(quick_transaction.tags.all())
new_transaction.entities.set(quick_transaction.entities.all())
transaction_created.send(sender=new_transaction)
messages.success(request, _("Transaction added successfully"))
return HttpResponse(
@@ -150,3 +160,70 @@ def quick_transaction_add_as_transaction(request, quick_transaction_id):
"HX-Trigger": "updated, hide_offcanvas",
},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def quick_transaction_add_as_quick_transaction(request, transaction_id):
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
if (
transaction.description
and QuickTransaction.objects.filter(
name__startswith=transaction.description
).exists()
) or QuickTransaction.objects.filter(
name__startswith=_("Quick Transaction")
).exists():
if transaction.description:
count = QuickTransaction.objects.filter(
name__startswith=transaction.description
).count()
qt_name = transaction.description + f" ({count + 1})"
else:
count = QuickTransaction.objects.filter(
name__startswith=_("Quick Transaction")
).count()
qt_name = _("Quick Transaction") + f" ({count + 1})"
else:
qt_name = transaction.description or _("Quick Transaction")
transaction_data = model_to_dict(
transaction,
exclude=[
"id",
"name",
"owner",
"account",
"category",
"tags",
"entities",
"date",
"reference_date",
"installment_plan",
"installment_id",
"recurring_transaction",
"deleted",
"deleted_at",
],
)
new_quick_transaction = QuickTransaction(**transaction_data)
new_quick_transaction.account = transaction.account
new_quick_transaction.category = transaction.category
new_quick_transaction.name = qt_name
new_quick_transaction.save()
new_quick_transaction.tags.set(transaction.tags.all())
new_quick_transaction.entities.set(transaction.entities.all())
messages.success(request, _("Item added successfully"))
return HttpResponse(
status=204,
headers={
"HX-Trigger": "toasts",
},
)

View File

@@ -1,10 +1,11 @@
import datetime
from copy import deepcopy
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q
from django.db.models import Q, When, Case, Value, IntegerField
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
@@ -388,6 +389,67 @@ def transaction_pay(request, transaction_id):
return response
@only_htmx
@login_required
@require_http_methods(["GET"])
def transaction_mute(request, transaction_id):
transaction = get_object_or_404(Transaction, pk=transaction_id)
new_mute = False if transaction.mute else True
transaction.mute = new_mute
transaction.save()
transaction_updated.send(sender=transaction)
response = render(
request,
"transactions/fragments/item.html",
context={"transaction": transaction, **request.GET},
)
response.headers["HX-Trigger"] = "selective_update"
return response
@only_htmx
@login_required
@require_http_methods(["GET"])
def transaction_change_month(request, transaction_id, change_type):
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
if change_type == "next":
transaction.reference_date = transaction.reference_date + relativedelta(
months=1
)
transaction.save()
transaction_updated.send(sender=transaction)
elif change_type == "previous":
transaction.reference_date = transaction.reference_date - relativedelta(
months=1
)
transaction.save()
transaction_updated.send(sender=transaction)
return HttpResponse(
status=204,
headers={"HX-Trigger": "updated"},
)
@only_htmx
@login_required
@require_http_methods(["GET"])
def transaction_move_to_today(request, transaction_id):
transaction: Transaction = get_object_or_404(Transaction, pk=transaction_id)
transaction.date = timezone.localdate(timezone.now())
transaction.save()
transaction_updated.send(sender=transaction)
return HttpResponse(
status=204,
headers={"HX-Trigger": "updated"},
)
@login_required
@require_http_methods(["GET"])
def transaction_all_index(request):
@@ -527,7 +589,10 @@ def transaction_all_currency_summary(request):
f = TransactionsFilter(request.GET, queryset=transactions)
currency_data = calculate_currency_totals(f.qs.all(), ignore_empty=True)
currency_data = calculate_currency_totals(
f.qs.exclude(account__in=request.user.untracked_accounts.all()),
ignore_empty=True,
)
currency_percentages = calculate_percentage_distribution(currency_data)
context = {
@@ -586,11 +651,26 @@ def get_recent_transactions(request, filter_type=None):
# Get search term from query params
search_term = request.GET.get("q", "").strip()
today = timezone.localdate(timezone.now())
yesterday = today - timezone.timedelta(days=1)
tomorrow = today + timezone.timedelta(days=1)
# Base queryset with selected fields
queryset = (
Transaction.objects.filter(deleted=False)
.annotate(
date_order=Case(
When(date=today, then=Value(0)),
When(date=tomorrow, then=Value(1)),
When(date=yesterday, then=Value(2)),
When(date__gt=tomorrow, then=Value(3)),
When(date__lt=yesterday, then=Value(4)),
default=Value(5),
output_field=IntegerField(),
)
)
.select_related("account", "category")
.order_by("-created_at")
.order_by("date_order", "date", "id")
)
if filter_type:

View File

@@ -2,7 +2,7 @@ from crispy_forms.bootstrap import (
FormActions,
)
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div
from crispy_forms.layout import Layout, Submit, Row, Column, Field, Div, HTML
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import (
@@ -45,7 +45,7 @@ class LoginForm(AuthenticationForm):
self.helper.layout = Layout(
"username",
"password",
Submit("Submit", "Login"),
Submit("Submit", "Login", css_class="btn btn-primary w-100"),
)
@@ -115,6 +115,7 @@ class UserSettingsForm(forms.ModelForm):
"date_format",
"datetime_format",
"number_format",
"volume",
]
def __init__(self, *args, **kwargs):
@@ -126,10 +127,14 @@ class UserSettingsForm(forms.ModelForm):
self.helper.layout = Layout(
"language",
"timezone",
HTML("<hr />"),
"date_format",
"datetime_format",
"number_format",
HTML("<hr />"),
"start_page",
HTML("<hr />"),
"volume",
FormActions(
NoClassSubmit(
"submit", _("Save"), css_class="btn btn-outline-primary w-100"

View File

@@ -0,0 +1,479 @@
# Generated by Django 5.1.1 on 2025-06-29 00:48
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0021_alter_usersettings_timezone"),
]
operations = [
migrations.AddField(
model_name="usersettings",
name="volume",
field=models.PositiveIntegerField(
default=10,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(10),
],
verbose_name="Volume",
),
),
migrations.AlterField(
model_name="usersettings",
name="timezone",
field=models.CharField(
choices=[
("auto", "Auto"),
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
(
"America/Argentina/Buenos_Aires",
"America/Argentina/Buenos_Aires",
),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
(
"America/Argentina/Rio_Gallegos",
"America/Argentina/Rio_Gallegos",
),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
(
"America/North_Dakota/New_Salem",
"America/North_Dakota/New_Salem",
),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Chita", "Asia/Chita"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/Perth", "Australia/Perth"),
("Australia/Sydney", "Australia/Sydney"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zurich", "Europe/Zurich"),
("GMT", "GMT"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("Pacific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("US/Alaska", "US/Alaska"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("UTC", "UTC"),
],
default="auto",
max_length=50,
verbose_name="Time Zone",
),
),
]

File diff suppressed because one or more lines are too long

View File

@@ -2,11 +2,449 @@ import pytz
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AbstractUser, Group
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.users.managers import UserManager
timezones = [
("auto", _("Auto")),
("Africa/Abidjan", "Africa/Abidjan"),
("Africa/Accra", "Africa/Accra"),
("Africa/Addis_Ababa", "Africa/Addis_Ababa"),
("Africa/Algiers", "Africa/Algiers"),
("Africa/Asmara", "Africa/Asmara"),
("Africa/Bamako", "Africa/Bamako"),
("Africa/Bangui", "Africa/Bangui"),
("Africa/Banjul", "Africa/Banjul"),
("Africa/Bissau", "Africa/Bissau"),
("Africa/Blantyre", "Africa/Blantyre"),
("Africa/Brazzaville", "Africa/Brazzaville"),
("Africa/Bujumbura", "Africa/Bujumbura"),
("Africa/Cairo", "Africa/Cairo"),
("Africa/Casablanca", "Africa/Casablanca"),
("Africa/Ceuta", "Africa/Ceuta"),
("Africa/Conakry", "Africa/Conakry"),
("Africa/Dakar", "Africa/Dakar"),
("Africa/Dar_es_Salaam", "Africa/Dar_es_Salaam"),
("Africa/Djibouti", "Africa/Djibouti"),
("Africa/Douala", "Africa/Douala"),
("Africa/El_Aaiun", "Africa/El_Aaiun"),
("Africa/Freetown", "Africa/Freetown"),
("Africa/Gaborone", "Africa/Gaborone"),
("Africa/Harare", "Africa/Harare"),
("Africa/Johannesburg", "Africa/Johannesburg"),
("Africa/Juba", "Africa/Juba"),
("Africa/Kampala", "Africa/Kampala"),
("Africa/Khartoum", "Africa/Khartoum"),
("Africa/Kigali", "Africa/Kigali"),
("Africa/Kinshasa", "Africa/Kinshasa"),
("Africa/Lagos", "Africa/Lagos"),
("Africa/Libreville", "Africa/Libreville"),
("Africa/Lome", "Africa/Lome"),
("Africa/Luanda", "Africa/Luanda"),
("Africa/Lubumbashi", "Africa/Lubumbashi"),
("Africa/Lusaka", "Africa/Lusaka"),
("Africa/Malabo", "Africa/Malabo"),
("Africa/Maputo", "Africa/Maputo"),
("Africa/Maseru", "Africa/Maseru"),
("Africa/Mbabane", "Africa/Mbabane"),
("Africa/Mogadishu", "Africa/Mogadishu"),
("Africa/Monrovia", "Africa/Monrovia"),
("Africa/Nairobi", "Africa/Nairobi"),
("Africa/Ndjamena", "Africa/Ndjamena"),
("Africa/Niamey", "Africa/Niamey"),
("Africa/Nouakchott", "Africa/Nouakchott"),
("Africa/Ouagadougou", "Africa/Ouagadougou"),
("Africa/Porto-Novo", "Africa/Porto-Novo"),
("Africa/Sao_Tome", "Africa/Sao_Tome"),
("Africa/Tripoli", "Africa/Tripoli"),
("Africa/Tunis", "Africa/Tunis"),
("Africa/Windhoek", "Africa/Windhoek"),
("America/Adak", "America/Adak"),
("America/Anchorage", "America/Anchorage"),
("America/Anguilla", "America/Anguilla"),
("America/Antigua", "America/Antigua"),
("America/Araguaina", "America/Araguaina"),
("America/Argentina/Buenos_Aires", "America/Argentina/Buenos_Aires"),
("America/Argentina/Catamarca", "America/Argentina/Catamarca"),
("America/Argentina/Cordoba", "America/Argentina/Cordoba"),
("America/Argentina/Jujuy", "America/Argentina/Jujuy"),
("America/Argentina/La_Rioja", "America/Argentina/La_Rioja"),
("America/Argentina/Mendoza", "America/Argentina/Mendoza"),
("America/Argentina/Rio_Gallegos", "America/Argentina/Rio_Gallegos"),
("America/Argentina/Salta", "America/Argentina/Salta"),
("America/Argentina/San_Juan", "America/Argentina/San_Juan"),
("America/Argentina/San_Luis", "America/Argentina/San_Luis"),
("America/Argentina/Tucuman", "America/Argentina/Tucuman"),
("America/Argentina/Ushuaia", "America/Argentina/Ushuaia"),
("America/Aruba", "America/Aruba"),
("America/Asuncion", "America/Asuncion"),
("America/Atikokan", "America/Atikokan"),
("America/Bahia", "America/Bahia"),
("America/Bahia_Banderas", "America/Bahia_Banderas"),
("America/Barbados", "America/Barbados"),
("America/Belem", "America/Belem"),
("America/Belize", "America/Belize"),
("America/Blanc-Sablon", "America/Blanc-Sablon"),
("America/Boa_Vista", "America/Boa_Vista"),
("America/Bogota", "America/Bogota"),
("America/Boise", "America/Boise"),
("America/Cambridge_Bay", "America/Cambridge_Bay"),
("America/Campo_Grande", "America/Campo_Grande"),
("America/Cancun", "America/Cancun"),
("America/Caracas", "America/Caracas"),
("America/Cayenne", "America/Cayenne"),
("America/Cayman", "America/Cayman"),
("America/Chicago", "America/Chicago"),
("America/Chihuahua", "America/Chihuahua"),
("America/Ciudad_Juarez", "America/Ciudad_Juarez"),
("America/Costa_Rica", "America/Costa_Rica"),
("America/Coyhaique", "America/Coyhaique"),
("America/Creston", "America/Creston"),
("America/Cuiaba", "America/Cuiaba"),
("America/Curacao", "America/Curacao"),
("America/Danmarkshavn", "America/Danmarkshavn"),
("America/Dawson", "America/Dawson"),
("America/Dawson_Creek", "America/Dawson_Creek"),
("America/Denver", "America/Denver"),
("America/Detroit", "America/Detroit"),
("America/Dominica", "America/Dominica"),
("America/Edmonton", "America/Edmonton"),
("America/Eirunepe", "America/Eirunepe"),
("America/El_Salvador", "America/El_Salvador"),
("America/Fort_Nelson", "America/Fort_Nelson"),
("America/Fortaleza", "America/Fortaleza"),
("America/Glace_Bay", "America/Glace_Bay"),
("America/Goose_Bay", "America/Goose_Bay"),
("America/Grand_Turk", "America/Grand_Turk"),
("America/Grenada", "America/Grenada"),
("America/Guadeloupe", "America/Guadeloupe"),
("America/Guatemala", "America/Guatemala"),
("America/Guayaquil", "America/Guayaquil"),
("America/Guyana", "America/Guyana"),
("America/Halifax", "America/Halifax"),
("America/Havana", "America/Havana"),
("America/Hermosillo", "America/Hermosillo"),
("America/Indiana/Indianapolis", "America/Indiana/Indianapolis"),
("America/Indiana/Knox", "America/Indiana/Knox"),
("America/Indiana/Marengo", "America/Indiana/Marengo"),
("America/Indiana/Petersburg", "America/Indiana/Petersburg"),
("America/Indiana/Tell_City", "America/Indiana/Tell_City"),
("America/Indiana/Vevay", "America/Indiana/Vevay"),
("America/Indiana/Vincennes", "America/Indiana/Vincennes"),
("America/Indiana/Winamac", "America/Indiana/Winamac"),
("America/Inuvik", "America/Inuvik"),
("America/Iqaluit", "America/Iqaluit"),
("America/Jamaica", "America/Jamaica"),
("America/Juneau", "America/Juneau"),
("America/Kentucky/Louisville", "America/Kentucky/Louisville"),
("America/Kentucky/Monticello", "America/Kentucky/Monticello"),
("America/Kralendijk", "America/Kralendijk"),
("America/La_Paz", "America/La_Paz"),
("America/Lima", "America/Lima"),
("America/Los_Angeles", "America/Los_Angeles"),
("America/Lower_Princes", "America/Lower_Princes"),
("America/Maceio", "America/Maceio"),
("America/Managua", "America/Managua"),
("America/Manaus", "America/Manaus"),
("America/Marigot", "America/Marigot"),
("America/Martinique", "America/Martinique"),
("America/Matamoros", "America/Matamoros"),
("America/Mazatlan", "America/Mazatlan"),
("America/Menominee", "America/Menominee"),
("America/Merida", "America/Merida"),
("America/Metlakatla", "America/Metlakatla"),
("America/Mexico_City", "America/Mexico_City"),
("America/Miquelon", "America/Miquelon"),
("America/Moncton", "America/Moncton"),
("America/Monterrey", "America/Monterrey"),
("America/Montevideo", "America/Montevideo"),
("America/Montserrat", "America/Montserrat"),
("America/Nassau", "America/Nassau"),
("America/New_York", "America/New_York"),
("America/Nome", "America/Nome"),
("America/Noronha", "America/Noronha"),
("America/North_Dakota/Beulah", "America/North_Dakota/Beulah"),
("America/North_Dakota/Center", "America/North_Dakota/Center"),
("America/North_Dakota/New_Salem", "America/North_Dakota/New_Salem"),
("America/Nuuk", "America/Nuuk"),
("America/Ojinaga", "America/Ojinaga"),
("America/Panama", "America/Panama"),
("America/Paramaribo", "America/Paramaribo"),
("America/Phoenix", "America/Phoenix"),
("America/Port-au-Prince", "America/Port-au-Prince"),
("America/Port_of_Spain", "America/Port_of_Spain"),
("America/Porto_Velho", "America/Porto_Velho"),
("America/Puerto_Rico", "America/Puerto_Rico"),
("America/Punta_Arenas", "America/Punta_Arenas"),
("America/Rankin_Inlet", "America/Rankin_Inlet"),
("America/Recife", "America/Recife"),
("America/Regina", "America/Regina"),
("America/Resolute", "America/Resolute"),
("America/Rio_Branco", "America/Rio_Branco"),
("America/Santarem", "America/Santarem"),
("America/Santiago", "America/Santiago"),
("America/Santo_Domingo", "America/Santo_Domingo"),
("America/Sao_Paulo", "America/Sao_Paulo"),
("America/Scoresbysund", "America/Scoresbysund"),
("America/Sitka", "America/Sitka"),
("America/St_Barthelemy", "America/St_Barthelemy"),
("America/St_Johns", "America/St_Johns"),
("America/St_Kitts", "America/St_Kitts"),
("America/St_Lucia", "America/St_Lucia"),
("America/St_Thomas", "America/St_Thomas"),
("America/St_Vincent", "America/St_Vincent"),
("America/Swift_Current", "America/Swift_Current"),
("America/Tegucigalpa", "America/Tegucigalpa"),
("America/Thule", "America/Thule"),
("America/Tijuana", "America/Tijuana"),
("America/Toronto", "America/Toronto"),
("America/Tortola", "America/Tortola"),
("America/Vancouver", "America/Vancouver"),
("America/Whitehorse", "America/Whitehorse"),
("America/Winnipeg", "America/Winnipeg"),
("America/Yakutat", "America/Yakutat"),
("Antarctica/Casey", "Antarctica/Casey"),
("Antarctica/Davis", "Antarctica/Davis"),
("Antarctica/DumontDUrville", "Antarctica/DumontDUrville"),
("Antarctica/Macquarie", "Antarctica/Macquarie"),
("Antarctica/Mawson", "Antarctica/Mawson"),
("Antarctica/McMurdo", "Antarctica/McMurdo"),
("Antarctica/Palmer", "Antarctica/Palmer"),
("Antarctica/Rothera", "Antarctica/Rothera"),
("Antarctica/Syowa", "Antarctica/Syowa"),
("Antarctica/Troll", "Antarctica/Troll"),
("Antarctica/Vostok", "Antarctica/Vostok"),
("Arctic/Longyearbyen", "Arctic/Longyearbyen"),
("Asia/Aden", "Asia/Aden"),
("Asia/Almaty", "Asia/Almaty"),
("Asia/Amman", "Asia/Amman"),
("Asia/Anadyr", "Asia/Anadyr"),
("Asia/Aqtau", "Asia/Aqtau"),
("Asia/Aqtobe", "Asia/Aqtobe"),
("Asia/Ashgabat", "Asia/Ashgabat"),
("Asia/Atyrau", "Asia/Atyrau"),
("Asia/Baghdad", "Asia/Baghdad"),
("Asia/Bahrain", "Asia/Bahrain"),
("Asia/Baku", "Asia/Baku"),
("Asia/Bangkok", "Asia/Bangkok"),
("Asia/Barnaul", "Asia/Barnaul"),
("Asia/Beirut", "Asia/Beirut"),
("Asia/Bishkek", "Asia/Bishkek"),
("Asia/Brunei", "Asia/Brunei"),
("Asia/Chita", "Asia/Chita"),
("Asia/Colombo", "Asia/Colombo"),
("Asia/Damascus", "Asia/Damascus"),
("Asia/Dhaka", "Asia/Dhaka"),
("Asia/Dili", "Asia/Dili"),
("Asia/Dubai", "Asia/Dubai"),
("Asia/Dushanbe", "Asia/Dushanbe"),
("Asia/Famagusta", "Asia/Famagusta"),
("Asia/Gaza", "Asia/Gaza"),
("Asia/Hebron", "Asia/Hebron"),
("Asia/Ho_Chi_Minh", "Asia/Ho_Chi_Minh"),
("Asia/Hong_Kong", "Asia/Hong_Kong"),
("Asia/Hovd", "Asia/Hovd"),
("Asia/Irkutsk", "Asia/Irkutsk"),
("Asia/Jakarta", "Asia/Jakarta"),
("Asia/Jayapura", "Asia/Jayapura"),
("Asia/Jerusalem", "Asia/Jerusalem"),
("Asia/Kabul", "Asia/Kabul"),
("Asia/Kamchatka", "Asia/Kamchatka"),
("Asia/Karachi", "Asia/Karachi"),
("Asia/Kathmandu", "Asia/Kathmandu"),
("Asia/Khandyga", "Asia/Khandyga"),
("Asia/Kolkata", "Asia/Kolkata"),
("Asia/Krasnoyarsk", "Asia/Krasnoyarsk"),
("Asia/Kuala_Lumpur", "Asia/Kuala_Lumpur"),
("Asia/Kuching", "Asia/Kuching"),
("Asia/Kuwait", "Asia/Kuwait"),
("Asia/Macau", "Asia/Macau"),
("Asia/Magadan", "Asia/Magadan"),
("Asia/Makassar", "Asia/Makassar"),
("Asia/Manila", "Asia/Manila"),
("Asia/Muscat", "Asia/Muscat"),
("Asia/Nicosia", "Asia/Nicosia"),
("Asia/Novokuznetsk", "Asia/Novokuznetsk"),
("Asia/Novosibirsk", "Asia/Novosibirsk"),
("Asia/Omsk", "Asia/Omsk"),
("Asia/Oral", "Asia/Oral"),
("Asia/Phnom_Penh", "Asia/Phnom_Penh"),
("Asia/Pontianak", "Asia/Pontianak"),
("Asia/Pyongyang", "Asia/Pyongyang"),
("Asia/Qatar", "Asia/Qatar"),
("Asia/Qostanay", "Asia/Qostanay"),
("Asia/Qyzylorda", "Asia/Qyzylorda"),
("Asia/Riyadh", "Asia/Riyadh"),
("Asia/Sakhalin", "Asia/Sakhalin"),
("Asia/Samarkand", "Asia/Samarkand"),
("Asia/Seoul", "Asia/Seoul"),
("Asia/Shanghai", "Asia/Shanghai"),
("Asia/Singapore", "Asia/Singapore"),
("Asia/Srednekolymsk", "Asia/Srednekolymsk"),
("Asia/Taipei", "Asia/Taipei"),
("Asia/Tashkent", "Asia/Tashkent"),
("Asia/Tbilisi", "Asia/Tbilisi"),
("Asia/Tehran", "Asia/Tehran"),
("Asia/Thimphu", "Asia/Thimphu"),
("Asia/Tokyo", "Asia/Tokyo"),
("Asia/Tomsk", "Asia/Tomsk"),
("Asia/Ulaanbaatar", "Asia/Ulaanbaatar"),
("Asia/Urumqi", "Asia/Urumqi"),
("Asia/Ust-Nera", "Asia/Ust-Nera"),
("Asia/Vientiane", "Asia/Vientiane"),
("Asia/Vladivostok", "Asia/Vladivostok"),
("Asia/Yakutsk", "Asia/Yakutsk"),
("Asia/Yangon", "Asia/Yangon"),
("Asia/Yekaterinburg", "Asia/Yekaterinburg"),
("Asia/Yerevan", "Asia/Yerevan"),
("Atlantic/Azores", "Atlantic/Azores"),
("Atlantic/Bermuda", "Atlantic/Bermuda"),
("Atlantic/Canary", "Atlantic/Canary"),
("Atlantic/Cape_Verde", "Atlantic/Cape_Verde"),
("Atlantic/Faroe", "Atlantic/Faroe"),
("Atlantic/Madeira", "Atlantic/Madeira"),
("Atlantic/Reykjavik", "Atlantic/Reykjavik"),
("Atlantic/South_Georgia", "Atlantic/South_Georgia"),
("Atlantic/St_Helena", "Atlantic/St_Helena"),
("Atlantic/Stanley", "Atlantic/Stanley"),
("Australia/Adelaide", "Australia/Adelaide"),
("Australia/Brisbane", "Australia/Brisbane"),
("Australia/Broken_Hill", "Australia/Broken_Hill"),
("Australia/Darwin", "Australia/Darwin"),
("Australia/Eucla", "Australia/Eucla"),
("Australia/Hobart", "Australia/Hobart"),
("Australia/Lindeman", "Australia/Lindeman"),
("Australia/Lord_Howe", "Australia/Lord_Howe"),
("Australia/Melbourne", "Australia/Melbourne"),
("Australia/Perth", "Australia/Perth"),
("Australia/Sydney", "Australia/Sydney"),
("Canada/Atlantic", "Canada/Atlantic"),
("Canada/Central", "Canada/Central"),
("Canada/Eastern", "Canada/Eastern"),
("Canada/Mountain", "Canada/Mountain"),
("Canada/Newfoundland", "Canada/Newfoundland"),
("Canada/Pacific", "Canada/Pacific"),
("Europe/Amsterdam", "Europe/Amsterdam"),
("Europe/Andorra", "Europe/Andorra"),
("Europe/Astrakhan", "Europe/Astrakhan"),
("Europe/Athens", "Europe/Athens"),
("Europe/Belgrade", "Europe/Belgrade"),
("Europe/Berlin", "Europe/Berlin"),
("Europe/Bratislava", "Europe/Bratislava"),
("Europe/Brussels", "Europe/Brussels"),
("Europe/Bucharest", "Europe/Bucharest"),
("Europe/Budapest", "Europe/Budapest"),
("Europe/Busingen", "Europe/Busingen"),
("Europe/Chisinau", "Europe/Chisinau"),
("Europe/Copenhagen", "Europe/Copenhagen"),
("Europe/Dublin", "Europe/Dublin"),
("Europe/Gibraltar", "Europe/Gibraltar"),
("Europe/Guernsey", "Europe/Guernsey"),
("Europe/Helsinki", "Europe/Helsinki"),
("Europe/Isle_of_Man", "Europe/Isle_of_Man"),
("Europe/Istanbul", "Europe/Istanbul"),
("Europe/Jersey", "Europe/Jersey"),
("Europe/Kaliningrad", "Europe/Kaliningrad"),
("Europe/Kirov", "Europe/Kirov"),
("Europe/Kyiv", "Europe/Kyiv"),
("Europe/Lisbon", "Europe/Lisbon"),
("Europe/Ljubljana", "Europe/Ljubljana"),
("Europe/London", "Europe/London"),
("Europe/Luxembourg", "Europe/Luxembourg"),
("Europe/Madrid", "Europe/Madrid"),
("Europe/Malta", "Europe/Malta"),
("Europe/Mariehamn", "Europe/Mariehamn"),
("Europe/Minsk", "Europe/Minsk"),
("Europe/Monaco", "Europe/Monaco"),
("Europe/Moscow", "Europe/Moscow"),
("Europe/Oslo", "Europe/Oslo"),
("Europe/Paris", "Europe/Paris"),
("Europe/Podgorica", "Europe/Podgorica"),
("Europe/Prague", "Europe/Prague"),
("Europe/Riga", "Europe/Riga"),
("Europe/Rome", "Europe/Rome"),
("Europe/Samara", "Europe/Samara"),
("Europe/San_Marino", "Europe/San_Marino"),
("Europe/Sarajevo", "Europe/Sarajevo"),
("Europe/Saratov", "Europe/Saratov"),
("Europe/Simferopol", "Europe/Simferopol"),
("Europe/Skopje", "Europe/Skopje"),
("Europe/Sofia", "Europe/Sofia"),
("Europe/Stockholm", "Europe/Stockholm"),
("Europe/Tallinn", "Europe/Tallinn"),
("Europe/Tirane", "Europe/Tirane"),
("Europe/Ulyanovsk", "Europe/Ulyanovsk"),
("Europe/Vaduz", "Europe/Vaduz"),
("Europe/Vatican", "Europe/Vatican"),
("Europe/Vienna", "Europe/Vienna"),
("Europe/Vilnius", "Europe/Vilnius"),
("Europe/Volgograd", "Europe/Volgograd"),
("Europe/Warsaw", "Europe/Warsaw"),
("Europe/Zagreb", "Europe/Zagreb"),
("Europe/Zurich", "Europe/Zurich"),
("GMT", "GMT"),
("Indian/Antananarivo", "Indian/Antananarivo"),
("Indian/Chagos", "Indian/Chagos"),
("Indian/Christmas", "Indian/Christmas"),
("Indian/Cocos", "Indian/Cocos"),
("Indian/Comoro", "Indian/Comoro"),
("Indian/Kerguelen", "Indian/Kerguelen"),
("Indian/Mahe", "Indian/Mahe"),
("Indian/Maldives", "Indian/Maldives"),
("Indian/Mauritius", "Indian/Mauritius"),
("Indian/Mayotte", "Indian/Mayotte"),
("Indian/Reunion", "Indian/Reunion"),
("Pacific/Apia", "Pacific/Apia"),
("Pacific/Auckland", "Pacific/Auckland"),
("Pacific/Bougainville", "Pacific/Bougainville"),
("Pacific/Chatham", "Pacific/Chatham"),
("Pacific/Chuuk", "Pacific/Chuuk"),
("Pacific/Easter", "Pacific/Easter"),
("Pacific/Efate", "Pacific/Efate"),
("Pacific/Fakaofo", "Pacific/Fakaofo"),
("Pacific/Fiji", "Pacific/Fiji"),
("Pacific/Funafuti", "Pacific/Funafuti"),
("Pacific/Galapagos", "Pacific/Galapagos"),
("Pacific/Gambier", "Pacific/Gambier"),
("Pacific/Guadalcanal", "Pacific/Guadalcanal"),
("P2025-06-29T01:43:14.671389745Z acific/Guam", "Pacific/Guam"),
("Pacific/Honolulu", "Pacific/Honolulu"),
("Pacific/Kanton", "Pacific/Kanton"),
("Pacific/Kiritimati", "Pacific/Kiritimati"),
("Pacific/Kosrae", "Pacific/Kosrae"),
("Pacific/Kwajalein", "Pacific/Kwajalein"),
("Pacific/Majuro", "Pacific/Majuro"),
("Pacific/Marquesas", "Pacific/Marquesas"),
("Pacific/Midway", "Pacific/Midway"),
("Pacific/Nauru", "Pacific/Nauru"),
("Pacific/Niue", "Pacific/Niue"),
("Pacific/Norfolk", "Pacific/Norfolk"),
("Pacific/Noumea", "Pacific/Noumea"),
("Pacific/Pago_Pago", "Pacific/Pago_Pago"),
("Pacific/Palau", "Pacific/Palau"),
("Pacific/Pitcairn", "Pacific/Pitcairn"),
("Pacific/Pohnpei", "Pacific/Pohnpei"),
("Pacific/Port_Moresby", "Pacific/Port_Moresby"),
("Pacific/Rarotonga", "Pacific/Rarotonga"),
("Pacific/Saipan", "Pacific/Saipan"),
("Pacific/Tahiti", "Pacific/Tahiti"),
("Pacific/Tarawa", "Pacific/Tarawa"),
("Pacific/Tongatapu", "Pacific/Tongatapu"),
("Pacific/Wake", "Pacific/Wake"),
("Pacific/Wallis", "Pacific/Wallis"),
("US/Alaska", "US/Alaska"),
("US/Arizona", "US/Arizona"),
("US/Central", "US/Central"),
("US/Eastern", "US/Eastern"),
("US/Hawaii", "US/Hawaii"),
("US/Mountain", "US/Mountain"),
("US/Pacific", "US/Pacific"),
("UTC", "UTC"),
]
class User(AbstractUser):
username = None
@@ -36,6 +474,11 @@ class UserSettings(models.Model):
)
hide_amounts = models.BooleanField(default=False)
mute_sounds = models.BooleanField(default=False)
volume = models.PositiveIntegerField(
default=10,
validators=[MinValueValidator(1), MaxValueValidator(10)],
verbose_name=_("Volume"),
)
date_format = models.CharField(
max_length=100, default="SHORT_DATE_FORMAT", verbose_name=_("Date Format")
@@ -57,7 +500,7 @@ class UserSettings(models.Model):
)
timezone = models.CharField(
max_length=50,
choices=[("auto", _("Auto"))] + [(tz, tz) for tz in pytz.common_timezones],
choices=timezones,
default="auto",
verbose_name=_("Time Zone"),
)

View File

@@ -3,6 +3,7 @@ from django.urls import path
from . import views
urlpatterns = [
path("yearly/", views.index, name="yearly_index"),
path("yearly/currency/", views.index_by_currency, name="yearly_index_currency"),
path("yearly/account/", views.index_by_account, name="yearly_index_account"),
path(

View File

@@ -16,6 +16,22 @@ from apps.transactions.utils.calculations import (
)
@login_required
def index(request):
if "view_type" in request.GET:
view_type = request.GET["view_type"]
request.session["yearly_view_type"] = view_type
else:
view_type = request.session.get("yearly_view_type", "currency")
now = timezone.localdate(timezone.now())
if view_type == "currency":
return redirect(to="yearly_overview_currency", year=now.year)
else:
return redirect(to="yearly_overview_account", year=now.year)
@login_required
def index_by_currency(request):
now = timezone.localdate(timezone.now())
@@ -32,6 +48,8 @@ def index_by_account(request):
@login_required
def index_yearly_overview_by_currency(request, year: int):
request.session["yearly_view_type"] = "currency"
next_year = year + 1
previous_year = year - 1
@@ -49,6 +67,7 @@ def index_yearly_overview_by_currency(request, year: int):
"previous_year": previous_year,
"months": month_options,
"currencies": currency_options,
"type": "currency",
},
)
@@ -75,7 +94,8 @@ def yearly_overview_by_currency(request, year: int):
transactions = (
Transaction.objects.filter(**filter_params)
.exclude(Q(category__mute=True) & ~Q(category=None))
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.exclude(account__in=request.user.untracked_accounts.all())
.order_by("account__currency__name")
)
@@ -95,6 +115,7 @@ def yearly_overview_by_currency(request, year: int):
@login_required
def index_yearly_overview_by_account(request, year: int):
request.session["yearly_view_type"] = "account"
next_year = year + 1
previous_year = year - 1
@@ -115,6 +136,7 @@ def index_yearly_overview_by_account(request, year: int):
"previous_year": previous_year,
"months": month_options,
"accounts": account_options,
"type": "account",
},
)
@@ -141,7 +163,7 @@ def yearly_overview_by_account(request, year: int):
transactions = (
Transaction.objects.filter(**filter_params)
.exclude(Q(category__mute=True) & ~Q(category=None))
.exclude(Q(Q(category__mute=True) & ~Q(category=None)) | Q(mute=True))
.order_by(
"account__group__name",
"account__name",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Account Groups' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Accounts' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
@@ -71,6 +71,17 @@
hx-get="{% url 'account_share_settings' pk=account.id %}">
<i class="fa-solid fa-share fa-fw"></i></a>
{% endif %}
<a class="btn btn-secondary btn-sm"
role="button"
hx-get="{% url 'account_toggle_untracked' pk=account.id %}"
data-bs-toggle="tooltip"
data-bs-title="{% if account.is_untracked_by %}{% translate "Track" %}{% else %}{% translate "Untrack" %}{% endif %}">
{% if account.is_untracked_by %}
<i class="fa-solid fa-eye fa-fw"></i>
{% else %}
<i class="fa-solid fa-eye-slash fa-fw"></i>
{% endif %}
</a>
</div>
</td>
<td class="col">{{ account.name }}</td>

View File

@@ -2,7 +2,7 @@
{% load i18n %}
<div>
<div class="tw-hidden lg:tw-grid lg:tw-grid-cols-7 tw-gap-4 lg:tw-gap-0">
<div class="tw:hidden tw:lg:grid tw:lg:grid-cols-7 tw:gap-4 tw:lg:gap-0">
<div class="border-start border-top border-bottom p-2 text-center">
{% translate 'MON' %}
</div>
@@ -25,44 +25,44 @@
{% translate 'SUN' %}
</div>
</div>
<div class="tw-grid tw-grid-cols-1 tw-grid-rows-1 lg:tw-grid-cols-7 lg:tw-grid-rows-6 tw-gap-4 lg:tw-gap-0">
<div class="tw:grid tw:grid-cols-1 tw:grid-rows-1 tw:lg:grid-cols-7 tw:lg:grid-rows-6 tw:gap-4 tw:lg:gap-0">
{% for date in dates %}
{% if date %}
<div class="card h-100 hover:tw-bg-zinc-900 rounded-0{% if not date.transactions %} !tw-hidden lg:!tw-flex{% endif %}{% if today == date.date %} tw-border-yellow-300 border-primary{% endif %} " role="button"
<div class="card h-100 tw:hover:bg-zinc-900! rounded-0{% if not date.transactions %} tw:hidden! tw:lg:flex!{% endif %}{% if today == date.date %} tw:border-yellow-300 border-primary{% endif %} " role="button"
hx-get="{% url 'calendar_transactions_list' day=date.date.day month=date.date.month year=date.date.year %}"
hx-target="#persistent-generic-offcanvas-left">
<div class="card-header border-0 bg-transparent text-end tw-flex justify-content-between p-2 w-100">
<div class="lg:tw-hidden text-start w-100">{{ date.date|date:"l"|lower }}</div>
<div class="card-header border-0 bg-transparent text-end tw:flex justify-content-between p-2 w-100">
<div class="tw:lg:hidden text-start w-100">{{ date.date|date:"l"|lower }}</div>
<div class="text-end w-100">{{ date.day }}</div>
</div>
<div class="card-body p-2">
{% for transaction in date.transactions %}
{% if transaction.is_paid %}
{% if transaction.type == "IN" and not transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
<i class="fa-solid fa-circle-check tw:text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "IN" and transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
<i class="fa-solid fa-circle-check tw:text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
<i class="fa-solid fa-circle-check tw:text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% elif transaction.type == "EX" and transaction.account.is_asset %}
<i class="fa-solid fa-circle-check tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
<i class="fa-solid fa-circle-check tw:text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% endif %}
{% else %}
{% if transaction.type == "IN" and not transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
<i class="fa-regular fa-circle tw:text-green-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "IN" and transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
<i class="fa-regular fa-circle tw:text-green-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Income' %}{% endif %}"></i>
{% elif transaction.type == "EX" and not transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
<i class="fa-regular fa-circle tw:text-red-400" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% elif transaction.type == "EX" and transaction.account.is_asset %}
<i class="fa-regular fa-circle tw-text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
<i class="fa-regular fa-circle tw:text-red-300" data-bs-toggle="tooltip" data-bs-title="{% if transaction.description %}{{ transaction.description }}{% else %}{% trans 'Expense' %}{% endif %}"></i>
{% endif %}
{% endif %}
{% endfor %}
</div>
</div>
{% else %}
<div class="!tw-hidden lg:!tw-block card h-100 rounded-0"></div>
<div class="tw:hidden! tw:lg:block! card h-100 rounded-0"></div>
{% endif %}
{% endfor %}
</div>

View File

@@ -17,7 +17,7 @@
<div class="row mb-3 gx-xl-4 gy-3 mb-4">
{# Date picker#}
<div class="col-12 col-xl-4 flex-row align-items-center d-flex">
<div class="tw-text-base h-100 align-items-center d-flex">
<div class="tw:text-base h-100 align-items-center d-flex">
<a role="button"
class="pe-4 py-2"
hx-boost="true"
@@ -25,14 +25,14 @@
href="{% url 'calendar' month=previous_month year=previous_year %}"><i
class="fa-solid fa-chevron-left"></i></a>
</div>
<div class="tw-text-3xl fw-bold font-monospace tw-w-full text-center"
<div class="tw:text-3xl fw-bold font-monospace tw:w-full text-center"
hx-get="{% url 'month_year_picker' %}"
hx-target="#generic-offcanvas-left"
hx-trigger="click, date_picker from:window"
hx-vals='{"month": {{ month }}, "year": {{ year }}, "for": "calendar", "field": "date"}' role="button">
{{ month|month_name }} {{ year }}
</div>
<div class="tw-text-base mx-2 h-100 align-items-center d-flex">
<div class="tw:text-base mx-2 h-100 align-items-center d-flex">
<a role="button"
class="ps-3 py-2"
hx-boost="true"
@@ -52,7 +52,7 @@
</div>
<div class="row">
<div class="show-loading" hx-get="{% url 'calendar_list' month=month year=year %}"
hx-trigger="load, updated from:window, selective_update from:window"></div>
hx-trigger="load, updated from:window, selective_update from:window, every 10m"></div>
</div>
</div>
<c-ui.transactions_fab></c-ui.transactions_fab>

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Categories' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -32,7 +32,7 @@
tabindex="0">
<ul class="list-group list-group-flush" id="month-year-list">
{% for month_data in x.list %}
<li class="list-group-item hover:tw-bg-zinc-900
<li class="list-group-item tw:hover:bg-zinc-900
{% if month_data.month == current_month and month_data.year == current_year %} disabled bg-primary{% endif %}"
{% if month_data.month == current_month and month_data.year == current_year %}aria-disabled="true"{% endif %}>
<div class="d-flex justify-content-between">

View File

@@ -3,10 +3,10 @@
{% if not divless %}
<div class="{% if text_end %}text-end{% elif text_start %}text-start{% endif %}">
{% endif %}
<span class="amount{% if color == 'grey' or color == "gray" %} tw-text-gray-500{% elif color == 'green' %} tw-text-green-400{% elif color == 'red' %} tw-text-red-400{% endif %} {{ custom_class }}"
<span class="amount{% if color == 'grey' or color == "gray" %} tw:text-gray-500{% elif color == 'green' %} tw:text-green-400{% elif color == 'red' %} tw:text-red-400{% endif %} {{ custom_class }}"
data-original-value="{% currency_display amount=amount prefix=prefix suffix=suffix decimal_places=decimal_places %}"
data-amount="{{ amount|floatformat:"-40u" }}">
</span><span>{{ slot }}</span>
{% if not divless %}
</div>
{% endif %}
{% endif %}

View File

@@ -1,33 +1,33 @@
<div class="tw-min-h-16">
<div class="tw:min-h-16">
<div
id="fab-wrapper"
class="tw-fixed tw-bottom-5 tw-right-5 tw-ml-auto tw-w-max tw-flex tw-flex-col tw-items-end mt-5">
class="tw:fixed tw:bottom-5 tw:right-5 tw:ml-auto tw:w-max tw:flex tw:flex-col tw:items-end mt-5 tw:z-20">
<div
id="menu"
class="tw-flex tw-flex-col tw-items-end tw-space-y-6 tw-transition-all tw-duration-300 tw-ease-in-out tw-opacity-0 tw-invisible tw-hidden tw-mb-2">
class="tw:flex tw:flex-col tw:items-end tw:space-y-6 tw:transition-all tw:duration-300 tw:ease-in-out tw:opacity-0 tw:invisible tw:hidden tw:mb-2">
{{ slot }}
</div>
<button
class="btn btn-primary rounded-circle p-0 tw-w-12 tw-h-12 tw-flex tw-items-center tw-justify-center tw-shadow-lg hover:tw-shadow-xl focus:tw-shadow-xl tw-transition-all tw-duration-300 tw-ease-in-out"
class="btn btn-primary rounded-circle p-0 tw:w-12 tw:h-12 tw:flex tw:items-center tw:justify-center tw:shadow-lg tw:hover:shadow-xl tw:focus:shadow-xl tw:transition-all tw:duration-300 tw:ease-in-out"
_="
on click or focusout
if #menu matches .tw-invisible and event.type === 'click'
add .tw-rotate-45 to #fab-icon
remove .tw-invisible from #menu
remove .tw-hidden from #menu
remove .tw-opacity-0 from #menu
if #menu.classList.contains('tw:invisible') and event.type === 'click'
add .{'tw:rotate-45'} to #fab-icon
remove .{'tw:invisible'} from #menu
remove .{'tw:hidden'} from #menu
remove .{'tw:opacity-0'} from #menu
else
wait 0.2s
remove .tw-rotate-45 from #fab-icon
add .tw-invisible to #menu
add .tw-hidden to #menu
add .tw-opacity-0 to #menu
remove .{'tw:rotate-45'} from #fab-icon
add .{'tw:invisible'} to #menu
add .{'tw:hidden'} to #menu
add .{'tw:opacity-0'} to #menu
end
"
>
<i id="fab-icon" class="fa-solid fa-plus tw-text-3xl tw-transition-transform tw-duration-300 tw-ease-in-out"></i>
<i id="fab-icon" class="fa-solid fa-plus tw:text-3xl tw:transition-transform tw:duration-300 tw:ease-in-out"></i>
</button>
</div>
</div>

View File

@@ -1,5 +1,5 @@
{% load i18n %}
<div class="tw-relative fab-item">
<div class="tw:relative fab-item">
<button class="btn btn-sm btn-{{ color }}"
hx-get="{{ url }}"
hx-trigger="{{ hx_trigger }}"

View File

@@ -0,0 +1,12 @@
<li class="tw:lg:hidden tw:lg:group-hover:block">
<div class="d-flex align-items-center" data-bs-toggle="collapse" href="#{{ title|slugify }}" role="button"
aria-expanded="false" aria-controls="{{ title|slugify }}">
<span
class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
<hr class="flex-grow-1"/>
<i class="fas fa-chevron-down text-muted tw:lg:before:hidden tw:lg:group-hover:before:inline tw:ml-2 tw:lg:ml-0 tw:lg:group-hover:ml-2"></i>
</div>
</li>
<div class="collapse tw:lg:hidden tw:lg:group-hover:block" id="{{ title|slugify }}">
{{ slot }}
</div>

View File

@@ -0,0 +1,6 @@
<li>
<div class="d-flex align-items-center">
<span class="text-muted small fw-bold text-uppercase tw:lg:hidden tw:lg:group-hover:inline me-2">{{ title }}</span>
<hr class="flex-grow-1"/>
</div>
</li>

View File

@@ -0,0 +1,14 @@
{% load active_link %}
<li>
<a href="{% url url %}"
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views=active css_class="sidebar-active" %}"
{% if tooltip %}
data-bs-placement="right"
data-bs-toggle="tooltip"
data-bs-title="{{ tooltip }}"
{% endif %}>
<i class="{{ icon }} fa-fw"></i>
<span
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
</a>
</li>

View File

@@ -0,0 +1,16 @@
{% load active_link %}
<li>
<a href="{{ url }}"
hx-boost="false"
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views=active css_class="sidebar-active" %}"
{% if tooltip %}
data-bs-placement="right"
data-bs-toggle="tooltip"
data-bs-title="{{ tooltip }}"
{% endif %}>
<i class="{{ icon }} fa-fw"></i>
<span
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">{{ title }}</span>
</a>
</li>

View File

@@ -1,9 +1,9 @@
<div class="row {% if not remove_padding %}p-5{% endif %}">
<div class="col {% if not remove_padding %}p-5{% endif %}">
<div class="text-center">
<i class="{% if icon %}{{ icon }}{% else %}fa-solid fa-circle-xmark{% endif %} tw-text-6xl"></i>
<i class="{% if icon %}{{ icon }}{% else %}fa-solid fa-circle-xmark{% endif %} tw:text-6xl"></i>
<p class="lead mt-4 mb-0">{{ title }}</p>
<p class="tw-text-gray-500">{{ subtitle }}</p>
<p class="tw:text-gray-500">{{ subtitle }}</p>
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
{% load markdown %}
{% load i18n %}
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %}">
<div class="transaction {% if transaction.type == "EX" %}expense{% else %}income{% endif %} tw:group/transaction tw:relative tw:hover:z-10">
<div class="d-flex my-1">
{% if not disable_selection %}
<label class="px-3 d-flex align-items-center justify-content-center">
@@ -8,17 +8,15 @@
id="check-{{ transaction.id }}" aria-label="{% translate 'Select' %}" hx-preserve>
</label>
{% endif %}
<div class="tw-border-s-6 tw-border-e-0 tw-border-t-0 tw-border-b-0 border-bottom
hover:tw-bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw-border-dashed{% else %}tw-border-solid{% endif %}
{% if transaction.type == "EX" %}tw-border-red-500{% else %}tw-border-green-500{% endif %} tw-relative
w-100 transaction-item"
_="on mouseover remove .tw-invisible from the first .transaction-actions in me end
on mouseout add .tw-invisible to the first .transaction-actions in me end">
<div class="row font-monospace tw-text-sm align-items-center">
<div class="tw:border-s-4 tw:border-e-0 tw:border-t-0 tw:border-b-0 border-bottom
tw:hover:bg-zinc-900 p-2 {% if transaction.account.is_asset %}tw:border-dashed{% else %}tw:border-solid{% endif %}
{% if transaction.type == "EX" %}tw:border-red-500{% else %}tw:border-green-500{% endif %} tw:relative
w-100 transaction-item">
<div class="row font-monospace tw:text-sm align-items-center">
<div
class="col-lg-auto col-12 d-flex align-items-center tw-text-2xl lg:tw-text-xl text-lg-center text-center p-0 ps-1">
class="col-lg-auto col-12 d-flex align-items-center tw:text-2xl tw:lg:text-xl text-lg-center text-center p-0 ps-1">
{% if not transaction.deleted %}
<a class="text-decoration-none p-3 tw-text-gray-500"
<a class="text-decoration-none p-3 tw:text-gray-500!"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}"
role="button"
hx-get="{% url 'transaction_pay' transaction_id=transaction.id %}"
@@ -28,41 +26,41 @@
class="fa-regular fa-circle"></i>{% endif %}
</a>
{% else %}
<div class="text-decoration-none p-3 tw-text-gray-500"
<div class="text-decoration-none p-3 tw:text-gray-500!"
title="{% if transaction.is_paid %}{% trans 'Paid' %}{% else %}{% trans 'Projected' %}{% endif %}">
{% if transaction.is_paid %}<i class="fa-regular fa-circle-check"></i>{% else %}<i
class="fa-regular fa-circle"></i>{% endif %}
</div>
{% endif %}
</div>
<div class="col-lg col-12">
<div class="col-lg col-12 {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
{# Date#}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-calendar fa-fw me-1 fa-xs"></i></div>
<div
class="col ps-0">{{ transaction.date|date:"SHORT_DATE_FORMAT" }} • {{ transaction.reference_date|date:"b/Y" }}</div>
</div>
{# Description#}
<div class="mb-2 mb-lg-1 text-white tw-text-base">
<div class="mb-2 mb-lg-1 text-body tw:text-base">
{% spaceless %}
<span class="{% if transaction.description %}me-2{% endif %}">{{ transaction.description }}</span>
{% if transaction.installment_plan and transaction.installment_id %}
<span
class="badge text-bg-secondary">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
class="badge text-bg-secondary mx-1">{{ transaction.installment_id }}/{{ transaction.installment_plan.installment_total_number }}</span>
{% endif %}
{% if transaction.recurring_transaction %}
<span class="text-primary tw-text-xs"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
<span class="text-primary tw:text-xs mx-1"><i class="fa-solid fa-arrows-rotate fa-fw"></i></span>
{% endif %}
{% if transaction.dca_expense_entries.all or transaction.dca_income_entries.all %}
<span class="badge text-bg-secondary">{% trans 'DCA' %}</span>
<span class="badge text-bg-secondary mx-1">{% trans 'DCA' %}</span>
{% endif %}
{% endspaceless %}
</div>
<div class="tw-text-gray-400 tw-text-sm">
<div class="tw:text-gray-400 tw:text-sm">
{# Entities #}
{% with transaction.entities.all as entities %}
{% if entities %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-user-group fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ entities|join:", " }}</div>
</div>
@@ -70,14 +68,14 @@
{% endwith %}
{# Notes#}
{% if transaction.notes %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-align-left fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.notes | limited_markdown | linebreaksbr }}</div>
</div>
{% endif %}
{# Category#}
{% if transaction.category %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-icons fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ transaction.category.name }}</div>
</div>
@@ -85,7 +83,7 @@
{# Tags#}
{% with transaction.tags.all as tags %}
{% if tags %}
<div class="row mb-2 mb-lg-1 tw-text-gray-400">
<div class="row mb-2 mb-lg-1 tw:text-gray-400">
<div class="col-auto pe-1"><i class="fa-solid fa-hashtag fa-fw me-1 fa-xs"></i></div>
<div class="col ps-0">{{ tags|join:", " }}</div>
</div>
@@ -93,7 +91,7 @@
{% endwith %}
</div>
</div>
<div class="col-lg-auto col-12 text-lg-end align-self-end">
<div class="col-lg-auto col-12 text-lg-end align-self-end {% if transaction.account.is_untracked_by or transaction.category.mute or transaction.mute %}tw:brightness-80{% endif %}">
<div class="main-amount mb-2 mb-lg-0">
<c-amount.display
:amount="transaction.amount"
@@ -122,8 +120,8 @@
<div>
{# Item actions#}
<div
class="transaction-actions !tw-absolute tw-left-1/2 tw-top-0 tw--translate-x-1/2 tw--translate-y-1/2 tw-invisible d-flex flex-row card">
<div class="card-body p-1 shadow-lg">
class="transaction-actions tw:absolute! tw:left-1/2 tw:top-0 tw:-translate-x-1/2 tw:-translate-y-1/2 tw:invisible tw:group-hover/transaction:visible d-flex flex-row card">
<div class="card-body p-1 shadow-lg d-flex flex-row gap-1">
{% if not transaction.deleted %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
@@ -132,14 +130,6 @@
hx-get="{% url 'transaction_edit' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas" hx-swap="innerHTML">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Duplicate" %}"
hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"
_="on click if event.ctrlKey set @hx-get to `{% url 'transaction_clone' transaction_id=transaction.id %}?edit=true` then call htmx.process(me) end then trigger ready"
hx-trigger="ready">
<i class="fa-solid fa-clone fa-fw"></i></a>
<a class="btn btn-secondary btn-sm transaction-action"
role="button"
data-bs-toggle="tooltip"
@@ -152,6 +142,43 @@
data-confirm-text="{% translate "Yes, delete it!" %}"
_="install prompt_swal"><i class="fa-solid fa-trash fa-fw text-danger"></i>
</a>
<button class="btn btn-secondary btn-sm transaction-action" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-ellipsis fa-fw"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-md-start">
{% if transaction.account.is_untracked_by %}
<li>
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
<i class="fa-solid fa-eye fa-fw me-2"></i>
<div>
{% translate 'Show on summaries' %}
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by account' %}</div>
</div>
</a>
</li>
{% elif transaction.category.mute %}
<li>
<a class="dropdown-item disabled d-flex align-items-center" aria-disabled="true">
<i class="fa-solid fa-eye fa-fw me-2"></i>
<div>
{% translate 'Show on summaries' %}
<div class="d-block text-body-secondary tw:text-xs tw:font-medium">{% translate 'Controlled by category' %}</div>
</div>
</a>
</li>
{% elif transaction.mute %}
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye fa-fw me-2"></i>{% translate 'Show on summaries' %}</a></li>
{% else %}
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_mute' transaction_id=transaction.id %}" hx-target="closest .transaction" hx-swap="outerHTML"><i class="fa-solid fa-eye-slash fa-fw me-2"></i>{% translate 'Hide from summaries' %}</a></li>
{% endif %}
<li><a class="dropdown-item" href="#" hx-get="{% url 'quick_transaction_add_as_quick_transaction' transaction_id=transaction.id %}"><i class="fa-solid fa-person-running fa-fw me-2"></i>{% translate 'Add as quick transaction' %}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='previous' %}"><i class="fa-solid fa-calendar-minus fa-fw me-2"></i>{% translate 'Move to previous month' %}</a></li>
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_change_month' transaction_id=transaction.id change_type='next' %}"><i class="fa-solid fa-calendar-plus fa-fw me-2"></i>{% translate 'Move to next month' %}</a></li>
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_move_to_today' transaction_id=transaction.id %}"><i class="fa-solid fa-calendar-day fa-fw me-2"></i>{% translate 'Move to today' %}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" hx-get="{% url 'transaction_clone' transaction_id=transaction.id %}"><i class="fa-solid fa-clone fa-fw me-2"></i>{% translate 'Duplicate' %}</a></li>
</ul>
{% else %}
<a class="btn btn-secondary btn-sm transaction-action"
role="button"

View File

@@ -1,9 +1,9 @@
<div class="card mb-2 transaction-item">
<div class="card-body p-2 tw-flex tw-items-center tw-gap-3" data-bs-toggle="collapse" data-bs-target="#{{ transaction.id }}" role="button" aria-expanded="false" aria-controls="{{ transaction.id }}">
<div class="card-body p-2 tw:flex tw:items-center tw:gap-3" data-bs-toggle="collapse" data-bs-target="#{{ transaction.id }}" role="button" aria-expanded="false" aria-controls="{{ transaction.id }}">
<!-- Main visible content -->
<div class="tw-flex flex-lg-row flex-column lg:tw-items-center tw-w-full tw-gap-3">
<div class="tw:flex flex-lg-row flex-column tw:lg:items-center tw:w-full tw:gap-3">
<!-- Type indicator -->
<div class="tw-w-8">
<div class="tw:w-8">
{% if transaction.type == 'IN' %}
<span class="badge bg-success"></span>
{% else %}
@@ -12,7 +12,7 @@
</div>
<!-- Payment status -->
<div class="tw-w-8">
<div class="tw:w-8">
{% if transaction.is_paid %}
<span class="badge bg-success"></span>
{% else %}
@@ -21,13 +21,13 @@
</div>
<!-- Description -->
<div class="tw-flex-grow">
<span class="tw-font-medium">{{ transaction.description }}</span>
<div class="tw:flex-grow">
<span class="tw:font-medium">{{ transaction.description }}</span>
</div>
<!-- Amount -->
<div class="tw-text-right tw-whitespace-nowrap">
<span class="{% if transaction.type == 'IN' %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
<div class="tw:text-right tw:whitespace-nowrap">
<span class="{% if transaction.type == 'IN' %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
{{ transaction.amount }}
</span>
{% if transaction.exchanged_amount %}
@@ -91,4 +91,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<div class="col card shadow">
<div class="card-body">
{% if account.account.group %}
<div class="tw-text-sm mb-2">
<div class="tw:text-sm mb-2">
<span class="badge text-bg-primary ">{{ account.account.group }}</span>
</div>
{% endif %}
@@ -12,11 +12,11 @@
</h5>
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
<div class="tw:text-gray-400">{% translate 'projected income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.income_projected != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<div class="text-end font-monospace tw:text-green-400">
<c-amount.display
:amount="account.income_projected"
:prefix="account.currency.prefix"
@@ -28,7 +28,7 @@
{% endif %}
</div>
{% if account.exchanged and account.exchanged.income_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.income_projected"
:prefix="account.exchanged.currency.prefix"
@@ -38,12 +38,12 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
<div class="tw:text-gray-400">{% translate 'projected expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
{% if account.expense_projected != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<div class="text-end font-monospace tw:text-red-400">
<c-amount.display
:amount="account.expense_projected"
:prefix="account.currency.prefix"
@@ -56,7 +56,7 @@
</div>
</div>
{% if account.exchanged and account.exchanged.expense_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.expense_projected"
:prefix="account.exchanged.currency.prefix"
@@ -66,7 +66,7 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
<div class="tw:text-gray-400">{% translate 'projected total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div
@@ -80,7 +80,7 @@
</div>
</div>
{% if account.exchanged.total_projected and account.exchanged.total_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.total_projected"
:prefix="account.exchanged.currency.prefix"
@@ -91,11 +91,11 @@
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
<div class="tw:text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.income_current != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<div class="text-end font-monospace tw:text-green-400">
<c-amount.display
:amount="account.income_current"
:prefix="account.currency.prefix"
@@ -107,7 +107,7 @@
{% endif %}
</div>
{% if account.exchanged and account.exchanged.income_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.income_current"
:prefix="account.exchanged.currency.prefix"
@@ -117,11 +117,11 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
<div class="tw:text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if account.expense_current != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<div class="text-end font-monospace tw:text-red-400">
<c-amount.display
:amount="account.expense_current"
:prefix="account.currency.prefix"
@@ -133,7 +133,7 @@
{% endif %}
</div>
{% if account.exchanged and account.exchanged.expense_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.expense_current"
:prefix="account.exchanged.currency.prefix"
@@ -143,7 +143,7 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
<div class="tw:text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
@@ -156,7 +156,7 @@
</div>
</div>
{% if account.exchanged and account.exchanged.total_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.total_current"
:prefix="account.exchanged.currency.prefix"
@@ -168,7 +168,7 @@
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
<div class="tw:text-gray-400">{% translate 'final total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
@@ -181,7 +181,7 @@
</div>
</div>
{% if account.exchanged and account.exchanged.total_final %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="account.exchanged.total_final"
:prefix="account.exchanged.currency.prefix"

View File

@@ -1,5 +1,5 @@
<div class="card tw-relative h-100 shadow">
<div class="card tw:relative h-100 shadow">
<div class="card-body">
{{ slot }}
</div>
</div>
</div>

View File

@@ -7,11 +7,11 @@
</h5>
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected income' %}</div>
<div class="tw:text-gray-400">{% translate 'projected income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.income_projected != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<div class="text-end font-monospace tw:text-green-400">
<c-amount.display
:amount="currency.income_projected"
:prefix="currency.currency.prefix"
@@ -23,7 +23,7 @@
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.income_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.income_projected"
:prefix="currency.exchanged.currency.prefix"
@@ -33,12 +33,12 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected expenses' %}</div>
<div class="tw:text-gray-400">{% translate 'projected expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div>
{% if currency.expense_projected != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<div class="text-end font-monospace tw:text-red-400">
<c-amount.display
:amount="currency.expense_projected"
:prefix="currency.currency.prefix"
@@ -51,7 +51,7 @@
</div>
</div>
{% if currency.exchanged and currency.exchanged.expense_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.expense_projected"
:prefix="currency.exchanged.currency.prefix"
@@ -61,7 +61,7 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'projected total' %}</div>
<div class="tw:text-gray-400">{% translate 'projected total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
@@ -74,7 +74,7 @@
</div>
</div>
{% if currency.exchanged.total_projected and currency.exchanged.total_projected %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.total_projected"
:prefix="currency.exchanged.currency.prefix"
@@ -85,11 +85,11 @@
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current income' %}</div>
<div class="tw:text-gray-400">{% translate 'current income' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.income_current != 0 %}
<div class="text-end font-monospace tw-text-green-400">
<div class="text-end font-monospace tw:text-green-400">
<c-amount.display
:amount="currency.income_current"
:prefix="currency.currency.prefix"
@@ -101,7 +101,7 @@
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.income_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.income_current"
:prefix="currency.exchanged.currency.prefix"
@@ -111,11 +111,11 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current expenses' %}</div>
<div class="tw:text-gray-400">{% translate 'current expenses' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
{% if currency.expense_current != 0 %}
<div class="text-end font-monospace tw-text-red-400">
<div class="text-end font-monospace tw:text-red-400">
<c-amount.display
:amount="currency.expense_current"
:prefix="currency.currency.prefix"
@@ -127,7 +127,7 @@
{% endif %}
</div>
{% if currency.exchanged and currency.exchanged.expense_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.expense_current"
:prefix="currency.exchanged.currency.prefix"
@@ -137,7 +137,7 @@
{% endif %}
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'current total' %}</div>
<div class="tw:text-gray-400">{% translate 'current total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
@@ -150,7 +150,7 @@
</div>
</div>
{% if currency.exchanged and currency.exchanged.total_current %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.total_current"
:prefix="currency.exchanged.currency.prefix"
@@ -162,7 +162,7 @@
<hr class="my-3">
<div class="d-flex justify-content-between align-items-baseline mt-2">
<div class="text-end font-monospace">
<div class="tw-text-gray-400">{% translate 'final total' %}</div>
<div class="tw:text-gray-400">{% translate 'final total' %}</div>
</div>
<div class="dotted-line flex-grow-1"></div>
<div class="text-end font-monospace">
@@ -175,7 +175,7 @@
</div>
</div>
{% if currency.exchanged and currency.exchanged.total_final %}
<div class="text-end font-monospace tw-text-gray-500">
<div class="text-end font-monospace tw:text-gray-500">
<c-amount.display
:amount="currency.exchanged.total_final"
:prefix="currency.exchanged.currency.prefix"

View File

@@ -1,16 +1,16 @@
{% load i18n %}
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
<div class="tw:sticky tw:bottom-4 tw:left-0 tw:right-0 tw:z-50 tw:hidden mx-auto tw:w-fit" id="actions-bar"
_="on change from #transactions-list or htmx:afterSettle from window
if #actions-bar then
if no <input[type='checkbox']:checked/> in #transactions-list
if #actions-bar
add .slide-in-bottom-reverse then settle
then add .tw-hidden to #actions-bar
then add .tw:hidden to #actions-bar
then remove .slide-in-bottom-reverse
end
else
if #actions-bar
remove .tw-hidden from #actions-bar
remove .tw:hidden from #actions-bar
then trigger selected_transactions_updated
end
end
@@ -26,20 +26,20 @@
</button>
<ul class="dropdown-menu">
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
<i class="fa-regular fa-square-check tw-text-green-400 me-3"></i>{% translate 'Select All' %}
<i class="fa-regular fa-square-check tw:text-green-400 me-3"></i>{% translate 'Select All' %}
</div>
</li>
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
<i class="fa-regular fa-square tw-text-red-400 me-3"></i>{% translate 'Unselect All' %}
<i class="fa-regular fa-square tw:text-red-400 me-3"></i>{% translate 'Unselect All' %}
</div>
</li>
</ul>
</div>
<div class="vr tw-align-middle"></div>
<div class="vr tw:align-middle"></div>
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_undelete' %}"
hx-include=".transaction"
@@ -60,7 +60,7 @@
_="install prompt_swal">
<i class="fa-solid fa-trash text-danger"></i>
</button>
<div class="vr tw-align-middle"></div>
<div class="vr tw:align-middle"></div>
<div class="btn-group"
_="on selected_transactions_updated from #actions-bar
set realTotal to math.bignumber(0)
@@ -118,10 +118,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Flat Total" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-flat-total"
_="on click
set original_value to my innerText
@@ -138,10 +138,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Real Total" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-real-total"
_="on click
set original_value to my innerText
@@ -158,10 +158,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Mean" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-mean"
_="on click
set original_value to my innerText
@@ -178,10 +178,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Max" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-max"
_="on click
set original_value to my innerText
@@ -198,10 +198,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Min" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-min"
_="on click
set original_value to my innerText
@@ -218,10 +218,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Count" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-count"
_="on click
set original_value to my innerText

View File

@@ -1,6 +1,6 @@
{% spaceless %}
{% load i18n %}
<span class="tw-text-xs text-white-50 mx-1"
<span class="tw:text-xs text-white-50 mx-1"
data-bs-toggle="tooltip"
data-bs-title="{{ content }}">
<i class="{% if not icon %}fa-solid fa-circle-question{% else %}{{ icon }}{% endif %} fa-fw"></i>

View File

@@ -1,9 +1,9 @@
<div class="card tw-relative h-100 shadow">
<div class="tw-absolute tw-h-8 tw-w-8 tw-right-2 tw-top-2 tw-bg-{{ color }}-300 tw-text-{{ color }}-800 text-center align-items-center d-flex justify-content-center rounded-2">
<div class="card tw:relative h-100 shadow">
<div class="tw:absolute tw:h-8 tw:w-8 tw:right-2 tw:top-2 tw:bg-{{ color }}-300 tw:text-{{ color }}-800 text-center align-items-center d-flex justify-content-center rounded-2">
{% if icon %}<i class="{{ icon }}"></i>{% else %}<span class="fw-bold">{{ title.0 }}</span>{% endif %}
</div>
<div class="card-body">
<h5 class="tw-text-{{ color }}-400 fw-bold tw-mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
<h5 class="tw:text-{{ color }}-400 fw-bold tw:mr-[50px]" {{ attrs }}>{{ title }}{% if help_text %}<c-ui.help-icon :content="help_text" icon=""></c-ui.help-icon>{% endif %}</h5>
{{ slot }}
</div>
</div>

View File

@@ -1,28 +1,28 @@
{% load i18n %}
<div class="progress-stacked">
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_projected|floatformat:"2u" }}%">
<div class="progress-bar progress-bar-striped !tw-bg-green-300"
<div class="progress-bar progress-bar-striped tw:bg-green-300!"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% trans 'Projected Income' %} ({{ percentage.percentages.income_projected|floatformat:2 }}%)">
</div>
</div>
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Income' %} ({{ percentage.percentages.income_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.income_current|floatformat:"2u" }}%">
<div class="progress-bar !tw-bg-green-400"
<div class="progress-bar tw:bg-green-400!"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% trans 'Current Income' %} ({{ p.percentages.income_current|floatformat:2 }}%)">
</div>
</div>
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_projected|floatformat:"2u" }}%">
<div class="progress-bar progress-bar-striped !tw-bg-red-300"
<div class="progress-bar progress-bar-striped tw:bg-red-300!"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% trans 'Projected Expenses' %} ({{ percentage.percentages.expense_projected|floatformat:2 }}%)">
</div>
</div>
<div class="progress position-relative" role="progressbar" aria-label="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)" aria-valuenow="{{ percentage.percentages.expense_projected|floatformat:0 }}" aria-valuemin="0" aria-valuemax="100" style="width: {{ percentage.percentages.expense_current|floatformat:"2u" }}%">
<div class="progress-bar !tw-bg-red-400"
<div class="progress-bar tw:bg-red-400!"
data-bs-toggle="tooltip"
data-bs-placement="top"
title="{% trans 'Current Expenses' %} ({{ percentage.percentages.expense_current|floatformat:2 }}%)">

View File

@@ -1,45 +1,48 @@
{% load i18n %}
<div class="tw-sticky tw-bottom-4 tw-left-0 tw-right-0 tw-z-50 tw-hidden mx-auto tw-w-fit" id="actions-bar"
<div class="tw:sticky tw:bottom-4 tw:left-0 tw:right-0 tw:z-50 tw:hidden mx-auto tw:w-fit" id="actions-bar"
_="on change from #transactions-list or htmx:afterSettle from window
if #actions-bar then
if no <input[type='checkbox']:checked/> in #transactions-list
if #actions-bar
add .slide-in-bottom-reverse then settle
then add .tw-hidden to #actions-bar
then add .tw:hidden to #actions-bar
then remove .slide-in-bottom-reverse
end
else
if #actions-bar
remove .tw-hidden from #actions-bar
set #selected-count's innerHTML to length of <input[type='checkbox']:checked/> in #transactions-list
then remove .tw:hidden from #actions-bar
then trigger selected_transactions_updated
end
end
end
end">
<div class="card slide-in-bottom">
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3">
<div class="card slide-in-bottom tw:max-w-[90vw]">
<div class="card-body p-2 d-flex justify-content-between align-items-center gap-3 tw:overflow-x-auto">
{% spaceless %}
<div class="tw:font-bold tw:text-md ms-2" id="selected-count">0</div>
<div class="vr tw:align-middle"></div>
<div class="dropdown">
<button class="btn btn-secondary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
aria-expanded="false" data-bs-popper-config='{"strategy":"fixed"}'>
<i class="fa-regular fa-square-check fa-fw"></i>
</button>
<ul class="dropdown-menu">
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
_="on click set <#transactions-list .transaction:not([style*='display: none']) input[type='checkbox']/>'s checked to true then call me.blur() then trigger change">
<i class="fa-regular fa-square-check tw-text-green-400 me-3"></i>{% translate 'Select All' %}
<i class="fa-regular fa-square-check tw:text-green-400 me-3"></i>{% translate 'Select All' %}
</div>
</li>
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
_="on click set <#transactions-list input[type='checkbox']/>'s checked to false then call me.blur() then trigger change">
<i class="fa-regular fa-square tw-text-red-400 me-3"></i>{% translate 'Unselect All' %}
<i class="fa-regular fa-square tw:text-red-400 me-3"></i>{% translate 'Unselect All' %}
</div>
</li>
</ul>
</div>
<div class="vr tw-align-middle"></div>
<div class="vr tw:align-middle"></div>
<div class="btn-group">
<button class="btn btn-secondary btn-sm"
hx-get="{% url 'transactions_bulk_edit' %}"
@@ -50,23 +53,24 @@
<i class="fa-solid fa-pencil"></i>
</button>
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
data-bs-toggle="dropdown" data-bs-popper-config='{"strategy":"fixed"}' aria-expanded="false"
data-bs-auto-close="outside">
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
</button>
<ul class="dropdown-menu">
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
hx-get="{% url 'transactions_bulk_unpay' %}"
hx-include=".transaction">
<i class="fa-regular fa-circle tw-text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
<i class="fa-regular fa-circle tw:text-red-400 fa-fw me-3"></i>{% translate 'Mark as unpaid' %}
</div>
</li>
<li>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
hx-get="{% url 'transactions_bulk_pay' %}"
hx-include=".transaction">
<i class="fa-regular fa-circle-check tw-text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
<i class="fa-regular fa-circle-check tw:text-green-400 fa-fw me-3"></i>{% translate 'Mark as paid' %}
</div>
</li>
</ul>
@@ -91,7 +95,7 @@
_="install prompt_swal">
<i class="fa-solid fa-trash text-danger"></i>
</button>
<div class="vr tw-align-middle"></div>
<div class="vr tw:align-middle"></div>
<div class="btn-group"
_="on selected_transactions_updated from #actions-bar
set realTotal to math.bignumber(0)
@@ -141,7 +145,8 @@
<span class="d-none d-md-inline-block" id="real-total-front">0</span>
</button>
<button type="button" class="btn btn-sm btn-secondary dropdown-toggle dropdown-toggle-split"
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside"
data-bs-popper-config='{"strategy":"fixed"}'>
<span class="visually-hidden">{% trans "Toggle Dropdown" %}</span>
</button>
@@ -149,10 +154,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Flat Total" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-flat-total"
_="on click
set original_value to my innerText
@@ -169,10 +174,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Real Total" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-real-total"
_="on click
set original_value to my innerText
@@ -189,10 +194,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Mean" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-mean"
_="on click
set original_value to my innerText
@@ -209,10 +214,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Max" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-max"
_="on click
set original_value to my innerText
@@ -229,10 +234,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Min" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-min"
_="on click
set original_value to my innerText
@@ -249,10 +254,10 @@
<li>
<div class="dropdown-item-text p-0">
<div>
<div class="text-body-secondary tw-text-xs tw-font-medium px-3">
<div class="text-body-secondary tw:text-xs tw:font-medium px-3">
{% trans "Count" %}
</div>
<div class="dropdown-item px-3 tw-cursor-pointer"
<div class="dropdown-item px-3 tw:cursor-pointer"
id="calc-menu-count"
_="on click
set original_value to my innerText

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Currencies' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -2,10 +2,10 @@
{% load i18n %}
<div class="container-fluid px-md-3 py-3 column-gap-5">
<div class="d-lg-flex justify-content-between mb-3 w-100">
<div class="tw-text-3xl fw-bold font-monospace d-flex align-items-center">
<div class="tw:text-3xl fw-bold font-monospace d-flex align-items-center">
{{ strategy.name }}
</div>
<div class="tw-text-sm text-lg-end mt-2 mt-lg-0">
<div class="tw:text-sm text-lg-end mt-2 mt-lg-0">
<div class="mb-2">
<span class="badge rounded-pill text-bg-secondary">{{ strategy.payment_currency.name }}</span> x <span class="badge rounded-pill text-bg-secondary">{{ strategy.target_currency.name }}</span>
</div>
@@ -19,7 +19,7 @@
• {{ strategy.current_price.1|date:"SHORT_DATETIME_FORMAT" }}
</c-amount.display>
{% else %}
<div class="tw-text-red-400">{% trans "No exchange rate available" %}</div>
<div class="tw:text-red-400">{% trans "No exchange rate available" %}</div>
{% endif %}
</div>
</div>
@@ -30,7 +30,7 @@
<div class="card">
<div class="card-body">
{% spaceless %}
<div class="card-title tw-text-xl">{% trans "Entries" %}<span>
<div class="card-title tw:text-xl">{% trans "Entries" %}<span>
<a class="text-decoration-none p-1 category-action"
role="button"
data-bs-toggle="tooltip"
@@ -190,7 +190,7 @@
<div class="card-body">
<h5 class="card-title">{% trans "Total P/L" %}</h5>
<div
class="card-text {% if strategy.total_profit_loss >= 0 %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
class="card-text {% if strategy.total_profit_loss >= 0 %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
<c-amount.display
:amount="strategy.total_profit_loss"
:prefix="strategy.payment_currency.prefix"
@@ -206,7 +206,7 @@
<div class="card-body">
<h5 class="card-title">{% trans "Total % P/L" %}</h5>
<div
class="card-text {% if strategy.total_profit_loss >= 0 %}tw-text-green-400{% else %}tw-text-red-400{% endif %}">
class="card-text {% if strategy.total_profit_loss >= 0 %}tw:text-green-400{% else %}tw:text-red-400{% endif %}">
{{ strategy.total_profit_loss_percentage|floatformat:2 }}%
</div>
</div>
@@ -451,7 +451,7 @@
<div class="card">
<div class="card-body">
<h5 class="card-title">{% trans "Investment Frequency" %}</h5>
<p class="card-text tw-text-gray-400">
<p class="card-text tw:text-gray-400">
{% trans "The straighter the blue line, the more consistent your DCA strategy is." %}
</p>
<canvas id="frequencyChart"></canvas>

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Dollar Cost Average Strategies' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"
@@ -25,12 +25,12 @@
<a href="{% url 'dca_strategy_detail_index' strategy_id=strategy.id %}" hx-boost="true"
class="text-decoration-none card-body">
<div class="">
<div class="card-title tw-text-xl">{{ strategy.name }}</div>
<div class="card-text tw-text-gray-400">{{ strategy.notes }}</div>
<div class="card-title tw:text-xl">{{ strategy.name }}</div>
<div class="card-text tw:text-gray-400">{{ strategy.notes }}</div>
</div>
</a>
<div class="card-footer text-end">
<a class="text-decoration-none tw-text-gray-400 p-1"
<a class="text-decoration-none tw:text-gray-400 p-1"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Edit" %}"

View File

@@ -1,9 +1,9 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Entities' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -1,10 +1,10 @@
{% load currency_display %}
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Exchange Rates' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -58,7 +58,7 @@
<nav aria-label="{% translate 'Page navigation' %}">
<ul class="pagination justify-content-center mt-5">
<li class="page-item">
<a class="page-link tw-cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
<a class="page-link tw:cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"
@@ -79,13 +79,13 @@
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
{% if page_obj.number == page_number %}
<li class="page-item active">
<a class="page-link tw-cursor-pointer">
<a class="page-link tw:cursor-pointer">
{{ page_number }}
</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link tw-cursor-pointer"
<a class="page-link tw:cursor-pointer"
hx-get="{% url 'exchange_rates_list_pair' %}"
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-target="#exchange-rates-table"
@@ -104,7 +104,7 @@
</a>
</li>
<li class="page-item">
<a class="page-link tw-cursor-pointer"
<a class="page-link tw:cursor-pointer"
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"
@@ -115,7 +115,7 @@
</li>
{% endif %}
<li class="page-item">
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw-cursor-pointer"
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw:cursor-pointer"
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"

View File

@@ -1,10 +1,10 @@
{% load currency_display %}
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Automatic Exchange Rates' %}<span>
<a class="text-decoration-none tw-text-2xl p-1 category-action"
<a class="text-decoration-none tw:text-2xl p-1 category-action"
role="button"
data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}"

View File

@@ -58,7 +58,7 @@
<nav aria-label="{% translate 'Page navigation' %}">
<ul class="pagination justify-content-center mt-5">
<li class="page-item">
<a class="page-link tw-cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
<a class="page-link tw:cursor-pointer {% if not page_obj.has_previous %}disabled{% endif %}"
hx-get="{% if page_obj.has_previous %}{% url 'exchange_rates_list_pair' %}{% endif %}"
hx-vals='{"page": 1, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"
@@ -79,13 +79,13 @@
{% if page_number <= page_obj.number|add:3 and page_number >= page_obj.number|add:-3 %}
{% if page_obj.number == page_number %}
<li class="page-item active">
<a class="page-link tw-cursor-pointer">
<a class="page-link tw:cursor-pointer">
{{ page_number }}
</a>
</li>
{% else %}
<li class="page-item">
<a class="page-link tw-cursor-pointer"
<a class="page-link tw:cursor-pointer"
hx-get="{% url 'exchange_rates_list_pair' %}"
hx-vals='{"page": {{ page_number }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-target="#exchange-rates-table"
@@ -104,7 +104,7 @@
</a>
</li>
<li class="page-item">
<a class="page-link tw-cursor-pointer"
<a class="page-link tw:cursor-pointer"
hx-get="{% url 'exchange_rates_list_pair' %}" hx-target="#exchange-rates-table"
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"
@@ -115,7 +115,7 @@
</li>
{% endif %}
<li class="page-item">
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw-cursor-pointer"
<a class="page-link {% if not page_obj.has_next %}disabled{% endif %} tw:cursor-pointer"
hx-get="{% if page_obj.has_next %}{% url 'exchange_rates_list_pair' %}{% endif %}"
hx-vals='{"page": {{ page_obj.paginator.num_pages }}, "from": "{{ from_currency|default_if_none:"" }}", "to": "{{ to_currency|default_if_none:"" }}"}'
hx-include="#filter, #order"

View File

@@ -8,7 +8,7 @@
{% block body %}
{% if message %}
<div class="alert alert-info" role="alert" id="msg" hx-preserve="true">
<h6 class="alert-heading tw-italic tw-font-bold">{% trans 'A message from the author' %}</h6>
<h6 class="alert-heading tw:italic tw:font-bold">{% trans 'A message from the author' %}</h6>
<hr>
<p class="mb-0">{{ message|linebreaksbr }}</p>
</div>

View File

@@ -1,11 +1,11 @@
{% load i18n %}
<div class="container px-md-3 py-3 column-gap-5">
<div class="tw-text-3xl fw-bold font-monospace tw-w-full mb-3">
<div class="tw:text-3xl fw-bold font-monospace tw:w-full mb-3">
{% spaceless %}
<div>{% translate 'Import Profiles' %}<span>
<span class="dropdown" data-bs-toggle="tooltip"
data-bs-title="{% translate "Add" %}">
<a class="text-decoration-none tw-text-2xl p-1" role="button"
<a class="text-decoration-none tw:text-2xl p-1" role="button"
data-bs-toggle="dropdown"
data-bs-title="{% translate "Add" %}" aria-expanded="false">
<i class="fa-solid fa-circle-plus fa-fw"></i>

View File

@@ -15,20 +15,20 @@
{% for run in runs %}
<div class="col">
<div class="card">
<div class="card-header tw-text-sm {% if run.status == run.Status.QUEUED %}tw-text-white{% elif run.status == run.Status.PROCESSING %}text-warning{% elif run.status == run.Status.FINISHED %}text-success{% else %}text-danger{% endif %}">
<div class="card-header tw:text-sm {% if run.status == run.Status.QUEUED %}text-body{% elif run.status == run.Status.PROCESSING %}text-warning{% elif run.status == run.Status.FINISHED %}text-success{% else %}text-danger{% endif %}">
<span><i class="fa-solid {% if run.status == run.Status.QUEUED %}fa-hourglass-half{% elif run.status == run.Status.PROCESSING %}fa-spinner{% elif run.status == run.Status.FINISHED %}fa-check{% else %}fa-xmark{% endif %} fa-fw me-2"></i>{{ run.get_status_display }}</span>
</div>
<div class="card-body">
<h5 class="card-title"><i class="fa-solid fa-hashtag me-1 tw-text-xs tw-text-gray-400"></i>{{ run.id }}<span class="tw-text-xs tw-text-gray-400 ms-1">({{ run.file_name }})</span></h5>
<h5 class="card-title"><i class="fa-solid fa-hashtag me-1 tw:text-xs tw:text-gray-400"></i>{{ run.id }}<span class="tw:text-xs tw:text-gray-400 ms-1">({{ run.file_name }})</span></h5>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 w-100 g-4">
<div class="col">
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="text-body-secondary tw-text-xs tw-font-medium">
<div class="text-body-secondary tw:text-xs tw:font-medium">
{% trans 'Total Items' %}
</div>
<div class="tw-text-sm">
<div class="tw:text-sm">
{{ run.total_rows }}
</div>
</div>
@@ -38,10 +38,10 @@
<div class="col">
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="text-body-secondary tw-text-xs tw-font-medium">
<div class="text-body-secondary tw:text-xs tw:font-medium">
{% trans 'Processed Items' %}
</div>
<div class="tw-text-sm">
<div class="tw:text-sm">
{{ run.processed_rows }}
</div>
</div>
@@ -51,10 +51,10 @@
<div class="col">
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="text-body-secondary tw-text-xs tw-font-medium">
<div class="text-body-secondary tw:text-xs tw:font-medium">
{% trans 'Skipped Items' %}
</div>
<div class="tw-text-sm">
<div class="tw:text-sm">
{{ run.skipped_rows }}
</div>
</div>
@@ -64,10 +64,10 @@
<div class="col">
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="text-body-secondary tw-text-xs tw-font-medium">
<div class="text-body-secondary tw:text-xs tw:font-medium">
{% trans 'Failed Items' %}
</div>
<div class="tw-text-sm">
<div class="tw:text-sm">
{{ run.failed_rows }}
</div>
</div>
@@ -77,10 +77,10 @@
<div class="col">
<div class="d-flex flex-row">
<div class="d-flex flex-column">
<div class="text-body-secondary tw-text-xs tw-font-medium">
<div class="text-body-secondary tw:text-xs tw:font-medium">
{% trans 'Successful Items' %}
</div>
<div class="tw-text-sm">
<div class="tw:text-sm">
{{ run.successful_rows }}
</div>
</div>

View File

@@ -5,7 +5,7 @@
{% block title %}{% translate 'Logs for' %} #{{ run.id }}{% endblock %}
{% block body %}
<div class="card tw-max-h-full tw-overflow-auto">
<div class="card tw:max-h-full tw:overflow-auto">
<div class="card-body">
{{ run.logs|linebreaks }}
</div>

View File

@@ -0,0 +1,16 @@
{% load cache_access %}
{% load settings %}
{% load static %}
{% load i18n %}
{% load active_link %}
<nav class="navbar border-bottom bg-body-tertiary d-flex d-lg-none" hx-boost="true">
<div class="container-fluid">
<a class="navbar-brand fw-bold text-primary font-base" href="{% url 'index' %}">
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="40" width="40" title="WYGIWYH"/>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebar"
aria-controls="sidebar" aria-label={% translate "Toggle navigation" %}>
<span class="navbar-toggler-icon"></span>
</button>
</div>
</nav>

View File

@@ -1,3 +1,4 @@
{% load cache_access %}
{% load settings %}
{% load static %}
{% load i18n %}
@@ -12,7 +13,7 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 nav-underline" hx-push-url="true">
<ul class="navbar-nav me-auto mb-3 mb-lg-0 nav-underline" hx-push-url="true">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% active_link views='monthly_overview||yearly_overview_currency||yearly_overview_account||calendar' %}"
href="#"
@@ -161,16 +162,22 @@
</ul>
</li>
</ul>
<ul class="navbar-nav mt-3 mb-2 mb-lg-0 mt-lg-0">
<li class="nav-item text-center w-100">
<a class="nav-item tw-text-2xl tw-cursor-pointer me-lg-4"
<ul class="navbar-nav mb-2 mb-lg-0 gap-3">
{% get_update_check as update_check %}
{% if update_check.update_available %}
<li class="nav-item my-auto">
<a class="badge text-bg-secondary text-decoration-none tw:cursor-pointer" href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i class="fa-solid fa-circle-info fa-fw me-2"></i>v.{{ update_check.latest_version }} {% translate 'is available' %}!</a>
</li>
{% endif %}
<li class="nav-item">
<div class="nav-link tw:lg:text-2xl! tw:cursor-pointer"
data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="{% trans "Calculator" %}"
_="on click trigger show on #calculator">
<i class="fa-solid fa-calculator"></i>
</a>
<span class="d-lg-none d-inline">{% trans "Calculator" %}</span>
</div>
</li>
<li class="text-center w-100">{% include 'includes/navbar/user_menu.html' %}</li>
<li class="w-100">{% include 'includes/navbar/user_menu.html' %}</li>
</ul>
</div>
</div>

View File

@@ -1,12 +1,10 @@
{% load settings %}
{% load i18n %}
<div class="dropdown">
<a class="tw-text-2xl" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-user"></i>
</a>
<div class="btn btn-secondary btn-sm" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-solid fa-cog"></i>
</div>
<ul class="dropdown-menu dropdown-menu-start dropdown-menu-lg-end">
<li class="dropdown-item-text">{{ user.email }}</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item"
hx-get="{% url 'user_settings' %}"
hx-target="#generic-offcanvas"

View File

@@ -1,4 +1,4 @@
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
<div id="persistent-generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
@@ -6,7 +6,7 @@
on force_hide_offcanvas call bootstrap.Offcanvas.getOrCreateInstance(me).hide() end
on hidden.bs.offcanvas set my innerHTML to '' end">
</div>
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
<div id="persistent-generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
@@ -16,7 +16,7 @@
</div>
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl"
<div id="generic-offcanvas" class="offcanvas offcanvas-end offcanvas-size-xl tw:z-1100!"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end
@@ -24,7 +24,7 @@
on htmx:beforeOnLoad[detail.boosted] call bootstrap.Offcanvas.getOrCreateInstance(me).hide()
on hidden.bs.offcanvas set my innerHTML to '' end">
</div>
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl"
<div id="generic-offcanvas-left" class="offcanvas offcanvas-start offcanvas-size-xl tw:z-1100!"
data-bs-backdrop="static"
tabindex="-1"
_="on htmx:afterSettle call bootstrap.Offcanvas.getOrCreateInstance(me).show() end

View File

@@ -1,14 +1,14 @@
{# We use this to preload dynamically generated tailwind classes so the compiler can build them ahead of time #}
<div class="tw-text-blue-800"></div>
<div class="tw-text-yellow-800"></div>
<div class="tw-text-red-800"></div>
<div class="tw-text-green-800"></div>
<div class="tw-text-blue-400"></div>
<div class="tw-text-yellow-400"></div>
<div class="tw-text-red-400"></div>
<div class="tw-text-green-400"></div>
<div class="tw-bg-blue-300"></div>
<div class="tw-bg-yellow-300"></div>
<div class="tw-bg-red-300"></div>
<div class="tw-bg-green-300"></div>
<div class="tw:text-blue-800"></div>
<div class="tw:text-yellow-800"></div>
<div class="tw:text-red-800"></div>
<div class="tw:text-green-800"></div>
<div class="tw:text-blue-400"></div>
<div class="tw:text-yellow-400"></div>
<div class="tw:text-red-400"></div>
<div class="tw:text-green-400"></div>
<div class="tw:bg-blue-300"></div>
<div class="tw:bg-yellow-300"></div>
<div class="tw:bg-red-300"></div>
<div class="tw:bg-green-300"></div>

View File

@@ -8,6 +8,7 @@ behavior htmx_error_handler
title: '{% trans "Access Denied" %}',
text: '{% trans "You do not have permission to perform this action or access this resource." %}',
icon: 'warning',
timer: 60000,
customClass: {
confirmButton: 'btn btn-warning' -- Optional: different button style
},
@@ -18,6 +19,7 @@ behavior htmx_error_handler
title: '{% trans "Something went wrong loading your data" %}',
text: '{% trans "Try reloading the page or check the console for more information." %}',
icon: 'error',
timer: 60000,
customClass: {
confirmButton: 'btn btn-primary'
},

View File

@@ -1,15 +1,19 @@
<script type="text/hyperscript">
on paid if body do not include #settings-mute-sound
js
volume = JSON.parse(document.getElementById('volume').textContent) / 10
paidSound.pause()
paidSound.currentTime = 0
paidSound.volume = volume
paidSound.play()
end
end
on unpaid if body do not include #settings-mute-sound
js
volume = JSON.parse(document.getElementById('volume').textContent) / 10
unpaidSound.pause()
unpaidSound.currentTime = 0
unpaidSound.volume = volume
unpaidSound.play()
end
end

View File

@@ -0,0 +1,289 @@
{% load active_link %}
{% load cache_access %}
{% load settings %}
{% load i18n %}
{% load static %}
<div
class="tw:group tw:lg:w-16 tw:lg:hover:w-112 tw:transition-all tw:duration-100 tw:fixed tw:top-0 tw:start-0 tw:h-full tw:z-1020">
<nav
id="sidebar"
hx-boost="true"
data-bs-scroll="true"
class="offcanvas-lg offcanvas-start d-lg-flex flex-column position-fixed top-0 start-0 h-100 bg-body-tertiary shadow-sm tw:z-1020 tw:lg:w-16 tw:lg:group-hover:w-104 tw:transition-all tw:duration-100 tw:overflow-hidden">
<div
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-8 tw:h-full tw:bg-transparent tw:pointer-events-auto tw:z-10"></div>
<a href="{% url 'index' %}" class="d-none d-lg-flex tw:justify-start p-3 text-decoration-none">
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
</a>
<div class="offcanvas-header">
<a href="{% url 'index' %}" class="offcanvas-title d-flex tw:justify-start text-decoration-none">
<img src="{% static 'img/logo-icon.svg' %}" alt="WYGIWYH Logo" height="30" width="30" title="WYGIWYH"/>
<span class="fs-4 fw-bold ms-3 tw:lg:invisible tw:lg:group-hover:visible">WYGIWYH</span>
</a>
<button type="button" class="btn-close" data-bs-target="#sidebar" data-bs-dismiss="offcanvas"
aria-label={% translate 'Close' %}></button>
</div>
<hr class="m-0">
<ul class="list-unstyled p-3 d-flex flex-column gap-0 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
style="animation-duration: 100ms">
<c-components.sidebar-menu-item
title="{% translate 'Monthly' %}"
url='monthly_index'
active="monthly_overview"
icon="fa-solid fa-list">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Yearly' %}"
url='yearly_index'
active="yearly_overview_currency||yearly_overview_account"
icon="fa-solid fa-calendar-plus">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Calendar' %}"
url='calendar_index'
active="calendar"
icon="fa-solid fa-calendar-days">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Insights' %}"
url='insights_index'
active="insights_index"
icon="fa-solid fa-chart-simple">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Net Worth' %}"
url='net_worth'
active="net_worth"
icon="fa-solid fa-money-bill">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'All' %}"
url='transactions_all_index'
active="transactions_all_index"
icon="fa-solid fa-right-left">
</c-components.sidebar-menu-item>
{% settings "ENABLE_SOFT_DELETE" as enable_soft_delete %}
{% if enable_soft_delete %}
<c-components.sidebar-menu-item
title="{% translate 'Trash Can' %}"
url='transactions_trash_index'
active="transactions_trash_index"
icon="fa-solid fa-trash">
</c-components.sidebar-menu-item>
{% endif %}
<c-components.sidebar-menu-item
title="{% translate 'Quick Transactions' %}"
url='quick_transactions_index'
active="quick_transactions_index"
icon="fa-solid fa-person-running">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Installment Plans' %}"
url='installment_plans_index'
active="installment_plans_index"
icon="fa-solid fa-divide">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Recurring Transactions' %}"
url='recurring_trasanctions_index'
active="recurring_trasanctions_index"
icon="fa-solid fa-repeat">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-header title="{% translate 'Tools' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Dollar Cost Average Tracker' %}"
url='dca_strategy_index'
active="dca_strategy_index||dca_strategy_detail_index"
icon="fa-solid fa-magnifying-glass-chart">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Unit Price Calculator' %}"
url='unit_price_calculator'
active="unit_price_calculator"
icon="fa-solid fa-tags">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Currency Converter' %}"
url='currency_converter'
active="currency_converter"
icon="fa-solid fa-money-bill-transfer">
</c-components.sidebar-menu-item>
<div>
<hr>
</div>
<div type="button"
data-bs-toggle="collapse"
data-bs-target="#collapsible-panel"
aria-expanded="false"
aria-controls="collapsible-panel"
class="tw:text-wrap tw:lg:text-nowrap tw:lg:text-sm d-flex align-items-center text-decoration-none p-2 rounded-3 sidebar-item {% active_link views='tags_index||entities_index||categories_index||accounts_index||account_groups_index||currencies_index||exchange_rates_index||rules_index||import_profiles_index||automatic_exchange_rates_index||export_index||users_index' css_class="sidebar-active" %}">
<i class="fa-solid fa-toolbox fa-fw"></i>
<span
class="ms-3 fw-medium tw:lg:invisible tw:lg:group-hover:visible tw:lg:group-hover:truncate tw:lg:group-focus:truncate tw:lg:group-hover:text-ellipsis tw:lg:group-focus:text-ellipsis">
{% translate 'Management' %}
</span>
</div>
</ul>
<div class="mt-auto p-2 w-100">
<div id="collapsible-panel"
class="p-0 collapse tw:absolute tw:bottom-0 tw:left-0 tw:w-full tw:z-30 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:max-h-dvh">
<div class="tw:h-dvh tw:backdrop-blur-3xl">
<!-- Header -->
<div
class="tw:flex tw:justify-between tw:items-center tw:p-4 tw:border-b tw:border-gray-600 tw:lg:hidden tw:lg:group-hover:flex">
<h5 class="tw:text-lg tw:font-semibold tw:text-gray-800 tw:lg:invisible tw:lg:group-hover:visible">
{% trans 'Management' %}
</h5>
<button type="button" class="btn-close tw:lg:hidden tw:lg:group-hover:inline" aria-label="Close"
data-bs-toggle="collapse"
data-bs-target="#collapsible-panel"
aria-expanded="true"
aria-controls="collapsible-panel">
</button>
</div>
<ul class="list-unstyled p-3 d-flex flex-column gap-1 tw:group-hover:lg:overflow-y-auto tw:lg:overflow-y-hidden tw:overflow-y-auto tw:overflow-x-hidden tw:text-nowrap tw:lg:group-hover:animate-[disable-pointer-events]"
style="animation-duration: 100ms">
<c-components.sidebar-menu-header title="{% translate 'Transactions' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Categories' %}"
url='categories_index'
active="categories_index"
icon="fa-solid fa-icons">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Tags' %}"
url='tags_index'
active="tags_index"
icon="fa-solid fa-hashtag">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Entities' %}"
url='entities_index'
active="entities_index"
icon="fa-solid fa-user-group">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-header title="{% translate 'Accounts' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Accounts' %}"
url='accounts_index'
active="accounts_index"
icon="fa-solid fa-wallet">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Account Groups' %}"
url='account_groups_index'
active="account_groups_index"
icon="fa-solid fa-wallet">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-header title="{% translate 'Currencies' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Currencies' %}"
url='currencies_index'
active="currencies_index"
icon="fa-solid fa-coins">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Exchange Rates' %}"
url='exchange_rates_index'
active="exchange_rates_index"
icon="fa-solid fa-right-left">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-header title="{% translate 'Automation' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Rules' %}"
url='rules_index'
active="rules_index"
icon="fa-solid fa-pen-ruler">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-item
title="{% translate 'Import' %}"
url='import_profiles_index'
active="import_profiles_index"
icon="fa-solid fa-file-import">
</c-components.sidebar-menu-item>
{% if user.is_superuser %}
<c-components.sidebar-menu-item
title="{% translate 'Export and Restore' %}"
url='export_index'
active="export_index"
icon="fa-solid fa-file-export">
</c-components.sidebar-menu-item>
{% endif %}
<c-components.sidebar-menu-item
title="{% translate 'Automatic Exchange Rates' %}"
url='automatic_exchange_rates_index'
active="automatic_exchange_rates_index"
icon="fa-solid fa-right-left">
</c-components.sidebar-menu-item>
{% if user.is_superuser %}
<c-components.sidebar-menu-header title="{% translate 'Admin' %}"></c-components.sidebar-menu-header>
<c-components.sidebar-menu-item
title="{% translate 'Users' %}"
url='users_index'
active="users_index"
icon="fa-solid fa-users">
</c-components.sidebar-menu-item>
<c-components.sidebar-menu-url-item
title="{% translate 'Django Admin' %}"
tooltip="{% translate "Only use this if you know what you're doing" %}"
url='/admin/'
icon="fa-solid fa-screwdriver-wrench">
</c-components.sidebar-menu-url-item>
{% endif %}
</ul>
</div>
</div>
{% get_update_check as update_check %}
{% if update_check.update_available %}
<div class="my-3">
<a class="px-3 badge text-bg-primary text-decoration-none tw:cursor-pointer w-100 tw:text-xs!"
href="https://github.com/eitchtee/WYGIWYH/releases/latest" target="_blank"><i
class="fa-solid fa-circle-exclamation fa-fw me-2"></i><span
class="tw:lg:invisible tw:lg:group-hover:visible">v.{{ update_check.latest_version }} {% translate 'is available' %}!</span></a>
</div>
{% endif %}
<div class="btn-group w-100" role="group">
<button type="button" class="btn btn-secondary btn-sm" data-bs-toggle="tooltip"
data-bs-title="{% trans "Calculator" %}"
_="on click trigger show on #calculator">
<i class="fa-solid fa-calculator fa-fw"></i>
</button>
</div>
</div>
<div>
<hr class="my-1">
<div
class="ps-4 pe-2 py-2 d-flex align-items-center text-body-secondary text-decoration-none justify-content-between tw:text-wrap tw:lg:text-nowrap">
<div>
<i class="fa-solid fa-circle-user"></i>
<strong class="ms-2 tw:lg:invisible tw:lg:group-hover:visible">{{ user.email }}</strong>
</div>
<div class="tw:lg:invisible tw:lg:group-hover:visible">
{% include 'includes/navbar/user_menu.html' %}
</div>
</div>
</div>
</nav>
<div
class="tw:hidden tw:lg:group-hover:block tw:absolute tw:top-0 tw:left-104 tw:w-16 tw:h-full tw:bg-transparent"></div>
</div>

Some files were not shown because too many files have changed in this diff Show More