Compare commits

...

82 commits

Author SHA1 Message Date
34d9253f9f
Change the way the showcase JS works.
- Put the images in a language folder ("en", "fr"), which will make it
  easier to add orther languages later on.
- Resize the images to fit the already existing ones.
- Add a `display_showcase` parameter to the
  `list_bills` and `home` views.
2025-01-05 13:56:57 +01:00
Theo
354a482e60
adding the english version of the comics, including the modifications in the frontend structure 2025-01-05 13:43:06 +01:00
Theo
73a90d7dbc
html update 2025-01-05 13:43:06 +01:00
bd689f931a
Merge branch 'fix-1336' 2025-01-05 13:01:16 +01:00
67938eabbc
Do not display deactivated users when their balance is really small
Cases has been reported of rounding issues making deactivated users
reapparing. This is due to the fact we're using floats (see #528 for
details)

Fixes #1336
2025-01-05 13:00:54 +01:00
662ff97795
Drop python 3.8
Some checks failed
CI / lint (push) Has been cancelled
CI / docs (push) Has been cancelled
Docker build / test (push) Has been cancelled
CI / test (mariadb, minimal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.9) (push) Has been cancelled
CI / test (postgresql, minimal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.9) (push) Has been cancelled
CI / test (sqlite, minimal, 3.10) (push) Has been cancelled
CI / test (sqlite, minimal, 3.11) (push) Has been cancelled
CI / test (sqlite, minimal, 3.12) (push) Has been cancelled
CI / test (sqlite, minimal, 3.9) (push) Has been cancelled
CI / test (sqlite, normal, 3.10) (push) Has been cancelled
CI / test (sqlite, normal, 3.11) (push) Has been cancelled
CI / test (sqlite, normal, 3.12) (push) Has been cancelled
CI / test (sqlite, normal, 3.9) (push) Has been cancelled
Docker build / build_upload (push) Has been cancelled
Security support has ended in October.
2025-01-03 15:22:09 +01:00
Jojo144
cfc2ffa671
Add a tooltip to document the Settle button (#1360) 2025-01-03 15:19:49 +01:00
Jojo144
3a007714bf
Move the bill type field in the More options section (#1361) 2025-01-03 15:19:33 +01:00
4f9cad88bd
Rename master to main
Some checks failed
CI / lint (push) Has been cancelled
CI / docs (push) Has been cancelled
Docker build / test (push) Has been cancelled
CI / test (mariadb, minimal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.9) (push) Has been cancelled
CI / test (postgresql, minimal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.9) (push) Has been cancelled
CI / test (sqlite, minimal, 3.10) (push) Has been cancelled
CI / test (sqlite, minimal, 3.11) (push) Has been cancelled
CI / test (sqlite, minimal, 3.12) (push) Has been cancelled
CI / test (sqlite, minimal, 3.9) (push) Has been cancelled
CI / test (sqlite, normal, 3.10) (push) Has been cancelled
CI / test (sqlite, normal, 3.11) (push) Has been cancelled
CI / test (sqlite, normal, 3.12) (push) Has been cancelled
CI / test (sqlite, normal, 3.8) (push) Has been cancelled
CI / test (sqlite, normal, 3.9) (push) Has been cancelled
Docker build / build_upload (push) Has been cancelled
2024-12-28 00:50:27 +01:00
zorun
c2db991ffd
Update changelog
Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2024-12-27 00:07:07 +01:00
ferid333
794f26f767 Update default_settings.py
Co-Authored-By: qurbanov <10328930+qurbanov@users.noreply.github.com>
2024-12-26 13:41:23 +01:00
ferid333
85eccb74b2 I added Azerbaijani Translation
I added Azerbaijani Translation

Co-Authored-By: qurbanov <10328930+qurbanov@users.noreply.github.com>
2024-12-26 13:41:23 +01:00
4b96e89422
Remove flake8 2024-12-26 08:35:49 +01:00
Jojo144
dcb61b62b1 Update contibuting.md with uv dependency 2024-12-26 08:33:51 +01:00
Jojo144
e00c39a62c Update ihatemoney/history.py
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2024-12-26 08:31:25 +01:00
Jojo144
2f099674ed Workaround to avoid history bug #1324 2024-12-26 08:31:25 +01:00
dependabot[bot]
752c80d29f Update flask-cors requirement from <4,>=3.0.8 to >=3.0.8,<6
Some checks failed
CI / lint (push) Has been cancelled
CI / docs (push) Has been cancelled
Docker build / test (push) Has been cancelled
CI / test (mariadb, minimal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.9) (push) Has been cancelled
CI / test (postgresql, minimal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.9) (push) Has been cancelled
CI / test (sqlite, minimal, 3.10) (push) Has been cancelled
CI / test (sqlite, minimal, 3.11) (push) Has been cancelled
CI / test (sqlite, minimal, 3.12) (push) Has been cancelled
CI / test (sqlite, minimal, 3.9) (push) Has been cancelled
CI / test (sqlite, normal, 3.10) (push) Has been cancelled
CI / test (sqlite, normal, 3.11) (push) Has been cancelled
CI / test (sqlite, normal, 3.12) (push) Has been cancelled
CI / test (sqlite, normal, 3.8) (push) Has been cancelled
CI / test (sqlite, normal, 3.9) (push) Has been cancelled
Docker build / build_upload (push) Has been cancelled
Updates the requirements on [flask-cors](https://github.com/corydolphin/flask-cors) to permit the latest version.
- [Release notes](https://github.com/corydolphin/flask-cors/releases)
- [Changelog](https://github.com/corydolphin/flask-cors/blob/main/CHANGELOG.md)
- [Commits](https://github.com/corydolphin/flask-cors/compare/3.0.8...5.0.0)

---
updated-dependencies:
- dependency-name: flask-cors
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-24 12:28:05 +01:00
dependabot[bot]
61ea1f54d2 Update psycopg2-binary requirement
Some checks are pending
CI / lint (push) Waiting to run
CI / test (mariadb, minimal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.9) (push) Blocked by required conditions
CI / test (postgresql, minimal, 3.11) (push) Blocked by required conditions
CI / test (postgresql, normal, 3.11) (push) Blocked by required conditions
CI / test (postgresql, normal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.10) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.10) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.8) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.9) (push) Blocked by required conditions
CI / docs (push) Waiting to run
Docker build / test (push) Waiting to run
Docker build / build_upload (push) Blocked by required conditions
Updates the requirements on [psycopg2-binary](https://github.com/psycopg/psycopg2) to permit the latest version.
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/compare/2.9.6...2.9.10)

---
updated-dependencies:
- dependency-name: psycopg2-binary
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:26:07 +01:00
dependabot[bot]
299c384908 Bump ruff from 0.6.8 to 0.8.4
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.8 to 0.8.4.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.6.8...0.8.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:24:04 +01:00
dependabot[bot]
4e9ff9b1ac Update qrcode requirement from <8,>=7.1 to >=7.1,<9
Updates the requirements on [qrcode](https://github.com/lincolnloop/python-qrcode) to permit the latest version.
- [Changelog](https://github.com/lincolnloop/python-qrcode/blob/main/CHANGES.rst)
- [Commits](https://github.com/lincolnloop/python-qrcode/compare/v7.1...v8.0)

---
updated-dependencies:
- dependency-name: qrcode
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:23:54 +01:00
dependabot[bot]
2aa410c68f Update cachetools requirement from <5,>=4.1 to >=4.1,<6
Updates the requirements on [cachetools](https://github.com/tkem/cachetools) to permit the latest version.
- [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/tkem/cachetools/compare/v4.1.0...v5.5.0)

---
updated-dependencies:
- dependency-name: cachetools
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-23 21:23:33 +01:00
Mickaël Schoentgen
7505cbe25a feat: Add a SITE_NAME setting and use it everywhere.
Some checks failed
CI / lint (push) Has been cancelled
CI / docs (push) Has been cancelled
Docker build / test (push) Has been cancelled
CI / test (mariadb, minimal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.11) (push) Has been cancelled
CI / test (mariadb, normal, 3.9) (push) Has been cancelled
CI / test (postgresql, minimal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.11) (push) Has been cancelled
CI / test (postgresql, normal, 3.9) (push) Has been cancelled
CI / test (sqlite, minimal, 3.10) (push) Has been cancelled
CI / test (sqlite, minimal, 3.11) (push) Has been cancelled
CI / test (sqlite, minimal, 3.12) (push) Has been cancelled
CI / test (sqlite, minimal, 3.9) (push) Has been cancelled
CI / test (sqlite, normal, 3.10) (push) Has been cancelled
CI / test (sqlite, normal, 3.11) (push) Has been cancelled
CI / test (sqlite, normal, 3.12) (push) Has been cancelled
CI / test (sqlite, normal, 3.8) (push) Has been cancelled
CI / test (sqlite, normal, 3.9) (push) Has been cancelled
Docker build / build_upload (push) Has been cancelled
2024-12-20 23:27:11 +01:00
jjspill1
83a60b1289 Add a cli to count the number of active projects
Some checks are pending
CI / test (postgresql, normal, 3.11) (push) Blocked by required conditions
CI / test (postgresql, normal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.10) (push) Blocked by required conditions
CI / lint (push) Waiting to run
CI / test (mariadb, minimal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.11) (push) Blocked by required conditions
CI / test (mariadb, normal, 3.9) (push) Blocked by required conditions
CI / test (postgresql, minimal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, minimal, 3.9) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.10) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.11) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.12) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.8) (push) Blocked by required conditions
CI / test (sqlite, normal, 3.9) (push) Blocked by required conditions
CI / docs (push) Waiting to run
Docker build / test (push) Waiting to run
Docker build / build_upload (push) Blocked by required conditions
2024-12-20 18:07:51 +01:00
dependabot[bot]
ce20f9adea Update myst-parser requirement from <3,>=2 to >=2,<5
Updates the requirements on [myst-parser](https://github.com/executablebooks/MyST-Parser) to permit the latest version.
- [Release notes](https://github.com/executablebooks/MyST-Parser/releases)
- [Changelog](https://github.com/executablebooks/MyST-Parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/MyST-Parser/compare/v2.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: myst-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:46:40 +01:00
2b21795e94 ci: Pin runners to ubuntu 22.04 2024-12-20 17:46:02 +01:00
dependabot[bot]
0b09476865 Bump vermin from 1.5.2 to 1.6.0
Bumps [vermin](https://github.com/netromdk/vermin) from 1.5.2 to 1.6.0.
- [Release notes](https://github.com/netromdk/vermin/releases)
- [Commits](https://github.com/netromdk/vermin/compare/v1.5.2...v1.6.0)

---
updated-dependencies:
- dependency-name: vermin
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:56 +01:00
dependabot[bot]
62fbeee15c Update pymysql requirement from <1.1,>=0.9 to >=0.9,<1.2
Updates the requirements on [pymysql](https://github.com/PyMySQL/PyMySQL) to permit the latest version.
- [Release notes](https://github.com/PyMySQL/PyMySQL/releases)
- [Changelog](https://github.com/PyMySQL/PyMySQL/blob/main/CHANGELOG.md)
- [Commits](https://github.com/PyMySQL/PyMySQL/compare/v0.9.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pymysql
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:50 +01:00
dependabot[bot]
56e2ff6900 Update wtforms requirement from <3.2,>=2.3.3 to >=2.3.3,<3.3
Updates the requirements on [wtforms](https://github.com/pallets-eco/wtforms) to permit the latest version.
- [Release notes](https://github.com/pallets-eco/wtforms/releases)
- [Changelog](https://github.com/pallets-eco/wtforms/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets-eco/wtforms/compare/2.3.3...3.2.1)

---
updated-dependencies:
- dependency-name: wtforms
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:42 +01:00
dependabot[bot]
35ac04be20 Update flask requirement from <3,>=2 to >=2,<4
Updates the requirements on [flask](https://github.com/pallets/flask) to permit the latest version.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/2.0.0...3.1.0)

---
updated-dependencies:
- dependency-name: flask
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:32 +01:00
dependabot[bot]
a9f211d3f6 Update sphinx requirement from <8,>=7.0.1 to >=7.0.1,<9
Updates the requirements on [sphinx](https://github.com/sphinx-doc/sphinx) to permit the latest version.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/v8.1.3/CHANGES.rst)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v7.0.1...v8.1.3)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-20 17:32:17 +01:00
e568bb05cc tests: remove libfake time from the tests
libfaketime and python-libfaketime seem to cause our CI to fail when
used in conjunction with `uv`. This changes the way the tests are done
so they don't require libfaketime anymore.
2024-12-20 17:17:31 +01:00
86eb9b8662 build: remove support for python 3.7 2024-12-20 17:17:31 +01:00
6e31a9c8b5 Upgrade tooling on the project.
- Replace black by ruff, as it's quicker ;
- Use `uv` wherever possible as a replacement for pip, as it's way faster to run, add an `uv.lock` file which will be synced before the releases and published here ;
- Remove tox, it's too complex for this project and can easily be replaced by `uv` ;
- Apply `ruff` formatting ;
- Update the makefile accordingly ;
- Update the CI accordingly
2024-12-20 17:17:31 +01:00
MediMilk
cf77b4c346
Corrected typo Administation > Administration (#1332)
Some checks failed
Check doc / test_doc (push) Has been cancelled
Docker build / test (push) Has been cancelled
Lint & unit tests / lint (push) Has been cancelled
Docker build / build_upload (push) Has been cancelled
Lint & unit tests / test (mariadb, minimal, 3.11) (push) Has been cancelled
Lint & unit tests / test (mariadb, normal, 3.11) (push) Has been cancelled
Lint & unit tests / test (mariadb, normal, 3.9) (push) Has been cancelled
Lint & unit tests / test (postgresql, minimal, 3.11) (push) Has been cancelled
Lint & unit tests / test (postgresql, normal, 3.11) (push) Has been cancelled
Lint & unit tests / test (postgresql, normal, 3.9) (push) Has been cancelled
Lint & unit tests / test (sqlite, minimal, 3.10) (push) Has been cancelled
Lint & unit tests / test (sqlite, minimal, 3.11) (push) Has been cancelled
Lint & unit tests / test (sqlite, minimal, 3.12) (push) Has been cancelled
Lint & unit tests / test (sqlite, minimal, 3.7) (push) Has been cancelled
Lint & unit tests / test (sqlite, minimal, 3.9) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.10) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.11) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.12) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.7) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.8) (push) Has been cancelled
Lint & unit tests / test (sqlite, normal, 3.9) (push) Has been cancelled
Co-authored-by: MediMilk <chadricksoup@gmail.com>
2024-11-16 11:55:04 +01:00
adan-ea
6582e2c0c3 docs: add info about salted password hash 2024-09-20 11:58:50 +02:00
adan-ea
6e0a3689b8 doc: add info about docker compose
Should clarify and help close #1321
Issue also found in #1169 and #334 where i found my solution
2024-09-20 11:58:50 +02:00
Weblate (bot)
710aee9711
Translations update from Hosted Weblate (#1312)
Co-authored-by: Harshini K <harshinikondepudi@gmail.com>
Co-authored-by: Yamin Siahmargooei <yamin8000@yahoo.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Khang Tran <tranchikhang@outlook.com>
Co-authored-by: NtWriteCode <github@gyorffi.hu>
Co-authored-by: szabi <szabikun1@gmail.com>
2024-07-08 19:56:00 +02:00
Weblate (bot)
eb6e156c32
Translations update from Hosted Weblate (#1311)
Co-authored-by: Gesiane Pajarinen <gesianef@hotmail.com>
2024-05-10 12:12:22 +02:00
Baptiste Jonglez
9ef46e2c5d Add a success message when adding an automatic settlement bill 2024-04-27 17:54:13 +02:00
4e7496e49d
docs: fix broken links 2024-04-23 20:08:59 +02:00
e5dfbf2f37 doc: current status of the project
Update the README and docs with the current status of the project
regarding its maintenance and current direction.
2024-04-23 19:49:58 +02:00
Éloi Rivard
3ac1bb8afe tests: cache the jinja bytecode between unit tests
The jinja templates are compiled once per test session instead of once
per test, using jinja cache system and a pytest fixture.

https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.FileSystemBytecodeCache
2024-04-16 23:11:42 +02:00
Tom Roussel
a5f83de5ce Chore: ran black 2024-03-31 19:21:56 +02:00
Tom Roussel
050de4e8f6 Removed unnecessary FIXME
This fixme was not actually valid. I think it was mistakenly copied from
web.edit_bill
2024-03-31 19:21:56 +02:00
Tom Roussel
f9a96b0e0d Removed fromage erasure 2024-03-31 19:21:56 +02:00
Tom Roussel
a74cd97286 Removed reference to transfer billtype in test 2024-03-31 19:21:56 +02:00
Tom Roussel
a3009126dc Removed test for removed transfer billtype 2024-03-31 19:21:56 +02:00
Baptiste Jonglez
eef67cf84c Remove dead settlement code (we switched to an external lib long ago) 2024-03-29 15:06:11 +01:00
Timo Riski
a3d4e4250d fix: 'Bill Type: Invalid Choice: could not coerce' error
Error introduced in #1290. Fixes #1293. WTForms needs to be bumped to >=2.3.2
as it includes a fix to `SelectField` which is required for this change to work.

See:
  - https://wtforms.readthedocs.io/en/3.1.x/changes/#version-2-3-2
  - https://github.com/wtforms/wtforms/pull/598
2024-03-28 22:42:54 +01:00
Éloi Rivard
ae1cc309d7 fix: babel 2.14+ and python 3.12+ setuptools dependency 2024-03-26 09:18:22 +01:00
Baptiste Jonglez
510c8db07f CI: make sure the tests matrix depends on linting 2024-03-25 20:46:39 +01:00
Baptiste Jonglez
843f2df877 CI: Temporarily disable failing python 3.12 job
See #1297
2024-03-25 20:46:39 +01:00
Baptiste Jonglez
178fc94cef Fix duplicate unit tests 2024-03-25 20:46:39 +01:00
Baptiste Jonglez
312dfef14b Reformat code with black and isort 2024-03-25 20:46:39 +01:00
Baptiste Jonglez
a0409a296a CI: Update databases version to match more recent Debian 2024-03-25 20:46:39 +01:00
Baptiste Jonglez
73f014e90e CI: Move lint and docs to separate action for readability
Currently, linting is done in one specific job of the big test Matrix, and
it's very easy to overlook.  But we want linting to be the first thing to fix.

Also reorganize and rename jobs for readability.

Finally, use python 3.11 for lint/docs because python 3.12 seems to break dev install.
2024-03-25 20:46:39 +01:00
Baptiste Jonglez
4af4c10b1f Backport 6.1.5 changelog from stable-6.1 branch 2024-03-19 23:55:54 +01:00
Xander Jennie
c399611660 Translated using Weblate (Dutch)
Currently translated at 72.4% (200 of 276 strings)

Co-authored-by: Xander Jennie <xanderjennie21@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/nl/
Translation: I Hate Money/I Hate Money
2024-03-19 23:28:12 +01:00
Peter
f090d81358 Translated using Weblate (German)
Currently translated at 95.6% (264 of 276 strings)

Co-authored-by: Peter <peteramried@web.de>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/de/
Translation: I Hate Money/I Hate Money
2024-03-19 23:28:12 +01:00
Luca Bakan
d00f8063ed Translated using Weblate (German)
Currently translated at 95.2% (263 of 276 strings)

Co-authored-by: Luca Bakan <luca.bakan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/de/
Translation: I Hate Money/I Hate Money
2024-03-19 23:28:12 +01:00
TomRoussel
720f0e52dd
Adding bill types and automatic settling between people (#1290)
* Bill types added in Bill and Project Model, Implemented in BillForm
* import and export bill feature updated with bill type, tests modified to reflect the behavior
* eliminating unnecessary bill type
* typo fixed, test cases fixed for the current bill types
* button added
* settle button added
* new changes
* test cases added
* bchen-reimbursement
* tests for different bill types
* test cases fixed
* fixed reimbursement test case
* Replaced assertEqual with assert
* Fixed missing bill_type in unit tests
* Removed commented code
* Reverted unnecessary string edit
* Changed bill_type to an Enum
* Added test checking correct bill_type validation
* Fixed  billtype displaying in all caps
* Removed 'Transfer' bill type
* Added migration rule and set default bill_type in alembic
* bill_type is now an optional parameter in the BillForm
* Use enum name instead of value as SQL server_default

SQLAlchemy uses the Enum names in the database, as the values could be
generic python objects.
https://docs.sqlalchemy.org/en/20/core/type_basics.html#sqlalchemy.types.Enum

* Removed bill type from the Bills html table
* Replaced string bill type with enum
* Made "Settlement" translatable
* Manually handle the new Enum creation

Alembic does not handle postgres Enums correctly, so we need to manually
generate the new enum type.
See https://github.com/sqlalchemy/alembic/issues/278

---------

Co-authored-by: Ruitao Li <ruital@andrew.cmu.edu>
Co-authored-by: MelodyZhangYiqun <98992024+MelodyZhangYiqun@users.noreply.github.com>
Co-authored-by: Ruitao Li <49292515+FlowingCloudRTL@users.noreply.github.com>
Co-authored-by: MelodyZhangYiqun <yiqunz@andrew.cmu.edu>
Co-authored-by: Brandan Chen <bychen@andrew.cmu.edu>
Co-authored-by: Emilie Zhou <54161959+ez157@users.noreply.github.com>
Co-authored-by: Tom <tom.roussel@esat.kuleuven.be>
2024-03-16 12:20:48 +01:00
Turtle6665
ba117ba0a6
Changing any settings is prevented when project has existing currency (#1292) 2024-03-15 22:32:10 +01:00
2bb535070a
[chore] Remove deprecated Jinja2 extensions. (#1279)
autoescape and with extensions are now built-in
to the Jinja2 compiler since v3.

See https://jinja.palletsprojects.com/en/3.1.x/changes/#version-3-0-0
2024-02-06 03:33:08 +01:00
zorun
1dcb0ba78b
Update security support status (#1275)
Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
2024-02-06 03:32:49 +01:00
Jinx
fa4a881ae1 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Jinx <me@qqays.xyz>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/zh_Hans/
Translation: I Hate Money/I Hate Money
2023-12-14 02:03:15 +01:00
Éloi Rivard
edefb51cfb
move from setuptools to hatch (#1258)
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2023-12-12 14:20:34 +01:00
Oğuz Ersen
9f7ecf6614 Translated using Weblate (Turkish)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/tr/
Translation: I Hate Money/I Hate Money
2023-12-12 12:12:31 +01:00
Clonewayx
7fd00344e2 Translated using Weblate (Czech)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Clonewayx <fillip1@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/cs/
Translation: I Hate Money/I Hate Money
2023-12-12 12:12:31 +01:00
Ema Havrdová
6ec3ba6f77 Translated using Weblate (Czech)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Ema Havrdová <emicka.havrdova@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/cs/
Translation: I Hate Money/I Hate Money
2023-12-12 12:12:31 +01:00
Wilfredo Gomez
511ba86c4c Translated using Weblate (Spanish (Latin America))
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Wilfredo Gomez <thepageguy@mailfence.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es_419/
Translation: I Hate Money/I Hate Money
2023-12-12 12:12:31 +01:00
Jesper
54a5b0e63e Translated using Weblate (Swedish)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Jesper <93771679+Bjorkan@users.noreply.github.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/sv/
Translation: I Hate Money/I Hate Money
2023-12-12 12:12:31 +01:00
zorun
2ce1ea4bf2
Fix missing markdown include in manifest (#1274)
Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
2023-12-07 22:21:07 +01:00
Éloi Rivard
8bce025c15 chore: support for python 3.12 2023-11-23 23:08:39 +01:00
Nicholas (Nick) Meyer
bb30813ec4 Make APPLICATION_ROOT wording more consistent
Default APPLICATION_ROOT is "/", so "By default" is more accurate than
"If empty" for the default value.
2023-11-23 08:35:21 +01:00
Nicholas (Nick) Meyer
ecf9a7b590 Fix APPLICATION_ROOT example in docs
If the application is hosted on the path /somestring, static asset
paths need to have prefix /somestring/static/ but setting
APPLICATION_ROOT to "somestring" will result in a relative path which
will give a 4XX. Using "/somestring" fixes this.

Also fix default value, which is "/" and not the empty string.
2023-11-23 08:35:21 +01:00
Baptiste Jonglez
b74ac1077c Backport changelog for 6.1.3 from stable-6.1 branch 2023-11-23 08:26:18 +01:00
Baptiste Jonglez
76e8b3baf0 CI: run tests and docker build on stable branches 2023-11-19 11:19:08 +01:00
Baptiste Jonglez
8cd60c1bf5 Back to development: 6.2.0 2023-11-19 11:19:08 +01:00
Baptiste Jonglez
2d7c6486e9 Preparing release 6.1.2 2023-11-19 11:19:08 +01:00
Baptiste Jonglez
db7c9ea2b3 Update changelog for 6.1.2 2023-11-19 11:19:08 +01:00
gallegonovato
d1382a691b Translated using Weblate (Spanish)
Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es/
Translation: I Hate Money/I Hate Money
2023-11-18 10:36:00 +01:00
Kamborio
d02ec152f7 Translated using Weblate (Spanish)
Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Spanish)

Currently translated at 47.4% (131 of 276 strings)

Co-authored-by: Kamborio <Kamborio15@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es/
Translation: I Hate Money/I Hate Money
2023-11-18 10:35:59 +01:00
134 changed files with 6121 additions and 1797 deletions

View file

@ -14,9 +14,7 @@ CONTRIBUTORS
docker-compose.*
Dockerfile
docs
LICENSE
Makefile
MANIFEST.in
README.md
SECURITY.md
tox.ini

122
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,122 @@
name: CI
on:
push:
branches: [ 'main', 'stable-*' ]
pull_request:
branches: [ 'main', 'stable-*' ]
jobs:
lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
- name: Run Lint
run: make lint
test:
# Dependency on linting to avoid running our expensive matrix test for nothing
needs: lint
runs-on: ubuntu-22.04
# Use postgresql and MariaDB versions of Debian bookworm
services:
postgres:
image: postgres:15
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ihatemoney
POSTGRES_DB: ihatemoney_ci
options:
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
mariadb:
image: mariadb:10.11
env:
MARIADB_ROOT_PASSWORD: ihatemoney
MARIADB_DATABASE: ihatemoney_ci
options: >-
--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
ports:
- 3306:3306
strategy:
fail-fast: false
matrix:
python-version: [3.9, "3.10", "3.11", "3.12"]
dependencies: [normal]
database: [sqlite]
# Test other databases with only a few versions of Python
# (Debian bullseye has 3.9, bookworm has 3.11)
include:
- python-version: 3.9
dependencies: normal
database: postgresql
- python-version: 3.9
dependencies: normal
database: mariadb
- python-version: 3.11
dependencies: normal
database: postgresql
- python-version: 3.11
dependencies: normal
database: mariadb
# Try a few variants with the minimal versions supported
- python-version: 3.9
dependencies: minimal
database: sqlite
- python-version: "3.10"
dependencies: minimal
database: sqlite
- python-version: "3.11"
dependencies: minimal
database: sqlite
- python-version: "3.11"
dependencies: minimal
database: postgresql
- python-version: "3.11"
dependencies: minimal
database: mariadb
- python-version: "3.12"
dependencies: minimal
database: sqlite
steps:
- uses: actions/checkout@v3
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: ${{ matrix.python-version }}
- name: Change dependencies to minimal supported versions
# This sed comment installs the minimal supported version
# for all versions except for "requires-python"
# This is to detect that the minimum versions are really
# supported, in the CI
run: sed -i -e '/requires-python/!s/>=/==/g; /requires-python/!s/~=.*==\(.*\)/==\1/g; /requires-python/!s/~=/==/g;' pyproject.toml
if: matrix.dependencies == 'minimal'
- name: Run tests
run: uv run --extra dev --extra database pytest .
env:
# Setup the DATABASE_URI depending on the matrix we are using.
TESTING_SQLALCHEMY_DATABASE_URI: ${{
matrix.database == 'sqlite'
&& 'sqlite:///budget.db'
|| matrix.database == 'postgresql'
&& 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci'
|| 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci'
}}
docs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Install uv and set the python version
uses: astral-sh/setup-uv@v4
with:
python-version: "3.11"
- name: Build docs
run: make build-docs

View file

@ -1,16 +1,16 @@
name: CI to Docker Hub
name: Docker build
on:
push:
tags: ['*']
branches: [ master ]
branches: [ 'main', 'stable-*' ]
pull_request:
branches: [ master ]
branches: [ 'main', 'stable-*' ]
jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v2
@ -18,8 +18,8 @@ jobs:
- name: Test image # Checks we are able to run the container.
run: docker compose -f docker-compose.test.yml run sut
build:
runs-on: ubuntu-latest
build_upload:
runs-on: ubuntu-22.04
needs: test
if: github.event_name != 'pull_request'
steps:

View file

@ -1,103 +0,0 @@
name: Test & Docs
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
# Use postgresql and MariaDB versions of Debian buster
services:
postgres:
image: postgres:11
ports:
- 5432:5432
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ihatemoney
POSTGRES_DB: ihatemoney_ci
options:
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
mariadb:
image: mariadb:10.3
env:
MARIADB_ROOT_PASSWORD: ihatemoney
MARIADB_DATABASE: ihatemoney_ci
options: >-
--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
ports:
- 3306:3306
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
dependencies: [normal]
database: [sqlite]
# Test other databases only with one version of Python (Debian buster has 3.7)
include:
- python-version: 3.7
dependencies: normal
database: postgresql
- python-version: 3.7
dependencies: normal
database: mariadb
# Try a few variants with the minimal versions supported
- python-version: 3.7
dependencies: minimal
database: sqlite
- python-version: 3.7
dependencies: minimal
database: postgresql
- python-version: 3.7
dependencies: minimal
database: mariadb
- python-version: 3.9
dependencies: minimal
database: sqlite
- python-version: "3.10"
dependencies: minimal
database: sqlite
- python-version: "3.11"
dependencies: minimal
database: sqlite
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: '**/pyproject.toml'
- name: Change dependencies to minimal supported versions
run: sed -i -e 's/>=/==/g; s/~=.*==\(.*\)/==\1/g; s/~=/==/g;' pyproject.toml
if: matrix.dependencies == 'minimal'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox
# Run tox using the version of Python in `PATH`
- name: Run Tox with sqlite
run: tox -e py
if: matrix.database == 'sqlite'
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'sqlite:///budget.db'
- name: Run Tox with postgresql
run: tox -e py
if: matrix.database == 'postgresql'
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci'
- name: Run Tox with mariadb
run: tox -e py
if: matrix.database == 'mariadb'
env:
TESTING_SQLALCHEMY_DATABASE_URI: 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci'
- name: Run Lint & Docs
run: tox -e lint_docs
if: matrix.python-version == '3.11'

1
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.pyc
*.egg-info
*.mo
dist
.venv
docs/_build/

View file

@ -1,13 +0,0 @@
[settings]
# Needed for black compatibility
multi_line_output=3
include_trailing_comma=True
force_grid_wrap=0
line_length=88
combine_as_imports=True
# If set, imports will be sorted within their section independent to the import_type.
force_sort_within_sections=True
# skip
skip_glob=.local,**/migrations/**,**/node_modules/**,**/node-forge/**

View file

@ -2,10 +2,40 @@
This document describes changes between each past release.
## 6.1.2 (unreleased)
## 6.2.0 (unreleased)
- Add support for python 3.12 (#757)
- Migrate from setup.cfg to pyproject.toml (#1243)
- Update to wtforms 3.1 (#1248)
- Document [repository rules](https://ihatemoney.readthedocs.io/en/latest/contributing.html#repository-rules) (#1253)
- Add "reimbursement" bills and allow to create them directly from the "Settle" page (#1290)
- Remove support for python 3.7
- Replace the black linter by ruff
- Replace virtualenv and pip by uv
- Remove tox
## 6.1.5 (2024-03-19)
- Fix README and changelog not being displayed on PyPI
- Fix ability to change project settings when project has existing currency (#1292)
- Update translations for Dutch and German
- Nothing changed yet.
## 6.1.4 (2023-12-14)
- Fix missing markdown include in manifest (#1274)
- Update translations for Chinese, Turkish, Czech, Spanish (Latin America), Swedish
## 6.1.3 (2023-11-23)
- Revert update to flask and werkzeug 2.3 because of a regression (see #1272)
## 6.1.2 (2023-11-19)
- Fix password generation command line crash (#1242)
- Update to flask and werkzeug 2.3 (#1244)
## 6.1.1 (2023-10-04)

View file

@ -1,3 +0,0 @@
include *.rst
recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.webp *.ini *.cfg *.j2 *.jpg *.gif *.ico *.xml
include LICENSE CONTRIBUTORS CHANGELOG.rst

View file

@ -1,60 +1,40 @@
VIRTUALENV = python3 -m venv
SPHINX_BUILDDIR = docs/_build
VIRTUALENV = uv venv
VENV := $(shell realpath $${VIRTUAL_ENV-.venv})
PYTHON = $(VENV)/bin/python3
BIN := uv tool run
PIP := uv pip
PYTHON = $(BIN)/python3
DEV_STAMP = $(VENV)/.dev_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d)
ZOPFLIPNG := zopflipng
MAGICK_MOGRIFY := mogrify
.PHONY: all
all: install ## Alias for install
.PHONY: install
install: virtualenv pyproject.toml $(INSTALL_STAMP) ## Install dependencies
$(INSTALL_STAMP):
$(VENV)/bin/pip install -U pip
$(VENV)/bin/pip install -e .
touch $(INSTALL_STAMP)
.PHONY: virtualenv
virtualenv: $(PYTHON)
$(PYTHON):
$(VIRTUALENV) $(VENV)
.PHONY: install-dev
install-dev: virtualenv pyproject.toml $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
$(DEV_STAMP): $(PYTHON)
$(VENV)/bin/pip install -Ue .[dev]
touch $(DEV_STAMP)
.PHONY: remove-install-stamp
remove-install-stamp:
rm $(INSTALL_STAMP)
.PHONY: update
update: remove-install-stamp install ## Update the dependencies
.PHONY: serve
serve: install ## Run the ihatemoney server
serve: build-translations ## Run the ihatemoney server
@echo 'Running ihatemoney on http://localhost:5000'
FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi $(VENV)/bin/flask run --host=0.0.0.0
FLASK_DEBUG=1 FLASK_APP=ihatemoney.wsgi uv run flask run --host=0.0.0.0
.PHONY: test
test: install-dev ## Run the tests
$(VENV)/bin/tox
test:
uv run --extra dev --extra database pytest .
.PHONY: black
black: install-dev ## Run the tests
$(VENV)/bin/black --target-version=py37 .
.PHONY: lint
lint:
uv tool run ruff check .
uv tool run vermin --no-tips --violations -t=3.8- .
.PHONY: isort
isort: install-dev ## Run the tests
$(VENV)/bin/isort .
.PHONY: format
format:
uv tool run ruff format .
.PHONY: release
release: install-dev ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
$(VENV)/bin/fullrelease
release: # Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
uv run --extra dev fullreleas
.PHONY: compress-showcase
compress-showcase:
@ -72,27 +52,30 @@ compress-assets: compress-showcase ## Compress static assets
.PHONY: build-translations
build-translations: ## Build the translations
$(VENV)/bin/pybabel compile -d ihatemoney/translations
uv run --extra dev pybabel compile -d ihatemoney/translations
.PHONY: update-translations
update-translations: ## Extract new translations from source code
$(VENV)/bin/pybabel extract --add-comments "I18N:" --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
$(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: extract-translations
extract-translations: ## Extract new translations from source code
uv run --extra dev pybabel extract --add-comments "I18N:" --strip-comments --omit-header --no-location --mapping-file ihatemoney/babel.cfg -o ihatemoney/messages.pot ihatemoney
uv run --extra dev pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: create-database-revision
create-database-revision: ## Create a new database revision
@read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
uv run python -m ihatemoney.manage db migrate -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: create-empty-database-revision
create-empty-database-revision: ## Create an empty database revision
@read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
uv run python -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: clean
clean: ## Destroy the virtual environment
rm -rf .venv
build-docs:
uv run --extra doc sphinx-build -a -n -b html -d docs/_build/doctrees docs docs/_build/html
.PHONY: help
help: ## Show the help indications
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

View file

@ -22,10 +22,41 @@ highly encouraged to do so.
## Requirements
- **Python**: version 3.7 to 3.11.
- **Python**: version 3.8 to 3.12.
- **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above),
Memory.
## Current direction (as of 2024)
Ihatemoney was started in 2011, and we believe the project has reached a certain
level of maturity now. The overall energy of contributors is not as high as it
used to be.
In addition, there are now several self-hosted alternatives (for instance
[cospend](https://github.com/julien-nc/cospend-nc/tree/main),
[spliit](https://github.com/spliit-app/spliit)).
As maintainers, we believe that the project is still relevant but should gear
towards some kind of "maintenance mode":
* **Simplicity** is now the main goal of the project. It has always been a compass
for the project, and the resulting software is appreciated by both users and
server administrators. For us, "simplicity" is positive and encompasses both
technical aspects (very few javascript code, manageable dependencies, small code
size...) and user-visible aspects (straightforward interface, no need to create
accounts for people you invite, same web interface on mobile...)
* **Stability** is prioritized over adding major new features. We found ourselves
complexifying the codebase (and the interface) while accepting some
contributions. Our goal now is to have a minimal set of features that do most of
the job. We believe this will help lower the maintainance burden.
* **User interface and user experience improvements** are always super welcome !
It is still possible to propose new features, but they should fit into
this new direction. Simplicity of the UI/UX and simplicity of the technical
implementation will be the main factors when deciding to accept new features.
## Contributing
Do you wish to contribute to IHateMoney? Fantastic! There's a lot of

View file

@ -4,9 +4,9 @@
| Version | Supported |
| ------- | ------------------ |
| 5.0.x | :heavy_check_mark: |
| 4.1.x | :heavy_check_mark: |
| <= 4.0 | :x: |
| 6.2.x | :heavy_check_mark: |
| 6.1.x | :heavy_check_mark: |
| <= 6.0 | :x: |
## Reporting a Vulnerability

View file

@ -127,11 +127,11 @@ ADMIN_PASSWORD needs to be set.
## APPLICATION_ROOT
If empty, ihatemoney will be served at domain root (e.g:
*http://domain.tld*), if set to `"somestring"`, it will be served from a
By default, ihatemoney will be served at domain root (e.g:
*http://domain.tld*), if set to `"/somestring"`, it will be served from a
"folder" (e.g: *http://domain.tld/somestring*).
- **Default value:** `""` (empty string)
- **Default value:** `"/"`
## BABEL_DEFAULT_TIMEZONE
@ -173,6 +173,14 @@ URL you want.
- **Default value:** `""` (empty string)
- **Production value:** The URL of your chosing.
## SITE_NAME
It is possible to change the name of the site to something at your liking.
- **Default value:** `"I Hate Money"` (empty string)
- **Production value:** The name of your choosing
## Configuring email sending
By default, Ihatemoney sends emails using a local SMTP server, but it's

View file

@ -1,5 +1,37 @@
# Contributing
## Current direction (as of 2024)
Ihatemoney was started in 2011, and we believe the project has reached a certain
level of maturity now. The overall energy of contributors is not as high as it
used to be.
In addition, there are now several self-hosted alternatives (for instance
[cospend](https://github.com/julien-nc/cospend-nc/tree/main),
[spliit](https://github.com/spliit-app/spliit)).
As maintainers, we believe that the project is still relevant but should gear
towards some kind of "maintenance mode":
* **Simplicity** is now the main goal of the project. It has always been a compass
for the project, and the resulting software is appreciated by both users and
server administrators. For us, "simplicity" is positive and encompasses both
technical aspects (very few javascript code, manageable dependencies, small code
size...) and user-visible aspects (straightforward interface, no need to create
accounts for people you invite, same web interface on mobile...)
* **Stability** is prioritized over adding major new features. We found ourselves
complexifying the codebase (and the interface) while accepting some
contributions. Our goal now is to have a minimal set of features that do most of
the job. We believe this will help lower the maintainance burden.
* **User interface and user experience improvements** are always super welcome !
It is still possible to propose new features, but they should fit into
this new direction. Simplicity of the UI/UX and simplicity of the technical
implementation will be the main factors when deciding to accept new features.
## How to contribute
You would like to contribute? First, thanks a bunch! This project is a
@ -46,6 +78,15 @@ Thanks again!
(setup-dev-environment)=
## Set up a dev environment
### Requirements
In addition to general {ref}`requirements<system-requirements>`, you will need
**uv**. It recommended to install uv [system
wide](https://docs.astral.sh/uv/getting-started/installation/#standalone-installer)
as it is a kind of replacement for pip.
### Getting the sources
You must develop on top of the Git master branch:
git clone https://github.com/spiral-project/ihatemoney.git
@ -151,7 +192,7 @@ We are using [black](https://black.readthedocs.io/en/stable/) and
Python files in this project. Be sure to run it locally on your files.
To do so, just run:
make black isort
make lint
You can also integrate them with your dev environment (as a
*format-on-save* hook, for instance).
@ -274,10 +315,9 @@ In order to issue a new release, follow the following steps:
make compress-assets
- Build the translations:
- Extract the translations:
make update-translations
make build-translations
make extract-translations
- If you're not completely sure of yourself at this point, you can
optionally: create a new branch, push it, open a pull request, check

View file

@ -26,7 +26,7 @@ hub](https://hub.docker.com/r/ihatemoney/ihatemoney/).
This is probably the simplest way to get something running. Once you
have Docker installed on your system, just issue :
docker run -d -p 8000:8000 ihatemoney/ihatemoney
docker run -d -p 8000:8000 ihatemoney/ihatemoney:latest
Ihatemoney is now available on <http://localhost:8000>.
@ -54,6 +54,10 @@ To enable the Admin dashboard, first generate a hashed password with:
docker run -it --rm --entrypoint ihatemoney ihatemoney/ihatemoney generate_password_hash
:::{note}
The generated password hash is salted. Which means that the same password will generate a different hash each time. This is normal and expected behavior.
:::
At the prompt, enter a password to use for the admin dashboard. The
command will print the hashed password string.
@ -62,12 +66,18 @@ Add these additional environment variables to the docker run invocation:
-e ACTIVATE_ADMIN_DASHBOARD=True \
-e ADMIN_PASSWORD=<hashed_password_string> \
:::{note}
If you are using a `docker-compose.yml` file and need to include a password hash, use `$$` instead of `$` to escape the dollar sign. This ensures that the hash is treated as a literal string rather than a variable in Bash.
:::
Additional gunicorn parameters can be passed using the docker `CMD`
parameter. For example, use the following command to add more gunicorn
workers:
docker run -d -p 8000:8000 ihatemoney/ihatemoney -w 3
If needed, there is a `docker-compose.yml` file available as an example on the [project repository](https://github.com/spiral-project/ihatemoney/blob/master/docker-compose.yml)
(cloud)=
## On a Cloud Provider
@ -83,7 +93,7 @@ Some Paas (Platform-as-a-Service), provide a documentation or even a quick insta
«Ihatemoney» depends on:
- **Python**: any version from 3.7 to 3.11 will work.
- **Python**: any version from 3.8 to 3.12 will work.
- **A database backend**: choose among SQLite, PostgreSQL, MariaDB (>=
10.3.2).
- **Virtual environment** (recommended): [python3-venv]{.title-ref}

11
hatch_build.py Normal file
View file

@ -0,0 +1,11 @@
import sys
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CustomBuildHook(BuildHookInterface):
def initialize(self, version, build_data):
sys.path.insert(0, "./ihatemoney")
from babel_utils import compile_catalogs
compile_catalogs()

View file

@ -1,3 +1,2 @@
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_

11
ihatemoney/babel_utils.py Normal file
View file

@ -0,0 +1,11 @@
from pathlib import Path
from babel.messages.frontend import compile_catalog
def compile_catalogs():
cmd = compile_catalog()
cmd.directory = Path(__file__).parent / "translations"
cmd.statistics = True
cmd.finalize_options()
cmd.run()

View file

@ -3,6 +3,7 @@ DEBUG = SQLACHEMY_ECHO = False
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/ihatemoney.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = "tralala"
SITE_NAME = "I Hate Money"
MAIL_DEFAULT_SENDER = "Budget manager <admin@example.com>"
SHOW_ADMIN_EMAIL = True
ACTIVATE_DEMO_PROJECT = True
@ -14,6 +15,7 @@ APPLICATION_ROOT = "/"
ENABLE_CAPTCHA = False
LEGAL_LINK = ""
SUPPORTED_LANGUAGES = [
"az",
"ca",
"cs",
"de",
@ -46,3 +48,4 @@ SUPPORTED_LANGUAGES = [
"uk",
"zh_Hans",
]
SHOWCASE_LANGUAGES = ["en", "fr"]

View file

@ -39,7 +39,7 @@ from wtforms.validators import (
)
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import Bill, LoggingMode, Person, Project
from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
from ihatemoney.utils import (
em_surround,
eval_arithmetic_expression,
@ -90,7 +90,6 @@ def get_billform_for(project, set_default=True, **kwargs):
class CommaDecimalField(DecimalField):
"""A class to deal with comma in Decimal Field"""
def process_formdata(self, value):
@ -195,7 +194,7 @@ class EditProjectForm(FlaskForm):
raise ValidationError(msg)
if (
project is not None
and field.data != CurrencyConverter.no_currency
and field.data != project.default_currency
and project.has_bills()
):
msg = _(
@ -364,6 +363,12 @@ class BillForm(FlaskForm):
payed_for = SelectMultipleField(
_("For whom?"), validators=[DataRequired()], coerce=int
)
bill_type = SelectField(
_("Bill Type"),
choices=BillType.choices(),
coerce=BillType,
default=BillType.EXPENSE,
)
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))
@ -377,12 +382,14 @@ class BillForm(FlaskForm):
payer_id=self.payer.data,
project_default_currency=project.default_currency,
what=self.what.data,
bill_type=self.bill_type.data,
)
def save(self, bill, project):
bill.payer_id = self.payer.data
bill.amount = self.amount.data
bill.what = self.what.data
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data
bill.date = self.date.data
bill.owers = Person.query.get_by_ids(self.payed_for.data, project)
@ -396,6 +403,7 @@ class BillForm(FlaskForm):
self.payer.data = bill.payer_id
self.amount.data = bill.amount
self.what.data = bill.what
self.bill_type.data = bill.bill_type
self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency
self.date.data = bill.date

View file

@ -38,6 +38,9 @@ def history_sort_key(history_item_dict):
def describe_version(version_obj):
"""Use the base model str() function to describe a version object"""
if not version_obj:
return ""
else:
return parent_class(type(version_obj)).__str__(version_obj)

View file

@ -4,6 +4,7 @@ import getpass
import os
import random
import sys
import datetime
import click
from flask.cli import FlaskGroup
@ -93,5 +94,31 @@ def delete_project(project_name):
db.session.commit()
@cli.command()
@click.argument("print_emails", default=False)
@click.argument("bills", default=0) # default values will get total projects
@click.argument("days", default=73000) # approximately 200 years
def get_project_count(print_emails, bills, days):
"""Count projets with at least x bills and at less than x days old"""
projects = [
pr
for pr in Project.query.all()
if pr.get_bills().count() > bills
and pr.get_bills()[0].date
> datetime.date.today() - datetime.timedelta(days=days)
]
click.secho("Number of projects: " + str(len(projects)))
if print_emails:
emails = set([pr.contact_email for pr in projects])
emails_str = ", ".join(emails)
if len(emails) > 1:
click.secho("Contact emails: " + emails_str)
elif len(emails) == 1:
click.secho("Contact email: " + emails_str)
else:
click.secho("No contact emails found")
if __name__ == "__main__":
cli()

View file

@ -759,7 +759,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -0,0 +1,34 @@
"""new bill type attribute added
Revision ID: 7a9b38559992
Revises: 927ed575acbd
Create Date: 2022-12-10 17:25:38.387643
"""
# revision identifiers, used by Alembic.
revision = "7a9b38559992"
down_revision = "927ed575acbd"
from alembic import op
import sqlalchemy as sa
from ihatemoney.models import BillType
def upgrade():
billtype_enum = sa.Enum(BillType)
billtype_enum.create(op.get_bind(), checkfirst=True)
op.add_column(
"bill",
sa.Column("bill_type", billtype_enum, server_default=BillType.EXPENSE.name),
)
op.add_column("bill_version", sa.Column("bill_type", sa.UnicodeText()))
def downgrade():
op.drop_column("bill", "bill_type")
op.drop_column("bill_version", "bill_type")
billtype_enum = sa.Enum(BillType)
billtype_enum.drop(op.get_bind())

View file

@ -1,5 +1,6 @@
from collections import defaultdict
import datetime
from enum import Enum
import itertools
from dateutil.parser import parse
@ -50,6 +51,16 @@ make_versioned(
],
)
class BillType(Enum):
EXPENSE = "Expense"
REIMBURSEMENT = "Reimbursement"
@classmethod
def choices(cls):
return [(choice.value, choice.value) for choice in cls]
db = SQLAlchemy()
@ -112,22 +123,33 @@ class Project(db.Model):
- dict mapping each member to how much he/she should be paid by
others (i.e. how much he/she has paid for bills)
balance spent paid
"""
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
for bill in self.get_bills_unordered().all():
should_receive[bill.payer.id] += bill.converted_amount
total_weight = sum(ower.weight for ower in bill.owers)
if bill.bill_type == BillType.EXPENSE:
should_receive[bill.payer.id] += bill.converted_amount
for ower in bill.owers:
should_pay[ower.id] += (
ower.weight * bill.converted_amount / total_weight
)
if bill.bill_type == BillType.REIMBURSEMENT:
should_receive[bill.payer.id] += bill.converted_amount
for ower in bill.owers:
should_receive[ower.id] -= bill.converted_amount
for person in self.members:
balance = should_receive[person.id] - should_pay[person.id]
balances[person.id] = balance
return balances, should_pay, should_receive
return (
balances,
should_pay,
should_receive,
)
@property
def balance(self):
@ -160,6 +182,7 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all():
if bill.bill_type == BillType.EXPENSE:
monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@ -186,7 +209,6 @@ class Project(db.Model):
)
return pretty_transactions
# cache value for better performance
members = {person.id: person for person in self.members}
settle_plan = settle(self.balance.items()) or []
@ -202,22 +224,6 @@ class Project(db.Model):
return prettify(transactions, pretty_output)
def exactmatch(self, credit, debts):
"""Recursively try and find subsets of 'debts' whose sum is equal to credit"""
if not debts:
return None
if debts[0]["balance"] > credit:
return self.exactmatch(credit, debts[1:])
elif debts[0]["balance"] == credit:
return [debts[0]]
else:
match = self.exactmatch(credit - debts[0]["balance"], debts[1:])
if match:
match.append(debts[0])
else:
match = self.exactmatch(credit, debts[1:])
return match
def has_bills(self):
"""return if the project do have bills or not"""
return self.get_bills_unordered().count() > 0
@ -336,6 +342,7 @@ class Project(db.Model):
pretty_bills.append(
{
"what": bill.what,
"bill_type": bill.bill_type.value,
"amount": round(bill.amount, 2),
"currency": bill.original_currency,
"date": str(bill.date),
@ -407,6 +414,7 @@ class Project(db.Model):
new_bill = Bill(
amount=b["amount"],
date=parse(b["date"]),
bill_type=b["bill_type"],
external_link="",
original_currency=b["currency"],
owers=Person.query.get_by_names(b["owers"], self),
@ -537,14 +545,15 @@ class Project(db.Model):
db.session.commit()
operations = (
("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping"),
("Alice", 20, ("Amina", "Alice"), "Beer !"),
("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP"),
("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping", "Expense"),
("Alice", 20, ("Amina", "Alice"), "Beer !", "Expense"),
("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP", "Expense"),
)
for payer, amount, owers, what in operations:
for payer, amount, owers, what, bill_type in operations:
db.session.add(
Bill(
amount=amount,
bill_type=bill_type,
original_currency=project.default_currency,
owers=[members[name] for name in owers],
payer_id=members[payer].id,
@ -677,6 +686,7 @@ class Bill(db.Model):
date = db.Column(db.Date, default=datetime.datetime.now)
creation_date = db.Column(db.Date, default=datetime.datetime.now)
what = db.Column(db.UnicodeText)
bill_type = db.Column(db.Enum(BillType))
external_link = db.Column(db.UnicodeText)
original_currency = db.Column(db.String(3))
@ -696,6 +706,7 @@ class Bill(db.Model):
payer_id: int = None,
project_default_currency: str = "",
what: str = "",
bill_type: str = "Expense",
):
super().__init__()
self.amount = amount
@ -705,6 +716,7 @@ class Bill(db.Model):
self.owers = owers
self.payer_id = payer_id
self.what = what
self.bill_type = BillType(bill_type)
self.converted_amount = self.currency_helper.exchange_currency(
self.amount, self.original_currency, project_default_currency
)
@ -719,6 +731,7 @@ class Bill(db.Model):
"date": self.date,
"creation_date": self.creation_date,
"what": self.what,
"bill_type": self.bill_type.value,
"external_link": self.external_link,
"original_currency": self.original_currency,
"converted_amount": self.converted_amount,

View file

@ -103,7 +103,7 @@ def validate_configuration(app):
if "MAIL_DEFAULT_SENDER" not in app.config:
app.config["MAIL_DEFAULT_SENDER"] = default_settings.DEFAULT_MAIL_SENDER
if type(app.config["MAIL_DEFAULT_SENDER"]) == tuple:
if type(app.config["MAIL_DEFAULT_SENDER"]) is tuple:
(name, address) = app.config["MAIL_DEFAULT_SENDER"]
app.config["MAIL_DEFAULT_SENDER"] = f"{name} <{address}>"
warnings.warn(

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View file

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View file

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 79 KiB

View file

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View file

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

View file

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View file

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View file

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View file

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

View file

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -200,6 +200,7 @@
{% if g.project.default_currency != "XXX" %}
{{ input(form.original_currency, inline=True, class="form-control custom-select") }}
{% endif %}
{{ input(form.bill_type, inline=True) }}
{{ input(form.external_link, inline=True) }}
</details>
</fieldset>

View file

@ -1,90 +1,81 @@
{% extends "layout.html" %}
{% block body %}
<header id="header" class="row">
<div class="col-xs-12 col-sm-5 offset-md-2">
<h2>{{ _("Manage your shared <br />expenses, easily") }}</h2>
<h2>
{{ _("Manage your shared <br />expenses, easily") }}
</h2>
{% if is_demo_project_activated %}
<a href="{{ url_for('.demo') }}" class="tryout btn">
{{ _("Try out the demo") }}
</a>
<a href="{{ url_for(".demo") }}" class="tryout btn">{{ _("Try out the demo") }}</a>
{% endif %}
{% if g.lang == 'fr' %}
ou
{% if g.lang == 'fr' or g.lang == 'en' %}
{{ _("or") }}
<span class="side-to-side">
<a class="showcase btn" onclick="javascript:showGallery(); return false;">Voir la BD explicative</a>
<img class="showcaseimg" src="{{ url_for("static", filename='images/indicate.svg') }}" />
<a class="showcase btn"
onclick="javascript:showGallery(); return false;">{{ _("See the explanatory comics") }}</a>
<img class="showcaseimg"
src="{{ url_for("static", filename='images/indicate.svg') }}" />
</span>
{% endif %}
</div>
<div class="col-xs-12 col-sm-4">
<table class="additional-content"><tr>
<table class="additional-content">
<tr>
<td>
{{ _("You're sharing a house?") }}<br />
{{ _("Going on holidays with friends?") }}<br />
{{ _("Simply sharing money with others?") }} <br />
{{ _("You're sharing a house?") }}
<br />
{{ _("Going on holidays with friends?") }}
<br />
{{ _("Simply sharing money with others?") }}
<br />
<strong>{{ _("We can help!") }}</strong>
</td>
<td>
<img class="shareimg" src="{{ url_for("static", filename='images/share.svg') }}" />
<img class="shareimg"
src="{{ url_for("static", filename='images/share.svg') }}" />
</td>
</tr></table>
</tr>
</table>
</div>
</header>
<main class="row home">
<div class="card-deck ml-auto mr-auto">
<div class="card">
<div class="card-header">
{{ _("Log in to an existing project") }}
</div>
<div class="card-header">{{ _("Log in to an existing project") }}</div>
<div class="card-body">
<form
id="authentication-form"
<form id="authentication-form"
class="form-horizontal"
action="{{ url_for('.authenticate') }}"
action="{{ url_for(".authenticate") }}"
method="post">
<fieldset class="form-group">
<legend></legend>
{{ forms.authenticate(auth_form, home=True) }}
</fieldset>
<div class="controls">
<button class="btn btn-primary btn-block" type="submit">
{{ _("Log in") }}
</button>
<a
class="password-reminder btn btn-link"
href="{{ url_for('.remind_password') }}"
>{{ _("can't remember your password?") }}</a
>
<button class="btn btn-primary btn-block" type="submit">{{ _("Log in") }}</button>
<a class="password-reminder btn btn-link"
href="{{ url_for(".remind_password") }}">{{ _("can't remember your password?") }}</a>
</div>
</form>
</div>
</div>
<div class="card">
<div class="card-header">
{{ _("Create a new project") }}
</div>
<div class="card-header">{{ _("Create a new project") }}</div>
<div class="card-body">
{% if is_public_project_creation_allowed %}
<form
id="creation-form"
<form id="creation-form"
class="form-horizontal"
action="{{ url_for('.create_project') }}"
method="post"
>
action="{{ url_for(".create_project") }}"
method="post">
<fieldset class="form-group">
{{ forms.create_project(project_form, home=True) }}
</fieldset>
<div class="controls">
<button class="btn btn-primary btn-block" type="submit">
{{ _("Create") }}
</button>
<button class="btn btn-primary btn-block" type="submit">{{ _("Create") }}</button>
</div>
</form>
{% else %}
<a href="{{ url_for('.create_project') }}">
{{ _("Create a new project") }}
</a>
<a href="{{ url_for(".create_project") }}">{{ _("Create a new project") }}</a>
{% endif %}
</div>
</div>

View file

@ -20,7 +20,7 @@
<!DOCTYPE html>
<html class="h-100">
<head>
<title>{{ _("Account manager") }}{% block title %}{% endblock %}</title>
<title>{{ SITE_NAME }} — {{ _("Account manager") }}{% block title %}{% endblock %}</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}">
@ -46,7 +46,7 @@
</script>
</head>
<body class="d-flex flex-column h-100">
{% if g.lang == 'fr' %}{% include "showcase.html" %}{% endif %}
{% if display_showcase %}{% include "showcase.html" %}{% endif %}
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarToggler" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
@ -168,7 +168,7 @@
<i class="icon book">{{ static_include("images/book.svg") | safe }}</i>
</a>
{% if g.show_admin_dashboard_link %}
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Administation Dashboard') }}" href="{{ url_for('main.dashboard') }}">
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Administration Dashboard') }}" href="{{ url_for('main.dashboard') }}">
<i class="icon admin">{{ static_include("images/cog.svg") | safe }}</i>
</a>
{% endif %}

View file

@ -64,10 +64,10 @@
</div>
</div>
<div class="identifier">
{% if g.lang == 'fr' %}
{% if display_showcase %}
<a class="btn btn-secondary btn-block" href="" onclick="javascript:showGallery(); return false;">
<i class="icon icon-white high before-text">{{ static_include("images/read.svg") | safe }}</i>
Voir la BD explicative
{{ _("See the explanatory comics") }}
</a>
{% endif %}
<a class="btn btn-secondary btn-block" href="{{ url_for('.invite') }}">

View file

@ -9,13 +9,22 @@
{% block content %}
<table id="bill_table" class="split_bills table table-striped">
<thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th></tr></thead>
<thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Settled?") }}</th></tr></thead>
<tbody>
{% for bill in bills %}
<tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</td>
<td>{{ bill.amount|currency }}</td>
<td>
<span id="settle-bill" class="ml-auto pb-2">
<a href="{{ url_for('.settle', amount = bill.amount, ower_id = bill.ower.id, payer_id = bill.receiver.id) }}" class="btn btn-primary">
<div data-toggle="tooltip" title='{{ _("Click here to record that the money transfer has been done") }}'>
{{ ("Settle") }}
</div>
</a>
</span>
</td>
</tr>
{% endfor %}
</tbody>

View file

@ -1,4 +1,8 @@
<div id="pswp" class="hiddenpswp" tabindex="-1" role="dialog" aria-hidden="true">
<div id="pswp"
class="hiddenpswp"
tabindex="-1"
role="dialog"
aria-hidden="true">
<div class="pswp__bg"></div>
<div class="pswp__scroll-wrap">
<div class="pswp__container">
@ -9,7 +13,7 @@
<div class="pswp__ui pswp__ui--hidden">
<div class="pswp__top-bar">
<div class="pswp__counter"></div>
<button class="pswp__button pswp__button--close" title="Fermer (Esc)"></button>
<button class="pswp__button pswp__button--close" title="{{ _("Close (Esc)") }}">
<div class="pswp__preloader">
<div class="pswp__preloader__icn">
<div class="pswp__preloader__cut">
@ -18,13 +22,10 @@
</div>
</div>
</div>
<button class="pswp__button pswp__button--arrow--left" title="Suivant (flèche droite)">
</button>
<button class="pswp__button pswp__button--arrow--right" title="Précédent (flèche gauche)">
</button>
<button class="pswp__button pswp__button--arrow--left"
title="{{ _("Next (right arrow)") }}"></button>
<button class="pswp__button pswp__button--arrow--right"
title="{{ _("Previous (left arrow)") }}"></button>
<div class="pswp__caption">
<div class="pswp__caption__center"></div>
</div>
@ -32,18 +33,26 @@
</div>
</div>
<script type="text/javascript">
var pswpElement = document.getElementById('pswp');
var items = JSON.parse('[{"h": 2450, "src": "{{ url_for("static", filename="showcase/1.webp") }}", "w": 2450}, {"h": 2509, "src": "{{ url_for("static", filename="showcase/2.webp") }}", "w": 2221}, {"h": 2536, "src": "{{ url_for("static", filename="showcase/3.webp") }}", "w": 2101}, {"h": 2722, "src": "{{ url_for("static", filename="showcase/4.webp") }}", "w": 2348}, {"h": 2745, "src": "{{ url_for("static", filename="showcase/5.webp") }}", "w": 1804}, {"h": 3307, "src": "{{ url_for("static", filename="showcase/6.webp") }}", "w": 2897}, {"h": 2321, "src": "{{ url_for("static", filename="showcase/7.webp") }}", "w": 2239}, {"h": 2470, "src": "{{ url_for("static", filename="showcase/8.webp") }}", "w": 2419}, {"h": 3307, "src": "{{ url_for("static", filename="showcase/9.webp") }}", "w": 2602}]');
var pswpElement = document.getElementById("pswp");
var options = {
index: 0,
loop: false,
};
function showGallery(){
let items = [
{"h": 2450, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/1.webp') }}", "w": 2450},
{"h": 2509, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/2.webp') }}", "w": 2221},
{"h": 2536, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/3.webp') }}", "w": 2101},
{"h": 2722, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/4.webp') }}", "w": 2348},
{"h": 2745, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/5.webp') }}", "w": 1804},
{"h": 3307, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/6.webp') }}", "w": 2897},
{"h": 2321, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/7.webp') }}", "w": 2239},
{"h": 2470, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/8.webp') }}", "w": 2419},
{"h": 3307, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/9.webp') }}", "w": 2602}
];
/* the CSS and JS for photoswipe is loaded dynamically
* so that they are not loaded if the gallery is not open */
// CSS and JS are loaded dynamically when the gallery opens
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/default-skin/default-skin.css") }}">');
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/photoswipe.css") }}">');
@ -63,5 +72,4 @@ function showGallery(){
};
document.body.appendChild(script);
}
</script>

View file

@ -11,7 +11,7 @@
</tr>
</thead>
{%- endif %}
{%- for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
{%- for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2)|abs > 0.01 %}
<tr id="bal-member-{{ member.id }}" action="{% if member.activated %}delete{% else %}reactivate{% endif %}">
<td class="balance-name">{{ member.name }}
{%- if show_weight -%}

View file

@ -9,7 +9,6 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
class TestAPI(IhatemoneyTestCase):
"""Tests the API"""
def api_create(
@ -43,7 +42,7 @@ class TestAPI(IhatemoneyTestCase):
def get_auth(self, username, password=None):
password = password or username
base64string = (
base64.encodebytes(f"{username}:{password}".encode("utf-8"))
base64.encodebytes(f"{username}:{password}".encode("utf-8")) # noqa: E231
.decode("utf-8")
.replace("\n", "")
)
@ -407,6 +406,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -431,6 +431,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -462,6 +463,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -479,6 +481,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "beer",
"payer": "2",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -500,6 +503,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-09-10",
"external_link": "https://raclette.fr",
@ -554,6 +558,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": input_amount,
},
headers=self.get_auth("raclette"),
@ -578,6 +583,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
@ -611,6 +617,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": amount,
},
headers=self.get_auth("raclette"),
@ -658,6 +665,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -682,6 +690,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -706,6 +715,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "30",
"external_link": "https://raclette.fr",
"original_currency": "CAD",
@ -727,6 +737,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1.0},
],
"bill_type": "Expense",
"amount": 30.0,
"date": "2011-08-10",
"id": 1,
@ -747,6 +758,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "Pierogi",
"payer": "1",
"payed_for": ["2", "3"],
"bill_type": "Expense",
"amount": "80",
"original_currency": "PLN",
},
@ -791,6 +803,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@ -855,6 +868,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@ -877,6 +891,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeannedy familly", "weight": 4},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -962,6 +977,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "Expense",
"amount": "0",
},
headers=self.get_auth("raclette"),
@ -990,8 +1006,71 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
def test_validate_bill_type(self):
self.api_create("raclette")
self.api_add_member("raclette", "zorglub")
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "wrong_bill_type",
"amount": "50",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "Expense",
"amount": "50",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(201, req)
def test_default_bill_type(self):
self.api_create("raclette")
self.api_add_member("raclette", "zorglub")
# Post a bill without adding a bill type
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"amount": "50",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(201, req)
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
# Bill type should now be "Expense"
got = json.loads(req.data.decode("utf-8"))
assert got["bill_type"] == "Expense"

View file

@ -1,10 +1,9 @@
from collections import defaultdict
import datetime
from datetime import datetime, timedelta, date
import re
from urllib.parse import unquote, urlparse, urlunparse
from flask import session, url_for
from libfaketime import fake_time
import pytest
from werkzeug.security import check_password_hash
@ -239,7 +238,10 @@ class TestBudget(IhatemoneyTestCase):
url, data={"password": "pass", "password_confirmation": "pass"}
)
resp = self.login("raclette", password="pass")
assert "<title>Account manager - raclette</title>" in resp.data.decode("utf-8")
assert (
"<title>I Hate Money — Account manager - raclette</title>"
in resp.data.decode("utf-8")
)
# Test empty and null tokens
resp = self.client.get("/reset-password")
assert "No token provided" in resp.data.decode("utf-8")
@ -428,6 +430,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": jeanne_id,
"payed_for": [jeanne_id],
"bill_type": "Expense",
"amount": "25",
},
)
@ -479,6 +482,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": zorglub.id,
"payed_for": [zorglub.id],
"bill_type": "Expense",
"amount": "25",
},
)
@ -646,6 +650,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "25",
},
)
@ -661,6 +666,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "10",
},
)
@ -684,6 +690,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "19",
},
)
@ -695,6 +702,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[1],
"payed_for": members_ids[0],
"bill_type": "Expense",
"amount": "20",
},
)
@ -706,6 +714,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[1],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "17",
},
)
@ -721,6 +730,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "-25",
},
)
@ -735,6 +745,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "25,02",
},
)
@ -749,6 +760,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "42",
"external_link": "https://example.com/fromage",
},
@ -764,12 +776,63 @@ class TestBudget(IhatemoneyTestCase):
"what": "mauvais fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "42000",
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
},
)
assert "Invalid URL" in resp.data.decode("utf-8")
def test_reimbursement_bill(self):
self.post_project("rent")
# add two participants
self.client.post("/rent/members/add", data={"name": "bob"})
self.client.post("/rent/members/add", data={"name": "alice"})
members_ids = [m.id for m in self.get_project("rent").members]
# create a bill to test reimbursement
self.client.post(
"/rent/add",
data={
"date": "2022-12-12",
"what": "december rent",
"payer": members_ids[0], # bob
"payed_for": members_ids, # bob and alice
"bill_type": "Expense",
"amount": "1000",
},
)
# check balance
balance = self.get_project("rent").balance
assert set(balance.values()), set([500 == -500])
# check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 1000
assert alice_paid == 0
# test reimbursement bill
self.client.post(
"/rent/add",
data={
"date": "2022-12-13",
"what": "reimbursement for rent",
"payer": members_ids[1], # alice
"payed_for": members_ids[0], # bob
"bill_type": "Reimbursement",
"amount": "500",
},
)
balance = self.get_project("rent").balance
assert set(balance.values()), set([0 == 0])
# check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 500
assert alice_paid == 500
def test_weighted_balance(self):
self.post_project("raclette")
@ -789,6 +852,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "10",
},
)
@ -800,6 +864,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "pommes de terre",
"payer": members_ids[1],
"payed_for": members_ids,
"bill_type": "Expense",
"amount": "10",
},
)
@ -864,6 +929,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "24.36",
},
)
@ -875,6 +941,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "19.12",
},
)
@ -886,6 +953,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "22",
},
)
@ -964,9 +1032,7 @@ class TestBudget(IhatemoneyTestCase):
assert """<thead>
<tr>
<th>Project</th>
<th>Number of participants</th>""" in resp.data.decode(
"utf-8"
)
<th>Number of participants</th>""" in resp.data.decode("utf-8")
def test_dashboard_project_deletion(self):
self.post_project("raclette")
@ -1019,6 +1085,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -1030,6 +1097,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -1041,6 +1109,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -1077,7 +1146,7 @@ class TestBudget(IhatemoneyTestCase):
assert re.search(re.compile(regex2, re.DOTALL), response.data.decode("utf-8"))
# Check monthly expenses again: it should have a single month and the correct amount
august = datetime.date(year=2011, month=8, day=1)
august = date(year=2011, month=8, day=1)
assert project.active_months_range() == [august]
assert dict(project.monthly_stats[2011]) == {8: 40.0}
@ -1089,15 +1158,16 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 2,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "30",
},
)
months = [
datetime.date(year=2011, month=12, day=1),
datetime.date(year=2011, month=11, day=1),
datetime.date(year=2011, month=10, day=1),
datetime.date(year=2011, month=9, day=1),
datetime.date(year=2011, month=8, day=1),
date(year=2011, month=12, day=1),
date(year=2011, month=11, day=1),
date(year=2011, month=10, day=1),
date(year=2011, month=9, day=1),
date(year=2011, month=8, day=1),
]
amounts_2011 = {
12: 30.0,
@ -1114,6 +1184,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "ice cream",
"payer": 2,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -1129,6 +1200,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "champomy",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -1144,10 +1216,11 @@ class TestBudget(IhatemoneyTestCase):
"what": "smoothie",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20",
},
)
months.append(datetime.date(year=2011, month=7, day=1))
months.append(date(year=2011, month=7, day=1))
amounts_2011[7] = 20.0
assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1160,10 +1233,11 @@ class TestBudget(IhatemoneyTestCase):
"what": "more champomy",
"payer": 2,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "30",
},
)
months.insert(0, datetime.date(year=2012, month=1, day=1))
months.insert(0, date(year=2012, month=1, day=1))
amounts_2012 = {1: 30.0}
assert project.active_months_range() == months
assert dict(project.monthly_stats[2011]) == amounts_2011
@ -1192,6 +1266,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -1203,6 +1278,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -1214,6 +1290,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -1229,6 +1306,76 @@ class TestBudget(IhatemoneyTestCase):
assert abs(a - balance[m.id]) < 0.01
return
def test_settle_button(self):
self.post_project("raclette")
# add participants
self.client.post("/raclette/members/add", data={"name": "zorglub"})
self.client.post("/raclette/members/add", data={"name": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
# Add a participant with a balance at 0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})
# create bills
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
project = self.get_project("raclette")
transactions = project.get_transactions_to_settle_bill()
count = 0
for t in transactions:
count += 1
self.client.get(
"/raclette/settle"
+ "/"
+ str(t["amount"])
+ "/"
+ str(t["ower"].id)
+ "/"
+ str(t["receiver"].id)
)
temp_transactions = project.get_transactions_to_settle_bill()
# test if the one has disappeared
assert len(temp_transactions) == len(transactions) - count
# test if theres a new one with bill_type reimbursement
bill = project.get_newest_bill()
assert bill.bill_type == models.BillType.REIMBURSEMENT
return
def test_settle_zero(self):
self.post_project("raclette")
@ -1245,6 +1392,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -1256,6 +1404,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
"bill_type": "Expense",
"amount": "20",
},
)
@ -1267,6 +1416,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund",
"payer": 3,
"payed_for": [2],
"bill_type": "Expense",
"amount": "13.33",
},
)
@ -1299,6 +1449,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3, 4],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -1317,6 +1468,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "roblochon",
"payer": 2,
"payed_for": [1, 3, 4],
"bill_type": "Expense",
"amount": "100.0",
}
# Try to access bill of another project
@ -1422,6 +1574,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -1433,6 +1586,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
"bill_type": "Expense",
"amount": "20",
},
)
@ -1444,6 +1598,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund",
"payer": 3,
"payed_for": [2],
"bill_type": "Expense",
"amount": "13.33",
},
)
@ -1469,6 +1624,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund from EUR",
"payer": 3,
"payed_for": [2],
"bill_type": "Expense",
"amount": "20",
"original_currency": "EUR",
},
@ -1492,6 +1648,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "Poutine",
"payer": 3,
"payed_for": [2],
"bill_type": "Expense",
"amount": "18",
"original_currency": "CAD",
},
@ -1548,6 +1705,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10.0",
"original_currency": "EUR",
},
@ -1583,6 +1741,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10.0",
"original_currency": "EUR",
},
@ -1595,6 +1754,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "aspirine",
"payer": 2,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "5.0",
"original_currency": "EUR",
},
@ -1629,6 +1789,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "0",
"original_currency": "XXX",
},
@ -1673,6 +1834,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12",
"original_currency": "EUR",
},
@ -1708,7 +1870,6 @@ class TestBudget(IhatemoneyTestCase):
"""
Tests that the RSS feed output content is expected.
"""
with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", default_currency="EUR")
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
@ -1723,6 +1884,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
@ -1734,6 +1896,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
@ -1745,6 +1908,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
@ -1752,12 +1916,10 @@ class TestBudget(IhatemoneyTestCase):
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
content = resp.data.decode()
assert (
f"""<channel>
<title>I Hate Money raclette</title>
<description>Latest bills from raclette</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
@ -1767,32 +1929,18 @@ class TestBudget(IhatemoneyTestCase):
<guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : 4.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>charcuterie - 15.00</title>
<guid isPermaLink="false">2</guid>
<dc:creator>peter</dc:creator>
<description>December 30, 2016 - george, peter : 7.50</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>vin blanc - 10.00</title>
<guid isPermaLink="false">3</guid>
<dc:creator>peter</dc:creator>
<description>December 29, 2016 - george, peter : 5.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
</channel>
</rss>""" # noqa: E501
assert resp.data.decode() == expected_rss_content
"""
in content
)
assert """<title>charcuterie - €15.00</title>""" in content
assert """<title>vin blanc - €10.00</title>""" in content
def test_rss_feed_history_disabled(self):
"""
Tests that RSS feeds is correctly rendered even if the project
history is disabled.
"""
with fake_time("2023-07-25 12:00:00"):
self.post_project("raclette", default_currency="EUR", project_history=False)
self.client.post("/raclette/members/add", data={"name": "george"})
self.client.post("/raclette/members/add", data={"name": "peter"})
@ -1807,6 +1955,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
@ -1818,6 +1967,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
self.client.post(
@ -1829,6 +1979,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
"bill_type": "Expense",
},
)
@ -1836,44 +1987,12 @@ class TestBudget(IhatemoneyTestCase):
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:atom="http://www.w3.org/2005/Atom"
>
<channel>
<title>I Hate Money raclette</title>
<description>Latest bills from raclette</description>
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
<link>http://localhost/raclette/</link>
<item>
<title>fromage à raclette - 12.00</title>
<guid isPermaLink="false">1</guid>
<dc:creator>george</dc:creator>
<description>December 31, 2016 - george, peter, steven : 4.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>charcuterie - 15.00</title>
<guid isPermaLink="false">2</guid>
<dc:creator>peter</dc:creator>
<description>December 30, 2016 - george, peter : 7.50</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
<item>
<title>vin blanc - 10.00</title>
<guid isPermaLink="false">3</guid>
<dc:creator>peter</dc:creator>
<description>December 29, 2016 - george, peter : 5.00</description>
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
</item>
</channel>
</rss>""" # noqa: E501
assert resp.data.decode() == expected_rss_content
content = resp.data.decode()
assert """<title>charcuterie - €15.00</title>""" in content
assert """<title>vin blanc - €10.00</title>""" in content
def test_rss_if_modified_since_header(self):
# Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
@ -1881,22 +2000,33 @@ class TestBudget(IhatemoneyTestCase):
resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.status_code == 200
assert resp.headers.get("Last-Modified") == "Wed, 26 Jul 2023 13:00:00 UTC"
assert "Last-Modified" in resp.headers.keys()
last_modified = resp.headers.get("Last-Modified")
# Get a date 1 hour before the last modified date
before = datetime.strptime(
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
) - timedelta(hours=1)
before_str = before.strftime("%a, %d %b %Y %H:%M:%S %Z")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 26 Jul 2023 12:00:00 UTC"},
headers={"If-Modified-Since": before_str},
)
assert resp.status_code == 200
after = datetime.strptime(
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
) + timedelta(hours=1)
after_str = after.strftime("%a, %d %b %Y %H:%M:%S %Z")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 26 Jul 2023 14:00:00 UTC"},
headers={"If-Modified-Since": after_str},
)
assert resp.status_code == 304
# Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette")
resp = self.client.post(
"/raclette/add",
@ -1907,6 +2037,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1],
"amount": "12",
"original_currency": "XXX",
"bill_type": "Expense",
},
follow_redirects=True,
)
@ -1915,38 +2046,34 @@ class TestBudget(IhatemoneyTestCase):
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 27 Jul 2023 12:00:00 UTC"},
headers={"If-Modified-Since": before_str},
)
assert resp.headers.get("Last-Modified") == "Thu, 27 Jul 2023 13:00:00 UTC"
assert resp.status_code == 200
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={"If-Modified-Since": "Tue, 27 Jul 2023 14:00:00 UTC"},
headers={"If-Modified-Since": after_str},
)
assert resp.status_code == 304
def test_rss_etag_headers(self):
# Project creation
with fake_time("2023-07-26 13:00:00"):
self.post_project("raclette")
self.client.post("/raclette/members/add", data={"name": "george"})
project = self.get_project("raclette")
token = project.generate_token("feed")
resp = self.client.get(f"/raclette/feed/{token}.xml")
assert resp.headers.get("ETag") == build_etag(
project.id, "2023-07-26T13:00:00"
)
etag = resp.headers.get("ETag")
assert resp.status_code == 200
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-26T12:00:00"),
"If-None-Match": etag,
},
)
assert resp.status_code == 200
assert resp.status_code == 304
resp = self.client.get(
f"/raclette/feed/{token}.xml",
@ -1954,10 +2081,9 @@ class TestBudget(IhatemoneyTestCase):
"If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"),
},
)
assert resp.status_code == 304
assert resp.status_code == 200
# Add bill
with fake_time("2023-07-27 13:00:00"):
self.login("raclette")
resp = self.client.post(
"/raclette/add",
@ -1967,26 +2093,26 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1,
"payed_for": [1],
"amount": "12",
"bill_type": "Expense",
"original_currency": "XXX",
},
follow_redirects=True,
)
assert resp.status_code == 200
assert "The bill has been added" in resp.data.decode()
etag = resp.headers.get("ETag")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-27T12:00:00"),
},
headers={"If-None-Match": etag},
)
assert resp.headers.get("ETag") == build_etag(project.id, "2023-07-27T13:00:00")
assert resp.status_code == 200
new_etag = resp.headers.get("ETag")
resp = self.client.get(
f"/raclette/feed/{token}.xml",
headers={
"If-None-Match": build_etag(project.id, "2023-07-27T13:00:00"),
"If-None-Match": new_etag,
},
)
assert resp.status_code == 304
@ -2074,6 +2200,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1],
"payed_for": members_ids,
"amount": "25",
"bill_type": "Expense",
},
)
@ -2091,6 +2218,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids_tartif[2],
"payed_for": members_ids_tartif,
"amount": "24",
"bill_type": "Expense",
},
)
@ -2125,6 +2253,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1],
"payed_for": members_ids[1:],
"amount": "25",
"bill_type": "Expense",
},
)

View file

@ -1,17 +1,32 @@
from unittest.mock import MagicMock
from flask import Flask
from jinja2 import FileSystemBytecodeCache
import pytest
from ihatemoney.babel_utils import compile_catalogs
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.run import create_app, db
@pytest.fixture(autouse=True, scope="session")
def babel_catalogs():
compile_catalogs()
@pytest.fixture(scope="session")
def jinja_cache_directory(tmp_path_factory):
return tmp_path_factory.mktemp("cache")
@pytest.fixture
def app(request: pytest.FixtureRequest):
def app(request: pytest.FixtureRequest, jinja_cache_directory):
"""Create the Flask app with database"""
app = create_app(request.cls)
# Caches the jinja templates so they are compiled only once per test session
app.jinja_env.bytecode_cache = FileSystemBytecodeCache(jinja_cache_directory)
with app.app_context():
db.create_all()
request.cls.app = app

View file

@ -212,6 +212,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
@ -228,6 +229,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
@ -371,6 +373,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
@ -391,6 +394,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "new thing",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
@ -477,6 +481,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25",
},
)
@ -487,6 +492,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20",
},
)
@ -505,6 +511,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "88",
},
)
@ -545,6 +552,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25",
},
)
@ -567,6 +575,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20",
},
)
@ -627,6 +636,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "10",
"original_currency": "EUR",
},

View file

@ -3,6 +3,7 @@
DEBUG = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db'
SQLACHEMY_ECHO = DEBUG
SITE_NAME = "I Hate Money"
SECRET_KEY = "supersecret"

View file

@ -16,11 +16,13 @@ def import_data(request: pytest.FixtureRequest):
"amount": 13.33,
"payer_name": "tata",
"payer_weight": 1.0,
"bill_type": "Expense",
"owers": ["jeanne"],
},
{
"date": "2016-12-31",
"what": "red wine",
"bill_type": "Expense",
"amount": 200.0,
"payer_name": "jeanne",
"payer_weight": 1.0,
@ -28,6 +30,7 @@ def import_data(request: pytest.FixtureRequest):
},
{
"date": "2016-12-31",
"bill_type": "Expense",
"what": "fromage a raclette",
"amount": 10.0,
"payer_name": "zorglub",
@ -48,6 +51,7 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Expense",
"amount": 13.33,
"payer_name": "tata",
"payer_weight": 1.0,
@ -56,6 +60,7 @@ class CommonTestCase(object):
{
"date": "2016-12-31",
"what": "red wine",
"bill_type": "Expense",
"amount": 200.0,
"payer_name": "jeanne",
"payer_weight": 1.0,
@ -63,7 +68,8 @@ class CommonTestCase(object):
},
{
"date": "2016-12-31",
"what": "fromage a raclette",
"what": "a raclette",
"bill_type": "Expense",
"amount": 10.0,
"payer_name": "zorglub",
"payer_weight": 2.0,
@ -108,6 +114,7 @@ class CommonTestCase(object):
assert b["currency"] == d["currency"]
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@ -150,6 +157,7 @@ class CommonTestCase(object):
assert b["currency"] == "XXX"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@ -208,6 +216,7 @@ class CommonTestCase(object):
assert b["currency"] == "EUR"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@ -247,6 +256,7 @@ class CommonTestCase(object):
assert b["currency"] == "XXX"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@ -271,6 +281,7 @@ class CommonTestCase(object):
data={
"date": "2016-12-31",
"what": "red wine",
"bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "200",
@ -303,6 +314,7 @@ class CommonTestCase(object):
assert b["currency"] == d["currency"]
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@ -326,6 +338,7 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["jeanne"],
@ -353,7 +366,8 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"bill_type": "Expense",
"what": "à raclette",
"payer": 1,
"payed_for": [1, 2, 3, 4],
"amount": "10.0",
@ -364,6 +378,7 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
"bill_type": "Expense",
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
@ -375,6 +390,7 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2017-01-01",
"bill_type": "Reimbursement",
"what": "refund",
"payer": 3,
"payed_for": [2],
@ -387,6 +403,7 @@ class TestExport(IhatemoneyTestCase):
expected = [
{
"date": "2017-01-01",
"bill_type": "Reimbursement",
"what": "refund",
"amount": 13.33,
"currency": "XXX",
@ -396,6 +413,7 @@ class TestExport(IhatemoneyTestCase):
},
{
"date": "2016-12-31",
"bill_type": "Expense",
"what": "red wine",
"amount": 200.0,
"currency": "XXX",
@ -405,7 +423,8 @@ class TestExport(IhatemoneyTestCase):
},
{
"date": "2016-12-31",
"what": "fromage \xe0 raclette",
"bill_type": "Expense",
"what": "\xe0 raclette",
"amount": 10.0,
"currency": "XXX",
"payer_name": "zorglub",
@ -418,10 +437,10 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
"date,what,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,XXX,13.33,tata,1.0,jeanne",
'2016-12-31,red wine,XXX,200.0,jeanne,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,Reimbursement,XXX,13.33,tata,1.0,jeanne",
'2016-12-31,red wine,Expense,XXX,200.0,jeanne,1.0,"zorglub, tata"',
'2016-12-31,à raclette,Expense,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
]
received_lines = resp.data.decode("utf-8").split("\n")
@ -481,7 +500,8 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
"what": "fromage à raclette",
"what": "à raclette",
"bill_type": "Expense",
"payer": 1,
"payed_for": [1, 2, 3, 4],
"amount": "10.0",
@ -494,6 +514,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "poutine from Québec",
"bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "100",
@ -506,6 +527,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"payer": 3,
"payed_for": [2],
"amount": "13.33",
@ -519,6 +541,7 @@ class TestExport(IhatemoneyTestCase):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"amount": 13.33,
"currency": "EUR",
"payer_name": "tata",
@ -528,6 +551,7 @@ class TestExport(IhatemoneyTestCase):
{
"date": "2016-12-31",
"what": "poutine from Qu\xe9bec",
"bill_type": "Expense",
"amount": 100.0,
"currency": "CAD",
"payer_name": "jeanne",
@ -537,6 +561,7 @@ class TestExport(IhatemoneyTestCase):
{
"date": "2016-12-31",
"what": "fromage \xe0 raclette",
"bill_type": "Expense",
"amount": 10.0,
"currency": "EUR",
"payer_name": "zorglub",
@ -549,10 +574,10 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
"date,what,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,13.33,EUR,tata,1.0,jeanne",
'2016-12-31,poutine from Québec,100.0,CAD,jeanne,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
"2017-01-01,refund,Reimbursement,13.33,EUR,tata,1.0,jeanne",
'2016-12-31,poutine from Québec,Expense,100.0,CAD,jeanne,1.0,"zorglub, tata"',
'2016-12-31,à raclette,Expense,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
]
received_lines = resp.data.decode("utf-8").split("\n")
@ -643,6 +668,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "=COS(36)",
"bill_type": "Expense",
"payer": 1,
"payed_for": [1],
"amount": "10.0",
@ -653,8 +679,8 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
"date,what,amount,currency,payer_name,payer_weight,owers",
"2016-12-31,'=COS(36),10.0,EUR,zorglub,1.0,zorglub",
"date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
"2016-12-31,'=COS(36),Expense,10.0,EUR,zorglub,1.0,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")

View file

@ -3,13 +3,17 @@ import smtplib
import socket
from unittest.mock import MagicMock, patch
import pytest
from sqlalchemy import orm
from werkzeug.security import check_password_hash
from ihatemoney import models
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.manage import delete_project, generate_config, password_hash
from ihatemoney.manage import (
delete_project,
generate_config,
get_project_count,
password_hash,
)
from ihatemoney.run import load_configuration
from ihatemoney.tests.common.ihatemoney_testcase import BaseTestCase, IhatemoneyTestCase
@ -126,6 +130,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -137,6 +142,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -148,6 +154,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -181,6 +188,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -192,6 +200,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -203,6 +212,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -223,6 +233,65 @@ class TestModels(IhatemoneyTestCase):
pay_each_expected = 10 / 3
assert bill.pay_each() == pay_each_expected
def test_demo_project_count(self):
"""Test command the get-project-count"""
self.post_project("raclette")
# add members
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
self.client.post("/raclette/members/add", data={"name": "fred"})
self.client.post("/raclette/members/add", data={"name": "tata"})
self.client.post("/raclette/members/add", data={"name": "pépé"})
# create bills
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "10.0",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "red wine",
"payer": 2,
"payed_for": [1],
"amount": "20",
},
)
assert self.get_project("raclette").has_bills()
# Now check the different parameters
runner = self.app.test_cli_runner()
result0 = runner.invoke(get_project_count)
assert result0.output.strip() == "Number of projects: 1"
# With more than 1 bill, without printing emails
result1 = runner.invoke(get_project_count, "False 1")
assert result1.output.strip() == "Number of projects: 1"
# With more than 2 bill, without printing emails
result2 = runner.invoke(get_project_count, "False 2")
assert result2.output.strip() == "Number of projects: 0"
# With more than 0 days old
result3 = runner.invoke(get_project_count, "False 0 0")
assert result3.output.strip() == "Number of projects: 0"
result4 = runner.invoke(get_project_count, "False 0 20000")
assert result4.output.strip() == "Number of projects: 1"
# Print emails
result5 = runner.invoke(get_project_count, "True")
assert "raclette@notmyidea.org" in result5.output
class TestEmailFailure(IhatemoneyTestCase):
def test_creation_email_failure_smtp(self):
@ -395,9 +464,7 @@ class TestCurrencyConverter:
def test_failing_remote(self):
rates = {}
with patch("requests.Response.json", new=lambda _: {}), pytest.warns(
UserWarning
):
with patch("requests.Response.json", new=lambda _: {}):
# we need a non-patched converter, but it seems that MagickMock
# is mocking EVERY instance of the class method. Too bad.
rates = CurrencyConverter.get_rates(self.converter)

File diff suppressed because it is too large Load diff

View file

@ -782,7 +782,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -785,7 +785,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2022-09-12 15:25+0000\n"
"Last-Translator: Maite Guix <maite.guix@gmail.com>\n"
"PO-Revision-Date: 2024-07-03 19:09+0000\n"
"Last-Translator: Quentin PAGÈS <quentinantonin@free.fr>\n"
"Language-Team: Catalan <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/ca/>\n"
"Language: ca\n"
"Language-Team: Catalan <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/ca/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.7-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -222,7 +222,7 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}"
msgid "No Currency"
msgstr "Sense moneda"
msgstr "Cap moneda"
#. Form error with only one error
msgid "{prefix}: {error}"
@ -826,7 +826,7 @@ msgstr "Aplicació mòbil"
msgid "Documentation"
msgstr "Documentació"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panell d'administració"
msgid "Legal information"
@ -1082,4 +1082,3 @@ msgstr "Període"
#~ msgstr ""
#~ "Pots compartir directament l'enllaç següent"
#~ " a través del teu mitjà preferit"

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-08-20 00:58+0000\n"
"Last-Translator: Ettore Atalan <atalanttore@googlemail.com>\n"
"PO-Revision-Date: 2024-02-24 00:03+0000\n"
"Last-Translator: Peter <peteramried@web.de>\n"
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/de/>\n"
"Language: de\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.0-dev\n"
"X-Generator: Weblate 5.5-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -35,7 +35,7 @@ msgid "Current private code"
msgstr "Aktueller privater Code"
msgid "Enter existing private code to edit project"
msgstr ""
msgstr "Geben Sie Ihren privaten Code ein, um das Projekt zu bearbeiten"
msgid "New private code"
msgstr "Neuer privater Code"
@ -197,16 +197,15 @@ msgid "Logout"
msgstr "Ausloggen"
msgid "Please check the email configuration of the server."
msgstr ""
msgstr "Bitte überprüfen Sie die E-Mail Konfiguration des Servers."
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails "
"auf. Bitte überprüfe die E-Mail Konfiguration des Servers oder "
"kontaktiere einen Administrator."
"Bitte überprüfe die E-Mail Konfiguration des Servers oder kontaktiere einen "
"Administrator: %(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -235,11 +234,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts."
msgstr ""
"Zu viele fehlgeschlagene Anmeldeversuche, bitte versuche es später "
"nochmal."
msgstr "Zu viele fehlgeschlagene Anmeldeversuche."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
@ -284,15 +280,16 @@ msgstr "Unbekanntes Projekt"
msgid "Password successfully reset."
msgstr "Passwort erfolgreich zurückgesetzt."
#, fuzzy
msgid "Project settings have been changed successfully."
msgstr ""
msgstr "Einstellungen wurden erfolgreich übernommen."
msgid "Unable to parse CSV"
msgstr ""
#, python-format
#, fuzzy, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "Fehlendes Attribut: %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
@ -311,7 +308,7 @@ msgid "Error deleting project"
msgstr "Fehler bei Projektlöschung"
msgid "Unable to logout"
msgstr ""
msgstr "Verlassen nicht möglich"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -320,12 +317,9 @@ msgstr "Du wurdest eingeladen, deine Ausgaben für %(project)s zu teilen"
msgid "Your invitations have been sent"
msgstr "Deine Einladungen wurden versendet"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
"Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails "
"auf. Bitte überprüfe die E-Mail Konfiguration des Servers oder "
"kontaktiere einen Administrator."
"Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails auf."
#, python-format
msgid "%(member)s has been added"
@ -371,7 +365,7 @@ msgstr "Die Ausgabe wurde bearbeitet"
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgstr "%(lang)s ist keine unterstützte Sprache"
msgid "Error deleting project history"
msgstr "Projekthistorie konnte nicht gelöscht werden"
@ -486,7 +480,7 @@ msgstr "Dies wird alle Ausgaben und Mitglieder dieses Projektes löschen!"
#, fuzzy
msgid "Import previously exported project"
msgstr "Zuvor exportierte JSON-Datei importieren"
msgstr "Zuvor exportierte Projekt-Datei importieren"
msgid "Choose file"
msgstr "Datei auswählen"
@ -497,8 +491,9 @@ msgstr "Ausgabe bearbeiten"
msgid "Add a bill"
msgstr "Ausgabe hinzufügen"
#, fuzzy
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgstr "einfache Operationen sind erlaubt, z.B. (18+36.2)/3"
msgid "Everyone"
msgstr "Jeder"
@ -590,12 +585,14 @@ msgstr "Rechnung %(name)s: %(owers_list_str)s zur Eigentümerliste hinzugefügt"
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
msgstr "Rechnung %(name)s: %(owers_list_str)s von der Eigentümerliste entfernt"
#, fuzzy
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
"Dieses Projekt hat den Projektverlauf deaktiviert. Neue Aktionen werden "
"unten nicht mehr angezeigt."
#, fuzzy
msgid "You can enable history on the settings page."
msgstr "IP-Adresserfassung kann in den Einstellungen aktiviert werden"
msgstr "Der Verlauf kann in den Einstellungen aktiviert werden."
msgid ""
"The table below reflects actions recorded prior to disabling project "
@ -816,7 +813,7 @@ msgstr "Dashboard"
#, python-format
msgid "Please retry after %(date)s."
msgstr ""
msgstr "Bitte nach %(date)s erneut versuchen."
msgid "Code"
msgstr "Code"
@ -827,7 +824,7 @@ msgstr "Handy-Applikation"
msgid "Documentation"
msgstr "Dokumentation"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Dashboard Administration"
msgid "Legal information"
@ -881,9 +878,8 @@ msgstr "Keine Ausgaben"
msgid "Nothing to list yet."
msgstr "Noch nichts aufzulisten."
#, fuzzy
msgid "Add your first bill"
msgstr "eine Ausgabe hinzufügen"
msgstr "Mach deine erste Rechnung"
#, fuzzy
msgid "Add the first participant"
@ -912,7 +908,7 @@ msgid "Invite people to join this project"
msgstr "Lade Leute ein, diesem Projekt beizutreten"
msgid "Share an invitation link"
msgstr ""
msgstr "Einladung verschicken"
msgid ""
"The easiest way to invite people is to give them the following invitation"
@ -920,26 +916,29 @@ msgid ""
" add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgstr ""
"Am einfachsten lädt man jemanden ein, indem man folgenden Einladungs-Link "
"teilt. <br /> Sie werden dann in der Lage sein dem Projekt beizutreten, zu "
"bearbeiten und Rechnungen hinzuzufügen, bearbeiten und löschen. Sie werden "
"allerdings keinen Zugriff auf wichtige Einstellungen bekommen, wie Privaten "
"Code zu ändern oder gar das ganze Projekt zu löschen."
msgid "Scan QR code"
msgstr ""
msgstr "QR-Code scannen"
msgid "Use a mobile device with a compatible app."
msgstr ""
msgstr "Benutzen sie ein Smartphone mit Kompatibler App."
msgid "Send via Emails"
msgstr "Per E-Mail versenden"
#, fuzzy
msgid ""
"Specify a list of email adresses (separated by comma) of people you want "
"to notify about the creation of this project. We will send them an email "
"with the invitation link."
msgstr ""
"Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du "
"über die\n"
"\t\t\tErstellung dieses Projekts informieren möchtest, und wir senden "
"ihnen eine E-Mail."
"Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du über "
"die Erstellung dieses Projekts informieren möchtest, und wir werden ihnen "
"eine E-Mail senden."
msgid "Share Identifier & code"
msgstr "Teile die ID & den Code"
@ -954,12 +953,11 @@ msgstr ""
msgid "Identifier:"
msgstr "ID:"
#, fuzzy
msgid "Private code:"
msgstr "Privater Code"
msgstr "Privater Code:"
msgid "the private code was defined when you created the project"
msgstr ""
msgstr "Der Private Code wurde beim erstellen des Projekts von Ihnen festgelegt"
msgid "Who pays?"
msgstr "Wer zahlt?"

View file

@ -811,7 +811,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
#, fuzzy

View file

@ -821,7 +821,7 @@ msgstr "Poŝaparata programo"
msgid "Documentation"
msgstr "Dokumentaro"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administra panelo"
#, fuzzy

View file

@ -818,7 +818,7 @@ msgstr "Aplicación móvil"
msgid "Documentation"
msgstr "Documentación"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panel de administración"
msgid "Legal information"

View file

@ -1,19 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2022-04-11 17:12+0000\n"
"Last-Translator: Santiago José Gutiérrez Llanos "
"<gutierrezapata17@gmail.com>\n"
"PO-Revision-Date: 2023-11-27 05:02+0000\n"
"Last-Translator: Wilfredo Gomez <thepageguy@mailfence.com>\n"
"Language-Team: Spanish (Latin America) <https://hosted.weblate.org/projects/"
"i-hate-money/i-hate-money/es_419/>\n"
"Language: es_419\n"
"Language-Team: Spanish (Latin America) "
"<https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es_419/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.2.1-rc\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -30,18 +29,17 @@ msgstr ""
msgid "Project name"
msgstr "Nombre del Proyecto"
#, fuzzy
msgid "Current private code"
msgstr "Nuevo código privado"
msgstr "código privado actual"
msgid "Enter existing private code to edit project"
msgstr ""
msgstr "Ingrese el código privado existente para editar el proyecto"
msgid "New private code"
msgstr "Nuevo código privado"
msgstr "Nuevo Código privado"
msgid "Enter a new code if you want to change it"
msgstr "Entra un nuevo código si tu quieres cambiarlo"
msgstr "Introduce un nuevo código si quieres cambiarlo"
msgid "Email"
msgstr "Correo Electrónico"
@ -50,31 +48,31 @@ msgid "Enable project history"
msgstr "Habilitar historial del proyecto"
msgid "Use IP tracking for project history"
msgstr "Registrar la IPs para el historial del proyecto"
msgstr "Utilice el seguimiento de IP para el historial del proyecto"
msgid "Default Currency"
msgstr "Moneda por defecto"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
"Establecer una moneda predeterminada permite la conversión de divisas "
"entre facturas"
"Establecer una moneda predeterminada permite la conversión de moneda entre "
"billetes"
msgid "Unknown error"
msgstr "Error desconocido"
msgid "Invalid private code."
msgstr "Código privado inválido."
msgstr "Código privado no válido."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
msgstr ""
"Este proyecto no se puede establecer en 'ninguna moneda' porque contiene "
"facturas en varias monedas."
"Este proyecto no se puede configurar como \"sin moneda\" porque contiene "
"billetes en varias monedas."
msgid "Compatible with Cospend"
msgstr ""
msgstr "Compatible con Cospend"
msgid "Project identifier"
msgstr "Identificador de proyecto"
@ -94,16 +92,16 @@ msgstr ""
"favor, elija un nuevo identificador"
msgid "Which is a real currency: Euro or Petro dollar?"
msgstr "¿Cuál es una moneda real: euro o petro dólar?"
msgstr "¿Cuál es la moneda real: el euro o el petrodólar?"
msgid "euro"
msgstr "Euro"
msgstr "euro"
msgid "Please, validate the captcha to proceed."
msgstr "Por favor, completa el captcha para seguir."
msgstr "Por favor, valide la captcha para proceder."
msgid "Enter private code to confirm deletion"
msgstr "Introduzca el código privado para confirmar la eliminación"
msgstr "Ingrese el código privado para confirmar la eliminación"
msgid "Get in"
msgstr "Entrar"
@ -124,7 +122,7 @@ msgid "Password"
msgstr "Contraseña"
msgid "Password confirmation"
msgstr "Confirmar contraseña"
msgstr "confirmación de contraseña"
msgid "Reset password"
msgstr "Restablecer contraseña"
@ -161,7 +159,7 @@ msgstr "Enviar y agregar uno nuevo"
#, python-format
msgid "Project default: %(currency)s"
msgstr "moneda predeterminada del projecto: %(currency)s"
msgstr "Projecto por defecto: %(currency)s"
msgid "Name"
msgstr "Nombre"
@ -196,15 +194,15 @@ msgstr "Cerrar sesión"
msgid "Please check the email configuration of the server."
msgstr ""
"Por favor verifique la configuración de correo electrónico del servidor."
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"Lo sentimos, hubo un error cuando intentamos enviarle correos de "
"invitación. Por favor, revise la configuración de correo en el servidor o"
" contactese con el administrador."
"Verifique la configuración de correo electrónico del servidor o comuníquese "
"con el administrador: %(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -223,53 +221,47 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}"
msgid "No Currency"
msgstr "no moneda"
msgstr "No Moneda"
#. Form error with only one error
msgid "{prefix}: {error}"
msgstr "{prefijo}: {error}"
msgstr "{prefix}: {error}"
#. Form error with a list of errors
msgid "{prefix}:<br />{errors}"
msgstr "{prefijo}:<br />{errores}"
msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts."
msgstr ""
"Demasiados intentos fallidos de inicio de sesión, vuelva a intentarlo más"
" tarde."
msgstr "Demasiados intentos fallidos de inicio de sesión."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
"Esta contraseña de administrador no es la correcta. Solo quedan %(num)d "
"intentos."
"intentos restantes."
msgid "Provided token is invalid"
msgstr "La muestra proporcionada no es válida"
msgstr "El token proporcionado no es válido"
msgid "This private code is not the right one"
msgstr "Este código privado no es el correcto"
msgid "A reminder email has just been sent to you"
msgstr "Acabamos de enviarte un email de recordatorio"
msgstr "Se le acaba de enviar un correo electrónico de recordatorio"
msgid ""
"We tried to send you an reminder email, but there was an error. You can "
"still use the project normally."
msgstr ""
"Te hemos intentado enviar un correo electrónico recordatorio pero ha "
"habido un error. Todavía puedes usar el proyecto habitualmente."
"Intentamos enviarte un correo electrónico de recordatorio, pero hubo un "
"error. Aún puedes usar el proyecto normalmente."
#, fuzzy
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions."
msgstr ""
"Lo sentimos, hubo un error al enviarle un correo electrónico con las "
"instrucciones de restablecimiento de contraseña. Compruebe la "
"configuración de correo electrónico del servidor o póngase en contacto "
"con el administrador."
"Lo sentimos, hubo un error al enviarle un correo electrónico con "
"instrucciones para restablecer la contraseña."
msgid "No token provided"
msgstr "No se proporciono ningún token"
@ -284,33 +276,33 @@ msgid "Password successfully reset."
msgstr "Contraseña restablecida con éxito."
msgid "Project settings have been changed successfully."
msgstr ""
msgstr "La configuración del proyecto se ha cambiado correctamente."
msgid "Unable to parse CSV"
msgstr ""
msgstr "No se puede analizar CSV"
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "Atributo faltante: %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
"currency"
msgstr ""
"No se pueden agregar facturas en varias monedas a un proyecto sin la "
"moneda predeterminada"
"No se pueden agregar billetes en varias monedas a un proyecto sin moneda "
"predeterminada"
msgid "Project successfully uploaded"
msgstr "El proyecto se subió exitosamente"
msgstr "Proyecto cargado exitosamente"
msgid "Project successfully deleted"
msgstr "Proyecto eliminado correctamente"
msgid "Error deleting project"
msgstr "Error al borrar poryecto"
msgstr "Error al borrar proyecto"
msgid "Unable to logout"
msgstr ""
msgstr "No se puede cerrar sesión"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -319,26 +311,24 @@ msgstr "Usted ha sido invitado a compartir sus gastos para %(project)s"
msgid "Your invitations have been sent"
msgstr "Sus invitaciones han sido enviadas"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
"Lo sentimos, hubo un error cuando intentamos enviarle correos de "
"invitación. Por favor, revise la configuración de correo en el servidor o"
" contactese con el administrador."
"Lo sentimos, hubo un error al intentar enviar los correos electrónicos de "
"invitación."
#, python-format
msgid "%(member)s has been added"
msgstr "Se añadieron %(member)s"
msgstr "%(member)s ha sido añadido"
msgid "Error activating participant"
msgstr "Error activando participante"
msgstr "Error al activar el participante"
#, python-format
msgid "%(name)s is part of this project again"
msgstr "%(name)s es parte de este nuevo proyecto"
msgstr "%(name)s es parte de este proyecto otra vez"
msgid "Error removing participant"
msgstr "Error eliminando participante"
msgstr "Error al eliminar el participante"
#, python-format
msgid ""
@ -360,7 +350,7 @@ msgid "The bill has been added"
msgstr "La factura ha sido agregada"
msgid "Error deleting bill"
msgstr "Error eliminando factura"
msgstr "Error al eliminar la factura"
msgid "The bill has been deleted"
msgstr "La factura ha sido eliminada"
@ -370,7 +360,7 @@ msgstr "La factura ha sido modificada"
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgstr "%(lang)s no es un idioma admitido"
msgid "Error deleting project history"
msgstr "Error al eliminar el historial del proyecto"
@ -450,9 +440,8 @@ msgstr "Conseguir en"
msgid "Edit project"
msgstr "Editar proyecto"
#, fuzzy
msgid "Import project"
msgstr "Editar proyecto"
msgstr "Importar proyecto"
msgid "Download project's data"
msgstr "Descargar datos del proyecto"
@ -481,14 +470,13 @@ msgid "Privacy Settings"
msgstr "Ajustes de privacidad"
msgid "Save changes"
msgstr ""
msgstr "Guardar cambios"
msgid "This will remove all bills and participants in this project!"
msgstr "Esto va a remover todas las facturas y participantes en este proyecto!"
#, fuzzy
msgid "Import previously exported project"
msgstr "Importar archivo JSON previamente exportado"
msgstr "Importar proyecto previamente exportado"
msgid "Choose file"
msgstr "Escoger un archivo"
@ -500,7 +488,7 @@ msgid "Add a bill"
msgstr "Agregar una factura"
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgstr "Se permiten operaciones simples, e.j. (18+36.2)/3"
msgid "Everyone"
msgstr "Todos"
@ -524,13 +512,13 @@ msgid "Download"
msgstr "Descargar"
msgid "Disabled Project History"
msgstr "Historial de proyecto activo"
msgstr "Historial de proyectos deshabilitado"
msgid "Disabled Project History & IP Address Recording"
msgstr "Historial de proyecto y registros de dirección IP inactivos"
msgid "Enabled Project History"
msgstr "Historial de proyecto activo"
msgstr "Historial de proyectos habilitado"
msgid "Disabled IP Address Recording"
msgstr "Registro de direcciones IP activo"
@ -592,19 +580,21 @@ msgstr "Factura %(name)s: removida %(owers_list_str)s de la lista de dueños"
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
"Este proyecto tiene el historial deshabilitado. Las nuevas acciones no "
"aparecerán a continuación."
#, fuzzy
msgid "You can enable history on the settings page."
msgstr "El registro de direcciones IP se puede activar en la página de ajustes"
msgstr "Puede habilitar el historial en la página de configuración."
msgid ""
"The table below reflects actions recorded prior to disabling project "
"history."
msgstr ""
"La siguiente tabla refleja las acciones registradas antes de deshabilitar el "
"historial del proyecto."
#, fuzzy
msgid "You can clear the project history to remove them."
msgstr "Es probable que alguien borrara el historial del proyecto."
msgstr "Puede borrar el historial del proyecto para eliminarlos."
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
@ -801,7 +791,7 @@ msgid "Settings"
msgstr "Configuración"
msgid "RSS Feed"
msgstr ""
msgstr "rss Feed"
msgid "Other projects :"
msgstr "Otros proyectos :"
@ -814,7 +804,7 @@ msgstr "Tablero"
#, python-format
msgid "Please retry after %(date)s."
msgstr ""
msgstr "Vuelva a intentarlo después de %(date)s."
msgid "Code"
msgstr "Código"
@ -825,7 +815,7 @@ msgstr "Aplicación móvil"
msgid "Documentation"
msgstr "Documentación"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panel de administración"
msgid "Legal information"
@ -879,13 +869,11 @@ msgstr "Sin facturas"
msgid "Nothing to list yet."
msgstr "Aún no hay nada que listar."
#, fuzzy
msgid "Add your first bill"
msgstr "agregar una factura"
msgstr "Añade tu primera factura"
#, fuzzy
msgid "Add the first participant"
msgstr "Editar este participante"
msgstr "Agregar el primer participante"
msgid "Password reminder"
msgstr "Recordar contraseña"
@ -910,7 +898,7 @@ msgid "Invite people to join this project"
msgstr "Invita a personas a unirse a este proyecto"
msgid "Share an invitation link"
msgstr ""
msgstr "Compartir un enlace de invitación"
msgid ""
"The easiest way to invite people is to give them the following invitation"
@ -918,26 +906,29 @@ msgid ""
" add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgstr ""
"La forma más sencilla de invitar personas es dándoles el siguiente enlace de "
"invitación.<br />Podrán acceder al proyecto, administrar participantes, "
"agregar/editar/eliminar facturas. Sin embargo, no tendrán acceso a "
"configuraciones importantes como cambiar el código privado o eliminar todo "
"el proyecto."
msgid "Scan QR code"
msgstr ""
msgstr "Escanear código QR"
msgid "Use a mobile device with a compatible app."
msgstr ""
msgstr "Utilice un dispositivo móvil con una aplicación compatible."
msgid "Send via Emails"
msgstr "Enviar por correo electrónico"
#, fuzzy
msgid ""
"Specify a list of email adresses (separated by comma) of people you want "
"to notify about the creation of this project. We will send them an email "
"with the invitation link."
msgstr ""
"Especifique una lista (separada por comas) de las direcciones de correo "
"electrónico a las que desea notificar acerca de la\n"
"creación de este proyecto de gestión presupuestaria y les enviaremos un "
"correo electrónico para usted."
"Especifique una lista de direcciones de correo electrónico (separadas por "
"comas) de las personas a las que desea notificar sobre la creación de este "
"proyecto. Les enviaremos un correo electrónico con el enlace de invitación."
msgid "Share Identifier & code"
msgstr "Compartir identificador y código"
@ -948,16 +939,20 @@ msgid ""
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
"Puede compartir el identificador del proyecto y el código privado por "
"cualquier medio de comunicación.<br />Cualquier persona con el código "
"privado tendrá acceso al proyecto completo, incluido el cambio de "
"configuraciones como el código privado o la dirección de correo electrónico "
"del proyecto, o incluso la eliminación completa. proyecto."
msgid "Identifier:"
msgstr "Identificador:"
#, fuzzy
msgid "Private code:"
msgstr "Código privado"
msgstr "Código privado:"
msgid "the private code was defined when you created the project"
msgstr ""
msgstr "el código privado se definió cuando creaste el proyecto"
msgid "Who pays?"
msgstr "¿Quién paga?"
@ -1170,4 +1165,3 @@ msgstr "Período"
#~ "Puedes compartir directamente el siguiente "
#~ "enlace a través de tu medio "
#~ "preferido"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-03-19 21:40+0000\n"
"Last-Translator: Sai Mohammad-Hossein Emami <emami@outlook.com>\n"
"PO-Revision-Date: 2024-05-23 03:01+0000\n"
"Last-Translator: Yamin Siahmargooei <yamin8000@yahoo.com>\n"
"Language-Team: Persian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/fa/>\n"
"Language: fa\n"
"Language-Team: Persian <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/fa/>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.6-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -125,16 +125,16 @@ msgid "Reset password"
msgstr "بازنشانی گذرواژه"
msgid "When?"
msgstr ""
msgstr "چه زمانی؟"
msgid "What?"
msgstr "چی؟"
msgid "Who paid?"
msgstr ""
msgstr "چه کسی پرداخت کرد؟"
msgid "How much?"
msgstr ""
msgstr "چقدر؟"
msgid "Currency"
msgstr "واحد پولی"
@ -180,14 +180,14 @@ msgid "People to notify"
msgstr "افرادی که براشون نوتیفیکیشن ارسال میشه"
msgid "Send the invitations"
msgstr ""
msgstr "ارسال دعوت نامه ها"
#, python-format
msgid "The email %(email)s is not valid"
msgstr "ایمیل %(email)s نامعتبره"
msgid "Logout"
msgstr ""
msgstr "خروج"
msgid "Please check the email configuration of the server."
msgstr ""
@ -782,7 +782,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -1092,4 +1092,3 @@ msgstr ""
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -824,7 +824,7 @@ msgstr "Application mobile"
msgid "Documentation"
msgstr "Documentation"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panneau d'administration"
msgid "Legal information"

View file

@ -788,7 +788,7 @@ msgstr "יישום לנייד"
msgid "Documentation"
msgstr "דוקומנטציה"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -829,7 +829,7 @@ msgstr "मोबाइल एप्लीकेशन"
msgid "Documentation"
msgstr "प्रलेखन"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "व्यवस्थापन डैशबोर्ड"
#, fuzzy

File diff suppressed because it is too large Load diff

View file

@ -812,7 +812,7 @@ msgstr "Aplikasi Gawai"
msgid "Documentation"
msgstr "Dokumentasi"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Dasbor Administrasi"
msgid "Legal information"

View file

@ -817,7 +817,7 @@ msgstr "Applicazione mobile"
msgid "Documentation"
msgstr "Documentazione"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Cruscotto Amministrazione"
msgid "Legal information"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2020-11-11 16:28+0000\n"
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n"
"PO-Revision-Date: 2024-06-24 15:09+0000\n"
"Last-Translator: Khang Tran <tranchikhang@outlook.com>\n"
"Language-Team: Japanese <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/ja/>\n"
"Language: ja\n"
"Language-Team: Japanese <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/ja/>\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.6-rc\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -27,19 +27,17 @@ msgstr "無効な入力です。数字と「+ - * / 」の演算子しか入力
msgid "Project name"
msgstr "プロジェクトの名前"
#, fuzzy
msgid "Current private code"
msgstr "暗証コード"
msgstr "現在の暗証コード"
msgid "Enter existing private code to edit project"
msgstr ""
msgstr "プロジェクトを編集するために、暗証コードを入力してください"
#, fuzzy
msgid "New private code"
msgstr "暗証コード"
msgstr "暗証コード"
msgid "Enter a new code if you want to change it"
msgstr ""
msgstr "変更するために、新しい暗証コードを入力してください"
msgid "Email"
msgstr "メールアドレス"
@ -54,15 +52,13 @@ msgid "Default Currency"
msgstr "初期設定にする通貨"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgstr "明細通貨変換のため、デフォルトの通貨を設定してください"
#, fuzzy
msgid "Unknown error"
msgstr "未知のプロジェクト"
msgstr "不明エラー"
#, fuzzy
msgid "Invalid private code."
msgstr "暗証コード"
msgstr "無効な暗証コード"
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
@ -179,7 +175,7 @@ msgid "This project already have this participant"
msgstr "プロジェクトはすでにこのメンバーを含めています"
msgid "People to notify"
msgstr ""
msgstr "通知したい人"
msgid "Send the invitations"
msgstr "招待状を送る"
@ -194,11 +190,12 @@ msgstr "ログアウト"
msgid "Please check the email configuration of the server."
msgstr ""
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr "申し訳ございませんが、招待メールを送ったとき、エラーが発生しました。メールアドレスを再度チェックするかまたは管理者に連絡ください。"
msgstr "申し訳ございませんが、エラーが発生しました。メールアドレスを再度チェックする"
"か、または管理者( %(admin_email)sに連絡ください"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -271,7 +268,7 @@ msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
msgstr "CSVを読み込むことができません"
#, python-format
msgid "Missing attribute: %(attribute)s"
@ -292,7 +289,7 @@ msgid "Error deleting project"
msgstr ""
msgid "Unable to logout"
msgstr ""
msgstr "ログアウトできません"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -353,9 +350,8 @@ msgstr ""
msgid "Error deleting project history"
msgstr "プロジェクトの歴史を有効にする"
#, fuzzy
msgid "Deleted project history."
msgstr "プロジェクトの歴史を有効にする"
msgstr "プロジェクトの歴史を削除しました。"
#, fuzzy
msgid "Error deleting recorded IP addresses"
@ -574,9 +570,8 @@ msgstr ""
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
#, fuzzy
msgid "You can enable history on the settings page."
msgstr "設定ページでIPアドレス記録を編集可能にすることができる"
msgstr "設定ページでIPアドレス記録を編集可能にすることができる"
msgid ""
"The table below reflects actions recorded prior to disabling project "
@ -622,13 +617,13 @@ msgstr "設定ページでIPアドレス記録を編集不可にすることが
msgid "From IP"
msgstr "IPから"
#, fuzzy, python-format
#, python-format
msgid "Project %(name)s added"
msgstr "プロジェクトの名前"
msgstr "プロジェクト%(name)sが作成されました"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s added"
msgstr "明細が追加されました"
msgstr "%(name)s明細が追加されました"
#, python-format
msgid "Participant %(name)s added"
@ -641,9 +636,9 @@ msgstr "プロジェクトの私用コードが変更された"
msgid "Project renamed to %(new_project_name)s"
msgstr "プロジェクト名は%(new_project_name)s"
#, fuzzy, python-format
#, python-format
msgid "Project contact email changed to %(new_email)s"
msgstr "プロジェクトの連絡メールが…に変更された"
msgstr "プロジェクトの連絡メールが%(new_email)sに変更されました"
msgid "Project settings modified"
msgstr "プロジェクトの設定が修正された"
@ -681,9 +676,9 @@ msgstr "日付"
msgid "Amount in %(currency)s"
msgstr "%(currency)sでの金額"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s modified"
msgstr "明細が変更されました"
msgstr "%(name)s明細が変更されました"
#, python-format
msgid "Participant %(name)s modified"
@ -697,17 +692,17 @@ msgstr "ユーザー%(name)sが既に取り除かれました"
msgid "Participant %(name)s removed"
msgstr "ユーザー%(name)sが既に取り除かれました"
#, fuzzy, python-format
#, python-format
msgid "Project %(name)s changed in an unknown way"
msgstr "未知の方法で変更された"
msgstr "プロジェクト%(name)sが不明な方法で変更されました"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s changed in an unknown way"
msgstr "未知の方法で変更された"
msgstr "明細%(name)sが不明な方法で変更されました"
#, fuzzy, python-format
#, python-format
msgid "Participant %(name)s changed in an unknown way"
msgstr "未知の方法で変更された"
msgstr "参加者%(name)sが不明な方法で変更されました"
msgid "Nothing to list"
msgstr "表示できるものがない"
@ -802,7 +797,7 @@ msgstr "携帯アプリ"
msgid "Documentation"
msgstr "書類"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "管理ダッシュボード"
#, fuzzy
@ -904,14 +899,12 @@ msgstr ""
msgid "Send via Emails"
msgstr "メールで送る"
#, fuzzy
msgid ""
"Specify a list of email adresses (separated by comma) of people you want "
"to notify about the creation of this project. We will send them an email "
"with the invitation link."
msgstr ""
"…を知らせたいメールアドレスのリストを特定する(カンマ区切り)\n"
"彼らにこの予算管理プロジェクトの作成をメールでお知らせします。"
msgstr "知らせたいメールアドレスのリスト(カンマ区切り)を指定してください。メールで"
"招待リンクを送信します。"
msgid "Share Identifier & code"
msgstr "名前とコードを共有する"
@ -926,9 +919,8 @@ msgstr ""
msgid "Identifier:"
msgstr "名前:"
#, fuzzy
msgid "Private code:"
msgstr "暗証コード"
msgstr "暗証コード"
msgid "the private code was defined when you created the project"
msgstr ""
@ -1109,4 +1101,3 @@ msgstr "期間"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "好きの手段で以下のリンクを直接に共有できる"

View file

@ -793,7 +793,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -793,7 +793,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"

View file

@ -855,7 +855,7 @@ msgstr "Mobilprogram"
msgid "Documentation"
msgstr "Dokumentasjon"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administrasjonsoversiktspanel"
#, fuzzy

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2021-02-17 02:50+0000\n"
"Last-Translator: Sander Kooijmans <weblate@gogognome.nl>\n"
"PO-Revision-Date: 2024-03-17 12:01+0000\n"
"Last-Translator: Xander Jennie <xanderjennie21@gmail.com>\n"
"Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/nl/>\n"
"Language: nl\n"
"Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/nl/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.5-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -31,19 +31,17 @@ msgstr ""
msgid "Project name"
msgstr "Projectnaam"
#, fuzzy
msgid "Current private code"
msgstr "Privécode"
msgstr "Huidige Privécode"
msgid "Enter existing private code to edit project"
msgstr ""
msgstr "Voer het huidige privécode in om dit project te bewerken"
#, fuzzy
msgid "New private code"
msgstr "Privécode"
msgstr "Nieuwe privécode"
msgid "Enter a new code if you want to change it"
msgstr ""
msgstr "Voer een nieuwe code in als u deze wilt wijzigen"
msgid "Email"
msgstr "E-mailadres"
@ -59,22 +57,24 @@ msgstr "Standaard munteenheid"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
"Door een standaardvaluta in te stellen, is valutaconversie tussen facturen "
"mogelijk"
#, fuzzy
msgid "Unknown error"
msgstr "Onbekend project"
msgstr "Onbekende fout"
#, fuzzy
msgid "Invalid private code."
msgstr "Privécode"
msgstr "Ongeldige privécode."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
msgstr ""
"Dit project kan niet op 'geen valuta' worden gezet, omdat het facturen in "
"meerdere valuta's bevat."
msgid "Compatible with Cospend"
msgstr ""
msgstr "Compatibel met mede besteden"
msgid "Project identifier"
msgstr "Project-id"
@ -92,17 +92,16 @@ msgid ""
msgstr "Er is al een project genaamd (\"%(project)s\"). Kies een andere naam"
msgid "Which is a real currency: Euro or Petro dollar?"
msgstr ""
msgstr "Wat is een echte valuta: de euro of de petro-dollar?"
#, fuzzy
msgid "euro"
msgstr "Periode"
msgstr "euro"
msgid "Please, validate the captcha to proceed."
msgstr ""
msgstr "Valideer de captcha om door te gaan."
msgid "Enter private code to confirm deletion"
msgstr ""
msgstr "Voer de privécode in om de verwijder opdracht te bevestigen"
msgid "Get in"
msgstr "Inloggen"
@ -126,7 +125,7 @@ msgid "Password confirmation"
msgstr "Wachtwoord bevestigen"
msgid "Reset password"
msgstr "Wachtwoord herstellen"
msgstr "Wachtwoord opnieuw instellen"
msgid "When?"
msgstr "Wanneer?"
@ -174,16 +173,14 @@ msgstr "Gewicht"
msgid "Add"
msgstr "Toevoegen"
#, fuzzy
msgid "The participant name is invalid"
msgstr "De gebruiker '%(name)s' is verwijderd"
msgstr "De naam van de deelnemer is ongeldig"
#, fuzzy
msgid "This project already have this participant"
msgstr "Deze deelnemer is al lid van het project"
msgid "People to notify"
msgstr ""
msgstr "Mensen om op de hoogte te stellen"
msgid "Send the invitations"
msgstr "Uitnodigingen versturen"
@ -196,54 +193,52 @@ msgid "Logout"
msgstr "Uitloggen"
msgid "Please check the email configuration of the server."
msgstr ""
msgstr "Controleer de e-mailconfiguratie van de server."
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"Sorry, er is iets fout gegaan bij het verzenden van de uitnodigingsmails."
"Controleer de e-mailinstellingen van de server of neem contact op met de "
" beheerder."
"beheerder: %(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
msgstr ""
msgstr "{dual_object_0} en {dual_object_1}"
#. Last two items of a list with more than 3 items
msgid "{previous_object}, and {end_object}"
msgstr ""
msgstr "{previous_object} en{end_object}"
#. Two items in a middle of a list with more than 5 objects
msgid "{previous_object}, {next_object}"
msgstr ""
msgstr "{previous_object}, {next_object}"
#. First two items of a list with more than 3 items
msgid "{start_object}, {next_object}"
msgstr ""
msgstr "{start_object}, {next_object}"
msgid "No Currency"
msgstr "Geen munteenheid"
msgstr "Geen valuta"
#. Form error with only one error
msgid "{prefix}: {error}"
msgstr ""
msgstr "{prefix}: {error}"
#. Form error with a list of errors
msgid "{prefix}:<br />{errors}"
msgstr ""
msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts."
msgstr "Te vaak onjuist ingelogd. Probeer het later opnieuw."
msgstr "Te veel mislukte inlogpogingen."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "Het admin-wachtwoord is onjuist. Je kunt het nog %(num)d keer proberen."
msgid "Provided token is invalid"
msgstr ""
msgstr "Het opgegeven token is ongeldig"
msgid "This private code is not the right one"
msgstr "Deze privécode is onjuist"
@ -258,14 +253,12 @@ msgstr ""
"We hebben geprobeerd een herinneringsmail te versturen, maar er is iets "
"fout gegaan. Je kunt het project nog steeds normaal gebruiken."
#, fuzzy
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions."
msgstr ""
"Sorry, er is iets fout gegaan bij het verzenden van een e-mail met "
"instructies om je wachtwoord te herstellen. Controleer de "
"e-mailinstellingen van de server of neem contact op met de beheerder."
"instructies om je wachtwoord te herstellen."
msgid "No token provided"
msgstr "Geen toegangssleutel opgegeven"
@ -283,11 +276,11 @@ msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
msgstr "Kan CSV niet parseren"
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "Ontbrekende attribute: %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
@ -304,7 +297,7 @@ msgid "Error deleting project"
msgstr ""
msgid "Unable to logout"
msgstr ""
msgstr "Kan niet uitloggen"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -354,10 +347,10 @@ msgid "The bill has been added"
msgstr "De rekening is toegevoegd"
msgid "Error deleting bill"
msgstr ""
msgstr "Fout bij verwijderen factuur"
msgid "The bill has been deleted"
msgstr "De rekening is verwijderd"
msgstr "De factuur is verwijderd"
msgid "The bill has been modified"
msgstr "De rekening is aangepast"
@ -393,9 +386,8 @@ msgstr "Terug naar de lijst"
msgid "Administration tasks are currently disabled."
msgstr "Beheerderstaken zijn momenteel uitgeschakeld."
#, fuzzy
msgid "Authentication"
msgstr "Documentatie"
msgstr "Authenticatie"
msgid "The project you are trying to access do not exist, do you want to"
msgstr "Het project dat je probeert te benaderen bestaat niet. Wil je"
@ -412,18 +404,17 @@ msgstr "Nieuw project aanmaken"
msgid "Project"
msgstr "Project"
#, fuzzy
msgid "Number of participants"
msgstr "deelnemers toevoegen"
msgstr "Aantal deelnemers"
msgid "Number of bills"
msgstr "Aantal rekeningen"
msgstr "Aantal facturen"
msgid "Newest bill"
msgstr "Nieuwste rekening"
msgstr "Nieuwste factuur"
msgid "Oldest bill"
msgstr "Oudste rekening"
msgstr "Oudste factuur"
msgid "Actions"
msgstr "Acties"
@ -431,9 +422,8 @@ msgstr "Acties"
msgid "edit"
msgstr "bewerken"
#, fuzzy
msgid "Delete project"
msgstr "Project aanpassen"
msgstr "Project verwijderen"
msgid "show"
msgstr "tonen"
@ -441,9 +431,8 @@ msgstr "tonen"
msgid "The Dashboard is currently deactivated."
msgstr "De overzichtspagina is momenteel uitgeschakeld."
#, fuzzy
msgid "Download Mobile Application"
msgstr "Mobiele app"
msgstr "Mobiele applicatie downloaden"
#, fuzzy
msgid "Get it on"
@ -452,9 +441,8 @@ msgstr "Inloggen"
msgid "Edit project"
msgstr "Project aanpassen"
#, fuzzy
msgid "Import project"
msgstr "Project aanpassen"
msgstr "Project importeren"
msgid "Download project's data"
msgstr "Projectgegevens downloaden"
@ -826,7 +814,7 @@ msgstr "Mobiele app"
msgid "Documentation"
msgstr "Documentatie"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administratie-overzicht"
#, fuzzy
@ -1163,4 +1151,3 @@ msgstr "Periode"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Je kunt de volgende link direct delen"

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