Compare commits

..

128 Commits

Author SHA1 Message Date
Herculino Trotta 6987b54dba chore: bump python and node dependencies 2026-06-06 05:14:26 -03:00
Herculino Trotta b44563b09b Merge pull request #550 from eitchtee/dependabot/npm_and_yarn/frontend/postcss-8.5.15
build(deps): bump postcss from 8.5.6 to 8.5.15 in /frontend
2026-06-06 04:42:23 -03:00
Herculino Trotta e839f31104 Merge pull request #549 from eitchtee/dependabot/uv/django-5.2.14
build(deps): bump django from 5.2.13 to 5.2.14
2026-06-06 04:42:09 -03:00
Herculino Trotta 2282625790 Merge pull request #548 from eitchtee/dependabot/uv/idna-3.15
build(deps): bump idna from 3.11 to 3.15
2026-06-06 04:41:50 -03:00
Herculino Trotta aa8b559152 Merge pull request #547 from eitchtee/dependabot/uv/mistune-3.2.1
build(deps): bump mistune from 3.1.4 to 3.2.1
2026-06-06 04:41:37 -03:00
Herculino Trotta e1862b8241 Merge pull request #546 from eitchtee/dependabot/uv/urllib3-2.7.0
build(deps): bump urllib3 from 2.6.3 to 2.7.0
2026-06-06 04:41:22 -03:00
eitchtee eb6be8548c chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-06-06 07:41:13 +00:00
Herculino Trotta 6ee4e21939 Merge pull request #545 from eitchtee/feat/attatchments
feat(transactions): add attatchments
2026-06-06 04:40:49 -03:00
dependabot[bot] bdd8aed891 build(deps): bump postcss from 8.5.6 to 8.5.15 in /frontend
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.6 to 8.5.15.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.6...8.5.15)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.15
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:39:37 +00:00
dependabot[bot] 801b2a9edd build(deps): bump django from 5.2.13 to 5.2.14
Bumps [django](https://github.com/django/django) from 5.2.13 to 5.2.14.
- [Commits](https://github.com/django/django/compare/5.2.13...5.2.14)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.14
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:39:25 +00:00
dependabot[bot] 968499f1ab build(deps): bump idna from 3.11 to 3.15
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.15.
- [Release notes](https://github.com/kjd/idna/releases)
- [Changelog](https://github.com/kjd/idna/blob/master/HISTORY.md)
- [Commits](https://github.com/kjd/idna/compare/v3.11...v3.15)

---
updated-dependencies:
- dependency-name: idna
  dependency-version: '3.15'
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:37:29 +00:00
dependabot[bot] 5b351821b1 build(deps): bump mistune from 3.1.4 to 3.2.1
Bumps [mistune](https://github.com/lepture/mistune) from 3.1.4 to 3.2.1.
- [Release notes](https://github.com/lepture/mistune/releases)
- [Changelog](https://github.com/lepture/mistune/blob/main/docs/changes.rst)
- [Commits](https://github.com/lepture/mistune/compare/v3.1.4...v3.2.1)

---
updated-dependencies:
- dependency-name: mistune
  dependency-version: 3.2.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:36:43 +00:00
dependabot[bot] 7b49072848 build(deps): bump urllib3 from 2.6.3 to 2.7.0
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.3...2.7.0)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.7.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-06 07:36:28 +00:00
Herculino Trotta 0ee32724f1 style(toas): move toast to the top of offcanvas 2026-06-06 04:33:23 -03:00
Herculino Trotta 6a19381672 feat(transactions): add attachments 2026-06-06 04:33:06 -03:00
Herculino Trotta 248fec8b4c Merge pull request #541 from eitchtee/weblate
Translations update from Weblate
2026-05-02 16:30:56 -03:00
Weblate b34c0557fa Merge remote-tracking branch 'origin/main' 2026-05-02 19:30:37 +00:00
Herculino Trotta 2af4066aab Merge pull request #542 from eitchtee/fix-procrastinate
fix: stabilize Procrastinate worker database handling
2026-05-02 16:30:33 -03:00
Herculino Trotta d72ff3cdf5 fix(rules): allow category expressions to clear categories 2026-05-02 16:16:27 -03:00
Herculino Trotta 63c69e5c6a test(api): expect unauthorized for anonymous requests 2026-05-02 16:16:08 -03:00
Herculino Trotta 78171183cc test(currencies): avoid test discovery collision 2026-05-02 16:15:48 -03:00
Herculino Trotta 34a2b6bfd4 fix(procrastinate): close Django connections around jobs 2026-05-02 16:15:26 -03:00
masttera 1dc24f855e locale(Russian): update translation
Currently translated at 75.2% (545 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-05-01 07:24:32 +00:00
masttera 1390aff07d locale(Russian): update translation
Currently translated at 74.4% (539 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-05-01 06:24:32 +00:00
Herculino Trotta 8fc11b0acf feat(transactions): hide filter on page load to prevent flashing 2026-05-01 00:07:43 -03:00
Herculino Trotta 9a30a0d3c0 chore: bump versions and other minor things 2026-05-01 00:07:15 -03:00
Herculino Trotta 10eecd09ff fix(frontend): hyperscript not working correctly for offcanvas and modals 2026-04-30 23:16:19 -03:00
Herculino Trotta 2cfb3fb12e fix(frontend): bootstrap-grid-plugin broke 2026-04-30 23:03:42 -03:00
Herculino Trotta 47af8b135b Merge pull request #540 from eitchtee/weblate
Translations update from Weblate
2026-04-30 18:09:27 -03:00
Herculino Trotta 39d0e63375 locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (724 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2026-04-30 02:24:32 +00:00
Herculino Trotta 792154eba2 Merge pull request #539 from eitchtee/dev
fix(tom-select): dropdown covers select field when height increases
2026-04-23 23:30:56 -03:00
Herculino Trotta dc76ed3156 fix(tom-select): dropdown covers select field when height increases
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 23:29:42 -03:00
Herculino Trotta e627dd50be Merge pull request #537 from eitchtee/dev
fix: deduplication breaks when given m2m fields
2026-04-18 11:48:44 -03:00
Herculino Trotta 5527389196 fix: deduplication breaks when given m2m fields 2026-04-18 14:47:19 +00:00
Herculino Trotta be24ca014e Merge pull request #536 from eitchtee/dev
chore: deps upgrade
2026-04-18 10:48:18 -03:00
Herculino Trotta 7c7056536e feat: enable chunk splitting 2026-04-18 13:46:30 +00:00
Herculino Trotta 66d5d7a83b fix: _hyperscript not working 2026-04-18 13:45:58 +00:00
Herculino Trotta 4d3ce087d6 chore: bump versions 2026-04-18 13:45:28 +00:00
Herculino Trotta aeaf9fac43 Merge pull request #535 from eitchtee/dependabot/uv/requests-2.33.0
build(deps): bump requests from 2.32.5 to 2.33.0
2026-04-18 02:41:18 -03:00
Herculino Trotta dcedf53b83 Merge pull request #534 from eitchtee/dependabot/uv/cryptography-46.0.7
build(deps): bump cryptography from 46.0.5 to 46.0.7
2026-04-18 02:41:06 -03:00
Herculino Trotta 549648bd6b Merge pull request #532 from eitchtee/dependabot/npm_and_yarn/frontend/multi-bf05dc1ecf
build(deps): bump picomatch in /frontend
2026-04-18 02:40:43 -03:00
dependabot[bot] 79149abdd2 build(deps): bump picomatch in /frontend
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:40:24 +00:00
Herculino Trotta d66d1530bb Merge pull request #533 from eitchtee/dependabot/uv/pyjwt-2.12.0
build(deps): bump pyjwt from 2.10.1 to 2.12.0
2026-04-18 02:40:12 -03:00
Herculino Trotta 4b9b6484d3 Merge pull request #531 from eitchtee/dependabot/uv/django-5.2.13
build(deps): bump django from 5.2.11 to 5.2.13
2026-04-18 02:39:59 -03:00
Herculino Trotta a02944bdae Merge pull request #530 from eitchtee/dependabot/npm_and_yarn/frontend/immutable-5.1.5
build(deps): bump immutable from 5.1.3 to 5.1.5 in /frontend
2026-04-18 02:39:31 -03:00
Herculino Trotta 3ede5304f1 Merge pull request #529 from eitchtee/dependabot/npm_and_yarn/frontend/mathjs-15.2.0
build(deps): bump mathjs from 15.1.0 to 15.2.0 in /frontend
2026-04-18 02:39:19 -03:00
Herculino Trotta 27041695b8 Merge pull request #528 from eitchtee/dependabot/npm_and_yarn/frontend/vite-7.3.2
build(deps): bump vite from 7.2.2 to 7.3.2 in /frontend
2026-04-18 02:39:04 -03:00
dependabot[bot] 0c927a2fe9 build(deps): bump requests from 2.32.5 to 2.33.0
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.32.5...v2.33.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-version: 2.33.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:37:40 +00:00
dependabot[bot] c52db80c64 build(deps): bump cryptography from 46.0.5 to 46.0.7
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5 to 46.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.5...46.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:37:14 +00:00
dependabot[bot] 330ce8069c build(deps): bump pyjwt from 2.10.1 to 2.12.0
Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.10.1 to 2.12.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/compare/2.10.1...2.12.0)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-version: 2.12.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:36:41 +00:00
dependabot[bot] 2989f11b01 build(deps): bump django from 5.2.11 to 5.2.13
Bumps [django](https://github.com/django/django) from 5.2.11 to 5.2.13.
- [Commits](https://github.com/django/django/compare/5.2.11...5.2.13)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.13
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:36:10 +00:00
dependabot[bot] 738bb7fb74 build(deps): bump immutable from 5.1.3 to 5.1.5 in /frontend
Bumps [immutable](https://github.com/immutable-js/immutable-js) from 5.1.3 to 5.1.5.
- [Release notes](https://github.com/immutable-js/immutable-js/releases)
- [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/immutable-js/immutable-js/compare/v5.1.3...v5.1.5)

---
updated-dependencies:
- dependency-name: immutable
  dependency-version: 5.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:35:35 +00:00
dependabot[bot] 79e50cd853 build(deps): bump mathjs from 15.1.0 to 15.2.0 in /frontend
Bumps [mathjs](https://github.com/josdejong/mathjs) from 15.1.0 to 15.2.0.
- [Changelog](https://github.com/josdejong/mathjs/blob/develop/HISTORY.md)
- [Commits](https://github.com/josdejong/mathjs/compare/v15.1.0...v15.2.0)

---
updated-dependencies:
- dependency-name: mathjs
  dependency-version: 15.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:35:08 +00:00
dependabot[bot] 0a23c3ad5b build(deps): bump vite from 7.2.2 to 7.3.2 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.2.2 to 7.3.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.3.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.3.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.3.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:34:28 +00:00
Herculino Trotta 43c7749102 Merge pull request #523 from eitchtee/dependabot/uv/simpleeval-1.0.5
build(deps): bump simpleeval from 1.0.3 to 1.0.5
2026-04-18 02:32:39 -03:00
Herculino Trotta c1c4ccda8c Merge pull request #527 from eitchtee/dependabot/npm_and_yarn/frontend/rollup-4.60.1
build(deps): bump rollup from 4.52.3 to 4.60.1 in /frontend
2026-04-18 02:32:26 -03:00
Herculino Trotta 615a689c61 Merge pull request #524 from eitchtee/weblate
Translations update from Weblate
2026-04-18 02:31:51 -03:00
dependabot[bot] c5ccc42f99 build(deps): bump rollup from 4.52.3 to 4.60.1 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.52.3 to 4.60.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.3...v4.60.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.60.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 05:30:37 +00:00
LordTimothyHKIT 2baa8b21e8 locale(German): update translation
Currently translated at 92.4% (669 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/de/
2026-04-09 11:24:31 +00:00
LordTimothyHKIT 2e554141ba locale(German): update translation
Currently translated at 91.9% (666 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/de/
2026-04-09 10:24:32 +00:00
masttera 73ec6dc0fe locale(Russian): update translation
Currently translated at 72.7% (527 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 13:24:31 +00:00
masttera e19449ff99 locale(Russian): update translation
Currently translated at 72.7% (527 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 12:24:31 +00:00
masttera e81651119c locale(Russian): update translation
Currently translated at 72.7% (527 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 12:21:41 +00:00
masttera 55e9ef1b3f locale(Russian): update translation
Currently translated at 72.7% (527 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 11:50:00 +00:00
masttera c414179135 locale(Russian): update translation
Currently translated at 66.0% (478 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 11:24:31 +00:00
masttera 14c507de0f locale(Russian): update translation
Currently translated at 65.8% (477 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-31 11:24:08 +00:00
masttera 4722690fe9 locale(Russian): update translation
Currently translated at 62.1% (450 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-29 11:24:32 +00:00
masttera 493619a4ff locale(Russian): update translation
Currently translated at 55.6% (403 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-28 18:24:32 +00:00
masttera fb4aec88f1 locale(Russian): update translation
Currently translated at 35.3% (256 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-03-28 17:24:32 +00:00
dependabot[bot] 4a35e770a4 build(deps): bump simpleeval from 1.0.3 to 1.0.5
Bumps [simpleeval](https://github.com/danthedeckie/simpleeval) from 1.0.3 to 1.0.5.
- [Release notes](https://github.com/danthedeckie/simpleeval/releases)
- [Commits](https://github.com/danthedeckie/simpleeval/compare/1.0.3...1.0.5)

---
updated-dependencies:
- dependency-name: simpleeval
  dependency-version: 1.0.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-14 05:54:33 +00:00
Herculino Trotta 83b81edbae Merge pull request #522 from eitchtee/weblate
Translations update from Weblate
2026-03-02 19:30:42 -03:00
Herculino Trotta a7dc2c955e locale(Portuguese (Brazil)): update translation
Currently translated at 100.0% (724 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pt_BR/
2026-03-02 22:30:07 +00:00
Herculino Trotta ce2ae562c6 Merge pull request #520 from eitchtee/weblate
Translations update from Weblate
2026-03-02 19:27:28 -03:00
Juan David Afanador de2881ffd4 locale(Spanish): update translation
Currently translated at 100.0% (724 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2026-02-24 01:24:32 +00:00
Erwan Colin 838bf22498 locale(French): update translation
Currently translated at 99.0% (717 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-02-23 08:24:31 +00:00
Pawel Augustyn d3797ae4a5 locale(Polish): update translation
Currently translated at 72.6% (526 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-02-18 11:24:31 +00:00
Dimitri Decrock 0532397afd locale(Dutch): update translation
Currently translated at 100.0% (724 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2026-02-17 18:24:31 +00:00
Pawel Augustyn 8106dc58e5 locale(Polish): update translation
Currently translated at 70.4% (510 of 724 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-02-16 07:24:31 +00:00
Herculino Trotta 5986cf675b Merge pull request #519
fix: pulltorefresh enabled globally
2026-02-15 23:37:42 -03:00
Herculino Trotta 80da9142f1 fix: pulltorefresh enabled globally 2026-02-15 23:36:23 -03:00
eitchtee 766516d248 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-02-16 02:24:31 +00:00
Herculino Trotta 3fd0fba1b8 Merge pull request #513 from pawelaugustyn/feat/default-account
feat: default account for new transactions
2026-02-15 23:24:08 -03:00
Herculino Trotta c787565c04 refactor: move help_text to model definition 2026-02-15 23:22:52 -03:00
Herculino Trotta 0413921dbe fix: migrations set default as 0 instead of null 2026-02-15 23:22:10 -03:00
pawelaugustyn 9ecf8279b4 feat: default account for new transactions 2026-02-15 22:59:18 +01:00
Herculino Trotta 86cf625158 Merge pull request #518 from eitchtee/dev
feat(auth): trust OIDC connections and automatically connect them with local accounts
2026-02-15 14:43:33 -03:00
Herculino Trotta ea097ab6f0 feat(auth): trust OIDC connections and connect them with local accounts 2026-02-15 14:41:45 -03:00
eitchtee b1201b51bb chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-02-15 14:37:18 +00:00
Herculino Trotta 4c1d20215c Merge pull request #517 from eitchtee/dev
feat(frontend): add pull to refresh for iOS PWA
2026-02-15 11:36:59 -03:00
Herculino Trotta 27e85c4776 feat(frontend): add pull to refresh for iOS PWA 2026-02-15 11:34:28 -03:00
Herculino Trotta 5a73cd20da Merge pull request #516 from eitchtee/dependabot/uv/cryptography-46.0.5
build(deps): bump cryptography from 46.0.3 to 46.0.5
2026-02-11 21:47:14 -03:00
dependabot[bot] e305fab300 build(deps): bump cryptography from 46.0.3 to 46.0.5
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.3 to 46.0.5.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.3...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-12 00:44:59 +00:00
Herculino Trotta c11f525373 Merge pull request #515 from eitchtee/dev
add allauth logging and fix allauth not sending https redirect_uri
2026-02-11 21:43:12 -03:00
Herculino Trotta ea5d86dbf8 fix: allauth not sending https redirect_uri 2026-02-11 21:42:00 -03:00
Herculino Trotta a1d3539e3c feat: add allauth logging 2026-02-11 21:41:17 -03:00
Herculino Trotta 1028a11c8b Merge pull request #511 from eitchtee/dependabot/uv/django-5.2.11
build(deps): bump django from 5.2.9 to 5.2.11
2026-02-11 21:18:05 -03:00
Herculino Trotta e387a5e2a8 Merge pull request #510 from eitchtee/weblate
Translations update from Weblate
2026-02-11 21:17:18 -03:00
dependabot[bot] 624dc382cf build(deps): bump django from 5.2.9 to 5.2.11
Bumps [django](https://github.com/django/django) from 5.2.9 to 5.2.11.
- [Commits](https://github.com/django/django/compare/5.2.9...5.2.11)

---
updated-dependencies:
- dependency-name: django
  dependency-version: 5.2.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-03 23:24:38 +00:00
Pawel Augustyn f88699b333 locale(Polish): update translation
Currently translated at 69.6% (500 of 718 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-02-03 11:24:30 +00:00
Dimitri Decrock ca98dc073b locale(Dutch): update translation
Currently translated at 100.0% (718 of 718 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/nl/
2026-02-03 05:24:31 +00:00
eitchtee 63ba7af3c8 chore(locale): update translation files
[skip ci] Automatically generated by Django makemessages workflow
2026-02-02 01:28:27 +00:00
Herculino Trotta 2d0dee4a9b Merge pull request #509 from eitchtee/dev
feat: add reload button to the HTMX error popup
2026-02-01 22:28:03 -03:00
Herculino Trotta 0000a9ee03 Merge pull request #507 from eitchtee/weblate
Translations update from Weblate
2026-02-01 22:27:06 -03:00
Herculino Trotta 41adb37fdb feat: add reload button to the HTMX error popup 2026-02-01 22:25:47 -03:00
Pawel Augustyn 496651173e locale(Polish): update translation
Currently translated at 69.4% (498 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-31 09:24:30 +00:00
Pawel Augustyn 8836f06b80 locale(Polish): update translation
Currently translated at 68.6% (492 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-31 08:24:31 +00:00
Herculino Trotta e98a48b3a7 Merge pull request #505 from eitchtee/weblate
Translations update from Weblate
2026-01-30 10:37:50 -03:00
obervinov f9bc9f449b locale(Russian): update translation
Currently translated at 27.4% (197 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-30 10:24:30 +00:00
Pawel Augustyn 26eb1ae813 locale(Polish): update translation
Currently translated at 62.9% (451 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-28 12:24:31 +00:00
Pawel Augustyn 29a2cb9813 locale(Polish): update translation
Currently translated at 52.5% (377 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-27 21:24:32 +00:00
Pawel Augustyn be79e1b25a locale(Polish): update translation
Currently translated at 22.3% (160 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-27 20:24:31 +00:00
obervinov 3fd08466a7 locale(Russian): update translation
Currently translated at 26.4% (190 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-27 09:24:30 +00:00
obervinov 6896cdcdca locale(Russian): update translation
Currently translated at 25.2% (181 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-27 08:24:30 +00:00
Pawel Augustyn 2532930a64 locale(Polish): update translation
Currently translated at 15.2% (109 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/pl/
2026-01-25 21:24:31 +00:00
Herculino Trotta 24a1ef2d0a fix: add encoding to qif preset 2026-01-25 16:57:00 -03:00
Herculino Trotta 163f2f4e5b Merge pull request #504 from eitchtee/dev
feat: add QIF import
2026-01-25 16:54:02 -03:00
Herculino Trotta ede63acf5f Merge pull request #503 from eitchtee/dependabot/uv/urllib3-2.6.3
build(deps): bump urllib3 from 2.6.2 to 2.6.3
2026-01-25 16:53:41 -03:00
Herculino Trotta a8ba3d8754 Merge pull request #500 from eitchtee/weblate
Translations update from Weblate
2026-01-25 16:53:09 -03:00
dependabot[bot] e2f1156264 build(deps): bump urllib3 from 2.6.2 to 2.6.3
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.2 to 2.6.3.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.6.2...2.6.3)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-version: 2.6.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-25 19:49:42 +00:00
Herculino Trotta d5bbad7887 feat: add QIF import 2026-01-25 16:46:56 -03:00
Andrei Kamianets 7ebacff6e4 locale(Russian): update translation
Currently translated at 25.1% (180 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-19 10:24:31 +00:00
Andrei Kamianets df8ef5d04c locale(Russian): update translation
Currently translated at 12.6% (91 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/ru/
2026-01-19 09:24:31 +00:00
Andrei Kamianets fa2a8b8c65 locale((Russian)): added translation using Weblate 2026-01-19 08:57:52 +00:00
Juan David Afanador e44ac5dab6 locale(Spanish): update translation
Currently translated at 100.0% (717 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/es/
2026-01-18 21:24:30 +00:00
Ebrahim Tayabali f9261d1283 locale(Swahili): update translation
Currently translated at 0.9% (7 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/sw/
2026-01-18 19:24:30 +00:00
Ebrahim Tayabali 4c73c1cae5 locale(Swahili): update translation
Currently translated at 0.0% (0 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/sw/
2026-01-15 21:24:30 +00:00
Ebrahim Tayabali 0315a56f88 locale((Swahili)): added translation using Weblate 2026-01-15 20:56:34 +00:00
sorcierwax 44d6b8b53c locale(French): update translation
Currently translated at 100.0% (717 of 717 strings)

Translation: WYGIWYH/App
Translate-URL: https://translations.herculino.com/projects/wygiwyh/app/fr/
2026-01-14 22:24:30 +00:00
70 changed files with 15166 additions and 4367 deletions
+3
View File
@@ -165,3 +165,6 @@ cython_debug/
node_modules/
postgres_data/
.prod.env
# Private local uploads
app/attachments/
+29
View File
@@ -0,0 +1,29 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Docker: Dev",
"type": "node-terminal",
"request": "launch",
"command": "docker compose --env-file .env -f docker-compose.dev.yml up --build",
"cwd": "${workspaceFolder}",
"postDebugTask": "Docker: Dev Down"
},
{
"name": "Docker: Dev (no rebuild)",
"type": "node-terminal",
"request": "launch",
"command": "docker compose --env-file .env -f docker-compose.dev.yml up",
"cwd": "${workspaceFolder}",
"postDebugTask": "Docker: Dev Down"
},
{
"name": "Docker: Prod",
"type": "node-terminal",
"request": "launch",
"command": "docker compose --env-file .prod.env -f docker-compose.prod.yml up --build",
"cwd": "${workspaceFolder}",
"postDebugTask": "Docker: Prod Down"
}
]
}
+119
View File
@@ -0,0 +1,119 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Docker: Dev",
"type": "shell",
"command": "docker",
"args": [
"compose",
"--env-file",
".env",
"-f",
"docker-compose.dev.yml",
"up",
"--build"
],
"options": {
"cwd": "${workspaceFolder}"
},
"group": "build",
"problemMatcher": []
},
{
"label": "Docker: Dev (no rebuild)",
"type": "shell",
"command": "docker",
"args": [
"compose",
"--env-file",
".env",
"-f",
"docker-compose.dev.yml",
"up"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "Docker: Dev Refresh Vite Deps",
"type": "shell",
"command": "docker compose --env-file .env -f docker-compose.dev.yml rm -sfv vite; docker compose --env-file .env -f docker-compose.dev.yml up --build",
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "Docker: Dev Down",
"type": "shell",
"command": "docker",
"args": [
"compose",
"--env-file",
".env",
"-f",
"docker-compose.dev.yml",
"down"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "Docker: Prod",
"type": "shell",
"command": "docker",
"args": [
"compose",
"--env-file",
".prod.env",
"-f",
"docker-compose.prod.yml",
"up",
"--build"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "Docker: Prod Down",
"type": "shell",
"command": "docker",
"args": [
"compose",
"--env-file",
".prod.env",
"-f",
"docker-compose.prod.yml",
"down"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": []
},
{
"label": "Django: Runserver localhost:8000",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"manage.py",
"runserver",
"localhost:8000"
],
"options": {
"cwd": "${workspaceFolder}/app",
"env": {
"PYTHONUNBUFFERED": "1"
}
},
"problemMatcher": []
}
]
}
+7
View File
@@ -157,6 +157,13 @@ WYGIWYH supports login via OpenID Connect (OIDC) through `django-allauth`. This
> [!NOTE]
> Currently only OpenID Connect is supported as a provider, open an issue if you need something else.
> [!Caution]
> WYGIWYH automatically connects OIDC accounts to existing local accounts with matching email addresses.
> This means if a user already exists with email `user@example.com` and someone logs in via OIDC with the same email, the OIDC account will be automatically linked to the existing account without requiring user confirmation.
> This is only recommended for trusted OIDC providers that verify email addresses and where you control who can create accounts.
### Configuration
To configure OIDC, you need to set the following environment variables:
| Variable | Description |
+28 -27
View File
@@ -311,6 +311,7 @@ LOCALE_PATHS = [BASE_DIR / "locale"]
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static_files"
ATTACHMENT_MEDIA_ROOT = BASE_DIR / "attachments"
STATICFILES_DIRS = [
ROOT_DIR / "frontend" / "build",
@@ -376,8 +377,10 @@ ACCOUNT_EMAIL_VERIFICATION = "none"
SOCIALACCOUNT_LOGIN_ON_GET = True
SOCIALACCOUNT_ONLY = True
SOCIALACCOUNT_AUTO_SIGNUP = os.getenv("OIDC_ALLOW_SIGNUP", "true").lower() == "true"
SOCIALACCOUNT_EMAIL_AUTHENTICATION = True
SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter"
SOCIALACCOUNT_ADAPTER = "allauth.socialaccount.adapter.DefaultSocialAccountAdapter"
SOCIALACCOUNT_ADAPTER = "apps.users.adapters.AutoConnectSocialAccountAdapter"
# CRISPY FORMS
CRISPY_ALLOWED_TEMPLATE_PACKS = [
@@ -390,6 +393,10 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = int(os.getenv("SESSION_EXPIRY_TIME", 2678400)) # 31 days
SESSION_COOKIE_SECURE = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
HTTPS_ENABLED = os.getenv("HTTPS_ENABLED", "false").lower() == "true"
ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" if HTTPS_ENABLED else "http"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") if HTTPS_ENABLED else None
DEBUG_TOOLBAR_CONFIG = {
"ROOT_TAG_EXTRA_ATTRS": "hx-preserve",
# "SHOW_TOOLBAR_CALLBACK": lambda r: False, # disables it
@@ -434,14 +441,14 @@ REST_FRAMEWORK = {
"apps.api.permissions.NotInDemoMode",
"rest_framework.permissions.DjangoModelPermissions",
],
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.OrderingFilter',
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend",
"rest_framework.filters.OrderingFilter",
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.BasicAuthentication",
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PAGINATION_CLASS": "apps.api.custom.pagination.CustomPageNumberPagination",
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
@@ -458,7 +465,7 @@ SPECTACULAR_SETTINGS = {
if "procrastinate" in sys.argv:
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"disable_existing_loggers": True,
"formatters": {
"standard": {
"format": "[%(asctime)s] - %(levelname)s - %(name)s - %(message)s",
@@ -466,26 +473,19 @@ if "procrastinate" in sys.argv:
},
},
"handlers": {
"procrastinate": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "standard",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "standard",
"level": "INFO",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"procrastinate": {
"handlers": ["procrastinate"],
"propagate": False,
},
"root": {
"handlers": ["console"],
"level": "INFO",
"propagate": False,
},
},
}
@@ -505,19 +505,20 @@ else:
"formatter": "standard",
"level": "INFO",
},
"procrastinate": {
"level": "INFO",
"class": "logging.StreamHandler",
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
"loggers": {
"procrastinate": {
"handlers": None,
"handlers": [],
"propagate": False,
},
"root": {
"allauth": {
"handlers": ["console"],
"level": "INFO",
"level": "DEBUG",
"propagate": False,
},
},
}
+2 -2
View File
@@ -90,10 +90,10 @@ class AccountBalanceAPITests(TestCase):
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_get_balance_unauthenticated(self):
"""Test unauthenticated request returns 403"""
"""Test unauthenticated request returns 401"""
unauthenticated_client = APIClient()
response = unauthenticated_client.get(
f"/api/accounts/{self.account.id}/balance/"
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+6 -6
View File
@@ -159,7 +159,7 @@ column_mapping:
self.assertIn("import_run_id", response.data)
def test_unauthenticated_request(self):
"""Test unauthenticated request returns 403"""
"""Test unauthenticated request returns 401"""
unauthenticated_client = APIClient()
csv_content = b"date,description,amount\n2025-01-01,Test,100"
@@ -173,7 +173,7 @@ column_mapping:
format="multipart",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@override_settings(
@@ -266,11 +266,11 @@ column_mapping:
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_profiles_unauthenticated(self):
"""Test unauthenticated request returns 403"""
"""Test unauthenticated request returns 401"""
unauthenticated_client = APIClient()
response = unauthenticated_client.get("/api/import/profiles/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
@override_settings(
@@ -397,8 +397,8 @@ column_mapping:
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_runs_unauthenticated(self):
"""Test unauthenticated request returns 403"""
"""Test unauthenticated request returns 401"""
unauthenticated_client = APIClient()
response = unauthenticated_client.get("/api/import/runs/")
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+42 -1
View File
@@ -1,6 +1,47 @@
import functools
import inspect
import procrastinate
from django.db import close_old_connections
_CONNECTION_CLEANUP_WRAPPED = "_wygiwyh_connection_cleanup_wrapped"
def _wrap_task_with_django_connection_cleanup(task):
if getattr(task.func, _CONNECTION_CLEANUP_WRAPPED, False):
return
func = task.func
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapped(*args, **kwargs):
close_old_connections()
try:
return await func(*args, **kwargs)
finally:
close_old_connections()
wrapped = async_wrapped
else:
@functools.wraps(func)
def sync_wrapped(*args, **kwargs):
close_old_connections()
try:
return func(*args, **kwargs)
finally:
close_old_connections()
wrapped = sync_wrapped
setattr(wrapped, _CONNECTION_CLEANUP_WRAPPED, True)
task.func = wrapped
def on_app_ready(app: procrastinate.App):
"""This function is ran upon procrastinate initialization."""
...
for task in set(app.tasks.values()):
_wrap_task_with_django_connection_cleanup(task)
+1
View File
@@ -0,0 +1 @@
@@ -0,0 +1,89 @@
from unittest.mock import patch
import procrastinate
from django.db import connection
from django.test import SimpleTestCase, TransactionTestCase
from procrastinate.testing import InMemoryConnector
from apps.common.procrastinate import on_app_ready
def make_app_with_task(func):
app = procrastinate.App(connector=InMemoryConnector())
task = app.task(name="sample_task")(func)
return app, task
class ProcrastinateConnectionCleanupTests(SimpleTestCase):
def test_app_ready_closes_old_connections_around_sync_tasks(self):
calls = []
def sample_task(value):
calls.append(("task", value))
return value * 2
app, task = make_app_with_task(sample_task)
with patch(
"apps.common.procrastinate.close_old_connections",
create=True,
side_effect=lambda: calls.append(("cleanup", None)),
):
on_app_ready(app)
result = task.func(3)
self.assertEqual(result, 6)
self.assertEqual(
calls,
[
("cleanup", None),
("task", 3),
("cleanup", None),
],
)
def test_app_ready_closes_old_connections_when_sync_task_raises(self):
calls = []
def sample_task():
calls.append(("task", None))
raise RuntimeError("boom")
app, task = make_app_with_task(sample_task)
with patch(
"apps.common.procrastinate.close_old_connections",
create=True,
side_effect=lambda: calls.append(("cleanup", None)),
):
on_app_ready(app)
with self.assertRaises(RuntimeError):
task.func()
self.assertEqual(
calls,
[
("cleanup", None),
("task", None),
("cleanup", None),
],
)
class ProcrastinateConnectionRecoveryTests(TransactionTestCase):
def test_wrapped_task_recovers_from_closed_django_connection(self):
def sample_task():
with connection.cursor() as cursor:
cursor.execute("SELECT 1")
return cursor.fetchone()[0]
app, task = make_app_with_task(sample_task)
on_app_ready(app)
connection.ensure_connection()
connection.connection.close()
self.assertEqual(task.func(), 1)
+12 -1
View File
@@ -106,6 +106,17 @@ class ExcelImportSettings(BaseModel):
sheets: list[str] | str = "*"
class QIFImportSettings(BaseModel):
skip_errors: bool = Field(
default=False,
description="If True, errors during import will be logged and skipped",
)
file_type: Literal["qif"] = "qif"
importing: Literal["transactions"] = "transactions"
encoding: str = Field(default="utf-8", description="File encoding")
date_format: str = Field(..., description="Date format (e.g. %d/%m/%Y)")
class ColumnMapping(BaseModel):
source: Optional[str] | Optional[list[str]] = Field(
default=None,
@@ -342,7 +353,7 @@ class CurrencyExchangeMapping(ColumnMapping):
class ImportProfileSchema(BaseModel):
settings: CSVImportSettings | ExcelImportSettings
settings: CSVImportSettings | ExcelImportSettings | QIFImportSettings
mapping: Dict[
str,
TransactionAccountMapping
+290 -7
View File
@@ -3,6 +3,8 @@ import hashlib
import logging
import os
import re
import zipfile
from django.db import transaction
from datetime import datetime, date
from decimal import Decimal, InvalidOperation
from typing import Dict, Any, Literal, Union
@@ -11,6 +13,7 @@ import openpyxl
import xlrd
import yaml
from cachalot.api import cachalot_disabled
from django.core.exceptions import FieldDoesNotExist
from django.utils import timezone
from openpyxl.utils.exceptions import InvalidFileException
@@ -363,7 +366,7 @@ class ImportService:
try:
if entities_mapping:
if entities_mapping.type == "id":
entity = TransactionTag.objects.filter(
entity = TransactionEntity.objects.filter(
id=entity_name
).first()
else: # name
@@ -460,12 +463,12 @@ class ImportService:
for field in rule.fields:
if field in transaction_data:
value = transaction_data[field]
# Use __iexact only for string fields; non-string types
# (date, Decimal, bool, int, etc.) don't support UPPER()
if rule.match_type == "strict" or not isinstance(value, str):
query = query.filter(**{field: value})
else: # lax matching for strings only
query = query.filter(**{f"{field}__iexact": value})
query = self._apply_deduplication_filter(
query=query,
field=field,
value=value,
match_type=rule.match_type,
)
# If we found any matching transaction, it's a duplicate
if query.exists():
@@ -473,6 +476,71 @@ class ImportService:
return False
@staticmethod
def _is_int_like(value: Any) -> bool:
try:
int(value)
except (TypeError, ValueError):
return False
return True
def _apply_deduplication_filter(
self,
query,
field: str,
value: Any,
match_type: Literal["lax", "strict"],
):
if isinstance(value, list):
return self._apply_list_deduplication_filter(
query=query,
field=field,
values=value,
match_type=match_type,
)
# Use __iexact only for string fields; non-string types
# (date, Decimal, bool, int, etc.) don't support UPPER()
if match_type == "strict" or not isinstance(value, str):
return query.filter(**{field: value})
return query.filter(**{f"{field}__iexact": value})
def _apply_list_deduplication_filter(
self,
query,
field: str,
values: list[Any],
match_type: Literal["lax", "strict"],
):
clean_values = [v for v in values if v not in (None, "")]
if not clean_values:
return query
try:
model_field = Transaction._meta.get_field(field)
except FieldDoesNotExist:
return query.filter(**{f"{field}__in": clean_values})
if getattr(model_field, "many_to_many", False):
# For m2m fields (e.g., entities/tags), apply one filter per value so
# all provided values must be present in the matched transaction.
if all(self._is_int_like(v) for v in clean_values):
for value in clean_values:
query = query.filter(**{f"{field}__id": int(value)})
else:
for value in clean_values:
lookup = (
f"{field}__name"
if match_type == "strict"
else f"{field}__name__iexact"
)
query = query.filter(**{lookup: str(value).strip()})
return query.distinct()
return query.filter(**{f"{field}__in": clean_values})
def _coerce_type(
self, value: str, mapping: version_1.ColumnMapping
) -> Union[str, int, bool, Decimal, datetime, list, None]:
@@ -845,6 +913,219 @@ class ImportService:
f"Invalid {self.settings.file_type.upper()} file format: {str(e)}"
)
def _parse_and_import_qif(self, content_lines: list[str], filename: str) -> None:
# Infer account from filename (remove extension)
account_name = os.path.splitext(os.path.basename(filename))[0]
current_transaction = {}
raw_lines_buffer = []
account = Account.objects.filter(name=account_name).first()
if not account:
raise ValueError(f"Account '{account_name}' not found.")
row_number = 0
for line in content_lines:
row_number += 1
line = line.strip()
if not line:
continue
raw_lines_buffer.append(line)
if line == "^":
if current_transaction:
# Deduplication using hash of raw lines
raw_content = "".join(raw_lines_buffer)
internal_id = hashlib.sha256(
raw_content.encode("utf-8")
).hexdigest()
# Reset buffer for next transaction
raw_lines_buffer = []
try:
with transaction.atomic():
if Transaction.objects.filter(
internal_id=internal_id
).exists():
self._increment_totals("skipped", 1)
self._log(
"info",
f"Skipped duplicate transaction from {filename}",
)
current_transaction = {}
continue
# Handle Account
if account:
current_transaction["account"] = account
else:
acc = Account.objects.filter(name=account_name).first()
if acc:
current_transaction["account"] = acc
else:
raise ValueError(
f"Account '{account_name}' not found."
)
current_transaction["internal_id"] = internal_id
# Handle Description/Memo mapping
if "memo" in current_transaction:
current_transaction["description"] = (
current_transaction.pop("memo")
)
# Handle Payee mapping
entities = []
if "payee" in current_transaction:
payee_name = current_transaction.pop("payee")
# "Treat the payee (P) as the entity. Use existing or create"
entity, _ = TransactionEntity.objects.get_or_create(
name=payee_name
)
entities.append(entity)
# Handle Label/Category
category = None
tags = []
if "label" in current_transaction:
label = current_transaction.pop("label")
if label.startswith("[") and label.endswith("]"):
# Transfer: set label as description, ignore category/tags
clean_label = label[1:-1]
current_transaction["description"] = clean_label
else:
parts = label.split(":")
if parts:
cat_name = parts[0].strip()
if cat_name:
category, _ = (
TransactionCategory.objects.get_or_create(
name=cat_name
)
)
if len(parts) > 1:
for tag_name in parts[1:]:
tag_name = tag_name.strip()
if tag_name:
tag, _ = (
TransactionTag.objects.get_or_create(
name=tag_name
)
)
tags.append(tag)
current_transaction["category"] = category
# Create transaction
new_trans = Transaction.objects.create(
**current_transaction
)
if entities:
new_trans.entities.set(entities)
if tags:
new_trans.tags.set(tags)
self.import_run.transactions.add(new_trans)
self._increment_totals("successful", 1)
except Exception as e:
if not self.settings.skip_errors:
raise e
self._log(
"warning",
f"Error processing transaction in {filename}: {str(e)}",
)
self._increment_totals("failed", 1)
# Reset for next transaction
current_transaction = {}
else:
# Empty transaction record (orphaned ^)
raw_lines_buffer = []
pass
self._increment_totals("processed", 1)
continue
if line.startswith("!"):
continue
code = line[0]
value = line[1:]
if code == "D":
try:
current_transaction["date"] = datetime.strptime(
value, self.settings.date_format
).date()
except ValueError:
self._log(
"warning",
f"Could not parse date '{value}' using format '{self.settings.date_format}' in {filename}",
)
if not self.settings.skip_errors:
raise ValueError(f"Invalid date format '{value}'")
elif code == "T":
try:
cleaned_value = value.replace(",", "")
amount = Decimal(cleaned_value)
if amount < 0:
current_transaction["type"] = Transaction.Type.EXPENSE
current_transaction["amount"] = abs(amount)
else:
current_transaction["type"] = Transaction.Type.INCOME
current_transaction["amount"] = amount
except InvalidOperation:
self._log(
"warning", f"Could not parse amount '{value}' in {filename}"
)
if not self.settings.skip_errors:
raise ValueError(f"Invalid amount format '{value}'")
elif code == "P":
current_transaction["payee"] = value
elif code == "M":
current_transaction["memo"] = value
elif code == "L":
current_transaction["label"] = value
elif code == "N":
pass
def _process_qif(self, file_path):
def process_logic():
if zipfile.is_zipfile(file_path):
try:
with zipfile.ZipFile(file_path, "r") as zf:
for filename in zf.namelist():
if filename.lower().endswith(
".qif"
) and not filename.startswith("__MACOSX"):
self._log(
"info", f"Processing QIF from ZIP: {filename}"
)
with zf.open(filename) as f:
content = f.read().decode(self.settings.encoding)
self._parse_and_import_qif(
content.splitlines(), filename
)
except Exception as e:
raise ValueError(f"Error processing ZIP file: {str(e)}")
else:
with open(file_path, "r", encoding=self.settings.encoding) as f:
self._parse_and_import_qif(
f.readlines(), os.path.basename(file_path)
)
if not self.settings.skip_errors:
with transaction.atomic():
process_logic()
else:
process_logic()
def _validate_file_path(self, file_path: str) -> str:
"""
Validates that the file path is within the allowed temporary directory.
@@ -871,6 +1152,8 @@ class ImportService:
self._process_csv(file_path)
elif isinstance(self.settings, version_1.ExcelImportSettings):
self._process_excel(file_path)
elif isinstance(self.settings, version_1.QIFImportSettings):
self._process_qif(file_path)
self._update_status("FINISHED")
self._log(
@@ -15,7 +15,7 @@ from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency
from apps.import_app.models import ImportProfile, ImportRun
from apps.import_app.services.v1 import ImportService
from apps.transactions.models import Transaction
from apps.transactions.models import Transaction, TransactionEntity
class DeduplicationTests(TestCase):
@@ -273,3 +273,39 @@ deduplication:
}
)
self.assertTrue(is_duplicate)
def test_deduplication_with_entities_list_value(self):
"""Test that list values for m2m entities deduplicate correctly."""
entity = TransactionEntity.objects.create(name="DB Vertrieb GmbH")
self.existing_transaction.entities.add(entity)
service = self._create_import_service_with_deduplication(
fields=["date", "amount", "entities"], match_type="strict"
)
is_duplicate = service._check_duplicate_transaction(
{
"date": date(2024, 1, 15),
"amount": Decimal("100.00"),
"entities": ["DB Vertrieb GmbH"],
}
)
self.assertTrue(is_duplicate)
def test_deduplication_with_entities_list_value_not_matching(self):
"""Test that non-matching entity list values are not marked duplicate."""
entity = TransactionEntity.objects.create(name="DB Vertrieb GmbH")
self.existing_transaction.entities.add(entity)
service = self._create_import_service_with_deduplication(
fields=["date", "amount", "entities"], match_type="strict"
)
is_duplicate = service._check_duplicate_transaction(
{
"date": date(2024, 1, 15),
"amount": Decimal("100.00"),
"entities": ["Different Entity"],
}
)
self.assertFalse(is_duplicate)
@@ -0,0 +1,259 @@
from decimal import Decimal
import os
import shutil
from django.test import TestCase
from django.contrib.auth import get_user_model
from apps.accounts.models import Account, AccountGroup
from apps.currencies.models import Currency
from apps.common.middleware.thread_local import write_current_user, delete_current_user
from apps.import_app.models import ImportProfile, ImportRun
from apps.import_app.services.v1 import ImportService
from apps.transactions.models import (
Transaction,
)
class QIFImportTests(TestCase):
def setUp(self):
# Patch TEMP_DIR for testing
self.original_temp_dir = ImportService.TEMP_DIR
self.test_dir = os.path.abspath("temp_test_import")
ImportService.TEMP_DIR = self.test_dir
os.makedirs(self.test_dir, exist_ok=True)
# Create user and set context
User = get_user_model()
self.user = User.objects.create_user(
email="test@example.com", password="password"
)
write_current_user(self.user)
self.currency = Currency.objects.create(
code="BRL", name="Real", decimal_places=2, prefix="R$ "
)
self.group = AccountGroup.objects.create(name="Test Group", owner=self.user)
self.account = Account.objects.create(
name="bradesco-checking",
group=self.group,
currency=self.currency,
owner=self.user,
)
def tearDown(self):
delete_current_user()
ImportService.TEMP_DIR = self.original_temp_dir
if os.path.exists(self.test_dir):
shutil.rmtree(self.test_dir)
def test_import_single_qif_valid_mapping(self):
content = """!Type:Bank
D04/01/2015
T8069.46
PMy Payee -> Entity
MNote -> Desc
LOld Cat:New Tag
^
D05/01/2015
T-100.00
PSupermarket
MWeekly shopping
L[Transfer]
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 2)
# Transaction 1: Income, Category+Tag
t1 = Transaction.objects.get(description="Note -> Desc")
self.assertEqual(t1.amount, Decimal("8069.46"))
self.assertEqual(t1.type, Transaction.Type.INCOME)
self.assertEqual(t1.category.name, "Old Cat")
self.assertTrue(t1.tags.filter(name="New Tag").exists())
self.assertTrue(t1.entities.filter(name="My Payee -> Entity").exists())
self.assertEqual(t1.account, self.account)
# Transaction 2: Expense, Transfer ([Transfer] -> Description)
t2 = Transaction.objects.get(description="Transfer")
self.assertEqual(t2.amount, Decimal("100.00"))
self.assertEqual(t2.type, Transaction.Type.EXPENSE)
self.assertIsNone(t2.category)
self.assertFalse(t2.tags.exists())
self.assertTrue(t2.entities.filter(name="Supermarket").exists())
self.assertEqual(t2.description, "Transfer")
def test_import_deduplication_hash(self):
# Same content twice. Should result in only 1 transaction due to hash deduplication.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
# First run
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 1)
# Service deletes file after processing, so recreate it for second run
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
# Second run - Duplicate content
service.process_file(file_path)
self.assertEqual(Transaction.objects.count(), 1)
def test_import_strict_error_rollback(self):
# atomic check.
# Transaction 1 valid, Transaction 2 invalid date.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
DINVALID
T100.00
PBad
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
skip_errors: false
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
with self.assertRaises(Exception) as cm:
service.process_file(file_path)
self.assertEqual(str(cm.exception), "Import failed")
# Should be 0 transactions because of atomic rollback
self.assertEqual(Transaction.objects.count(), 0)
def test_import_missing_account(self):
# File with account name that doesn't exist
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
"""
filename = "missing-account.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
# Should fail because account doesn't exist
with self.assertRaises(Exception) as cm:
service.process_file(file_path)
self.assertEqual(str(cm.exception), "Import failed")
def test_import_skip_errors(self):
# skip_errors: true.
# Transaction 1 valid, Transaction 2 invalid date.
content = """!Type:Bank
D04/01/2015
T100.00
POK
^
DINVALID
T100.00
PBad
^
"""
filename = "bradesco-checking.qif"
file_path = os.path.join(self.test_dir, filename)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
yaml_config = """
settings:
file_type: qif
importing: transactions
date_format: "%d/%m/%Y"
skip_errors: true
mapping: {}
"""
profile = ImportProfile.objects.create(
name="QIF Profile",
yaml_config=yaml_config,
version=ImportProfile.Versions.VERSION_1,
)
run = ImportRun.objects.create(profile=profile, file_name=filename)
service = ImportService(run)
service.process_file(file_path)
# Should be 1 transaction (valid one)
self.assertEqual(Transaction.objects.count(), 1)
self.assertEqual(
Transaction.objects.first().description, ""
) # empty desc if no memo
+6 -2
View File
@@ -365,7 +365,9 @@ def check_for_transaction_rules(
if processed_action.set_category:
value = simple.eval(processed_action.set_category)
if isinstance(value, int):
if value is None:
transaction.category = None
elif isinstance(value, int):
transaction.category = TransactionCategory.objects.get(id=value)
else:
transaction.category = TransactionCategory.objects.get(name=value)
@@ -458,7 +460,9 @@ def check_for_transaction_rules(
transaction.account = account
elif field == TransactionRuleAction.Field.category:
if isinstance(new_value, int):
if new_value is None:
transaction.category = None
elif isinstance(new_value, int):
category = TransactionCategory.objects.get(id=new_value)
transaction.category = category
elif isinstance(new_value, str):
+1
View File
@@ -0,0 +1 @@
+82
View File
@@ -0,0 +1,82 @@
from datetime import date
from decimal import Decimal
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TransactionTestCase
from apps.accounts.models import Account
from apps.currencies.models import Currency
from apps.rules.models import TransactionRule, UpdateOrCreateTransactionRuleAction
from apps.rules.tasks import check_for_transaction_rules
from apps.transactions.models import Transaction
def run_check_for_transaction_rules_without_worker_wrapper(**kwargs):
task_func = check_for_transaction_rules.func
task_func = getattr(task_func, "__wrapped__", task_func)
return task_func(**kwargs)
class CheckForTransactionRulesTests(TransactionTestCase):
def setUp(self):
User = get_user_model()
self.user = User.objects.create_user(
email="rules@example.com",
password="testpass123",
)
self.currency = Currency.objects.create(
code="USD",
name="US Dollar",
decimal_places=2,
)
self.account = Account.objects.create(
name="Main Account",
currency=self.currency,
owner=self.user,
)
@patch("apps.rules.signals.check_for_transaction_rules.defer")
def test_update_or_create_action_can_clear_category_from_none_expression(
self, mock_defer
):
source_transaction = Transaction.objects.create(
account=self.account,
type=Transaction.Type.EXPENSE,
amount=Decimal("10.00"),
date=date(2026, 5, 4),
reference_date=date(2026, 5, 1),
description="Source without category",
category=None,
owner=self.user,
)
rule = TransactionRule.objects.create(
active=True,
on_create=False,
on_update=True,
name="Copy transaction",
trigger="True",
owner=self.user,
)
UpdateOrCreateTransactionRuleAction.objects.create(
rule=rule,
set_account="account_id",
set_type="'EX'",
set_date="date",
set_reference_date="reference_date",
set_amount="amount",
set_description="'Generated transaction'",
set_category="category_name",
)
run_check_for_transaction_rules_without_worker_wrapper(
instance_id=source_transaction.id,
user_id=self.user.id,
signal="transaction_updated",
)
generated_transaction = Transaction.objects.get(
description="Generated transaction"
)
self.assertIsNone(generated_transaction.category)
+63
View File
@@ -5,6 +5,7 @@ from apps.common.fields.forms.dynamic_select import (
DynamicModelChoiceField,
DynamicModelMultipleChoiceField,
)
from apps.common.middleware.thread_local import get_current_user
from apps.common.widgets.crispy.daisyui import Switch
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.datepicker import AirDatePickerInput, AirMonthYearPickerInput
@@ -13,6 +14,7 @@ from apps.common.widgets.tom_select import TomSelect
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.models import (
InstallmentPlan,
TransactionAttachment,
QuickTransaction,
RecurringTransaction,
Transaction,
@@ -35,6 +37,22 @@ from django.db.models import Q
from django.utils.translation import gettext_lazy as _
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
widget = MultipleFileInput
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
return [single_file_clean(file, initial) for file in data]
if data:
return [single_file_clean(data, initial)]
return []
class TransactionForm(forms.ModelForm):
category = DynamicModelChoiceField(
create_field="name",
@@ -116,6 +134,9 @@ class TransactionForm(forms.ModelForm):
self.fields["account"].queryset = Account.objects.filter(
is_archived=False,
)
user_settings = get_current_user().settings
if user_settings.default_account:
self.fields["account"].initial = user_settings.default_account
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
@@ -243,6 +264,41 @@ class TransactionForm(forms.ModelForm):
return instance
class TransactionAttachmentForm(forms.Form):
attachments = MultipleFileField(
required=True,
label=_("Attachments"),
help_text=_("Files are private and only visible to users with access to this transaction."),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
self.helper.layout = Layout(
"attachments",
FormActions(
NoClassSubmit("submit", _("Upload"), css_class="btn btn-primary"),
),
)
def save(self, transaction, uploaded_by):
created = []
for attachment in self.cleaned_data.get("attachments") or []:
created.append(
TransactionAttachment.objects.create(
transaction=transaction,
file=attachment,
original_name=attachment.name,
content_type=getattr(attachment, "content_type", ""),
size=attachment.size,
uploaded_by=uploaded_by,
)
)
return created
class QuickTransactionForm(forms.ModelForm):
category = DynamicModelChoiceField(
create_field="name",
@@ -768,6 +824,9 @@ class InstallmentPlanForm(forms.ModelForm):
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
user_settings = get_current_user().settings
if user_settings.default_account:
self.fields["account"].initial = user_settings.default_account
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
@@ -1010,6 +1069,10 @@ class RecurringTransactionForm(forms.ModelForm):
).distinct()
else:
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
user_settings = get_current_user().settings
if user_settings.default_account:
self.fields["account"].initial = user_settings.default_account
self.fields["category"].queryset = TransactionCategory.objects.filter(
active=True
@@ -0,0 +1,38 @@
# Generated by Django 5.2.13 on 2026-06-06 02:34
import apps.transactions.models
import apps.transactions.storage
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('transactions', '0048_recurringtransaction_keep_at_most'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='TransactionAttachment',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('file', models.FileField(storage=apps.transactions.storage.PrivateMediaStorage(), upload_to=apps.transactions.models.transaction_attachment_path, verbose_name='File')),
('original_name', models.CharField(max_length=255, verbose_name='Original Name')),
('content_type', models.CharField(blank=True, max_length=255, verbose_name='Content Type')),
('size', models.PositiveBigIntegerField(default=0, verbose_name='Size')),
('created_at', models.DateTimeField(auto_now_add=True)),
('transaction', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='transactions.transaction', verbose_name='Transaction')),
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='transaction_attachments', to=settings.AUTH_USER_MODEL, verbose_name='Uploaded By')),
],
options={
'verbose_name': 'Transaction Attachment',
'verbose_name_plural': 'Transaction Attachments',
'db_table': 'transaction_attachments',
'ordering': ['-created_at', 'original_name'],
},
),
]
+64 -1
View File
@@ -1,6 +1,8 @@
import decimal
import logging
import uuid
from copy import deepcopy
from pathlib import Path
from apps.common.fields.month_year import MonthYearModelField
from apps.common.functions.decimals import truncate_decimal
@@ -13,13 +15,15 @@ from apps.common.models import (
)
from apps.common.templatetags.decimal import drop_trailing_zeros, localize_number
from apps.currencies.utils.convert import convert
from apps.transactions.storage import PrivateMediaStorage
from apps.transactions.validators import validate_decimal_places, validate_non_negative
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Q
from django.dispatch import Signal
from django.db.models.signals import post_delete
from django.dispatch import Signal, receiver
from django.template.defaultfilters import date
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@@ -32,6 +36,11 @@ transaction_updated = Signal()
transaction_deleted = Signal()
def transaction_attachment_path(instance, filename):
extension = Path(filename).suffix
return f"transaction_attachments/{instance.transaction_id}/{instance.id}{extension}"
class SoftDeleteQuerySet(models.QuerySet):
@staticmethod
def _emit_signals(instances, created=False, old_data=None):
@@ -526,6 +535,60 @@ class Transaction(OwnedObject):
return new_obj
class TransactionAttachment(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
transaction = models.ForeignKey(
Transaction,
on_delete=models.CASCADE,
related_name="attachments",
verbose_name=_("Transaction"),
)
file = models.FileField(
upload_to=transaction_attachment_path,
storage=PrivateMediaStorage(),
verbose_name=_("File"),
)
original_name = models.CharField(max_length=255, verbose_name=_("Original Name"))
content_type = models.CharField(
max_length=255, blank=True, verbose_name=_("Content Type")
)
size = models.PositiveBigIntegerField(default=0, verbose_name=_("Size"))
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name="transaction_attachments",
verbose_name=_("Uploaded By"),
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name = _("Transaction Attachment")
verbose_name_plural = _("Transaction Attachments")
db_table = "transaction_attachments"
ordering = ["-created_at", "original_name"]
def save(self, *args, **kwargs):
if self.file:
if not self.original_name:
self.original_name = Path(self.file.name).name
if not self.size:
self.size = self.file.size
if not self.content_type:
self.content_type = getattr(self.file.file, "content_type", "")
super().save(*args, **kwargs)
def __str__(self):
return self.original_name
@receiver(post_delete, sender=TransactionAttachment)
def delete_transaction_attachment_file(sender, instance, **kwargs):
if not instance.file.name:
return
storage = instance.file.storage
if storage.exists(instance.file.name):
storage.delete(instance.file.name)
class InstallmentPlan(models.Model):
class Recurrence(models.TextChoices):
+9
View File
@@ -0,0 +1,9 @@
from django.conf import settings
from django.core.files.storage import FileSystemStorage
class PrivateMediaStorage(FileSystemStorage):
def __init__(self, *args, **kwargs):
kwargs.setdefault("location", settings.ATTACHMENT_MEDIA_ROOT)
kwargs.setdefault("base_url", None)
super().__init__(*args, **kwargs)
@@ -0,0 +1,219 @@
import shutil
import tempfile
from datetime import date
from decimal import Decimal
from pathlib import Path
from apps.accounts.models import Account
from apps.common.middleware.thread_local import delete_current_user, write_current_user
from apps.currencies.models import Currency
from apps.transactions.models import Transaction, TransactionAttachment
from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.urls import reverse
@override_settings(
STORAGES={
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage"
},
},
WHITENOISE_AUTOREFRESH=True,
)
class TransactionAttachmentTests(TestCase):
def setUp(self):
self.attachment_media_root = tempfile.mkdtemp()
self.override_private_media = override_settings(
ATTACHMENT_MEDIA_ROOT=self.attachment_media_root
)
self.override_private_media.enable()
self.addCleanup(self.override_private_media.disable)
self.addCleanup(shutil.rmtree, self.attachment_media_root, ignore_errors=True)
self.attachment_storage = TransactionAttachment._meta.get_field("file").storage
self.original_storage_location = self.attachment_storage._location
self.attachment_storage._location = self.attachment_media_root
self.attachment_storage.__dict__.pop("base_location", None)
self.attachment_storage.__dict__.pop("location", None)
self.addCleanup(self.restore_attachment_storage)
User = get_user_model()
self.user1 = User.objects.create_user(
email="user1@test.com", password="testpass123"
)
self.user2 = User.objects.create_user(
email="user2@test.com", password="testpass123"
)
self.currency = Currency.objects.create(
code="USD", name="US Dollar", decimal_places=2, prefix="$ "
)
self.user1_account = Account.all_objects.create(
name="User1 Account", currency=self.currency, owner=self.user1
)
self.user2_account = Account.all_objects.create(
name="User2 Account", currency=self.currency, owner=self.user2
)
self.transaction = Transaction.userless_all_objects.create(
account=self.user1_account,
type=Transaction.Type.EXPENSE,
amount=Decimal("12.34"),
is_paid=True,
date=date(2026, 6, 5),
description="Receipt transaction",
owner=self.user1,
)
self.other_transaction = Transaction.userless_all_objects.create(
account=self.user2_account,
type=Transaction.Type.EXPENSE,
amount=Decimal("56.78"),
is_paid=True,
date=date(2026, 6, 5),
description="Other receipt transaction",
owner=self.user2,
)
def restore_attachment_storage(self):
self.attachment_storage._location = self.original_storage_location
self.attachment_storage.__dict__.pop("base_location", None)
self.attachment_storage.__dict__.pop("location", None)
def test_attachment_uses_uuid_and_preserves_original_download_name(self):
attachment = TransactionAttachment.objects.create(
transaction=self.transaction,
file=SimpleUploadedFile(
"receipt June.pdf", b"receipt bytes", content_type="application/pdf"
),
uploaded_by=self.user1,
)
self.assertEqual(attachment.original_name, "receipt June.pdf")
self.assertNotIn("receipt June.pdf", attachment.file.name)
self.client.force_login(self.user1)
response = self.client.get(
reverse(
"transaction_attachment_download",
kwargs={"attachment_id": attachment.id},
)
)
self.assertEqual(response.status_code, 200)
self.assertEqual(b"".join(response.streaming_content), b"receipt bytes")
self.assertIn('filename="receipt June.pdf"', response["Content-Disposition"])
def test_user_without_transaction_access_cannot_download_attachment(self):
attachment = TransactionAttachment.objects.create(
transaction=self.other_transaction,
file=SimpleUploadedFile("private.txt", b"private"),
uploaded_by=self.user2,
)
self.client.force_login(self.user1)
response = self.client.get(
reverse(
"transaction_attachment_download",
kwargs={"attachment_id": attachment.id},
)
)
self.assertEqual(response.status_code, 404)
def test_attachment_button_lives_in_transaction_hover_toolbar(self):
template = Path("templates/cotton/transaction/item.html").read_text()
before_toolbar, toolbar = template.split("{# Item actions#}", 1)
self.assertNotIn("transaction_attachments", before_toolbar)
self.assertLess(
toolbar.index("transaction_edit"),
toolbar.index("transaction_attachments"),
)
self.assertLess(
toolbar.index("transaction_attachments"),
toolbar.index("transaction_delete"),
)
def test_transaction_edit_form_does_not_include_attachment_upload(self):
self.client.force_login(self.user1)
response = self.client.get(
reverse("transaction_edit", kwargs={"transaction_id": self.transaction.id}),
HTTP_HX_REQUEST="true",
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "multipart/form-data")
self.assertNotContains(response, 'type="file"')
def test_attachment_management_uploads_multiple_attachments(self):
self.client.force_login(self.user1)
response = self.client.post(
reverse(
"transaction_attachments",
kwargs={"transaction_id": self.transaction.id},
),
{
"attachments": [
SimpleUploadedFile("first.txt", b"first"),
SimpleUploadedFile("second.txt", b"second"),
],
},
HTTP_HX_REQUEST="true",
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "first.txt")
self.assertContains(response, "second.txt")
self.assertEqual(self.transaction.attachments.count(), 2)
def test_attachment_delete_returns_refreshed_attachment_list(self):
attachment = TransactionAttachment.objects.create(
transaction=self.transaction,
file=SimpleUploadedFile("delete-me.txt", b"delete"),
uploaded_by=self.user1,
)
self.client.force_login(self.user1)
response = self.client.delete(
reverse(
"transaction_attachment_delete",
kwargs={"attachment_id": attachment.id},
),
HTTP_HX_REQUEST="true",
)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, "delete-me.txt")
self.assertContains(response, "No attachments yet")
self.assertFalse(
TransactionAttachment.objects.filter(id=attachment.id).exists()
)
def test_hard_deleting_transaction_deletes_attachment_files(self):
attachment = TransactionAttachment.objects.create(
transaction=self.transaction,
file=SimpleUploadedFile("hard-delete.txt", b"delete with transaction"),
uploaded_by=self.user1,
)
file_path = Path(attachment.file.path)
self.assertTrue(file_path.exists())
write_current_user(self.user1)
self.addCleanup(delete_current_user)
self.transaction.delete()
self.assertTrue(file_path.exists())
self.assertTrue(TransactionAttachment.objects.filter(id=attachment.id).exists())
self.transaction.delete()
self.assertFalse(file_path.exists())
self.assertFalse(
TransactionAttachment.objects.filter(id=attachment.id).exists()
)
+20
View File
@@ -81,6 +81,26 @@ urlpatterns = [
views.transaction_move_to_today,
name="transaction_move_to_today",
),
path(
"transaction/<int:transaction_id>/attachments/",
views.transaction_attachments,
name="transaction_attachments",
),
path(
"transaction/<int:transaction_id>/attachments/list/",
views.transaction_attachments_list,
name="transaction_attachments_list",
),
path(
"transaction/attachments/<uuid:attachment_id>/download/",
views.transaction_attachment_download,
name="transaction_attachment_download",
),
path(
"transaction/attachments/<uuid:attachment_id>/delete/",
views.transaction_attachment_delete,
name="transaction_attachment_delete",
),
path(
"transaction/<int:transaction_id>/delete/",
views.transaction_delete,
+102 -14
View File
@@ -1,32 +1,120 @@
import datetime
from copy import deepcopy
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Q, When, Case, Value, IntegerField
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, get_object_or_404
from django.utils import timezone
from django.utils.translation import gettext_lazy as _, ngettext_lazy
from django.views.decorators.http import require_http_methods
from apps.common.decorators.demo import disabled_on_demo
from apps.common.decorators.htmx import only_htmx
from apps.rules.signals import transaction_created, transaction_updated
from apps.transactions.filters import TransactionsFilter
from apps.transactions.forms import (
BulkEditTransactionForm,
TransactionAttachmentForm,
TransactionForm,
TransferForm,
BulkEditTransactionForm,
)
from apps.transactions.models import Transaction
from apps.transactions.models import Transaction, TransactionAttachment
from apps.transactions.utils.calculations import (
calculate_currency_totals,
calculate_account_totals,
calculate_currency_totals,
calculate_percentage_distribution,
)
from apps.transactions.utils.default_ordering import default_order
from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
from django.db.models import Case, IntegerField, Q, Value, When
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ngettext_lazy
from django.views.decorators.http import require_http_methods
def _get_accessible_transaction_or_404(transaction_id):
return get_object_or_404(Transaction.objects, id=transaction_id)
def _get_accessible_attachment_or_404(attachment_id):
attachment = get_object_or_404(
TransactionAttachment.objects.select_related("transaction"),
id=attachment_id,
)
if not Transaction.objects.filter(id=attachment.transaction_id).exists():
raise Http404()
return attachment
@only_htmx
@login_required
@disabled_on_demo
@require_http_methods(["GET", "POST"])
def transaction_attachments(request, transaction_id):
transaction = _get_accessible_transaction_or_404(transaction_id)
if request.method == "POST":
form = TransactionAttachmentForm(request.POST, request.FILES)
if form.is_valid():
form.save(transaction=transaction, uploaded_by=request.user)
messages.success(request, _("Attachment uploaded successfully"))
form = TransactionAttachmentForm()
else:
form = TransactionAttachmentForm()
response = render(
request,
"transactions/fragments/attachments_manage.html",
{"form": form, "transaction": transaction},
)
response["HX-Trigger"] = "toasts, updated"
return response
@only_htmx
@login_required
@disabled_on_demo
@require_http_methods(["GET"])
def transaction_attachments_list(request, transaction_id):
transaction = _get_accessible_transaction_or_404(transaction_id)
return render(
request,
"transactions/fragments/attachments.html",
{"transaction": transaction},
)
@login_required
@disabled_on_demo
@require_http_methods(["GET"])
def transaction_attachment_download(request, attachment_id):
attachment = _get_accessible_attachment_or_404(attachment_id)
return FileResponse(
attachment.file.open("rb"),
as_attachment=False,
filename=attachment.original_name,
content_type=attachment.content_type or "application/octet-stream",
)
@only_htmx
@login_required
@disabled_on_demo
@require_http_methods(["DELETE"])
def transaction_attachment_delete(request, attachment_id):
attachment = _get_accessible_attachment_or_404(attachment_id)
transaction = attachment.transaction
attachment.file.delete(save=False)
attachment.delete()
messages.success(request, _("Attachment deleted successfully"))
response = render(
request,
"transactions/fragments/attachments.html",
{"transaction": transaction},
)
response["HX-Trigger"] = "toasts, updated"
return response
@only_htmx
+75
View File
@@ -0,0 +1,75 @@
import logging
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from django.contrib.auth import get_user_model
User = get_user_model()
logger = logging.getLogger(__name__)
class AutoConnectSocialAccountAdapter(DefaultSocialAccountAdapter):
"""
Custom adapter to automatically connect social accounts to existing users
with the same email address.
SECURITY WARNING:
This adapter automatically connects OIDC accounts to existing local accounts
based on email matching.
If your OIDC provider allows unverified emails, this could lead to
ACCOUNT TAKEOVER attacks where an attacker creates an OIDC account
with someone else's email and gains access to their account.
"""
def pre_social_login(self, request, sociallogin):
"""
Invoked just after a user successfully authenticates via a
social provider, but before the login is actually processed.
If a user with the same email already exists, connect the social
account to that existing user instead of creating a new account.
"""
# If the social account is already connected to a user, do nothing
if sociallogin.is_existing:
return
# Check if we have an email from the social provider
if not sociallogin.email_addresses:
logger.warning(
"OIDC login attempted without email address. "
f"Provider: {sociallogin.account.provider}"
)
return
# Get the email from the social login
email = sociallogin.email_addresses[0].email.lower()
# Try to find an existing user with this email
try:
user = User.objects.get(email__iexact=email)
# Log this connection for security audit trail
logger.info(
f"Auto-connecting OIDC account to existing user. "
f"Email: {email}, Provider: {sociallogin.account.provider}, "
f"User ID: {user.id}"
)
# Connect the social account to the existing user
sociallogin.connect(request, user)
except User.DoesNotExist:
# No user with this email exists, proceed with normal signup flow
logger.debug(
f"No existing user found for email {email}. "
"Proceeding with new account creation."
)
pass
except User.MultipleObjectsReturned:
# Multiple users with the same email (shouldn't happen with unique constraint)
logger.error(
f"Multiple users found with email {email}. "
"This should not happen with unique constraint. "
"Blocking auto-connect."
)
# Let the default behavior handle this
pass
+20
View File
@@ -1,6 +1,8 @@
from apps.common.middleware.thread_local import get_current_user
from apps.common.widgets.crispy.submit import NoClassSubmit
from apps.common.widgets.tom_select import TomSelect
from apps.users.models import UserSettings
from apps.accounts.models import Account
from crispy_forms.bootstrap import (
FormActions,
)
@@ -116,6 +118,15 @@ class UserSettingsForm(forms.ModelForm):
label=_("Number Format"),
)
default_account = forms.ModelChoiceField(
queryset=Account.objects.filter(
is_archived=False,
),
label=_("Default Account"),
widget=TomSelect(clear_button=False, group_by="group"),
required=False,
)
class Meta:
model = UserSettings
fields = [
@@ -126,11 +137,19 @@ class UserSettingsForm(forms.ModelForm):
"datetime_format",
"number_format",
"volume",
"default_account",
]
widgets = {
"default_account": TomSelect(clear_button=False, group_by="group"),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["default_account"].queryset = Account.objects.filter(
is_archived=False,
)
self.helper = FormHelper()
self.helper.form_tag = False
self.helper.form_method = "post"
@@ -143,6 +162,7 @@ class UserSettingsForm(forms.ModelForm):
"number_format",
HTML('<hr class="hr my-3" />'),
"start_page",
"default_account",
HTML('<hr class="hr my-3" />'),
"volume",
FormActions(
@@ -0,0 +1,25 @@
# Generated by Django 5.2.9 on 2026-02-15 21:35
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("accounts", "0016_account_untracked_by"),
("users", "0023_alter_usersettings_timezone"),
]
operations = [
migrations.AddField(
model_name="usersettings",
name="default_account",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="accounts.account",
verbose_name="Default account",
),
),
]
@@ -0,0 +1,20 @@
# Generated by Django 5.2.9 on 2026-02-16 01:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('accounts', '0016_account_untracked_by'),
('users', '0024_usersettings_default_account'),
]
operations = [
migrations.AlterField(
model_name='usersettings',
name='default_account',
field=models.ForeignKey(blank=True, help_text='Selects the account by default when creating new transactions', null=True, on_delete=django.db.models.deletion.SET_NULL, to='accounts.account', verbose_name='Default account'),
),
]
+8
View File
@@ -510,6 +510,14 @@ class UserSettings(models.Model):
default=StartPage.MONTHLY,
verbose_name=_("Start page"),
)
default_account = models.ForeignKey(
"accounts.Account",
on_delete=models.SET_NULL,
verbose_name=_("Default account"),
help_text=_("Selects the account by default when creating new transactions"),
blank=True,
null=True,
)
def __str__(self):
return f"{self.user.email}'s settings"
@@ -0,0 +1,10 @@
settings:
file_type: qif
importing: transactions
encoding: cp1252
date_format: "%d/%m/%Y"
skip_errors: true
mapping: {}
deduplicate: []
@@ -0,0 +1,7 @@
{
"author": "eitchtee",
"description": "Standard QIF Import. Mapping is automatic.",
"schema_version": 1,
"name": "Standard QIF",
"message": "Account is inferred from filename (e.g., 'Checking.qif' -> Account 'Checking').\nYou might need to change the date format to match the date format on your file."
}
File diff suppressed because it is too large Load Diff
+272 -177
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -26,12 +26,12 @@ msgstr ""
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
#: apps/users/forms.py:380
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
#: apps/users/forms.py:400
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
@@ -42,11 +42,11 @@ msgstr ""
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
#: apps/users/forms.py:228 apps/users/forms.py:386
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
#: apps/users/forms.py:248 apps/users/forms.py:406
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr ""
@@ -62,12 +62,12 @@ msgstr ""
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
#: apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
#: apps/transactions/models.py:578 apps/transactions/models.py:778
#: apps/transactions/models.py:1026
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: templates/insights/fragments/month_by_month.html:84
@@ -79,12 +79,12 @@ msgstr ""
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
#: apps/rules/models.py:315 apps/transactions/filters.py:73
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
#: apps/transactions/models.py:580 apps/transactions/models.py:782
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
@@ -96,8 +96,8 @@ msgstr ""
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
#: apps/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:214 apps/transactions/models.py:239
#: apps/transactions/models.py:263 apps/transactions/models.py:994
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -163,11 +163,11 @@ msgstr ""
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
#: apps/transactions/models.py:538 apps/transactions/models.py:760
#: apps/transactions/models.py:1000
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -344,7 +344,7 @@ msgid ""
"owner.<br/>Public: Shown for all users. Only editable by the owner."
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:149
#: apps/common/forms.py:76 apps/users/forms.py:169
msgid "Save"
msgstr ""
@@ -480,8 +480,8 @@ msgstr ""
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
#: templates/dca/fragments/strategy/details.html:49
#: templates/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -567,8 +567,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
#: apps/transactions/models.py:218 apps/transactions/models.py:242
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/fragments/list.html:16
#: templates/recurring_transactions/fragments/list.html:16
@@ -696,11 +696,11 @@ msgstr ""
msgid "Create transaction"
msgstr ""
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
msgid "From Account"
msgstr ""
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
msgid "To Account"
msgstr ""
@@ -725,7 +725,7 @@ msgstr ""
msgid "You must provide an account."
msgstr ""
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
msgid "From and To accounts must be different."
msgstr ""
@@ -744,9 +744,9 @@ msgstr ""
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
#: apps/transactions/models.py:318 apps/transactions/models.py:587
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
#: apps/transactions/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
msgid "Notes"
msgstr ""
@@ -809,7 +809,7 @@ msgid "Users"
msgstr ""
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
#: templates/includes/sidebar.html:142
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -830,11 +830,11 @@ msgstr ""
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
#: apps/rules/models.py:307 apps/transactions/filters.py:78
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
#: apps/transactions/models.py:333 apps/transactions/models.py:583
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -846,14 +846,14 @@ msgid "Entities"
msgstr ""
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -906,7 +906,7 @@ msgstr ""
msgid "Update or create transaction actions"
msgstr ""
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
@@ -1102,16 +1102,16 @@ msgid "Operator"
msgstr ""
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:377
#: apps/transactions/models.py:301 apps/transactions/models.py:543
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
#: apps/rules/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1119,17 +1119,17 @@ msgid "Paid"
msgstr ""
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
#: apps/rules/models.py:283 apps/transactions/forms.py:71
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
#: apps/transactions/models.py:561 apps/transactions/models.py:790
#: apps/rules/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:404
#: apps/transactions/models.py:311 apps/transactions/models.py:771
#: apps/transactions/models.py:1015
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/quick_transactions/fragments/list.html:15
@@ -1139,28 +1139,28 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
#: apps/transactions/models.py:316 apps/transactions/models.py:545
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:355
#: apps/transactions/models.py:1042
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:357
#: apps/transactions/models.py:1044
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:564
#: apps/transactions/models.py:215 apps/transactions/models.py:306
#: apps/transactions/models.py:1010
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
msgid "Mute"
msgstr ""
@@ -1173,7 +1173,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
#: apps/transactions/models.py:378
#: apps/transactions/models.py:387 apps/transactions/models.py:544
msgid "Transaction"
msgstr ""
@@ -1378,96 +1378,110 @@ msgstr ""
msgid "No entity"
msgstr ""
#: apps/transactions/forms.py:170
#: apps/transactions/forms.py:191
msgid "More"
msgstr ""
#: apps/transactions/forms.py:207
#: apps/transactions/forms.py:228
msgid "Save and add similar"
msgstr ""
#: apps/transactions/forms.py:212
#: apps/transactions/forms.py:233
msgid "Save and add another"
msgstr ""
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
#: templates/transactions/fragments/attachments.html:4
msgid "Attachments"
msgstr ""
#: apps/transactions/forms.py:271
msgid ""
"Files are private and only visible to users with access to this transaction."
msgstr ""
#: apps/transactions/forms.py:282
msgid "Upload"
msgstr ""
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
msgid "Muted transactions won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:503
#: apps/transactions/forms.py:559
msgid "From Amount"
msgstr ""
#: apps/transactions/forms.py:508
#: apps/transactions/forms.py:564
msgid "To Amount"
msgstr ""
#: apps/transactions/forms.py:606
#: apps/transactions/forms.py:662
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
msgid "Transfer"
msgstr ""
#: apps/transactions/forms.py:847
#: apps/transactions/forms.py:906
msgid "Tag name"
msgstr ""
#: apps/transactions/forms.py:875
#: apps/transactions/forms.py:934
msgid "Entity name"
msgstr ""
#: apps/transactions/forms.py:903
#: apps/transactions/forms.py:962
msgid "Category name"
msgstr ""
#: apps/transactions/forms.py:905
#: apps/transactions/forms.py:964
msgid "Muted categories won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:1055
#: apps/transactions/forms.py:1118
msgid "future transactions"
msgstr ""
#: apps/transactions/forms.py:1081
#: apps/transactions/forms.py:1144
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:220
#: apps/transactions/models.py:229
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:228
#: apps/transactions/models.py:237
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:238
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:244
#: apps/transactions/models.py:253
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:252 apps/transactions/models.py:253
#: apps/transactions/models.py:261 apps/transactions/models.py:262
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:268
#: apps/transactions/models.py:277
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:276
#: apps/transactions/models.py:285
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
msgstr ""
#: apps/transactions/models.py:288 apps/transactions/models.py:987
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1479,7 +1493,7 @@ msgstr ""
msgid "Income"
msgstr ""
#: apps/transactions/models.py:289 apps/transactions/models.py:988
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1490,129 +1504,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:344 apps/transactions/models.py:600
#: apps/transactions/models.py:353 apps/transactions/models.py:663
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:824
#: apps/transactions/models.py:362 apps/transactions/models.py:887
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:361
#: apps/transactions/models.py:370
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:366
#: apps/transactions/models.py:375
msgid "Deleted At"
msgstr ""
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
msgid "No tags"
msgstr ""
#: apps/transactions/models.py:482
#: apps/transactions/models.py:491
msgid "No category"
msgstr ""
#: apps/transactions/models.py:484
#: apps/transactions/models.py:493
msgid "No description"
msgstr ""
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
#: apps/transactions/models.py:549
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr ""
#: apps/transactions/models.py:533 apps/users/models.py:464
#: apps/transactions/models.py:596 apps/users/models.py:464
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:534
#: apps/transactions/models.py:597
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:535
#: apps/transactions/models.py:598
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:548
#: apps/transactions/models.py:611
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:616
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:554
#: apps/transactions/models.py:617
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:559 apps/transactions/models.py:794
#: apps/transactions/models.py:622 apps/transactions/models.py:857
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:563 apps/transactions/models.py:795
#: apps/transactions/models.py:626 apps/transactions/models.py:858
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:568
#: apps/transactions/models.py:631
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:571
#: apps/transactions/models.py:634
msgid "Installment Amount"
msgstr ""
#: apps/transactions/models.py:590 apps/transactions/models.py:814
#: apps/transactions/models.py:653 apps/transactions/models.py:877
msgid "Add description to transactions"
msgstr ""
#: apps/transactions/models.py:593 apps/transactions/models.py:817
#: apps/transactions/models.py:656 apps/transactions/models.py:880
msgid "Add notes to transactions"
msgstr ""
#: apps/transactions/models.py:753
#: apps/transactions/models.py:816
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:754
#: apps/transactions/models.py:817
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:755
#: apps/transactions/models.py:818
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:756
#: apps/transactions/models.py:819
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:758
#: apps/transactions/models.py:821
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:797
#: apps/transactions/models.py:860
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:800
#: apps/transactions/models.py:863
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:803
#: apps/transactions/models.py:866
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:807
#: apps/transactions/models.py:870
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:810
#: apps/transactions/models.py:873
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1054
#: apps/transactions/models.py:1117
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1621,7 +1663,7 @@ msgstr ""
msgid "Quick Transaction"
msgstr ""
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1726,8 +1768,8 @@ msgid "Item deleted successfully"
msgstr ""
#: apps/transactions/views/quick_transactions.py:156
#: apps/transactions/views/transactions.py:53
#: apps/transactions/views/transactions.py:238
#: apps/transactions/views/transactions.py:141
#: apps/transactions/views/transactions.py:326
msgid "Transaction added successfully"
msgstr ""
@@ -1767,30 +1809,38 @@ msgstr ""
msgid "Tag deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:262
#: apps/transactions/views/transactions.py:59
msgid "Attachment uploaded successfully"
msgstr ""
#: apps/transactions/views/transactions.py:110
msgid "Attachment deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:350
msgid "Transaction updated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:313
#: apps/transactions/views/transactions.py:401
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/transactions.py:349
#: apps/transactions/views/transactions.py:437
msgid "Transaction duplicated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:391
#: apps/transactions/views/transactions.py:479
msgid "Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:409
#: apps/transactions/views/transactions.py:497
msgid "Transaction restored successfully"
msgstr ""
#: apps/transactions/views/transactions.py:435
#: apps/transactions/views/transactions.py:523
msgid "Transfer added successfully"
msgstr ""
@@ -1814,24 +1864,24 @@ msgstr ""
msgid "Important dates"
msgstr ""
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
#: templates/users/login.html:18
msgid "E-mail"
msgstr ""
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
msgid "Password"
msgstr ""
#: apps/users/forms.py:45
#: apps/users/forms.py:47
msgid "Invalid e-mail or password"
msgstr ""
#: apps/users/forms.py:46
#: apps/users/forms.py:48
msgid "This account is deactivated"
msgstr ""
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
#: templates/monthly_overview/pages/overview.html:98
#: templates/monthly_overview/pages/overview.html:245
#: templates/transactions/pages/transactions.html:47
@@ -1839,19 +1889,23 @@ msgstr ""
msgid "Default"
msgstr ""
#: apps/users/forms.py:105 apps/users/models.py:484
#: apps/users/forms.py:107 apps/users/models.py:484
msgid "Date Format"
msgstr ""
#: apps/users/forms.py:110 apps/users/models.py:489
#: apps/users/forms.py:112 apps/users/models.py:489
msgid "Datetime Format"
msgstr ""
#: apps/users/forms.py:116 apps/users/models.py:492
#: apps/users/forms.py:118 apps/users/models.py:492
msgid "Number Format"
msgstr ""
#: apps/users/forms.py:154
#: apps/users/forms.py:125
msgid "Default Account"
msgstr ""
#: apps/users/forms.py:174
#, python-format
msgid ""
"This changes the language (if available) and how numbers and dates are "
@@ -1859,59 +1913,59 @@ msgid ""
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
msgstr ""
#: apps/users/forms.py:163
#: apps/users/forms.py:183
msgid "New Password"
msgstr ""
#: apps/users/forms.py:166
#: apps/users/forms.py:186
msgid "Leave blank to keep the current password."
msgstr ""
#: apps/users/forms.py:169
#: apps/users/forms.py:189
msgid "Confirm New Password"
msgstr ""
#: apps/users/forms.py:181 apps/users/forms.py:338
#: apps/users/forms.py:201 apps/users/forms.py:358
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
#: apps/users/forms.py:184 apps/users/forms.py:341
#: apps/users/forms.py:204 apps/users/forms.py:361
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
#: apps/users/forms.py:251
#: apps/users/forms.py:271
msgid "This email address is already in use by another account."
msgstr ""
#: apps/users/forms.py:259
#: apps/users/forms.py:279
msgid "The two password fields didn't match."
msgstr ""
#: apps/users/forms.py:261
#: apps/users/forms.py:281
msgid "Please confirm your new password."
msgstr ""
#: apps/users/forms.py:263
#: apps/users/forms.py:283
msgid "Please enter the new password first."
msgstr ""
#: apps/users/forms.py:283
#: apps/users/forms.py:303
msgid "You cannot deactivate your own account using this form."
msgstr ""
#: apps/users/forms.py:296
#: apps/users/forms.py:316
msgid "Cannot remove status from the last superuser."
msgstr ""
#: apps/users/forms.py:302
#: apps/users/forms.py:322
msgid "You cannot remove your own superuser status using this form."
msgstr ""
#: apps/users/forms.py:395
#: apps/users/forms.py:415
msgid "A user with this email address already exists."
msgstr ""
@@ -1955,6 +2009,14 @@ msgstr ""
msgid "Start page"
msgstr ""
#: apps/users/models.py:516
msgid "Default account"
msgstr ""
#: apps/users/models.py:517
msgid "Selects the account by default when creating new transactions"
msgstr ""
#: apps/users/views.py:67
msgid "Transaction amounts are now hidden"
msgstr ""
@@ -2049,8 +2111,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:54
#: templates/accounts/fragments/list.html:71
#: templates/categories/fragments/table.html:51
#: templates/cotton/transaction/item.html:158
#: templates/cotton/transaction/item.html:230
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:236
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:82
#: templates/currencies/fragments/list.html:40
@@ -2078,8 +2140,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:58
#: templates/accounts/fragments/list.html:75
#: templates/categories/fragments/table.html:56
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:236
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:242
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:44
@@ -2108,8 +2170,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:59
#: templates/accounts/fragments/list.html:76
#: templates/categories/fragments/table.html:57
#: templates/cotton/transaction/item.html:161
#: templates/cotton/transaction/item.html:237
#: templates/cotton/transaction/item.html:167
#: templates/cotton/transaction/item.html:243
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:45
@@ -2130,8 +2192,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:60
#: templates/accounts/fragments/list.html:77
#: templates/categories/fragments/table.html:58
#: templates/cotton/transaction/item.html:162
#: templates/cotton/transaction/item.html:238
#: templates/cotton/transaction/item.html:168
#: templates/cotton/transaction/item.html:244
#: templates/currencies/fragments/list.html:46
#: templates/dca/fragments/strategy/details.html:77
#: templates/dca/fragments/strategy/list.html:44
@@ -2148,6 +2210,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:65
#: templates/rules/fragments/transaction_rule/view.html:98
#: templates/tags/fragments/table.html:57
#: templates/transactions/fragments/attachments.html:22
msgid "Yes, delete it!"
msgstr ""
@@ -2283,41 +2346,41 @@ msgstr ""
msgid "Select"
msgstr ""
#: templates/cotton/transaction/item.html:175
#: templates/cotton/transaction/item.html:186
#: templates/cotton/transaction/item.html:196
#: templates/cotton/transaction/item.html:181
#: templates/cotton/transaction/item.html:192
#: templates/cotton/transaction/item.html:202
msgid "Show on summaries"
msgstr ""
#: templates/cotton/transaction/item.html:177
#: templates/cotton/transaction/item.html:183
msgid "Controlled by account"
msgstr ""
#: templates/cotton/transaction/item.html:188
#: templates/cotton/transaction/item.html:194
msgid "Controlled by category"
msgstr ""
#: templates/cotton/transaction/item.html:201
#: templates/cotton/transaction/item.html:207
msgid "Hide from summaries"
msgstr ""
#: templates/cotton/transaction/item.html:205
#: templates/cotton/transaction/item.html:211
msgid "Add as quick transaction"
msgstr ""
#: templates/cotton/transaction/item.html:210
#: templates/cotton/transaction/item.html:216
msgid "Move to previous month"
msgstr ""
#: templates/cotton/transaction/item.html:214
#: templates/cotton/transaction/item.html:220
msgid "Move to next month"
msgstr ""
#: templates/cotton/transaction/item.html:217
#: templates/cotton/transaction/item.html:223
msgid "Move to today"
msgstr ""
#: templates/cotton/transaction/item.html:221
#: templates/cotton/transaction/item.html:227
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr ""
@@ -2798,6 +2861,10 @@ msgstr ""
msgid "Try reloading the page or check the console for more information."
msgstr ""
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
msgid "Cancel"
msgstr ""
@@ -2806,6 +2873,18 @@ msgstr ""
msgid "Confirm"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
msgid "Pull down to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
msgid "Release to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
msgid "Refreshing"
msgstr ""
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
msgid "Insights"
msgstr ""
@@ -3449,6 +3528,22 @@ msgstr ""
msgid "Add Installment Plan"
msgstr ""
#: templates/transactions/fragments/attachments.html:20
msgid "Delete this attachment?"
msgstr ""
#: templates/transactions/fragments/attachments.html:21
msgid "This file will be removed from the transaction."
msgstr ""
#: templates/transactions/fragments/attachments.html:30
msgid "No attachments yet"
msgstr ""
#: templates/transactions/fragments/attachments_manage.html:5
msgid "Transaction attachments"
msgstr ""
#: templates/transactions/fragments/bulk_edit.html:5
msgid "Bulk Editing"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+272 -177
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
@@ -25,12 +25,12 @@ msgstr ""
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
#: apps/users/forms.py:380
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
#: apps/users/forms.py:400
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
@@ -41,11 +41,11 @@ msgstr ""
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
#: apps/users/forms.py:228 apps/users/forms.py:386
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
#: apps/users/forms.py:248 apps/users/forms.py:406
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr ""
@@ -61,12 +61,12 @@ msgstr ""
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
#: apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
#: apps/transactions/models.py:578 apps/transactions/models.py:778
#: apps/transactions/models.py:1026
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: templates/insights/fragments/month_by_month.html:84
@@ -78,12 +78,12 @@ msgstr ""
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
#: apps/rules/models.py:315 apps/transactions/filters.py:73
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
#: apps/transactions/models.py:580 apps/transactions/models.py:782
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
@@ -95,8 +95,8 @@ msgstr ""
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
#: apps/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:214 apps/transactions/models.py:239
#: apps/transactions/models.py:263 apps/transactions/models.py:994
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -162,11 +162,11 @@ msgstr ""
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
#: apps/transactions/models.py:538 apps/transactions/models.py:760
#: apps/transactions/models.py:1000
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -343,7 +343,7 @@ msgid ""
"owner.<br/>Public: Shown for all users. Only editable by the owner."
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:149
#: apps/common/forms.py:76 apps/users/forms.py:169
msgid "Save"
msgstr ""
@@ -479,8 +479,8 @@ msgstr ""
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
#: templates/dca/fragments/strategy/details.html:49
#: templates/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -566,8 +566,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
#: apps/transactions/models.py:218 apps/transactions/models.py:242
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/fragments/list.html:16
#: templates/recurring_transactions/fragments/list.html:16
@@ -695,11 +695,11 @@ msgstr ""
msgid "Create transaction"
msgstr ""
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
msgid "From Account"
msgstr ""
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
msgid "To Account"
msgstr ""
@@ -724,7 +724,7 @@ msgstr ""
msgid "You must provide an account."
msgstr ""
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
msgid "From and To accounts must be different."
msgstr ""
@@ -743,9 +743,9 @@ msgstr ""
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
#: apps/transactions/models.py:318 apps/transactions/models.py:587
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
#: apps/transactions/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
msgid "Notes"
msgstr ""
@@ -808,7 +808,7 @@ msgid "Users"
msgstr ""
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
#: templates/includes/sidebar.html:142
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -829,11 +829,11 @@ msgstr ""
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
#: apps/rules/models.py:307 apps/transactions/filters.py:78
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
#: apps/transactions/models.py:333 apps/transactions/models.py:583
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -845,14 +845,14 @@ msgid "Entities"
msgstr ""
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -905,7 +905,7 @@ msgstr ""
msgid "Update or create transaction actions"
msgstr ""
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
@@ -1101,16 +1101,16 @@ msgid "Operator"
msgstr ""
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:377
#: apps/transactions/models.py:301 apps/transactions/models.py:543
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
#: apps/rules/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1118,17 +1118,17 @@ msgid "Paid"
msgstr ""
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
#: apps/rules/models.py:283 apps/transactions/forms.py:71
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
#: apps/transactions/models.py:561 apps/transactions/models.py:790
#: apps/rules/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:404
#: apps/transactions/models.py:311 apps/transactions/models.py:771
#: apps/transactions/models.py:1015
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/quick_transactions/fragments/list.html:15
@@ -1138,28 +1138,28 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
#: apps/transactions/models.py:316 apps/transactions/models.py:545
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:355
#: apps/transactions/models.py:1042
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:357
#: apps/transactions/models.py:1044
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:564
#: apps/transactions/models.py:215 apps/transactions/models.py:306
#: apps/transactions/models.py:1010
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
msgid "Mute"
msgstr ""
@@ -1172,7 +1172,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
#: apps/transactions/models.py:378
#: apps/transactions/models.py:387 apps/transactions/models.py:544
msgid "Transaction"
msgstr ""
@@ -1377,96 +1377,110 @@ msgstr ""
msgid "No entity"
msgstr ""
#: apps/transactions/forms.py:170
#: apps/transactions/forms.py:191
msgid "More"
msgstr ""
#: apps/transactions/forms.py:207
#: apps/transactions/forms.py:228
msgid "Save and add similar"
msgstr ""
#: apps/transactions/forms.py:212
#: apps/transactions/forms.py:233
msgid "Save and add another"
msgstr ""
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
#: templates/transactions/fragments/attachments.html:4
msgid "Attachments"
msgstr ""
#: apps/transactions/forms.py:271
msgid ""
"Files are private and only visible to users with access to this transaction."
msgstr ""
#: apps/transactions/forms.py:282
msgid "Upload"
msgstr ""
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
msgid "Muted transactions won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:503
#: apps/transactions/forms.py:559
msgid "From Amount"
msgstr ""
#: apps/transactions/forms.py:508
#: apps/transactions/forms.py:564
msgid "To Amount"
msgstr ""
#: apps/transactions/forms.py:606
#: apps/transactions/forms.py:662
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
msgid "Transfer"
msgstr ""
#: apps/transactions/forms.py:847
#: apps/transactions/forms.py:906
msgid "Tag name"
msgstr ""
#: apps/transactions/forms.py:875
#: apps/transactions/forms.py:934
msgid "Entity name"
msgstr ""
#: apps/transactions/forms.py:903
#: apps/transactions/forms.py:962
msgid "Category name"
msgstr ""
#: apps/transactions/forms.py:905
#: apps/transactions/forms.py:964
msgid "Muted categories won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:1055
#: apps/transactions/forms.py:1118
msgid "future transactions"
msgstr ""
#: apps/transactions/forms.py:1081
#: apps/transactions/forms.py:1144
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:220
#: apps/transactions/models.py:229
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:228
#: apps/transactions/models.py:237
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:238
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:244
#: apps/transactions/models.py:253
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:252 apps/transactions/models.py:253
#: apps/transactions/models.py:261 apps/transactions/models.py:262
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:268
#: apps/transactions/models.py:277
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:276
#: apps/transactions/models.py:285
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
msgstr ""
#: apps/transactions/models.py:288 apps/transactions/models.py:987
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1478,7 +1492,7 @@ msgstr ""
msgid "Income"
msgstr ""
#: apps/transactions/models.py:289 apps/transactions/models.py:988
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1489,129 +1503,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:344 apps/transactions/models.py:600
#: apps/transactions/models.py:353 apps/transactions/models.py:663
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:824
#: apps/transactions/models.py:362 apps/transactions/models.py:887
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:361
#: apps/transactions/models.py:370
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:366
#: apps/transactions/models.py:375
msgid "Deleted At"
msgstr ""
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
msgid "No tags"
msgstr ""
#: apps/transactions/models.py:482
#: apps/transactions/models.py:491
msgid "No category"
msgstr ""
#: apps/transactions/models.py:484
#: apps/transactions/models.py:493
msgid "No description"
msgstr ""
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
#: apps/transactions/models.py:549
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr ""
#: apps/transactions/models.py:533 apps/users/models.py:464
#: apps/transactions/models.py:596 apps/users/models.py:464
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:534
#: apps/transactions/models.py:597
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:535
#: apps/transactions/models.py:598
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:548
#: apps/transactions/models.py:611
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:616
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:554
#: apps/transactions/models.py:617
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:559 apps/transactions/models.py:794
#: apps/transactions/models.py:622 apps/transactions/models.py:857
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:563 apps/transactions/models.py:795
#: apps/transactions/models.py:626 apps/transactions/models.py:858
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:568
#: apps/transactions/models.py:631
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:571
#: apps/transactions/models.py:634
msgid "Installment Amount"
msgstr ""
#: apps/transactions/models.py:590 apps/transactions/models.py:814
#: apps/transactions/models.py:653 apps/transactions/models.py:877
msgid "Add description to transactions"
msgstr ""
#: apps/transactions/models.py:593 apps/transactions/models.py:817
#: apps/transactions/models.py:656 apps/transactions/models.py:880
msgid "Add notes to transactions"
msgstr ""
#: apps/transactions/models.py:753
#: apps/transactions/models.py:816
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:754
#: apps/transactions/models.py:817
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:755
#: apps/transactions/models.py:818
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:756
#: apps/transactions/models.py:819
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:758
#: apps/transactions/models.py:821
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:797
#: apps/transactions/models.py:860
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:800
#: apps/transactions/models.py:863
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:803
#: apps/transactions/models.py:866
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:807
#: apps/transactions/models.py:870
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:810
#: apps/transactions/models.py:873
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1054
#: apps/transactions/models.py:1117
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1620,7 +1662,7 @@ msgstr ""
msgid "Quick Transaction"
msgstr ""
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1725,8 +1767,8 @@ msgid "Item deleted successfully"
msgstr ""
#: apps/transactions/views/quick_transactions.py:156
#: apps/transactions/views/transactions.py:53
#: apps/transactions/views/transactions.py:238
#: apps/transactions/views/transactions.py:141
#: apps/transactions/views/transactions.py:326
msgid "Transaction added successfully"
msgstr ""
@@ -1766,30 +1808,38 @@ msgstr ""
msgid "Tag deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:262
#: apps/transactions/views/transactions.py:59
msgid "Attachment uploaded successfully"
msgstr ""
#: apps/transactions/views/transactions.py:110
msgid "Attachment deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:350
msgid "Transaction updated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:313
#: apps/transactions/views/transactions.py:401
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/transactions.py:349
#: apps/transactions/views/transactions.py:437
msgid "Transaction duplicated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:391
#: apps/transactions/views/transactions.py:479
msgid "Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:409
#: apps/transactions/views/transactions.py:497
msgid "Transaction restored successfully"
msgstr ""
#: apps/transactions/views/transactions.py:435
#: apps/transactions/views/transactions.py:523
msgid "Transfer added successfully"
msgstr ""
@@ -1813,24 +1863,24 @@ msgstr ""
msgid "Important dates"
msgstr ""
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
#: templates/users/login.html:18
msgid "E-mail"
msgstr ""
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
msgid "Password"
msgstr ""
#: apps/users/forms.py:45
#: apps/users/forms.py:47
msgid "Invalid e-mail or password"
msgstr ""
#: apps/users/forms.py:46
#: apps/users/forms.py:48
msgid "This account is deactivated"
msgstr ""
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
#: templates/monthly_overview/pages/overview.html:98
#: templates/monthly_overview/pages/overview.html:245
#: templates/transactions/pages/transactions.html:47
@@ -1838,19 +1888,23 @@ msgstr ""
msgid "Default"
msgstr ""
#: apps/users/forms.py:105 apps/users/models.py:484
#: apps/users/forms.py:107 apps/users/models.py:484
msgid "Date Format"
msgstr ""
#: apps/users/forms.py:110 apps/users/models.py:489
#: apps/users/forms.py:112 apps/users/models.py:489
msgid "Datetime Format"
msgstr ""
#: apps/users/forms.py:116 apps/users/models.py:492
#: apps/users/forms.py:118 apps/users/models.py:492
msgid "Number Format"
msgstr ""
#: apps/users/forms.py:154
#: apps/users/forms.py:125
msgid "Default Account"
msgstr ""
#: apps/users/forms.py:174
#, python-format
msgid ""
"This changes the language (if available) and how numbers and dates are "
@@ -1858,59 +1912,59 @@ msgid ""
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
msgstr ""
#: apps/users/forms.py:163
#: apps/users/forms.py:183
msgid "New Password"
msgstr ""
#: apps/users/forms.py:166
#: apps/users/forms.py:186
msgid "Leave blank to keep the current password."
msgstr ""
#: apps/users/forms.py:169
#: apps/users/forms.py:189
msgid "Confirm New Password"
msgstr ""
#: apps/users/forms.py:181 apps/users/forms.py:338
#: apps/users/forms.py:201 apps/users/forms.py:358
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
#: apps/users/forms.py:184 apps/users/forms.py:341
#: apps/users/forms.py:204 apps/users/forms.py:361
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
#: apps/users/forms.py:251
#: apps/users/forms.py:271
msgid "This email address is already in use by another account."
msgstr ""
#: apps/users/forms.py:259
#: apps/users/forms.py:279
msgid "The two password fields didn't match."
msgstr ""
#: apps/users/forms.py:261
#: apps/users/forms.py:281
msgid "Please confirm your new password."
msgstr ""
#: apps/users/forms.py:263
#: apps/users/forms.py:283
msgid "Please enter the new password first."
msgstr ""
#: apps/users/forms.py:283
#: apps/users/forms.py:303
msgid "You cannot deactivate your own account using this form."
msgstr ""
#: apps/users/forms.py:296
#: apps/users/forms.py:316
msgid "Cannot remove status from the last superuser."
msgstr ""
#: apps/users/forms.py:302
#: apps/users/forms.py:322
msgid "You cannot remove your own superuser status using this form."
msgstr ""
#: apps/users/forms.py:395
#: apps/users/forms.py:415
msgid "A user with this email address already exists."
msgstr ""
@@ -1954,6 +2008,14 @@ msgstr ""
msgid "Start page"
msgstr ""
#: apps/users/models.py:516
msgid "Default account"
msgstr ""
#: apps/users/models.py:517
msgid "Selects the account by default when creating new transactions"
msgstr ""
#: apps/users/views.py:67
msgid "Transaction amounts are now hidden"
msgstr ""
@@ -2048,8 +2110,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:54
#: templates/accounts/fragments/list.html:71
#: templates/categories/fragments/table.html:51
#: templates/cotton/transaction/item.html:158
#: templates/cotton/transaction/item.html:230
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:236
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:82
#: templates/currencies/fragments/list.html:40
@@ -2077,8 +2139,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:58
#: templates/accounts/fragments/list.html:75
#: templates/categories/fragments/table.html:56
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:236
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:242
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:44
@@ -2107,8 +2169,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:59
#: templates/accounts/fragments/list.html:76
#: templates/categories/fragments/table.html:57
#: templates/cotton/transaction/item.html:161
#: templates/cotton/transaction/item.html:237
#: templates/cotton/transaction/item.html:167
#: templates/cotton/transaction/item.html:243
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:45
@@ -2129,8 +2191,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:60
#: templates/accounts/fragments/list.html:77
#: templates/categories/fragments/table.html:58
#: templates/cotton/transaction/item.html:162
#: templates/cotton/transaction/item.html:238
#: templates/cotton/transaction/item.html:168
#: templates/cotton/transaction/item.html:244
#: templates/currencies/fragments/list.html:46
#: templates/dca/fragments/strategy/details.html:77
#: templates/dca/fragments/strategy/list.html:44
@@ -2147,6 +2209,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:65
#: templates/rules/fragments/transaction_rule/view.html:98
#: templates/tags/fragments/table.html:57
#: templates/transactions/fragments/attachments.html:22
msgid "Yes, delete it!"
msgstr ""
@@ -2282,41 +2345,41 @@ msgstr ""
msgid "Select"
msgstr ""
#: templates/cotton/transaction/item.html:175
#: templates/cotton/transaction/item.html:186
#: templates/cotton/transaction/item.html:196
#: templates/cotton/transaction/item.html:181
#: templates/cotton/transaction/item.html:192
#: templates/cotton/transaction/item.html:202
msgid "Show on summaries"
msgstr ""
#: templates/cotton/transaction/item.html:177
#: templates/cotton/transaction/item.html:183
msgid "Controlled by account"
msgstr ""
#: templates/cotton/transaction/item.html:188
#: templates/cotton/transaction/item.html:194
msgid "Controlled by category"
msgstr ""
#: templates/cotton/transaction/item.html:201
#: templates/cotton/transaction/item.html:207
msgid "Hide from summaries"
msgstr ""
#: templates/cotton/transaction/item.html:205
#: templates/cotton/transaction/item.html:211
msgid "Add as quick transaction"
msgstr ""
#: templates/cotton/transaction/item.html:210
#: templates/cotton/transaction/item.html:216
msgid "Move to previous month"
msgstr ""
#: templates/cotton/transaction/item.html:214
#: templates/cotton/transaction/item.html:220
msgid "Move to next month"
msgstr ""
#: templates/cotton/transaction/item.html:217
#: templates/cotton/transaction/item.html:223
msgid "Move to today"
msgstr ""
#: templates/cotton/transaction/item.html:221
#: templates/cotton/transaction/item.html:227
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr ""
@@ -2796,6 +2859,10 @@ msgstr ""
msgid "Try reloading the page or check the console for more information."
msgstr ""
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
msgid "Cancel"
msgstr ""
@@ -2804,6 +2871,18 @@ msgstr ""
msgid "Confirm"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
msgid "Pull down to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
msgid "Release to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
msgid "Refreshing"
msgstr ""
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
msgid "Insights"
msgstr ""
@@ -3447,6 +3526,22 @@ msgstr ""
msgid "Add Installment Plan"
msgstr ""
#: templates/transactions/fragments/attachments.html:20
msgid "Delete this attachment?"
msgstr ""
#: templates/transactions/fragments/attachments.html:21
msgid "This file will be removed from the transaction."
msgstr ""
#: templates/transactions/fragments/attachments.html:30
msgid "No attachments yet"
msgstr ""
#: templates/transactions/fragments/attachments_manage.html:5
msgid "Transaction attachments"
msgstr ""
#: templates/transactions/fragments/bulk_edit.html:5
msgid "Bulk Editing"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+272 -177
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-10 20:50+0000\n"
"POT-Creation-Date: 2026-06-06 07:41+0000\n"
"PO-Revision-Date: 2025-04-14 06:16+0000\n"
"Last-Translator: Emil <emil.bjorkroth@gmail.com>\n"
"Language-Team: Swedish <https://translations.herculino.com/projects/wygiwyh/"
@@ -27,12 +27,12 @@ msgstr ""
#: apps/currencies/forms.py:53 apps/currencies/forms.py:87
#: apps/currencies/forms.py:136 apps/dca/forms.py:46 apps/dca/forms.py:205
#: apps/import_app/forms.py:32 apps/rules/forms.py:60 apps/rules/forms.py:100
#: apps/rules/forms.py:385 apps/transactions/forms.py:197
#: apps/transactions/forms.py:361 apps/transactions/forms.py:480
#: apps/transactions/forms.py:821 apps/transactions/forms.py:860
#: apps/transactions/forms.py:888 apps/transactions/forms.py:919
#: apps/transactions/forms.py:1065 apps/users/forms.py:222
#: apps/users/forms.py:380
#: apps/rules/forms.py:385 apps/transactions/forms.py:218
#: apps/transactions/forms.py:417 apps/transactions/forms.py:536
#: apps/transactions/forms.py:880 apps/transactions/forms.py:919
#: apps/transactions/forms.py:947 apps/transactions/forms.py:978
#: apps/transactions/forms.py:1128 apps/users/forms.py:242
#: apps/users/forms.py:400
#: templates/rules/fragments/transaction_rule/dry_run/updated.html:5
#: templates/rules/fragments/transaction_rule/view.html:128
msgid "Update"
@@ -43,11 +43,11 @@ msgstr "Uppdatera"
#: apps/currencies/forms.py:93 apps/currencies/forms.py:142
#: apps/dca/forms.py:52 apps/dca/forms.py:211 apps/import_app/forms.py:38
#: apps/rules/forms.py:66 apps/rules/forms.py:106 apps/rules/forms.py:391
#: apps/transactions/forms.py:184 apps/transactions/forms.py:204
#: apps/transactions/forms.py:368 apps/transactions/forms.py:827
#: apps/transactions/forms.py:866 apps/transactions/forms.py:894
#: apps/transactions/forms.py:925 apps/transactions/forms.py:1071
#: apps/users/forms.py:228 apps/users/forms.py:386
#: apps/transactions/forms.py:205 apps/transactions/forms.py:225
#: apps/transactions/forms.py:424 apps/transactions/forms.py:886
#: apps/transactions/forms.py:925 apps/transactions/forms.py:953
#: apps/transactions/forms.py:984 apps/transactions/forms.py:1134
#: apps/users/forms.py:248 apps/users/forms.py:406
#: templates/mini_tools/unit_price_calculator.html:168
msgid "Add"
msgstr ""
@@ -63,12 +63,12 @@ msgstr ""
#: apps/accounts/forms.py:125 apps/dca/forms.py:79 apps/dca/forms.py:86
#: apps/insights/forms.py:117 apps/rules/forms.py:181 apps/rules/forms.py:197
#: apps/rules/models.py:44 apps/rules/models.py:311
#: apps/transactions/forms.py:43 apps/transactions/forms.py:251
#: apps/transactions/forms.py:419 apps/transactions/forms.py:516
#: apps/transactions/forms.py:523 apps/transactions/forms.py:707
#: apps/transactions/forms.py:948 apps/transactions/models.py:322
#: apps/transactions/models.py:578 apps/transactions/models.py:778
#: apps/transactions/models.py:1026
#: apps/transactions/forms.py:61 apps/transactions/forms.py:307
#: apps/transactions/forms.py:475 apps/transactions/forms.py:572
#: apps/transactions/forms.py:579 apps/transactions/forms.py:763
#: apps/transactions/forms.py:1007 apps/transactions/models.py:331
#: apps/transactions/models.py:641 apps/transactions/models.py:841
#: apps/transactions/models.py:1089
#: templates/insights/fragments/category_overview/index.html:86
#: templates/insights/fragments/category_overview/index.html:542
#: templates/insights/fragments/month_by_month.html:84
@@ -80,12 +80,12 @@ msgstr ""
#: apps/export_app/forms.py:43 apps/export_app/forms.py:132
#: apps/rules/forms.py:184 apps/rules/forms.py:194 apps/rules/models.py:45
#: apps/rules/models.py:315 apps/transactions/filters.py:73
#: apps/transactions/forms.py:51 apps/transactions/forms.py:259
#: apps/transactions/forms.py:427 apps/transactions/forms.py:532
#: apps/transactions/forms.py:540 apps/transactions/forms.py:700
#: apps/transactions/forms.py:941 apps/transactions/models.py:328
#: apps/transactions/models.py:580 apps/transactions/models.py:782
#: apps/transactions/models.py:1032 templates/includes/sidebar.html:150
#: apps/transactions/forms.py:69 apps/transactions/forms.py:315
#: apps/transactions/forms.py:483 apps/transactions/forms.py:588
#: apps/transactions/forms.py:596 apps/transactions/forms.py:756
#: apps/transactions/forms.py:1000 apps/transactions/models.py:337
#: apps/transactions/models.py:643 apps/transactions/models.py:845
#: apps/transactions/models.py:1095 templates/includes/sidebar.html:150
#: templates/insights/fragments/category_overview/index.html:40
#: templates/insights/fragments/month_by_month.html:29
#: templates/insights/fragments/month_by_month.html:32
@@ -97,8 +97,8 @@ msgstr ""
#: apps/accounts/models.py:12 apps/accounts/models.py:29 apps/dca/models.py:13
#: apps/import_app/models.py:14 apps/rules/models.py:13
#: apps/transactions/models.py:214 apps/transactions/models.py:239
#: apps/transactions/models.py:263 apps/transactions/models.py:994
#: apps/transactions/models.py:223 apps/transactions/models.py:248
#: apps/transactions/models.py:272 apps/transactions/models.py:1057
#: templates/account_groups/fragments/list.html:22
#: templates/accounts/fragments/list.html:22
#: templates/categories/fragments/table.html:17
@@ -164,11 +164,11 @@ msgstr ""
#: apps/accounts/models.py:75 apps/rules/forms.py:173 apps/rules/forms.py:187
#: apps/rules/models.py:35 apps/rules/models.py:267
#: apps/transactions/forms.py:63 apps/transactions/forms.py:271
#: apps/transactions/forms.py:386 apps/transactions/forms.py:692
#: apps/transactions/forms.py:933 apps/transactions/models.py:294
#: apps/transactions/models.py:538 apps/transactions/models.py:760
#: apps/transactions/models.py:1000
#: apps/transactions/forms.py:81 apps/transactions/forms.py:327
#: apps/transactions/forms.py:442 apps/transactions/forms.py:748
#: apps/transactions/forms.py:992 apps/transactions/models.py:303
#: apps/transactions/models.py:601 apps/transactions/models.py:823
#: apps/transactions/models.py:1063
#: templates/installment_plans/fragments/table.html:17
#: templates/quick_transactions/fragments/list.html:14
#: templates/recurring_transactions/fragments/table.html:19
@@ -345,7 +345,7 @@ msgid ""
"owner.<br/>Public: Shown for all users. Only editable by the owner."
msgstr ""
#: apps/common/forms.py:76 apps/users/forms.py:149
#: apps/common/forms.py:76 apps/users/forms.py:169
msgid "Save"
msgstr ""
@@ -481,8 +481,8 @@ msgstr ""
#: apps/currencies/forms.py:66 apps/dca/models.py:158 apps/rules/forms.py:176
#: apps/rules/forms.py:190 apps/rules/models.py:38 apps/rules/models.py:279
#: apps/transactions/forms.py:67 apps/transactions/forms.py:391
#: apps/transactions/forms.py:544 apps/transactions/models.py:304
#: apps/transactions/forms.py:85 apps/transactions/forms.py:447
#: apps/transactions/forms.py:600 apps/transactions/models.py:313
#: templates/dca/fragments/strategy/details.html:49
#: templates/exchange_rates/fragments/table.html:10
#: templates/exchange_rates_services/fragments/table.html:11
@@ -568,8 +568,8 @@ msgid "Service Type"
msgstr ""
#: apps/currencies/models.py:118 apps/transactions/filters.py:27
#: apps/transactions/models.py:218 apps/transactions/models.py:242
#: apps/transactions/models.py:266 templates/categories/fragments/list.html:16
#: apps/transactions/models.py:227 apps/transactions/models.py:251
#: apps/transactions/models.py:275 templates/categories/fragments/list.html:16
#: templates/entities/fragments/list.html:16
#: templates/installment_plans/fragments/list.html:16
#: templates/recurring_transactions/fragments/list.html:16
@@ -697,11 +697,11 @@ msgstr ""
msgid "Create transaction"
msgstr ""
#: apps/dca/forms.py:64 apps/transactions/forms.py:491
#: apps/dca/forms.py:64 apps/transactions/forms.py:547
msgid "From Account"
msgstr ""
#: apps/dca/forms.py:70 apps/transactions/forms.py:496
#: apps/dca/forms.py:70 apps/transactions/forms.py:552
msgid "To Account"
msgstr ""
@@ -726,7 +726,7 @@ msgstr ""
msgid "You must provide an account."
msgstr ""
#: apps/dca/forms.py:290 apps/transactions/forms.py:638
#: apps/dca/forms.py:290 apps/transactions/forms.py:694
msgid "From and To accounts must be different."
msgstr ""
@@ -745,9 +745,9 @@ msgstr ""
#: apps/dca/models.py:26 apps/dca/models.py:181 apps/rules/forms.py:180
#: apps/rules/forms.py:196 apps/rules/models.py:43 apps/rules/models.py:295
#: apps/transactions/forms.py:413 apps/transactions/forms.py:560
#: apps/transactions/models.py:318 apps/transactions/models.py:587
#: apps/transactions/models.py:788 apps/transactions/models.py:1022
#: apps/transactions/forms.py:469 apps/transactions/forms.py:616
#: apps/transactions/models.py:327 apps/transactions/models.py:650
#: apps/transactions/models.py:851 apps/transactions/models.py:1085
msgid "Notes"
msgstr ""
@@ -810,7 +810,7 @@ msgid "Users"
msgstr ""
#: apps/export_app/forms.py:31 apps/export_app/forms.py:134
#: apps/transactions/models.py:379 templates/includes/sidebar.html:81
#: apps/transactions/models.py:388 templates/includes/sidebar.html:81
#: templates/includes/sidebar.html:142
#: templates/recurring_transactions/fragments/list_transactions.html:5
#: templates/recurring_transactions/fragments/table.html:37
@@ -831,11 +831,11 @@ msgstr ""
#: apps/export_app/forms.py:49 apps/export_app/forms.py:133
#: apps/rules/forms.py:185 apps/rules/forms.py:195 apps/rules/models.py:46
#: apps/rules/models.py:307 apps/transactions/filters.py:78
#: apps/transactions/forms.py:59 apps/transactions/forms.py:267
#: apps/transactions/forms.py:435 apps/transactions/forms.py:715
#: apps/transactions/forms.py:956 apps/transactions/models.py:277
#: apps/transactions/models.py:333 apps/transactions/models.py:583
#: apps/transactions/models.py:785 apps/transactions/models.py:1037
#: apps/transactions/forms.py:77 apps/transactions/forms.py:323
#: apps/transactions/forms.py:491 apps/transactions/forms.py:771
#: apps/transactions/forms.py:1015 apps/transactions/models.py:286
#: apps/transactions/models.py:342 apps/transactions/models.py:646
#: apps/transactions/models.py:848 apps/transactions/models.py:1100
#: templates/entities/fragments/list.html:9
#: templates/entities/pages/index.html:4 templates/includes/sidebar.html:156
#: templates/insights/fragments/category_overview/index.html:54
@@ -847,14 +847,14 @@ msgid "Entities"
msgstr ""
#: apps/export_app/forms.py:55 apps/export_app/forms.py:137
#: apps/transactions/models.py:825 templates/includes/sidebar.html:110
#: apps/transactions/models.py:888 templates/includes/sidebar.html:110
#: templates/recurring_transactions/fragments/list.html:9
#: templates/recurring_transactions/pages/index.html:4
msgid "Recurring Transactions"
msgstr ""
#: apps/export_app/forms.py:61 apps/export_app/forms.py:135
#: apps/transactions/models.py:601 templates/includes/sidebar.html:104
#: apps/transactions/models.py:664 templates/includes/sidebar.html:104
#: templates/installment_plans/fragments/list.html:9
#: templates/installment_plans/pages/index.html:4
msgid "Installment Plans"
@@ -907,7 +907,7 @@ msgstr ""
msgid "Update or create transaction actions"
msgstr ""
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:224
#: apps/export_app/forms.py:181 templates/cotton/transaction/item.html:230
#: templates/cotton/ui/deleted_transactions_action_bar.html:53
#: templates/export_app/fragments/restore.html:5
#: templates/export_app/pages/index.html:19
@@ -1103,16 +1103,16 @@ msgid "Operator"
msgstr ""
#: apps/rules/forms.py:174 apps/rules/forms.py:188 apps/rules/models.py:36
#: apps/rules/models.py:271 apps/transactions/forms.py:377
#: apps/transactions/models.py:301 apps/transactions/models.py:543
#: apps/transactions/models.py:766 apps/transactions/models.py:1007
#: apps/rules/models.py:271 apps/transactions/forms.py:433
#: apps/transactions/models.py:310 apps/transactions/models.py:606
#: apps/transactions/models.py:829 apps/transactions/models.py:1070
msgid "Type"
msgstr ""
#: apps/rules/forms.py:175 apps/rules/forms.py:189 apps/rules/models.py:37
#: apps/rules/models.py:275 apps/transactions/filters.py:22
#: apps/transactions/forms.py:381 apps/transactions/models.py:303
#: apps/transactions/models.py:1009 templates/cotton/transaction/item.html:20
#: apps/transactions/forms.py:437 apps/transactions/models.py:312
#: apps/transactions/models.py:1072 templates/cotton/transaction/item.html:20
#: templates/cotton/transaction/item.html:31
#: templates/transactions/widgets/paid_toggle_button.html:10
#: templates/transactions/widgets/unselectable_paid_toggle_button.html:13
@@ -1120,17 +1120,17 @@ msgid "Paid"
msgstr ""
#: apps/rules/forms.py:177 apps/rules/forms.py:191 apps/rules/models.py:39
#: apps/rules/models.py:283 apps/transactions/forms.py:71
#: apps/transactions/forms.py:397 apps/transactions/forms.py:547
#: apps/transactions/forms.py:721 apps/transactions/models.py:305
#: apps/transactions/models.py:561 apps/transactions/models.py:790
#: apps/rules/models.py:283 apps/transactions/forms.py:89
#: apps/transactions/forms.py:453 apps/transactions/forms.py:603
#: apps/transactions/forms.py:777 apps/transactions/models.py:314
#: apps/transactions/models.py:624 apps/transactions/models.py:853
msgid "Reference Date"
msgstr ""
#: apps/rules/forms.py:178 apps/rules/forms.py:192 apps/rules/models.py:41
#: apps/rules/models.py:287 apps/transactions/forms.py:404
#: apps/transactions/models.py:311 apps/transactions/models.py:771
#: apps/transactions/models.py:1015
#: apps/rules/models.py:287 apps/transactions/forms.py:460
#: apps/transactions/models.py:320 apps/transactions/models.py:834
#: apps/transactions/models.py:1078
#: templates/insights/fragments/sankey.html:102
#: templates/installment_plans/fragments/table.html:18
#: templates/quick_transactions/fragments/list.html:15
@@ -1140,28 +1140,28 @@ msgstr ""
#: apps/rules/forms.py:179 apps/rules/forms.py:193 apps/rules/models.py:14
#: apps/rules/models.py:42 apps/rules/models.py:291
#: apps/transactions/forms.py:408 apps/transactions/forms.py:551
#: apps/transactions/models.py:316 apps/transactions/models.py:545
#: apps/transactions/models.py:774 apps/transactions/models.py:1020
#: apps/transactions/forms.py:464 apps/transactions/forms.py:607
#: apps/transactions/models.py:325 apps/transactions/models.py:608
#: apps/transactions/models.py:837 apps/transactions/models.py:1083
msgid "Description"
msgstr ""
#: apps/rules/forms.py:182 apps/rules/forms.py:198 apps/rules/models.py:47
#: apps/rules/models.py:299 apps/transactions/models.py:355
#: apps/transactions/models.py:1042
#: apps/rules/models.py:299 apps/transactions/models.py:364
#: apps/transactions/models.py:1105
msgid "Internal Note"
msgstr ""
#: apps/rules/forms.py:183 apps/rules/forms.py:199 apps/rules/models.py:48
#: apps/rules/models.py:303 apps/transactions/models.py:357
#: apps/transactions/models.py:1044
#: apps/rules/models.py:303 apps/transactions/models.py:366
#: apps/transactions/models.py:1107
msgid "Internal ID"
msgstr ""
#: apps/rules/forms.py:186 apps/rules/forms.py:200 apps/rules/models.py:40
#: apps/rules/models.py:319 apps/transactions/forms.py:564
#: apps/transactions/models.py:215 apps/transactions/models.py:306
#: apps/transactions/models.py:1010
#: apps/rules/models.py:319 apps/transactions/forms.py:620
#: apps/transactions/models.py:224 apps/transactions/models.py:315
#: apps/transactions/models.py:1073
msgid "Mute"
msgstr ""
@@ -1174,7 +1174,7 @@ msgid "Set Values"
msgstr ""
#: apps/rules/forms.py:407 apps/rules/forms.py:442 apps/rules/forms.py:477
#: apps/transactions/models.py:378
#: apps/transactions/models.py:387 apps/transactions/models.py:544
msgid "Transaction"
msgstr ""
@@ -1379,96 +1379,110 @@ msgstr ""
msgid "No entity"
msgstr ""
#: apps/transactions/forms.py:170
#: apps/transactions/forms.py:191
msgid "More"
msgstr ""
#: apps/transactions/forms.py:207
#: apps/transactions/forms.py:228
msgid "Save and add similar"
msgstr ""
#: apps/transactions/forms.py:212
#: apps/transactions/forms.py:233
msgid "Save and add another"
msgstr ""
#: apps/transactions/forms.py:295 apps/transactions/forms.py:567
#: apps/transactions/forms.py:270 templates/cotton/transaction/item.html:158
#: templates/transactions/fragments/attachments.html:4
msgid "Attachments"
msgstr ""
#: apps/transactions/forms.py:271
msgid ""
"Files are private and only visible to users with access to this transaction."
msgstr ""
#: apps/transactions/forms.py:282
msgid "Upload"
msgstr ""
#: apps/transactions/forms.py:351 apps/transactions/forms.py:623
msgid "Muted transactions won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:503
#: apps/transactions/forms.py:559
msgid "From Amount"
msgstr ""
#: apps/transactions/forms.py:508
#: apps/transactions/forms.py:564
msgid "To Amount"
msgstr ""
#: apps/transactions/forms.py:606
#: apps/transactions/forms.py:662
#: templates/cotton/ui/quick_transactions_buttons.html:40
#: templates/cotton/ui/transactions_fab.html:44
msgid "Transfer"
msgstr ""
#: apps/transactions/forms.py:847
#: apps/transactions/forms.py:906
msgid "Tag name"
msgstr ""
#: apps/transactions/forms.py:875
#: apps/transactions/forms.py:934
msgid "Entity name"
msgstr ""
#: apps/transactions/forms.py:903
#: apps/transactions/forms.py:962
msgid "Category name"
msgstr ""
#: apps/transactions/forms.py:905
#: apps/transactions/forms.py:964
msgid "Muted categories won't be displayed on monthly summaries"
msgstr ""
#: apps/transactions/forms.py:1055
#: apps/transactions/forms.py:1118
msgid "future transactions"
msgstr ""
#: apps/transactions/forms.py:1081
#: apps/transactions/forms.py:1144
msgid "End date should be after the start date"
msgstr ""
#: apps/transactions/models.py:220
#: apps/transactions/models.py:229
msgid ""
"Deactivated categories won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:228
#: apps/transactions/models.py:237
msgid "Transaction Category"
msgstr ""
#: apps/transactions/models.py:229
#: apps/transactions/models.py:238
msgid "Transaction Categories"
msgstr ""
#: apps/transactions/models.py:244
#: apps/transactions/models.py:253
msgid ""
"Deactivated tags won't be able to be selected when creating new transactions"
msgstr ""
#: apps/transactions/models.py:252 apps/transactions/models.py:253
#: apps/transactions/models.py:261 apps/transactions/models.py:262
msgid "Transaction Tags"
msgstr ""
#: apps/transactions/models.py:268
#: apps/transactions/models.py:277
msgid ""
"Deactivated entities won't be able to be selected when creating new "
"transactions"
msgstr ""
#: apps/transactions/models.py:276
#: apps/transactions/models.py:285
#: templates/insights/fragments/month_by_month.html:88
#: templates/insights/fragments/year_by_year.html:56
msgid "Entity"
msgstr ""
#: apps/transactions/models.py:288 apps/transactions/models.py:987
#: apps/transactions/models.py:297 apps/transactions/models.py:1050
#: templates/calendar_view/fragments/list.html:42
#: templates/calendar_view/fragments/list.html:44
#: templates/calendar_view/fragments/list.html:52
@@ -1480,7 +1494,7 @@ msgstr ""
msgid "Income"
msgstr ""
#: apps/transactions/models.py:289 apps/transactions/models.py:988
#: apps/transactions/models.py:298 apps/transactions/models.py:1051
#: templates/calendar_view/fragments/list.html:46
#: templates/calendar_view/fragments/list.html:48
#: templates/calendar_view/fragments/list.html:56
@@ -1491,129 +1505,157 @@ msgstr ""
msgid "Expense"
msgstr ""
#: apps/transactions/models.py:344 apps/transactions/models.py:600
#: apps/transactions/models.py:353 apps/transactions/models.py:663
msgid "Installment Plan"
msgstr ""
#: apps/transactions/models.py:353 apps/transactions/models.py:824
#: apps/transactions/models.py:362 apps/transactions/models.py:887
msgid "Recurring Transaction"
msgstr ""
#: apps/transactions/models.py:361
#: apps/transactions/models.py:370
msgid "Deleted"
msgstr ""
#: apps/transactions/models.py:366
#: apps/transactions/models.py:375
msgid "Deleted At"
msgstr ""
#: apps/transactions/models.py:480 templates/tags/fragments/table.html:69
#: apps/transactions/models.py:489 templates/tags/fragments/table.html:69
msgid "No tags"
msgstr ""
#: apps/transactions/models.py:482
#: apps/transactions/models.py:491
msgid "No category"
msgstr ""
#: apps/transactions/models.py:484
#: apps/transactions/models.py:493
msgid "No description"
msgstr ""
#: apps/transactions/models.py:532 templates/includes/sidebar.html:57
#: apps/transactions/models.py:549
msgid "File"
msgstr ""
#: apps/transactions/models.py:551
msgid "Original Name"
msgstr ""
#: apps/transactions/models.py:553
msgid "Content Type"
msgstr ""
#: apps/transactions/models.py:555
msgid "Size"
msgstr ""
#: apps/transactions/models.py:560
msgid "Uploaded By"
msgstr ""
#: apps/transactions/models.py:565
msgid "Transaction Attachment"
msgstr ""
#: apps/transactions/models.py:566
msgid "Transaction Attachments"
msgstr ""
#: apps/transactions/models.py:595 templates/includes/sidebar.html:57
msgid "Yearly"
msgstr ""
#: apps/transactions/models.py:533 apps/users/models.py:464
#: apps/transactions/models.py:596 apps/users/models.py:464
#: templates/includes/sidebar.html:51
msgid "Monthly"
msgstr ""
#: apps/transactions/models.py:534
#: apps/transactions/models.py:597
msgid "Weekly"
msgstr ""
#: apps/transactions/models.py:535
#: apps/transactions/models.py:598
msgid "Daily"
msgstr ""
#: apps/transactions/models.py:548
#: apps/transactions/models.py:611
msgid "Number of Installments"
msgstr ""
#: apps/transactions/models.py:553
#: apps/transactions/models.py:616
msgid "Installment Start"
msgstr ""
#: apps/transactions/models.py:554
#: apps/transactions/models.py:617
msgid "The installment number to start counting from"
msgstr ""
#: apps/transactions/models.py:559 apps/transactions/models.py:794
#: apps/transactions/models.py:622 apps/transactions/models.py:857
msgid "Start Date"
msgstr ""
#: apps/transactions/models.py:563 apps/transactions/models.py:795
#: apps/transactions/models.py:626 apps/transactions/models.py:858
msgid "End Date"
msgstr ""
#: apps/transactions/models.py:568
#: apps/transactions/models.py:631
msgid "Recurrence"
msgstr ""
#: apps/transactions/models.py:571
#: apps/transactions/models.py:634
msgid "Installment Amount"
msgstr ""
#: apps/transactions/models.py:590 apps/transactions/models.py:814
#: apps/transactions/models.py:653 apps/transactions/models.py:877
msgid "Add description to transactions"
msgstr ""
#: apps/transactions/models.py:593 apps/transactions/models.py:817
#: apps/transactions/models.py:656 apps/transactions/models.py:880
msgid "Add notes to transactions"
msgstr ""
#: apps/transactions/models.py:753
#: apps/transactions/models.py:816
msgid "day(s)"
msgstr ""
#: apps/transactions/models.py:754
#: apps/transactions/models.py:817
msgid "week(s)"
msgstr ""
#: apps/transactions/models.py:755
#: apps/transactions/models.py:818
msgid "month(s)"
msgstr ""
#: apps/transactions/models.py:756
#: apps/transactions/models.py:819
msgid "year(s)"
msgstr ""
#: apps/transactions/models.py:758
#: apps/transactions/models.py:821
#: templates/recurring_transactions/fragments/list.html:18
msgid "Paused"
msgstr ""
#: apps/transactions/models.py:797
#: apps/transactions/models.py:860
msgid "Recurrence Type"
msgstr ""
#: apps/transactions/models.py:800
#: apps/transactions/models.py:863
msgid "Recurrence Interval"
msgstr ""
#: apps/transactions/models.py:803
#: apps/transactions/models.py:866
msgid "Keep at most"
msgstr ""
#: apps/transactions/models.py:807
#: apps/transactions/models.py:870
msgid "Last Generated Date"
msgstr ""
#: apps/transactions/models.py:810
#: apps/transactions/models.py:873
msgid "Last Generated Reference Date"
msgstr ""
#: apps/transactions/models.py:1054
#: apps/transactions/models.py:1117
#: apps/transactions/views/quick_transactions.py:178
#: apps/transactions/views/quick_transactions.py:187
#: apps/transactions/views/quick_transactions.py:189
@@ -1622,7 +1664,7 @@ msgstr ""
msgid "Quick Transaction"
msgstr ""
#: apps/transactions/models.py:1055 templates/includes/sidebar.html:98
#: apps/transactions/models.py:1118 templates/includes/sidebar.html:98
#: templates/quick_transactions/pages/index.html:5
#: templates/quick_transactions/pages/index.html:15
msgid "Quick Transactions"
@@ -1727,8 +1769,8 @@ msgid "Item deleted successfully"
msgstr ""
#: apps/transactions/views/quick_transactions.py:156
#: apps/transactions/views/transactions.py:53
#: apps/transactions/views/transactions.py:238
#: apps/transactions/views/transactions.py:141
#: apps/transactions/views/transactions.py:326
msgid "Transaction added successfully"
msgstr ""
@@ -1768,30 +1810,38 @@ msgstr ""
msgid "Tag deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:262
#: apps/transactions/views/transactions.py:59
msgid "Attachment uploaded successfully"
msgstr ""
#: apps/transactions/views/transactions.py:110
msgid "Attachment deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:350
msgid "Transaction updated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:313
#: apps/transactions/views/transactions.py:401
#, python-format
msgid "%(count)s transaction updated successfully"
msgid_plural "%(count)s transactions updated successfully"
msgstr[0] ""
msgstr[1] ""
#: apps/transactions/views/transactions.py:349
#: apps/transactions/views/transactions.py:437
msgid "Transaction duplicated successfully"
msgstr ""
#: apps/transactions/views/transactions.py:391
#: apps/transactions/views/transactions.py:479
msgid "Transaction deleted successfully"
msgstr ""
#: apps/transactions/views/transactions.py:409
#: apps/transactions/views/transactions.py:497
msgid "Transaction restored successfully"
msgstr ""
#: apps/transactions/views/transactions.py:435
#: apps/transactions/views/transactions.py:523
msgid "Transfer added successfully"
msgstr ""
@@ -1815,24 +1865,24 @@ msgstr ""
msgid "Important dates"
msgstr ""
#: apps/users/forms.py:22 apps/users/forms.py:26 apps/users/models.py:451
#: apps/users/forms.py:24 apps/users/forms.py:28 apps/users/models.py:451
#: templates/users/login.html:18
msgid "E-mail"
msgstr ""
#: apps/users/forms.py:33 apps/users/forms.py:38 templates/users/login.html:19
#: apps/users/forms.py:35 apps/users/forms.py:40 templates/users/login.html:19
msgid "Password"
msgstr ""
#: apps/users/forms.py:45
#: apps/users/forms.py:47
msgid "Invalid e-mail or password"
msgstr ""
#: apps/users/forms.py:46
#: apps/users/forms.py:48
msgid "This account is deactivated"
msgstr ""
#: apps/users/forms.py:62 apps/users/forms.py:75 apps/users/forms.py:97
#: apps/users/forms.py:64 apps/users/forms.py:77 apps/users/forms.py:99
#: templates/monthly_overview/pages/overview.html:98
#: templates/monthly_overview/pages/overview.html:245
#: templates/transactions/pages/transactions.html:47
@@ -1840,19 +1890,23 @@ msgstr ""
msgid "Default"
msgstr ""
#: apps/users/forms.py:105 apps/users/models.py:484
#: apps/users/forms.py:107 apps/users/models.py:484
msgid "Date Format"
msgstr ""
#: apps/users/forms.py:110 apps/users/models.py:489
#: apps/users/forms.py:112 apps/users/models.py:489
msgid "Datetime Format"
msgstr ""
#: apps/users/forms.py:116 apps/users/models.py:492
#: apps/users/forms.py:118 apps/users/models.py:492
msgid "Number Format"
msgstr ""
#: apps/users/forms.py:154
#: apps/users/forms.py:125
msgid "Default Account"
msgstr ""
#: apps/users/forms.py:174
#, python-format
msgid ""
"This changes the language (if available) and how numbers and dates are "
@@ -1860,59 +1914,59 @@ msgid ""
"Consider helping translate WYGIWYH to your language at %(translation_link)s"
msgstr ""
#: apps/users/forms.py:163
#: apps/users/forms.py:183
msgid "New Password"
msgstr ""
#: apps/users/forms.py:166
#: apps/users/forms.py:186
msgid "Leave blank to keep the current password."
msgstr ""
#: apps/users/forms.py:169
#: apps/users/forms.py:189
msgid "Confirm New Password"
msgstr ""
#: apps/users/forms.py:181 apps/users/forms.py:338
#: apps/users/forms.py:201 apps/users/forms.py:358
msgid ""
"Designates whether this user should be treated as active. Unselect this "
"instead of deleting accounts."
msgstr ""
#: apps/users/forms.py:184 apps/users/forms.py:341
#: apps/users/forms.py:204 apps/users/forms.py:361
msgid ""
"Designates that this user has all permissions without explicitly assigning "
"them."
msgstr ""
#: apps/users/forms.py:251
#: apps/users/forms.py:271
msgid "This email address is already in use by another account."
msgstr ""
#: apps/users/forms.py:259
#: apps/users/forms.py:279
msgid "The two password fields didn't match."
msgstr ""
#: apps/users/forms.py:261
#: apps/users/forms.py:281
msgid "Please confirm your new password."
msgstr ""
#: apps/users/forms.py:263
#: apps/users/forms.py:283
msgid "Please enter the new password first."
msgstr ""
#: apps/users/forms.py:283
#: apps/users/forms.py:303
msgid "You cannot deactivate your own account using this form."
msgstr ""
#: apps/users/forms.py:296
#: apps/users/forms.py:316
msgid "Cannot remove status from the last superuser."
msgstr ""
#: apps/users/forms.py:302
#: apps/users/forms.py:322
msgid "You cannot remove your own superuser status using this form."
msgstr ""
#: apps/users/forms.py:395
#: apps/users/forms.py:415
msgid "A user with this email address already exists."
msgstr ""
@@ -1956,6 +2010,14 @@ msgstr ""
msgid "Start page"
msgstr ""
#: apps/users/models.py:516
msgid "Default account"
msgstr ""
#: apps/users/models.py:517
msgid "Selects the account by default when creating new transactions"
msgstr ""
#: apps/users/views.py:67
msgid "Transaction amounts are now hidden"
msgstr ""
@@ -2050,8 +2112,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:54
#: templates/accounts/fragments/list.html:71
#: templates/categories/fragments/table.html:51
#: templates/cotton/transaction/item.html:158
#: templates/cotton/transaction/item.html:230
#: templates/cotton/transaction/item.html:164
#: templates/cotton/transaction/item.html:236
#: templates/cotton/ui/deleted_transactions_action_bar.html:57
#: templates/cotton/ui/transactions_action_bar.html:82
#: templates/currencies/fragments/list.html:40
@@ -2079,8 +2141,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:58
#: templates/accounts/fragments/list.html:75
#: templates/categories/fragments/table.html:56
#: templates/cotton/transaction/item.html:160
#: templates/cotton/transaction/item.html:236
#: templates/cotton/transaction/item.html:166
#: templates/cotton/transaction/item.html:242
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:44
@@ -2109,8 +2171,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:59
#: templates/accounts/fragments/list.html:76
#: templates/categories/fragments/table.html:57
#: templates/cotton/transaction/item.html:161
#: templates/cotton/transaction/item.html:237
#: templates/cotton/transaction/item.html:167
#: templates/cotton/transaction/item.html:243
#: templates/cotton/ui/deleted_transactions_action_bar.html:58
#: templates/cotton/ui/transactions_action_bar.html:83
#: templates/currencies/fragments/list.html:45
@@ -2131,8 +2193,8 @@ msgstr ""
#: templates/account_groups/fragments/list.html:60
#: templates/accounts/fragments/list.html:77
#: templates/categories/fragments/table.html:58
#: templates/cotton/transaction/item.html:162
#: templates/cotton/transaction/item.html:238
#: templates/cotton/transaction/item.html:168
#: templates/cotton/transaction/item.html:244
#: templates/currencies/fragments/list.html:46
#: templates/dca/fragments/strategy/details.html:77
#: templates/dca/fragments/strategy/list.html:44
@@ -2149,6 +2211,7 @@ msgstr ""
#: templates/rules/fragments/transaction_rule/view.html:65
#: templates/rules/fragments/transaction_rule/view.html:98
#: templates/tags/fragments/table.html:57
#: templates/transactions/fragments/attachments.html:22
msgid "Yes, delete it!"
msgstr ""
@@ -2284,41 +2347,41 @@ msgstr ""
msgid "Select"
msgstr ""
#: templates/cotton/transaction/item.html:175
#: templates/cotton/transaction/item.html:186
#: templates/cotton/transaction/item.html:196
#: templates/cotton/transaction/item.html:181
#: templates/cotton/transaction/item.html:192
#: templates/cotton/transaction/item.html:202
msgid "Show on summaries"
msgstr ""
#: templates/cotton/transaction/item.html:177
#: templates/cotton/transaction/item.html:183
msgid "Controlled by account"
msgstr ""
#: templates/cotton/transaction/item.html:188
#: templates/cotton/transaction/item.html:194
msgid "Controlled by category"
msgstr ""
#: templates/cotton/transaction/item.html:201
#: templates/cotton/transaction/item.html:207
msgid "Hide from summaries"
msgstr ""
#: templates/cotton/transaction/item.html:205
#: templates/cotton/transaction/item.html:211
msgid "Add as quick transaction"
msgstr ""
#: templates/cotton/transaction/item.html:210
#: templates/cotton/transaction/item.html:216
msgid "Move to previous month"
msgstr ""
#: templates/cotton/transaction/item.html:214
#: templates/cotton/transaction/item.html:220
msgid "Move to next month"
msgstr ""
#: templates/cotton/transaction/item.html:217
#: templates/cotton/transaction/item.html:223
msgid "Move to today"
msgstr ""
#: templates/cotton/transaction/item.html:221
#: templates/cotton/transaction/item.html:227
#: templates/cotton/ui/transactions_action_bar.html:78
msgid "Duplicate"
msgstr ""
@@ -2799,6 +2862,10 @@ msgstr ""
msgid "Try reloading the page or check the console for more information."
msgstr ""
#: templates/includes/scripts/hyperscript/htmx_error_handler.html:24
msgid "Reload"
msgstr ""
#: templates/includes/scripts/hyperscript/swal.html:13
msgid "Cancel"
msgstr ""
@@ -2807,6 +2874,18 @@ msgstr ""
msgid "Confirm"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:4
msgid "Pull down to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:5
msgid "Release to refresh"
msgstr ""
#: templates/includes/scripts/pull_to_refresh_i18n.html:6
msgid "Refreshing"
msgstr ""
#: templates/includes/sidebar.html:69 templates/insights/pages/index.html:5
msgid "Insights"
msgstr ""
@@ -3450,6 +3529,22 @@ msgstr ""
msgid "Add Installment Plan"
msgstr ""
#: templates/transactions/fragments/attachments.html:20
msgid "Delete this attachment?"
msgstr ""
#: templates/transactions/fragments/attachments.html:21
msgid "This file will be removed from the transaction."
msgstr ""
#: templates/transactions/fragments/attachments.html:30
msgid "No attachments yet"
msgstr ""
#: templates/transactions/fragments/attachments_manage.html:5
msgid "Transaction attachments"
msgstr ""
#: templates/transactions/fragments/bulk_edit.html:5
msgid "Bulk Editing"
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -147,6 +147,12 @@
hx-target="#generic-offcanvas" hx-swap="innerHTML"
data-tippy-content="{% translate "Edit" %}">
<i class="fa-solid fa-pencil fa-fw"></i></a>
<a class="btn btn-soft btn-sm transaction-action gap-1"
role="button"
hx-get="{% url 'transaction_attachments' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas" hx-swap="innerHTML"
data-tippy-content="{% translate "Attachments" %}">
<i class="fa-solid fa-paperclip fa-fw"></i><span>{{ transaction.attachments.count }}</span></a>
<a class="btn btn-error btn-soft btn-sm transaction-action"
role="button"
hx-delete="{% url 'transaction_delete' transaction_id=transaction.id %}"
+2 -1
View File
@@ -9,9 +9,10 @@
{% include 'includes/scripts/hyperscript/sounds.html' %}
{% include 'includes/scripts/hyperscript/swal.html' %}
{% include 'includes/scripts/hyperscript/autosize.html' %}
{% include 'includes/scripts/pull_to_refresh_i18n.html' %}
<script defer>
let tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
var tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
if (!tz) {
tz = "UTC"
}
@@ -20,11 +20,16 @@ behavior htmx_error_handler
text: '{% trans "Try reloading the page or check the console for more information." %}',
icon: 'error',
timer: 60000,
showDenyButton: true,
denyButtonText: '{% trans "Reload" %}',
customClass: {
confirmButton: 'btn btn-primary'
confirmButton: 'btn btn-primary',
denyButton: 'btn btn-error',
actions: 'gap-2'
},
buttonsStyling: false
})
buttonsStyling: false,
reverseButtons: true
}) then if it.isDenied call location.reload()
end
then log event
then halt the event
@@ -0,0 +1,6 @@
{% load i18n %}
<span id="ptr-i18n"
class="hidden"
data-pull="{% translate 'Pull down to refresh' %}"
data-release="{% translate 'Release to refresh' %}"
data-refreshing="{% translate 'Refreshing' %}"></span>
@@ -268,7 +268,7 @@
</div>
{# Filter transactions form #}
<div class="z-1" x-show="filterOpen" x-collapse>
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
<div class="card card-body bg-base-200 mt-2">
<div class="text-right">
<button class="btn btn-outline btn-error btn-sm w-fit"
@@ -0,0 +1,32 @@
{% load i18n %}
<div id="transaction-attachments-list" class="mt-4">
<h3 class="font-semibold mb-2">{% translate 'Attachments' %}</h3>
{% if transaction.attachments.exists %}
<ul class="flex flex-col gap-2">
{% for attachment in transaction.attachments.all %}
<li class="flex items-center justify-between gap-3 rounded-box border border-base-content/20 p-3">
<a class="link link-primary truncate"
target="_blank"
href="{% url 'transaction_attachment_download' attachment_id=attachment.id %}">
{{ attachment.original_name }}
</a>
<button class="btn btn-error btn-sm btn-soft"
type="button"
hx-delete="{% url 'transaction_attachment_delete' attachment_id=attachment.id %}"
hx-trigger="confirmed"
hx-target="#transaction-attachments-list"
hx-swap="outerHTML"
data-title="{% translate 'Delete this attachment?' %}"
data-text="{% translate 'This file will be removed from the transaction.' %}"
data-confirm-text="{% translate 'Yes, delete it!' %}"
_="install prompt_swal">
<i class="fa-solid fa-trash fa-fw"></i>
</button>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-base-content/60">{% translate 'No attachments yet' %}</p>
{% endif %}
</div>
@@ -0,0 +1,17 @@
{% extends 'extends/offcanvas.html' %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title %}{% translate 'Transaction attachments' %}{% endblock %}
{% block body %}
<form hx-post="{% url 'transaction_attachments' transaction_id=transaction.id %}"
hx-target="#generic-offcanvas"
hx-swap="innerHTML"
enctype="multipart/form-data"
novalidate>
{% crispy form %}
</form>
{% include 'transactions/fragments/attachments.html' %}
{% endblock %}
@@ -218,7 +218,7 @@
</div>
{# Filter transactions form #}
<div class="z-1" x-show="filterOpen" x-collapse>
<div class="z-1" x-show="filterOpen" x-collapse x-cloak>
<div class="card card-body bg-base-200 mt-2">
<div class="text-right">
<button class="btn btn-outline btn-error btn-sm w-fit"
+2
View File
@@ -1,6 +1,7 @@
volumes:
wygiwyh_dev_postgres_data: {}
wygiwyh_temp:
wygiwyh_attachments:
services:
@@ -14,6 +15,7 @@ services:
- ./app/:/usr/src/app/:z
- ./frontend/:/usr/src/frontend:z
- wygiwyh_temp:/usr/src/app/temp/
- wygiwyh_attachments:/usr/src/app/attachments/
ports:
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}"
env_file:
+2
View File
@@ -6,6 +6,8 @@ services:
- "${OUTBOUND_PORT:-8000}:${INTERNAL_PORT:-8000}"
env_file:
- .env
volumes:
- ./media:/usr/src/app/attachments/
depends_on:
- db
restart: unless-stopped
+2 -2
View File
@@ -2,9 +2,9 @@ FROM node:lts-alpine
WORKDIR /usr/src/frontend
COPY ./frontend/package.json .
COPY ./frontend/package.json ./frontend/package-lock.json ./
RUN npm install --verbose && npm cache clean --force
RUN npm ci --verbose && npm cache clean --force
ENV PATH ./node_modules/.bin/:$PATH
+802 -735
View File
File diff suppressed because it is too large Load Diff
+21 -20
View File
@@ -16,34 +16,35 @@
},
"type": "module",
"dependencies": {
"@alpinejs/collapse": "^3.15.1",
"@alpinejs/mask": "^3.15.1",
"@alpinejs/collapse": "^3.15.12",
"@alpinejs/mask": "^3.15.12",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@fortawesome/fontawesome-free": "^7.1.0",
"@fortawesome/fontawesome-free": "^7.2.0",
"@popperjs/core": "^2.11.8",
"@rollup/plugin-commonjs": "^29.0.0",
"@tailwindcss/vite": "^4.1.17",
"@rollup/plugin-commonjs": "^29.0.3",
"@tailwindcss/vite": "^4.3.0",
"air-datepicker": "^3.6.0",
"alpinejs": "^3.15.1",
"autoprefixer": "^10.4.22",
"alpinejs": "^3.15.12",
"autoprefixer": "^10.5.0",
"autosize": "^6.0.1",
"bootstrap": "^5.3.8",
"chart.js": "^4.5.1",
"chartjs-chart-sankey": "^0.14.0",
"daisyui": "^5.5.5",
"htmx.org": "^2.0.8",
"hyperscript.org": "^0.9.14",
"mathjs": "^15.1.0",
"postcss": "^8.5.6",
"sass": "^1.94.0",
"sweetalert2": "^11.26.3",
"tailwindcss": "^4.1.17",
"chartjs-chart-sankey": "^0.14.3",
"daisyui": "5.5.20",
"htmx.org": "^2.0.10",
"hyperscript.org": "^0.9.91",
"mathjs": "^15.2.0",
"postcss": "^8.5.15",
"pulltorefreshjs": "^0.1.22",
"sass": "^1.100.0",
"sweetalert2": "^11.26.25",
"tailwindcss": "^4.3.0",
"tippy.js": "^6.3.7",
"tom-select": "^2.4.3",
"tw-bootstrap-grid": "^1.3.2",
"vite": "7.2.2"
"tom-select": "^2.6.1",
"tw-bootstrap-grid": "^1.4.0",
"vite": "7.3.2"
},
"resolutions": {
"rollup": "npm:@rollup/wasm-node"
}
}
}
+1 -4
View File
@@ -1,4 +1,4 @@
import _hyperscript from 'hyperscript.org/dist/_hyperscript.min';
import 'hyperscript.org';
import './_htmx.js';
import Alpine from "alpinejs";
import mask from '@alpinejs/mask';
@@ -6,7 +6,6 @@ import collapse from '@alpinejs/collapse'
import { create, all } from 'mathjs';
window.Alpine = Alpine;
window._hyperscript = _hyperscript;
window.math = create(all, {
number: 'BigNumber',
});
@@ -15,8 +14,6 @@ Alpine.plugin(mask);
Alpine.plugin(collapse);
Alpine.start();
_hyperscript.browserInit();
const successAudio = new Audio("/static/sounds/success.mp3");
const popAudio = new Audio("/static/sounds/pop.mp3");
window.paidSound = successAudio;
+112
View File
@@ -0,0 +1,112 @@
import PullToRefresh from 'pulltorefreshjs';
const isOverlayOpen = () => !!document.querySelector('.offcanvas.show, .swal2-container');
const isIosPwa = () => {
const ua = window.navigator.userAgent.toLowerCase();
const isIos = /iphone|ipad|ipod/.test(ua) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
const isStandalone = window.navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches;
return isIos && isStandalone;
};
const ptrMarkup = `
<div class="__PREFIX__box">
<div class="__PREFIX__content">
<div class="__PREFIX__icon"></div>
<div class="__PREFIX__text"></div>
</div>
</div>
`;
const ptrStyles = `
.__PREFIX__ptr {
box-shadow: inset 0 -3px 5px rgba(0, 0, 0, 0.12);
pointer-events: none;
font-size: 0.85em;
font-weight: bold;
top: 0;
height: 0;
transition: height 0.3s, min-height 0.3s;
text-align: center;
width: 100%;
overflow: hidden;
display: flex;
align-items: flex-end;
align-content: stretch;
}
.__PREFIX__box {
padding: 10px;
flex-basis: 100%;
}
.__PREFIX__pull {
transition: none;
}
.__PREFIX__text {
margin-top: .33em;
color: var(--color-base-content);
}
.__PREFIX__icon {
color: var(--color-base-content);
transition: transform .3s;
}
/*
When at the top of the page, disable vertical overscroll so passive touch
listeners can take over.
*/
.__PREFIX__top {
touch-action: pan-x pan-down pinch-zoom;
}
.__PREFIX__release .__PREFIX__icon {
transform: rotate(180deg);
}
`;
const getPtrStrings = () => {
const ptrStringsEl = document.querySelector('#ptr-i18n');
return {
pull: ptrStringsEl?.dataset.pull,
release: ptrStringsEl?.dataset.release,
refreshing: ptrStringsEl?.dataset.refreshing
};
};
const initPullToRefresh = () => {
const ptrStrings = getPtrStrings();
PullToRefresh.destroyAll();
let ptr = PullToRefresh.init({
mainElement: 'body',
triggerElement: '#content',
getMarkup() {
return ptrMarkup;
},
getStyles() {
return ptrStyles;
},
instructionsPullToRefresh: ptrStrings.pull || 'Pull down to refresh',
instructionsReleaseToRefresh: ptrStrings.release || 'Release to refresh',
instructionsRefreshing: ptrStrings.refreshing || 'Refreshing',
shouldPullToRefresh() {
return isIosPwa() && !isOverlayOpen() && window.scrollY === 0;
},
onRefresh() {
window.location.reload();
}
});
};
if (isIosPwa()) {
initPullToRefresh();
document.body.addEventListener('htmx:afterSwap', (event) => {
if (event.detail.target === document.body) {
initPullToRefresh();
}
});
}
+27 -3
View File
@@ -4,6 +4,15 @@ import '../styles/_tom-select.scss'
window.TomSelect = function createDynamicTomSelect(element) {
const schedulePopperUpdate = function (instance) {
// Wait for TomSelect DOM updates before recalculating dropdown position.
requestAnimationFrame(() => {
if (instance.popper) {
instance.popper.update();
}
});
};
// Basic configuration
const config = {
plugins: {},
@@ -27,10 +36,16 @@ window.TomSelect = function createDynamicTomSelect(element) {
this.popper = Popper.createPopper(this.control, this.dropdown, {
placement: "bottom-start",
modifiers: [
{
name: "offset",
options: {
offset: [0, 4],
},
},
{
name: "sameWidth",
enabled: true,
fn: ({state}) => {
fn: ({ state }) => {
state.styles.popper.width = `${state.rects.reference.width}px`;
},
phase: "beforeWrite",
@@ -48,8 +63,17 @@ window.TomSelect = function createDynamicTomSelect(element) {
},
onDropdownOpen: function () {
this.popper.update();
}
schedulePopperUpdate(this);
},
onItemAdd: function () {
schedulePopperUpdate(this);
},
onItemRemove: function () {
schedulePopperUpdate(this);
},
onClear: function () {
schedulePopperUpdate(this);
},
};
if (element.dataset.checkboxes === 'true') {
+1
View File
@@ -10,3 +10,4 @@ import './js/sweetalert2.js';
import './js/style.js';
import './js/_utils.js';
import './js/hide_amounts.js';
import './js/pulltorefresh.js';
@@ -0,0 +1,3 @@
import twBootstrapGrid from "tw-bootstrap-grid";
export default twBootstrapGrid;
+5 -1
View File
@@ -49,6 +49,10 @@ div:where(.swal2-container) {
z-index: 1101 !important;
}
#toasts .toast-container {
z-index: 1100;
}
.logo {
/* Set the background-color to DaisyUI CSS variable */
background-color: var(--color-primary);
@@ -77,4 +81,4 @@ div:where(.swal2-container) {
[x-cloak] {
display: none !important;
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
themes: wygiwyh_dark --default, wygiwyh_light;
logs: true;
}
@plugin "tw-bootstrap-grid";
@plugin "../plugins/tw-bootstrap-grid-plugin.js";
@plugin "daisyui/theme" {
name: "wygiwyh_light";
+34
View File
@@ -51,10 +51,44 @@ export default defineConfig({
manifest: 'manifest.json',
emptyOutDir: true,
target: 'es2017',
chunkSizeWarningLimit: 800,
rollupOptions: {
input: rollupInputs,
output: {
chunkFileNames: undefined,
manualChunks(id) {
if (!id.includes('node_modules')) {
return;
}
if (id.includes('/chart.js/') || id.includes('/chartjs-chart-sankey/')) {
return 'vendor-chart';
}
if (id.includes('/mathjs/')) {
return 'vendor-math';
}
if (
id.includes('/alpinejs/') ||
id.includes('/@alpinejs/') ||
id.includes('/htmx.org/') ||
id.includes('/hyperscript.org/')
) {
return 'vendor-interaction';
}
if (
id.includes('/bootstrap/') ||
id.includes('/@popperjs/') ||
id.includes('/sweetalert2/') ||
id.includes('/tippy.js/') ||
id.includes('/tom-select/') ||
id.includes('/air-datepicker/')
) {
return 'vendor-ui';
}
},
},
},
},
+18 -18
View File
@@ -5,34 +5,34 @@ description = "An opinionated and powerful finance tracker."
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"crispy-bootstrap5==2025.6",
"django~=5.2.9",
"django-allauth[socialaccount]~=65.13.1",
"crispy-bootstrap5==2026.3",
"django~=5.2.15",
"django-allauth[socialaccount]~=65.18.0",
"django-browser-reload==1.21.0",
"django-cachalot~=2.8.0",
"django-cotton<2.3.0",
"django-crispy-forms==2.5",
"django-debug-toolbar==6.1.0",
"django-cachalot~=2.9.0",
"django-cotton~=2.7.2",
"django-crispy-forms==2.6",
"django-debug-toolbar==6.3.0",
"django-filter==25.2",
"django-hijack==3.7.4",
"django-import-export~=4.3.9",
"django-hijack==3.7.8",
"django-import-export~=4.4.1",
"django-pwa~=2.0.1",
"django-vite==3.1.0",
"djangorestframework~=3.16.0",
"djangorestframework~=3.17.1",
"drf-spectacular~=0.29.0",
"gunicorn==23.0.0",
"mistune~=3.1.3",
"gunicorn==26.0.0",
"mistune~=3.2.1",
"openpyxl~=3.1.5",
"procrastinate[django]~=3.5.3",
"psycopg[binary,pool]==3.2.9",
"pydantic~=2.12.3",
"procrastinate[django]~=3.8.1",
"psycopg[binary,pool]==3.3.4",
"pydantic~=2.13.4",
"python-dateutil~=2.9.0.post0",
"pytz>=2025.2",
"pyyaml~=6.0.2",
"requests~=2.32.5",
"requests~=2.34.2",
"simpleeval~=1.0.3",
"watchfiles==1.1.1",
"whitenoise[brotli]==6.11.0",
"watchfiles==1.2.0",
"whitenoise[brotli]==6.12.0",
"xlrd~=2.0.1",
]
Generated
+612 -537
View File
File diff suppressed because it is too large Load Diff