Compare commits

..

481 commits

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
60c875c2a5
Add missing data for snapshot html plot 2023-01-03 18:36:02 +01:00
344a1558af
Update search form
Fixes #6, fixes #8
2023-01-01 11:05:54 +01:00
09fe3f6835
Update navbar layout 2023-01-01 10:39:33 +01:00
63253526fa
Update tables to hide redundant fields
Closes #7
2023-01-01 10:36:40 +01:00
695e1d4d52
Update snapshot table to hide account on account page (#7) 2023-01-01 10:27:08 +01:00
39b620135a
Update sorting 2023-01-01 10:17:06 +01:00
4c34716992
Update translations 2023-01-01 10:03:10 +01:00
d767ef1a7a
Fix invoice delete 2023-01-01 09:18:01 +01:00
01dcd561d6
Fix styling of new invoice line in invoice table 2023-01-01 09:02:25 +01:00
08227d3af3
Add default account field
Fixes #3 again
2022-12-31 19:08:09 +01:00
d9304db43d
Default fields
Fixes #3
2022-12-31 18:59:29 +01:00
748f9e5fb6
Fix migration; fix invoice saving 2022-12-31 18:44:03 +01:00
2550c52a61
Add file migration 2022-12-31 18:17:40 +01:00
06704aaa77
Limit access to files to user
No database migration is made
2022-12-31 17:28:15 +01:00
de06312a2a
Update translations 2022-12-31 12:14:28 +01:00
2a0fc642b6
Update link visibility depending on item number in table 2022-12-31 12:12:15 +01:00
65a35b31f0
Add links to create snapshot in account, transaction in snapshot 2022-12-31 11:59:33 +01:00
6b54cc7546
Hide tables on account form when empty 2022-12-31 11:39:39 +01:00
1536e7f674
Code cleanup
Use mixins for listviews
2022-12-31 11:38:39 +01:00
94671b9ac4
Add back link to list pages 2022-12-31 11:34:25 +01:00
39896129a6
Update account snapshot list 2022-12-31 11:28:39 +01:00
7f4b6a0415
Update all transaction lists 2022-12-31 11:25:46 +01:00
2a898c706e
Only show all transactions line when number is strictly higher than showed 2022-12-31 11:13:46 +01:00
7c5e804aba
Update templates with new snapshot and transaction tables
Add link on homepage to see all transactions and snapshots
2022-12-31 11:07:11 +01:00
057734afd3
Add snapshot list view, general and per account
Closes #4
2022-12-31 10:27:35 +01:00
acd83704af
Add transaction list views for snapshots and accounts (#4) 2022-12-31 10:13:15 +01:00
4e6dd25c99
Use get parameters as initial form values (#3) 2022-12-31 09:59:47 +01:00
34d5ce647c
Order form fields - closes #2 2022-12-31 09:57:34 +01:00
ce28f6d0b6
Update fields icon to slugfield 2022-12-31 09:53:30 +01:00
25e9751de2
Update translations 2022-12-31 09:51:33 +01:00
f066cf1092
Update to use lazy translations - Close #1 2022-12-31 09:49:20 +01:00
bae4fed8d6
Fix create snapshot bug 2022-12-30 18:24:49 +01:00
9ea0839ba8
Add account to statement __str__ 2022-12-30 18:05:00 +01:00
a8bd70fab1
Update PKGBUILD backup with correct config 2022-12-30 17:45:23 +01:00
411d258cd7
Fix updating of snapshot values 2022-12-30 17:37:11 +01:00
0184cba30d
Forms update
Add transactions to snapshot
Fix snapshot delete
2022-12-30 17:09:02 +01:00
ea9cd1b9b8
Updated migrations to enable transition from earlier db versions
User action is needed to cleanup everything afterwards
2022-12-30 14:10:33 +01:00
e438cfd78a
Import sorting 2022-12-29 22:18:14 +01:00
0d9ff3b771
Migration 2022-12-29 22:18:03 +01:00
fd15e60085
Validate icon slugs 2022-12-29 22:17:55 +01:00
a1c3563348
Updated translations 2022-12-29 22:12:29 +01:00
ab894867f4
Split models.py into models and forms 2022-12-29 22:08:20 +01:00
2bd2eca26e
Fix order of saving transactions
Snapshot value was not calculated correctly
2022-12-29 22:00:50 +01:00
cb253d4305
Remove useless db operation 2022-12-29 21:59:31 +01:00
c083fff07e
Update snapshots with sum db columns 2022-12-29 21:58:26 +01:00
02eb781492
Updated snapshots -- major incompatibility with previous migrations, needs manual intervention 2022-12-29 21:41:57 +01:00
b9e3ee059e
Change default icon for accounts 2022-12-29 21:06:23 +01:00
0e0b9fc927
Fix categories and accounts being visible to all users 2022-12-29 21:04:40 +01:00
e26f552aff
User settings.AUTH_USER_MODEL instead of User in models 2022-12-29 20:05:25 +01:00
7bb6b2f671
Reformat migrations 2022-12-29 20:04:55 +01:00
7356d02ada
Add account column to tables 2022-12-29 19:54:15 +01:00
f2df88d091
Add account creation and selection 2022-12-29 19:45:07 +01:00
667c6d3da1
Reformat blank lines 2022-12-29 19:23:59 +01:00
7d0ed1dd5a
Fix unique date field on snapshots 2022-12-29 19:22:38 +01:00
2ac316aaca
Add User field to models
Support for multiple users !
2022-12-29 19:20:52 +01:00
df2f2e4c37
Remove useless imports 2022-12-29 18:49:14 +01:00
963c91594a
Add local media to gitignore 2022-12-29 18:44:58 +01:00
7f6c00c0a2
Reformat settings 2022-12-29 18:44:44 +01:00
077c39adde
Fix alignment of icons on invoice list 2022-12-29 18:39:05 +01:00
49834e10e8
Add missing dependency in PKGBUILD 2022-12-29 18:21:01 +01:00
22bd94f3be
Fix permissions in PKGBUILD 2022-12-29 18:19:22 +01:00
1aaafc01e2
Use toml python package to support python3.10 2022-12-29 18:14:55 +01:00
21da8a41ec
Updated PKGBUILD and systemd service for new config 2022-12-29 17:48:02 +01:00
a06f49c2a5
Updated configuration using config.toml file instead of environment variables 2022-12-29 17:26:54 +01:00
bcbf33984b
Switched index view to class-based 2022-12-28 12:38:20 +01:00
a722165ee2
Black reformat on migration 2022-12-28 12:34:33 +01:00
0d37594f0b
Updated search view to process post via class-based view 2022-12-28 12:33:18 +01:00
a6805212fc
Updated migrations 2022-12-28 12:21:14 +01:00
ecf8962562
Updated snapshot views as class-based views 2022-12-28 12:18:20 +01:00
a43ed6c039
Updated category views as class-based views 2022-12-28 12:04:09 +01:00
033698b38a
Added views for invoices
Added class-based views for all operations
Added corresponding urls
Updated transaction page to use new views
2022-12-28 11:52:24 +01:00
d8c95fac25
Added delete view for transaction 2022-12-28 10:17:30 +01:00
a48e34281f
Removed now unused code 2022-12-28 09:56:35 +01:00
e911cdcf6b
Fix tabulation issue in template 2022-12-28 09:56:23 +01:00
60631b77b7
Update styling for textarea 2022-12-28 09:55:57 +01:00
757ce1f45f
Update views using class based views
Updated views for transaction
Invoice form temporarily removed
2022-12-28 09:54:39 +01:00
41499f1329
Update search to use listview
Enables pagination
2022-12-26 18:50:02 +01:00
c58ef42bd0
Fix translation error 2022-12-26 18:19:14 +01:00
b611085467
Update __str__ of snapshots 2022-12-26 18:11:09 +01:00
8b859651e8
Improve accessibility
Replace trash-can icon with text
2022-12-26 18:04:16 +01:00
0676b52613
Improve accessibility 2022-12-26 18:00:03 +01:00
01710a9476
Fix whitespace in navbar 2022-12-26 17:54:14 +01:00
542ddbc781
Update theme
Set background color of table header to theme color
2022-12-26 17:50:03 +01:00
47b9d6a3fb
Update categories layout on homepage 2022-12-25 16:33:41 +01:00
16dba43810
Reformat template files with djLint 2022-12-25 16:24:40 +01:00
3a31eec326
Update invoice list layout 2022-12-25 16:10:39 +01:00
93d827dea3
Update translations 2022-12-21 17:31:49 +01:00
5953d80ff2
Improve search
Search in trader
Combine ranked and trigram
2022-12-21 17:18:02 +01:00
4b4c5f827d
Add search 2022-12-21 17:05:23 +01:00
d8b00d91e8
Redirect on valid form input 2022-12-21 15:49:27 +01:00
6d0fb174eb
Organise imports using isort --profile black 2022-12-21 15:42:30 +01:00
364ad42c05
Only show budgeted categories on snapshot page 2022-12-21 15:37:57 +01:00
08b3c3dd18
Use new media server for invoices 2022-12-21 15:32:51 +01:00
ef3e3ad03e
Merge branch 'main' of ssh://git.edgarpierre.fr:39529/edpibu/nummi 2022-12-21 15:19:26 +01:00
b77c5b6064
Fix style on login page 2022-12-21 15:18:52 +01:00
98fea25615
Merge branch 'main' of ssh://git.edgarpierre.fr:39529/edpibu/nummi 2022-12-21 15:14:36 +01:00
cf23e3f79d
Try to fix permission issues with nginx 2022-12-21 15:13:21 +01:00
ee26d0d9a8
Update media file server for debugging 2022-12-21 14:56:28 +01:00
100dc9f508
Update checksums 2022-12-21 14:48:55 +01:00
4a41f998d7
Use path converter in media url 2022-12-21 14:43:01 +01:00
4a1bd56fd9
Update nginx proxy of media files 2022-12-21 14:39:56 +01:00
55021db557
Updated checksums 2022-12-21 14:28:29 +01:00
b995b9a0f5
Add missing sysusers config 2022-12-21 14:24:38 +01:00
043f5e7112
Add private media server using nginx
Use internal nginx root with X-Accel-Redirect header
2022-12-21 14:23:02 +01:00
65af7105b2
Merge pkgbuild
Add environment file, nginx config, systemd service, tmpfiles and PKGBUILD
2022-12-21 14:15:43 +01:00
ba07636a85
Add display for snapshot files on homepage 2022-12-21 14:09:38 +01:00
4efc421f04
Fix file selector styling 2022-12-21 14:05:15 +01:00
e11749f187
Add file selector to snapshots 2022-12-21 14:02:15 +01:00
996f6a9f18
Add visibility on invoices
Add links to invoices on transaction tables
Added invoices and has_invoice properties to transactions
2022-12-21 09:38:17 +01:00
7895210aea
Add default date on snapshots 2022-12-21 09:21:17 +01:00
495c64fcdc
Update layout
Modified navbar content
Moved categories up on homepage
Load all snapshots on homepage
Load 64 transactions per page
2022-12-21 09:18:47 +01:00
f2ca55f451
Update dependencies 2022-12-20 17:00:42 +01:00
b090902648
Update plot style 2022-12-20 16:31:03 +01:00
d04fc756d4
Add timeline graph to category page 2022-12-20 16:20:44 +01:00
bbcaf1c1d3
Add timeline per category 2022-12-20 16:14:06 +01:00
d88ba21869
Update style for checkbox compatibility 2022-12-20 15:45:23 +01:00
60bdff43e7
Remove non-budget categories for plot 2022-12-20 15:32:34 +01:00
396ef9ead5
Add budget option to categories 2022-12-20 15:28:56 +01:00
19490e62b2
Add category plot 2022-12-19 14:36:02 +01:00
e095d8d35f
Add first plot 2022-12-19 14:18:25 +01:00
3cf3cb649e
Default to fr-fr 2022-12-19 12:09:02 +01:00
a7e3f92469
Add form errors 2022-12-19 12:03:37 +01:00
0e2fd6e74f
Update style, fix contrast issues 2022-12-19 11:57:16 +01:00
b844947cfd
Updated text and improved button contrast 2022-12-19 11:42:48 +01:00
e6e7fa328a
Updated translations 2022-12-19 11:19:22 +01:00
edf6ae0584
Update design 2022-12-18 09:58:40 +01:00
e026445ec3
Update gitignore 2022-12-18 09:49:52 +01:00
1f40f6bdc6
Fix hidden inputs 2022-08-11 08:41:50 +02:00
178f82d36a
Start translation to fr-fr 2022-05-30 19:21:40 +02:00
0131146c55
Add styling to password field 2022-05-30 15:10:28 +02:00
78907323a6
Add accesskeys to navbar 2022-05-30 15:08:54 +02:00
984dadf7e6
Update form buttons for accessibility & nojs behavior 2022-05-30 15:04:30 +02:00
f21e2a1b2e
Fix bug when snapshot only has negative or positive transactions 2022-05-30 14:55:52 +02:00
f359426997
Fix bug when adding earliest snapshot 2022-05-30 14:48:28 +02:00
e42863dfe6
Remove matplotlib dependency 2022-05-24 17:16:22 +02:00
9bd38840f7
Add matplotlib to dependencies 2022-05-24 15:37:29 +02:00
00a214c948
Update gitignore; bump PKGBUILD 2022-05-22 18:54:44 +02:00
aeacf58612
Update PKGBUILD with b2 checksums 2022-05-21 19:06:16 +02:00
5c9668ca0c
Fix permissions on env file 2022-05-21 18:59:53 +02:00
6d4947bcca
Fix pkgver 2022-05-21 18:54:18 +02:00
2ccacd0d21
Fix pkgsums 2022-05-21 18:45:13 +02:00
74872b94b7
Add environment file 2022-05-21 18:44:46 +02:00
589f9a9b46
Fix VCS -> git in pkgbuild 2022-05-21 18:30:11 +02:00
8c3f573811
Update pkgbuild 2022-05-21 18:29:30 +02:00
c778a5fe32
Update PKGBUILD 2022-05-21 18:25:08 +02:00
ce0910ce97
Remove sha256 check 2022-05-21 18:18:55 +02:00
04d9c8ada7
Fix collectstatic --noinput 2022-05-21 18:15:34 +02:00
1e378f6306
Fix permission issues 2022-05-21 18:08:06 +02:00
ab35d774dc
Update pkgbuild and nginx to serve static 2022-05-21 17:53:07 +02:00
4157e20376
Fix socket issues 2022-05-21 16:09:40 +02:00
d3d69c513a
Remove reference to systemd socket 2022-05-21 16:08:02 +02:00
67f145a992
Make uvicorn work with socket 2022-05-21 16:07:12 +02:00
ea4959dc44
Replace gunicorn by uvicorn 2022-05-21 16:02:15 +02:00
5590308287
Add nginx conf to source 2022-05-21 16:00:06 +02:00
3b127c157b
Add nginx conf 2022-05-21 15:59:21 +02:00
7a23e798e5
Fix systemd service 2022-05-21 12:34:48 +02:00
dafa5bc75a
Update PKGBUILD 2022-05-21 12:32:03 +02:00
184 changed files with 6632 additions and 1360 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

3
.gitignore vendored
View file

@ -1,2 +1,3 @@
env
/env*
__pycache__
/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

@ -2,8 +2,3 @@
Nummi is a web-based accounting software.
Nummi uses Django.
## Installation
A PKGBUILD along with system files is provided on the
[pkgbuild](https://git.edgarpierre.fr/edpibu/nummi/src/branch/pkgbuild) branch.

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

View file

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

@ -0,0 +1,65 @@
# 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),
("main", "0002_segmentation"),
]
state_operations = [
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"
),
),
(
"icon",
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="User",
),
),
],
options={
"verbose_name": "Account",
"verbose_name_plural": "Accounts",
"ordering": ["-default", "name"],
"db_table": "account_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

@ -0,0 +1,17 @@
# Generated by Django 4.2 on 2024-12-29 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("account", "0002_alter_account_table"),
]
operations = [
migrations.AddField(
model_name="account",
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",
},
),
]

