Compare commits

...
Sign in to create a new pull request.

324 commits
api ... main

Author SHA1 Message Date
fc68368f3f
Fix entrypoint script path and update permissions for execution
All checks were successful
Build and push Docker image / build (push) Successful in 1m36s
2025-05-08 21:07:28 +02:00
100773c958
Remove version tag from Nummi image in Docker Compose configuration
All checks were successful
Build and push Docker image / build (push) Successful in 1m35s
2025-05-08 21:03:28 +02:00
530e6eaf47
Refactor static file settings to use configuration for STATIC_ROOT 2025-05-08 21:03:19 +02:00
0e0da7f658
Update Docker build workflow to include latest tag in image push 2025-05-08 20:59:08 +02:00
8a86ff9a4b
Update build workflow to trigger on version tags instead of main branch
All checks were successful
Build and push Docker image / build (push) Successful in 2m0s
2025-05-08 20:57:26 +02:00
b12f151b8d
Add Docker support with Dockerfile and docker-compose; implement entrypoint script for application startup 2025-05-08 20:56:55 +02:00
2c0e7b7699
Move to UV and Docker
All checks were successful
Build and push Docker image / build (push) Successful in 1m39s
2025-05-06 08:55:04 +02:00
08732b6ad4
Fix add transaction to empty statement bug 2025-02-06 18:01:52 +01:00
6563260b34
Add metadata and tags fields to Statement model; enhance search results to include statements
Close #44
2025-01-05 18:59:24 +01:00
4974c30397
Enhance invoice display with improved CSS styling and update invoice links
Fix #47
2025-01-05 18:51:59 +01:00
a277b37526
Refactor invoice handling to remove unused formset and improve metadata extraction from PDF files
Fix #46
2025-01-05 18:42:58 +01:00
7194039706
Fix history_plot function with regard to category and account checks
Fix #31
2025-01-05 17:12:29 +01:00
c153000d3d
Add invoice model with metadata and tags; update search and templates for invoice handling
Progress #44
2025-01-05 16:01:26 +01:00
608da4be55
Refactor category_plot tag to remove budget parameter
Fix #45
2025-01-05 15:15:35 +01:00
d246843be0
Enhance category plot template with improved URL handling for transactions based on year, month, statement, and account 2025-01-05 11:47:40 +01:00
e5ae5caf2a
Add history URL routing and templates for transaction history views 2025-01-05 11:38:28 +01:00
71f7a82f60
Refactor transaction archive templates
Fix #42
2025-01-05 09:43:29 +01:00
d5292911c2
Refactor models to inherit from NummiModel and implement custom query sets for enhanced search functionality 2025-01-04 21:57:21 +01:00
d44407d9ab
Refactor transaction URL handling and enhance filter form functionality
Close #43
2025-01-04 21:16:10 +01:00
02c53c7dab
Fix template variable references in transaction detail view 2025-01-04 20:28:32 +01:00
e42410863d
Update French translations 2025-01-04 18:57:13 +01:00
a238401f13
Add AccountSelect widget and update account field in forms
- Introduced AccountSelect widget for improved account selection in forms.
- Updated AccountForm, StatementForm, and TransactionFiltersForm to use AccountSelect.
- Modified account model options to include 'archived' in ordering.
- Added new migration for account model options change.
2025-01-04 18:45:36 +01:00
ccbf433ec0
Add filter icon 2025-01-04 18:33:04 +01:00
b848bf8d65
Implement filters and sorting for transactions
Close #5
2025-01-04 18:28:37 +01:00
7851e8afbb
Improve form buttons styling
Close #34
2025-01-04 14:34:23 +01:00
6a17a3e333
Add pre-commit hook to prevent commits to main 2025-01-04 14:21:59 +01:00
e1f29f90c6
Move transaction link
Close #35
Progress #38
2025-01-04 14:17:48 +01:00
805c7d3dc0
Enable file drag and drop on transaction details page
Close #37
2025-01-04 12:37:09 +01:00
cfb2ceb2c3
Fix #39 2025-01-03 22:46:53 +01:00
cb1882ef8a
Default statement transaction date
Fix #40
2025-01-03 21:53:01 +01:00
d2d101fe34
Resize block on textarea
Fix #41
2025-01-03 21:49:47 +01:00
ec5c0e2fef
Fix update_sum left in transaction models 2025-01-03 19:16:03 +01:00
0c5ba6899b
Fix no autocomplete on statement transaction creation
Fix #36
2025-01-03 19:04:40 +01:00
ff519ea7d1
Auto-fill transaction form
Close #27
2025-01-03 18:50:50 +01:00
0cb4a681f1
Refactor datalist input usage 2025-01-03 16:52:02 +01:00
f203d1db46
Fix transaction table (fix #33) 2025-01-03 16:46:36 +01:00
38ab298094
Improve transaction form
Default to no statement (fix #28)
Add link to statement on statement transaction creation
Fix textarea height
2025-01-03 16:25:49 +01:00
7478404d8a
Improve statement details page
Fix #29
2025-01-03 15:55:58 +01:00
d8961c8198
Improve forms
Form errors displayed correctly (fix #30)
Fix #26
Close #20
2025-01-03 15:21:06 +01:00
5b49836bad
Fix history plot
Fix #31
2025-01-03 14:55:16 +01:00
c14e075cad
Add VS Code debug config 2025-01-03 14:54:47 +01:00
a6fc7f5a92
Move statement sum and diff to properties instead of model fields
Fix #32
2025-01-03 14:25:40 +01:00
26f97dd179
Use only budgeted categories in history plot 2025-01-03 13:05:46 +01:00
2cba5c6d83
Hide archived accounts in select for new statements 2025-01-03 12:46:19 +01:00
c6c67b9f93
Fix invoices not being shown on transactions 2025-01-03 12:33:33 +01:00
1f22ac7042
JS bugfix 2025-01-02 17:36:45 +01:00
57b315c842
Fix bug with js for archived accounts 2025-01-02 17:33:25 +01:00
224f55a8b1
Add icon field, closes #24 2025-01-02 17:30:59 +01:00
b4654ec445
Add automatic icons to category select (#24) 2025-01-02 16:40:27 +01:00
348ac494e9
Use templatetag for transaction_table 2025-01-02 15:19:12 +01:00
229033aac0
Use templatetag for history_plot 2025-01-02 15:03:10 +01:00
b1fddd0dd6
Use templatetag for statement_table 2025-01-02 14:41:53 +01:00
786d7c2130
Use templatetags for statement_table 2025-01-02 14:19:06 +01:00
090f1a3a5c
Fix #21 2025-01-02 14:05:08 +01:00
bac3b59358
Fix #22 2025-01-02 14:02:01 +01:00
cb939df81e
Fix #23, Fix #25 2025-01-02 13:58:28 +01:00
e8050fadb9
Fix statement list, gaps 2025-01-02 10:16:05 +01:00
57d5330d75
Create account list view 2025-01-02 10:07:01 +01:00
8575f43475
Fix transaction table with no account 2025-01-02 09:37:47 +01:00
79f87779dd
Fix display on index page 2025-01-01 19:05:34 +01:00
2991786444
Improve statement form 2025-01-01 19:01:35 +01:00
08551a865d
Improve category style 2025-01-01 18:53:56 +01:00
744ea277f5
Fix forms 2025-01-01 18:44:14 +01:00
cfe5389658
Fix login form buttons 2025-01-01 17:11:45 +01:00
f2f6335ad5
Fix forms 2025-01-01 17:10:15 +01:00
75ec967c40
Fix fileinput 2025-01-01 16:46:06 +01:00
c0aa8d17b6
Improve fieldsets 2025-01-01 16:21:31 +01:00
eeb5f4f04f
Improve forms 2025-01-01 15:39:18 +01:00
221dbb196e
Improve form style 2025-01-01 14:56:29 +01:00
804b2b38e0
Order datalist items by count 2025-01-01 14:14:11 +01:00
f98990d0a8
Update djlint settings 2025-01-01 13:59:31 +01:00
898c92df23
Fix orphan transaction detail page 2025-01-01 13:51:24 +01:00
c7994114a1
Add autocomplete for transaction fields
Needs work on ordering by -count
2025-01-01 09:23:19 +01:00
951f157de9
Fix invoice list (edit, bold title) 2025-01-01 08:40:52 +01:00
c8b58a18ac
Improve invoice list 2025-01-01 08:38:04 +01:00
275a1f6bc7
Fix transaction details on mobile 2025-01-01 08:20:16 +01:00
fe519bd4f0
Create transaction detail view 2024-12-31 19:39:55 +01:00
a8ff27e245
Add edit icon to category edit 2024-12-31 17:21:32 +01:00
093ca475d8
Fix icons 2024-12-31 17:19:52 +01:00
fe59869a1d
Fix show archived button 2024-12-31 17:13:33 +01:00
94d1907f9a
Add statement summary to details page 2024-12-31 17:04:34 +01:00
7d4dbdc0df
Create statement detail view separate from edit view 2024-12-31 16:47:36 +01:00
fd140a9314
Hide expected total when it is correct 2024-12-31 16:19:37 +01:00
f9e489218d
Add icons to messages 2024-12-31 16:14:24 +01:00
b20ef58e18
Reduce statement size to allow for better mobile experience 2024-12-31 15:58:06 +01:00
86a354f353
Fix category plot on statement page 2024-12-31 15:44:33 +01:00
cb08cb3d46
Improve account list 2024-12-31 15:41:02 +01:00
412cf94f93
Add last statements to home page 2024-12-31 15:31:07 +01:00
c754e869fc
Include icons in links 2024-12-31 15:14:42 +01:00
fac26bc224
Merge branch 'main' into dev 2024-12-31 15:04:35 +01:00
63f1cf4e4d
Update statement display (cards instead of table) 2024-12-31 15:03:01 +01:00
7109142b4e
Fix bug in history 2024-12-30 19:20:53 +01:00
886e682650
Fix history plot on mobile 2024-12-30 18:14:10 +01:00
5d6bd9ea2b
Improve history view using tenth centile times 125% for min max 2024-12-30 17:56:55 +01:00
28ac9c8ef7
Improve calendar table 2024-12-30 17:29:42 +01:00
a3e598acb6
Improve history view with outliers 2024-12-30 16:57:00 +01:00
46ea394422
Allow orphan transactions
Closes #18
2024-12-30 16:35:03 +01:00
93c4b43fa3
Improve account list
Closes #19
2024-12-30 16:11:43 +01:00
cf25fd1826
Enable archiving accounts 2024-12-29 10:38:25 +01:00
50ae922a99
Add total line in account list on index page 2024-03-23 11:51:37 +01:00
18a58783c8
Fix form table header background (fix #15) 2024-03-23 10:56:22 +01:00
e93f6f5d2b
Fix empty accounts producing error 500 2024-03-23 10:52:28 +01:00
ee3ec21527
Update styling on invoice table 2024-01-05 09:11:38 +01:00
ad020e476b
Add icon to log in link in navbar 2024-01-05 09:00:08 +01:00
0ff191e0a4
Fix account table display on small devices 2024-01-05 08:56:16 +01:00
a6e84fbc13
Fix back link styling on year page 2024-01-04 18:45:42 +01:00
7369b36ab1
Move connected as item in navbar 2024-01-04 18:45:17 +01:00
63258147ee
Add icons to navbar 2024-01-04 18:33:56 +01:00
67e71b717b
Fix back link 2024-01-04 17:16:13 +01:00
e41b989862
Add creation links to statement and transaction list pages 2024-01-04 17:04:37 +01:00
ca7cd790b5
Update check icons in statement table 2024-01-04 16:58:47 +01:00
9d50dc7154
Separate detail and edit view for category, update statement page 2024-01-04 16:51:48 +01:00
218a6aca6f
Separate detail and edit view for account 2024-01-04 16:05:38 +01:00
6bd83feafe
Update up-down icons 2024-01-04 15:38:53 +01:00
b0716a65b7
Add icons on calendar plot 2024-01-03 19:02:08 +01:00
8db2720f75
Remove create links from navbar; Add a link to create categories in the category list on the homepage 2024-01-03 18:49:24 +01:00
954ee9ce17
Update big links styling 2024-01-03 18:35:31 +01:00
08b234a070
Add colors back to history 2024-01-03 18:29:14 +01:00
bf1d15574d
Fix previous commit 2024-01-03 16:14:54 +01:00
ef90d52807
Fix history regarding category__budget 2024-01-03 16:07:10 +01:00
e4169bd1c3
Split columns for income and expenses 2024-01-03 15:52:36 +01:00
e2f7a1dcc3
Fix history calendar 2024-01-03 15:37:48 +01:00
cc551c536c
Change date format in transaction table on month and year page 2024-01-03 15:35:06 +01:00
b3c9642adc
Update year history (remove year in plot lines) 2024-01-03 15:20:45 +01:00
b478286f47
Fix page layout 2024-01-03 15:13:54 +01:00
6d14602dd2
Fix homepage on small screens 2024-01-03 14:59:08 +01:00
0f1aba45ea
Add class value to balance in account table 2024-01-03 14:54:24 +01:00
05b7418162
Reshape home page 2024-01-03 14:51:40 +01:00
d48818e455
Create table for accounts 2024-01-03 14:40:48 +01:00
9927cbbab4
Move new and more links to tables 2024-01-03 14:25:25 +01:00
e44e3a51f0
Fix contrast issues with table icons 2024-01-02 17:03:55 +01:00
65d97f523c
Add title attribute to calendar plot for accessibility 2024-01-02 16:58:24 +01:00
6b08113f60
Add back link on month and year page 2024-01-02 16:04:04 +01:00
e5cdf19fa3
Add transaction list on year view 2024-01-02 15:42:51 +01:00
b4c7f88b3d
Add time elements for accessibility 2024-01-02 15:28:24 +01:00
87c12f47e9
Fix pagination on small devices 2024-01-02 15:15:57 +01:00
8ebb940f5b
Add missing section block in month page 2024-01-02 15:09:50 +01:00
5ccae7f4a3
Fix spacing with pagination 2024-01-02 15:07:42 +01:00
3fd87ff370
Update month page template 2024-01-02 15:03:45 +01:00
f8c0f9dced
Change date format in month pagination 2024-01-02 15:00:27 +01:00
2ba21fbd10
Refactor pagination for month/year 2024-01-02 14:56:13 +01:00
35b26f2d10
Add year link on month view 2024-01-02 14:36:36 +01:00
57279b1cda
Simplify duplicated code (archive views) 2024-01-02 14:21:58 +01:00
47c4de8fbb
Fix calendar scroll 2024-01-02 14:18:57 +01:00
1137cfc1cc
Fix top-gap in main 2024-01-02 12:21:36 +01:00
2f32c2b80f
Fix year urls on account pages 2024-01-02 12:13:57 +01:00
60f84fe20d
Add pagination to year archive view 2024-01-02 12:06:20 +01:00
8b9af8e1a4
Add links to category in year category plot 2024-01-02 11:59:44 +01:00
309281f5e1
Fix row colors in history plot 2024-01-02 11:55:54 +01:00
75df57f42a
Update year view (add link) 2024-01-02 11:52:04 +01:00
3dbc31eebc
Fix row colors in category plot 2024-01-02 11:47:58 +01:00
caa113859d
Add year archive view 2024-01-02 11:45:06 +01:00
45b47dd1ba
Move templatetags to history templatetags 2024-01-02 11:11:05 +01:00
003902b43f
Minor style updates 2024-01-02 11:00:56 +01:00
0940904cd8
Refactor History plot 2024-01-02 10:58:30 +01:00
6b50de5e35
Fix opacity calculation on chrome 2023-12-31 18:41:54 +01:00
e4e0f56be0
Use css nesting 2023-12-31 09:37:29 +01:00
ee043d3956
Fix back link in transation form (fix #12) 2023-12-31 09:03:49 +01:00
fe95222474
Fix bugs in html templates 2023-12-30 11:12:38 +01:00
47d8ff0382
Create tag for css imports 2023-12-30 10:54:01 +01:00
7f3b9c7b5c
Structure page content with sections 2023-12-30 10:38:30 +01:00
0e5b8ea85d
Use :is css pseudo-class 2023-12-30 10:24:24 +01:00
715822e48f
Remove unused spaceless tags 2023-12-30 10:18:17 +01:00
ee4e6b7ceb
Improve history plot with empty segments 2023-12-30 10:10:53 +01:00
55923e7709
Fix form fields on alternating background 2023-12-30 09:42:58 +01:00
14755df86b
Fix pagination on month archive, keep account 2023-12-30 09:40:11 +01:00
a0bc1a6608
Update pagination on montharchive to keep category 2023-12-30 09:35:05 +01:00
68ff2ae8ad
Set table borders to 1px 2023-12-30 09:17:47 +01:00
17fe61c9c0
Improve accessibility for links 2023-12-30 09:14:07 +01:00
aeb22521f3
Add alternating row colors 2023-12-30 09:12:03 +01:00
5013df903f
Add pagination to month archive view (fix #13) 2023-12-30 09:04:52 +01:00
c3ba376793
Fix month offset (missing first month in history) 2023-12-29 09:53:59 +01:00
4bb682fc88
Fix HTML for index page 2023-12-29 09:50:08 +01:00
8652eb0b57
Remove borders in calendar table 2023-12-29 09:23:51 +01:00
6f631607b8
Reduce computations in history generation 2023-12-28 16:31:13 +01:00
9dbbd3d48e
Add monthly year chart 2023-12-28 15:46:52 +01:00
4bbb5de3c5
Update translations 2023-11-25 12:07:01 +01:00
ffc0fa3ce1
Update messages styling 2023-11-25 11:56:14 +01:00
8dd29be8bf
Add success message on object creation with link to object 2023-11-25 11:28:20 +01:00
76ac6bc7fb
Use tomllib instead of toml 2023-11-25 10:44:57 +01:00
3c1e4ac45d
Fix broken negative bars when total is negative (fixes #10) 2023-05-04 22:02:33 +02:00
02bd1853d5
Fix wrong total values (#10) 2023-05-04 22:00:36 +02:00
a5dd41b533
In category_plot templatetag, replace filter budget with exclude not budget 2023-05-03 12:56:00 +02:00
eabdaf2516
Fix urls in category plot on month page 2023-05-03 12:20:19 +02:00
574d1d2875
Add total line to category plot
Closes #9
2023-05-03 12:18:20 +02:00
278d252cfd
Merge branch 'main' of ssh://git.edgarpierre.fr:39529/edpibu/nummi 2023-05-03 12:01:59 +02:00
b9e129e80a
Move category plot to templatetag, hide non-budget categories 2023-05-03 12:01:25 +02:00
fcdfd0e9ad
Update PKGBUILD to v0.9.1 2023-04-24 09:12:22 +02:00
819bd20fdf
Fix file uploads (borken by missing enctype on form) 2023-04-24 09:07:30 +02:00
3d9424a1d5
Remove LocaleMiddleware 2023-04-23 17:14:56 +02:00
3980f2230c
Add colors to logo 2023-04-23 16:54:28 +02:00
09c8da0650
Update PKGBUILD with version 2023-04-23 14:07:54 +02:00
9a666b6d22
Fix .gitignore 2023-04-23 08:10:30 +02:00
a0872b65c4
Use get_absolute_url to get model urls 2023-04-23 08:08:43 +02:00
1bad7d7291
Fix icon margin 2023-04-22 19:47:58 +02:00
b9cd7a8460
Fix icon margin 2023-04-22 19:43:31 +02:00
517dd28b2e
Fix navbar scrolling 2023-04-22 19:41:03 +02:00
b6326bdc8f
Fix category plot on month page 2023-04-22 15:57:36 +02:00
f7564eb282
Add links to category in month category plot 2023-04-22 15:52:32 +02:00
b828324220
Add category plot on history pages 2023-04-22 15:44:27 +02:00
681651109a
Fix translations 2023-04-22 15:24:12 +02:00
068dab59d8
Log LOCALE_PATHS 2023-04-22 15:11:51 +02:00
d478f038ba
Log BASE_DIR 2023-04-22 15:10:34 +02:00
5abb9a3b30
Update settings for locale 2023-04-22 15:08:21 +02:00
13b014e16e
Update translations 2023-04-22 14:46:34 +02:00
a77cfbe339
Fix urls 2023-04-22 14:33:07 +02:00
2d7957b813
Empty tables are now visible 2023-04-22 14:28:49 +02:00
a3f28631df
Create base template for lists 2023-04-22 14:06:24 +02:00
2e3d76ad19
Remove unused templates 2023-04-22 13:59:39 +02:00
210268928b
Move pagination file up 2023-04-22 13:58:30 +02:00
2d5f209c41
Move history to separate app 2023-04-22 13:53:45 +02:00
f0a232f366
Moved views to adequate apps 2023-04-22 13:47:43 +02:00
62f360e77b
Hide connected as when not logged in 2023-04-22 13:33:12 +02:00
2d45fef975
Move config.toml to pkgbuild dir 2023-04-22 13:31:12 +02:00
06c8cab4bf
Move PKGBUILD to pkgbuild dir 2023-04-22 13:27:58 +02:00
ee25223e73
Implemented frontend for transactions and invoices 2023-04-22 12:44:34 +02:00
a98b073eea
Implemented frontend for statements 2023-04-22 12:30:08 +02:00
c9d1496e00
Implemented frontend for search 2023-04-22 12:20:03 +02:00
f6e8b583cc
Implemented frontend for category 2023-04-22 12:15:48 +02:00
719436f9ad
Implemented frontend for account 2023-04-22 12:14:42 +02:00
bb4a8c5db1
Fixed migrations 2023-04-22 11:40:12 +02:00
b05c3e6760
Split backend in applications 2023-04-22 11:16:42 +02:00
a0d0b5d594
Move media handling to new app 2023-04-22 09:58:38 +02:00
ad18226974
Remove tests.py 2023-04-22 09:49:17 +02:00
82c7a03965
Move adding property to filters 2023-04-22 09:48:11 +02:00
88f4f9099b
Add user name in navbar 2023-04-22 09:42:21 +02:00
b18088f9d6
Remove useless config 2023-04-22 09:29:44 +02:00
4c75cb460d
Remove admin modification of models 2023-04-22 09:27:57 +02:00
0e1c1be189
Fix monthly view for category and account 2023-04-21 18:58:46 +02:00
17e150032e
Add monthly view 2023-04-21 17:55:46 +02:00
966100536e
Update translations 2023-04-21 15:42:18 +02:00
44e20fbbcf
Manually squashed migration 2023-04-20 18:14:10 +02:00
2972b3f741
Remove squashed migration 2023-04-20 17:36:11 +02:00
d063f35e2b
Change migration name 2023-04-20 17:34:51 +02:00
4738dffc82
Transition squashed migration to normal migration 2023-04-20 17:27:06 +02:00
f3b355d76b
Add flake8 linter 2023-04-20 17:24:01 +02:00
5a76d58894
Update title style 2023-04-20 16:59:39 +02:00
8d68a7091f
Update title and paragraph styles 2023-04-20 16:14:31 +02:00
d9e03df878
Update title style 2023-04-20 16:06:29 +02:00
ea7d8faecd
Style title on create 2023-04-20 15:58:37 +02:00
dfe89fb716
Remove last New ... 2023-04-20 15:53:55 +02:00
442b36055a
Remove None fallback in get 2023-04-20 15:51:26 +02:00
d595d73417
Disable fixed fields in forms 2023-04-20 15:50:40 +02:00
42c44c8c11
Make links full-width 2023-04-20 15:32:30 +02:00
51d57564c4
Fix no-income / no-expense snapshots 2023-04-20 15:22:04 +02:00
ae39b4870b
Add back link to create forms linked to accounts, snapshots 2023-04-20 15:15:00 +02:00
7521cc236f
Remove get_create_url, redirect createform to same page using hidden form field 2023-04-20 14:41:16 +02:00
c744c87baa
Use different urls for create pages 2023-04-20 12:22:45 +02:00
b673950e99
Fix translation error 2023-04-20 12:15:06 +02:00
d7d9a44894
Change create titles (again) 2023-04-20 12:14:25 +02:00
e87893ae0c
Capitalize links to creation form 2023-04-20 12:06:01 +02:00
45d12d0b07
Remove lowercase from creation form title 2023-04-20 12:04:14 +02:00
df19c5586d
Change save button to create on creation form 2023-04-20 12:03:30 +02:00
37bb725f23
Use form template 2023-04-20 12:01:22 +02:00
110a699f1a
Show title as New on forms 2023-04-20 11:28:06 +02:00
9263af0d40
Create view returns to create another ; remove checkbox 2023-04-20 11:11:06 +02:00
3eb40cd5ed
Remove addmore checkbox from update forms 2023-04-20 11:07:17 +02:00
e3ddf4cbaa
Right-align whole tfoot in forms 2023-04-20 10:22:01 +02:00
a1d48ca246
Remove empty value fill-ins 2023-04-20 10:21:18 +02:00
c77b88c2f7
Fix add more checkbox in login and search form 2023-04-20 10:17:40 +02:00
751ddba463
Fix empty account bug 2023-04-20 10:11:14 +02:00
4a1d7778df
Add backend for Add more checkbox 2023-04-20 10:07:53 +02:00
821d0d290a
Update translations 2023-04-20 09:40:39 +02:00
306a1e06c2
Add checkbox 'Add more' to frontend 2023-04-20 09:39:40 +02:00
3ca027afc5
Update invoice form 2023-04-20 09:39:24 +02:00
492e6e77d1
Update __str__ functions 2023-04-20 09:23:30 +02:00
4792388ccd
Move function definition to utils 2023-04-20 09:19:15 +02:00
5a0958d52c
Fill empty months in history 2023-04-19 21:43:43 +02:00
917531aa97
Add history function to view 2023-04-19 18:59:37 +02:00
5534a4352d
Add trend to history chart 2023-04-19 18:35:57 +02:00
9db123457f
Change skip link font weight 2023-04-19 17:27:12 +02:00
b98743d88b
Remove footer 2023-04-19 17:25:33 +02:00
9deba5b28f
Show file extension 2023-04-19 17:19:01 +02:00
06b51a87ff
Update PKGBUILD 2023-04-19 17:09:00 +02:00
2c95379870
Raise skip link opacity 2023-04-19 17:08:03 +02:00
f4077456af
Fix snapshot page 2023-04-19 16:58:40 +02:00
0ec3fa74b2
Set icon field length to 24 2023-04-19 15:44:05 +02:00
787427beec
Switch to remixicon 2023-04-19 15:32:01 +02:00
10ba4f118e
Use remixicon + emoji uicons in frontend 2023-04-19 14:54:32 +02:00
df0005ebe2
Set uicon length to 2 for composite emojis 2023-04-19 10:11:21 +02:00
93b31dabee
Fix uicon field type 2023-04-19 10:08:41 +02:00
629c993072
Replace icon field with uicon in backend 2023-04-19 10:03:22 +02:00
71269377b4
Add prettier pre-commit hook 2023-04-18 19:51:15 +02:00
044a7f1c06
Add djlint pre-commit hook 2023-04-18 19:01:41 +02:00
d0cfc2d729
Add pre-commit hooks 2023-04-18 18:55:23 +02:00
6ed05ad33d
Update translations 2023-04-18 18:31:48 +02:00
8a2cb5b974
Add footer 2023-04-18 17:36:36 +02:00
2b9180f25a
Enable form lateral scrolling for mobile devices 2023-04-18 17:19:17 +02:00
9b05d41eec
Do not take budget into account for history view on account page 2023-04-18 17:15:48 +02:00
a21f655f18
Improve mobile design 2023-04-18 16:57:05 +02:00
ae5e5f1104
Improve design for small screen use 2023-04-18 16:53:08 +02:00
a578c5c2a7
Fix background on file input 2023-04-18 16:16:00 +02:00
7e34c1f149
Add background to form headers 2023-04-18 16:11:27 +02:00
8b0c0d245c
Update pagination 2023-04-18 16:03:12 +02:00
b2bbbb405f
Fix confirm_delete page 2023-04-18 15:40:20 +02:00
5e9fc9c0e7
Fix padding on file input 2023-04-18 15:35:35 +02:00
5116133a52
Moved form templates 2023-04-18 15:33:25 +02:00
c210e91fff
Change tables with more items to show 2023-04-18 15:27:14 +02:00
64b5b22def
Fix file input 2023-04-18 15:14:52 +02:00
74c95c7d16
Fix snapshot list on account page 2023-04-18 14:37:51 +02:00
ab3f7f4e40
Update snapshot page 2023-04-18 14:35:32 +02:00
a14f73779b
Update translations 2023-04-18 14:27:24 +02:00
fecf8caccf
Round numbers instead of truncating 2023-04-18 14:23:24 +02:00
9a2bbcd5c3
Add history chart in accounts 2023-04-18 14:18:20 +02:00
3f92a6d2ba
Add history chart in categories 2023-04-18 14:15:09 +02:00
9fd41851bc
Sticky all nav 2023-04-18 14:06:56 +02:00
c912343442
Add skip link, add sticky navigation 2023-04-18 14:05:13 +02:00
057e0ecf74
Update design 2023-04-18 13:46:07 +02:00
58c18195a6
Update login form 2023-04-18 13:38:29 +02:00
d0447e0c8e
Update form style 2023-04-18 12:15:48 +02:00
6381263786
Updage paginate_by to 96 2023-04-18 11:27:36 +02:00
5b34e41021
Update links 2023-04-18 11:25:58 +02:00
c7f5311c02
Fix chart width 2023-04-18 11:01:49 +02:00
e298ec88f1
Fix empty invoice table 2023-04-18 10:55:35 +02:00
1b7cc543f0
Update invoice table 2023-04-18 10:48:29 +02:00
a364b38651
Update snapshot table 2023-04-18 10:35:12 +02:00
6ca8ca6493
Use html tables, update design 2023-04-18 10:20:45 +02:00
fdc9214b10
Add background to thead 2023-04-18 09:39:39 +02:00
de9b73ed3a
Add borders 2023-04-18 09:38:07 +02:00
8cf48cfdf5
Update chart on snapshot page 2023-04-18 09:31:11 +02:00
93a800bf79
Update dates 2023-04-17 18:22:16 +02:00
65df9aae18
Update translations for history 2023-04-17 18:16:50 +02:00
656449db9f
HTML/CSS only plot implemented for history on homepage 2023-04-17 17:25:11 +02:00
ee7e6e60a7
Update page titles 2023-04-09 08:58:03 +02:00
243 changed files with 6411 additions and 22384 deletions

View file

@ -0,0 +1,29 @@
name: Build and push Docker image
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: code.edgarpierre.fr
username: ${{ vars.DOCKER_PUSH_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_PASSWORD }}
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: |
code.edgarpierre.fr/${{ github.repository }}:${{ github.ref_name }}
code.edgarpierre.fr/${{ github.repository }}:latest

8
.gitignore vendored
View file

@ -1,7 +1,3 @@
env*
/env*
__pycache__
*.pkg.tar.zst
/nummi-git/
/pkg/
/src/
/nummi/media
/media

31
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,31 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
- id: no-commit-to-branch
args: ["--branch", "main"]
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
args: ["--profile", "black"]
- repo: https://github.com/psf/black
rev: 23.3.0
hooks:
- id: black
- repo: https://github.com/PyCQA/flake8
rev: "6.0.0"
hooks:
- id: flake8
args: ["--max-line-length=88", "--extend-ignore=E203"]
- repo: https://github.com/Riverside-Healthcare/djLint
rev: v1.23.3
hooks:
- id: djlint-django
args: ["--reformat", "--lint", "--quiet"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.0-alpha.6"
hooks:
- id: prettier
types_or: ["css", "javascript", "svg"]

1
.prettierrc.toml Normal file
View file

@ -0,0 +1 @@
endOfLine = "auto"

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.12

22
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Django",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}\\nummi\\manage.py",
"args": [
"runserver"
],
"env": {
"NUMMI_CONFIG": "${workspaceFolder}\\env\\config.toml"
},
"django": true
}
]
}

17
Dockerfile Normal file
View file

@ -0,0 +1,17 @@
FROM ghcr.io/astral-sh/uv:debian-slim
ADD . /app
WORKDIR /app
RUN useradd -m -r nummi && \
chown -R nummi /app
USER nummi
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV NUMMI_CONFIG=/nummi/config.toml
RUN uv sync --locked
CMD ["/app/entrypoint.sh"]

View file

@ -1,47 +0,0 @@
pkgname=nummi-git
pkgver=r151.ee26d0d
pkgrel=1
pkgdesc="Web-based accounting interface"
arch=("any")
url="https://git.edgarpierre.fr/edpibu/nummi"
license=('AGPL3')
depends=(
"gunicorn"
"python-django"
"python-matplotlib"
"python-toml"
"python-psycopg2"
)
makedepends=("git")
optdepends=("postgresql: database")
backup=("etc/${pkgname%-git}/config.toml")
source=(
"${pkgname}::git+https://git.edgarpierre.fr/edpibu/nummi"
"${pkgname%-git}.service"
"${pkgname%-git}.tmpfiles"
"${pkgname%-git}.sysusers"
"${pkgname%-git}.nginx"
"config.toml"
)
b2sums=('SKIP'
'9e9b62a141bb4594dc203978d5c56dd397ed6c095e6f83fdea07ded2fa46dd2e052aa8f80729a3b612e3773aa487660fcca791079ad4885b825c9c831df61b45'
'cd42e864d82ca5ea191d2c15cec55afea272a875f19ebc1252e03fb86ef3d62820e2fedd706a8da0b0e3a7aee355e0bb5dcdb7f3bd803497c1320fab540f7d70'
'78bebc6cca3f8b520783565de9122cc57e62eb629d3693dfaa322a77f4be3c55c5cd5a460bd1f6f3028f4dd1b2018ec6166b7f9aaf55ac3be6178bd1a3a00405'
'c878c463ecb58d94b3f9b96d798b098a7718aec6df4b424a60674c0a9daf9abcb4556e527e55f67dbb093e109145b0ca17556f7dce66116ba7b170ead61acc16'
'5bdc097ab3fa7e8e128d194bfb6c0fefb5528903e2b8765557a82033f8925c1174dbe73007a595e32c5e415cf6f19957c35bbe463c50d2813894c20c8a28a88d')
pkgver() {
cd "$pkgname"
printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
}
package() {
install -Dm644 ${pkgname%-git}.service -t "${pkgdir}"/usr/lib/systemd/system/
install -Dm644 ${pkgname%-git}.tmpfiles "${pkgdir}"/usr/lib/tmpfiles.d/${pkgname%-git}.conf
install -Dm644 ${pkgname%-git}.sysusers "${pkgdir}"/usr/lib/sysusers.d/${pkgname%-git}.conf
install -Dm644 ${pkgname%-git}.nginx "${pkgdir}"/etc/nginx/sites-enabled/${pkgname%-git}.conf
install -Dm750 -o nummi -g nummi config.toml -t "${pkgdir}"/etc/${pkgname%-git}/
cd ${pkgname}/${pkgname%-git}
find * -type f -exec install -Dm0644 "{}" "${pkgdir}/usr/share/webapps/${pkgname%-git}/{}" \;
}

23
compose.yaml Normal file
View file

@ -0,0 +1,23 @@
services:
nummi:
image: code.edgarpierre.fr/edpibu/nummi
container_name: nummi
restart: unless-stopped
ports:
- 33001:8000
volumes:
- /docker/nummi/config:/nummi
- /docker/nummi/static:/app/static
- /docker/nummi/media:/app/media
depends_on:
- postgres
postgres:
image: postgres:17-alpine
container_name: nummi_postgres
restart: unless-stopped
environment:
POSTGRES_USER: nummi
POSTGRES_PASSWORD:
volumes:
- /docker/nummi/postgres:/var/lib/postgresql/data

5
entrypoint.sh Executable file
View file

@ -0,0 +1,5 @@
#!/usr/bin/env sh
cd /app/nummi
uv run manage.py collectstatic --noinput
uv run manage.py migrate --noinput
uv run gunicorn --bind :8000 --workers 2 nummi.wsgi:application

6
nummi/account/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class AccountConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "account"

22
nummi/account/forms.py Normal file
View file

@ -0,0 +1,22 @@
from django.forms.widgets import Select
from main.forms import IconInput, NummiForm
from .models import Account
class AccountForm(NummiForm):
class Meta:
model = Account
fields = [
"name",
"icon",
"default",
"archived",
]
widgets = {
"icon": IconInput(),
}
class AccountSelect(Select):
template_name = "account/forms/widgets/account.html"

Binary file not shown.

View file

@ -0,0 +1,75 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:17+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\account\models.py:12 .\account\models.py:45 .\account\models.py:53
#: .\account\templates\account\account_list.html:9
msgid "Account"
msgstr "Compte"
#: .\account\models.py:12
msgid "Name"
msgstr "Nom"
#: .\account\models.py:16
msgid "Icon"
msgstr "Icône"
#: .\account\models.py:18
msgid "Default"
msgstr "Défaut"
#: .\account\models.py:19
msgid "Archived"
msgstr "Archivé"
#: .\account\models.py:46 .\account\templates\account\account_list.html:12
msgid "Accounts"
msgstr "Comptes"
#: .\account\templates\account\account_detail.html:15
msgid "Edit account"
msgstr "Modifier le compte"
#: .\account\templates\account\account_detail.html:18
msgid "Statements"
msgstr "Relevés"
#: .\account\templates\account\account_detail.html:24
msgid "History"
msgstr "Historique"
#: .\account\templates\account\account_form.html:5
#: .\account\templates\account\account_table.html:35
msgid "Create account"
msgstr "Créer un compte"
#: .\account\templates\account\account_form.html:8
msgid "New account"
msgstr "Nouveau compte"
#: .\account\templates\account\account_table.html:16
msgid "All accounts"
msgstr "Tous les comptes"
#: .\account\templates\account\account_table.html:26
msgid "Show archived"
msgstr "Afficher archivés"
#~ msgid "Transactions"
#~ msgstr "Transactions"

View file

@ -1,4 +1,4 @@
# Generated by Django 4.1.4 on 2022-12-29 18:32
# Generated by Django 4.1.4 on 2023-04-22 09:01
import uuid
@ -8,13 +8,14 @@ from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("main", "0012_alter_snapshot_date"),
("main", "0002_segmentation"),
]
operations = [
state_operations = [
migrations.CreateModel(
name="Account",
fields=[
@ -30,51 +31,35 @@ class Migration(migrations.Migration):
(
"name",
models.CharField(
default="Account", max_length=64, verbose_name="Nom"
default="Account", max_length=64, verbose_name="Name"
),
),
(
"icon",
models.CharField(
default="folder", max_length=64, verbose_name="Icône"
models.SlugField(
default="bank", max_length=24, verbose_name="Icon"
),
),
("default", models.BooleanField(default=False, verbose_name="Default")),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
verbose_name="User",
),
),
],
options={
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
"ordering": ["name"],
"ordering": ["-default", "name"],
"db_table": "account_account",
},
),
migrations.AddField(
model_name="snapshot",
name="account",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.account",
verbose_name="Account",
),
),
migrations.AddField(
model_name="transaction",
name="account",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.account",
verbose_name="Account",
),
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

View file

@ -0,0 +1,16 @@
# Generated by Django 4.1.4 on 2023-04-22 09:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("account", "0001_initial"),
]
operations = [
migrations.AlterModelTable(
name="account",
table=None,
),
]

View file

@ -1,18 +1,17 @@
# Generated by Django 4.1.4 on 2022-12-31 18:04
# Generated by Django 4.2 on 2024-12-29 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0023_auto_20221231_1741"),
("account", "0002_alter_account_table"),
]
operations = [
migrations.AddField(
model_name="account",
name="default",
field=models.BooleanField(default=False, verbose_name="Default"),
name="archived",
field=models.BooleanField(default=False, verbose_name="Archived"),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 4.2.7 on 2025-01-04 17:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("account", "0003_account_archived"),
]
operations = [
migrations.AlterModelOptions(
name="account",
options={
"ordering": ["-default", "archived", "name"],
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
},
),
]

57
nummi/account/models.py Normal file
View file

@ -0,0 +1,57 @@
from uuid import uuid4
from django.apps import apps
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from main.models import NummiModel
class Account(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
icon = models.SlugField(
max_length=24,
default="bank",
verbose_name=_("Icon"),
)
default = models.BooleanField(default=False, verbose_name=_("Default"))
archived = models.BooleanField(default=False, verbose_name=_("Archived"))
def save(self, *args, **kwargs):
if self.default:
for ac in Account.objects.filter(user=self.user, default=True):
ac.default = False
ac.save()
super().save(*args, **kwargs)
def __str__(self):
return str(self.name)
def get_absolute_url(self):
return reverse("account", args=(self.pk,))
def get_delete_url(self):
return reverse("del_account", args=(self.pk,))
@property
def transactions(self):
return apps.get_model("transaction", "Transaction").objects.filter(
statement__account=self
)
class Meta:
ordering = ["-default", "archived", "name"]
verbose_name = _("Account")
verbose_name_plural = _("Accounts")
class AccountModel(NummiModel):
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,
verbose_name=_("Account"),
)
class Meta:
abstract = True

View file

@ -0,0 +1,27 @@
{% extends "main/base.html" %}
{% load main_extras history_extras statement_extras %}
{% load i18n %}
{% block title %}
{{ account }} {{ block.super }}
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
<h2>{{ account.icon|remix }}{{ account }}</h2>
<p>
<a href="{% url "edit_account" account.pk %}">{{ "edit"|remix }}{% translate "Edit account" %}</a>
</p>
<section>
<h3>{% translate "Statements" %}</h3>
{% url "new_statement" account=account.pk as ns_url %}
{% url "account_statements" account=account.pk as s_url %}
{% statement_table account.statement_set.all statements_url=s_url new_statement_url=ns_url n_max=6 %}
</section>
<section>
<h3>{% translate "History" %}</h3>
{% history_plot account.transactions account=account %}
</section>
{% endblock body %}

View file

@ -0,0 +1,10 @@
{% extends "main/form/base.html" %}
{% load main_extras %}
{% load i18n %}
{% block title_new %}
{% translate "Create account" %}
{% endblock %}
{% block h2_new %}
{% translate "New account" %}
{% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "main/list.html" %}
{% load i18n main_extras account_extras %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block name %}
{% translate "Account" %}
{% endblock name %}
{% block h2 %}
{% translate "Accounts" %}
{% endblock h2 %}
{% block table %}
<div class="split">{% account_table accounts %}</div>
{% endblock table %}

View file

@ -0,0 +1,39 @@
{% load i18n main_extras %}
<dl class="accounts">
{% for acc in accounts %}
<div class="account {% if not search and acc.archived %}archived{% endif %}">
<dt>
<a href="{{ acc.get_absolute_url }}">{{ acc.icon|remix }}{{ acc }}</a>
</dt>
<dd class="value">
{% if acc.statement_set.first %}{{ acc.statement_set.first.value|value }}{% endif %}
</dd>
</div>
{% endfor %}
{% if index %}
<div class="more account">
<dt>
<a href="{% url "accounts" %}">{{ "gallery-view"|remixnl }}{% translate "All accounts" %}</a>
</dt>
<dd class="value">
{{ accounts|balance|value }}
</dd>
</div>
{% elif not search %}
<div class="more account">
<dt>
<label class="wi" for="show-archived-accounts">
{{ "archive"|remix }}{% translate "Show archived" %}
</label>
</dt>
<dd>
<input type="checkbox" class="show-archived" id="show-archived-accounts" />
</dd>
</div>
<div class="new account">
<dt>
<a href="{% url "new_account" %}">{{ "add-box"|remix }}{% translate "Create account" %}</a>
</dt>
</div>
{% endif %}
</dl>

View file

@ -0,0 +1,5 @@
{% load main_extras %}
<span class="ico-input account-select">
{{ "bank"|remix }}
{% include "django/forms/widgets/select.html" %}
</span>

View file

View file

@ -0,0 +1,10 @@
from django import template
register = template.Library()
@register.inclusion_tag("account/account_table.html")
def account_table(accounts, **kwargs):
return kwargs | {
"accounts": accounts,
}

31
nummi/account/urls.py Normal file
View file

@ -0,0 +1,31 @@
from django.urls import path
from statement.views import StatementCreateView
from . import views
urlpatterns = [
path("list", views.AccountListView.as_view(), name="accounts"),
path("new", views.AccountCreateView.as_view(), name="new_account"),
path("<account>", views.AccountDetailView.as_view(), name="account"),
path("<account>/edit", views.AccountUpdateView.as_view(), name="edit_account"),
path(
"<account>/transactions",
views.AccountTListView.as_view(),
name="account_transactions",
),
path(
"<account>/statements",
views.AccountSListView.as_view(),
name="account_statements",
),
path(
"<account>/statement",
StatementCreateView.as_view(),
name="new_statement",
),
path(
"<account>/delete",
views.AccountDeleteView.as_view(),
name="del_account",
),
]

60
nummi/account/views.py Normal file
View file

@ -0,0 +1,60 @@
from django.shortcuts import get_object_or_404
from main.views import (
NummiCreateView,
NummiDeleteView,
NummiDetailView,
NummiListView,
NummiUpdateView,
)
from statement.views import StatementListView
from transaction.views import TransactionListView
from .forms import AccountForm
from .models import Account
class AccountCreateView(NummiCreateView):
model = Account
form_class = AccountForm
class AccountUpdateView(NummiUpdateView):
model = Account
form_class = AccountForm
pk_url_kwarg = "account"
class AccountDeleteView(NummiDeleteView):
model = Account
pk_url_kwarg = "account"
class AccountDetailView(NummiDetailView):
model = Account
pk_url_kwarg = "account"
context_object_name = "account"
class AccountMixin:
def get_queryset(self):
self.account = get_object_or_404(
Account.objects.filter(user=self.request.user),
pk=self.kwargs.get("account"),
)
return super().get_queryset().filter(account=self.account)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"account": self.account}
class AccountListView(NummiListView):
model = Account
context_object_name = "accounts"
class AccountTListView(AccountMixin, TransactionListView):
pass
class AccountSListView(AccountMixin, StatementListView):
pass

View file

6
nummi/category/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CategoryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "category"

21
nummi/category/forms.py Normal file
View file

@ -0,0 +1,21 @@
from django.forms.widgets import Select
from main.forms import IconInput, NummiForm
from .models import Category
class CategoryForm(NummiForm):
class Meta:
model = Category
fields = [
"name",
"icon",
"budget",
]
widgets = {
"icon": IconInput,
}
class CategorySelect(Select):
template_name = "category/forms/widgets/category.html"

Binary file not shown.

View file

@ -0,0 +1,83 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:18+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\category\models.py:12 .\category\models.py:32
#: .\category\templates\category\category_plot.html:13
msgid "Category"
msgstr "Catégorie"
#: .\category\models.py:12
msgid "Name"
msgstr "Nom"
#: .\category\models.py:17
msgid "Icon"
msgstr "Icône"
#: .\category\models.py:19
msgid "Budget"
msgstr "Budget"
#: .\category\models.py:33
msgid "Categories"
msgstr "Catégories"
#: .\category\templates\category\category_detail.html:14
msgid "Edit category"
msgstr "Modifier la catégorie"
#: .\category\templates\category\category_detail.html:17
msgid "Transactions"
msgstr "Transactions"
#: .\category\templates\category\category_detail.html:20
msgid "View all transactions"
msgstr "Voir toutes les transactions"
#: .\category\templates\category\category_detail.html:25
msgid "History"
msgstr "Historique"
#: .\category\templates\category\category_form.html:5
msgid "Create category"
msgstr "Créer une catégorie"
#: .\category\templates\category\category_form.html:8
msgid "New category"
msgstr "Nouvelle catégorie"
#: .\category\templates\category\category_plot.html:14
msgid "Expenses"
msgstr "Dépenses"
#: .\category\templates\category\category_plot.html:15
msgid "Income"
msgstr "Revenus"
#: .\category\templates\category\category_plot.html:58
msgid "No transaction"
msgstr "Aucune transaction"
#: .\category\templates\category\category_plot.html:66
msgid "Total"
msgstr "Total"
#: .\category\templates\category\category_plot.html:89
msgid "Expected total"
msgstr "Total attendu"

View file

@ -0,0 +1,64 @@
# Generated by Django 4.1.4 on 2023-04-22 09:01
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
state_operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="Category", max_length=64, verbose_name="Name"
),
),
(
"icon",
models.SlugField(
default="folder", max_length=24, verbose_name="Icon"
),
),
("budget", models.BooleanField(default=True, verbose_name="Budget")),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
options={
"verbose_name": "Category",
"verbose_name_plural": "Categories",
"ordering": ["name"],
"db_table": "category_category",
},
),
]
operations = [
migrations.SeparateDatabaseAndState(state_operations=state_operations)
]

