Compare commits

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

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
211 changed files with 6632 additions and 20047 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
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()
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),
)
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
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank")
)
class Meta:
ordering = ["-date"]
class SnapshotForm(ModelForm):
template_name = "main/form.html"
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:
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;
}
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, 32rem);
@media (width < 1024px) {
grid-template-columns: 1fr;
}
form ul.errorlist {
grid-column: 1 / -1;
color: var(--red);
font-weight: 550;
list-style-type: "! ";
margin: 0;
}
&.hidden {
display: none;
}
form input,
form select,
form textarea {
padding: 0 var(--gap);
}
.column {
display: grid;
gap: 0.5rem;
border: 1px solid var(--gray);
padding: var(--gap);
block-size: min-content;
}
.fieldset {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: inherit;
padding: 0;
margin: 0;
border: none;
}
form > label,
form > input[type="text"],
form > input[type="number"],
form > select {
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 {
border: none;
border-radius: var(--radius);
background: var(--bg-01);
}
ul.errorlist {
color: var(--red);
font-weight: 550;
list-style: none;
padding: 0;
margin: 0;
}
form > .buttons {
grid-column: 1 / -1;
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);
cursor: pointer;
border-radius: var(--radius);
}
form > .buttons input {
margin-left: var(--gap);
}
form > .buttons input:hover {
text-decoration: underline;
}
form > .buttons input[type="submit"] {
background: var(--green);
color: var(--text-inv);
}
form > .buttons input[type="reset"] {
color: var(--red);
}
form > .buttons input.del {
background: var(--red);
color: var(--text-inv);
.field {
display: grid;
grid-auto-rows: min-content;
align-items: center;
column-gap: 0.5rem;
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;
grid-row: 1;
grid-column: 2;
padding: 0.5rem;
line-height: initial;
}
> input {
grid-row: 1;
grid-column: 1;
margin: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
> label {
font-size: 0.8rem;
line-height: 0.8rem;
z-index: 10;
}
> a {
padding: 0.5rem;
}
input,
select,
textarea {
font: inherit;
line-height: initial;
border: none;
border: 1px solid transparent;
border-bottom: 1px solid var(--gray);
background: none;
z-index: 1;
padding: 0.5rem;
&:has(~ ul.errorlist) {
border-color: var(--red);
}
&.autocompleted:not(.mod) {
border-bottom-color: var(--green);
}
&:not([type="checkbox"]) {
width: 100%;
margin: 0;
}
&[name*="value"] {
text-align: right;
font-feature-settings: var(--num);
}
&[name*="date"] {
font-feature-settings: var(--num);
}
&:focus {
outline: none;
background: var(--bg-1);
}
}
> .file-input {
display: grid;
> .current {
display: grid;
grid-template-columns: 1fr;
grid-auto-columns: max-content;
grid-auto-flow: column;
a {
padding: 0.5rem;
}
}
input[type="file"] {
&::file-selector-button {
display: none;
}
}
}
> .ico-input {
display: grid;
grid-template-columns: min-content 1fr;
column-gap: 0.5rem;
align-items: center;
span[class|="ri"] {
padding: 0.5rem;
}
&:has(> :focus) {
background: var(--bg-1);
}
}
}
.buttons {
grid-column: 1 / -1;
line-height: 2rem;
input,
a {
font: inherit;
cursor: pointer;
padding: 0 var(--gap);
border: var(--gray) 1px solid;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
color: inherit;
&:hover {
text-decoration: underline;
}
}
input[type="submit"] {
background: var(--green-1);
border-color: var(--green);
}
input[type="reset"] {
background: var(--bg-1);
}
a.del {
color: var(--red);
border-color: var(--red);
border-style: dashed;
}
}
}

View file

