Compare commits

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

215 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
146 changed files with 4529 additions and 9118 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

6
.gitignore vendored
View file

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

View file

@ -1,4 +1,10 @@
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:
@ -17,9 +23,9 @@ repos:
rev: v1.23.3
hooks:
- id: djlint-django
args: ["--reformat"]
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"]
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"]

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

@ -1,4 +1,5 @@
from main.forms import NummiForm
from django.forms.widgets import Select
from main.forms import IconInput, NummiForm
from .models import Account
@ -10,4 +11,12 @@ class AccountForm(NummiForm):
"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,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

@ -1,12 +1,13 @@
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 UserModel
from main.models import NummiModel
class Account(UserModel):
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(
@ -15,6 +16,7 @@ class Account(UserModel):
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:
@ -32,13 +34,19 @@ class Account(UserModel):
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", "name"]
ordering = ["-default", "archived", "name"]
verbose_name = _("Account")
verbose_name_plural = _("Accounts")
class AccountModel(UserModel):
class AccountModel(NummiModel):
account = models.ForeignKey(
Account,
on_delete=models.CASCADE,

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

@ -2,21 +2,9 @@
{% load main_extras %}
{% load i18n %}
{% block title_new %}
{% translate "Create account" %}
{% translate "Create account" %}
{% endblock %}
{% block h2_new %}
{% translate "New account" %}
{% translate "New account" %}
{% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
{% block tables %}
{% if not form.instance|adding %}
<h3>{% translate "Statements" %}</h3>
{% include "main/table/statement.html" %}
{% endif %}
{% if transactions %}
<h3>{% translate "Transactions" %}</h3>
{% include "main/table/transaction.html" %}
<h3>{% translate "History" %}</h3>
{% include "main/plot/history.html" %}
{% endif %}
{% 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,
}

View file

@ -1,12 +1,13 @@
from django.urls import path
from statement.views import StatementCreateView
from transaction.views import TransactionMonthView
from . import views
urlpatterns = [
path("", views.AccountCreateView.as_view(), name="new_account"),
path("<account>", views.AccountUpdateView.as_view(), name="account"),
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(),
@ -27,9 +28,4 @@ urlpatterns = [
views.AccountDeleteView.as_view(),
name="del_account",
),
path(
"<account>/history/<int:year>/<int:month>",
TransactionMonthView.as_view(),
name="transaction_month",
),
]

View file

@ -1,8 +1,12 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
from main.views import (
NummiCreateView,
NummiDeleteView,
NummiDetailView,
NummiListView,
NummiUpdateView,
)
from statement.views import StatementListView
from transaction.utils import history
from transaction.views import TransactionListView
from .forms import AccountForm
@ -19,37 +23,18 @@ class AccountUpdateView(NummiUpdateView):
form_class = AccountForm
pk_url_kwarg = "account"
def get_context_data(self, **kwargs):
_max = 8
data = super().get_context_data(**kwargs)
account = data["form"].instance
_transactions = account.transaction_set.all()
if _transactions.count() > _max:
data["transactions_url"] = reverse_lazy(
"account_transactions", args=(account.pk,)
)
_statements = account.statement_set.all()
if _statements.count() > _max:
data["statements_url"] = reverse_lazy(
"account_statements", args=(account.pk,)
)
return data | {
"transactions": _transactions[:8],
"new_statement_url": reverse_lazy(
"new_statement", kwargs={"account": account.pk}
),
"statements": _statements[:8],
"history": history(account.transaction_set),
}
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(
@ -62,6 +47,11 @@ class AccountMixin:
return super().get_context_data(**kwargs) | {"account": self.account}
class AccountListView(NummiListView):
model = Account
context_object_name = "accounts"
class AccountTListView(AccountMixin, TransactionListView):
pass

View file

@ -1,4 +1,5 @@
from main.forms import NummiForm
from django.forms.widgets import Select
from main.forms import IconInput, NummiForm
from .models import Category
@ -11,3 +12,10 @@ class CategoryForm(NummiForm):
"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

@ -3,10 +3,10 @@ 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 UserModel
from main.models import NummiModel
class Category(UserModel):
class Category(NummiModel):
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
name = models.CharField(
max_length=64, default=_("Category"), verbose_name=_("Name")

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

@ -2,19 +2,9 @@
{% load main_extras %}
{% load i18n %}
{% block title_new %}
{% translate "Create category" %}
{% translate "Create category" %}
{% endblock %}
{% block h2_new %}
{% translate "New category" %}
{% translate "New category" %}
{% endblock %}
{% block h2 %}{{ form.instance.icon|remix }}{{ form.instance }}{% endblock %}
{% block tables %}
{% if transactions %}
<h3>{% translate "Transactions" %}</h3>
{% include "main/table/transaction.html" %}
{% endif %}
{% if history.data %}
<h3>{% translate "History" %}</h3>
{% include "main/plot/history.html" %}
{% endif %}
{% 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"),
)
)

View file

@ -1,20 +1,15 @@
from django.urls import path
from transaction.views import TransactionMonthView
from . import views
urlpatterns = [
path("", views.CategoryCreateView.as_view(), name="new_category"),
path("<category>", views.CategoryUpdateView.as_view(), name="category"),
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"),
path(
"<category>/history/<int:year>/<int:month>",
TransactionMonthView.as_view(),
name="transaction_month",
),
]

View file

@ -1,7 +1,10 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy
from main.views import NummiCreateView, NummiDeleteView, NummiUpdateView
from transaction.utils import history
from main.views import (
NummiCreateView,
NummiDeleteView,
NummiDetailView,
NummiUpdateView,
)
from transaction.views import TransactionListView
from .forms import CategoryForm
@ -18,17 +21,11 @@ class CategoryUpdateView(NummiUpdateView):
form_class = CategoryForm
pk_url_kwarg = "category"
def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs)
category = data["form"].instance
return data | {
"transactions": category.transaction_set.all()[:8],
"transactions_url": reverse_lazy(
"category_transactions", args=(category.pk,)
),
"history": history(category.transaction_set),
}
class CategoryDetailView(NummiDetailView):
model = Category
pk_url_kwarg = "category"
context_object_name = "category"
class CategoryDeleteView(NummiDeleteView):

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,4 +1,7 @@
from django import forms
from django.forms.widgets import TextInput
from .utils import get_icons
class NummiFileInput(forms.ClearableFileInput):
@ -7,6 +10,42 @@ class NummiFileInput(forms.ClearableFileInput):
class NummiForm(forms.ModelForm):
template_name = "main/form/form_base.html"
meta_fieldsets = []
def __init__(self, *args, user, **kwargs):
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

View file

@ -1,324 +0,0 @@
# NUMMI.
# Copyright (C) 2022
# This file is distributed under the same license as the nummi package.
# edpibu <git@edgarpierre.fr>, 2022.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-04-22 09:41+0200\n"
"PO-Revision-Date: 2022-12-21 17:30+0100\n"
"Last-Translator: edpibu <git@edgarpierre.fr>\n"
"Language-Team: edpibu <git@edgarpierre.fr>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: .\forms.py:95
msgid "Add transactions"
msgstr "Ajouter des transactions"
#: .\forms.py:113 .\templates\main\base.html:70
#: .\templates\main\form\search.html:5 .\templates\main\list\transaction.html:9
#: .\templates\main\list\transaction.html:26 .\templates\main\search.html:6
#: .\templates\main\search.html:18
msgid "Search"
msgstr "Rechercher"
#: .\models.py:18
msgid "User"
msgstr "Utilisateur"
#: .\models.py:37 .\models.py:71 .\models.py:79 .\models.py:229
#: .\templates\main\table\snapshot.html:24
#: .\templates\main\table\transaction.html:35
msgid "Account"
msgstr "Compte"
#: .\models.py:37 .\models.py:89 .\models.py:200 .\models.py:270
#: .\templates\main\table\invoice.html:9
#: .\templates\main\table\transaction.html:28
msgid "Name"
msgstr "Nom"
#: .\models.py:41 .\models.py:94
msgid "Icon"
msgstr "Icône"
#: .\models.py:43
msgid "Default"
msgstr "Défaut"
#: .\models.py:72 .\templates\main\index.html:16
msgid "Accounts"
msgstr "Comptes"
#: .\models.py:89 .\models.py:113 .\models.py:219
#: .\templates\main\plot\category.html:14
#: .\templates\main\table\transaction.html:32
msgid "Category"
msgstr "Catégorie"
#: .\models.py:96
msgid "Budget"
msgstr "Budget"
#: .\models.py:114 .\templates\main\form\snapshot.html:23
#: .\templates\main\index.html:30
msgid "Categories"
msgstr "Catégories"
#: .\models.py:119
msgid "End date"
msgstr "Date de fin"
#: .\models.py:121
msgid "Start date"
msgstr "Date de début"
#: .\models.py:124
msgid "End value"
msgstr "Valeur de fin"
#: .\models.py:127
msgid "Start value"
msgstr "Valeur de début"
#: .\models.py:133 .\templates\main\table\snapshot.html:27
msgid "Difference"
msgstr "Différence"
#: .\models.py:140
msgid "Transaction difference"
msgstr "Différence des transactions"
#: .\models.py:146 .\models.py:275 .\templates\main\form\fileinput.html:8
#: .\templates\main\table\invoice.html:10
#: .\templates\main\table\invoice.html:21
msgid "File"
msgstr "Fichier"
#: .\models.py:153
#, python-format
msgid "%(date)s statement"
msgstr "Relevé du %(date)s"
#: .\models.py:193 .\models.py:224
msgid "Statement"
msgstr "Relevé"
#: .\models.py:194 .\templates\main\form\account.html:13
#: .\templates\main\list\snapshot.html:6 .\templates\main\list\snapshot.html:20
msgid "Statements"
msgstr "Relevés"
#: .\models.py:200 .\models.py:263
msgid "Transaction"
msgstr "Transaction"
#: .\models.py:202
msgid "Description"
msgstr "Description"
#: .\models.py:204 .\templates\main\table\snapshot.html:26
#: .\templates\main\table\transaction.html:29
msgid "Value"
msgstr "Valeur"
#: .\models.py:206 .\templates\main\table\snapshot.html:22
#: .\templates\main\table\transaction.html:27
msgid "Date"
msgstr "Date"
#: .\models.py:207
msgid "Real date"
msgstr "Date réelle"
#: .\models.py:209 .\templates\main\table\transaction.html:30
msgid "Trader"
msgstr "Commerçant"
#: .\models.py:212
msgid "Payment"
msgstr "Paiement"
#: .\models.py:264 .\templates\main\base.html:44
#: .\templates\main\form\account.html:17 .\templates\main\form\category.html:13
#: .\templates\main\form\snapshot.html:27 .\templates\main\index.html:26
#: .\templates\main\list\transaction.html:6
#: .\templates\main\list\transaction.html:23
#: .\templates\main\month\transaction.html:15
#: .\templates\main\table\snapshot.html:28
msgid "Transactions"
msgstr "Transactions"
#: .\models.py:270 .\models.py:307
msgid "Invoice"
msgstr "Facture"
#: .\models.py:308 .\templates\main\form\transaction.html:18
msgid "Invoices"
msgstr "Factures"
#: .\templates\main\base.html:27
msgid "Skip to main content"
msgstr "Aller au contenu principal"
#: .\templates\main\base.html:33
msgid "Home"
msgstr "Accueil"
#: .\templates\main\base.html:38 .\templates\main\index.html:40
msgid "Snapshots"
msgstr "Relevés"
#: .\templates\main\base.html:50 .\templates\main\form\account.html:5
msgid "Create account"
msgstr "Créer un compte"
#: .\templates\main\base.html:55 .\templates\main\form\snapshot.html:5
msgid "Create snapshot"
msgstr "Créer un relevé"
#: .\templates\main\base.html:60 .\templates\main\form\category.html:5
msgid "Create category"
msgstr "Créer une catégorie"
#: .\templates\main\base.html:65 .\templates\main\form\transaction.html:4
#: .\templates\main\table\transaction.html:5
msgid "Create transaction"
msgstr "Créer une transaction"
#: .\templates\main\base.html:73
msgid "Log out"
msgstr "Se déconnecter"
#: .\templates\main\base.html:78 .\templates\main\form\login.html:6
msgid "Log in"
msgstr "Se connecter"
#: .\templates\main\base.html:83
msgid "Logged in as <strong>%(user)s</strong>"
msgstr "Connecté en tant que <strong>%(user)s</strong>"
#: .\templates\main\confirm_delete.html:19
#, python-format
msgid "Are you sure you want do delete <strong>%(object)s</strong> ?"
msgstr "Êtes-vous sûr de vouloir supprimer <strong>%(object)s</strong> ?"
#: .\templates\main\confirm_delete.html:23
msgid "Cancel"
msgstr "Annuler"
#: .\templates\main\confirm_delete.html:24
msgid "Confirm"
msgstr "Confirmer"
#: .\templates\main\form\account.html:8
msgid "New account"
msgstr "Nouveau compte"
#: .\templates\main\form\account.html:19 .\templates\main\form\category.html:15
#: .\templates\main\index.html:44
msgid "History"
msgstr "Historique"
#: .\templates\main\form\category.html:8
msgid "New category"
msgstr "Nouvelle catégorie"
#: .\templates\main\form\form_base.html:29
#: .\templates\main\table\invoice.html:11
#: .\templates\main\table\invoice.html:24
msgid "Delete"
msgstr "Supprimer"
#: .\templates\main\form\form_base.html:31
msgid "Reset"
msgstr "Réinitialiser"
#: .\templates\main\form\form_base.html:33
msgid "Create"
msgstr "Créer"
#: .\templates\main\form\form_base.html:35
msgid "Save"
msgstr "Enregistrer"
#: .\templates\main\form\invoice.html:4 .\templates\main\table\invoice.html:37
msgid "Create invoice"
msgstr "Créer une facture"
#: .\templates\main\form\invoice.html:7
msgid "New invoice"
msgstr "Nouvelle facture"
#: .\templates\main\form\snapshot.html:8
msgid "New snapshot"
msgstr "Nouveau relevé"
#: .\templates\main\form\transaction.html:7
msgid "New transaction"
msgstr "Nouvelle transaction"
#: .\templates\main\list\snapshot.html:27
msgid "No snapshots to show"
msgstr "Aucun relevé à afficher"
#: .\templates\main\list\transaction.html:33
msgid "No transactions to show"
msgstr "Aucune transaction à afficher"
#: .\templates\main\login.html:14
msgid "Log In"
msgstr "Se connecter"
#: .\templates\main\plot\category.html:15 .\templates\main\plot\history.html:16
msgid "Expenses"
msgstr "Dépenses"
#: .\templates\main\plot\category.html:16 .\templates\main\plot\history.html:17
msgid "Income"
msgstr "Revenus"
#: .\templates\main\plot\history.html:15
msgid "Month"
msgstr "Mois"
#: .\templates\main\table\invoice.html:30
msgid "No invoice"
msgstr "Aucune facture"
#: .\templates\main\table\snapshot.html:5
msgid "Create statement"
msgstr "Créer un relevé"
#: .\templates\main\table\snapshot.html:60
msgid "View all statements"
msgstr "Voir tous les relevés"
#: .\templates\main\table\transaction.html:73
msgid "View all transactions"
msgstr "Voir toutes les transactions"
#~ msgid "New statement"
#~ msgstr "Nouveau relevé"
#~ msgid "Create Account"
#~ msgstr "Créer Compte"
#~ msgid "Create Snapshot"
#~ msgstr "Créer Relevé"
#~ msgid "Create Category"
#~ msgstr "Créer Catégorie"
#~ msgid "Create Transaction"
#~ msgstr "Créer Transaction"
#, python-format
#~ msgid "Create %(name)s"
#~ msgstr "Créer %(name)s"

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,15 +1,46 @@
from django.conf import settings
from django.contrib.postgres.search import (
SearchQuery,
SearchRank,
SearchVector,
TrigramSimilarity,
)
from django.db import models
from django.utils.translation import gettext_lazy as _
class UserModel(models.Model):
class NummiQuerySet(models.QuerySet):
main_field = "name"
fields = dict()
def search(self, search):
return (
self.annotate(
rank=SearchRank(
sum(
(
SearchVector(field, weight=weight)
for field, weight in self.fields.items()
),
start=SearchVector(self.main_field, weight="A"),
),
SearchQuery(search, search_type="websearch"),
),
similarity=TrigramSimilarity(self.main_field, search),
)
.filter(models.Q(rank__gte=0.1) | models.Q(similarity__gte=0.3))
.order_by("-rank")
)
class NummiModel(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
verbose_name=_("User"),
editable=False,
)
objects = NummiQuerySet.as_manager()
class Meta:
abstract = True

View file

@ -1,62 +1,217 @@
form ul.errorlist {
color: var(--red);
font-weight: 550;
list-style-type: "! ";
margin: 0;
.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 > table > tbody > tr > th {
background: var(--bg-01);
background-clip: padding-box;
}
form tbody input,
form tbody select,
form tbody textarea {
font: inherit;
border: none;
background: var(--bg);
width: 100%;
height: 100%;
line-height: 1.5;
}
form input[type="checkbox"] {
width: initial;
}
table.file-input tr {
border: none;
}
table.file-input th {
text-align: left;
}
table.file-input tr :first-child {
padding-left: 0;
}
table.file-input tr :last-child {
padding-right: 0;
}
form {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fill, 32rem);
@media (width < 1024px) {
grid-template-columns: 1fr;
}
form tfoot {
text-align: right;
}
.buttons input {
font: inherit;
line-height: 1.5;
margin-left: var(--gap);
border-radius: var(--radius);
padding: 0 var(--gap);
cursor: pointer;
}
.buttons input:hover {
text-decoration: underline;
}
.buttons input[type="submit"] {
border: 0.1rem solid var(--green);
background: var(--green-1);
}
.buttons input[type="reset"] {
border: 0.1rem solid var(--red);
background: var(--red-1);
}
.buttons a.del {
color: var(--red);
&.hidden {
display: none;
}
.column {
display: grid;
gap: 0.5rem;
border: 1px solid var(--gray);
padding: var(--gap);
block-size: min-content;
}
.fieldset {
display: grid;
grid-auto-columns: 1fr;
grid-auto-flow: column;
gap: inherit;
padding: 0;
margin: 0;
border: none;
}
ul.errorlist {
color: var(--red);
font-weight: 550;
list-style: none;
padding: 0;
margin: 0;
}
.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,4 +1,5 @@
@import "https://rsms.me/inter/inter.css";
@import "https://cdn.jsdelivr.net/npm/remixicon@4.5.0/fonts/remixicon.css";
*,
*::before,
@ -23,7 +24,7 @@
--bg-inv: var(--theme-1);
--text-inv: #ffffffde;
--bg-01: #f0f0f0;
--bg-1: #f0f0f0;
--text-green: #296629;
--text-link: var(--text-green);
@ -39,7 +40,8 @@
--border: 0.5em;
--radius: 0.25em;
--num: "tnum", "ss01", "ss02", "case";
--default-ffs: "dlig", "ss01", "ss04";
--num: var(--default-ffs), "tnum", "case";
}
body {
@ -51,6 +53,7 @@ body {
display: grid;
grid-template-columns: max-content 1fr;
font-feature-settings: var(--default-ffs);
}
p {
@ -60,9 +63,10 @@ a {
color: var(--text-link);
text-decoration: none;
display: inline-block;
}
a:hover {
text-decoration: underline;
&:is(:hover, :focus) {
text-decoration: underline;
}
}
.red {
@ -77,12 +81,33 @@ a:hover {
main,
nav,
footer {
padding: 2rem;
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;
@ -91,41 +116,50 @@ nav {
height: 100vh;
position: sticky;
top: 0;
overflow-y: auto;
background: var(--bg-01);
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);
}
}
}
}
nav h1 img {
height: 1cap;
}
nav ul {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
nav .skip-link {
opacity: 0.8;
font-weight: 300;
}
nav .skip-link:active,
nav .skip-link:focus {
opacity: initial;
font-weight: 500;
}
nav a {
display: block;
}
nav a.cur {
font-weight: 550;
}
nav a.cur::after {
content: "◎";
position: absolute;
right: 0;
}
nav > :first-child,
main > :first-child {
:is(nav, main) > :first-child,
main > section:first-child > :first-child {
margin-top: 0;
}
footer {
@ -134,16 +168,46 @@ footer {
font-weight: 250;
}
#pagination {
.pagination {
text-align: center;
font-feature-settings: var(--num);
}
#pagination a {
width: 2rem;
}
#pagination a.cur {
font-weight: 650;
text-decoration: underline dotted;
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) {
@ -159,13 +223,38 @@ footer {
height: initial;
}
}
a.big-link {
margin-right: 1em;
}
[class^="ri-"] {
margin-right: 0.5em;
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,
@ -185,9 +274,299 @@ h2 {
h3 {
font-size: 1.5rem;
}
main h2.new {
opacity: 0.8;
}
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,3 +0,0 @@
.pagination .current {
font-feature-settings: "tnum", "ss01";
}

View file

@ -1,72 +1,123 @@
table.full-width col.bar {
width: auto;
}
.plot {
overflow-x: auto;
}
.plot td.bar {
position: relative;
padding: 0;
}
.plot td.bar div {
position: absolute;
height: 0.5rem;
top: 0;
}
table {
min-width: 40rem;
}
.plot td.bar div:not(.tot) {
width: 0;
box-sizing: border-box;
z-index: 1;
display: inline-block;
}
.plot td.bar.p div {
left: 0;
border-radius: 0 var(--radius) var(--radius) 0;
}
.plot td.bar.m div {
right: 0;
border-radius: var(--radius) 0 0 var(--radius);
}
.plot td.bar.m div {
background: var(--red-1);
}
.plot td.bar.p div {
background: var(--green-1);
}
.plot td.bar div.tot {
z-index: 10;
height: 0.5rem;
}
.plot td.bar.m div.tot {
background: var(--red);
}
.plot td.bar.p div.tot {
background: var(--green);
}
.plot td.bar div.tot 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);
}
.plot td.bar.p div.tot span {
left: 0;
}
.plot td.bar.m div.tot span {
right: 0;
}
@media (width < 720px) {
.plot .bar {
width: 0;
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,49 +1,59 @@
.table,
form {
.table {
overflow-x: auto;
width: 100%;
}
table {
border-collapse: collapse;
}
table.more tbody:last-child tr:last-child {
border-bottom: 0.1rem dashed var(--gray);
}
table.full-width {
width: 100%;
}
thead {
background: var(--bg-01);
}
table.full-width col {
width: 8rem;
}
table col.icon {
width: 1ch;
}
tr {
border: 0.1rem solid var(--gray);
height: 2rem;
line-height: 2rem;
}
td,
th {
padding: 0 var(--gap);
position: relative;
white-space: nowrap;
text-overflow: ellipsis;
}
.date,
.value {
font-feature-settings: var(--num);
}
.l {
text-align: left;
}
.r,
.value {
text-align: right;
}
.c,
.date {
text-align: center;
&.full-width {
width: 100%;
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;
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;
}
}
.l {
text-align: left;
}
.r,
.value {
text-align: right;
}
.c,
.date {
text-align: center;
}
}

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

View file

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

Before

Width:  |  Height:  |  Size: 539 B

View file

@ -6,6 +6,10 @@
<defs>
<rect id="basemask" x="5" y="-5" width="90" height="110" />
<rect id="line" x="45" y="-10" width="10" height="120" />
<linearGradient id="grad" gradientTransform="rotate(90)">
<stop offset="5%" stop-color="#66cc66" />
<stop offset="95%" stop-color="#cc6699" />
</linearGradient>
</defs>
<mask id="mask">
@ -15,8 +19,8 @@
</mask>
<circle cx="50" cy="50" r="50"
fill="#000000"
mask="url(#mask)"
transform-origin="center center" transform="rotate(10) scale(.8)" />
fill="url('#grad')"
mask="url('#mask')"
transform-origin="center center" transform="rotate(10)" />
</svg>

Before

Width:  |  Height:  |  Size: 539 B

After

Width:  |  Height:  |  Size: 707 B

Before After
Before After

View file

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

View file

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

View file

@ -3,42 +3,38 @@
{% load main_extras %}
{% load i18n %}
{% block title %}
{% if form.instance|adding %}
{% block title_new %}{% endblock %}
{% else %}
{{ form.instance }}
{% endif %}
Nummi
{% if form.instance|adding %}
{% block title_new %}{% endblock %}
{% else %}
{{ form.instance }}
{% endif %}
Nummi
{% endblock %}
{% block link %}
{{ block.super }}
<link rel="stylesheet"
href="{% static 'main/css/form.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/plot.css' %}"
type="text/css" />
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock %}
{% block body %}
{% with instance=form.instance %}
{% if instance|adding %}
<h2 class="new">
{% block h2_new %}{% endblock %}
</h2>
{% else %}
<h2>
{% block h2 %}{{ instance }}{% endblock %}
</h2>
{% endif %}
{% block pre %}{% endblock %}
<form method="post">
{% csrf_token %}
{% if instance|adding %}<input hidden name="next" value="{{ request.path }}" />{% endif %}
{{ form }}
</form>
{% block tables %}{% endblock %}
{% endwith %}
{% with instance=form.instance %}
{% if instance|adding %}
<h2 class="new">
{% block h2_new %}{% endblock %}
</h2>
{% else %}
<h2>
{% block h2 %}{{ instance }}{% endblock %}
</h2>
{% endif %}
{% block pre %}{% endblock %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{% if instance|adding %}
<input hidden name="next" value="{{ request.path }}">
{% endif %}
{{ form }}
</form>
{% block tables %}{% endblock %}
{% endwith %}
{% endblock %}

View file

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

View file

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

View file

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

View file

@ -1,6 +0,0 @@
{% extends "main/form/form_base.html" %}
{% load i18n %}
{% block buttons %}
<input type="reset" />
<input type="submit" value="{% translate "Search" %}" />
{% endblock %}

View file

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

View file

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

View file

@ -1,47 +1,37 @@
{% extends "main/base.html" %}
{% load static %}
{% load main_extras %}
{% load main_extras account_extras history_extras statement_extras %}
{% load i18n %}
{% block link %}
{{ block.super }}
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/plot.css' %}"
type="text/css" />
{% endblock %}
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/plot.css" %}
{% endblock link %}
{% block body %}
{% if accounts %}
<h2>{% translate "Accounts" %}</h2>
{% spaceless %}
<p>
{% for acc in accounts %}
<a class="big-link" href="{% url 'account' acc.id %}">{{ acc.icon|remix }}{{ acc }}</a>
{% endfor %}
</p>
{% endspaceless %}
{% endif %}
{% if transactions %}
<h2>{% translate "Transactions" %}</h2>
{% include "main/table/transaction.html" %}
{% endif %}
{% if categories %}
<h2>{% translate "Categories" %}</h2>
{% spaceless %}
<p>
{% for cat in categories %}
<a class="big-link" href="{% url 'category' cat.id %}">{{ cat.icon|remix }}{{ cat }}</a>
{% endfor %}
</p>
{% endspaceless %}
{% endif %}
{% if statements %}
<h2>{% translate "Statements" %}</h2>
{% include "main/table/statement.html" %}
{% endif %}
{% if history.data %}
<h2>{% translate "History" %}</h2>
{% include "main/plot/history.html" %}
{% endif %}
{% endblock %}
<div class="split">
<section>
<h2>{% translate "Accounts" %}</h2>
{% account_table user.account_set.all index=True %}
</section>
<section>
<h2>{% translate "Statements" %}</h2>
{% url "statements" as s_url %}
{% statement_table statements statements_url=s_url %}
</section>
</div>
<section>
<h2>{% translate "Categories" %}</h2>
{% spaceless %}
<p>
{% for cat in user.category_set.all %}
<a class="category" href="{{ cat.get_absolute_url }}">{{ cat.icon|remix }}{{ cat }}</a>
{% endfor %}
<a class="category add" href="{% url "new_category" %}">{{ "add"|remix }}{% translate "Create category" %}</a>
</p>
{% endspaceless %}
</section>
<section>
<h2>{% translate "History" %}</h2>
{% history_plot user.transaction_set.all %}
</section>
{% endblock body %}

View file

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

View file

@ -1,9 +0,0 @@
{% load i18n %}
{% if page_obj %}
<p id="pagination">
{% for page in paginator.page_range %}
<a href="?page={{ page }}"
{% if page == page_obj.number %}class="cur"{% endif %}>{{ page }}</a>
{% endfor %}
</p>
{% endif %}

View file

@ -1,19 +1,16 @@
{% extends "main/base.html" %}
{% load main_extras %}
{% load static %}
{% load i18n %}
{% block link %}
{{ block.super }}
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/form.css' %}"
type="text/css" />
{{ block.super }}
{% css "main/css/table.css" %}
{% css "main/css/form.css" %}
{% endblock %}
{% block body %}
<h2>{% translate "Log In" %}</h2>
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
{% include "main/form/login.html" %}
</form>
<h2>{% translate "Log in" %}</h2>
<form action="{% url 'login' %}" method="post">
{% csrf_token %}
{% include "main/form/login.html" %}
</form>
{% endblock %}

View file

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

View file

@ -0,0 +1,13 @@
{% load i18n main_extras %}
{% if first.show %}
<a href="?{% page_url 1 %}" class="first">1</a>
{% if first.dots %}<span></span>{% endif %}
{% endif %}
{% for page in pages %}
<a href="?{% page_url page.number %}"
{% if page.current %}class="cur"{% endif %}>{{ page.number }}</a>
{% endfor %}
{% if last.show %}
{% if last.dots %}<span></span>{% endif %}
<a href="?{% page_url last.number %}" class="last">{{ last.number }}</a>
{% endif %}

View file

@ -1,54 +0,0 @@
{% load main_extras %}
{% load i18n %}
<div class="plot">
<table class="full-width">
<colgroup>
<col class="desc">
<col class="icon">
<col class="value">
<col span="2" class="bar">
<col class="value">
</colgroup>
<thead>
<tr>
<th scope="col" colspan="2">{% 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.data %}
<tr>
<th scope="row">
{% if cat.category %}{{ cat.category__name }}{% endif %}
</th>
<td class="c">
{% if cat.category %}{{ cat.category__icon|remix }}{% endif %}
</td>
<td class="value">{{ cat.sum_m|pmrvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio cat.sum_m categories.max -100 %}%"></div>
{% if cat.sum < 0 %}
<div class="tot"
style="width:{% widthratio cat.sum categories.max -100 %}%">
<span>{{ cat.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio cat.sum_p categories.max 100 %}%"></div>
{% if cat.sum > 0 %}
<div class="tot"
style="width:{% widthratio cat.sum categories.max 100 %}%">
<span>{{ cat.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ cat.sum_p|pmrvalue }}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
</table>
</div>

View file

@ -1,63 +0,0 @@
{% load main_extras %}
{% load i18n %}
<div class="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>
{% spaceless %}
{% for date in history.data %}
<tr>
<td class="icon">
<span class="ri-{% if date.sum > 0 %}arrow-up-s-line green{% elif date.sum < 0 %}arrow-down-s-line red{% endif %}"></span>
</td>
<th class="date" scope="row">
{% if date.has_transactions %}
{% if account %}
<a href="{% url "transaction_month" account=account.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% elif category %}
<a href="{% url "transaction_month" category=category.pk year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% else %}
<a href="{% url "transaction_month" year=date.month.year month=date.month.month %}">{{ date.month|date:"Y-m" }}</a>
{% endif %}
{% else %}
{{ date.month|date:"Y-m" }}
{% endif %}
</th>
<td class="value">{{ date.sum_m|pmrvalue }}</td>
<td class="bar m">
<div style="width: {% widthratio date.sum_m history.max -100 %}%"></div>
{% if date.sum < 0 %}
<div class="tot" style="width:{% widthratio date.sum history.max -100 %}%">
<span>{{ date.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="bar p">
<div style="width: {% widthratio date.sum_p history.max 100 %}%"></div>
{% if date.sum > 0 %}
<div class="tot" style="width:{% widthratio date.sum history.max 100 %}%">
<span>{{ date.sum|pmrvalue }}</span>
</div>
{% endif %}
</td>
<td class="value">{{ date.sum_p|pmrvalue }}</td>
</tr>
{% endfor %}
{% endspaceless %}
</tbody>
</table>
</div>

View file

@ -1,42 +0,0 @@
{% load main_extras %}
{% load i18n %}
<div id="invoices">
<table>
<colgroup>
<col class="desc" span="3">
</colgroup>
<thead>
<th>{% translate "Name" %}</th>
<th>{% translate "File" %}</th>
<th>{% translate "Delete" %}</th>
</thead>
<tbody>
{% if transaction.invoices %}
{% for invoice in transaction.invoices %}
<tr>
<th scope="row" class="l">
<a href="{{ invoice.get_absolute_url }}">{{ invoice.name }}</a>
</th>
<td>
<a href="{{ invoice.file.url }}">{% translate "File" %} [{{ invoice.file|extension }}]</a>
</td>
<td>
<a href="{{ invoice.get_delete_url }}">{% translate "Delete" %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="3">{% translate "No invoice" %}</td>
</tr>
{% endif %}
</tbody>
<tfoot>
<tr>
<td colspan="3">
<a href="{% url "new_invoice" transaction.pk %}">{% translate "Create invoice" %}</a>
</td>
</tr>
</tfoot>
</table>
</div>

View file

@ -1,62 +0,0 @@
{% load main_extras %}
{% load i18n %}
{% if new_statement_url %}
<p>
<a href="{{ new_statement_url }}">{% translate "Create statement" %}</a>
</p>
{% endif %}
<div id="statements" class="table">
<table class="full-width {% if statements_url %}more{% endif %}">
<colgroup>
<col class="icon" span="2">
<col class="date">
{% if not account %}
<col class="icon">
<col class="desc">
{% endif %}
<col class="value" span="3">
</colgroup>
<thead>
<th>{{ "check"|remix }}</th>
<th>{{ "attachment"|remix }}</th>
<th>{% translate "Date" %}</th>
{% if not account %}
<th colspan="2">{% translate "Account" %}</th>
{% endif %}
<th>{% translate "Value" %}</th>
<th>{% translate "Difference" %}</th>
<th>{% translate "Transactions" %}</th>
</thead>
<tbody>
{% for snap in statements %}
<tr>
{% if snap.sum == snap.diff %}
<td class="c green">{{ "check"|remix }}</td>
{% else %}
<td class="c red">{{ "close"|remix }}</td>
{% endif %}
<td class="c">
{% if snap.file %}<a href="{{ snap.file.url }}">{{ "attachment"|remix }}</a>{% endif %}
</td>
<th class="date" scope="row">
<a href="{% url "statement" snap.id %}">{{ snap.date|date:"Y-m-d" }}</a>
</th>
{% if not account %}
<td class="r">{{ snap.account.icon|remix }}</td>
<td>
<a href="{% url "account" snap.account.id %}">{{ snap.account }}</a>
</td>
{% endif %}
<td class="value">{{ snap.value|value }}</td>
<td class="value">{{ snap.diff|pmvalue }}</td>
<td class="value">{{ snap.sum|pmvalue }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if statements_url %}
<p>
<a href="{{ statements_url }}">{% translate "View all statements" %}</a>
</p>
{% endif %}

View file

@ -1,75 +0,0 @@
{% load main_extras %}
{% load i18n %}
{% if new_transaction_url %}
<p>
<a href="{{ new_transaction_url }}">{% translate "Create transaction" %}</a>
</p>
{% endif %}
<div id="transactions" class="table">
<table class="full-width {% if transactions_url %}more{% endif %}">
<colgroup>
<col class="icon">
<col class="date">
<col class="desc">
<col class="value">
<col class="desc">
{% if not category %}
<col class="icon">
<col class="desc">
{% endif %}
{% if not account %}
<col class="icon">
<col class="desc">
{% endif %}
</colgroup>
<thead>
<th>{{ "attachment"|remix }}</th>
<th>{% translate "Date" %}</th>
<th>{% translate "Name" %}</th>
<th>{% translate "Value" %}</th>
<th>{% translate "Trader" %}</th>
{% if not category %}
<th colspan="2">{% translate "Category" %}</th>
{% endif %}
{% if not account %}
<th colspan="2">{% translate "Account" %}</th>
{% endif %}
</thead>
<tbody>
{% for trans in transactions %}
<tr>
<td class="c">
{% for invoice in trans.invoices %}<a href="{{ invoice.file.url }}">{{ "attachment"|remix }}</a>{% endfor %}
</td>
<td class="date">{{ trans.date|date:"Y-m-d" }}</td>
<th scope="row" class="l">
<a href="{% url "transaction" trans.id %}">{{ trans.name }}</a>
</th>
<td class="value">{{ trans.value|pmvalue }}</td>
<td>{{ trans.trader|default_if_none:"" }}</td>
{% if not category %}
{% if trans.category %}
<td class="r">{{ trans.category.icon|remix }}</td>
<td>
<a href="{% url "category" trans.category.id %}">{{ trans.category }}</a>
</td>
{% else %}
<td colspan="2"></td>
{% endif %}
{% endif %}
{% if not account %}
<td class="r">{{ trans.account.icon|remix }}</td>
<td>
<a href="{% url "account" trans.account.id %}">{{ trans.account }}</a>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if transactions_url %}
<p>
<a href="{{ transactions_url }}">{% translate "View all transactions" %}</a>
</p>
{% endif %}

View file

@ -1,9 +0,0 @@
{% extends "main/tag/value.html" %}
{% block "value" %}
{% if value %}
{% if value > 0 %}+{% endif %}
{{ block.super }}
{% else %}
{% endif %}
{% endblock %}

View file

@ -1,5 +0,0 @@
{% spaceless %}
<span>
{% block "value" %}{{ value }} €{% endblock %}
</span>
{% endspaceless %}

View file

@ -1,4 +1,9 @@
from urllib import parse
from dateutil.relativedelta import relativedelta
from django import template
from django.templatetags.static import static
from django.urls import reverse
from django.utils import formats
from django.utils.safestring import mark_safe
@ -7,16 +12,16 @@ register = template.Library()
@register.filter
def value(val, pm=False, r=2):
if not val:
if val is None:
return ""
_prefix = ""
_suffix = "&nbsp;€"
_val = formats.number_format(round(val, r), r, use_l10n=True, force_grouping=True)
_val = formats.number_format(val, decimal_pos=r, use_l10n=True, force_grouping=True)
if val > 0:
if pm:
_prefix += "&plus;&nbsp;"
else:
elif val < 0:
_val = _val[1:]
_prefix += "&minus;&nbsp;"
@ -34,21 +39,31 @@ def pmrvalue(val):
@register.filter
def remix(icon, cls=""):
return mark_safe(f"""<span class="ri-{icon}-line {cls}"></span>""")
def remix(icon, *args):
return remixnl(f"{icon}-line", *args)
@register.filter
def check(sum, diff):
if sum == diff:
return remix("check", "green")
else:
return remix("close", "red")
def remixnl(icon, cls=""):
return mark_safe(f"""<span class="ri-{icon} {cls}"></span>""")
@register.filter
def messageicon(level):
ico = {
10: "bug",
20: "information",
25: "check",
30: "alert",
40: "error-warning",
}
return remix(ico.get(level, "question"))
@register.filter
def extension(file):
return file.name.split(".")[-1].upper()
return file.name.split(".", 1)[1].upper()
@register.filter
@ -59,3 +74,67 @@ def verbose_name(obj):
@register.filter
def adding(obj):
return obj._state.adding
@register.simple_tag
def css(href):
return mark_safe(
f"""<link rel="stylesheet" href="{static(href)}" type="text/css">"""
)
@register.simple_tag
def js(href):
return mark_safe(f"""<script src="{static(href)}" defer></script>""")
@register.filter
def balance(accounts):
return sum(
statement.value for acc in accounts if (statement := acc.statement_set.first())
)
@register.inclusion_tag("main/pagination_links.html", takes_context=True)
def pagination_links(context, page_obj):
_n = 3
return {
"request": context["request"],
"pages": [
{"number": p, "current": p == page_obj.number}
for p in page_obj.paginator.page_range
if abs(p - page_obj.number) < _n
],
"first": {
"show": page_obj.number > _n,
"dots": page_obj.number > _n + 1,
},
"last": {
"show": page_obj.number <= page_obj.paginator.num_pages - _n,
"dots": page_obj.number < page_obj.paginator.num_pages - _n,
"number": page_obj.paginator.num_pages,
},
}
@register.simple_tag(takes_context=True)
def page_url(context, page):
query = context["request"].GET.copy()
query["page"] = page
return query.urlencode()
@register.simple_tag
def url_get(name, **kwargs):
kwargs = {k: v for k, v in kwargs.items() if v}
return f"{reverse(name)}?{parse.urlencode(kwargs)}"
@register.filter
def end_of_month(month):
return month + relativedelta(months=1, days=-1)
@register.filter
def end_of_year(year):
return year + relativedelta(years=1, days=-1)

View file

@ -10,5 +10,6 @@ urlpatterns = [
path("category/", include("category.urls")),
path("statement/", include("statement.urls")),
path("transaction/", include("transaction.urls")),
path("history/", include("history.urls")),
path("search/", include("search.urls")),
]

18
nummi/main/utils.py Normal file
View file

@ -0,0 +1,18 @@
import json
import pathlib
from django.contrib.staticfiles import finders
def get_icons():
with open(finders.find("main/icons/remixicon.glyph.json"), "r") as f:
data = json.load(f)
return [i.removesuffix("-line") for i in data.keys() if i.endswith("-line")]
def pdf_outline_to_str(outline):
return " ".join(
(
dest.title if not isinstance(dest, list) else pdf_outline_to_str(dest)
for dest in outline
)
)

View file

@ -1,41 +1,30 @@
from account.models import Account
from category.models import Category
from django.contrib import messages
from django.contrib.auth import views as auth_views
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext as _
from django.views.generic import (
CreateView,
DeleteView,
DetailView,
ListView,
TemplateView,
UpdateView,
)
from statement.models import Statement
from transaction.models import Transaction
from transaction.utils import history
class IndexView(LoginRequiredMixin, TemplateView):
template_name = "main/index.html"
def get_context_data(self, **kwargs):
_max = 8
_transactions = Transaction.objects.filter(user=self.request.user)
_statements = Statement.objects.filter(user=self.request.user)
res = {
"accounts": Account.objects.filter(user=self.request.user),
"transactions": _transactions[:_max],
"categories": Category.objects.filter(user=self.request.user),
"statements": _statements[:_max],
"history": history(_transactions.exclude(category__budget=False)),
return super().get_context_data(**kwargs) | {
"statements": (
self.request.user.statement_set.exclude(account__archived=True)
.order_by("account__id", "-date")
.distinct("account__id")
)
}
if _transactions.count() > _max:
res["transactions_url"] = reverse_lazy("transactions")
if _statements.count() > _max:
res["statements_url"] = reverse_lazy("statements")
return super().get_context_data(**kwargs) | res
class UserMixin(LoginRequiredMixin):
@ -55,13 +44,28 @@ class NummiCreateView(UserMixin, CreateView):
return super().form_valid(form)
def get_success_url(self):
return self.next or super().get_success_url()
surl = super().get_success_url()
messages.success(
self.request,
format_html(
"<a href='{surl}'>{name}</a> {msg}",
surl=surl,
name=self.object,
msg=_("was created successfully"),
),
)
return self.next or surl
class NummiUpdateView(UserMixin, UpdateView):
pass
class NummiDetailView(UserMixin, DetailView):
pass
class NummiDeleteView(UserMixin, DeleteView):
template_name = "main/confirm_delete.html"
success_url = reverse_lazy("index")
@ -82,4 +86,4 @@ class LogoutView(auth_views.LogoutView):
class NummiListView(UserMixin, ListView):
paginate_by = 96
pass

View file

@ -11,18 +11,18 @@ https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
import tomllib
from pathlib import Path
import toml
CONFIG_PATH = os.environ.get("NUMMI_CONFIG", None)
if CONFIG_PATH is None:
CONFIG = dict()
else:
CONFIG = toml.load(CONFIG_PATH)
CONFIG = dict()
if CONFIG_PATH is not None:
with Path(CONFIG_PATH).open("rb") as CONFIG_FILE:
CONFIG = tomllib.load(CONFIG_FILE)
# Build paths inside the project like this: BASE_DIR / 'subdir'.
MEDIA_CONF = CONFIG.get("media", {})
STATIC_CONF = CONFIG.get("static", {})
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = Path(MEDIA_CONF.get("root", "/var/lib/nummi"))
MEDIA_URL = "media/"
@ -51,6 +51,7 @@ INSTALLED_APPS = [
"statement",
"transaction",
"search",
"history",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -117,16 +118,14 @@ AUTH_PASSWORD_VALIDATORS = []
LANGUAGE_CODE = "fr-fr"
TIME_ZONE = CONFIG.get("time_zone", "CET")
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = [
"locale",
]
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = "/srv/nummi"
STATIC_ROOT = STATIC_CONF.get("root", "/srv/nummi")
LOGIN_URL = "login"
# Default primary key field type

View file

@ -3,5 +3,5 @@ from django.utils.translation import gettext_lazy as _
class SearchForm(forms.Form):
template_name = "main/form/search.html"
template_name = "search/search_form.html"
search = forms.CharField(label=_("Search"), max_length=128)

Binary file not shown.

View file

@ -0,0 +1,24 @@
# 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: 2024-01-02 15:52+0100\n"
"PO-Revision-Date: 2023-04-22 15:20+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"
#: .\search\forms.py:7 .\search\templates\search\search.html:6
#: .\search\templates\search\search.html:14
#: .\search\templates\search\search_form.html:5
msgid "Search"
msgstr "Rechercher"

View file

@ -3,23 +3,19 @@
{% load main_extras %}
{% load i18n %}
{% block title %}
{% translate "Search" %} Nummi
{% translate "Search" %} Nummi
{% endblock %}
{% block link %}
{{ block.super }}
<link rel="stylesheet"
href="{% static 'main/css/form.css' %}"
type="text/css" />
<link rel="stylesheet"
href="{% static 'main/css/table.css' %}"
type="text/css" />
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% endblock %}
{% block body %}
<h2>{% translate "Search" %}</h2>
{% spaceless %}
<form id="search" method="post">
{% csrf_token %}
{{ form }}
</form>
{% endspaceless %}
<h2>{% translate "Search" %}</h2>
{% spaceless %}
<form id="search" method="post">
{% csrf_token %}
{{ form }}
</form>
{% endspaceless %}
{% endblock %}

View file

@ -0,0 +1,7 @@
{% extends "main/form/form_base.html" %}
{% load i18n %}
{% block buttons %}
<input type="submit" value="{% translate "Search" %}" />
<input type="reset" value="{% translate "Reset" %}" />
<a href="{% url "search" %}">{% translate "Clear" %}</a>
{% endblock buttons %}

View file

@ -0,0 +1,59 @@
{% extends "main/base.html" %}
{% load i18n static %}
{% load main_extras account_extras transaction_extras statement_extras %}
{% block title %}
{% translate "Search" %} Nummi
{% endblock title %}
{% block link %}
{{ block.super }}
{% css "main/css/form.css" %}
{% css "main/css/table.css" %}
{% endblock link %}
{% block body %}
<h2>{% translate "Search" %}</h2>
<form method="post" action="{% url "search" %}">
{% csrf_token %}
{{ form }}
</form>
{% if accounts %}
<section>
<h3>{% translate "Accounts" %}</h3>
<div class="split">{% account_table accounts search=True %}</div>
</section>
{% endif %}
{% if statements %}
<section>
<h3>{% translate "Statements" %}</h3>
{% statement_table statements %}
</section>
{% endif %}
{% if categories %}
<section>
<h3>{% translate "Categories" %}</h3>
<p>
{% for cat in categories %}
<a class="category" href="{{ cat.get_absolute_url }}">{{ cat.icon|remix }}{{ cat }}</a>
{% endfor %}
</p>
</section>
{% endif %}
{% if transactions %}
<section>
<h3>{% translate "Transactions" %}</h3>
{% url_get "transactions" search=search as t_url %}
<p>
<a class="big-link" href="{{ t_url }}">{{ "list-check"|remixnl }}{% translate "Show all transactions" %}</a>
</p>
{% transaction_table transactions n_max=8 transactions_url=t_url %}
</section>
{% endif %}
{% if invoices %}
<section>
<h3>{% translate "Invoices" %}</h3>
{% invoice_table invoices=invoices %}
</section>
{% endif %}
{% if not accounts and not categories and not transactions and not invoices %}
<p>{% translate "No results found." %}</p>
{% endif %}
{% endblock body %}

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