View file

@ -0,0 +1,16 @@
# Generated by Django 4.1.4 on 2023-04-22 09:28
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("category", "0001_initial"),
]
operations = [
migrations.AlterModelTable(
name="category",
table=None,
),
]

View file

33
nummi/category/models.py Normal file
View file

@ -0,0 +1,33 @@
from uuid import uuid4
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from main.models import NummiModel
class Category(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name")
)
icon = models.SlugField(
max_length=24,
default="folder",
verbose_name=_("Icon"),
)
budget = models.BooleanField(default=True, verbose_name=_("Budget"))
def __str__(self):
return str(self.name)
def get_absolute_url(self):
return reverse("category", args=(self.pk,))
def get_delete_url(self):
return reverse("del_category", args=(self.pk,))
class Meta:
ordering = ["name"]
verbose_name = _("Category")
verbose_name_plural = _("Categories")

View file

@ -0,0 +1,28 @@
{% extends "main/base.html" %}
{% load i18n main_extras history_extras transaction_extras %}
{% block title %}
{{ category }} {{ block.super }}
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
<h2>{{ category.icon|remix }}{{ category }}</h2>
<p>
<a href="{% url "edit_category" category.pk %}">{{ "edit"|remix }}{% translate "Edit category" %}</a>
</p>
<section>
<h3>{% translate "Transactions" %}</h3>
{% url_get "transactions" category=category.id as t_url %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "View all transactions" %}</a>
</p>
{% transaction_table category.transaction_set.all n_max=8 transactions_url=t_url %}
</section>
<section>
<h3>{% translate "History" %}</h3>
{% history_plot category.transaction_set category=category %}
</section>
{% endblock body %}