@ -1,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 {
box-sizing: border-box;
*,
*::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: #ffffff;
--text: #000000de;
--bg-inv: var(--theme-4);
--text-inv: #ffffffde;
--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;
--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;
font-family: "Inter var experimental", "Inter var", "Inter", sans-serif;
margin: 0;
background: var(--bg);
color: var(--text);
background: var(--bg);
color: var(--text);
display: grid;
grid-template-columns: max-content 1fr;
font-feature-settings: var(--default-ffs);
}
h1 {
margin: 0;
p {
line-height: 2em;
}
a {
color: var(--text-link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
color: var(--text-link);
text-decoration: none;
display: inline-block;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
.red {color: var(--red)}
.green {color: var(--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);
}
tbody &:where(:nth-of-type(even)) {
background: #eeeeff;
}
&.more,
&.new {
text-align: center;
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;
}
}
.table > div > .center {
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;
text-overflow: ellipsis;
.l {
text-align: left;
}
.r,
.value {
text-align: right;
}
.c,
.date {
text-align: center;
}
}

View file

@ -1,165 +0,0 @@
Fonticons, Inc. (https://fontawesome.com)
--------------------------------------------------------------------------------
Font Awesome Free License
Font Awesome Free is free, open source, and GPL friendly. You can use it for
commercial projects, open source projects, or really almost whatever you want.
Full Font Awesome Free license: https://fontawesome.com/license/free.
--------------------------------------------------------------------------------
# Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/)
The Font Awesome Free download is licensed under a Creative Commons
Attribution 4.0 International License and applies to all icons packaged
as SVG and JS file types.
--------------------------------------------------------------------------------
# Fonts: SIL OFL 1.1 License
In the Font Awesome Free download, the SIL OFL license applies to all icons
packaged as web and desktop font files.
Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com)
with Reserved Font Name: "Font Awesome".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE
Version 1.1 - 26 February 2007
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting — in part or in whole — any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
--------------------------------------------------------------------------------
# Code: MIT License (https://opensource.org/licenses/MIT)
In the Font Awesome Free download, the MIT license applies to all non-font and
non-icon files.
Copyright 2022 Fonticons, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in the
Software without restriction, including without limitation the rights to use, copy,
modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
# Attribution
Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font
Awesome Free files already contain embedded comments with sufficient
attribution, so you shouldn't need to do anything additional when using these
files normally.
We've kept attribution comments terse, so we ask that you do not actively work
to remove them from files, especially code. They're a great way for folks to
learn about Font Awesome.
--------------------------------------------------------------------------------
# Brand Icons
All brand icons are trademarks of their respective owners. The use of these
trademarks does not indicate endorsement of the trademark holder by Font
Awesome, nor vice versa. **Please do not use brand logos for any purpose except
to represent the company, product, or service to which they refer.**

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,19 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-font-regular: normal 400 1em/1 "Font Awesome 6 Free"; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 400;
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }
.far,
.fa-regular {
font-family: 'Font Awesome 6 Free';
font-weight: 400; }

View file

@ -1,6 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:host,:root{--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-family:"Font Awesome 6 Free";font-weight:400}

View file

@ -1,19 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 "Font Awesome 6 Free"; }
@font-face {
font-family: 'Font Awesome 6 Free';
font-style: normal;
font-weight: 900;
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
.fas,
.fa-solid {
font-family: 'Font Awesome 6 Free';
font-weight: 900; }

View file

@ -1,6 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-family:"Font Awesome 6 Free";font-weight:900}

View file