View file

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,8 +0,0 @@
from django.contrib import admin
from .models import Transaction, Invoice, Category, Snapshot
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"

51
nummi/main/forms.py Normal file
View file

@ -0,0 +1,51 @@
from django import forms
from django.forms.widgets import TextInput
from .utils import get_icons
class NummiFileInput(forms.ClearableFileInput):
template_name = "main/form/fileinput.html"
class NummiForm(forms.ModelForm):
template_name = "main/form/form_base.html"
meta_fieldsets = []
def __init__(self, *args, **kwargs):
kwargs.pop("user", None)
super().__init__(*args, **kwargs)
@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 DatalistInput(TextInput):
template_name = "main/forms/widgets/datalist.html"
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 IconInput(DatalistInput):
template_name = "main/forms/widgets/icon.html"
icon_list = get_icons()
def get_context(self, *args):
context = super().get_context(*args)
context["widget"]["options"] = self.icon_list
return context

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,153 +0,0 @@
# Generated by Django 4.0.4 on 2022-05-22 07:45
import datetime
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import re
import uuid
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,192 +1,46 @@
from datetime import date
import uuid
from django.conf import settings
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.forms import ModelForm
from django.core.validators import FileExtensionValidator
from django.utils.translation import gettext_lazy as _
class Category(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=64, default="New Category")
icon = models.CharField(max_length=64, default="folder")
class NummiQuerySet(models.QuerySet):
main_field = "name"
fields = dict()
def __str__(self):
return self.name
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:
ordering = ["name"]
class CategoryForm(ModelForm):
template_name = "main/form.html"
class Meta:
model = Category
fields = ["name", "icon"]
class Transaction(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=256, default="New Transaction")
description = models.TextField(null=True, blank=True)
value = models.DecimalField(max_digits=12, decimal_places=2, default=0)
date = models.DateField(default=date.today)
real_date = models.DateField(blank=True, null=True)
trader = models.CharField(max_length=128, blank=True, null=True)
payment = models.CharField(max_length=128, blank=True, null=True)
category = models.ForeignKey(
Category, on_delete=models.SET_NULL, blank=True, null=True
)
def __str__(self):
res = f"{self.date} {self.name}: {self.value}"
if self.category:
return f"{res} ({self.category})"
return res
class Meta:
ordering = ["-date"]
class TransactionForm(ModelForm):
template_name = "main/form.html"
class Meta:
model = Transaction
fields = [
"date",
"name",
"value",
"trader",
"category",
"real_date",
"payment",
"description",
]
class Invoice(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=256, default="New Invoice")
file = models.FileField(
upload_to="invoices/", validators=[FileExtensionValidator(["pdf"])]
)
transaction = models.ForeignKey(Transaction, on_delete=models.CASCADE)
def __str__(self):
return f"{self.name}: {self.transaction}"
def delete(self, *args, **kwargs):
self.file.delete()
super().delete(*args, **kwargs)
class InvoiceForm(ModelForm):
template_name = "main/form.html"
prefix = "invoice"
class Meta:
model = Invoice
fields = ["name", "file"]
class Snapshot(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
date = models.DateField(unique=True)
value = models.DecimalField(max_digits=12, decimal_places=2, default=0)
previous = models.OneToOneField(
"self", on_delete=models.SET_NULL, blank=True, null=True, editable=False
)
diff = models.DecimalField(
max_digits=12, decimal_places=2, editable=False, blank=True, null=True
)
def __str__(self):
if self.previous is None:
return f"Snapshot {self.date}: {self.value}"
return f"Snapshot {self.date}: {self.value}€ (Previous: {self.previous.date})"
def save(self, *args, only_super=False, **kwargs):
if not only_super:
_prev = (
self.__class__.objects.order_by("-date")
.exclude(id=self.id)
.filter(date__lt=self.date)
.first()
)
try:
_next = self.__class__.objects.exclude(id=self.id).get(previous=_prev)
except self.__class__.DoesNotExist:
pass
else:
try:
_prevnext = self.__class__.objects.exclude(id=self.id).get(
previous=self
)
except self.__class__.DoesNotExist:
pass
else:
_prevnext.previous = (
self.__class__.objects.order_by("-date")
.exclude(id=self.id)
.filter(date__lt=_prevnext.date)
.first()
)
_prevnext.save(only_super=True)
_next.previous = self
_next.save(only_super=True)
self.previous = _prev
if self.previous is None:
self.diff = None
else:
self.diff = self.value - self.previous.value
super().save(*args, **kwargs)
try:
_next = self.__class__.objects.get(previous=self)
except self.__class__.DoesNotExist:
pass
else:
_next.save(only_super=True)
def delete(self, *args, only_super=False, **kwargs):
if not only_super:
try:
_next = self.__class__.objects.get(previous=self)
except self.__class__.DoesNotExist:
super().delete(*args, **kwargs)
else:
_next.previous = self.previous
super().delete(*args, **kwargs)
_next.save(only_super=True)
else:
super().delete(*args, **kwargs)
@property
def sum(self):
if self.previous is None:
return None
trans = self.transactions.aggregate(sum=models.Sum("value"))
return trans["sum"] or 0
@property
def transactions(self):
if self.previous is None:
return Transaction.objects.none()
return Transaction.objects.filter(
date__lte=self.date, date__gt=self.previous.date
)
class Meta:
ordering = ["-date"]
class SnapshotForm(ModelForm):
template_name = "main/form.html"
class Meta:
model = Snapshot
fields = ["date", "value"]
abstract = True

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,74 +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;
}
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, 32rem);
@media (width < 1024px) {
grid-template-columns: 1fr;
}
form ul.errorlist {
grid-column: 1 / -1;
&.hidden {
display: none;
}
.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;
}
ul.errorlist {
color: var(--red);
font-weight: 550;
list-style-type: "! ";
list-style: none;
padding: 0;
margin: 0;
}
}
form input,
form select,
form textarea {
padding: 0 var(--gap);
}
.field {
display: grid;
grid-auto-rows: min-content;
align-items: center;
column-gap: 0.5rem;
form > label,
form > input[type="text"],
form > input[type="number"],
form > select {
ul.errorlist {
font-size: 0.8rem;
}
&: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;
line-height: inherit;
font-family: inherit;
}
form > textarea {
font-size: inherit;
font-family: inherit;
}
form > input[type="text"],
form > input[type="number"],
form > textarea {
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-radius: var(--radius);
background: var(--bg-01);
}
border: 1px solid transparent;
border-bottom: 1px solid var(--gray);
background: none;
z-index: 1;
padding: 0.5rem;
form > .buttons {
grid-column: 1 / -1;
&: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;
}
form > .buttons input,
form select,
form input[type="file"] {
font-size: inherit;
line-height: inherit;
font-family: inherit;
border: 0;
background: var(--bg-01);
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;
border-radius: var(--radius);
}
form > .buttons input {
margin-left: var(--gap);
}
form > .buttons input:hover {
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
color: inherit;
&:hover {
text-decoration: underline;
}
form > .buttons input[type="submit"] {
background: var(--green);
color: var(--text-inv);
}
form > .buttons input[type="reset"] {
}
}
input[type="submit"] {
background: var(--green-1);
border-color: var(--green);
}
input[type="reset"] {
background: var(--bg-1);
}
a.del {
color: var(--red);
}
form > .buttons input.del {
background: var(--red);
color: var(--text-inv);
border-color: var(--red);
border-style: dashed;
}
}
}