View file

@ -0,0 +1,10 @@
{% extends "main/form/base.html" %}
{% load main_extras %}
{% load i18n %}
{% block title_new %}
{% translate "Create category" %}
{% endblock %}
{% block h2_new %}
{% translate "New category" %}
{% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}

View file

@ -0,0 +1,114 @@
{% load main_extras statement_extras history_extras %}
{% load i18n %}
<div class="plot">
<table class="full-width">
<colgroup>
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{% translate "Category" %}</th>
<th scope="col" colspan="2">{% translate "Expenses" %}</th>
<th scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% spaceless %}
{% for cat in categories %}
<tr>
<th scope="row" class="l wi">
{% if cat.category %}
{% if year %}
<a href="{% history_url year=year category=cat.category account=account.id %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% elif month %}
<a href="{% url_get "transactions" start_date=month end_date=month|end_of_month account=account.id category=cat.category %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% elif statement %}
<a href="{% url_get "transactions" account=statement.account.id statement=statement.id category=cat.category %}">{{ cat.category_.icon|remix }}{{ cat.category__name }}</a>
{% elif account %}
<a href="{% url_get "transactions" account=account.id category=cat.category %}">{{ cat.category__icon|remix }}{{ cat.category__name }}</a>
{% else %}
{{ cat.category__icon|remix }}{{ cat.category__name }}
{% endif %}
{% endif %}
</th>
<td class="value">{{ cat.sum_m|pmvalue }}</td>
<td class="bar m">
{% if cat.sum_m %}
<div style="width: {% widthratio cat.sum_m max -100 %}%"></div>
{% endif %}
{% if cat.sum < 0 %}
<div class="tot" style="width:{% widthratio cat.sum max -100 %}%">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
{% if cat.sum_p %}
<div style="width: {% widthratio cat.sum_p max 100 %}%"></div>
{% endif %}
{% if cat.sum > 0 %}
<div class="tot" style="width:{% widthratio cat.sum max 100 %}%">
<span>{{ cat.sum|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ cat.sum_p|pmvalue }}</td>
</tr>
{% empty %}
<tr>
<td class="empty" colspan="5">{% translate "No transaction" %}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
<tfoot>
{% if categories %}
<tr>
<th scope="row" class="l">{% translate "Total" %}</th>
<td class="value">{{ total_m|pmvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio total_m max -100 %}%"></div>
{% if total < 0 %}
<div class="tot" style="width:{% widthratio total max -100 %}%">
<span>{{ total|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio total_p max 100 %}%"></div>
{% if total > 0 %}
<div class="tot" style="width:{% widthratio total max 100 %}%">
<span>{{ total|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ total_p|pmvalue }}</td>
</tr>
{% endif %}
{% if statement and statement.diff != statement.sum %}
<tr>
<th scope="row" class="l">{% translate "Expected total" %}</th>
<td class="c">{{ total|check:statement.diff }}</td>
<td class="bar m">
{% if statement.diff < 0 %}
<div class="tot" style="width:{% widthratio statement.diff max -100 %}%">
<span>{{ statement.diff|pmvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
{% if statement.diff >= 0 %}
<div class="tot" style="width:{% widthratio statement.diff max 100 %}%">
<span>{{ statement.diff|pmvalue }}</span>
</div>
{% endif %}
</td>
<td></td>
</tr>
{% endif %}
</tfoot>
</table>
</div>

View file

@ -0,0 +1,5 @@
{% load main_extras %}
<span class="ico-input category-select">
{{ "folder"|remix }}
{% include "django/forms/widgets/select.html" %}
</span>

View file

View file

@ -0,0 +1,34 @@
from django import template
from django.db import models
from django.db.models.functions import Greatest
register = template.Library()
@register.inclusion_tag("category/category_plot.html", takes_context=True)
def category_plot(context, transactions, **kwargs):
kwargs.setdefault("account", context.get("account"))
if not kwargs.get("account"):
transactions = transactions.exclude(category__budget=False)
categories = (
transactions.values("category", "category__name", "category__icon")
.annotate(
sum=models.Sum("value"),
sum_m=models.Sum("value", filter=models.Q(value__lt=0)),
sum_p=models.Sum("value", filter=models.Q(value__gt=0)),
)
.order_by("-sum")
)
return (
kwargs
| {
"categories": categories,
}
| categories.aggregate(
max=Greatest(-models.Sum("sum_m"), models.Sum("sum_p")),
total_m=models.Sum("sum_m"),
total_p=models.Sum("sum_p"),
total=models.Sum("sum"),
)
)

15
nummi/category/urls.py Normal file
View file

@ -0,0 +1,15 @@
from django.urls import path
from . import views
urlpatterns = [
path("new", views.CategoryCreateView.as_view(), name="new_category"),
path("<category>", views.CategoryDetailView.as_view(), name="category"),
path("<category>/edit", views.CategoryUpdateView.as_view(), name="edit_category"),
path(
"<category>/transactions",
views.CategoryTListView.as_view(),
name="category_transactions",
),
path("<category>/delete", views.CategoryDeleteView.as_view(), name="del_category"),
]

49
nummi/category/views.py Normal file
View file

@ -0,0 +1,49 @@
from django.shortcuts import get_object_or_404
from main.views import (
NummiCreateView,
NummiDeleteView,
NummiDetailView,
NummiUpdateView,
)
from transaction.views import TransactionListView
from .forms import CategoryForm
from .models import Category
class CategoryCreateView(NummiCreateView):
model = Category
form_class = CategoryForm
class CategoryUpdateView(NummiUpdateView):
model = Category
form_class = CategoryForm
pk_url_kwarg = "category"
class CategoryDetailView(NummiDetailView):
model = Category
pk_url_kwarg = "category"
context_object_name = "category"
class CategoryDeleteView(NummiDeleteView):
model = Category
pk_url_kwarg = "category"
class CategoryMixin:
def get_queryset(self):
self.category = get_object_or_404(
Category.objects.filter(user=self.request.user),
pk=self.kwargs.get("category"),
)
return super().get_queryset().filter(category=self.category)
def get_context_data(self, **kwargs):
return super().get_context_data(**kwargs) | {"category": self.category}
class CategoryTListView(CategoryMixin, TransactionListView):
pass

View file

6
nummi/history/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class HistoryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "history"

Binary file not shown.

View file

@ -0,0 +1,38 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-22 15:18+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\history\templates\history\plot.html:10
msgid "Year"
msgstr "Année"
#: .\history\templates\history\plot.html:13
msgid "Total"
msgstr "Total"
#: .\history\templates\history\plot.html:56
msgid "Month"
msgstr "Mois"
#: .\history\templates\history\plot.html:57
msgid "Expenses"
msgstr "Dépenses"
#: .\history\templates\history\plot.html:58
msgid "Income"
msgstr "Revenus"

View file

View file

@ -0,0 +1,25 @@
{% load history_extras %}
{% if month %}
<p class="pagination">
<a href="{% history_url year=month %}">{{ month.year }}</a>
</p>
{% endif %}
<p class="pagination n3">
{% if month %}
{% if previous_month %}
<a href="{% history_url month=previous_month %}">{{ previous_month|date:"F Y"|capfirst }}</a>
{% endif %}
<a class="cur" href="{% history_url month=month %}">{{ month|date:"F Y"|capfirst }}</a>
{% if next_month %}
<a href="{% history_url month=next_month %}">{{ next_month|date:"F Y"|capfirst }}</a>
{% endif %}
{% elif year %}
{% if previous_year %}
<a href="{% history_url year=previous_year %}">{{ previous_year|date:"Y" }}</a>
{% endif %}
<a class="cur" href="{% history_url year=year %}">{{ year|date:"Y" }}</a>
{% if next_year %}
<a href="{% history_url year=next_year %}">{{ next_year|date:"Y" }}</a>
{% endif %}
{% endif %}
</p>

View file

@ -0,0 +1,92 @@
{% load main_extras %}
{% load history_extras %}
{% load transaction_extras %}
{% load i18n %}
<div class="calendar">
<table>
<thead>
<tr>
{% if not year %}
<th scope="col">{% translate "Year" %}</th>
{% endif %}
{% calendar_head %}
<th scope="col">{% translate "Total" %}</th>
</tr>
</thead>
<tbody>
{% regroup history.data by month.year as years_list %}
{% for y, y_data in years_list reversed %}
<tr>
{% if not year %}
<th class="date" scope="row">
<a href="{% history_url year=y account=account.id category=category.id %}">{{ y }}</a>
</th>
{% endif %}
{% for m in y_data %}
{% if forloop.parentloop.last and forloop.first %}
{% empty_calendar_cells_start m.month.month %}
{% endif %}
{% if m %}
<td class="month {% if m.sum > 0 %}p{% else %}m{% endif %}"
style="--opacity: {% calendar_opacity m.sum history.max.sum %}"
title="{{ m.sum|pmrvalue }}">{% up_down_icon m.sum %}</td>
{% else %}
<td class="month"></td>
{% endif %}
{% if forloop.parentloop.first and forloop.last %}
{% empty_calendar_cells_end m.month.month %}
{% endif %}
{% endfor %}
<td class="total">{{ y_data|sum_year|pmrvalue }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="history plot">
<table class="full-width">
<colgroup>
<col class="icon">
<col class="desc">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col">{{ "expand-up-down"|remix }}</th>
<th scope="col">{% translate "Month" %}</th>
<th scope="col" colspan="2">{% translate "Expenses" %}</th>
<th scope="col" colspan="2">{% translate "Income" %}</th>
</tr>
</thead>
<tbody>
{% for date in history.data reversed %}
{% ifchanged %}
{% if date.sum_m or date.sum_p %}
<tr {% if not date.month.month|divisibleby:"2" %}class="even"{% endif %}>
<td class="icon">{% up_down_icon date.sum %}</td>
<th class="date" scope="row">
<a href="{% history_url year=date.month.year month=date.month.month account=account.id category=category.id %}">
{% if year %}
{{ date.month|date:"F"|capfirst }}
{% else %}
{{ date.month|date:"Y-m" }}
{% endif %}
</a>
</th>
<td class="value">{{ date.sum_m|pmrvalue }}</td>
<td class="bar m">{% plot_bar date.sum date.sum_m history.max.pm %}</td>
<td class="bar p">{% plot_bar date.sum date.sum_p history.max.pm %}</td>
<td class="value">{{ date.sum_p|pmrvalue }}</td>
</tr>
{% else %}
<tr class="empty">
<td colspan="6" class="empty"></td>
</tr>
{% endif %}
{% endifchanged %}
{% endfor %}
</tbody>
</table>
</div>

View file

@ -0,0 +1,51 @@
{% extends "main/base.html" %}
{% load i18n static main_extras transaction_extras category history_extras %}
{% block link %}
{{ block.super }}
{% css "main/css/plot.css" %}
{% css "main/css/table.css" %}
{% endblock link %}
{% block body %}
<h2>
{% block h2 %}
{% endblock h2 %}
</h2>
{% history_pagination %}
{% if account or category %}
<p class="back">
<a class="big-link"
href="{% history_url year=year month=month clear=True %}">{{ "arrow-go-back"|remix }}{% translate "Back" %}</a>
{% if account %}
<a class="big-link" href="{% url "account" account.id %}">{{ account.icon|remix }}{{ account }}</a>
{% endif %}
{% if category %}
<a class="big-link" href="{% url "category" category.id %}">{{ category.icon|remix }}{{ category }}</a>
{% endif %}
</p>
{% endif %}
{% if history %}
<section>
<h3>{% translate "History" %}</h3>
{% include "history/plot.html" %}
</section>
{% endif %}
{% if not category %}
<section>
<h3>{% translate "Categories" %}</h3>
{% category_plot transactions month=month year=year %}
</section>
{% endif %}
<section>
<h3>{% translate "Transactions" %}</h3>
{% if month %}
{% url_get "transactions" start_date=month end_date=month|end_of_month category=category.id account=account.id as t_url %}
{% elif year %}
{% url_get "transactions" start_date=year end_date=year|end_of_year category=category.id account=account.id as t_url %}
{% endif %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "View all transactions" %}</a>
</p>
{% transaction_table transactions n_max=8 transactions_url=t_url %}
</section>
{% history_pagination %}
{% endblock body %}

View file

@ -0,0 +1,8 @@
{% extends "history/transaction_archive.html" %}
{% load i18n static main_extras transaction_extras category %}
{% block title %}
{{ month|date:"F Y"|capfirst }} {{ block.super }}
{% endblock title %}
{% block h2 %}
{{ month|date:"F Y"|capfirst }}
{% endblock h2 %}

View file

@ -0,0 +1,8 @@
{% extends "history/transaction_archive.html" %}
{% load i18n static main_extras transaction_extras category %}
{% block title %}
{{ year|date:"Y" }} {{ block.super }}
{% endblock title %}
{% block h2 %}
{{ year|date:"Y" }}
{% endblock h2 %}

View file

@ -0,0 +1,119 @@
import datetime
import math
from urllib import parse
from django import template
from django.urls import reverse
from django.utils.safestring import mark_safe
from history.utils import history
from main.templatetags.main_extras import pmrvalue, remix
register = template.Library()
@register.inclusion_tag("history/plot.html", takes_context=True)
def history_plot(context, transactions, **kwargs):
kwargs.setdefault("account", context.get("account"))
kwargs.setdefault("category", context.get("category"))
if kwargs.get("category") or kwargs.get("account"):
kwargs["history"] = history(transactions.all())
else:
kwargs["history"] = history(transactions.exclude(category__budget=False))
return kwargs
@register.simple_tag
def calendar_opacity(v, vmax):
if v is None:
return "0%"
return f"{math.sin(min(1, math.fabs(v/vmax))*math.pi/2): .0%}"
@register.simple_tag
def empty_calendar_cells(n):
return mark_safe(n * "<td></td>")
@register.simple_tag
def empty_calendar_cells_start(n):
return empty_calendar_cells(n - 1)
@register.simple_tag
def empty_calendar_cells_end(n):
return empty_calendar_cells(12 - n)
@register.simple_tag
def up_down_icon(val):
if val is None:
return ""
if val > 0:
return remix("arrow-up-s", "green")
elif val < 0:
return remix("arrow-down-s", "red")
return remix("equal", "white")
@register.simple_tag
def plot_bar(s, sum_pm, s_max):
_res = ""
if s_max:
if sum_pm:
_w = abs(sum_pm / s_max)
_res += f"""<div style="width: {_w: .1%}"></div>"""
if sum_pm is not None and s * sum_pm > 0:
_w = abs(s / s_max)
_res += (
f"""<div class="tot" style="width: {_w: .1%}">"""
f"""<span>{pmrvalue(s)}</span></div>"""
)
else:
_res += "<div></div>"
return mark_safe(_res)
@register.simple_tag
def calendar_head():
months = range(1, 13)
th = (f"""<th>{month: 02d}</th>""" for month in months)
return mark_safe("".join(th))
@register.filter
def sum_year(y_data):
return sum(y["sum"] or 0 for y in y_data)
@register.inclusion_tag("history/pagination.html", takes_context=True)
def history_pagination(context):
return context
@register.simple_tag(takes_context=True)
def history_url(context, month=None, year=None, clear=False, **kwargs):
if not clear:
kwargs.setdefault("account", getattr(context.get("account"), "id", None))
kwargs.setdefault("category", getattr(context.get("category"), "id", None))
if month:
if isinstance(month, datetime.date):
year = month.year
month = month.month
url = reverse("history:month", kwargs={"year": year, "month": month})
elif year:
if isinstance(year, datetime.date):
year = year.year
url = reverse("history:year", kwargs={"year": year})
kwargs = {k: v for k, v in kwargs.items() if v}
if kwargs:
return f"{url}?{parse.urlencode(kwargs)}"
return url

13
nummi/history/urls.py Normal file
View file

@ -0,0 +1,13 @@
from django.urls import path
from . import views
app_name = "history"
urlpatterns = [
path(
"month/<int:year>/<int:month>",
views.TransactionMonthView.as_view(),
name="month",
),
path("year/<int:year>", views.TransactionYearView.as_view(), name="year"),
]

54
nummi/history/utils.py Normal file
View file

@ -0,0 +1,54 @@
import datetime
from django.db.models import Q, Sum
from django.db.models.functions import Abs, Greatest, TruncMonth
def history(transaction_set):
if not transaction_set.exists():
return None
_transaction_month = transaction_set.values(month=TruncMonth("date")).order_by(
"-date"
)
_first_month = _transaction_month.last()["month"]
_last_month = _transaction_month.first()["month"]
_history = (
_transaction_month.annotate(
sum_p=Sum("value", filter=Q(value__gt=0), default=0),
sum_m=Sum("value", filter=Q(value__lt=0), default=0),
sum=Sum("value"),
)
.annotate(max_sum=Greatest("sum_p", Abs("sum_m")))
.order_by("-month")
)
_data = [
_history.filter(month=datetime.date(y, m + 1, 1)).first()
or {"month": datetime.date(y, m + 1, 1), "sum": None}
for y in range(
_first_month.year,
_last_month.year + 1,
)
for m in range(
_first_month.month - 1 if _first_month.year == y else 0,
_last_month.month if _last_month.year == y else 12,
)
]
return {
"data": _data,
"max": {
"pm": 125
* _history.order_by("-max_sum")[len(_history.exclude(max_sum=0)) // 10][
"max_sum"
]
/ 100,
"sum": 125
* _history.annotate(abs_sum=Abs("sum")).order_by("-abs_sum")[
len(_history) // 10
]["abs_sum"]
/ 100,
},
}

54
nummi/history/views.py Normal file
View file

@ -0,0 +1,54 @@
from django.shortcuts import get_object_or_404
from django.views.generic.dates import MonthArchiveView, YearArchiveView
from history.utils import history
from main.views import UserMixin
from transaction.models import Transaction
class ACFilterMixin:
def get_queryset(self):
queryset = super().get_queryset()
if account := self.request.GET.get("account"):
queryset = queryset.filter(statement__account=account)
if category := self.request.GET.get("category"):
queryset = queryset.filter(category=category)
return queryset
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
if account := self.request.GET.get("account"):
context_data["account"] = get_object_or_404(
self.request.user.account_set, pk=account
)
if category := self.request.GET.get("category"):
context_data["category"] = get_object_or_404(
self.request.user.category_set, pk=category
)
return context_data
class TransactionMonthView(UserMixin, ACFilterMixin, MonthArchiveView):
model = Transaction
date_field = "date"
context_object_name = "transactions"
month_format = "%m"
template_name = "history/transaction_month.html"
class TransactionYearView(UserMixin, ACFilterMixin, YearArchiveView):
model = Transaction
date_field = "date"
context_object_name = "transactions"
make_object_list = True
template_name = "history/transaction_year.html"
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
h_data = context_data.get("transactions")
if not (context_data.get("account") or context_data.get("category")):
h_data = h_data.exclude(category__budget=False)
context_data["history"] = history(h_data)
return context_data

View file

@ -1,21 +0,0 @@
# NUMMI.
# Copyright (C) 2022
# This file is distributed under the same license as the nummi package.
# edpibu <git@edgarpierre.fr>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-12-21 17:26+0100\n"
"PO-Revision-Date: 2022-12-21 17:30+0100\n"
"Last-Translator: edpibu <git@edgarpierre.fr>\n"
"Language-Team: edpibu <git@edgarpierre.fr>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\plot\views.py:28
msgid "Snapshots"
msgstr "Relevés"

View file

@ -1,8 +0,0 @@
from django.contrib import admin
from .models import Category, Invoice, Snapshot, Transaction
admin.site.register(Transaction)
admin.site.register(Invoice)
admin.site.register(Category)
admin.site.register(Snapshot)

View file

@ -2,5 +2,4 @@ from django.apps import AppConfig
class MainConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "main"

View file

@ -1,97 +1,51 @@
from django import forms
from django.db import models, transaction
from django.utils.translation import gettext_lazy as _
from django.forms.widgets import TextInput
from .models import Account, Category, Invoice, Snapshot, Transaction
from .utils import get_icons
class NummiFileInput(forms.ClearableFileInput):
template_name = "main/form/fileinput.html"
class NummiForm(forms.ModelForm):
template_name = "main/form/base.html"
def __init__(self, *args, user, **kwargs):
super().__init__(*args, **kwargs)
class AccountForm(NummiForm):
class Meta:
model = Account
fields = [
"name",
"icon",
"default",
]
class CategoryForm(NummiForm):
class Meta:
model = Category
fields = [
"name",
"icon",
]
class TransactionForm(NummiForm):
class Meta:
model = Transaction
fields = [
"snapshot",
"name",
"value",
"date",
"real_date",
"category",
"trader",
"payment",
"description",
]
template_name = "main/form/form_base.html"
meta_fieldsets = []
def __init__(self, *args, **kwargs):
_user = kwargs.get("user")
kwargs.pop("user", None)
super().__init__(*args, **kwargs)
self.fields["category"].queryset = Category.objects.filter(user=_user)
self.fields["snapshot"].queryset = Snapshot.objects.filter(user=_user)
@property
def fieldsets(self):
if self.meta_fieldsets:
for group in self.meta_fieldsets:
yield ((self[f] for f in fieldset) for fieldset in group)
else:
yield ([f] for f in self)
class InvoiceForm(NummiForm):
prefix = "invoice"
class DatalistInput(TextInput):
template_name = "main/forms/widgets/datalist.html"
class Meta:
model = Invoice
fields = [
"name",
"file",
]
def __init__(self, *args, options=[]):
self.options = options
super().__init__(*args)
def get_context(self, *args):
context = super().get_context(*args)
name = context["widget"]["name"]
context["widget"]["attrs"]["list"] = f"{name}-list"
context["widget"]["attrs"]["autocomplete"] = "off"
context["widget"]["options"] = self.options
return context
class SnapshotForm(NummiForm):
class Meta:
model = Snapshot
fields = ["account", "start_date", "date", "start_value", "value", "file"]
class IconInput(DatalistInput):
template_name = "main/forms/widgets/icon.html"
icon_list = get_icons()
def __init__(self, *args, **kwargs):
_user = kwargs.get("user")
super().__init__(*args, **kwargs)
self.fields["account"].queryset = Account.objects.filter(user=_user)
self.fields["transactions"] = forms.MultipleChoiceField(
choices=(
((transaction.id), transaction)
for transaction in Transaction.objects.filter(user=_user)
),
label=_("Add transactions"),
required=False,
)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
new_transactions = Transaction.objects.filter(
id__in=self.cleaned_data["transactions"]
)
instance.transaction_set.add(*new_transactions, bulk=False)
return instance
class SearchForm(forms.Form):
template_name = "main/form/search.html"
search = forms.CharField(label=_("Search"), max_length=128)
def get_context(self, *args):
context = super().get_context(*args)
context["widget"]["options"] = self.icon_list
return context

View file

@ -1,231 +0,0 @@
# NUMMI.
# Copyright (C) 2022
# This file is distributed under the same license as the nummi package.
# edpibu <git@edgarpierre.fr>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-01-01 10:02+0100\n"
"PO-Revision-Date: 2022-12-21 17:30+0100\n"
"Last-Translator: edpibu <git@edgarpierre.fr>\n"
"Language-Team: edpibu <git@edgarpierre.fr>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\main\forms.py:81
msgid "Add transactions"
msgstr "Ajouter des transactions"
#: .\main\models.py:17
msgid "User"
msgstr "Utilisateur"
#: .\main\models.py:45 .\main\models.py:79 .\main\models.py:87
#: .\main\models.py:237 .\main\templates\main\base.html:39
#: .\main\templates\main\table\snapshot.html:7
#: .\main\templates\main\table\transaction.html:11
msgid "Account"
msgstr "Compte"
#: .\main\models.py:45 .\main\models.py:97 .\main\models.py:208
#: .\main\models.py:278 .\main\templates\main\table\transaction.html:7
#: .\main\templates\main\transaction_form.html:27
msgid "Name"
msgstr "Nom"
#: .\main\models.py:49 .\main\models.py:102
msgid "Icon"
msgstr "Icône"
#: .\main\models.py:51
msgid "Default"
msgstr "Défaut"
#: .\main\models.py:80 .\main\templates\main\index.html:16
msgid "Accounts"
msgstr "Comptes"
#: .\main\models.py:97 .\main\models.py:121 .\main\models.py:227
#: .\main\templates\main\base.html:49
#: .\main\templates\main\table\transaction.html:10
msgid "Category"
msgstr "Catégorie"
#: .\main\models.py:104
msgid "Budget"
msgstr "Budget"
#: .\main\models.py:122 .\main\templates\main\index.html:30
#: .\main\templates\main\snapshot_form.html:34
msgid "Categories"
msgstr "Catégories"
#: .\main\models.py:127
msgid "End date"
msgstr "Date de fin"
#: .\main\models.py:129
msgid "Start date"
msgstr "Date de début"
#: .\main\models.py:132
msgid "End value"
msgstr "Valeur de fin"
#: .\main\models.py:135
msgid "Start value"
msgstr "Valeur de début"
#: .\main\models.py:141 .\main\templates\main\table\snapshot.html:9
msgid "Difference"
msgstr "Différence"
#: .\main\models.py:148
msgid "Transaction difference"
msgstr "Différence des transactions"
#: .\main\models.py:154 .\main\models.py:283
msgid "File"
msgstr "Fichier"
#: .\main\models.py:161
#, python-format
msgid "%(date)s statement"
msgstr "Relevé du %(date)s"
#: .\main\models.py:201 .\main\models.py:232
msgid "Statement"
msgstr "Relevé"
#: .\main\models.py:202 .\main\templates\main\account_form.html:23
#: .\main\templates\main\list\snapshot.html:15
msgid "Statements"
msgstr "Relevés"
#: .\main\models.py:208 .\main\models.py:271 .\main\templates\main\base.html:44
msgid "Transaction"
msgstr "Transaction"
#: .\main\models.py:210 .\main\templates\main\table\transaction.html:12
msgid "Description"
msgstr "Description"
#: .\main\models.py:212 .\main\templates\main\table\snapshot.html:8
#: .\main\templates\main\table\transaction.html:8
msgid "Value"
msgstr "Valeur"
#: .\main\models.py:214 .\main\templates\main\table\snapshot.html:6
#: .\main\templates\main\table\transaction.html:6
msgid "Date"
msgstr "Date"
#: .\main\models.py:215
msgid "Real date"
msgstr "Date réelle"
#: .\main\models.py:217 .\main\templates\main\table\transaction.html:9
msgid "Trader"
msgstr "Commerçant"
#: .\main\models.py:220
msgid "Payment"
msgstr "Paiement"
#: .\main\models.py:272 .\main\templates\main\account_form.html:27
#: .\main\templates\main\category_form.html:25
#: .\main\templates\main\index.html:26
#: .\main\templates\main\list\transaction.html:15
#: .\main\templates\main\snapshot_form.html:75
#: .\main\templates\main\table\snapshot.html:10
msgid "Transactions"
msgstr "Transactions"
#: .\main\models.py:278 .\main\models.py:317
msgid "Invoice"
msgstr "Facture"
#: .\main\models.py:318 .\main\templates\main\transaction_form.html:23
msgid "Invoices"
msgstr "Factures"
#: .\main\templates\main\base.html:54
msgid "Snapshot"
msgstr "Relevé"
#: .\main\templates\main\base.html:60 .\main\templates\main\base.html:62
msgid "Search"
msgstr "Rechercher"
#: .\main\templates\main\base.html:64
msgid "Log out"
msgstr "Se déconnecter"
#: .\main\templates\main\confirm_delete.html:19
#, python-format
msgid "Are you sure you want do delete <strong>%(object)s</strong> ?"
msgstr "Êtes-vous sûr de vouloir supprimer <strong>%(object)s</strong> ?"
#: .\main\templates\main\confirm_delete.html:23
msgid "Cancel"
msgstr "Annuler"
#: .\main\templates\main\confirm_delete.html:24
msgid "Confirm"
msgstr "Confirmer"
#: .\main\templates\main\form\base.html:17
#: .\main\templates\main\tag\form_buttons.html:4
#: .\main\templates\main\transaction_form.html:34
msgid "Delete"
msgstr "Supprimer"
#: .\main\templates\main\form\base.html:20
#: .\main\templates\main\tag\form_buttons.html:8
msgid "Save"
msgstr "Enregistrer"
#: .\main\templates\main\index.html:40
msgid "Snapshots"
msgstr "Relevés"
#: .\main\templates\main\list\snapshot.html:35
msgid "No snapshots to show"
msgstr "Aucun relevé à afficher"
#: .\main\templates\main\list\transaction.html:35
msgid "No transactions to show"
msgstr "Aucune transaction à afficher"
#: .\main\templates\main\login.html:11 .\main\templates\main\login.html:27
msgid "Log In"
msgstr "Se connecter"
#: .\main\templates\main\table\snapshot.html:11
msgid "Valid"
msgstr "Valide"
#: .\main\templates\main\table\snapshot.html:15
msgid "New statement"
msgstr "Nouveau relevé"
#: .\main\templates\main\table\snapshot.html:50
msgid "View all statements"
msgstr "Voir tous les relevés"
#: .\main\templates\main\table\transaction.html:16
msgid "New transaction"
msgstr "Ajouter une transaction"
#: .\main\templates\main\table\transaction.html:55
msgid "View all transactions"
msgstr "Voir toutes les transactions"
#: .\main\templates\main\transaction_form.html:39
msgid "New invoice"
msgstr "Nouvelle facture"

Binary file not shown.

View file

@ -0,0 +1,134 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-01-04 18:51+0100\n"
"PO-Revision-Date: 2023-04-23 08:03+0200\n"
"Last-Translator: Edgar P. Burkhart <traduction@edgarpierre.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.2.2\n"
#: .\main\models.py:10
msgid "User"
msgstr "Utilisateur"
#: .\main\templates\main\base.html:28
msgid "Skip to main content"
msgstr "Aller au contenu principal"
#: .\main\templates\main\base.html:35
msgid "Home"
msgstr "Accueil"
#: .\main\templates\main\base.html:42 .\main\templates\main\index.html:17
msgid "Statements"
msgstr "Relevés"
#: .\main\templates\main\base.html:49
msgid "Transactions"
msgstr "Transactions"
#: .\main\templates\main\base.html:57 .\main\templates\main\list.html:10
#: .\main\templates\main\list.html:37
msgid "Search"
msgstr "Rechercher"
#: .\main\templates\main\base.html:62
#, python-format
msgid "Logged in as <strong>%(user)s</strong>"
msgstr "Connecté en tant que <strong>%(user)s</strong>"
#: .\main\templates\main\base.html:66
msgid "Log out"
msgstr "Se déconnecter"
#: .\main\templates\main\base.html:74 .\main\templates\main\form\login.html:5
#: .\main\templates\main\login.html:11
msgid "Log in"
msgstr "Se connecter"
#: .\main\templates\main\confirm_delete.html:15
#, python-format
msgid "Are you sure you want do delete <strong>%(object)s</strong> ?"
msgstr "Êtes-vous sûr de vouloir supprimer <strong>%(object)s</strong> ?"
#: .\main\templates\main\confirm_delete.html:20
msgid "Cancel"
msgstr "Annuler"
#: .\main\templates\main\confirm_delete.html:21
msgid "Confirm"
msgstr "Confirmer"
#: .\main\templates\main\form\fileinput.html:6
msgid "File"
msgstr "Fichier"
#: .\main\templates\main\form\form_base.html:46
msgid "Create"
msgstr "Créer"
#: .\main\templates\main\form\form_base.html:48
msgid "Save"
msgstr "Enregistrer"
#: .\main\templates\main\form\form_base.html:50
msgid "Reset"
msgstr "Réinitialiser"
#: .\main\templates\main\form\form_base.html:52
msgid "Delete"
msgstr "Supprimer"
#: .\main\templates\main\index.html:13
msgid "Accounts"
msgstr "Comptes"
#: .\main\templates\main\index.html:23
msgid "Categories"
msgstr "Catégories"
#: .\main\templates\main\index.html:29
msgid "Create category"
msgstr "Créer une catégorie"
#: .\main\templates\main\index.html:34
msgid "History"
msgstr "Historique"
#: .\main\views.py:54
msgid "was created successfully"
msgstr "a été créé avec succès"
#~ msgid "Account"
#~ msgstr "Compte"
#~ msgid "Balance"
#~ msgstr "Solde"
#~ msgid "Edit"
#~ msgstr "Modifier"
#~ msgid "No account"
#~ msgstr "Aucun compte"
#~ msgid "Create account"
#~ msgstr "Créer un compte"
#~ msgid "Create statement"
#~ msgstr "Créer un relevé"
#~ msgid "Create transaction"
#~ msgstr "Créer une transaction"
#~ msgid "No category"
#~ msgstr "Aucune catégorie"

View file

@ -1,154 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 07:45
import datetime
import re
import uuid
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="New Category",
max_length=64,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-\\w]+\\Z"),
"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens.",
"invalid",
)
],
),
),
("icon", models.CharField(default="folder", max_length=64)),
],
options={
"ordering": ["name"],
},
),
migrations.CreateModel(
name="Transaction",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(default="New Transaction", max_length=256)),
("description", models.TextField(blank=True, null=True)),
(
"value",
models.DecimalField(decimal_places=2, default=0, max_digits=12),
),
("date", models.DateField(default=datetime.date.today)),
("trader", models.CharField(blank=True, max_length=128, null=True)),
(
"category",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.category",
),
),
],
options={
"ordering": ["-date"],
},
),
migrations.CreateModel(
name="Snapshot",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("date", models.DateField(default=datetime.date.today, unique=True)),
(
"value",
models.DecimalField(decimal_places=2, default=0, max_digits=12),
),
(
"diff",
models.DecimalField(
decimal_places=2, default=0, editable=False, max_digits=12
),
),
(
"previous",
models.OneToOneField(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.snapshot",
),
),
],
options={
"ordering": ["-date"],
},
),
migrations.CreateModel(
name="Invoice",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("name", models.CharField(default="New Invoice", max_length=256)),
(
"file",
models.FileField(
upload_to="invoices/",
validators=[
django.core.validators.FileExtensionValidator(["pdf"])
],
),
),
(
"transaction",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.transaction",
),
),
],
),
]