@ -1,634 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
:root, :host {
--fa-font-solid: normal 900 1em/1 "Font Awesome 6 Solid";
--fa-font-regular: normal 400 1em/1 "Font Awesome 6 Regular";
--fa-font-light: normal 300 1em/1 "Font Awesome 6 Light";
--fa-font-thin: normal 100 1em/1 "Font Awesome 6 Thin";
--fa-font-duotone: normal 900 1em/1 "Font Awesome 6 Duotone";
--fa-font-brands: normal 400 1em/1 "Font Awesome 6 Brands"; }
svg:not(:root).svg-inline--fa, svg:not(:host).svg-inline--fa {
overflow: visible;
box-sizing: content-box; }
.svg-inline--fa {
display: var(--fa-display, inline-block);
height: 1em;
overflow: visible;
vertical-align: -.125em; }
.svg-inline--fa.fa-2xs {
vertical-align: 0.1em; }
.svg-inline--fa.fa-xs {
vertical-align: 0em; }
.svg-inline--fa.fa-sm {
vertical-align: -0.07143em; }
.svg-inline--fa.fa-lg {
vertical-align: -0.2em; }
.svg-inline--fa.fa-xl {
vertical-align: -0.25em; }
.svg-inline--fa.fa-2xl {
vertical-align: -0.3125em; }
.svg-inline--fa.fa-pull-left {
margin-right: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-pull-right {
margin-left: var(--fa-pull-margin, 0.3em);
width: auto; }
.svg-inline--fa.fa-li {
width: var(--fa-li-width, 2em);
top: 0.25em; }
.svg-inline--fa.fa-fw {
width: var(--fa-fw-width, 1.25em); }
.fa-layers svg.svg-inline--fa {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0; }
.fa-layers-text, .fa-layers-counter {
display: inline-block;
position: absolute;
text-align: center; }
.fa-layers {
display: inline-block;
height: 1em;
position: relative;
text-align: center;
vertical-align: -.125em;
width: 1em; }
.fa-layers svg.svg-inline--fa {
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-text {
left: 50%;
top: 50%;
-webkit-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
-webkit-transform-origin: center center;
transform-origin: center center; }
.fa-layers-counter {
background-color: var(--fa-counter-background-color, #ff253a);
border-radius: var(--fa-counter-border-radius, 1em);
box-sizing: border-box;
color: var(--fa-inverse, #fff);
line-height: var(--fa-counter-line-height, 1);
max-width: var(--fa-counter-max-width, 5em);
min-width: var(--fa-counter-min-width, 1.5em);
overflow: hidden;
padding: var(--fa-counter-padding, 0.25em 0.5em);
right: var(--fa-right, 0);
text-overflow: ellipsis;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-counter-scale, 0.25));
transform: scale(var(--fa-counter-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-bottom-right {
bottom: var(--fa-bottom, 0);
right: var(--fa-right, 0);
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom right;
transform-origin: bottom right; }
.fa-layers-bottom-left {
bottom: var(--fa-bottom, 0);
left: var(--fa-left, 0);
right: auto;
top: auto;
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: bottom left;
transform-origin: bottom left; }
.fa-layers-top-right {
top: var(--fa-top, 0);
right: var(--fa-right, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top right;
transform-origin: top right; }
.fa-layers-top-left {
left: var(--fa-left, 0);
right: auto;
top: var(--fa-top, 0);
-webkit-transform: scale(var(--fa-layers-scale, 0.25));
transform: scale(var(--fa-layers-scale, 0.25));
-webkit-transform-origin: top left;
transform-origin: top left; }
.fa-1x {
font-size: 1em; }
.fa-2x {
font-size: 2em; }
.fa-3x {
font-size: 3em; }
.fa-4x {
font-size: 4em; }
.fa-5x {
font-size: 5em; }
.fa-6x {
font-size: 6em; }
.fa-7x {
font-size: 7em; }
.fa-8x {
font-size: 8em; }
.fa-9x {
font-size: 9em; }
.fa-10x {
font-size: 10em; }
.fa-2xs {
font-size: 0.625em;
line-height: 0.1em;
vertical-align: 0.225em; }
.fa-xs {
font-size: 0.75em;
line-height: 0.08333em;
vertical-align: 0.125em; }
.fa-sm {
font-size: 0.875em;
line-height: 0.07143em;
vertical-align: 0.05357em; }
.fa-lg {
font-size: 1.25em;
line-height: 0.05em;
vertical-align: -0.075em; }
.fa-xl {
font-size: 1.5em;
line-height: 0.04167em;
vertical-align: -0.125em; }
.fa-2xl {
font-size: 2em;
line-height: 0.03125em;
vertical-align: -0.1875em; }
.fa-fw {
text-align: center;
width: 1.25em; }
.fa-ul {
list-style-type: none;
margin-left: var(--fa-li-margin, 2.5em);
padding-left: 0; }
.fa-ul > li {
position: relative; }
.fa-li {
left: calc(var(--fa-li-width, 2em) * -1);
position: absolute;
text-align: center;
width: var(--fa-li-width, 2em);
line-height: inherit; }
.fa-border {
border-color: var(--fa-border-color, #eee);
border-radius: var(--fa-border-radius, 0.1em);
border-style: var(--fa-border-style, solid);
border-width: var(--fa-border-width, 0.08em);
padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); }
.fa-pull-left {
float: left;
margin-right: var(--fa-pull-margin, 0.3em); }
.fa-pull-right {
float: right;
margin-left: var(--fa-pull-margin, 0.3em); }
.fa-beat {
-webkit-animation-name: fa-beat;
animation-name: fa-beat;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-bounce {
-webkit-animation-name: fa-bounce;
animation-name: fa-bounce;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.28, 0.84, 0.42, 1)); }
.fa-fade {
-webkit-animation-name: fa-fade;
animation-name: fa-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-beat-fade {
-webkit-animation-name: fa-beat-fade;
animation-name: fa-beat-fade;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1));
animation-timing-function: var(--fa-animation-timing, cubic-bezier(0.4, 0, 0.6, 1)); }
.fa-flip {
-webkit-animation-name: fa-flip;
animation-name: fa-flip;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, ease-in-out);
animation-timing-function: var(--fa-animation-timing, ease-in-out); }
.fa-shake {
-webkit-animation-name: fa-shake;
animation-name: fa-shake;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-delay: var(--fa-animation-delay, 0);
animation-delay: var(--fa-animation-delay, 0);
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 2s);
animation-duration: var(--fa-animation-duration, 2s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, linear);
animation-timing-function: var(--fa-animation-timing, linear); }
.fa-spin-reverse {
--fa-animation-direction: reverse; }
.fa-pulse,
.fa-spin-pulse {
-webkit-animation-name: fa-spin;
animation-name: fa-spin;
-webkit-animation-direction: var(--fa-animation-direction, normal);
animation-direction: var(--fa-animation-direction, normal);
-webkit-animation-duration: var(--fa-animation-duration, 1s);
animation-duration: var(--fa-animation-duration, 1s);
-webkit-animation-iteration-count: var(--fa-animation-iteration-count, infinite);
animation-iteration-count: var(--fa-animation-iteration-count, infinite);
-webkit-animation-timing-function: var(--fa-animation-timing, steps(8));
animation-timing-function: var(--fa-animation-timing, steps(8)); }
@media (prefers-reduced-motion: reduce) {
.fa-beat,
.fa-bounce,
.fa-fade,
.fa-beat-fade,
.fa-flip,
.fa-pulse,
.fa-shake,
.fa-spin,
.fa-spin-pulse {
-webkit-animation-delay: -1ms;
animation-delay: -1ms;
-webkit-animation-duration: 1ms;
animation-duration: 1ms;
-webkit-animation-iteration-count: 1;
animation-iteration-count: 1;
transition-delay: 0s;
transition-duration: 0s; } }
@-webkit-keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@keyframes fa-beat {
0%, 90% {
-webkit-transform: scale(1);
transform: scale(1); }
45% {
-webkit-transform: scale(var(--fa-beat-scale, 1.25));
transform: scale(var(--fa-beat-scale, 1.25)); } }
@-webkit-keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@keyframes fa-bounce {
0% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
10% {
-webkit-transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0);
transform: scale(var(--fa-bounce-start-scale-x, 1.1), var(--fa-bounce-start-scale-y, 0.9)) translateY(0); }
30% {
-webkit-transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em));
transform: scale(var(--fa-bounce-jump-scale-x, 0.9), var(--fa-bounce-jump-scale-y, 1.1)) translateY(var(--fa-bounce-height, -0.5em)); }
50% {
-webkit-transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0);
transform: scale(var(--fa-bounce-land-scale-x, 1.05), var(--fa-bounce-land-scale-y, 0.95)) translateY(0); }
57% {
-webkit-transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em));
transform: scale(1, 1) translateY(var(--fa-bounce-rebound, -0.125em)); }
64% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); }
100% {
-webkit-transform: scale(1, 1) translateY(0);
transform: scale(1, 1) translateY(0); } }
@-webkit-keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@keyframes fa-fade {
50% {
opacity: var(--fa-fade-opacity, 0.4); } }
@-webkit-keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@keyframes fa-beat-fade {
0%, 100% {
opacity: var(--fa-beat-fade-opacity, 0.4);
-webkit-transform: scale(1);
transform: scale(1); }
50% {
opacity: 1;
-webkit-transform: scale(var(--fa-beat-fade-scale, 1.125));
transform: scale(var(--fa-beat-fade-scale, 1.125)); } }
@-webkit-keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@keyframes fa-flip {
50% {
-webkit-transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg));
transform: rotate3d(var(--fa-flip-x, 0), var(--fa-flip-y, 1), var(--fa-flip-z, 0), var(--fa-flip-angle, -180deg)); } }
@-webkit-keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@keyframes fa-shake {
0% {
-webkit-transform: rotate(-15deg);
transform: rotate(-15deg); }
4% {
-webkit-transform: rotate(15deg);
transform: rotate(15deg); }
8%, 24% {
-webkit-transform: rotate(-18deg);
transform: rotate(-18deg); }
12%, 28% {
-webkit-transform: rotate(18deg);
transform: rotate(18deg); }
16% {
-webkit-transform: rotate(-22deg);
transform: rotate(-22deg); }
20% {
-webkit-transform: rotate(22deg);
transform: rotate(22deg); }
32% {
-webkit-transform: rotate(-12deg);
transform: rotate(-12deg); }
36% {
-webkit-transform: rotate(12deg);
transform: rotate(12deg); }
40%, 100% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); } }
@-webkit-keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
@keyframes fa-spin {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg); }
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg); } }
.fa-rotate-90 {
-webkit-transform: rotate(90deg);
transform: rotate(90deg); }
.fa-rotate-180 {
-webkit-transform: rotate(180deg);
transform: rotate(180deg); }
.fa-rotate-270 {
-webkit-transform: rotate(270deg);
transform: rotate(270deg); }
.fa-flip-horizontal {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1); }
.fa-flip-vertical {
-webkit-transform: scale(1, -1);
transform: scale(1, -1); }
.fa-flip-both,
.fa-flip-horizontal.fa-flip-vertical {
-webkit-transform: scale(-1, -1);
transform: scale(-1, -1); }
.fa-rotate-by {
-webkit-transform: rotate(var(--fa-rotate-angle, none));
transform: rotate(var(--fa-rotate-angle, none)); }
.fa-stack {
display: inline-block;
vertical-align: middle;
height: 2em;
position: relative;
width: 2.5em; }
.fa-stack-1x,
.fa-stack-2x {
bottom: 0;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 0;
z-index: var(--fa-stack-z-index, auto); }
.svg-inline--fa.fa-stack-1x {
height: 1em;
width: 1.25em; }
.svg-inline--fa.fa-stack-2x {
height: 2em;
width: 2.5em; }
.fa-inverse {
color: var(--fa-inverse, #fff); }
.sr-only,
.fa-sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.sr-only-focusable:not(:focus),
.fa-sr-only-focusable:not(:focus) {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0; }
.svg-inline--fa .fa-primary {
fill: var(--fa-primary-color, currentColor);
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa .fa-secondary {
fill: var(--fa-secondary-color, currentColor);
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-primary {
opacity: var(--fa-secondary-opacity, 0.4); }
.svg-inline--fa.fa-swap-opacity .fa-secondary {
opacity: var(--fa-primary-opacity, 1); }
.svg-inline--fa mask .fa-primary,
.svg-inline--fa mask .fa-secondary {
fill: black; }
.fad.fa-inverse,
.fa-duotone.fa-inverse {
color: var(--fa-inverse, #fff); }

File diff suppressed because one or more lines are too long

View file

@ -1,26 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face {
font-family: "FontAwesome";
font-display: block;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: "FontAwesome";
font-display: block;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: "FontAwesome";
font-display: block;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype");
unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; }
@font-face {
font-family: "FontAwesome";
font-display: block;
src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype");
unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F250,U+F252,U+F27A; }

View file

@ -1,6 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f250,u+f252,u+f27a}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -1,22 +0,0 @@
/*!
* Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com
* License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)
* Copyright 2022 Fonticons, Inc.
*/
@font-face {
font-family: "Font Awesome 5 Brands";
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); }
@font-face {
font-family: "Font Awesome 5 Free";
font-display: block;
font-weight: 900;
src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); }
@font-face {
font-family: "Font Awesome 5 Free";
font-display: block;
font-weight: 400;
src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); }

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