View file

@ -1,22 +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 {
margin-right: var(--gap);
margin-bottom: var(--gap);
display: inline-block;
}
#categories > a > i {
margin-right: .5rem;
}

View file

@ -1,62 +1,572 @@
@import "https://rsms.me/inter/inter.css";
@import "https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css";
*, *::before, *::after {
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
--theme: #006633;
--theme-0: #00f279;
--theme-1: #00b359;
--theme-2: #007339;
--theme-3: var(--theme);
--theme-4: #004d26;
--theme: #66cc66;
--theme-0: #338033;
--theme-1: #99ff99;
--theme-2: var(--theme);
--theme-3: #802653;
--theme-4: #cc6699;
--gray: #dedede;
--text-theme: var(--text-inv);
--text-theme: var(--text);
--bg: #ffffff;
--text: #000000de;
--bg-inv: var(--theme-4);
--bg-inv: var(--theme-1);
--text-inv: #ffffffde;
--bg-01: #dedede;
--bg-1: #f0f0f0;
--text-link: #0a35ccde;
--text-green: #296629;
--text-link: var(--text-green);
--gap: 1rem;
--red: #bf1500;
--green: var(--theme-2);
--red: var(--theme-3);
--green: var(--theme-0);
--red-1: #f21b00;
--green-1: var(--theme-1);
--red-1: var(--theme-4);
--green-1: var(--theme);
--radius: 2px;
--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;
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 {
display: inline-block;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
.red {color: var(--red)}
.green {color: var(--green)}
.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,42 +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 {
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.home {
font-variant: small-caps;
}
nav > a.logout {
float: right;
}
nav > a img {
height: var(--nav-lh);
vertical-align: top;
margin-right: .5rem;
}

View file

@ -1,3 +0,0 @@
.pagination .current {
font-feature-settings: "tnum", "ss01";
}

View file

@ -0,0 +1,123 @@
table.full-width col.bar {
width: auto;
}
.plot {
overflow-x: auto;
table {
min-width: 40rem;
}
td.bar {
position: relative;
padding: 0;
overflow: hidden;
div {
position: absolute;
height: 0.5rem;
top: 0;
&:not(.tot) {
width: 0;
box-sizing: border-box;
z-index: 1;
display: inline-block;
}
&.tot {
z-index: 10;
height: 0.5rem;
span {
position: absolute;
display: inline-block;
white-space: nowrap;
margin: 0 var(--gap);
font-weight: 650;
top: 0.5rem;
line-height: 1.5rem;
height: 1.5rem;
font-feature-settings: var(--num);
}
}
}
&.p div {
left: 0;
border-radius: 0 var(--radius) var(--radius) 0;
background: var(--green-1);
&.tot {
background: var(--green);
span {
left: 0;
}
}
}
&.m div {
right: 0;
border-radius: var(--radius) 0 0 var(--radius);
background: var(--red-1);
&.tot {
background: var(--red);
span {
right: 0;
}
}
}
}
&.history tbody tr {
background: initial;
}
tbody tr {
&.empty {
height: 0.5rem;
}
&.even {
background: #eeeeff;
}
}
}
.calendar {
overflow-x: auto;
margin-bottom: var(--gap);
font-feature-settings: var(--num);
table {
tbody tr {
background: initial;
&:not(:last-child) {
border-bottom: none;
}
&:not(:first-child) {
border-top: none;
}
td.month {
text-align: center;
background-color: color-mix(
in hsl,
var(--td-bg, var(--bg)) var(--opacity),
var(--bg)
);
padding: 0;
width: 4rem;
height: 4rem;
&.p {
--td-bg: var(--green);
}
&.m {
--td-bg: var(--red);
}
}
td.total {
text-align: right;
font-weight: 650;
font-feature-settings: var(--num);
}
}
}
}

View file

@ -1,46 +1,59 @@
.table {
display: grid;
margin: 2em 0;
border-radius: var(--radius);
overflow: hidden;
border-bottom: .5em solid var(--bg-inv);
overflow-x: auto;
width: 100%;
}
.table.col2 {grid-template-columns: repeat(2, auto)}
.table.col3 {grid-template-columns: repeat(3, auto)}
.table.col4 {grid-template-columns: repeat(4, auto)}
.table.col5 {grid-template-columns: repeat(5, auto)}
.table.col6 {grid-template-columns: repeat(6, auto)}
table {
border-collapse: collapse;
.table > div {
display: contents;
}
&.full-width {
width: 100%;
.table > div > * {
padding: 1em;
white-space: nowrap;
}
.table > div.g> * {background: var(--bg-01)}
.table > div.w> * {background: var(--bg)}
col {
width: 8rem;
}
}
col.icon {
width: 1ch;
}
thead tr:not(.new) {
background: var(--bg-1);
}
tr {
border: 1px solid var(--gray);
height: 2rem;
line-height: 2rem;
.table > div.header > * {
background: var(--bg-inv);
color: var(--text-inv);
}
.table > div > .center {
tbody &:where(:nth-of-type(even)) {
background: #eeeeff;
}
&.more,
&.new {
text-align: center;
}
.table > div > .right {
text-align: right;
}
.table > div > .num {
font-feature-settings: var(--num);
}
.table > div > span.value {
filter: brightness(95%);
}
.table > div > span.text {
overflow: hidden;
border-style: dashed;
}
}
td,
th {
padding: 0 var(--gap);
position: relative;
white-space: nowrap;
text-overflow: ellipsis;
&.empty {
text-align: center;
opacity: 0.8;
font-weight: 300;
}
}
.l {
text-align: left;
}
.r,
.value {
text-align: right;
}
.c,
.date {
text-align: center;
}
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,181 @@
const beforeUnloadHandler = (event) => {
event.preventDefault();
};
const forms = document.querySelectorAll("form");
for (let form of forms) {
let inputs = form.querySelectorAll("input");
for (input of inputs) {
input.addEventListener("input", (event) => {
window.addEventListener("beforeunload", beforeUnloadHandler);
});
}
form.addEventListener("submit", (event) => {
window.removeEventListener("beforeunload", beforeUnloadHandler);
});
form.addEventListener("reset", (event) => {
window.removeEventListener("beforeunload", beforeUnloadHandler);
});
let categorySelect = form.querySelector(".category-select");
if (categorySelect) {
let input = categorySelect.querySelector("select");
let icon = categorySelect.querySelector("span");
let icons = JSON.parse(input.dataset.icons);
function setIcon(event) {
icon.className = `ri-${icons[input.value] || "folder"}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let accountSelect = form.querySelector(".account-select");
if (accountSelect) {
let input = accountSelect.querySelector("select");
let icon = accountSelect.querySelector("span");
let icons = JSON.parse(input.dataset.icons);
function setIcon(event) {
icon.className = `ri-${icons[input.value] || "bank"}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let iconSelect = form.querySelector(".icon-select");
if (iconSelect) {
let input = iconSelect.querySelector("input");
let icon = iconSelect.querySelector("span");
let icons = Array.from(iconSelect.querySelector("datalist").options).map(
(opt) => opt.value,
);
function setIcon(event) {
icon.className = `ri-${
icons.includes(input.value) ? input.value : "square"
}-line`;
}
setIcon();
input.addEventListener("input", setIcon);
form.addEventListener("reset", (event) => {
setTimeout(setIcon, 0);
});
}
let ac_json = form.querySelector("#autocomplete");
if (ac_json) {
let autocomplete_data = JSON.parse(ac_json.textContent);
let fields = form.querySelectorAll("[name]");
function clearMod(event) {
event.target.classList.add("mod");
event.target.removeEventListener("input", clearMod);
}
function setMod() {
for (let f of fields) {
f.addEventListener("input", clearMod);
}
}
setMod();
form.addEventListener("reset", (event) => {
for (let f of fields) {
f.classList.remove("mod", "autocompleted");
}
setMod();
});
let old_values = [];
let field = form.querySelector(`[name="${autocomplete_data.field}"]`);
field.addEventListener("change", (event) => {
if (field.value in autocomplete_data.data) {
old_values = [];
for (let comp of autocomplete_data.data[field.value]) {
let f = form.querySelector(`[name="${comp.field}"]`);
if (!f.classList.contains("mod")) {
old_values.push({ field: f, value: f.value });
f.value = comp.value;
f.dispatchEvent(new Event("input"));
f.animate([{ borderColor: "var(--green)" }, {}], 750);
f.classList.remove("mod");
f.classList.add("autocompleted");
f.addEventListener("input", clearMod);
function ccAutocomplete(event) {
document.removeEventListener("keyup", cancelAutocomplete);
for (comp of old_values) {
comp.field.removeEventListener("input", ccAutocomplete);
}
}
f.addEventListener("input", ccAutocomplete);
}
}
function cancelAutocomplete(event) {
if (event.code == "Escape") {
for (comp of old_values) {
let f = comp.field;
f.value = comp.value;
f.dispatchEvent(new Event("input"));
f.animate([{ borderColor: "var(--red)" }, {}], 750);
f.classList.remove("mod");
f.classList.remove("autocompleted");
f.addEventListener("input", clearMod);
}
document.removeEventListener("keyup", cancelAutocomplete);
}
}
document.addEventListener("keyup", cancelAutocomplete);
form.addEventListener("reset", (event) => {
document.removeEventListener("keyup", cancelAutocomplete);
});
}
});
}
}
let accounts = document.querySelector("dl.accounts");
if (accounts) {
let toggle = accounts.querySelector("input#show-archived-accounts");
if (toggle) {
accounts.classList.toggle("show-archive", toggle.checked);
toggle.addEventListener("change", (event) => {
accounts.classList.toggle("show-archive", toggle.checked);
});
}
}
const filterForm = document.querySelector("form.filter");
if (filterForm) {
const accountSelect = filterForm.querySelector("[name='account']");
const statementSelect = filterForm.querySelector("[name='statement']");
if (!statementSelect.disabled) {
accountSelect.addEventListener("input", (event) => {
statementSelect.value = "";
statementSelect.disabled = true;
});
filterForm.addEventListener("reset", (event) => {
statementSelect.disabled = false;
});
}
let disableStatement = false;
filterForm.addEventListener("submit", (event) => {
for (element of filterForm.elements) {
if (
element.value == "" ||
(disableStatement && element.name == "statement")
) {
if (element.name == "account") {
disableStatement = true;
}
element.disabled = true;
}
}
});
}

View file

@ -1,22 +0,0 @@
<svg version="1.1"
width="100"
height="100"
xmlns="http://www.w3.org/2000/svg">
<defs>
<rect id="basemask" x="-10" y="-10" width="110" height="110" />
<rect id="line" x="45" y="-10" width="10" height="120" />
</defs>
<mask id="mask">
<use href="#basemask" fill="white" />
<use href="#line" fill="black" />
<use href="#line" transform="translate(20)" fill="black" />
</mask>
<polygon points="25,0 75,0 100,25 100,75 75,100 25,100 0,75 0,25"
fill="white"
mask="url(#mask)"
transform-origin="center center" transform="rotate(10) scale(.8)" />
</svg>

Before

Width:  |  Height:  |  Size: 576 B

View file

@ -4,8 +4,12 @@
xmlns="http://www.w3.org/2000/svg">
<defs>
<rect id="basemask" x="-10" y="-10" width="110" height="110" />
<rect id="basemask" x="5" y="-5" width="90" height="110" />
<rect id="line" x="45" y="-10" width="10" height="120" />
<linearGradient id="grad" gradientTransform="rotate(90)">
<stop offset="5%" stop-color="#66cc66" />
<stop offset="95%" stop-color="#cc6699" />
</linearGradient>
</defs>
<mask id="mask">
@ -14,9 +18,9 @@
<use href="#line" transform="translate(20)" fill="black" />
</mask>
<polygon points="25,0 75,0 100,25 100,75 75,100 25,100 0,75 0,25"
fill="#006633"
mask="url(#mask)"
transform-origin="center center" transform="rotate(10) scale(.8)" />
<circle cx="50" cy="50" r="50"
fill="url('#grad')"
mask="url('#mask')"
transform-origin="center center" transform="rotate(10)" />
</svg>

Before

Width:  |  Height:  |  Size: 578 B

After

Width:  |  Height:  |  Size: 707 B

Before After
Before After

View file

@ -1,50 +1,96 @@
{% load static %}
{% load i18n %}
{% load main_extras %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>{% block title %}Nummi{% endblock %}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>
{% block title %}Nummi{% endblock %}
</title>
{% block link %}
<link rel="icon" href="{% static 'main/svg/logo.svg' %}" type="image/svg+xml" />
<link rel="stylesheet" href="{% static 'main/css/main.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'main/css/nav.css' %}" type="text/css" />
<link rel="icon" href="{% static "main/svg/logo.svg" %}" type="image/svg+xml">
{% css "main/css/main.css" %}
{% js "main/js/base.js" %}
{% endblock %}
</head>
<body>
{% block nav %}
{% spaceless %}
<nav>
<a href="{% url 'index' %}"
class="home{% if request.resolver_match.url_name == 'index' %} cur{% endif %}">
<img src="{% static 'main/svg/logo-white.svg' %}" alt="" />Nummi
<h1>
<img src="{% static "main/svg/logo.svg" %}" alt="">
Nummi
</h1>
<ul>
<li>
<a class="skip-link" href="#main">{% translate "Skip to main content" %}</a>
</li>
{% if user.is_authenticated %}
<li>
<a href="{% url "index" %}"
class="home{% if request.resolver_match.url_name == "index" %} cur{% endif %}"
accesskey="h">
<span>{% translate "Home" %}</span>
{{ "home"|remix }}
</a>
<a href="{% url 'transactions' %}"
class="{% if request.resolver_match.url_name == 'transactions' %}cur{% endif %}">
Transactions
</li>
<li>
<a href="{% url "statements" %}"
class="{% if request.resolver_match.url_name == "statements" %}cur{% endif %}">
<span>{% translate "Statements" %}</span>
{{ "file"|remix }}
</a>
<a href="{% url 'transaction' %}"
class="{% if request.resolver_match.url_name == 'transaction' %}cur{% endif %}">
Add transaction
</li>
<li>
<a href="{% url "transactions" %}"
class="{% if request.resolver_match.url_name == "transactions" %}cur{% endif %}">
<span>{% translate "Transactions" %}</span>
{{ "receipt"|remix }}
</a>
<a href="{% url 'category' %}"
class="{% if request.resolver_match.url_name == 'category' %}cur{% endif %}">
Add category
</li>
<li>
<a href="{% url "search" %}"
class="{% if request.resolver_match.url_name == "search" %}cur{% endif %}"
accesskey="r">
<span>{% translate "Search" %}</span>
{{ "search"|remix }}
</a>
<a href="{% url 'snapshot' %}"
class="{% if request.resolver_match.url_name == 'snapshot' %}cur{% endif %}">
Add snapshot
</li>
<li>
{% blocktranslate %}Logged in as <strong>{{ user }}</strong>{% endblocktranslate %}
</li>
<li>
<a href="{% url "logout" %}" accesskey="l">
<span>{% translate "Log out" %}</span>
{{ "close-circle"|remix }}
</a>
<a href="{% url 'logout' %}"
class="logout">
Log Out
</li>
{% else %}
<li>
<a {% if request.resolver_match.url_name == "login" %}class="cur"{% endif %}
href="{% url "login" %}">
<span>{% translate "Log in" %}</span>
{{ "user"|remix }}
</a>
</li>
{% endif %}
</ul>
</nav>
{% endspaceless %}
{% endblock %}
{% block body %}
{% endblock %}
<main id="main">
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li class="msg-level-{{ message.level }}">
{{ message.level|messageicon }}{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
{% block body %}{% endblock %}
</main>
</body>
</html>

View file

@ -1,24 +0,0 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% block link %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'main/css/form.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'main/css/table.css' %}" type="text/css" />
{% endblock %}
{% block body %}
<h1>{{ category.icon }} {{ category }}</h1>
<form action="{% url 'category' category.id %}" method="post">
{% csrf_token %}
{{ form }}
{% form_buttons category %}
</form>
{% if transactions %}
<h2>Transactions</h2>
{% transaction_table transactions %}
{% endif %}
{% endblock %}

View file

@ -0,0 +1,25 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load i18n %}
{% block link %}
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% endblock %}
{% block body %}
{% spaceless %}
<form method="post">
{% csrf_token %}
<p>
{% blocktranslate %}Are you sure you want do delete <strong>{{ object }}</strong> ?{% endblocktranslate %}
</p>
{% block additionalinfo %}{% endblock %}
{{ form }}
<div class="buttons">
<a href="{{ object.get_absolute_url }}">{% translate "Cancel" %}</a>
<input class="del" type="submit" value="{% translate "Confirm" %}" />
</div>
</form>
{% endspaceless %}
{% endblock %}

View file

@ -1,5 +0,0 @@
{% for field in form %}
{{ field.errors }}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endfor %}

View file

@ -0,0 +1,40 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load i18n %}
{% block title %}
{% if form.instance|adding %}
{% block title_new %}{% endblock %}
{% else %}
{{ form.instance }}
{% endif %}
Nummi
{% endblock %}
{% block link %}
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock %}
{% block body %}
{% with instance=form.instance %}
{% if instance|adding %}
<h2 class="new">
{% block h2_new %}{% endblock %}
</h2>
{% else %}
<h2>
{% block h2 %}{{ instance }}{% endblock %}
</h2>
{% endif %}
{% block pre %}{% endblock %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if instance|adding %}
<input hidden name="next" value="{{ request.path }}">
{% endif %}
{{ form }}
</form>
{% block tables %}{% endblock %}
{% endwith %}
{% endblock %}

View file

@ -0,0 +1,21 @@
{% load i18n %}
{% load main_extras %}
<div class="file-input">
{% if widget.is_initial %}
<div class="current">
<a href="{{ widget.value.url }}" target="_blank">{{ "file"|remix }}{% translate "File" %} [{{ widget.value|extension }}]</a>
{% if not widget.required %}
<span class="field checkbox">
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>
<input type="checkbox"
name="{{ widget.checkbox_name }}"
id="{{ widget.checkbox_id }}"
{% if widget.attrs.disabled %}disabled{% endif %}>
</span>
{% endif %}
</div>
{% endif %}
<input type="{{ widget.type }}"
name="{{ widget.name }}"
{% include "django/forms/widgets/attrs.html" %}>
</div>

View file

@ -0,0 +1,56 @@
{% load i18n %}
{% load main_extras %}
{% block fields %}
{% if form.autocomplete %}{{ form.autocomplete|json_script:"autocomplete" }}{% endif %}
{% if form.non_field_errors %}
<ul class="errorlist">
{% for error in form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
</ul>
{% endif %}
{% if form.fieldsets %}
{% for group in form.fieldsets %}
<div class="column">
{% for fieldset in group %}
<div class="fieldset">
{% for field in fieldset %}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.errors %}
<ul class="errorlist">
{% for error in field.errors %}
<li class="wi">{{ "error-warning"|remix }}{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% endfor %}
{% else %}
<div class="column">
{% for field in form %}
{% if field.errors %}<p class="error">{{ field.errors }}</p>{% endif %}
<div class="field">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
</div>
{% endfor %}
</div>
{% endif %}
<div class="buttons">
{% block buttons %}
{% if form.instance|adding %}
<input type="submit" value="{% translate "Create" %}">
{% else %}
<input type="submit" value="{% translate "Save" %}">
{% endif %}
<input type="reset" value="{% translate "Reset" %}">
{% if not form.instance|adding %}
<a class="del" href="{{ form.instance.get_delete_url }}">{% translate "Delete" %}</a>
{% endif %}
{% endblock buttons %}
</div>
{% endblock fields %}

View file

@ -0,0 +1,7 @@
{% extends "main/form/form_base.html" %}
{% load i18n %}
{% block buttons %}
<input hidden value="{{ next }}" name="next">
<input type="submit" value="{% translate "Log in" %}">
<input type="reset">
{% endblock buttons %}

View file

@ -0,0 +1,4 @@
{% include "django/forms/widgets/text.html" %}
<datalist id="{{ widget.attrs.list }}">
{% for option in widget.options %}<option value="{{ option }}"></option>{% endfor %}
</datalist>

View file

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

View file

@ -1,67 +1,37 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load main_extras account_extras history_extras statement_extras %}
{% load i18n %}
{% block link %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'main/css/index.css' %}" type="text/css" />
<link rel="stylesheet" href="{% static 'main/css/table.css' %}" type="text/css" />
{% endblock %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
<h1><img src="{% static 'main/svg/logo.svg' %}" alt="" />Nummi</h1>
{% if transactions %}
<h2>Transactions</h2>
{% transaction_table transactions %}
{% endif %}
{% if snapshots %}
<h2>Relevés</h2>
<div id="snapshots" class="table col5">
<div class="header">
<strong class="date center">Date</strong>
<strong class="value center">Valeur</strong>
<strong class="diff center">Différence</strong>
<strong class="diff center">Transactions</strong>
<strong class="diff center">Valide</strong>
</div>
{% for snap in snapshots %}
<div class="snapshot {% cycle 'w' 'g' %}">
<span class="date num center">
<a href="{% url 'snapshot' snap.id %}">{{ snap.date|date:"Y-m-d" }}</a>
</span>
<span class="value num right">{{ snap.value|value }}</span>
<span class="diff num right">
{{ snap.diff|pmvalue }}
</span>
{% with sum=snap.sum %}
<span class="sum num right">
{{ sum|pmvalue }}
</span>
<span class="valid center">
{% if snap.previous is not None %}
{% if sum == snap.diff %}
{% else %}
{% endif %}
{% endif %}
</span>
{% endwith %}
<div class="split">
<section>
<h2>{% translate "Accounts" %}</h2>
{% account_table user.account_set.all index=True %}
</section>
<section>
<h2>{% translate "Statements" %}</h2>
{% url "statements" as s_url %}
{% statement_table statements statements_url=s_url %}
</section>
</div>
<section>
<h2>{% translate "Categories" %}</h2>
{% spaceless %}
<p>
{% for cat in user.category_set.all %}
<a class="category" href="{{ cat.get_absolute_url }}">{{ cat.icon|remix }}{{ cat }}</a>
{% endfor %}
</div>
{% endif %}
{% if categories %}
<h2>Catégories</h2>
<div id="categories">
{% for cat in categories %}
{{ cat.icon }}
<a href="{% url 'category' cat.id %}">{{ cat }}</a>
{% endfor %}
</div>
{% endif %}
{% endblock %}
<a class="category add" href="{% url "new_category" %}">{{ "add"|remix }}{% translate "Create category" %}</a>
</p>
{% endspaceless %}
</section>
<section>
<h2>{% translate "History" %}</h2>
{% history_plot user.transaction_set.all %}
</section>
{% endblock body %}

View file

@ -0,0 +1,47 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load i18n %}
{% block title %}
{% block name %}{% endblock %}
{% if account %} {{ account }}{% endif %}
{% if category %} {{ category }}{% endif %}
{% if search %}
{% translate "Search" %}
{% endif %}
Nummi
{% endblock %}
{% block link %}
{{ block.super }}
{% css "main/css/table.css" %}
{% endblock %}
{% block body %}
<h2>
{% block h2 %}{% endblock %}
</h2>
{% if account or category or search %}
<div class="backlinks">
{% block backlinks %}
{% if account %}
<p>
<a class="big-link" href="{{ account.get_absolute_url }}">{{ account.icon|remix }}{{ account }}</a>
</p>
{% endif %}
{% if category %}
<p>
<a class="big-link" href="{{ category.get_absolute_url }}">{{ category.icon|remix }}{{ category }}</a>
</p>
{% endif %}
{% if search %}
<p>
<a href="{% url "search" %}">{% translate "Search" %}</a>
</p>
{% endif %}
{% endblock %}
</div>
{% endif %}
{% include "main/pagination.html" %}
{% block table %}{% endblock %}
{% include "main/pagination.html" %}
{% block body_more %}{% endblock %}
{% endblock %}

View file

@ -1,27 +1,16 @@
{% extends "main/base.html" %}
{% load main_extras %}
{% load static %}
{% load i18n %}
{% block link %}
{{ block.super }}
<link rel="stylesheet" href="{% static 'main/css/form.css' %}" type="text/css" />
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/form.css" %}
{% endblock %}
{% block body %}
<h1>Login</h1>
<form action="{% url 'login' %}" method="post">
<h2>{% translate "Log in" %}</h2>
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
{% for field in form %}
{{ field.errors }}
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% endfor %}
<div class="buttons">
<input hidden value="{{ next }}" name="next" />
<input type="reset" />
<input type="submit" value="Log In" />
</div>
</form>
{% include "main/form/login.html" %}
</form>
{% endblock %}

View file

@ -0,0 +1,4 @@
{% load i18n main_extras transaction_extras %}
{% if page_obj %}
<p class="pagination">{% pagination_links page_obj %}</p>
{% endif %}

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