View file

@ -0,0 +1,349 @@
# Generated by Django 4.1.4 on 2023-04-20 15:32
import datetime
import uuid
import django.contrib.postgres.operations
import django.core.validators
import django.db.models.deletion
import media.utils
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
django.contrib.postgres.operations.TrigramExtension(),
django.contrib.postgres.operations.UnaccentExtension(),
migrations.CreateModel(
name="Category",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="Category", max_length=64, verbose_name="Name"
),
),
("budget", models.BooleanField(default=True, verbose_name="Budget")),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
(
"icon",
models.SlugField(
default="folder", max_length=24, verbose_name="Icon"
),
),
],
options={
"ordering": ["name"],
"verbose_name": "Category",
"verbose_name_plural": "Categories",
},
),
migrations.CreateModel(
name="Account",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="Account", max_length=64, verbose_name="Name"
),
),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
("default", models.BooleanField(default=False, verbose_name="Default")),
(
"icon",
models.SlugField(
default="bank", max_length=24, verbose_name="Icon"
),
),
],
options={
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
"ordering": ["-default", "name"],
},
),
migrations.CreateModel(
name="Snapshot",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"date",
models.DateField(
default=datetime.date.today, verbose_name="End date"
),
),
(
"value",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="End value",
),
),
(
"file",
models.FileField(
blank=True,
default="",
max_length=256,
upload_to=media.utils.get_path,
validators=[
django.core.validators.FileExtensionValidator(["pdf"])
],
verbose_name="File",
),
),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
(
"account",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
(
"start_date",
models.DateField(
default=datetime.date.today, verbose_name="Start date"
),
),
(
"start_value",
models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="Start value",
),
),
(
"diff",
models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Difference",
),
),
(
"sum",
models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Transaction difference",
),
),
],
options={
"ordering": ["-date", "account"],
"verbose_name": "Statement",
"verbose_name_plural": "Statements",
},
),
migrations.CreateModel(
name="Transaction",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="Transaction", max_length=256, verbose_name="Name"
),
),
(
"description",
models.TextField(blank=True, null=True, verbose_name="Description"),
),
(
"value",
models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Value"
),
),
(
"date",
models.DateField(default=datetime.date.today, verbose_name="Date"),
),
(
"trader",
models.CharField(
blank=True, max_length=128, null=True, verbose_name="Trader"
),
),
(
"category",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.category",
verbose_name="Category",
),
),
(
"real_date",
models.DateField(blank=True, null=True, verbose_name="Real date"),
),
(
"payment",
models.CharField(
blank=True, max_length=128, null=True, verbose_name="Payment"
),
),
(
"account",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
(
"snapshot",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.snapshot",
verbose_name="Statement",
),
),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
options={
"ordering": ["-date", "snapshot"],
"verbose_name": "Transaction",
"verbose_name_plural": "Transactions",
},
),
migrations.CreateModel(
name="Invoice",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"name",
models.CharField(
default="Invoice", max_length=256, verbose_name="Name"
),
),
(
"file",
models.FileField(
max_length=128,
upload_to=media.utils.get_path,
validators=[
django.core.validators.FileExtensionValidator(["pdf"])
],
verbose_name="File",
),
),
(
"transaction",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.transaction",
),
),
(
"user",
models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
],
options={
"ordering": ["transaction", "name"],
"verbose_name": "Invoice",
"verbose_name_plural": "Invoices",
},
),
]

View file

@ -1,25 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 09:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(unique=True),
),
migrations.AlterField(
model_name="snapshot",
name="diff",
field=models.DecimalField(
blank=True, decimal_places=2, editable=False, max_digits=12, null=True
),
),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 4.1.4 on 2023-04-22 09:01
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("main", "0001_v1"),
]
database_operations = [
migrations.AlterModelTable("Account", "account_account"),
migrations.AlterModelTable("Category", "category_category"),
migrations.AlterModelTable("Snapshot", "statement_statement"),
migrations.AlterModelTable("Transaction", "transaction_transaction"),
migrations.AlterModelTable("Invoice", "transaction_invoice"),
]
state_operations = [
migrations.DeleteModel("Account"),
migrations.DeleteModel("Category"),
migrations.DeleteModel("Snapshot"),
migrations.DeleteModel("Transaction"),
migrations.DeleteModel("Invoice"),
]
operations = [
migrations.RenameField("Transaction", "snapshot", "statement"),
migrations.SeparateDatabaseAndState(
database_operations=database_operations,
state_operations=state_operations,
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 11:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0002_alter_snapshot_date_alter_snapshot_diff"),
]
operations = [
migrations.AddField(
model_name="transaction",
name="real_date",
field=models.DateField(blank=True, null=True),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 11:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0003_transaction_real_date"),
]
operations = [
migrations.AddField(
model_name="transaction",
name="payment",
field=models.CharField(blank=True, max_length=128, null=True),
),
]

View file

@ -1,18 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0004_transaction_payment"),
]
operations = [
migrations.AlterField(
model_name="category",
name="name",
field=models.CharField(default="New Category", max_length=64),
),
]

View file

@ -1,146 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-20 14:26
import datetime
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0005_alter_category_name"),
]
operations = [
migrations.AlterModelOptions(
name="category",
options={
"ordering": ["name"],
"verbose_name": "Catégorie",
"verbose_name_plural": "Catégories",
},
),
migrations.AlterModelOptions(
name="invoice",
options={"verbose_name": "Facture", "verbose_name_plural": "Factures"},
),
migrations.AlterModelOptions(
name="snapshot",
options={
"ordering": ["-date"],
"verbose_name": "Relevé",
"verbose_name_plural": "Relevés",
},
),
migrations.AlterModelOptions(
name="transaction",
options={
"ordering": ["-date"],
"verbose_name": "Transaction",
"verbose_name_plural": "Transactions",
},
),
migrations.AddField(
model_name="category",
name="budget",
field=models.BooleanField(default=True, verbose_name="Budget"),
),
migrations.AlterField(
model_name="category",
name="icon",
field=models.CharField(
default="folder", max_length=64, verbose_name="Icône"
),
),
migrations.AlterField(
model_name="category",
name="name",
field=models.CharField(
default="Catégorie", max_length=64, verbose_name="Nom"
),
),
migrations.AlterField(
model_name="invoice",
name="file",
field=models.FileField(
upload_to="invoices/",
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="Fichier",
),
),
migrations.AlterField(
model_name="invoice",
name="name",
field=models.CharField(
default="Facture", max_length=256, verbose_name="Nom"
),
),
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(unique=True, verbose_name="Date"),
),
migrations.AlterField(
model_name="snapshot",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Valeur"
),
),
migrations.AlterField(
model_name="transaction",
name="category",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.category",
verbose_name="Catégorie",
),
),
migrations.AlterField(
model_name="transaction",
name="date",
field=models.DateField(default=datetime.date.today, verbose_name="Date"),
),
migrations.AlterField(
model_name="transaction",
name="description",
field=models.TextField(blank=True, null=True, verbose_name="Description"),
),
migrations.AlterField(
model_name="transaction",
name="name",
field=models.CharField(
default="Transaction", max_length=256, verbose_name="Nom"
),
),
migrations.AlterField(
model_name="transaction",
name="payment",
field=models.CharField(
blank=True, max_length=128, null=True, verbose_name="Paiement"
),
),
migrations.AlterField(
model_name="transaction",
name="real_date",
field=models.DateField(blank=True, null=True, verbose_name="Date réelle"),
),
migrations.AlterField(
model_name="transaction",
name="trader",
field=models.CharField(
blank=True, max_length=128, null=True, verbose_name="Commerçant"
),
),
migrations.AlterField(
model_name="transaction",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Valeur"
),
),
]

View file

@ -1,45 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-21 08:53
import datetime
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0006_alter_category_options_alter_invoice_options_and_more"),
]
operations = [
migrations.AddField(
model_name="snapshot",
name="file",
field=models.FileField(
blank=True,
max_length=128,
null=True,
upload_to="snapshots/",
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="Fichier",
),
),
migrations.AlterField(
model_name="invoice",
name="file",
field=models.FileField(
max_length=128,
upload_to="invoices/",
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="Fichier",
),
),
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(
default=datetime.date.today, unique=True, verbose_name="Date"
),
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-21 15:57
from django.contrib.postgres.operations import TrigramExtension, UnaccentExtension
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("main", "0007_snapshot_file_alter_invoice_file_alter_snapshot_date"),
]
operations = [
TrigramExtension(),
UnaccentExtension(),
]

View file

@ -1,48 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-28 11:20
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
import main.models
class Migration(migrations.Migration):
dependencies = [
("main", "0008_auto_20221221_1657"),
]
operations = [
migrations.AlterField(
model_name="invoice",
name="file",
field=models.FileField(
max_length=128,
upload_to=main.models.get_path,
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="Fichier",
),
),
migrations.AlterField(
model_name="invoice",
name="transaction",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.transaction",
),
),
migrations.AlterField(
model_name="snapshot",
name="file",
field=models.FileField(
blank=True,
default="",
max_length=256,
upload_to=main.models.get_path,
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="Fichier",
),
),
]

View file

@ -1,60 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 18:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("main", "0009_alter_invoice_file_alter_invoice_transaction_and_more"),
]
operations = [
migrations.AddField(
model_name="category",
name="user",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
preserve_default=False,
),
migrations.AddField(
model_name="invoice",
name="user",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
preserve_default=False,
),
migrations.AddField(
model_name="snapshot",
name="user",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
preserve_default=False,
),
migrations.AddField(
model_name="transaction",
name="user",
field=models.ForeignKey(
default=1,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
preserve_default=False,
),
]

View file

@ -1,56 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 18:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("main", "0010_category_user_invoice_user_snapshot_user_and_more"),
]
operations = [
migrations.AlterField(
model_name="category",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
),
migrations.AlterField(
model_name="invoice",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
),
migrations.AlterField(
model_name="snapshot",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
),
migrations.AlterField(
model_name="transaction",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="Utilisateur",
),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 18:22
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0011_alter_category_user_alter_invoice_user_and_more"),
]
operations = [
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(default=datetime.date.today, verbose_name="Date"),
),
]

View file

@ -1,20 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0013_account_snapshot_account_transaction_account"),
]
operations = [
migrations.AlterField(
model_name="account",
name="icon",
field=models.CharField(
default="building-columns", max_length=64, verbose_name="Icône"
),
),
]

View file

@ -1,71 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:26
import datetime
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0014_alter_account_icon"),
]
operations = [
migrations.AlterModelOptions(
name="snapshot",
options={
"ordering": ["-date"],
"verbose_name": "Statement",
"verbose_name_plural": "Statements",
},
),
migrations.RemoveField(
model_name="snapshot",
name="diff",
),
migrations.RemoveField(
model_name="snapshot",
name="previous",
),
migrations.AddField(
model_name="snapshot",
name="start_date",
field=models.DateField(
default=datetime.date.today, verbose_name="Start date"
),
),
migrations.AddField(
model_name="snapshot",
name="start_value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Start value"
),
),
migrations.AddField(
model_name="transaction",
name="snapshot",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.snapshot",
verbose_name="Statement",
),
),
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(
default=datetime.date.today, verbose_name="End date"
),
),
migrations.AlterField(
model_name="snapshot",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="End value"
),
),
]

View file

@ -1,46 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:32
import django.db.models.deletion
from django.db import migrations, models
def set_snapshot(apps, schema_editor):
Transaction = apps.get_model("main", "Transaction")
Snapshot = apps.get_model("main", "Snapshot")
for transaction in Transaction.objects.filter(snapshot__isnull=True):
transaction.snapshot = (
Snapshot.objects.filter(user=transaction.user).order_by("-date").first()
)
transaction.save()
class Migration(migrations.Migration):
dependencies = [
("main", "0015_alter_snapshot_options_remove_snapshot_diff_and_more"),
]
operations = [
migrations.AlterField(
model_name="transaction",
name="account",
field=models.ForeignKey(
blank=True,
editable=False,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.account",
verbose_name="Account",
),
),
migrations.RunPython(set_snapshot),
migrations.AlterField(
model_name="transaction",
name="snapshot",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.snapshot",
verbose_name="Statement",
),
),
]

View file

@ -1,44 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:34
import django.db.models.deletion
from django.db import migrations, models
def set_account(apps, schema_editor):
Transaction = apps.get_model("main", "Transaction")
Snapshot = apps.get_model("main", "Snapshot")
Account = apps.get_model("main", "Account")
for snapshot in Snapshot.objects.filter(account__isnull=True):
account = Account.objects.filter(user=snapshot.user).first()
if account is None:
account = Account(user=snapshot.user)
account.save()
snapshot.account = account
snapshot.save()
for transaction in Transaction.objects.filter(
account__isnull=True, snapshot__isnull=False
):
transaction.account = transaction.snapshot.account
transaction.save()
class Migration(migrations.Migration):
atomic = False
dependencies = [
("main", "0016_alter_transaction_account_alter_transaction_snapshot"),
]
operations = [
migrations.RunPython(set_account, atomic=True),
migrations.AlterField(
model_name="transaction",
name="account",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
]

View file

@ -1,34 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:56
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0017_alter_transaction_account"),
]
operations = [
migrations.AddField(
model_name="snapshot",
name="diff",
field=models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Différence",
),
),
migrations.AlterField(
model_name="snapshot",
name="account",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
]

View file

@ -1,24 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 20:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0018_snapshot_diff_alter_snapshot_account"),
]
operations = [
migrations.AddField(
model_name="snapshot",
name="sum",
field=models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Transaction difference",
),
),
]

View file

@ -1,141 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-29 21:15
import datetime
import re
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0019_snapshot_sum"),
]
operations = [
migrations.AlterModelOptions(
name="account",
options={
"ordering": ["name"],
"verbose_name": "Compte",
"verbose_name_plural": "Comptes",
},
),
migrations.AlterModelOptions(
name="snapshot",
options={
"ordering": ["-date"],
"verbose_name": "Relevé",
"verbose_name_plural": "Relevés",
},
),
migrations.AlterField(
model_name="account",
name="icon",
field=models.CharField(
default="building-columns",
max_length=64,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
verbose_name="Icône",
),
),
migrations.AlterField(
model_name="account",
name="name",
field=models.CharField(default="Compte", max_length=64, verbose_name="Nom"),
),
migrations.AlterField(
model_name="category",
name="icon",
field=models.CharField(
default="folder",
max_length=64,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
verbose_name="Icône",
),
),
migrations.AlterField(
model_name="snapshot",
name="account",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Compte",
),
),
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(
default=datetime.date.today, verbose_name="Date de fin"
),
),
migrations.AlterField(
model_name="snapshot",
name="start_date",
field=models.DateField(
default=datetime.date.today, verbose_name="Date de début"
),
),
migrations.AlterField(
model_name="snapshot",
name="start_value",
field=models.DecimalField(
decimal_places=2,
default=0,
max_digits=12,
verbose_name="Valeur de début",
),
),
migrations.AlterField(
model_name="snapshot",
name="sum",
field=models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Différence des transactions",
),
),
migrations.AlterField(
model_name="snapshot",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Valeur de fin"
),
),
migrations.AlterField(
model_name="transaction",
name="account",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Compte",
),
),
migrations.AlterField(
model_name="transaction",
name="snapshot",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.snapshot",
verbose_name="Relevé",
),
),
]

View file

@ -1,297 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-31 08:51
import datetime
import re
import django.core.validators
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import main.models
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("main", "0020_alter_account_options_alter_snapshot_options_and_more"),
]
operations = [
migrations.AlterModelOptions(
name="account",
options={
"ordering": ["name"],
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
},
),
migrations.AlterModelOptions(
name="category",
options={
"ordering": ["name"],
"verbose_name": "Category",
"verbose_name_plural": "Categories",
},
),
migrations.AlterModelOptions(
name="invoice",
options={"verbose_name": "Invoice", "verbose_name_plural": "Invoices"},
),
migrations.AlterModelOptions(
name="snapshot",
options={
"ordering": ["-date"],
"verbose_name": "Statement",
"verbose_name_plural": "Statements",
},
),
migrations.AlterField(
model_name="account",
name="icon",
field=models.CharField(
default="building-columns",
max_length=64,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
verbose_name="Icon",
),
),
migrations.AlterField(
model_name="account",
name="name",
field=models.CharField(
default="Account", max_length=64, verbose_name="Name"
),
),
migrations.AlterField(
model_name="account",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
migrations.AlterField(
model_name="category",
name="icon",
field=models.CharField(
default="folder",
max_length=64,
validators=[
django.core.validators.RegexValidator(
re.compile("^[-a-zA-Z0-9_]+\\Z"),
"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.",
"invalid",
)
],
verbose_name="Icon",
),
),
migrations.AlterField(
model_name="category",
name="name",
field=models.CharField(
default="Category", max_length=64, verbose_name="Name"
),
),
migrations.AlterField(
model_name="category",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
migrations.AlterField(
model_name="invoice",
name="file",
field=models.FileField(
max_length=128,
upload_to=main.models.get_path,
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="File",
),
),
migrations.AlterField(
model_name="invoice",
name="name",
field=models.CharField(
default="Invoice", max_length=256, verbose_name="Name"
),
),
migrations.AlterField(
model_name="invoice",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
migrations.AlterField(
model_name="snapshot",
name="account",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
migrations.AlterField(
model_name="snapshot",
name="date",
field=models.DateField(
default=datetime.date.today, verbose_name="End date"
),
),
migrations.AlterField(
model_name="snapshot",
name="diff",
field=models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Difference",
),
),
migrations.AlterField(
model_name="snapshot",
name="file",
field=models.FileField(
blank=True,
default="",
max_length=256,
upload_to=main.models.get_path,
validators=[django.core.validators.FileExtensionValidator(["pdf"])],
verbose_name="File",
),
),
migrations.AlterField(
model_name="snapshot",
name="start_date",
field=models.DateField(
default=datetime.date.today, verbose_name="Start date"
),
),
migrations.AlterField(
model_name="snapshot",
name="start_value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Start value"
),
),
migrations.AlterField(
model_name="snapshot",
name="sum",
field=models.DecimalField(
decimal_places=2,
default=0,
editable=False,
max_digits=12,
verbose_name="Transaction difference",
),
),
migrations.AlterField(
model_name="snapshot",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
migrations.AlterField(
model_name="snapshot",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="End value"
),
),
migrations.AlterField(
model_name="transaction",
name="account",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to="main.account",
verbose_name="Account",
),
),
migrations.AlterField(
model_name="transaction",
name="category",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="main.category",
verbose_name="Category",
),
),
migrations.AlterField(
model_name="transaction",
name="name",
field=models.CharField(
default="Transaction", max_length=256, verbose_name="Name"
),
),
migrations.AlterField(
model_name="transaction",
name="payment",
field=models.CharField(
blank=True, max_length=128, null=True, verbose_name="Payment"
),
),
migrations.AlterField(
model_name="transaction",
name="real_date",
field=models.DateField(blank=True, null=True, verbose_name="Real date"),
),
migrations.AlterField(
model_name="transaction",
name="snapshot",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="main.snapshot",
verbose_name="Statement",
),
),
migrations.AlterField(
model_name="transaction",
name="trader",
field=models.CharField(
blank=True, max_length=128, null=True, verbose_name="Trader"
),
),
migrations.AlterField(
model_name="transaction",
name="user",
field=models.ForeignKey(
editable=False,
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="User",
),
),
migrations.AlterField(
model_name="transaction",
name="value",
field=models.DecimalField(
decimal_places=2, default=0, max_digits=12, verbose_name="Value"
),
),
]

View file

@ -1,27 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-31 08:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("main", "0021_alter_account_options_alter_category_options_and_more"),
]
operations = [
migrations.AlterField(
model_name="account",
name="icon",
field=models.SlugField(
default="building-columns", max_length=64, verbose_name="Icon"
),
),
migrations.AlterField(
model_name="category",
name="icon",
field=models.SlugField(
default="folder", max_length=64, verbose_name="Icon"
),
),
]

View file

@ -1,57 +0,0 @@
# Generated by Django 4.1.4 on 2022-12-31 16:41
import pathlib
from django.conf import settings
from django.db import migrations
import main.models
def move_files(apps, schema_editor):
Invoice = apps.get_model("main", "Invoice")
Snapshot = apps.get_model("main", "Snapshot")
for invoice in Invoice.objects.all():
print(f"Invoice {invoice.pk}")
if invoice.file is None:
invoice.delete()
continue
try:
initial_path = pathlib.Path(invoice.file.path)
except ValueError:
invoice.delete()
continue
if invoice.file is None or not initial_path.exists():
print(f"!!! Path {initial_path} does not exist")
invoice.delete()
continue
prev_file = invoice.file.path
invoice.file.save(main.models.get_path(invoice, None), invoice.file)
pathlib.Path(prev_file).unlink()
for snapshot in Snapshot.objects.filter(file__isnull=False):
print(f"Snapshot {snapshot.pk}")
try:
initial_path = pathlib.Path(snapshot.file.path)
except ValueError:
snapshot.file = None
snapshot.save()
continue
if not initial_path.exists():
print(f"!!! Path {initial_path} does not exist")
snapshot.file = None
snapshot.save()
continue
prev_file = snapshot.file.path
snapshot.file.save(main.models.get_path(snapshot, None), snapshot.file)
pathlib.Path(prev_file).unlink()
class Migration(migrations.Migration):
dependencies = [
("main", "0022_alter_account_icon_alter_category_icon"),
]
operations = [migrations.RunPython(move_files)]

View file

@ -1,319 +1,46 @@
import datetime
import pathlib
import uuid
from django.conf import settings
from django.core.validators import FileExtensionValidator, validate_slug
from django.db import models, transaction
from django.urls import reverse
from django.utils.text import format_lazy
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.utils.translation import gettext_lazy as _
class UserModel(models.Model):
class NummiQuerySet(models.QuerySet):
main_field = "name"
fields = dict()
def search(self, search):
return (
self.annotate(
rank=SearchRank(
sum(
(
SearchVector(field, weight=weight)
for field, weight in self.fields.items()
),
start=SearchVector(self.main_field, weight="A"),
),
SearchQuery(search, search_type="websearch"),
),
similarity=TrigramSimilarity(self.main_field, search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank")
)
class NummiModel(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_("User"),
editable=False,
)
objects = NummiQuerySet.as_manager()
class Meta:
abstract = True
class CustomModel(UserModel):
@property
def adding(self):
return self._state.adding
class Meta:
abstract = True
def get_path(instance, filename):
return pathlib.Path(
"user",
str(instance.user.username),
instance._meta.model_name,
str(instance.pk),
).with_suffix(".pdf")
class Account(CustomModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=64, default=_("Account"), verbose_name=_("Name"))
icon = models.SlugField(
max_length=64,
default="building-columns",
verbose_name=_("Icon"),
)
default = models.BooleanField(default=False, verbose_name=_("Default"))
def save(self, *args, **kwargs):
if self.default:
for ac in Account.objects.filter(user=self.user, default=True):
ac.default = False
ac.save()
super().save(*args, **kwargs)
def __str__(self):
return str(self.name)
def get_absolute_url(self):
return reverse("account", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_account", kwargs={"pk": self.pk})
@property
def transactions(self):
return Transaction.objects.filter(account=self)
@property
def snapshots(self):
return Snapshot.objects.filter(account=self)
class Meta:
ordering = ["-default", "name"]
verbose_name = _("Account")
verbose_name_plural = _("Accounts")
class AccountModel(CustomModel):
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,
verbose_name=_("Account"),
)
class Meta:
abstract = True
class Category(CustomModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name")
)
icon = models.SlugField(
max_length=64,
default="folder",
verbose_name=_("Icon"),
)
budget = models.BooleanField(default=True, verbose_name=_("Budget"))
def __str__(self):
return str(self.name)
def get_absolute_url(self):
return reverse("category", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_category", kwargs={"pk": self.pk})
@property
def transactions(self):
return Transaction.objects.filter(category=self)
class Meta:
ordering = ["name"]
verbose_name = _("Category")
verbose_name_plural = _("Categories")
class Snapshot(AccountModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
date = models.DateField(default=datetime.date.today, verbose_name=_("End date"))
start_date = models.DateField(
default=datetime.date.today, verbose_name=_("Start date")
)
value = models.DecimalField(
max_digits=12, decimal_places=2, default=0, verbose_name=_("End value")
)
start_value = models.DecimalField(
max_digits=12, decimal_places=2, default=0, verbose_name=_("Start value")
)
diff = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
verbose_name=_("Difference"),
editable=False,
)
sum = models.DecimalField(
max_digits=12,
decimal_places=2,
default=0,
verbose_name=_("Transaction difference"),
editable=False,
)
file = models.FileField(
upload_to=get_path,
validators=[FileExtensionValidator(["pdf"])],
verbose_name=_("File"),
max_length=256,
blank=True,
default="",
)
def __str__(self):
desc = _("%(date)s statement") % {"date": self.date}
if hasattr(self, "account"):
return f"{desc} {self.account}"
return desc
def save(self, *args, **kwargs):
if Snapshot.objects.filter(id=self.id).exists():
_prever = Snapshot.objects.get(id=self.id)
if _prever.file and _prever.file != self.file:
pathlib.Path(_prever.file.path).unlink(missing_ok=True)
with transaction.atomic():
for trans in self.transaction_set.all():
trans.save()
self.diff = self.value - self.start_value
self.sum = (
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
)
super().save(*args, **kwargs)
def update_sum(self):
self.sum = (
self.transaction_set.aggregate(sum=models.Sum("value")).get("sum", 0) or 0
)
super().save()
def delete(self, *args, **kwargs):
if self.file:
self.file.delete()
super().delete(*args, **kwargs)
def get_absolute_url(self):
return reverse("snapshot", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_snapshot", kwargs={"pk": self.pk})
class Meta:
ordering = ["-date", "account"]
verbose_name = _("Statement")
verbose_name_plural = _("Statements")
class Transaction(CustomModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(
max_length=256, default=_("Transaction"), verbose_name=_("Name")
)
description = models.TextField(null=True, blank=True, verbose_name=_("Description"))
value = models.DecimalField(
max_digits=12, decimal_places=2, default=0, verbose_name=_("Value")
)
date = models.DateField(default=datetime.date.today, verbose_name=_("Date"))
real_date = models.DateField(blank=True, null=True, verbose_name=_("Real date"))
trader = models.CharField(
max_length=128, blank=True, null=True, verbose_name=_("Trader")
)
payment = models.CharField(
max_length=128, blank=True, null=True, verbose_name=_("Payment")
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
blank=True,
null=True,
verbose_name=_("Category"),
)
snapshot = models.ForeignKey(
Snapshot,
on_delete=models.CASCADE,
verbose_name=_("Statement"),
)
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,
verbose_name=_("Account"),
editable=False,
)
def save(self, *args, **kwargs):
if Transaction.objects.filter(pk=self.pk):
prev_self = Transaction.objects.get(pk=self.pk)
else:
prev_self = None
self.account = self.snapshot.account
super().save(*args, **kwargs)
if prev_self is not None:
prev_self.snapshot.update_sum()
self.snapshot.update_sum()
def __str__(self):
return f"{self.date} {self.name}"
def get_absolute_url(self):
return reverse("transaction", kwargs={"pk": self.pk})
def get_delete_url(self):
return reverse("del_transaction", kwargs={"pk": self.pk})
@property
def invoices(self):
return Invoice.objects.filter(transaction=self)
@property
def has_invoice(self):
return self.invoices.count() > 0
class Meta:
ordering = ["-date", "snapshot"]
verbose_name = _("Transaction")
verbose_name_plural = _("Transactions")
class Invoice(CustomModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(
max_length=256, default=_("Invoice"), verbose_name=_("Name")
)
file = models.FileField(
upload_to=get_path,
validators=[FileExtensionValidator(["pdf"])],
verbose_name=_("File"),
max_length=128,
)
transaction = models.ForeignKey(
Transaction, on_delete=models.CASCADE, editable=False
)
def save(self, *args, **kwargs):
if Invoice.objects.filter(id=self.id).exists():
_prever = Invoice.objects.get(id=self.id)
if _prever.file and _prever.file != self.file:
pathlib.Path(_prever.file.path).unlink(missing_ok=True)
super().save(*args, **kwargs)
def __str__(self):
if hasattr(self, "transaction"):
return str(format_lazy("{} {}", self.name, self.transaction.name))
return str(self.name)
def delete(self, *args, **kwargs):
self.file.delete()
super().delete(*args, **kwargs)
def get_absolute_url(self):
return reverse(
"invoice", kwargs={"transaction_pk": self.transaction.pk, "pk": self.pk}
)
def get_delete_url(self):
return reverse(
"del_invoice", kwargs={"transaction_pk": self.transaction.pk, "pk": self.pk}
)
class Meta:
verbose_name = _("Invoice")
verbose_name_plural = _("Invoices")
ordering = ["transaction", "name"]

View file

@ -1,59 +0,0 @@
.chart {
display: grid;
grid-template-columns: auto auto 1fr 1fr auto;
grid-gap: var(--gap) 0;
}
.chart > div {
position: relative;
height: 2rem;
line-height: 2rem;
}
.chart .left {
text-align: right;
}
.chart .bar,
.chart .value {
display: inline-block;
height: 2rem;
line-height: 2rem;
}
.chart .value {
padding: 0 var(--gap);
font-feature-settings: var(--num);
text-align: right;
}
.chart .bar {
width: 0;
box-sizing: border-box;
z-index: 1;
}
.chart .bar.tot {
position: absolute;
z-index: 10;
height: .5rem;
background: black;
}
.chart .left .bar.tot {right: 0}
.chart .right .bar.tot {left: 0}
.chart .left .bar {border-radius: var(--radius) 0 0 var(--radius)}
.chart .right .bar {border-radius: 0 var(--radius) var(--radius) 0}
.chart .bar_m {background: var(--red-1)}
.chart .bar_p {background: var(--green-1)}
.chart .bar span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: .5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num);
}
.chart .right .bar span {left: 0}
.chart .left .bar span {right: 0}

View file

@ -1,92 +1,217 @@
.drop-zone {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
align-items: center;
text-align: center;
color: transparent;
display: grid;
transition-property: backdrop-filter;
transition-duration: 750ms;
z-index: -1;
> span {
font-weight: 650;
font-size: 2rem;
transition-property: color;
transition-duration: inherit;
}
main.highlight > & {
z-index: 100;
backdrop-filter: blur(0.1rem);
> span {
color: var(--green);
}
}
}
form {
display: grid;
grid-template-columns: auto 1fr;
grid-gap: var(--gap);
margin: var(--gap) 0;
line-height: 2rem;
}
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, 32rem);
@media (width < 1024px) {
grid-template-columns: 1fr;
}
form ul.errorlist {
grid-column: 1 / -1;
color: var(--red);
font-weight: 550;
list-style-type: "! ";
margin: 0;
}
&.hidden {
display: none;
}
form input,
form select,
form a {
padding: 0 var(--gap);
}
form textarea {
padding: var(--gap);
}
.column {
display: grid;
gap: 0.5rem;
border: 1px solid var(--gray);
padding: var(--gap);
block-size: min-content;
}
.fieldset {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: inherit;
padding: 0;
margin: 0;
border: none;
}
form input {
color: inherit;
}
ul.errorlist {
color: var(--red);
font-weight: 550;
list-style: none;
padding: 0;
margin: 0;
}
form > label,
form > div > input[type="text"],
form > div > input[type="number"],
form > div > input[type="password"],
form > div > input[type="file"],
form > div > select,
form > div > textarea,
form > div > select {
font-size: inherit;
line-height: inherit;
font-family: inherit;
/*height: 100%;*/
width: 100%;
}
form > div > select {
height: 100%;
}
form > div > textarea {
line-height: initial;
}
form > div > input[type="text"],
form > div > input[type="number"],
form > div > input[type="password"],
form > div > input[type="file"],
form > div > textarea,
form > div > select {
border: none;
border-radius: var(--radius);
background: var(--bg-01);
}
.field {
display: grid;
grid-auto-rows: min-content;
align-items: center;
column-gap: 0.5rem;
form > .buttons {
grid-column: 1 / -1;
text-align: right;
}
form > .buttons > * {
font-size: inherit;
line-height: inherit;
font-family: inherit;
border: 0;
background: var(--bg-01);
cursor: pointer;
border-radius: var(--radius);
display: inline-block;
}
form > .buttons > * {
margin-left: var(--gap);
}
form > .buttons > *:hover {
text-decoration: underline;
}
form > .buttons input[type="submit"] {
background: var(--green-1);
}
form > .buttons a.del,
form > .buttons input.del {
background: var(--red);
color: var(--text-inv);
}
ul.errorlist {
font-size: 0.8rem;
}
form input[hidden] {
display: none;
&:has(> textarea) {
grid-template-rows: min-content 1fr;
textarea {
resize: block;
}
}
&:has(> input[type="checkbox"]) {
grid-template-columns: min-content 1fr;
> label {
font-size: inherit;
grid-row: 1;
grid-column: 2;
padding: 0.5rem;
line-height: initial;
}
> input {
grid-row: 1;
grid-column: 1;
margin: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
> label {
font-size: 0.8rem;
line-height: 0.8rem;
z-index: 10;
}
> a {
padding: 0.5rem;
}
input,
select,
textarea {
font: inherit;
line-height: initial;
border: none;
border: 1px solid transparent;
border-bottom: 1px solid var(--gray);
background: none;
z-index: 1;
padding: 0.5rem;
&:has(~ ul.errorlist) {
border-color: var(--red);
}
&.autocompleted:not(.mod) {
border-bottom-color: var(--green);
}
&:not([type="checkbox"]) {
width: 100%;
margin: 0;
}
&[name*="value"] {
text-align: right;
font-feature-settings: var(--num);
}
&[name*="date"] {
font-feature-settings: var(--num);
}
&:focus {
outline: none;
background: var(--bg-1);
}
}
> .file-input {
display: grid;
> .current {
display: grid;
grid-template-columns: 1fr;
grid-auto-columns: max-content;
grid-auto-flow: column;
a {
padding: 0.5rem;
}
}
input[type="file"] {
&::file-selector-button {
display: none;
}
}
}
> .ico-input {
display: grid;
grid-template-columns: min-content 1fr;
column-gap: 0.5rem;
align-items: center;
span[class|="ri"] {
padding: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
}
.buttons {
grid-column: 1 / -1;
line-height: 2rem;
input,
a {
font: inherit;
cursor: pointer;
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
color: inherit;
&:hover {
text-decoration: underline;
}
}
input[type="submit"] {
background: var(--green-1);
border-color: var(--green);
}
input[type="reset"] {
background: var(--bg-1);
}
a.del {
color: var(--red);
border-color: var(--red);
border-style: dashed;
}
}
}

View file

@ -1,23 +0,0 @@
h1 {
font-size: 4rem;
font-variant: small-caps;
font-weight: 850;
letter-spacing: -.05em;
margin: 0;
line-height: 1.2em;
}
h1 img {
height: 1.2em;
vertical-align: top;
margin-right: var(--gap);
}
#categories > a,
#accounts > a {
display: inline-block;
padding: 1em;
}
#categories > a > i,
#accounts > a > i {
margin-right: .5rem;
}

View file

@ -1,70 +1,572 @@
@import "https://rsms.me/inter/inter.css";
@import "https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css";
*, *::before, *::after {
box-sizing: border-box;
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--theme: #66cc66;
--theme-0: #338033;
--theme-1: #99ff99;
--theme-2: var(--theme);
--theme-3: #802653;
--theme-4: #cc6699;
--theme: #66cc66;
--theme-0: #338033;
--theme-1: #99ff99;
--theme-2: var(--theme);
--theme-3: #802653;
--theme-4: #cc6699;
--gray: #dedede;
--text-theme: var(--text);
--text-theme: var(--text);
--bg: #ffffff;
--text: #000000de;
--bg: #ffffff;
--text: #000000de;
--bg-inv: var(--theme-1);
--text-inv: #ffffffde;
--bg-inv: var(--theme-1);
--text-inv: #ffffffde;
--bg-01: #f0f0f0;
--bg-1: #f0f0f0;
--text-green: #296629;
--text-link: var(--text-green);
--text-green: #296629;
--text-link: var(--text-green);
--gap: 1rem;
--gap: 1rem;
--red: var(--theme-3);
--green: var(--theme-0);
--red: var(--theme-3);
--green: var(--theme-0);
--red-1: var(--theme-4);
--green-1: var(--theme);
--red-1: var(--theme-4);
--green-1: var(--theme);
--border: .5em;
--radius: .25em;
--border: 0.5em;
--radius: 0.25em;
--num: "tnum", "ss01", "case";
--default-ffs: "dlig", "ss01", "ss04";
--num: var(--default-ffs), "tnum", "case";
}
body {
font-family: "Inter var experimental", "Inter var", "Inter", sans-serif;
margin: 0;
padding: 2em;
font-family: "Inter var experimental", "Inter var", "Inter", sans-serif;
margin: 0;
background: var(--bg);
color: var(--text);
background: var(--bg);
color: var(--text);
display: grid;
grid-template-columns: max-content 1fr;
font-feature-settings: var(--default-ffs);
}
h1 {
margin: 0;
p {
line-height: 2em;
}
a {
color: var(--text-link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: var(--text-link);
text-decoration: none;
display: inline-block;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
.red {color: var(--red)}
.green {color: var(--text-green)}
.split {
display: grid;
grid-template-columns: repeat(2, auto);
grid-gap: var(--gap);
.red {
color: var(--red);
fill: var(--red);
}
.green {
color: var(--green);
fill: var(--green);
}
main,
nav,
footer {
padding: 2rem 1rem;
@media (width > 720px) {
padding: 2rem;
}
background: var(--bg);
}
main {
position: relative;
grid-column: 2;
grid-row: 1;
overflow-x: hidden;
h2.new {
opacity: 0.8;
}
.split {
display: grid;
gap: var(--gap);
grid-template-columns: 100%;
@media (width > 720px) {
grid-template-columns: minmax(20rem, max-content) 1fr;
}
& > section > :first-child {
margin-top: 0;
}
}
}
nav {
grid-column: 1;
grid-row: 1;
height: 100vh;
position: sticky;
top: 0;
overflow-y: auto;
background: var(--bg-1);
line-height: 2rem;
h1 img {
height: 1cap;
}
ul {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
a {
&.skip-link {
font-weight: 300;
&:is(:active, :focus) {
font-weight: 500;
}
}
display: grid;
grid-template-columns: 1fr max-content;
align-items: baseline;
[class^="ri-"] {
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
border-radius: var(--radius);
}
&.cur {
font-weight: 550;
[class^="ri-"] {
background: var(--text-link);
color: var(--bg);
}
}
}
}
:is(nav, main) > :first-child,
main > section:first-child > :first-child {
margin-top: 0;
}
footer {
text-align: center;
grid-column: 2;
font-weight: 250;
}
.pagination {
text-align: center;
font-feature-settings: var(--num);
a {
min-width: 1rem;
padding: 0 0.5rem;
&.cur {
font-weight: 650;
text-decoration: underline dotted;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
}
@media (width > 720px) {
&.n3 {
display: grid;
grid-template-columns: repeat(3, 1fr);
width: max-content;
margin: 0.5rem auto;
.prev {
grid-column: 1;
}
.cur {
grid-column: 2;
}
.next {
grid-column: 3;
}
}
}
& + section :first-child {
margin-top: 0;
}
}
@media (width < 1024px) {
body {
grid-template-columns: 1fr;
}
nav,
main,
footer {
grid-column: 1;
grid-row: auto;
position: relative;
height: initial;
}
}
[class^="ri-"] {
display: inline-block;
text-align: center;
font-weight: normal;
&.green,
&.red,
&.white {
&.green {
background: var(--green);
color: var(--bg);
}
&.red {
background: var(--red);
color: var(--bg);
}
&.white {
background: var(--bg-1);
}
border-radius: var(--radius);
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
}
a:not(.i) &,
.wi &,
h2 & {
&:first-child::after {
content: "\2002";
}
}
}
h1,
h2,
h3 {
font-weight: 300;
margin-top: 1em;
margin-bottom: 0.5em;
line-height: 1cap;
}
h1 {
font-size: 3rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.5rem;
}
p {
margin: 0.5em 0;
}
ul.messages {
font-weight: 550;
list-style-type: none;
margin: 0;
margin-bottom: var(--gap);
background: var(--bg-1);
padding: 0;
li {
--message-color: var(--text);
padding: calc(var(--gap) / 2) var(--gap);
border-left: var(--message-color) solid var(--border);
[class^="ri-"] {
height: 1.5em;
width: 1.5em;
line-height: 1.5em;
border-radius: var(--radius);
background: var(--message-color);
color: var(--bg);
margin-right: 0.5rem;
}
&.msg-level-20 {
--message-color: var(--green);
}
&.msg-level-25 {
--message-color: var(--green-1);
}
&.msg-level-30 {
--message-color: var(--red-1);
}
&.msg-level-40 {
--message-color: var(--red);
}
}
}
.backlinks {
display: grid;
grid-template-columns: repeat(2, 1fr);
p {
grid-column: 1;
&.back {
grid-column: 2;
text-align: right;
a {
margin-right: 0;
margin-left: 1em;
[class^="ri-"] {
margin-right: 0em;
margin-left: 0.5em;
}
}
}
}
}
dl.accounts {
margin: 0;
dt,
dd {
margin: 0;
}
.account {
padding: 0.5rem;
margin-bottom: 0.5rem;
border: 1px solid var(--gray);
display: grid;
grid-template-columns: 1fr min-content;
&.new,
&.more {
border-style: dashed;
}
&.more label {
display: block;
}
}
&:not(.show-archive) .account.archived {
display: none;
}
}
ul.statements,
ul.invoices {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
grid-auto-rows: 1fr;
gap: 0.5rem;
list-style: none;
padding: 0;
margin: 0;
li {
display: grid;
gap: 0.5rem;
padding: var(--gap);
border: var(--gray) 1px solid;
text-align: right;
align-items: center;
> * {
&.title {
font-weight: 650;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
&.new,
&.more {
border-style: dashed;
}
}
}
ul.statements {
li {
> * {
display: grid;
align-items: center;
&.date :first-child {
font-size: 2rem;
}
&.value :first-child {
font-weight: 650;
}
&.icon {
span {
margin: auto;
}
}
&.account a {
overflow: hidden;
text-overflow: ".";
white-space: nowrap;
}
}
}
}
ul.invoices {
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
}
.statement-details {
display: grid;
@media (width > 720px) {
grid-template-columns: repeat(3, min-content);
}
gap: var(--gap);
align-items: center;
> span.evolution {
display: grid;
grid-auto-rows: min-content;
> span[class^="ri-"] {
font-size: 2rem;
}
> span.value {
text-align: right;
}
}
> span.start,
> span.end,
> span.file,
> span.incons {
display: grid;
border: var(--gray) 1px solid;
padding: var(--gap);
&.file,
&.incons {
grid-column: 1 / -1;
}
&.incons {
border-color: var(--red);
grid-template-columns: min-content 1fr;
grid-auto-flow: column;
align-items: center;
gap: 0.5rem;
> .ds {
display: grid;
grid-auto-rows: min-content;
}
.value {
text-align: right;
}
.diff {
font-weight: 650;
}
.sum {
text-decoration: line-through;
}
.links {
grid-column: 1 / -1;
grid-row: 2;
display: grid;
gap: inherit;
a {
color: var(--red);
}
}
}
> .date {
text-align: right;
}
> .value {
font-size: 2rem;
}
}
}
.multilink {
display: grid;
grid-auto-columns: max-content;
}
.transaction-details {
display: grid;
grid-auto-columns: minmax(1fr, max-content);
grid-auto-rows: min-content;
max-width: 32rem;
ul {
list-style: none;
display: grid;
li.value {
font-size: 1.5rem;
text-align: right;
}
}
p.description,
ul {
border: var(--gray) 1px solid;
padding: var(--gap);
}
}
.category,
.big-link {
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
&.add {
border-style: dashed;
}
}
.date,
.value {
font-feature-settings: var(--num);
}
details {
border: var(--gray) 1px solid;
margin-bottom: var(--gap);
summary {
font-weight: 650;
cursor: pointer;
padding: var(--gap);
&::marker {
content: "\ed27\2002";
font-family: remixicon;
font-weight: initial;
}
}
&[open] summary {
background: var(--bg-1);
}
form {
padding: var(--gap);
}
}

View file

@ -1,66 +0,0 @@
:root {
--nav-lh: 2em;
--nav-pad: 1em;
--nav-height: calc(2 * var(--nav-pad) + var(--nav-lh));
}
body {
margin-top: var(--nav-height);
}
nav {
background: var(--theme);
color: var(--text-theme);
position: absolute;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
line-height: var(--nav-lh);
padding: var(--nav-pad);
}
nav > a,
nav > form {
display: inline-block;
color: inherit;
text-decoration: inherit;
height: var(--nav-lh);
margin: 0 var(--nav-pad);
}
nav > a.cur {
font-weight: 750;
}
nav > a.logout {
float: right;
}
nav > a img {
height: var(--nav-lh);
vertical-align: top;
margin-right: .5rem;
}
nav > a h1 {
display: inline;
font: inherit;
font-variant: small-caps;
}
nav > form input {
font: inherit;
height: inherit;
border: none;
background: #ffffff35;
padding: 0 var(--gap);
}
nav > form input[type="text"] {
border-radius: var(--radius) 0 0 var(--radius);
}
nav > form input[type="submit"] {
border-radius: 0 var(--radius) var(--radius) 0;
}
nav > form input[type="submit"] {
cursor: pointer;
color: var(--green-text);
}
nav > form input[type="submit"]:hover {
text-decoration: underline;
}

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