Compare commits

..

156 commits

Author SHA1 Message Date
Zhongqi Ma
6ddf0958e1
Merge 43260d0dc4 into 05742347c3 2025-01-05 16:05:40 +01:00
43260d0dc4
Fix the tests for user reactivation 2025-01-05 16:05:33 +01:00
Baptiste Jonglez
0d7308fccb
Remove unused test 2025-01-05 15:51:50 +01:00
Zhongqi Ma
dff1956c14
Update budget_test.py
Added 2 tests checking for validate_name() in MemberForm()
2025-01-05 15:51:48 +01:00
Zhongqi Ma
cedb2934b8
Updated checks for validate_name() in MemberForm()
The database allows users to deactivate an account with a non-zero value, and create a new user with the same name, reactivating the previous user will allow two users of the same name. This change assures that new user names can not be the same as deactivated users with associated bills (Users that are not deleted from deactivation).
2025-01-05 15:50:43 +01:00
singeltary
05742347c3
UI/navigation changes to in "Edit this participant" (#1323)
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.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
* Update forms.html

Added navigation element in 'Edit Participants' window

* Update forms.html

Updated button on form for editing participants--"Save" for edits instead of "Add."
2025-01-05 15:49:20 +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
aab32a2c3b
Swapping Out 'Fred' for 'Jeanne' in Docs/API – Personal Peacekeeping 😊 (#1260) 2023-11-13 00:47:24 +01:00
e507b44ee9 Actually update to flask and werkzeug 2.3
The update was previously made by @azmeuk but
lost in a bad merge (see #1244 for details).
2023-11-11 12:22:30 +01:00
7c7efcc24c Document repository rules (closes #1245) 2023-11-11 11:15:14 +01:00
Éloi Rivard
ee7289f5d2
chore: update to flask 2.3 (#1244)
Co-authored-by: Alexis Métaireau <alexis@notmyidea.org>
2023-11-06 17:53:58 +01:00
Éloi Rivard
c9a709953f
chore: support for wtforms 3.1 (#1248) 2023-10-18 23:47:21 +02:00
Éloi Rivard
eb7338c76c
chore: migrate from setup.cfg to pyproject.toml (#1243) 2023-10-08 01:56:25 +02:00
Baptiste Jonglez
e360ee7dfb Back to development: 6.1.2 2023-10-04 00:17:08 +02:00
Baptiste Jonglez
c74935c03c Preparing release 6.1.1 2023-10-04 00:16:07 +02:00
Baptiste Jonglez
38f3dd0d56 Prepare next release 2023-10-04 00:14:53 +02:00
zorun
1a2fa0476b
Currency hotfixes (#1240)
* hotfix: hardcode list of currencies to workaround failing API calls

See https://github.com/spiral-project/ihatemoney/issues/1232 for a discussion on currencies

* Temporarily disable some currency operations to prevent crashes

Here is what is disabled:

- setting or changing the default currency on an existing project

- adding or editing a bill with a currency that differs from the default
  currency of the project

---------

Co-authored-by: Baptiste Jonglez <git@bitsofnetworks.org>
2023-10-04 00:05:10 +02:00
Jojo144
c5c8dba631
Default owers when adding a bill (#1222)
* Remember last owers for next new bill

* Add a test for last_selected_payed_for
2023-10-03 20:20:58 +02:00
Jojo144
b92a36c049 Add backward compatibility for last_selected_payer 2023-10-02 23:50:25 +02:00
Jojo144
d4b178ed66 Add a test for last_selected_payer_per_project 2023-10-02 23:50:25 +02:00
Jojo144
3bcc9afb50 [Fix] Remember the last selected payer for each project (and not only for one) 2023-10-02 23:50:25 +02:00
Baptiste Jonglez
cda60d1626 Update changelog 2023-10-02 23:49:01 +02:00
Baptiste Jonglez
6741d1056f Revert "Limit version of flask-wtf to avoid an upstream bug"
This reverts commit d393c39d42.
2023-10-02 23:46:33 +02:00
Jojo144
ff43a83262 [History] Add details of bills in history 2023-10-01 23:57:47 +02:00
Jojo144
7412d67ed6 [History] Truncate external links in history when they are too long 2023-10-01 23:57:47 +02:00
Jojo144
c0f49cb124 [History] Small code reorganization 2023-10-01 23:57:47 +02:00
Baptiste Jonglez
382780b05c Update changelog 2023-10-01 23:56:11 +02:00
Baptiste Jonglez
d393c39d42 Limit version of flask-wtf to avoid an upstream bug 2023-10-01 23:55:53 +02:00
Éloi Rivard
21408f8bc9 tests: migrate to pytest
- replace setUp/tearDown with pytest fixtures
- rename test classes to use the pytest convention
- use pytest assertions

Co-authored-by: Glandos <bugs-github@antipoul.fr>
2023-10-01 23:33:36 +02:00
Weblate (bot)
2ce76158d2
Translations update from Hosted Weblate (#1230)
* 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

* 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

---------

Co-authored-by: Kamborio <Kamborio15@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
2023-09-26 15:47:33 +02:00
Mihail Iosilevitch
3681981a5c Translated using Weblate (Russian)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Mihail Iosilevitch <yosik@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/ru/
Translation: I Hate Money/I Hate Money
2023-09-17 19:38:34 +02:00
xXx
438263826b Translated using Weblate (Russian)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: xXx <xxx_xxx_xxxxxxxxx@mail.ru>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/ru/
Translation: I Hate Money/I Hate Money
2023-09-17 19:38:34 +02:00
Baptiste
f70ad5367b Translated using Weblate (Kannada)
Currently translated at 31.8% (88 of 276 strings)

Co-authored-by: Baptiste <weblate@bitsofnetworks.org>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/kn/
Translation: I Hate Money/I Hate Money
2023-09-17 19:38:34 +02:00
Chandrashekar MC
a642a17e40 Translated using Weblate (Kannada)
Currently translated at 31.8% (88 of 276 strings)

Co-authored-by: Chandrashekar MC <mc.chandrashekar@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/kn/
Translation: I Hate Money/I Hate Money
2023-09-17 19:38:34 +02:00
Kristoffer Grundström
a27f678428 Translated using Weblate (Swedish)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Kristoffer Grundström <swedishsailfishosuser@tutanota.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/sv/
Translation: I Hate Money/I Hate Money
2023-09-17 19:38:34 +02:00
Eryk Michalak
3464c2cf33 Translated using Weblate (Polish)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/pl/
Translation: I Hate Money/I Hate Money
2023-09-03 12:24:23 +02:00
Jojo144
fcdc057dba Simplifies adding a bill with keyboard only 2023-09-03 12:14:08 +02:00
Weblate (bot)
04c375e2e7
Translated using Weblate (Polish) (#1218)
Currently translated at 95.2% (263 of 276 strings)


Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/pl/
Translation: I Hate Money/I Hate Money

Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
2023-08-24 17:58:13 +02:00
Weblate (bot)
a169b89563
Translated using Weblate (German) (#1217)
Currently translated at 89.4% (247 of 276 strings)


Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/de/
Translation: I Hate Money/I Hate Money

Co-authored-by: Ettore Atalan <atalanttore@googlemail.com>
2023-08-22 01:15:00 +02:00
Éloi Rivard
15c43f496e
chore: remove travis configuration (#1216) 2023-08-13 02:17:09 +02:00
Éloi Rivard
857ca2d5b0
tests: speed up unit tests (#1215)
Adds two configuration parameters that are passed to
generate_password_hash:

- PASSWORD_HASH_METHOD
- PASSWORD_HASH_SALT_LENGTH

The unit tests use high-speed low-security values and
gain 50% speed.
2023-08-13 00:04:06 +02:00
Alessandro Andro
0187932365 Translated using Weblate (Italian)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Alessandro Andro <pasandro.pasandro@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/it/
Translation: I Hate Money/I Hate Money
2023-08-06 11:14:19 +02:00
a-g-rao
11ec51d36b Translated using Weblate (Kannada)
Currently translated at 30.4% (84 of 276 strings)

Co-authored-by: a-g-rao <athrigrao@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/kn/
Translation: I Hate Money/I Hate Money
2023-08-06 11:14:19 +02:00
noelopdur
d886218457 Translated using Weblate (Spanish)
Currently translated at 47.1% (130 of 276 strings)

Co-authored-by: noelopdur <noelialopezduran@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es/
Translation: I Hate Money/I Hate Money
2023-08-06 11:14:19 +02:00
Puyma
35f434b031 Translated using Weblate (Spanish)
Currently translated at 47.1% (130 of 276 strings)

Co-authored-by: Puyma <puyma@amyup.xyz>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es/
Translation: I Hate Money/I Hate Money
2023-08-06 11:14:19 +02:00
Éloi Rivard
2a0ec05648 docs: dynamic year 2023-07-30 14:36:55 +02:00
Baptiste Jonglez
e54302040b Fix changelog syntax 2023-07-29 15:10:23 +02:00
Baptiste Jonglez
33bb91a016 Back to development: 6.1.1 2023-07-29 15:09:44 +02:00
Baptiste Jonglez
154365456e Preparing release 6.1.0 2023-07-29 15:08:55 +02:00
Baptiste
30ead1de0e Translated using Weblate (French)
Currently translated at 100.0% (276 of 276 strings)

Co-authored-by: Baptiste <weblate@bitsofnetworks.org>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/fr/
Translation: I Hate Money/I Hate Money
2023-07-29 15:07:58 +02:00
Baptiste Jonglez
376b646dd5 Update translation catalog for new strings 2023-07-29 14:28:20 +02:00
Baptiste Jonglez
4ebe471131 Fix indentation of translation string 2023-07-29 14:28:20 +02:00
Baptiste
266fc42744 Translated using Weblate (French)
Currently translated at 99.6% (275 of 276 strings)

Co-authored-by: Baptiste <weblate@bitsofnetworks.org>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/fr/
Translation: I Hate Money/I Hate Money
2023-07-29 14:17:46 +02:00
Baptiste Jonglez
32a79b17c5 Update translation catalog for new strings 2023-07-29 14:06:38 +02:00
Baptiste Jonglez
7a8fa22a0c Update changelog 2023-07-29 14:06:00 +02:00
Baptiste Jonglez
7d30794420 security docs: Clarify what is possible with a token 2023-07-29 14:02:49 +02:00
Baptiste Jonglez
3e5cd9e04e API docs: new current_password field 2023-07-29 14:02:49 +02:00
Baptiste Jonglez
73c8a31dd2 Invite page: document the security implication of all options
Also move the "invitation link" option first, because it's the preferred
way to give access to people that only need to handle participants and
bills.

Sharing the identifier and private becomes the last option, because it
gives full access to changing settings.
2023-07-29 14:02:49 +02:00
Baptiste Jonglez
5dc9984577 Better feedback when changing project settings
We now display a success flash message, and we stay on the same page
(instead of redirecting to the list of bills, which makes little sense)
2023-07-29 14:02:49 +02:00
Baptiste Jonglez
68e1dac75c Require private code to edit a project settings
This is something we had documented in our security documentation [1], but
we didn't actually do it...

As mentioned in [1], this has good security properties: you can invite
somebody with an invitation link, and they will be able to access the
project but not change the private code (because they don't know the
current private code).

This new check also applies to all other settings (email address, history
settings, currency), which is desirable.  Only somebody with knowledge of
the private code can now change these settings.

[1] https://ihatemoney.readthedocs.io/en/latest/security.html#giving-access-to-a-project
2023-07-29 14:02:49 +02:00
Éloi Rivard
b1d4f34193
tests: unit test assertion fixes (#1203)
`self.assertTrue(200, resp.status_code)` style are always True
and thus are useless. It looks like the original author wanted
`self.assertEqual` there instead.
2023-07-28 17:44:43 +02:00
Baptiste Jonglez
4d3bcf69d3 Update security docs for the new feed token 2023-07-28 17:34:34 +02:00
Baptiste Jonglez
ad5b108ec0 Fix 404 page crash
The 404 page crashes when the user is logged in:

      File "/home/zorun/code/ihatemoney/ihatemoney/templates/404.html", line 1, in top-level template code
        {% extends "layout.html" %}
      File "/home/zorun/code/ihatemoney/ihatemoney/templates/layout.html", line 124, in top-level template code
        {{ g.logout_form.hidden_tag() }}
      File "/home/zorun/venv/py3-ihatemoney/lib/python3.9/site-packages/jinja2/environment.py", line 474, in getattr
        return getattr(obj, attribute)
    jinja2.exceptions.UndefinedError: 'flask.ctx._AppCtxGlobals object' has no attribute 'logout_form'

This is because the logout form is defined by a URL processor, and this
does not seem to apply for all pages.

To solve this, simply skip the logout form if it's not defined.
2023-07-28 17:20:32 +02:00
Baptiste Jonglez
be961e987d Update translation catalog for new strings 2023-07-28 17:20:11 +02:00
Baptiste Jonglez
db04f68652 translations: Avoid splitting strings to make translator's life easier 2023-07-28 17:20:11 +02:00
Nati Lintzer
c9c6795b21 Translated using Weblate (Hebrew)
Currently translated at 62.7% (170 of 271 strings)

Co-authored-by: Nati Lintzer <nlintzer@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/he/
Translation: I Hate Money/I Hate Money
2023-07-28 15:23:45 +02:00
Éloi Rivard
8d4584d660 feat: project RSS feed. 2023-07-28 15:22:55 +02:00
Baptiste Jonglez
7c782443d3 Back to development: 6.0.2 2023-07-22 20:02:51 +02:00
Baptiste Jonglez
284fb011f0 Preparing release 6.0.1 2023-07-22 20:02:10 +02:00
Baptiste Jonglez
f6ae1cbf59 Update changelog for next release 2023-07-22 20:01:45 +02:00
Glandos
23d912b703
Migrate existing sessions after conversion to dict (#1194)
Migrate existing session after #1082

fix #1188
2023-07-22 19:55:45 +02:00
Nati Lintzer
b150c7adc5 Translated using Weblate (Hebrew)
Currently translated at 59.0% (160 of 271 strings)

Co-authored-by: Nati Lintzer <nlintzer@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/he/
Translation: I Hate Money/I Hate Money
2023-07-22 19:49:43 +02:00
Luke
4919266072 Translated using Weblate (German)
Currently translated at 92.9% (252 of 271 strings)

Co-authored-by: Luke <luke@luporr.de>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/de/
Translation: I Hate Money/I Hate Money
2023-07-22 19:49:43 +02:00
Baptiste
17a2e14c0e Translated using Weblate (French)
Currently translated at 100.0% (271 of 271 strings)

Co-authored-by: Baptiste <weblate@bitsofnetworks.org>
Translate-URL: https://hosted.weblate.org/projects/i-hate-money/i-hate-money/fr/
Translation: I Hate Money/I Hate Money
2023-07-22 19:49:43 +02:00
Glandos
59f3e9bac1 fix #1192 2023-07-15 15:27:34 +02:00
Glandos
d09d19af45 add test to make it fail 2023-07-15 15:27:34 +02:00
Baptiste Jonglez
e61540dbbe Update readthedocs to python 3.11 (should fix #1185) 2023-07-14 16:00:33 +02:00
Baptiste Jonglez
3788b6f5e7 Add support for APPLICATION_ROOT in Docker container 2023-07-14 15:53:43 +02:00
132 changed files with 11265 additions and 4053 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: '**/setup.cfg'
- name: Change dependencies to minimal supported versions
run: sed -i -e 's/>=/==/g; s/~=.*==\(.*\)/==\1/g; s/~=/==/g;' setup.cfg
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'

4
.gitignore vendored
View file

@ -1,5 +1,6 @@
*.pyc
*.egg-info
*.mo
dist
.venv
docs/_build/
@ -16,4 +17,5 @@ ihatemoney/budget.db
.DS_Store
.idea
.python-version
.coverage*
prof

View file

@ -1,7 +1,11 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
python:
version: "3.7"
install:
- method: pip
path: .

View file

@ -1,11 +0,0 @@
sudo: false
language: python
python:
- "3.7"
- "3.8"
- "3.9"
script: tox
before_install:
- python -m pip install --upgrade pip virtualenv
install:
- pip install tox-travis

View file

@ -2,10 +2,93 @@
This document describes changes between each past release.
## 6.0.1 (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
## 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)
### Currency conversion API workarounds
We are using an external API for currency conversion. This API recently
started requiring an API key, and this broke I Hate Money in many ways.
This release adds a set of workarounds for this issue. This should restore
basic functionality such as adding bills. However, we had to disable
some operations to prevent crashing:
- Setting or changing the default currency on an existing project is no longer possible.
However, setting a project to "No currency" is still possible.
- Adding or editing a bill with a currency that differs from the default currency
of the project is no longer possible
[Longer-term solutions are being discussed](https://github.com/spiral-project/ihatemoney/issues/1232).
If you are using currencies in your projects, your input is welcome.
### Added
- Simplifies adding a bill with keyboard only (#1221)
- Add details of bills in history (#1223)
- Remember last "For whom?" field when adding a new bill (#1222)
- Speed up unit tests (#1214)
- Update translations for Spanish, Russian, Kannada, Swedish, Polish, German, and Italian
### Fixed
- Fix docker-compose example (#1164)
- Fix remembering the last selected payer when switching project (#1224)
## 6.1.0 (2023-07-29)
### Added
- Add RSS feed for each project (#1158)
- Security: require private code to edit a project settings (#1204)
### Fixed
- Fix 404 page crash (#1201)
## 6.0.1 (2023-07-22)
### Added
- Add support for `APPLICATION_ROOT` in Docker container (#1189)
- Improve docker-compose example: admin password and volume for database (#1169)
### Fixed
- Fix docker-compose example quoting (#1164)
- Fix crash when using existing sessions (migrate them to dict) (#1194)
- Add newly created projects to the list of projects (#1193)
## 6.0.0 (2023-07-13)

View file

@ -23,6 +23,7 @@ DavidRThrashJr
donkers
Edwin Smulders
Elizabeth Sherrock
Éloi Rivard
eMerzh
Erwan Lacoudre
Feth AREZKI
@ -32,6 +33,7 @@ Glandos
Heimen Stoffels
James Leong
Jocelyn Delalande
Jojo144
Lod
Luc Didry
Lucas Verney

View file

@ -26,6 +26,7 @@ ENV DEBUG="False" \
SHOW_ADMIN_EMAIL="True" \
SQLALCHEMY_DATABASE_URI="sqlite:////database/ihatemoney.db" \
SQLALCHEMY_TRACK_MODIFICATIONS="False" \
APPLICATION_ROOT="/" \
ENABLE_CAPTCHA="False" \
LEGAL_LINK=""

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
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 setup.cfg $(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 setup.cfg $(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

@ -23,6 +23,7 @@ SHOW_ADMIN_EMAIL = $SHOW_ADMIN_EMAIL
SQLACHEMY_DEBUG = DEBUG
SQLALCHEMY_DATABASE_URI = "$SQLALCHEMY_DATABASE_URI"
SQLALCHEMY_TRACK_MODIFICATIONS = $SQLALCHEMY_TRACK_MODIFICATIONS
APPLICATION_ROOT = "$APPLICATION_ROOT"
ENABLE_CAPTCHA = $ENABLE_CAPTCHA
LEGAL_LINK = "$LEGAL_LINK"
EOF

View file

@ -35,6 +35,7 @@ services:
- SHOW_ADMIN_EMAIL=True
- SQLALCHEMY_DATABASE_URI=sqlite:////database/ihatemoney.db
- SQLALCHEMY_TRACK_MODIFICATIONS=False
- APPLICATION_ROOT=/
- ENABLE_CAPTCHA=False
- LEGAL_LINK=
- PORT=8000

View file

@ -34,9 +34,9 @@ the token (of course, you need to authenticate):
$ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/token
{"token": "WyJ0ZXN0Il0.Rt04fNMmxp9YslCRq8hB6jE9s1Q"}
Make sure to store this token securely: it allows full access to the
Make sure to store this token securely: it allows almost full access to the
project. For instance, use it to obtain information about the project
(replace PROJECT_TOKEN with the actual token):
(replace `PROJECT_TOKEN` with the actual token):
$ curl --oauth2-bearer "PROJECT_TOKEN" https://ihatemoney.org/api/projects/demo
@ -51,7 +51,8 @@ simply create an URL of the form:
https://ihatemoney.org/demo/join/PROJECT_TOKEN
Such a link grants full access to the project associated with the token.
Such a link grants read-write access to the project associated with the token,
but it does not allow to change project settings.
### Projects
@ -67,8 +68,8 @@ A project needs the following arguments:
- `name`: the project name (string)
- `id`: the project identifier (string without special chars or
spaces)
- `password`: the project password / secret code (string)
- `contact_email`: the contact email (string)
- `password`: the project password / private code (string)
- `contact_email`: the contact email, used to recover the private code (string)
Optional arguments:
@ -83,7 +84,9 @@ Here is the command:
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
"yay"
As you can see, the API returns the identifier of the project.
As you can see, the API returns the identifier of the project. It might be different
from what you requested, because the ID is normalized (remove special characters,
change to lowercase, etc).
#### Getting information about the project
@ -108,7 +111,12 @@ Updating a project is done with the `PUT` verb:
$ curl --basic -u yay:yay -X PUT\
https://ihatemoney.org/api/projects/yay -d\
'name=yay&id=yay&password=yay&contact_email=youpi@notmyidea.org'
'name=yay&id=yay&current_password=yay&password=newyay&contact_email=youpi@notmyidea.org'
You need to give the current private code as the `current_password` field. This is a security
measure to ensure that knowledge of an auth token is not enough to update settings.
Note that in any case you can never change the ID of a project.
#### Deleting a project
@ -125,7 +133,7 @@ You can get all the members with a `GET` on
[{"weight": 1, "activated": true, "id": 31, "name": "Arnaud"},
{"weight": 1, "activated": true, "id": 32, "name": "Alexis"},
{"weight": 1, "activated": true, "id": 33, "name": "Olivier"},
{"weight": 1, "activated": true, "id": 34, "name": "Fred"}]
{"weight": 1, "activated": true, "id": 34, "name": "Jeanne"}]
Add a member with a `POST` request on `/api/projects/<id>/members`:
@ -236,7 +244,7 @@ You can get some project stats with a `GET` on
"balance": 10.5
},
{
"member": {"activated": true, "id": 2, "name": "fred", "weight": 1.0},
"member": {"activated": true, "id": 2, "name": "jeanne", "weight": 1.0},
"paid": 5,
"spent": 15.5,
"balance": -10.5

View file

@ -1,9 +1,12 @@
import datetime
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"
project = "I hate money"
copyright = "2011-2021, The 'I hate money' team"
year = datetime.datetime.now().strftime("%Y")
copyright = f"2011-{year}, The 'I hate money' team"
version = "5.0"
release = "5.0"

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
@ -188,3 +196,12 @@ project](https://pythonhosted.org/flask-mail/#configuring-flask-mail)
- **MAIL_USERNAME** : default **None**
- **MAIL_PASSWORD** : default **None**
- **DEFAULT_MAIL_SENDER** : default **None**
## Configuring password hashes
The werkzeug [generate_password_hash](https://werkzeug.palletsprojects.com/utils/#werkzeug.security.generate_password_hash)
is used to create password hashes. By default the default werkzeug values
are used, however you can customize those values with:
- **PASSWORD_HASH_METHOD** : default **None**
- **PASSWORD_HASH_SALT_LENGTH** : default **None**

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).
@ -190,6 +231,19 @@ revision file which can be created with the following command:
You then need to write the migration steps yourself.
## Repository rules
- Please, try to keep it to **one pull request per feature:** if you want to do more
than one thing, send multiple pull requests. It will be easier for us to review and
merge.
- **Document your code:** if you add a new feature, please document it in the
- All the people working on this project do it on their spare time. So please, be
patient if we don't answer right away.
- We try to have two maintainers review the pull requests before merging it. So please,
be patient if we don't merge it right away. After one week, only one maintainer approval
is required.
## How to build the documentation ?
The documentation is using
@ -261,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
@ -281,7 +334,7 @@ Index](https://pypi.org) (PyPI) and publish a tag in the git repository.
::: {note}
The above command will prompt for version number, handle
`CHANGELOG.md` and `setup.cfg` updates, package creation,
`CHANGELOG.md` and `pyproject.toml` updates, package creation,
pypi upload. It will prompt you before each step to get your consent.
:::

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}

View file

@ -11,39 +11,48 @@ expenses in the first place!
That being said, there are a few mechanisms to limit the impact of a
malicious member and to manage changes in membership (e.g. ensuring that
a previous member can no longer access the project). But these
mechanisms don\'t prevent a malicious member from breaking things in
mechanisms don't prevent a malicious member from breaking things in
your project!
## Security model
A project has three main parameters when it comes to security:
A project has four main parameters when it comes to security:
- **project identifier** (equivalent to a \"login\")
- **private code** (equivalent to a \"password\")
- **token** (cryptographically derived from the private code)
- **auth token** (cryptographically derived from the private code)
- **feed token** (also cryptographically derived from the private code)
Somebody with the private code can:
Somebody with the **private code** can:
- access the project through the web interface or the API
- add, modify or remove participants
- add, modify or remove bills
- view statistics of the project
- view project history
- change basic settings of the project
- change the email address associated to the project
- change the private code of the project
- delete the project
Somebody with the token can manipulate the project through the API to do
essentially the same thing:
Somebody with the **auth token** can manipulate the project through the API:
- access the project
- add, modify or remove participants
- add, modify or remove bills
- change basic settings of the project
- change the email address associated to the project
- change the private code of the project
- view statistics of the project
- delete the project
The token can also be used to build \"invitation links\". These links
The auth token is not enough to change basic settings of the project,
or to change the email address or the private code.
The auth token can also be used to build "invitation links". These links
allow to login on the web interface without knowing the private code,
see below.
Somebody with the **feed token** can only access a read-only view of the project
through a RSS feed (at `/<project_id>/feed/<token>.xml`).
## Giving access to a project
There are two main ways to give access to a project to a new person:
@ -57,25 +66,36 @@ The second method is interesting because it does not reveal the private
code. In particular, somebody that is logged-in through the invitation
link will not be able to change the private code, because the web
interface requires a confirmation of the existing private code to change
it. However, a motivated person could extract the token from the
it. Similarly, changing other important settings or deleting the project
from the web interface requires knowledge of the private code.
However, a motivated person could extract the auth token from the
invitation link, use it to access the project through the API, and
change the private code through the API.
delete the project through the API. This is a [known issue](https://github.com/spiral-project/ihatemoney/issues/1206).
## Removing access to a project
If a person should no longer be able to access a project, the only way
is to change the private code.
is to change the private code for the whole project.
This will also automatically change the token: old invitation links
won\'t work anymore, and anybody with the old token will no longer be
able to access the project through the API.
This will prevent anybody from logging in with the old private code.
However, anybody with an existing session cookie will still have
access to the project. This is a [known issue](https://github.com/spiral-project/ihatemoney/issues/857)
that should be fixed.
Changing the private code will automatically change the auth token:
old invitation links won't work anymore, and anybody with the old token
will no longer be able to access the project through the API.
This will also automatically change the feed token, so that existing
links to the RSS feed for the project will no longer work.
## Recovering access to a project
If the private code is no longer known, the creator of the project can
still recover access. He/she must have provided an email address when
creating the project, and Ihatemoney can send a reset link to this email
address (classical \"forgot your password\" functionality).
address (classical "forgot your password" functionality).
Note, however, that somebody with the private code could have changed
the email address in the settings at any time.
@ -91,6 +111,6 @@ Note, however, that the history feature is primarily meant to protect
against mistakes: a malicious member can easily remove all entries from
the history!
The best defense against this kind of issues is\... backups! All data
The best defense against this kind of issues is... backups! All data
for a project can be exported through the settings page or through the
API.
API. The server administrator can also backup the database.

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

@ -47,11 +47,14 @@ ACTIVATE_ADMIN_DASHBOARD = False
# service over plain HTTP.
SESSION_COOKIE_SECURE = True
# Set this to a URL path under which the application will be served. Defaults to "/"
APPLICATION_ROOT = "/"
# You can activate an optional CAPTCHA if you want to. It can be helpful
# to filter spammer bots.
# ENABLE_CAPTCHA = True
ENABLE_CAPTCHA = False
# You may want to point to a special legal page, for instance to give information
# about GDPR, or how you handle the data of your users.
# Set this variable to the URL you want.
# LEGAL_LINK = ""
LEGAL_LINK = ""

View file

@ -36,13 +36,181 @@ class CurrencyConverter(object, metaclass=Singleton):
return rates
def get_currencies(self, with_no_currency=True):
rates = [
rate
for rate in self.get_rates()
if with_no_currency or rate != self.no_currency
currencies = [
"AED",
"AFN",
"ALL",
"AMD",
"ANG",
"AOA",
"ARS",
"AUD",
"AWG",
"AZN",
"BAM",
"BBD",
"BDT",
"BGN",
"BHD",
"BIF",
"BMD",
"BND",
"BOB",
"BRL",
"BSD",
"BTC",
"BTN",
"BWP",
"BYN",
"BZD",
"CAD",
"CDF",
"CHF",
"CLF",
"CLP",
"CNH",
"CNY",
"COP",
"CRC",
"CUC",
"CUP",
"CVE",
"CZK",
"DJF",
"DKK",
"DOP",
"DZD",
"EGP",
"ERN",
"ETB",
"EUR",
"FJD",
"FKP",
"GBP",
"GEL",
"GGP",
"GHS",
"GIP",
"GMD",
"GNF",
"GTQ",
"GYD",
"HKD",
"HNL",
"HRK",
"HTG",
"HUF",
"IDR",
"ILS",
"IMP",
"INR",
"IQD",
"IRR",
"ISK",
"JEP",
"JMD",
"JOD",
"JPY",
"KES",
"KGS",
"KHR",
"KMF",
"KPW",
"KRW",
"KWD",
"KYD",
"KZT",
"LAK",
"LBP",
"LKR",
"LRD",
"LSL",
"LYD",
"MAD",
"MDL",
"MGA",
"MKD",
"MMK",
"MNT",
"MOP",
"MRU",
"MUR",
"MVR",
"MWK",
"MXN",
"MYR",
"MZN",
"NAD",
"NGN",
"NIO",
"NOK",
"NPR",
"NZD",
"OMR",
"PAB",
"PEN",
"PGK",
"PHP",
"PKR",
"PLN",
"PYG",
"QAR",
"RON",
"RSD",
"RUB",
"RWF",
"SAR",
"SBD",
"SCR",
"SDG",
"SEK",
"SGD",
"SHP",
"SLL",
"SOS",
"SRD",
"SSP",
"STD",
"STN",
"SVC",
"SYP",
"SZL",
"THB",
"TJS",
"TMT",
"TND",
"TOP",
"TRY",
"TTD",
"TWD",
"TZS",
"UAH",
"UGX",
"USD",
"UYU",
"UZS",
"VEF",
"VES",
"VND",
"VUV",
"WST",
"XAF",
"XAG",
"XAU",
"XCD",
"XDR",
"XOF",
"XPD",
"XPF",
"XPT",
"YER",
"ZAR",
"ZMW",
"ZWL",
]
rates.sort(key=lambda rate: "" if rate == self.no_currency else rate)
return rates
if with_no_currency:
currencies.append(self.no_currency)
return currencies
def exchange_currency(self, amount, source_currency, dest_currency):
if (

View file

@ -3,14 +3,19 @@ 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
ACTIVATE_ADMIN_DASHBOARD = False
ADMIN_PASSWORD = ""
ALLOW_PUBLIC_PROJECT_CREATION = True
ACTIVATE_ADMIN_DASHBOARD = False
SESSION_COOKIE_SECURE = True
APPLICATION_ROOT = "/"
ENABLE_CAPTCHA = False
LEGAL_LINK = ""
SUPPORTED_LANGUAGES = [
"az",
"ca",
"cs",
"de",
@ -43,5 +48,3 @@ SUPPORTED_LANGUAGES = [
"uk",
"zh_Hans",
]
ENABLE_CAPTCHA = False
LEGAL_LINK = ""

View file

@ -9,7 +9,7 @@ from flask_babel import lazy_gettext as _
from flask_wtf.file import FileAllowed, FileField, FileRequired
from flask_wtf.form import FlaskForm
from markupsafe import Markup
from werkzeug.security import check_password_hash, generate_password_hash
from werkzeug.security import check_password_hash
from wtforms.fields import (
BooleanField,
DateField,
@ -39,10 +39,11 @@ 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,
generate_password_hash,
render_localized_currency,
slugify,
)
@ -66,6 +67,9 @@ def get_billform_for(project, set_default=True, **kwargs):
if form.original_currency.data is None:
form.original_currency.data = project.default_currency
# Used in validate_original_currency
form.project_currency = project.default_currency
show_no_currency = form.original_currency.data == CurrencyConverter.no_currency
form.original_currency.choices = [
@ -86,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):
@ -121,6 +124,11 @@ class CalculatorStringField(StringField):
class EditProjectForm(FlaskForm):
name = StringField(_("Project name"), validators=[DataRequired()])
current_password = PasswordField(
_("Current private code"),
description=_("Enter existing private code to edit project"),
validators=[DataRequired()],
)
# If empty -> don't change the password
password = PasswordField(
_("New private code"),
@ -154,6 +162,13 @@ class EditProjectForm(FlaskForm):
for currency_name in self.currency_helper.get_currencies()
]
def validate_current_password(self, field):
project = Project.query.get(self.id.data)
if project is None:
raise ValidationError(_("Unknown error"))
if not check_password_hash(project.password, self.current_password.data):
raise ValidationError(_("Invalid private code."))
@property
def logging_preference(self):
"""Get the LoggingMode object corresponding to current form data."""
@ -172,12 +187,20 @@ class EditProjectForm(FlaskForm):
and field.data == CurrencyConverter.no_currency
and project.has_multiple_currencies()
):
raise ValidationError(
_(
"This project cannot be set to 'no currency'"
" because it contains bills in multiple currencies."
)
msg = _(
"This project cannot be set to 'no currency'"
" because it contains bills in multiple currencies."
)
raise ValidationError(msg)
if (
project is not None
and field.data != project.default_currency
and project.has_bills()
):
msg = _(
"Cannot change project currency because currency conversion is broken"
)
raise ValidationError(msg)
def update(self, project):
"""Update the project with the information from the form"""
@ -212,7 +235,9 @@ class ImportProjectForm(FlaskForm):
class ProjectForm(EditProjectForm):
id = StringField(_("Project identifier"), validators=[DataRequired()])
# This field overrides the one from EditProjectForm
# Remove this field that is inherited from EditProjectForm
current_password = None
# This field overrides the one from EditProjectForm (to make it mandatory)
password = PasswordField(_("Private code"), validators=[DataRequired()])
submit = SubmitField(_("Create the project"))
@ -338,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"))
@ -351,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)
@ -370,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
@ -391,6 +425,17 @@ class BillForm(FlaskForm):
# See https://github.com/python-babel/babel/issues/821
raise ValidationError(f"Result is too high: {field.data}")
def validate_original_currency(self, field):
# Workaround for currency API breakage
# See #1232
if field.data not in [CurrencyConverter.no_currency, self.project_currency]:
msg = _(
"Failed to convert from %(bill_currency)s currency to %(project_currency)s",
bill_currency=field.data,
project_currency=self.project_currency,
)
raise ValidationError(msg)
class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])

View file

@ -38,7 +38,10 @@ def history_sort_key(history_item_dict):
def describe_version(version_obj):
"""Use the base model str() function to describe a version object"""
return parent_class(type(version_obj)).__str__(version_obj)
if not version_obj:
return ""
else:
return parent_class(type(version_obj)).__str__(version_obj)
def describe_owers_change(version, human_readable_names):
@ -83,10 +86,26 @@ def get_history(project, human_readable_names=True):
"time": version.transaction.issued_at,
"operation_type": version.operation_type,
"object_type": object_type,
"bill_details": None,
"object_desc": object_str,
"ip": version.transaction.remote_addr,
}
if object_type == "Bill":
if version.operation_type == Operation.INSERT or not version.previous:
detailed_version = version
else:
detailed_version = version.previous
details = {
"date": detailed_version.date,
"payer": describe_version(detailed_version.payer),
"amount": detailed_version.amount,
"owers": [describe_version(o) for o in detailed_version.owers],
"external_link": detailed_version.external_link,
"original_currency": detailed_version.original_currency,
}
common_properties["bill_details"] = details
if version.operation_type == Operation.UPDATE:
# Only iterate the changeset if the previous version
# Was logged

View file

@ -4,14 +4,14 @@ import getpass
import os
import random
import sys
import datetime
import click
from flask.cli import FlaskGroup
from werkzeug.security import generate_password_hash
from ihatemoney.models import Project, db
from ihatemoney.run import create_app
from ihatemoney.utils import create_jinja_env
from ihatemoney.utils import create_jinja_env, generate_password_hash
@click.group(cls=FlaskGroup, create_app=create_app)
@ -33,14 +33,14 @@ def runserver(ctx):
ctx.forward(run)
@click.command(name="generate_password_hash")
@cli.command(name="generate_password_hash")
def password_hash():
"""Get password from user and hash it without printing it in clear text."""
password = getpass.getpass(prompt="Password: ")
print(generate_password_hash(password))
@click.command()
@cli.command()
@click.argument(
"config_file",
type=click.Choice(
@ -94,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

@ -10,6 +10,12 @@ msgstr ""
msgid "Project name"
msgstr ""
msgid "Current private code"
msgstr ""
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr ""
@ -31,6 +37,12 @@ msgstr ""
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid private code."
msgstr ""
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -66,12 +78,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid private code."
msgstr ""
msgid "Get in"
msgstr ""
@ -235,6 +241,9 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -725,6 +734,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -747,7 +759,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -801,13 +813,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -830,21 +839,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -857,10 +859,28 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Private code:"
msgstr ""
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"

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
@ -18,11 +19,10 @@ from sqlalchemy import orm
from sqlalchemy.sql import func
from sqlalchemy_continuum import make_versioned, version_class
from sqlalchemy_continuum.plugins import FlaskPlugin
from werkzeug.security import generate_password_hash
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory
from ihatemoney.utils import get_members, same_bill
from ihatemoney.utils import generate_password_hash, get_members, same_bill
from ihatemoney.versioning import (
ConditionalVersioningManager,
LoggingMode,
@ -51,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()
@ -113,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)
for ower in bill.owers:
should_pay[ower.id] += (
ower.weight * bill.converted_amount / total_weight
)
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):
@ -161,7 +182,8 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all():
monthly[bill.date.year][bill.date.month] += bill.converted_amount
if bill.bill_type == BillType.EXPENSE:
monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@property
@ -187,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 []
@ -203,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
@ -337,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),
@ -408,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),
@ -453,7 +460,8 @@ class Project(db.Model):
"""Generate a timed and serialized JsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed),
or "reset" for password reset (invalidated after expiration)
or "reset" for password reset (invalidated after expiration),
or "feed" for project feeds (invalidated when project code changed)
"""
if token_type == "reset":
@ -476,9 +484,10 @@ class Project(db.Model):
:param token: Serialized TimedJsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed),
or "reset" for password reset (invalidated after expiration)
:param project_id: Project ID. Used for token_type "auth" to use the password as serializer
secret key.
or "reset" for password reset (invalidated after expiration),
or "feed" for project feeds (invalidated when project code changed)
:param project_id: Project ID. Used for token_type "auth" and "feed" to use the password
as serializer secret key.
:param max_age: Token expiration time (in seconds). Only used with token_type "reset"
"""
loads_kwargs = {}
@ -536,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,
@ -676,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))
@ -695,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
@ -704,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
)
@ -718,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(

View file

@ -424,6 +424,15 @@ footer .footer-left {
display: table-cell;
}
/* used to tuncate long urls */
.truncated {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 250px;
display: inline-block;
vertical-align: top;
}
.balance .balance-value {
text-align: right;

View file

@ -100,6 +100,7 @@
</div>
{{ input(form.default_currency) }}
{{ input(form.current_password) }}
<div class="actions">
<button class="btn btn-primary">{{ _("Save changes") }}</button>
</div>
@ -178,7 +179,11 @@
<a class="badge badge-secondary" href="#" id="selectnone" onclick="selectCheckboxes(false)">{{_("No one")}}</a>
</p>
<div class="d-flex flex-column flex-wrap overflow-auto" style="max-height: 20em;">
{% for key, value, checked in form.payed_for.iter_choices() | sort(attribute='1') %}
{% for choices in form.payed_for.iter_choices() | sort(attribute='1') %}
{# Compatibility with wtforms<3.1 #}
{% set key = choices[0] %}
{% set value = choices[1] %}
{% set checked = choices[2] %}
<p class="form-check text-break" style="max-width: 50%;">
<label for="payed_for-{{key}}" class="form-check-label">
<input name="payed_for" type="checkbox" {% if checked %}checked{% endif %} class="form-check-input" value="{{key}}" id="payed_for-{{key}}"/>
@ -195,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>
@ -230,7 +236,8 @@
{{ input(form.weight) }}
</fieldset>
<div class="actions">
{{ form.submit(class="btn btn-primary") }}
<button class="btn btn-secondary input-group-addon" type="submit">{{ _("Save") }}</button>
<a href="{{ url_for(".list_bills") }}" class="btn btn-outline-secondary"> {{_("Cancel") }} </a>
</div>
{% endmacro %}

View file

@ -91,6 +91,21 @@
{% endif %}
{% endmacro %}
{% macro bill_details(details, before=False) %}
{% set owers_list_str=details.owers|localize_list|safe %}
<details class="small">
<summary>{% if before %} {{ _("Details of the bill (before the change)") }} {% else %} {{ _("Details of the bill") }} {% endif %}</summary>
{{ _("Date:") }} {{ details.date|em_surround }}.
{{ _("Payer:") }} {{ details.payer|em_surround }}.
{{ _("Amount:") }} {{ details.amount|currency(details.original_currency)|em_surround }}.
{{ _("Owers:") }} {{ owers_list_str }}.
{% if details.external_link %}
{{ _("External link:") }}
<a class="truncated" href="{{ details.external_link }}">{{ details.external_link }}</a>
{% endif %}
</details>
{% endmacro %}
{% block sidebar %}
<div id="table_overflow">
{{ balance_table(show_weight=False, show_header=True) }}
@ -177,13 +192,16 @@
{% trans %}Project {{ name }} added{% endtrans %}
{% elif event.object_type == "Bill" %}
{% trans %}Bill {{ name }} added{% endtrans %}
{{ bill_details(event.bill_details) }}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} added{% endtrans %}
{% endif %}
{% elif event.operation_type == OperationType.UPDATE %}
{% if event.object_type == "Project" %}
{% if event.prop_changed == "password" %}
{{ _("Project private code changed") }}
{{ _("Project private code changed") }}
{% elif event.prop_changed == "logging_preference" %}
{{ change_to_logging_preference(event) }}
{% elif event.prop_changed == "name" %}
@ -195,51 +213,58 @@
{% else %}
{{ _("Project settings modified") }}
{% endif %}
{% elif event.prop_changed == "activated" %}
{% if event.val_after == False %}
{% trans %}Participant {{ name }} deactivated{% endtrans %}
{% else %}
{% trans %}Participant {{ name }} reactivated{% endtrans %}
{% endif %}
{% elif event.prop_changed == "name" %}
{% set new_name=event.val_after|em_surround %}
{% trans %}Participant {{ name }} renamed to {{ new_name }}{% endtrans %}
{% elif event.prop_changed == "what" %}
{% elif event.object_type == "Bill" %}
{% if event.prop_changed == "what" %}
{% set new_description=event.val_after|em_surround %}
{% trans %}Bill {{ name }} renamed to {{ new_description }}{% endtrans %}
{% elif event.prop_changed == "weight" %}
{% set old_weight=event.val_before|em_surround %}
{% set new_weight=event.val_after|em_surround %}
{% trans %}Participant {{ name }}: weight changed from {{ old_weight }} to {{ new_weight }}{% endtrans %}
{% elif event.prop_changed == "external_link" %}
{{ bill_property_change(event, _("External link"), None, "<a href='{link}' >{link}</a>".format(link=event.val_after | escape) | safe | em_surround) }}
{% elif event.prop_changed == "owers_added" %}
{{ owers_changed(event, True)}}
{% elif event.prop_changed == "owers_removed" %}
{{ owers_changed(event, False)}}
{% elif event.prop_changed == "payer" %}
{{ bill_property_change(event, _("Payer"))}}
{% elif event.prop_changed == "amount" %}
{{ bill_property_change(event, _("Amount")) }}
{% elif event.prop_changed == "date" %}
{{ bill_property_change(event, _("Date")) }}
{% elif event.prop_changed == "original_currency" %}
{{ bill_property_change(event, _("Currency")) }}
{% elif event.prop_changed == "converted_amount" %}
{{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %}
{% if event.object_type == "Bill" %}
{% elif event.prop_changed == "external_link" %}
{{ bill_property_change(event, _("External link"), None, "<a class='truncated' href='{link}' >{link}</a>".format(link=event.val_after | escape) | safe | em_surround) }}
{% elif event.prop_changed == "owers_added" %}
{{ owers_changed(event, True)}}
{% elif event.prop_changed == "owers_removed" %}
{{ owers_changed(event, False)}}
{% elif event.prop_changed == "payer" %}
{{ bill_property_change(event, _("Payer"))}}
{% elif event.prop_changed == "amount" %}
{{ bill_property_change(event, _("Amount")) }}
{% elif event.prop_changed == "date" %}
{{ bill_property_change(event, _("Date")) }}
{% elif event.prop_changed == "original_currency" %}
{{ bill_property_change(event, _("Currency")) }}
{% elif event.prop_changed == "converted_amount" %}
{{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %}
{% trans %}Bill {{ name }} modified{% endtrans %}
{% elif event.object_type == "Person" %}
{% endif %}
{{ bill_details(event.bill_details, before=True) }}
{% elif event.object_type == "Person" %}
{% if event.prop_changed == "activated" %}
{% if event.val_after == False %}
{% trans %}Participant {{ name }} deactivated{% endtrans %}
{% else %}
{% trans %}Participant {{ name }} reactivated{% endtrans %}
{% endif %}
{% elif event.prop_changed == "name" %}
{% set new_name=event.val_after|em_surround %}
{% trans %}Participant {{ name }} renamed to {{ new_name }}{% endtrans %}
{% elif event.prop_changed == "weight" %}
{% set old_weight=event.val_before|em_surround %}
{% set new_weight=event.val_after|em_surround %}
{% trans %}Participant {{ name }}: weight changed from {{ old_weight }} to {{ new_weight }}{% endtrans %}
{% else %}
{% trans %}Participant {{ name }} modified{% endtrans %}
{% endif %}
{% endif %}
{% elif event.operation_type == OperationType.DELETE %}
{% if event.object_type == "Bill" %}
{% trans %}Bill {{ name }} removed{% endtrans %}
{{ bill_details(event.bill_details) }}
{% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} removed{% endtrans %}
{% endif %}
{% else %}
{# Should be unreachable #}
{% if event.object_type == "Project" %}

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') }}">
@ -103,6 +103,7 @@
{% if g.project %}
<li><a class="dropdown-item" href="{{ url_for("main.history") }}">{{ _("History") }}</a></li>
<li><a class="dropdown-item" href="{{ url_for("main.edit_project") }}">{{ _("Settings") }}</a></li>
<li><a class="dropdown-item" href="{{ url_for("main.feed", token=g.project.generate_token("feed")) }}">{{ _("RSS Feed") }}</a></li>
{% endif %}
{% if session['projects'] and not ((session['projects'] | length) == 1 and g.project and g.project.id in session['projects']) %}
@ -118,12 +119,14 @@
{% if session['is_admin'] %}
<li><a class="dropdown-item" href="{{ url_for("main.dashboard") }}">{{ _("Dashboard") }}</a></li>
{% endif %}
{% if g.logout_form %}
<li>
<form action="{{ url_for("main.exit") }}" method="post">
{{ g.logout_form.hidden_tag() }}
{{ g.logout_form.submit(class="dropdown-item") }}
</form>
</li>
{% endif %}
</ul>
</li>
{% endif %}
@ -165,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

@ -11,6 +11,11 @@
{% block js %}
{% if add_bill %} $('#new-bill > a').click(); {% endif %}
// focus on first field when adding a bill
$("#bill-form").on('shown.bs.modal', function(){
$(this).find('#what').focus();
});
// ask for confirmation before removing an user
$('.action.delete').each(function(){
var link = $(this).find('button');
@ -44,6 +49,9 @@
{% endblock %}
{% block head %}
<link href="{{ url_for(".feed", token=g.project.generate_token("feed")) }}" type="application/rss+xml" rel="alternate" title="{{ g.project.name }}" />
{% endblock %}
{% block sidebar %}
<div class="sidebar_content">
@ -98,7 +106,7 @@
</ul>
{% endif %}
<span id="new-bill" class="ml-auto pb-2" {% if not g.project.members %} data-toggle="tooltip" title="{{_('You should start by adding participants')}}" {% endif %}>
<a href="{{ url_for('.add_bill') }}" class="btn btn-primary {% if not g.project.members %} disabled {% endif %}" data-toggle="modal" data-keyboard="false" data-target="#bill-form">
<a href="{{ url_for('.add_bill') }}" class="btn btn-primary {% if not g.project.members %} disabled {% endif %}" data-toggle="modal" data-keyboard="true" data-target="#bill-form" autofocus>
<i class="icon icon-white before-text">{{ static_include("images/plus.svg") | safe }}</i>
{{ _("Add a new bill") }}
</a>
@ -161,13 +169,14 @@
<h3>{{ _('No bills')}}</h3>
<p>
{{ _("Nothing to list yet.")}}<br />
{{ _("You probably want to") }}
{%- if g.project.members %} <a href="{{ url_for('.add_bill') }}" data-toggle="modal" data-target="#bill-form">
{{- _("add a bill") -}}
</a> ?
{% else %} <a href="{{ url_for('.add_member') }}">
{{- _('add participants') -}}
</a> ?
{%- if g.project.members %}
<a href="{{ url_for('.add_bill') }}" data-toggle="modal" data-target="#bill-form">
{{- _("Add your first bill") -}}
</a>
{% else %}
<a href="{{ url_for('.add_member') }}">
{{- _("Add the first participant") -}}
</a>
{%- endif -%}
</p>
</div>

View file

@ -0,0 +1,22 @@
<?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 — {{ g.project.name }}</title>
<description>{% trans project_name=g.project.name %}Latest bills from {{ project_name }}{% endtrans %}</description>
<atom:link href="{{ url_for(".feed", token=g.project.generate_token("feed"), _external=True) }}" rel="self" type="application/rss+xml" />
<link>{{ url_for(".list_bills", _external=True) }}</link>
{% for (weights, bill) in bills.items -%}
<item>
<title>{{ bill.what }} - {{ bill.amount|currency(bill.original_currency) }}</title>
<guid isPermaLink="false">{{ bill.id }}</guid>
<dc:creator>{{ bill.payer }}</dc:creator>
{% if bill.external_link %}<link>{{ bill.external_link }}</link>{% endif -%}
<description>{{ bill.date|dateformat("long") }} - {{ bill.owers|join(', ', 'name') }} : {{ (bill.amount/weights)|currency(bill.original_currency) }}</description>
<pubDate>{{ bill.creation_date.strftime("%a, %d %b %Y %T") }} +0000</pubDate>
</item>
{% endfor -%}
</channel>
</rss>

View file

@ -7,20 +7,10 @@
<tbody>
<tr>
<td>
<h3>{{ _('Share Identifier & code') }}</h3>
<h3>{{ _('Share an invitation link') }}</h3>
</td>
<td>
{{ _("You can share the project identifier and the private code by any communication means.") }}
<br />
<strong>{{ _('Identifier:') }}</strong> <a href="{{ url_for("main.list_bills", project_id=g.project.id) }}">{{ g.project.id }}</a>
</td>
</tr>
<tr>
<td>
<h3>{{ _('Share the Link') }}</h3>
</td>
<td>
{{ _("You can directly share the following link via your prefered medium") }}</br>
{{ _("The easiest way to invite people is to give them the following invitation link.<br />They will be able to access the project, manage participants, add/edit/delete bills. However, they will not have access to important settings such as changing the private code or deleting the whole project.") }}</br>
<a href="{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}">
{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
</a>
@ -40,14 +30,26 @@
<h3>{{ _('Send via Emails') }}</h3>
</td>
<td>
<p>{{ _("Specify a (comma separated) list of email adresses you want to notify about the
creation of this budget management project and we will send them an email for you.") }}</p>
<p>{{ _("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.") }}</p>
{% include "display_errors.html" %}
<form class="invites form-horizontal" method="post" accept-charset="utf-8">
{{ forms.invites(form) }}
</form>
</td>
</tr>
<tr>
<td>
<h3>{{ _('Share Identifier & code') }}</h3>
</td>
<td>
<p>{{ _("You can share the project identifier and the private code by any communication means.<br />Anyone with the private code will have access to the full project, including changing settings such as the private code or project email address, or even deleting the whole project.") }}</p>
<p>
<strong>{{ _('Identifier:') }}</strong> <a href="{{ url_for("main.list_bills", project_id=g.project.id) }}">{{ g.project.id }}</a>
<br />
<strong>{{ _('Private code:') }}</strong> {{ _('the private code was defined when you created the project') }}
</p>
</td>
</tr>
</tbody>
</table>

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

@ -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 -%}
@ -61,4 +61,4 @@
{% endblock %}
{# It must be set outside of the block definition #}
{% set messages_shown = True %}
{% set messages_shown = True %}

View file

@ -1,14 +1,14 @@
import base64
import datetime
import json
import unittest
import pytest
from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
class APITestCase(IhatemoneyTestCase):
class TestAPI(IhatemoneyTestCase):
"""Tests the API"""
def api_create(
@ -42,7 +42,7 @@ class APITestCase(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", "")
)
@ -57,7 +57,7 @@ class APITestCase(IhatemoneyTestCase):
resp = self.client.options(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "*")
assert resp.headers["Access-Control-Allow-Origin"] == "*"
def test_basic_auth(self):
# create a project
@ -94,32 +94,32 @@ class APITestCase(IhatemoneyTestCase):
},
)
self.assertTrue(400, resp.status_code)
self.assertEqual(
'{"contact_email": ["Invalid email address."]}\n', resp.data.decode("utf-8")
assert 400 == resp.status_code
assert '{"contact_email": ["Invalid email address."]}\n' == resp.data.decode(
"utf-8"
)
# create it
with self.app.mail.record_messages() as outbox:
resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code)
assert 201 == resp.status_code
# Check that email messages have been sent.
self.assertEqual(len(outbox), 1)
self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"])
assert len(outbox) == 1
assert outbox[0].recipients == ["raclette@notmyidea.org"]
# create it twice should return a 400
resp = self.api_create("raclette")
self.assertTrue(400, resp.status_code)
self.assertIn("id", json.loads(resp.data.decode("utf-8")))
assert 400 == resp.status_code
assert "id" in json.loads(resp.data.decode("utf-8"))
# get information about it
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertTrue(200, resp.status_code)
assert 200 == resp.status_code
expected = {
"members": [],
"name": "raclette",
@ -129,9 +129,9 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
assert decoded_resp == expected
# edit should work
# edit should fail if we don't provide the current private code
resp = self.client.put(
"/api/projects/raclette",
data={
@ -143,14 +143,43 @@ class APITestCase(IhatemoneyTestCase):
},
headers=self.get_auth("raclette"),
)
assert 400 == resp.status_code
self.assertEqual(200, resp.status_code)
# edit should fail if we provide the wrong private code
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "XXX",
"current_password": "fromage aux patates",
"password": "raclette",
"name": "The raclette party",
"project_history": "y",
},
headers=self.get_auth("raclette"),
)
assert 400 == resp.status_code
# edit with the correct private code should work
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "XXX",
"current_password": "raclette",
"password": "raclette",
"name": "The raclette party",
"project_history": "y",
},
headers=self.get_auth("raclette"),
)
assert 200 == resp.status_code
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(200, resp.status_code)
assert 200 == resp.status_code
expected = {
"name": "The raclette party",
"contact_email": "yeah@notmyidea.org",
@ -160,7 +189,7 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
assert decoded_resp == expected
# password change is possible via API
resp = self.client.put(
@ -168,18 +197,19 @@ class APITestCase(IhatemoneyTestCase):
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "XXX",
"current_password": "raclette",
"password": "tartiflette",
"name": "The raclette party",
},
headers=self.get_auth("raclette"),
)
self.assertEqual(200, resp.status_code)
assert 200 == resp.status_code
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
)
self.assertEqual(200, resp.status_code)
assert 200 == resp.status_code
# delete should work
resp = self.client.delete(
@ -190,21 +220,21 @@ class APITestCase(IhatemoneyTestCase):
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(401, resp.status_code)
assert 401 == resp.status_code
def test_token_creation(self):
"""Test that token of project is generated"""
# Create project
resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code)
assert 201 == resp.status_code
# Get token
resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
self.assertEqual(200, resp.status_code)
assert 200 == resp.status_code
decoded_resp = json.loads(resp.data.decode("utf-8"))
@ -213,8 +243,22 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/token",
headers={"Authorization": f"Basic {decoded_resp['token']}"},
)
assert 200 == resp.status_code
self.assertEqual(200, resp.status_code)
# We shouldn't be able to edit project without private code
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "XXX",
"password": "tartiflette",
"name": "The raclette party",
},
headers={"Authorization": f"Basic {decoded_resp['token']}"},
)
assert 400 == resp.status_code
expected_resp = {"current_password": ["This field is required."]}
assert expected_resp == json.loads(resp.data.decode("utf-8"))
def test_token_login(self):
resp = self.api_create("raclette")
@ -225,7 +269,7 @@ class APITestCase(IhatemoneyTestCase):
decoded_resp = json.loads(resp.data.decode("utf-8"))
resp = self.client.get(f"/raclette/join/{decoded_resp['token']}")
# Test that we are redirected.
self.assertEqual(302, resp.status_code)
assert 302 == resp.status_code
def test_member(self):
# create a project
@ -237,7 +281,7 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
assert "[]\n" == req.data.decode("utf-8")
# add a member
req = self.client.post(
@ -248,7 +292,7 @@ class APITestCase(IhatemoneyTestCase):
# the id of the new member should be returned
self.assertStatus(201, req)
self.assertEqual("1\n", req.data.decode("utf-8"))
assert "1\n" == req.data.decode("utf-8")
# the list of participants should contain one member
req = self.client.get(
@ -256,7 +300,7 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(200, req)
self.assertEqual(len(json.loads(req.data.decode("utf-8"))), 1)
assert len(json.loads(req.data.decode("utf-8"))) == 1
# Try to add another member with the same name.
req = self.client.post(
@ -269,7 +313,7 @@ class APITestCase(IhatemoneyTestCase):
# edit the participant
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "weight": 2},
data={"name": "Jeanne", "weight": 2},
headers=self.get_auth("raclette"),
)
@ -281,14 +325,14 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data.decode("utf-8"))["name"])
self.assertEqual(2, json.loads(req.data.decode("utf-8"))["weight"])
assert "Jeanne" == json.loads(req.data.decode("utf-8"))["name"]
assert 2 == json.loads(req.data.decode("utf-8"))["weight"]
# edit this member with same information
# (test PUT idemopotence)
# (test PUT idempotence)
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred"},
data={"name": "Jeanne"},
headers=self.get_auth("raclette"),
)
@ -297,7 +341,7 @@ class APITestCase(IhatemoneyTestCase):
# de-activate the participant
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": False},
data={"name": "Jeanne", "activated": False},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
@ -306,12 +350,12 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(False, json.loads(req.data.decode("utf-8"))["activated"])
assert not json.loads(req.data.decode("utf-8"))["activated"]
# re-activate the participant
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": True},
data={"name": "Jeanne", "activated": True},
headers=self.get_auth("raclette"),
)
@ -319,7 +363,7 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(True, json.loads(req.data.decode("utf-8"))["activated"])
assert json.loads(req.data.decode("utf-8"))["activated"]
# delete a member
@ -335,7 +379,7 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
assert "[]\n" == req.data.decode("utf-8")
def test_bills(self):
# create a project
@ -343,7 +387,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "jeanne")
self.api_add_member("raclette", "quentin")
# get the list of bills (should be empty)
@ -352,7 +396,7 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
assert "[]\n" == req.data.decode("utf-8")
# add a bill
req = self.client.post(
@ -362,6 +406,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -370,7 +415,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "1\n")
assert req.data.decode("utf-8") == "1\n"
# get this bill details
req = self.client.get(
@ -384,8 +429,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -395,19 +441,19 @@ class APITestCase(IhatemoneyTestCase):
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
assert (
datetime.date.today()
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
)
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# the list of bills should length 1
req = self.client.get(
"/api/projects/raclette/bills", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(1, len(json.loads(req.data.decode("utf-8"))))
assert 1 == len(json.loads(req.data.decode("utf-8")))
# edit with errors should return an error
req = self.client.put(
@ -417,6 +463,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -424,9 +471,7 @@ class APITestCase(IhatemoneyTestCase):
)
self.assertStatus(400, req)
self.assertEqual(
'{"date": ["This field is required."]}\n', req.data.decode("utf-8")
)
assert '{"date": ["This field is required."]}\n' == req.data.decode("utf-8")
# edit a bill
req = self.client.put(
@ -436,6 +481,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "beer",
"payer": "2",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -455,8 +501,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 2,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "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",
@ -466,12 +513,12 @@ class APITestCase(IhatemoneyTestCase):
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
assert (
creation_date
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
)
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# delete a bill
req = self.client.delete(
@ -491,7 +538,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "jeanne")
# valid amounts
input_expected = [
@ -511,6 +558,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": input_amount,
},
headers=self.get_auth("raclette"),
@ -518,7 +566,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "{}\n".format(id))
assert req.data.decode("utf-8") == "{}\n".format(id)
# get this bill's details
req = self.client.get(
@ -533,8 +581,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
@ -544,12 +593,12 @@ class APITestCase(IhatemoneyTestCase):
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
assert (
datetime.date.today()
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
)
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# should raise errors
erroneous_amounts = [
@ -568,28 +617,30 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": amount,
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currencies(self):
# check /currencies for list of supported currencies
resp = self.client.get("/api/currencies")
self.assertTrue(201, resp.status_code)
self.assertIn("XXX", json.loads(resp.data.decode("utf-8")))
assert 200 == resp.status_code
assert "XXX" in json.loads(resp.data.decode("utf-8"))
# create project with a default currency
resp = self.api_create("raclette", default_currency="EUR")
self.assertTrue(201, resp.status_code)
assert 201 == resp.status_code
# get information about it
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertTrue(200, resp.status_code)
assert 200 == resp.status_code
expected = {
"members": [],
"name": "raclette",
@ -599,11 +650,11 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
assert decoded_resp == expected
# Add participants
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "jeanne")
self.api_add_member("raclette", "quentin")
# Add a bill without explicit currency
@ -614,6 +665,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@ -622,7 +674,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "1\n")
assert req.data.decode("utf-8") == "1\n"
# get this bill details
req = self.client.get(
@ -636,8 +688,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -647,12 +700,12 @@ class APITestCase(IhatemoneyTestCase):
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
assert (
datetime.date.today()
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
)
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# Change bill amount and currency
req = self.client.put(
@ -662,6 +715,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "30",
"external_link": "https://raclette.fr",
"original_currency": "CAD",
@ -681,8 +735,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
{"activated": True, "id": 2, "name": "fred", "weight": 1.0},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1.0},
],
"bill_type": "Expense",
"amount": 30.0,
"date": "2011-08-10",
"id": 1,
@ -693,7 +748,7 @@ class APITestCase(IhatemoneyTestCase):
got = json.loads(req.data.decode("utf-8"))
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# Add a bill with yet another currency
req = self.client.post(
@ -703,6 +758,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "Pierogi",
"payer": "1",
"payed_for": ["2", "3"],
"bill_type": "Expense",
"amount": "80",
"original_currency": "PLN",
},
@ -711,7 +767,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "2\n")
assert req.data.decode("utf-8") == "2\n"
# Try to remove default project currency, it should fail
req = self.client.put(
@ -719,15 +775,16 @@ class APITestCase(IhatemoneyTestCase):
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "XXX",
"current_password": "raclette",
"password": "raclette",
"name": "The raclette party",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
self.assertIn("This project cannot be set", req.data.decode("utf-8"))
self.assertIn(
"because it contains bills in multiple currencies", req.data.decode("utf-8")
assert "This project cannot be set" in req.data.decode("utf-8")
assert "because it contains bills in multiple currencies" in req.data.decode(
"utf-8"
)
def test_statistics(self):
@ -736,7 +793,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "jeanne")
# add a bill
req = self.client.post(
@ -746,6 +803,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@ -756,33 +814,30 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/statistics", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(
[
{
"balance": 12.5,
"member": {
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
},
"paid": 25.0,
"spent": 12.5,
assert [
{
"balance": 12.5,
"member": {
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
},
{
"balance": -12.5,
"member": {
"activated": True,
"id": 2,
"name": "fred",
"weight": 1.0,
},
"paid": 0,
"spent": 12.5,
"paid": 25.0,
"spent": 12.5,
},
{
"balance": -12.5,
"member": {
"activated": True,
"id": 2,
"name": "jeanne",
"weight": 1.0,
},
],
json.loads(req.data.decode("utf-8")),
)
"paid": 0,
"spent": 12.5,
},
] == json.loads(req.data.decode("utf-8"))
def test_username_xss(self):
# create a project
@ -794,7 +849,7 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "<script>")
result = self.client.get("/raclette/")
self.assertNotIn("<script>", result.data.decode("utf-8"))
assert "<script>" not in result.data.decode("utf-8")
def test_weighted_bills(self):
# create a project
@ -802,7 +857,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "freddy familly", 4)
self.api_add_member("raclette", "jeannedy familly", 4)
self.api_add_member("raclette", "quentin")
# add a bill
@ -813,6 +868,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@ -833,8 +889,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "freddy familly", "weight": 4},
{"activated": True, "id": 2, "name": "jeannedy familly", "weight": 4},
],
"bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@ -843,12 +900,12 @@ class APITestCase(IhatemoneyTestCase):
"original_currency": "XXX",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
assert (
creation_date
== datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
)
del got["creation_date"]
self.assertDictEqual(expected, got)
assert expected == got
# getting it should return a 404
req = self.client.get(
@ -867,7 +924,7 @@ class APITestCase(IhatemoneyTestCase):
{
"activated": True,
"id": 2,
"name": "freddy familly",
"name": "jeannedy familly",
"weight": 4.0,
"balance": -20.0,
},
@ -888,7 +945,7 @@ class APITestCase(IhatemoneyTestCase):
self.assertStatus(200, req)
decoded_req = json.loads(req.data.decode("utf-8"))
self.assertDictEqual(decoded_req, expected)
assert decoded_req == expected
def test_log_created_from_api_call(self):
# create a project
@ -899,15 +956,13 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "zorglub")
resp = self.client.get("/raclette/history", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
assert resp.status_code == 200
assert f"Participant {em_surround('zorglub')} added" in resp.data.decode(
"utf-8"
)
self.assertIn(
f"Project {em_surround('raclette')} added", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert f"Project {em_surround('raclette')} added" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
assert "127.0.0.1" not in resp.data.decode("utf-8")
def test_amount_is_null(self):
self.api_create("raclette")
@ -922,6 +977,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "Expense",
"amount": "0",
},
headers=self.get_auth("raclette"),
@ -950,12 +1006,71 @@ class APITestCase(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")
if __name__ == "__main__":
unittest.main()
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"

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,20 @@
import os
from unittest.mock import MagicMock
from flask_testing import TestCase
from werkzeug.security import generate_password_hash
import pytest
from ihatemoney import models
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.run import create_app, db
from ihatemoney.utils import generate_password_hash
class BaseTestCase(TestCase):
@pytest.mark.usefixtures("client", "converter")
class BaseTestCase:
SECRET_KEY = "TEST SESSION"
SQLALCHEMY_DATABASE_URI = os.environ.get(
"TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://"
)
ENABLE_CAPTCHA = False
def create_app(self):
# Pass the test object as a configuration.
return create_app(self)
def setUp(self):
db.create_all()
# Add dummy data to CurrencyConverter for all tests (since it's a singleton)
mock_data = {
"USD": 1,
"EUR": 0.8,
"CAD": 1.2,
"PLN": 4,
CurrencyConverter.no_currency: 1,
}
converter = CurrencyConverter()
converter.get_rates = MagicMock(return_value=mock_data)
# Also add it to an attribute to make tests clearer
self.converter = converter
def tearDown(self):
# clean after testing
db.session.remove()
db.drop_all()
PASSWORD_HASH_METHOD = "pbkdf2:sha1:1"
PASSWORD_HASH_SALT_LENGTH = 1
def login(self, project, password=None, test_client=None):
password = password or project
@ -56,6 +32,7 @@ class BaseTestCase(TestCase):
default_currency="XXX",
name=None,
password=None,
project_history=True,
):
"""Create a fake project"""
name = name or id
@ -69,6 +46,7 @@ class BaseTestCase(TestCase):
"password": password,
"contact_email": f"{id}@notmyidea.org",
"default_currency": default_currency,
"project_history": project_history,
},
follow_redirects=follow_redirects,
)
@ -79,7 +57,7 @@ class BaseTestCase(TestCase):
data=data,
# follow_redirects=True,
)
self.assertEqual("/{id}/edit" in str(resp.response), not success)
assert ("/{id}/edit" in str(resp.response)) == (not success)
def create_project(self, id, default_currency="XXX", name=None, password=None):
name = name or str(id)
@ -105,11 +83,9 @@ class IhatemoneyTestCase(BaseTestCase):
def assertStatus(self, expected, resp, url=None):
if url is None:
url = resp.request.path
return self.assertEqual(
expected,
resp.status_code,
f"{url} expected {expected}, got {resp.status_code}",
)
assert (
expected == resp.status_code
), f"{url} expected {expected}, got {resp.status_code}"
def enable_admin(self, password="adminpass"):
self.app.config["ACTIVATE_ADMIN_DASHBOARD"] = True

View file

@ -0,0 +1,64 @@
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, 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
yield app
# clean after testing
db.session.remove()
db.drop_all()
@pytest.fixture
def client(app: Flask, request: pytest.FixtureRequest):
client = app.test_client()
request.cls.client = client
yield client
@pytest.fixture
def converter(request: pytest.FixtureRequest):
# Add dummy data to CurrencyConverter for all tests (since it's a singleton)
mock_data = {
"USD": 1,
"EUR": 0.8,
"CAD": 1.2,
"PLN": 4,
CurrencyConverter.no_currency: 1,
}
converter = CurrencyConverter()
converter.get_rates = MagicMock(return_value=mock_data)
# Also add it to an attribute to make tests clearer
request.cls.converter = converter
yield converter

View file

@ -1,4 +1,6 @@
import unittest
import re
import pytest
from ihatemoney import history, models
from ihatemoney.tests.common.help_functions import em_surround
@ -6,24 +8,40 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
from ihatemoney.versioning import LoggingMode
class HistoryTestCase(IhatemoneyTestCase):
def setUp(self):
super().setUp()
self.post_project("demo")
self.login("demo")
@pytest.fixture
def demo(client):
client.post(
"/create",
data={
"name": "demo",
"id": "demo",
"password": "demo",
"contact_email": "demo@notmyidea.org",
"default_currency": "XXX",
"project_history": True,
},
)
client.post(
"/authenticate",
data=dict(id="demo", password="demo"),
)
@pytest.mark.usefixtures("demo")
class TestHistory(IhatemoneyTestCase):
def test_simple_create_logentry_no_ip(self):
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert resp.status_code == 200
assert f"Project {em_surround('demo')} added" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
assert "127.0.0.1" not in resp.data.decode("utf-8")
def change_privacy_to(self, logging_preference):
def change_privacy_to(self, current_password, logging_preference):
# Change only logging_preferences
new_data = {
"name": "demo",
"contact_email": "demo@notmyidea.org",
"current_password": current_password,
"password": "demo",
"default_currency": "XXX",
}
@ -35,142 +53,136 @@ class HistoryTestCase(IhatemoneyTestCase):
# Disable History
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertNotIn("alert-danger", resp.data.decode("utf-8"))
assert resp.status_code == 200
assert "alert-danger" not in resp.data.decode("utf-8")
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
if logging_preference == LoggingMode.DISABLED:
self.assertIn('<input id="project_history"', resp.data.decode("utf-8"))
assert '<input id="project_history"' in resp.data.decode("utf-8")
else:
self.assertIn(
'<input checked id="project_history"', resp.data.decode("utf-8")
)
assert '<input checked id="project_history"' in resp.data.decode("utf-8")
if logging_preference == LoggingMode.RECORD_IP:
self.assertIn('<input checked id="ip_recording"', resp.data.decode("utf-8"))
assert '<input checked id="ip_recording"' in resp.data.decode("utf-8")
else:
self.assertIn('<input id="ip_recording"', resp.data.decode("utf-8"))
assert '<input id="ip_recording"' in resp.data.decode("utf-8")
def assert_empty_history_logging_disabled(self):
resp = self.client.get("/demo/history")
self.assertIn(
"This project has history disabled. New actions won't appear below.",
resp.data.decode("utf-8"),
assert (
"This project has history disabled. New actions won't appear below."
in resp.data.decode("utf-8")
)
self.assertIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
assert "Nothing to list" in resp.data.decode("utf-8")
assert (
"The table below reflects actions recorded prior to disabling project history."
not in resp.data.decode("utf-8")
)
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertNotIn("<td> -- </td>", resp.data.decode("utf-8"))
self.assertNotIn(
f"Project {em_surround('demo')} added", resp.data.decode("utf-8")
assert "Some entries below contain IP addresses," not in resp.data.decode(
"utf-8"
)
assert "127.0.0.1" not in resp.data.decode("utf-8")
assert "<td> -- </td>" not in resp.data.decode("utf-8")
assert f"Project {em_surround('demo')} added" not in resp.data.decode("utf-8")
def test_project_edit(self):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"current_password": "demo",
"password": "123456",
"project_history": "y",
"default_currency": "USD", # Currency changed from default
}
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Project contact email changed to {em_surround('demo2@notmyidea.org')}",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert f"Project {em_surround('demo')} added" in resp.data.decode("utf-8")
assert (
f"Project contact email changed to {em_surround('demo2@notmyidea.org')}"
in resp.data.decode("utf-8")
)
self.assertIn("Project private code changed", resp.data.decode("utf-8"))
self.assertIn(
f"Project renamed to {em_surround('demo2')}", resp.data.decode("utf-8")
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project contact email changed to "),
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project private code changed"),
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert "Project private code changed" in resp.data.decode("utf-8")
assert f"Project renamed to {em_surround('demo2')}" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
"utf-8"
).index("Project contact email changed to ")
assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
"utf-8"
).index("Project private code changed")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
assert "127.0.0.1" not in resp.data.decode("utf-8")
def test_project_privacy_edit(self):
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
self.assertIn(
'<input checked id="project_history" name="project_history" type="checkbox" value="y">',
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert (
'<input checked id="project_history" name="project_history" type="checkbox" value="y">'
in resp.data.decode("utf-8")
)
self.change_privacy_to(LoggingMode.DISABLED)
self.change_privacy_to("demo", LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert resp.status_code == 200
assert "Disabled Project History\n" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
assert "127.0.0.1" not in resp.data.decode("utf-8")
self.change_privacy_to(LoggingMode.RECORD_IP)
self.change_privacy_to("demo", LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Enabled Project History & IP Address Recording", resp.data.decode("utf-8")
assert resp.status_code == 200
assert "Enabled Project History & IP Address Recording" in resp.data.decode(
"utf-8"
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
assert resp.data.decode("utf-8").count("127.0.0.1") == 1
self.change_privacy_to(LoggingMode.ENABLED)
self.change_privacy_to("demo", LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
assert resp.status_code == 200
assert "Disabled IP Address Recording\n" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
assert resp.data.decode("utf-8").count("127.0.0.1") == 2
def test_project_privacy_edit2(self):
self.change_privacy_to(LoggingMode.RECORD_IP)
self.change_privacy_to("demo", LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
assert resp.status_code == 200
assert "Enabled IP Address Recording\n" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
assert resp.data.decode("utf-8").count("127.0.0.1") == 1
self.change_privacy_to(LoggingMode.DISABLED)
self.change_privacy_to("demo", LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Disabled Project History & IP Address Recording", resp.data.decode("utf-8")
assert resp.status_code == 200
assert "Disabled Project History & IP Address Recording" in resp.data.decode(
"utf-8"
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
assert resp.data.decode("utf-8").count("127.0.0.1") == 2
self.change_privacy_to(LoggingMode.ENABLED)
self.change_privacy_to("demo", LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
assert resp.status_code == 200
assert "Enabled Project History\n" in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
assert resp.data.decode("utf-8").count("127.0.0.1") == 2
def do_misc_database_operations(self, logging_mode):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"current_password": "demo",
"password": "123456",
"default_currency": "USD",
}
@ -182,13 +194,13 @@ class HistoryTestCase(IhatemoneyTestCase):
new_data["ip_recording"] = "y"
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
user_id = models.Person.query.one().id
@ -200,11 +212,12 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
bill_id = models.Bill.query.one().id
@ -216,48 +229,46 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
# delete the bill
resp = self.client.post(f"/demo/delete/{bill_id}", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
# delete user using POST method
resp = self.client.post(
f"/demo/members/{user_id}/delete", follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
def test_disable_clear_no_new_records(self):
# Disable logging
self.change_privacy_to(LoggingMode.DISABLED)
self.change_privacy_to("demo", LoggingMode.DISABLED)
# Ensure we can't clear history with a GET or with a password-less POST
resp = self.client.get("/demo/erase_history")
self.assertEqual(resp.status_code, 405)
assert resp.status_code == 405
resp = self.client.post("/demo/erase_history", follow_redirects=True)
self.assertIn(
"Error deleting project history",
resp.data.decode("utf-8"),
)
assert "Error deleting project history" in resp.data.decode("utf-8")
# List history
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"This project has history disabled. New actions won't appear below.",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert (
"This project has history disabled. New actions won't appear below."
in resp.data.decode("utf-8")
)
self.assertIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
assert (
"The table below reflects actions recorded prior to disabling project history."
in resp.data.decode("utf-8")
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
assert "Nothing to list" not in resp.data.decode("utf-8")
assert "Some entries below contain IP addresses," not in resp.data.decode(
"utf-8"
)
# Clear Existing Entries
@ -266,7 +277,7 @@ class HistoryTestCase(IhatemoneyTestCase):
data={"password": "demo"},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
self.assert_empty_history_logging_disabled()
# Do lots of database operations & check that there's still no history
@ -276,52 +287,47 @@ class HistoryTestCase(IhatemoneyTestCase):
def test_clear_ip_records(self):
# Enable IP Recording
self.change_privacy_to(LoggingMode.RECORD_IP)
self.change_privacy_to("demo", LoggingMode.RECORD_IP)
# Do lots of database operations to generate IP address entries
self.do_misc_database_operations(LoggingMode.RECORD_IP)
# Disable IP Recording
self.change_privacy_to(LoggingMode.ENABLED)
self.change_privacy_to("123456", LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below.",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert (
"This project has history disabled. New actions won't appear below."
not in resp.data.decode("utf-8")
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
assert (
"The table below reflects actions recorded prior to disabling project history."
not in resp.data.decode("utf-8")
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
assert "Nothing to list" not in resp.data.decode("utf-8")
assert "Some entries below contain IP addresses," in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
# Generate more operations to confirm additional IP info isn't recorded
self.do_misc_database_operations(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 7)
assert resp.status_code == 200
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
# Ensure we can't clear IP data with a GET or with a password-less POST
resp = self.client.get("/demo/strip_ip_addresses")
self.assertEqual(resp.status_code, 405)
assert resp.status_code == 405
resp = self.client.post("/demo/strip_ip_addresses", follow_redirects=True)
self.assertIn(
"Error deleting recorded IP addresses",
resp.data.decode("utf-8"),
)
assert "Error deleting recorded IP addresses" in resp.data.decode("utf-8")
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 7)
assert resp.status_code == 200
assert resp.data.decode("utf-8").count("127.0.0.1") == 12
assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
# Clear IP Data
resp = self.client.post(
@ -330,33 +336,33 @@ class HistoryTestCase(IhatemoneyTestCase):
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below.",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert (
"This project has history disabled. New actions won't appear below."
not in resp.data.decode("utf-8")
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
assert (
"The table below reflects actions recorded prior to disabling project history."
not in resp.data.decode("utf-8")
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
assert "Nothing to list" not in resp.data.decode("utf-8")
assert "Some entries below contain IP addresses," not in resp.data.decode(
"utf-8"
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 0)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 19)
assert resp.data.decode("utf-8").count("127.0.0.1") == 0
assert resp.data.decode("utf-8").count("<td> -- </td>") == 19
def test_logs_for_common_actions(self):
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
assert resp.status_code == 200
assert f"Participant {em_surround('zorglub')} added" in resp.data.decode(
"utf-8"
)
# create a bill
@ -367,17 +373,17 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert f"Bill {em_surround('fromage à raclette')} added" in resp.data.decode(
"utf-8"
)
# edit the bill
@ -388,48 +394,42 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "new thing",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert f"Bill {em_surround('fromage à raclette')} added" in resp.data.decode(
"utf-8"
)
self.assertRegex(
resp.data.decode("utf-8"),
assert re.search(
r"Bill %s:\s* Amount changed\s* from %s\s* to %s"
% (
em_surround("fromage à raclette", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("10.0", regex_escape=True),
),
)
self.assertIn(
"Bill %s renamed to %s"
% (em_surround("fromage à raclette"), em_surround("new thing")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Bill {em_surround('fromage à raclette')} renamed to"
),
resp.data.decode("utf-8").index("Amount changed"),
)
assert "Bill %s renamed to %s" % (
em_surround("fromage à raclette"),
em_surround("new thing"),
) in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").index(
f"Bill {em_surround('fromage à raclette')} renamed to"
) < resp.data.decode("utf-8").index("Amount changed")
# delete the bill
resp = self.client.post("/demo/delete/1", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('new thing')} removed", resp.data.decode("utf-8")
)
assert resp.status_code == 200
assert f"Bill {em_surround('new thing')} removed" in resp.data.decode("utf-8")
# edit user
resp = self.client.post(
@ -437,39 +437,35 @@ class HistoryTestCase(IhatemoneyTestCase):
data={"weight": 2, "name": "new name"},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert re.search(
r"Participant %s:\s* weight changed\s* from %s\s* to %s"
% (
em_surround("zorglub", regex_escape=True),
em_surround("1.0", regex_escape=True),
em_surround("2.0", regex_escape=True),
),
)
self.assertIn(
"Participant %s renamed to %s"
% (em_surround("zorglub"), em_surround("new name")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Participant {em_surround('zorglub')} renamed"
),
resp.data.decode("utf-8").index("weight changed"),
)
assert "Participant %s renamed to %s" % (
em_surround("zorglub"),
em_surround("new name"),
) in resp.data.decode("utf-8")
assert resp.data.decode("utf-8").index(
f"Participant {em_surround('zorglub')} renamed"
) < resp.data.decode("utf-8").index("weight changed")
# delete user using POST method
resp = self.client.post("/demo/members/1/delete", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
assert resp.status_code == 200
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('new name')} removed", resp.data.decode("utf-8")
assert resp.status_code == 200
assert f"Participant {em_surround('new name')} removed" in resp.data.decode(
"utf-8"
)
def test_double_bill_double_person_edit_second(self):
@ -485,6 +481,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25",
},
)
@ -495,15 +492,16 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20",
},
)
# Should be 5 history entries at this point
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert resp.status_code == 200
assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
assert "127.0.0.1" not in resp.data.decode("utf-8")
# Edit ONLY the amount on the first bill
self.client.post(
@ -513,33 +511,33 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "88",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
assert resp.status_code == 200
assert re.search(
r"Bill {}:\s* Amount changed\s* from {}\s* to {}".format(
em_surround("Bill 1", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("88.0", regex_escape=True),
),
resp.data.decode("utf-8"),
)
self.assertNotRegex(
resp.data.decode("utf-8"),
assert not re.search(
r"Removed\s* {}\s* and\s* {}\s* from\s* owers list".format(
em_surround("User 1", regex_escape=True),
em_surround("User 2", regex_escape=True),
),
resp.data.decode("utf-8"),
)
), resp.data.decode("utf-8")
# Should be 6 history entries at this point
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
assert "127.0.0.1" not in resp.data.decode("utf-8")
def test_bill_add_remove_add(self):
# add two members
@ -554,6 +552,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25",
},
)
@ -562,13 +561,11 @@ class HistoryTestCase(IhatemoneyTestCase):
self.client.post("/demo/delete/1", follow_redirects=True)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
assert resp.status_code == 200
assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
assert "127.0.0.1" not in resp.data.decode("utf-8")
assert f"Bill {em_surround('Bill 1')} added" in resp.data.decode("utf-8")
assert f"Bill {em_surround('Bill 1')} removed" in resp.data.decode("utf-8")
# Add a new bill
self.client.post(
@ -578,22 +575,21 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertEqual(
resp.data.decode("utf-8").count(f"Bill {em_surround('Bill 1')} added"), 1
)
self.assertIn(f"Bill {em_surround('Bill 2')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
assert resp.status_code == 200
assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
assert "127.0.0.1" not in resp.data.decode("utf-8")
assert f"Bill {em_surround('Bill 1')} added" in resp.data.decode("utf-8")
assert (
resp.data.decode("utf-8").count(f"Bill {em_surround('Bill 1')} added") == 1
)
assert f"Bill {em_surround('Bill 2')} added" in resp.data.decode("utf-8")
assert f"Bill {em_surround('Bill 1')} removed" in resp.data.decode("utf-8")
def test_double_bill_double_person_edit_second_no_web(self):
u1 = models.Person(project_id="demo", name="User 1")
@ -614,7 +610,7 @@ class HistoryTestCase(IhatemoneyTestCase):
models.db.session.commit()
history_list = history.get_history(self.get_project("demo"))
self.assertEqual(len(history_list), 5)
assert len(history_list) == 5
# Change just the amount
b1.amount = 5
@ -623,8 +619,8 @@ class HistoryTestCase(IhatemoneyTestCase):
history_list = history.get_history(self.get_project("demo"))
for entry in history_list:
if "prop_changed" in entry:
self.assertNotIn("owers", entry["prop_changed"])
self.assertEqual(len(history_list), 6)
assert "owers" not in entry["prop_changed"]
assert len(history_list) == 6
def test_delete_history_with_project(self):
self.post_project("raclette", password="party")
@ -640,6 +636,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"bill_type": "Expense",
"amount": "10",
"original_currency": "EUR",
},
@ -656,8 +653,4 @@ class HistoryTestCase(IhatemoneyTestCase):
# History should be equal to project creation
history_list = history.get_history(self.get_project("raclette"))
self.assertEqual(len(history_list), 1)
if __name__ == "__main__":
unittest.main()
assert len(history_list) == 1

View file

@ -3,7 +3,11 @@
DEBUG = False
SQLALCHEMY_DATABASE_URI = 'sqlite:///budget.db'
SQLACHEMY_ECHO = DEBUG
SITE_NAME = "I Hate Money"
SECRET_KEY = "supersecret"
MAIL_DEFAULT_SENDER = "Budget manager <admin@example.com>"
PASSWORD_HASH_METHOD = "pbkdf2:sha1:1"
PASSWORD_HASH_SALT_LENGTH = 1

View file

@ -7,3 +7,6 @@ SQLACHEMY_ECHO = DEBUG
SECRET_KEY = "lalatra"
MAIL_DEFAULT_SENDER = "Budget manager <admin@example.com>"
PASSWORD_HASH_METHOD = "pbkdf2:sha1:1"
PASSWORD_HASH_SALT_LENGTH = 1

View file

@ -1,12 +1,49 @@
import copy
import json
import unittest
import pytest
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
from ihatemoney.utils import list_of_dicts2csv, list_of_dicts2json
@pytest.fixture
def import_data(request: pytest.FixtureRequest):
data = [
{
"date": "2017-01-01",
"what": "refund",
"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,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"bill_type": "Expense",
"what": "fromage a raclette",
"amount": 10.0,
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "jeanne", "tata", "pepe"],
},
]
request.cls.data = data
yield data
class CommonTestCase(object):
@pytest.mark.usefixtures("import_data")
class Import(IhatemoneyTestCase):
def setUp(self):
super().setUp()
@ -14,26 +51,29 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Expense",
"amount": 13.33,
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
"owers": ["jeanne"],
},
{
"date": "2016-12-31",
"what": "red wine",
"bill_type": "Expense",
"amount": 200.0,
"payer_name": "fred",
"payer_name": "jeanne",
"payer_weight": 1.0,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"what": "fromage a raclette",
"what": "a raclette",
"bill_type": "Expense",
"amount": 10.0,
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "fred", "tata", "pepe"],
"owers": ["zorglub", "jeanne", "tata", "pepe"],
},
]
@ -55,7 +95,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check if all bills have been added
self.assertEqual(len(bills), len(self.data))
assert len(bills) == len(self.data)
# Check if name of bills are ok
b = [e["what"] for e in bills]
@ -63,22 +103,23 @@ class CommonTestCase(object):
ref = [e["what"] for e in self.data]
ref.sort()
self.assertEqual(b, ref)
assert b == ref
# Check if other informations in bill are ok
for d in self.data:
for b in bills:
if b["what"] == d["what"]:
self.assertEqual(b["payer_name"], d["payer_name"])
self.assertEqual(b["amount"], d["amount"])
self.assertEqual(b["currency"], d["currency"])
self.assertEqual(b["payer_weight"], d["payer_weight"])
self.assertEqual(b["date"], d["date"])
assert b["payer_name"] == d["payer_name"]
assert b["amount"] == d["amount"]
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"]]
list_json.sort()
self.assertEqual(list_project, list_json)
assert list_project == list_json
def test_import_single_currency_in_empty_project_without_currency(self):
# Import JSON with a single currency in an empty project with no
@ -96,7 +137,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check if all bills have been added
self.assertEqual(len(bills), len(self.data))
assert len(bills) == len(self.data)
# Check if name of bills are ok
b = [e["what"] for e in bills]
@ -104,23 +145,24 @@ class CommonTestCase(object):
ref = [e["what"] for e in self.data]
ref.sort()
self.assertEqual(b, ref)
assert b == ref
# Check if other informations in bill are ok
for d in self.data:
for b in bills:
if b["what"] == d["what"]:
self.assertEqual(b["payer_name"], d["payer_name"])
self.assertEqual(b["amount"], d["amount"])
assert b["payer_name"] == d["payer_name"]
assert b["amount"] == d["amount"]
# Currency should have been stripped
self.assertEqual(b["currency"], "XXX")
self.assertEqual(b["payer_weight"], d["payer_weight"])
self.assertEqual(b["date"], d["date"])
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"]]
list_json.sort()
self.assertEqual(list_project, list_json)
assert list_project == list_json
def test_import_multiple_currencies_in_empty_project_without_currency(self):
# Import JSON with multiple currencies in an empty project with no
@ -138,7 +180,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check that there are no bills
self.assertEqual(len(bills), 0)
assert len(bills) == 0
def test_import_no_currency_in_empty_project_with_currency(self):
# Import JSON without currencies (from ihatemoney < 5) in an empty
@ -154,7 +196,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check if all bills have been added
self.assertEqual(len(bills), len(self.data))
assert len(bills) == len(self.data)
# Check if name of bills are ok
b = [e["what"] for e in bills]
@ -162,23 +204,24 @@ class CommonTestCase(object):
ref = [e["what"] for e in self.data]
ref.sort()
self.assertEqual(b, ref)
assert b == ref
# Check if other informations in bill are ok
for d in self.data:
for b in bills:
if b["what"] == d["what"]:
self.assertEqual(b["payer_name"], d["payer_name"])
self.assertEqual(b["amount"], d["amount"])
assert b["payer_name"] == d["payer_name"]
assert b["amount"] == d["amount"]
# All bills are converted to default project currency
self.assertEqual(b["currency"], "EUR")
self.assertEqual(b["payer_weight"], d["payer_weight"])
self.assertEqual(b["date"], d["date"])
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"]]
list_json.sort()
self.assertEqual(list_project, list_json)
assert list_project == list_json
def test_import_no_currency_in_empty_project_without_currency(self):
# Import JSON without currencies (from ihatemoney < 5) in an empty
@ -194,7 +237,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check if all bills have been added
self.assertEqual(len(bills), len(self.data))
assert len(bills) == len(self.data)
# Check if name of bills are ok
b = [e["what"] for e in bills]
@ -202,22 +245,23 @@ class CommonTestCase(object):
ref = [e["what"] for e in self.data]
ref.sort()
self.assertEqual(b, ref)
assert b == ref
# Check if other informations in bill are ok
for d in self.data:
for b in bills:
if b["what"] == d["what"]:
self.assertEqual(b["payer_name"], d["payer_name"])
self.assertEqual(b["amount"], d["amount"])
self.assertEqual(b["currency"], "XXX")
self.assertEqual(b["payer_weight"], d["payer_weight"])
self.assertEqual(b["date"], d["date"])
assert b["payer_name"] == d["payer_name"]
assert b["amount"] == d["amount"]
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"]]
list_json.sort()
self.assertEqual(list_project, list_json)
assert list_project == list_json
def test_import_partial_project(self):
# Import a JSON in a project with already existing data
@ -230,13 +274,14 @@ class CommonTestCase(object):
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": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
self.client.post(
"/raclette/add",
data={
"date": "2016-12-31",
"what": "red wine",
"bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "200",
@ -250,7 +295,7 @@ class CommonTestCase(object):
bills = project.get_pretty_bills()
# Check if all bills have been added
self.assertEqual(len(bills), len(self.data))
assert len(bills) == len(self.data)
# Check if name of bills are ok
b = [e["what"] for e in bills]
@ -258,22 +303,23 @@ class CommonTestCase(object):
ref = [e["what"] for e in self.data]
ref.sort()
self.assertEqual(b, ref)
assert b == ref
# Check if other informations in bill are ok
for d in self.data:
for b in bills:
if b["what"] == d["what"]:
self.assertEqual(b["payer_name"], d["payer_name"])
self.assertEqual(b["amount"], d["amount"])
self.assertEqual(b["currency"], d["currency"])
self.assertEqual(b["payer_weight"], d["payer_weight"])
self.assertEqual(b["date"], d["date"])
assert b["payer_name"] == d["payer_name"]
assert b["amount"] == d["amount"]
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"]]
list_json.sort()
self.assertEqual(list_project, list_json)
assert list_project == list_json
def test_import_wrong_data(self):
self.post_project("raclette")
@ -292,9 +338,10 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
"owers": ["jeanne"],
}
]
for data in [data_wrong_keys, data_amount_missing]:
@ -302,7 +349,7 @@ class CommonTestCase(object):
self.import_project("raclette", self.generate_form_data(data), 400)
class ExportTestCase(IhatemoneyTestCase):
class TestExport(IhatemoneyTestCase):
def test_export(self):
# Export a simple project without currencies
@ -310,7 +357,7 @@ class ExportTestCase(IhatemoneyTestCase):
# add participants
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": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
self.client.post("/raclette/members/add", data={"name": "pépé"})
@ -319,7 +366,8 @@ class ExportTestCase(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",
@ -330,6 +378,7 @@ class ExportTestCase(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
"bill_type": "Expense",
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
@ -341,6 +390,7 @@ class ExportTestCase(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2017-01-01",
"bill_type": "Reimbursement",
"what": "refund",
"payer": 3,
"payed_for": [2],
@ -353,48 +403,49 @@ class ExportTestCase(IhatemoneyTestCase):
expected = [
{
"date": "2017-01-01",
"bill_type": "Reimbursement",
"what": "refund",
"amount": 13.33,
"currency": "XXX",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
"owers": ["jeanne"],
},
{
"date": "2016-12-31",
"bill_type": "Expense",
"what": "red wine",
"amount": 200.0,
"currency": "XXX",
"payer_name": "fred",
"payer_name": "jeanne",
"payer_weight": 1.0,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"what": "fromage \xe0 raclette",
"bill_type": "Expense",
"what": "\xe0 raclette",
"amount": 10.0,
"currency": "XXX",
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
assert json.loads(resp.data.decode("utf-8")) == expected
# 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,fred",
'2016-12-31,red wine,XXX,200.0,fred,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,XXX,zorglub,2.0,"zorglub, fred, 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")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
# generate json export of transactions
resp = self.client.get("/raclette/export/transactions.json")
@ -402,46 +453,45 @@ class ExportTestCase(IhatemoneyTestCase):
{
"amount": 2.00,
"currency": "XXX",
"receiver": "fred",
"receiver": "jeanne",
"ower": "p\xe9p\xe9",
},
{"amount": 55.34, "currency": "XXX", "receiver": "fred", "ower": "tata"},
{"amount": 55.34, "currency": "XXX", "receiver": "jeanne", "ower": "tata"},
{
"amount": 127.33,
"currency": "XXX",
"receiver": "fred",
"receiver": "jeanne",
"ower": "zorglub",
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
assert json.loads(resp.data.decode("utf-8")) == expected
# generate csv export of transactions
resp = self.client.get("/raclette/export/transactions.csv")
expected = [
"amount,currency,receiver,ower",
"2.0,XXX,fred,pépé",
"55.34,XXX,fred,tata",
"127.33,XXX,fred,zorglub",
"2.0,XXX,jeanne,pépé",
"55.34,XXX,jeanne,tata",
"127.33,XXX,jeanne,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
# wrong export_format should return a 404
resp = self.client.get("/raclette/export/transactions.wrong")
self.assertEqual(resp.status_code, 404)
assert resp.status_code == 404
@pytest.mark.skip(reason="Currency conversion is broken")
def test_export_with_currencies(self):
self.post_project("raclette", default_currency="EUR")
# add participants
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": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
self.client.post("/raclette/members/add", data={"name": "pépé"})
@ -450,7 +500,8 @@ class ExportTestCase(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",
@ -463,6 +514,7 @@ class ExportTestCase(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "poutine from Québec",
"bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "100",
@ -475,6 +527,7 @@ class ExportTestCase(IhatemoneyTestCase):
data={
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"payer": 3,
"payed_for": [2],
"amount": "13.33",
@ -488,47 +541,48 @@ class ExportTestCase(IhatemoneyTestCase):
{
"date": "2017-01-01",
"what": "refund",
"bill_type": "Reimbursement",
"amount": 13.33,
"currency": "EUR",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["fred"],
"owers": ["jeanne"],
},
{
"date": "2016-12-31",
"what": "poutine from Qu\xe9bec",
"bill_type": "Expense",
"amount": 100.0,
"currency": "CAD",
"payer_name": "fred",
"payer_name": "jeanne",
"payer_weight": 1.0,
"owers": ["zorglub", "tata"],
},
{
"date": "2016-12-31",
"what": "fromage \xe0 raclette",
"bill_type": "Expense",
"amount": 10.0,
"currency": "EUR",
"payer_name": "zorglub",
"payer_weight": 2.0,
"owers": ["zorglub", "fred", "tata", "p\xe9p\xe9"],
"owers": ["zorglub", "jeanne", "tata", "p\xe9p\xe9"],
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
assert json.loads(resp.data.decode("utf-8")) == expected
# 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,fred",
'2016-12-31,poutine from Québec,100.0,CAD,fred,1.0,"zorglub, tata"',
'2016-12-31,fromage à raclette,10.0,EUR,zorglub,2.0,"zorglub, fred, 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")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
# generate json export of transactions (in EUR!)
resp = self.client.get("/raclette/export/transactions.json")
@ -536,30 +590,33 @@ class ExportTestCase(IhatemoneyTestCase):
{
"amount": 2.00,
"currency": "EUR",
"receiver": "fred",
"receiver": "jeanne",
"ower": "p\xe9p\xe9",
},
{"amount": 10.89, "currency": "EUR", "receiver": "fred", "ower": "tata"},
{"amount": 38.45, "currency": "EUR", "receiver": "fred", "ower": "zorglub"},
{"amount": 10.89, "currency": "EUR", "receiver": "jeanne", "ower": "tata"},
{
"amount": 38.45,
"currency": "EUR",
"receiver": "jeanne",
"ower": "zorglub",
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
assert json.loads(resp.data.decode("utf-8")) == expected
# generate csv export of transactions
resp = self.client.get("/raclette/export/transactions.csv")
expected = [
"amount,currency,receiver,ower",
"2.0,EUR,fred,pépé",
"10.89,EUR,fred,tata",
"38.45,EUR,fred,zorglub",
"2.0,EUR,jeanne,pépé",
"10.89,EUR,jeanne,tata",
"38.45,EUR,jeanne,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
# Change project currency to CAD
project = self.get_project("raclette")
@ -571,30 +628,33 @@ class ExportTestCase(IhatemoneyTestCase):
{
"amount": 3.00,
"currency": "CAD",
"receiver": "fred",
"receiver": "jeanne",
"ower": "p\xe9p\xe9",
},
{"amount": 16.34, "currency": "CAD", "receiver": "fred", "ower": "tata"},
{"amount": 57.67, "currency": "CAD", "receiver": "fred", "ower": "zorglub"},
{"amount": 16.34, "currency": "CAD", "receiver": "jeanne", "ower": "tata"},
{
"amount": 57.67,
"currency": "CAD",
"receiver": "jeanne",
"ower": "zorglub",
},
]
self.assertEqual(json.loads(resp.data.decode("utf-8")), expected)
assert json.loads(resp.data.decode("utf-8")) == expected
# generate csv export of transactions
resp = self.client.get("/raclette/export/transactions.csv")
expected = [
"amount,currency,receiver,ower",
"3.0,CAD,fred,pépé",
"16.34,CAD,fred,tata",
"57.67,CAD,fred,zorglub",
"3.0,CAD,jeanne,pépé",
"16.34,CAD,jeanne,tata",
"57.67,CAD,jeanne,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
def test_export_escape_formulae(self):
self.post_project("raclette", default_currency="EUR")
@ -608,6 +668,7 @@ class ExportTestCase(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "=COS(36)",
"bill_type": "Expense",
"payer": 1,
"payed_for": [1],
"amount": "10.0",
@ -618,29 +679,23 @@ class ExportTestCase(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")
for i, line in enumerate(expected):
self.assertEqual(
set(line.split(",")), set(received_lines[i].strip("\r").split(","))
)
assert set(line.split(",")) == set(received_lines[i].strip("\r").split(","))
class ImportTestCaseJSON(CommonTestCase.Import):
class TestImportJSON(CommonTestCase.Import):
def generate_form_data(self, data):
return {"file": (list_of_dicts2json(data), "test.json")}
class ImportTestCaseCSV(CommonTestCase.Import):
class TestImportCSV(CommonTestCase.Import):
def generate_form_data(self, data):
formatted_data = copy.deepcopy(data)
for d in formatted_data:
d["owers"] = ", ".join([o for o in d.get("owers", [])])
return {"file": (list_of_dicts2csv(formatted_data), "test.csv")}
if __name__ == "__main__":
unittest.main()

View file

@ -1,7 +1,6 @@
import os
import smtplib
import socket
import unittest
from unittest.mock import MagicMock, patch
from sqlalchemy import orm
@ -9,7 +8,12 @@ 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
@ -19,19 +23,18 @@ os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
__HERE__ = os.path.dirname(os.path.abspath(__file__))
class ConfigurationTestCase(BaseTestCase):
class TestConfiguration(BaseTestCase):
def test_default_configuration(self):
"""Test that default settings are loaded when no other configuration file is specified"""
self.assertFalse(self.app.config["DEBUG"])
self.assertFalse(self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"])
self.assertEqual(
self.app.config["MAIL_DEFAULT_SENDER"],
("Budget manager <admin@example.com>"),
assert not self.app.config["DEBUG"]
assert not self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]
assert self.app.config["MAIL_DEFAULT_SENDER"] == (
"Budget manager <admin@example.com>"
)
self.assertTrue(self.app.config["ACTIVATE_DEMO_PROJECT"])
self.assertTrue(self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"])
self.assertFalse(self.app.config["ACTIVATE_ADMIN_DASHBOARD"])
self.assertFalse(self.app.config["ENABLE_CAPTCHA"])
assert self.app.config["ACTIVATE_DEMO_PROJECT"]
assert self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"]
assert not self.app.config["ACTIVATE_ADMIN_DASHBOARD"]
assert not self.app.config["ENABLE_CAPTCHA"]
def test_env_var_configuration_file(self):
"""Test that settings are loaded from a configuration file specified
@ -40,7 +43,7 @@ class ConfigurationTestCase(BaseTestCase):
__HERE__, "ihatemoney_envvar.cfg"
)
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
assert self.app.config["SECRET_KEY"] == "lalatra"
# Test that the specified configuration file is loaded
# even if the default configuration file ihatemoney.cfg exists
@ -50,7 +53,7 @@ class ConfigurationTestCase(BaseTestCase):
)
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
assert self.app.config["SECRET_KEY"] == "lalatra"
os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
@ -59,10 +62,10 @@ class ConfigurationTestCase(BaseTestCase):
in the current directory."""
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "supersecret")
assert self.app.config["SECRET_KEY"] == "supersecret"
class ServerTestCase(IhatemoneyTestCase):
class TestServer(IhatemoneyTestCase):
def test_homepage(self):
# See https://github.com/spiral-project/ihatemoney/pull/358
self.app.config["APPLICATION_ROOT"] = "/"
@ -80,7 +83,7 @@ class ServerTestCase(IhatemoneyTestCase):
self.assertStatus(200, req)
class CommandTestCase(BaseTestCase):
class TestCommand(BaseTestCase):
def test_generate_config(self):
"""Simply checks that all config file generation
- raise no exception
@ -89,32 +92,32 @@ class CommandTestCase(BaseTestCase):
runner = self.app.test_cli_runner()
for config_file in generate_config.params[0].type.choices:
result = runner.invoke(generate_config, config_file)
self.assertNotEqual(len(result.output.strip()), 0)
assert len(result.output.strip()) != 0
def test_generate_password_hash(self):
runner = self.app.test_cli_runner()
with patch("getpass.getpass", new=lambda prompt: "secret"):
result = runner.invoke(password_hash)
self.assertTrue(check_password_hash(result.output.strip(), "secret"))
assert check_password_hash(result.output.strip(), "secret")
def test_demo_project_deletion(self):
self.create_project("demo")
self.assertEqual(self.get_project("demo").name, "demo")
assert self.get_project("demo").name == "demo"
runner = self.app.test_cli_runner()
runner.invoke(delete_project, "demo")
self.assertEqual(len(models.Project.query.all()), 0)
assert len(models.Project.query.all()) == 0
class ModelsTestCase(IhatemoneyTestCase):
class TestModels(IhatemoneyTestCase):
def test_weighted_bills(self):
"""Test the SQL request that fetch all bills and weights"""
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": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
# Add a member with a balance=0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})
@ -127,6 +130,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -138,6 +142,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -149,6 +154,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -156,20 +162,20 @@ class ModelsTestCase(IhatemoneyTestCase):
for weight, bill in project.get_bill_weights().all():
if bill.what == "red wine":
pay_each_expected = 20 / 2
self.assertEqual(bill.amount / weight, pay_each_expected)
assert bill.amount / weight == pay_each_expected
if bill.what == "fromage à raclette":
pay_each_expected = 10 / 4
self.assertEqual(bill.amount / weight, pay_each_expected)
assert bill.amount / weight == pay_each_expected
if bill.what == "delicatessen":
pay_each_expected = 10 / 3
self.assertEqual(bill.amount / weight, pay_each_expected)
assert bill.amount / weight == pay_each_expected
def test_bill_pay_each(self):
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": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
# Add a member with a balance=0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})
@ -182,6 +188,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
@ -193,6 +200,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
@ -204,6 +212,7 @@ class ModelsTestCase(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
@ -216,16 +225,75 @@ class ModelsTestCase(IhatemoneyTestCase):
for bill in zorglub_bills.all():
if bill.what == "red wine":
pay_each_expected = 20 / 2
self.assertEqual(bill.pay_each(), pay_each_expected)
assert bill.pay_each() == pay_each_expected
if bill.what == "fromage à raclette":
pay_each_expected = 10 / 4
self.assertEqual(bill.pay_each(), pay_each_expected)
assert bill.pay_each() == pay_each_expected
if bill.what == "delicatessen":
pay_each_expected = 10 / 3
self.assertEqual(bill.pay_each(), pay_each_expected)
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 EmailFailureTestCase(IhatemoneyTestCase):
class TestEmailFailure(IhatemoneyTestCase):
def test_creation_email_failure_smtp(self):
self.login("raclette")
with patch.object(
@ -233,14 +301,14 @@ class EmailFailureTestCase(IhatemoneyTestCase):
):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
assert (
"We tried to send you an reminder email, but there was an error"
in resp.data.decode("utf-8")
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
assert (
'<a href="/raclette/members/add">Add the first participant'
in resp.data.decode("utf-8")
)
def test_creation_email_failure_socket(self):
@ -248,14 +316,14 @@ class EmailFailureTestCase(IhatemoneyTestCase):
with patch.object(self.app.mail, "send", MagicMock(side_effect=socket.error)):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
assert (
"We tried to send you an reminder email, but there was an error"
in resp.data.decode("utf-8")
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
assert (
'<a href="/raclette/members/add">Add the first participant'
in resp.data.decode("utf-8")
)
def test_password_reset_email_failure(self):
@ -266,14 +334,13 @@ class EmailFailureTestCase(IhatemoneyTestCase):
"/password-reminder", data={"id": "raclette"}, follow_redirects=True
)
# Check that an error message is displayed
self.assertIn(
"there was an error while sending you an email",
resp.data.decode("utf-8"),
assert "there was an error while sending you an email" in resp.data.decode(
"utf-8"
)
# Check that we were not redirected to the success page
self.assertNotIn(
"A link to reset your password has been sent to you",
resp.data.decode("utf-8"),
assert (
"A link to reset your password has been sent to you"
not in resp.data.decode("utf-8")
)
def test_invitation_email_failure(self):
@ -287,17 +354,15 @@ class EmailFailureTestCase(IhatemoneyTestCase):
follow_redirects=True,
)
# Check that an error message is displayed
self.assertIn(
"there was an error while trying to send the invitation emails",
resp.data.decode("utf-8"),
assert (
"there was an error while trying to send the invitation emails"
in resp.data.decode("utf-8")
)
# Check that we are still on the same page (no redirection)
self.assertIn(
"Invite people to join this project", resp.data.decode("utf-8")
)
assert "Invite people to join this project" in resp.data.decode("utf-8")
class CaptchaTestCase(IhatemoneyTestCase):
class TestCaptcha(IhatemoneyTestCase):
ENABLE_CAPTCHA = True
def test_project_creation_with_captcha_case_insensitive(self):
@ -315,7 +380,7 @@ class CaptchaTestCase(IhatemoneyTestCase):
"captcha": "éùüß",
},
)
self.assertEqual(len(models.Project.query.all()), 1)
assert len(models.Project.query.all()) == 1
def test_project_creation_with_captcha(self):
with self.client as c:
@ -329,7 +394,7 @@ class CaptchaTestCase(IhatemoneyTestCase):
"default_currency": "USD",
},
)
self.assertEqual(len(models.Project.query.all()), 0)
assert len(models.Project.query.all()) == 0
c.post(
"/create",
@ -342,7 +407,7 @@ class CaptchaTestCase(IhatemoneyTestCase):
"captcha": "nope",
},
)
self.assertEqual(len(models.Project.query.all()), 0)
assert len(models.Project.query.all()) == 0
c.post(
"/create",
@ -355,7 +420,7 @@ class CaptchaTestCase(IhatemoneyTestCase):
"captcha": "euro",
},
)
self.assertEqual(len(models.Project.query.all()), 1)
assert len(models.Project.query.all()) == 1
def test_api_project_creation_does_not_need_captcha(self):
self.client.get("/")
@ -368,11 +433,11 @@ class CaptchaTestCase(IhatemoneyTestCase):
"contact_email": "raclette@notmyidea.org",
},
)
self.assertTrue(resp.status, 201)
self.assertEqual(len(models.Project.query.all()), 1)
assert resp.status_code == 201
assert len(models.Project.query.all()) == 1
class TestCurrencyConverter(unittest.TestCase):
class TestCurrencyConverter:
converter = CurrencyConverter()
mock_data = {
"USD": 1,
@ -386,28 +451,21 @@ class TestCurrencyConverter(unittest.TestCase):
def test_only_one_instance(self):
one = id(CurrencyConverter())
two = id(CurrencyConverter())
self.assertEqual(one, two)
assert one == two
def test_get_currencies(self):
self.assertCountEqual(
self.converter.get_currencies(),
["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency],
)
currencies = self.converter.get_currencies()
for currency in ["USD", "EUR", "CAD", "PLN", CurrencyConverter.no_currency]:
assert currency in currencies
def test_exchange_currency(self):
result = self.converter.exchange_currency(100, "USD", "EUR")
self.assertEqual(result, 80.0)
assert result == 80.0
def test_failing_remote(self):
rates = {}
with patch("requests.Response.json", new=lambda _: {}), self.assertWarns(
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)
self.assertDictEqual(rates, {CurrencyConverter.no_currency: 1})
if __name__ == "__main__":
unittest.main()
assert rates == {CurrencyConverter.no_currency: 1}

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2022-04-14 21:10+0000\n"
"Last-Translator: Hasidul Islam <ihasidul@gmail.com>\n"
"Language: bn\n"
@ -27,6 +27,13 @@ msgstr ""
msgid "Project name"
msgstr "প্রজেক্টের নাম"
#, fuzzy
msgid "Current private code"
msgstr "নতুন ব্যক্তিগত কোড"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "নতুন ব্যক্তিগত কোড"
@ -48,6 +55,12 @@ msgstr "ডিফল্ট মুদ্রা"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgid "Unknown error"
msgstr "অজানা ত্রুটি"
msgid "Invalid private code."
msgstr "অবৈধ ব্যক্তিগত কোড."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -87,12 +100,6 @@ msgstr "অনুগ্রহ করে, এগিয়ে যেতে ক্
msgid "Enter private code to confirm deletion"
msgstr "মুছে ফেলা নিশ্চিত করতে ব্যক্তিগত কোড লিখুন"
msgid "Unknown error"
msgstr "অজানা ত্রুটি"
msgid "Invalid private code."
msgstr "অবৈধ ব্যক্তিগত কোড."
msgid "Get in"
msgstr "প্রবেশ করুন"
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -747,6 +757,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -769,7 +782,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -823,13 +836,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -852,21 +862,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -879,10 +882,29 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "ব্যক্তিগত কোড"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -989,3 +1011,43 @@ msgstr ""
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2020-08-01 10:41+0000\n"
"Last-Translator: Oymate <dhruboadittya96@gmail.com>\n"
"Language: bn_BD\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name"
msgstr "প্রকল্পের নাম"
#, fuzzy
msgid "Current private code"
msgstr "ব্যক্তিগত কোড"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy
msgid "New private code"
msgstr "ব্যক্তিগত কোড"
@ -51,6 +58,13 @@ msgstr "ডিফল্ট মুদ্রা"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgid "Unknown error"
msgstr "অজানা ত্রুট"
#, fuzzy
msgid "Invalid private code."
msgstr "ব্যক্তিগত কোড"
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -86,13 +100,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr "মুছে ফেলার জন্য ব্যক্তিগত কোড লিখুন"
msgid "Unknown error"
msgstr "অজানা ত্রুট"
#, fuzzy
msgid "Invalid private code."
msgstr "ব্যক্তিগত কোড"
msgid "Get in"
msgstr "ভিতরে আস"
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -750,6 +760,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -772,7 +785,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -826,13 +839,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -855,21 +865,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -882,10 +885,29 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "ব্যক্তিগত কোড"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -1094,3 +1116,43 @@ msgstr ""
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2022-09-12 15:25+0000\n"
"Last-Translator: Maite Guix <maite.guix@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name"
msgstr "Nom del projecte"
#, fuzzy
msgid "Current private code"
msgstr "Codi privat nou"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "Codi privat nou"
@ -52,6 +59,12 @@ msgstr ""
"L'establiment d'una moneda predeterminada permet la conversió de moneda "
"entre factures"
msgid "Unknown error"
msgstr "Error desconegut"
msgid "Invalid private code."
msgstr "Codi privat no vàlid."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -91,12 +104,6 @@ msgstr "Si us plau, valida el captcha per a continuar."
msgid "Enter private code to confirm deletion"
msgstr "Introdueix el codi privat per a confirmar la supressió"
msgid "Unknown error"
msgstr "Error desconegut"
msgid "Invalid private code."
msgstr "Codi privat no vàlid."
msgid "Get in"
msgstr "Entrar-hi"
@ -215,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}"
@ -273,6 +280,9 @@ msgstr "Projecte desconegut"
msgid "Password successfully reset."
msgstr "La contrasenya s'ha restablert correctament."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -791,6 +801,9 @@ msgstr "Historial"
msgid "Settings"
msgstr "Configuració"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "Altres projectes:"
@ -813,7 +826,7 @@ msgstr "Aplicació mòbil"
msgid "Documentation"
msgstr "Documentació"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panell d'administració"
msgid "Legal information"
@ -867,14 +880,13 @@ msgstr "Sense factures"
msgid "Nothing to list yet."
msgstr "Res a enumerar encara."
msgid "You probably want to"
msgstr "Probablement vulguis"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "afegir una factura"
msgid "add participants"
msgstr "afegir participants"
#, fuzzy
msgid "Add the first participant"
msgstr "Editar aquest participant"
msgid "Password reminder"
msgstr "Recordatori de contrasenya"
@ -898,26 +910,15 @@ msgstr "Restablir la contrasenya"
msgid "Invite people to join this project"
msgstr "Convidar a persones a unir-se a aquest projecte"
msgid "Share Identifier & code"
msgstr "Compartir l'identificador i el codi"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Pots compartir l'identificador del projecte i el codi privat per "
"qualsevol mitjà de comunicació."
msgid "Identifier:"
msgstr "Identificador:"
msgid "Share the Link"
msgstr "Compartir l'enllaç"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Pots compartir directament l'enllaç següent a través del teu mitjà "
"preferit"
msgid "Scan QR code"
msgstr ""
@ -928,17 +929,37 @@ msgstr ""
msgid "Send via Emails"
msgstr "Enviar per correu electrònic"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Especifica una llista (separada per comes) dels correus electrònic als "
"que vols notificar la\n"
" creació d'aquest projecte de gestió pressupostària i els "
"hi enviarem un correu electrònic."
msgid "Share Identifier & code"
msgstr "Compartir l'identificador i el codi"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "Identificador:"
#, fuzzy
msgid "Private code:"
msgstr "Codi privat"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Qui ha de pagar?"
@ -1039,3 +1060,25 @@ msgstr "Període"
#~ msgid "Edit the project"
#~ msgstr "Editar el projecte"
#~ msgid "You probably want to"
#~ msgstr "Probablement vulguis"
#~ msgid "add participants"
#~ msgstr "afegir participants"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Pots compartir l'identificador del projecte"
#~ " i el codi privat per qualsevol "
#~ "mitjà de comunicació."
#~ msgid "Share the Link"
#~ msgstr "Compartir l'enllaç"
#~ msgid "You can directly share the following link via your prefered medium"
#~ 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

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2023-07-09 19:50+0000\n"
"Last-Translator: Sebastian Lay <mail@sebastian-lay.de>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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"
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/de/>\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,6 +31,12 @@ msgstr ""
msgid "Project name"
msgstr "Projektname"
msgid "Current private code"
msgstr "Aktueller privater Code"
msgid "Enter existing private code to edit project"
msgstr "Geben Sie Ihren privaten Code ein, um das Projekt zu bearbeiten"
msgid "New private code"
msgstr "Neuer privater Code"
@ -54,6 +60,12 @@ msgstr ""
"Das Festlegen einer Standardwährung ermöglicht die Währungsumrechnung "
"zwischen Rechnungen"
msgid "Unknown error"
msgstr "Unbekannter Fehler"
msgid "Invalid private code."
msgstr "ungültiger privater Code."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -62,7 +74,7 @@ msgstr ""
"es Rechnungen unterschiedlicher Währungen enthält."
msgid "Compatible with Cospend"
msgstr ""
msgstr "Kompatibel mit Cospend"
msgid "Project identifier"
msgstr "Projektkennung"
@ -93,12 +105,6 @@ msgstr "Bitte bestätige das Captcha, um fortzufahren."
msgid "Enter private code to confirm deletion"
msgstr "Geben Sie Ihren privaten Code ein, um die Löschung zu bestätigen"
msgid "Unknown error"
msgstr "Unbekannter Fehler"
msgid "Invalid private code."
msgstr "ungültiger privater Code."
msgid "Get in"
msgstr "Eintreten"
@ -191,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}"
@ -229,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."
@ -278,12 +280,16 @@ msgstr "Unbekanntes Projekt"
msgid "Password successfully reset."
msgstr "Passwort erfolgreich zurückgesetzt."
#, fuzzy
msgid "Project settings have been changed successfully."
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 "
@ -302,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"
@ -311,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"
@ -362,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"
@ -442,9 +445,8 @@ msgstr "Mach mit"
msgid "Edit project"
msgstr "Projekt bearbeiten"
#, fuzzy
msgid "Import project"
msgstr "Projekt bearbeiten"
msgstr "Projekt importieren"
msgid "Download project's data"
msgstr "Projektdaten herunterladen"
@ -471,14 +473,14 @@ msgid "Privacy Settings"
msgstr "Datenschutzeinstellungen"
msgid "Save changes"
msgstr ""
msgstr "Änderungen speichern"
msgid "This will remove all bills and participants in this project!"
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"
@ -489,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"
@ -582,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 "
@ -794,6 +799,9 @@ msgstr "Verlauf"
msgid "Settings"
msgstr "Einstellungen"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "Andere Projekte:"
@ -805,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"
@ -816,7 +824,7 @@ msgstr "Handy-Applikation"
msgid "Documentation"
msgstr "Dokumentation"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Dashboard Administration"
msgid "Legal information"
@ -859,7 +867,7 @@ msgstr "Hinzugefügt am %(date)s"
#, python-format
msgid "Everyone but %(excluded)s"
msgstr "Jeder außer %(excluded)s"
msgstr "Alle außer %(excluded)s"
msgid "delete"
msgstr "Löschen"
@ -870,14 +878,12 @@ msgstr "Keine Ausgaben"
msgid "Nothing to list yet."
msgstr "Noch nichts aufzulisten."
msgid "You probably want to"
msgstr "Du willst wahrscheinlich"
msgid "Add your first bill"
msgstr "Mach deine erste Rechnung"
msgid "add a bill"
msgstr "eine Ausgabe hinzufügen"
msgid "add participants"
msgstr "Teilnehmer hinzufügen"
#, fuzzy
msgid "Add the first participant"
msgstr "Diesen Benutzer bearbeiten"
msgid "Password reminder"
msgstr "Passwort-Erinnerung"
@ -901,44 +907,57 @@ msgstr "Setze dein Passwort zurück"
msgid "Invite people to join this project"
msgstr "Lade Leute ein, diesem Projekt beizutreten"
msgid "Share Identifier & code"
msgstr "Teile die ID & den Code"
msgid "Share an invitation link"
msgstr "Einladung verschicken"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Du kannst die Projekt-ID und den privaten Code auf jedem "
"Kommunikationsweg weitergeben."
msgid "Identifier:"
msgstr "ID:"
msgid "Share the Link"
msgstr "Link teilen"
msgid "You can directly share the following link via your prefered medium"
msgstr "Du kannst den folgenden Link direkt über dein bevorzugtes Medium teilen"
"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"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "ID:"
msgid "Private code:"
msgstr "Privater Code:"
msgid "the private code was defined when you created the project"
msgstr "Der Private Code wurde beim erstellen des Projekts von Ihnen festgelegt"
msgid "Who pays?"
msgstr "Wer zahlt?"
@ -1145,3 +1164,23 @@ msgstr "Zeitraum"
#~ msgid "Edit the project"
#~ msgstr "Projekt bearbeiten"
#~ msgid "You probably want to"
#~ msgstr "Du willst wahrscheinlich"
#~ msgid "add participants"
#~ msgstr "Teilnehmer hinzufügen"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Du kannst die Projekt-ID und den"
#~ " privaten Code auf jedem Kommunikationsweg"
#~ " weitergeben."
#~ msgid "Share the Link"
#~ msgstr "Link teilen"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Du kannst den folgenden Link direkt über dein bevorzugtes Medium teilen"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2021-08-01 08:34+0000\n"
"Last-Translator: Eugenia Russell <eugenia.russell2019@gmail.com>\n"
"Language: el\n"
@ -27,6 +27,13 @@ msgstr ""
msgid "Project name"
msgstr "Τίτλος εργασίας"
#, fuzzy
msgid "Current private code"
msgstr "Ιδιωτικός κωδικός"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy
msgid "New private code"
msgstr "Ιδιωτικός κωδικός"
@ -49,6 +56,14 @@ msgstr "Προεπιλεγμένο Νόμισμα"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "Άγνωστο πρότζεκτ"
#, fuzzy
msgid "Invalid private code."
msgstr "Ιδιωτικός κωδικός"
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -89,14 +104,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr "Εισαγάγετε ιδιωτικό κωδικό για επιβεβαίωση της διαγραφής"
#, fuzzy
msgid "Unknown error"
msgstr "Άγνωστο πρότζεκτ"
#, fuzzy
msgid "Invalid private code."
msgstr "Ιδιωτικός κωδικός"
msgid "Get in"
msgstr "Συνδεθείτε"
@ -269,6 +276,9 @@ msgstr "Άγνωστο πρότζεκτ"
msgid "Password successfully reset."
msgstr "Επαναφορά του κωδικού επιτυχώς."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -776,6 +786,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -798,7 +811,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
#, fuzzy
@ -853,14 +866,12 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgstr ""
#, fuzzy
msgid "Add the first participant"
msgstr "Προσθήκη συμμετέχοντος"
msgid "Password reminder"
msgstr ""
@ -882,21 +893,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -909,10 +913,29 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "Ιδιωτικός κωδικός"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -1097,3 +1120,43 @@ msgstr "Περίοδος"
#~ msgid "Edit the project"
#~ msgstr "Επεξεργαστείτε το έργο"
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2021-10-01 20:35+0000\n"
"Last-Translator: phlostically <phlostically@mailinator.com>\n"
"Language: eo\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name"
msgstr "Nomo de projekto"
#, fuzzy
msgid "Current private code"
msgstr "Nova privata kodo"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "Nova privata kodo"
@ -50,6 +57,12 @@ msgstr "Implicita valuto"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgid "Unknown error"
msgstr "Nekonata eraro"
msgid "Invalid private code."
msgstr "Nevalida privata kodo."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -90,12 +103,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr ""
msgid "Unknown error"
msgstr "Nekonata eraro"
msgid "Invalid private code."
msgstr "Nevalida privata kodo."
msgid "Get in"
msgstr "Eniri"
@ -270,6 +277,9 @@ msgstr "Nekonata projekto"
msgid "Password successfully reset."
msgstr "Pasvorto sukcese restarigita."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -786,6 +796,9 @@ msgstr "Historio"
msgid "Settings"
msgstr "Agordoj"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "Aliaj projektoj:"
@ -808,7 +821,7 @@ msgstr "Poŝaparata programo"
msgid "Documentation"
msgstr "Dokumentaro"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administra panelo"
#, fuzzy
@ -863,14 +876,13 @@ msgstr "Neniu fakturo"
msgid "Nothing to list yet."
msgstr "Nenio listigebla ankoraŭ."
msgid "You probably want to"
msgstr "Vi probable volas"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "aldoni fakturon"
msgid "add participants"
msgstr "aldoni partoprenantojn"
#, fuzzy
msgid "Add the first participant"
msgstr "Aldono partoprenanton"
msgid "Password reminder"
msgstr "Rememorigilo pri pasvorto"
@ -894,22 +906,15 @@ msgstr "Restarigi vian pasvorton"
msgid "Invite people to join this project"
msgstr "Inviti homojn aliĝi al ĉi tiu projekto"
msgid "Share Identifier & code"
msgstr "Konigi identigilon kaj kodon"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr "Vi povas iel ajn konigi la projektan identigilon kaj la privatan kodon."
msgid "Identifier:"
msgstr "Identigilo:"
msgid "Share the Link"
msgstr "Sendi la ligon"
msgid "You can directly share the following link via your prefered medium"
msgstr "Vi povas rekte sendi la jenan hiperligon per via preferata komunikilo"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
msgstr ""
@ -920,17 +925,37 @@ msgstr ""
msgid "Send via Emails"
msgstr "Sendi retpoŝte"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Specifu (kome apartigitan) liston de tiuj retpoŝtaj adresoj, kiujn vi "
"volas sciigi pri la\n"
" kreado de ĉi tiu buĝet-administra projekto, kaj ni sendos"
" al ili retpoŝtajn mesaĝojn por vi."
msgid "Share Identifier & code"
msgstr "Konigi identigilon kaj kodon"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "Identigilo:"
#, fuzzy
msgid "Private code:"
msgstr "Privata kodo"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Kiu pagas?"
@ -1096,3 +1121,21 @@ msgstr "Periodo"
#~ msgid "Edit the project"
#~ msgstr "Redakti la projekton"
#~ msgid "You probably want to"
#~ msgstr "Vi probable volas"
#~ msgid "add participants"
#~ msgstr "aldoni partoprenantojn"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr "Vi povas iel ajn konigi la projektan identigilon kaj la privatan kodon."
#~ msgid "Share the Link"
#~ msgstr "Sendi la ligon"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Vi povas rekte sendi la jenan hiperligon per via preferata komunikilo"

File diff suppressed because it is too large Load diff

View file

@ -1,19 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2022-04-11 17:12+0000\n"
"Last-Translator: Santiago José Gutiérrez Llanos "
"<gutierrezapata17@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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,11 +29,17 @@ msgstr ""
msgid "Project name"
msgstr "Nombre del Proyecto"
msgid "Current private code"
msgstr "código privado actual"
msgid "Enter existing private code to edit project"
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"
@ -43,25 +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 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"
@ -81,22 +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"
msgid "Unknown error"
msgstr "Error desconocido"
msgid "Invalid private code."
msgstr "Código privado inválido."
msgstr "Ingrese el código privado para confirmar la eliminación"
msgid "Get in"
msgstr "Entrar"
@ -117,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"
@ -154,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"
@ -189,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}"
@ -216,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"
@ -276,31 +275,34 @@ msgstr "Proyecto desconocido"
msgid "Password successfully reset."
msgstr "Contraseña restablecida con éxito."
msgid "Project settings have been changed successfully."
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"
@ -309,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 ""
@ -350,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"
@ -360,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"
@ -440,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"
@ -471,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"
@ -490,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"
@ -514,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"
@ -582,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 "
@ -790,6 +790,9 @@ msgstr "Historial"
msgid "Settings"
msgstr "Configuración"
msgid "RSS Feed"
msgstr "rss Feed"
msgid "Other projects :"
msgstr "Otros proyectos :"
@ -801,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"
@ -812,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"
@ -866,14 +869,11 @@ msgstr "Sin facturas"
msgid "Nothing to list yet."
msgstr "Aún no hay nada que listar."
msgid "You probably want to"
msgstr "Probablemente quieras"
msgid "Add your first bill"
msgstr "Añade tu primera factura"
msgid "add a bill"
msgstr "agregar una factura"
msgid "add participants"
msgstr "agregar participantes"
msgid "Add the first participant"
msgstr "Agregar el primer participante"
msgid "Password reminder"
msgstr "Recordar contraseña"
@ -897,46 +897,62 @@ msgstr "Restablecer su contraseña"
msgid "Invite people to join this project"
msgstr "Invita a personas a unirse a este proyecto"
msgid "Share Identifier & code"
msgstr "Compartir identificador y código"
msgid "Share an invitation link"
msgstr "Compartir un enlace de invitación"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Puede compartir el identificador del proyecto y el código privado por "
"cualquier medio de comunicación."
msgid "Identifier:"
msgstr "Identificador:"
msgid "Share the Link"
msgstr "Comparte el enlace"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Puedes compartir directamente el siguiente enlace a través de tu medio "
"preferido"
"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"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"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:"
msgid "Private code:"
msgstr "Código privado:"
msgid "the private code was defined when you created the project"
msgstr "el código privado se definió cuando creaste el proyecto"
msgid "Who pays?"
msgstr "¿Quién paga?"
@ -1126,3 +1142,26 @@ msgstr "Período"
#~ msgid "Edit the project"
#~ msgstr "Editar el proyecto"
#~ msgid "You probably want to"
#~ msgstr "Probablemente quieras"
#~ msgid "add participants"
#~ msgstr "agregar participantes"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Puede compartir el identificador del "
#~ "proyecto y el código privado por "
#~ "cualquier medio de comunicación."
#~ msgid "Share the Link"
#~ msgstr "Comparte el enlace"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ "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-14 10:01+0200\n"
"PO-Revision-Date: 2023-03-19 21:40+0000\n"
"Last-Translator: Sai Mohammad-Hossein Emami <emami@outlook.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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
@ -27,6 +27,13 @@ msgstr "مقدار یا عبارت نامعتبر. فقط اعداد و عملی
msgid "Project name"
msgstr "نام پروژه"
#, fuzzy
msgid "Current private code"
msgstr "کد خصوصی جدید"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "کد خصوصی جدید"
@ -48,6 +55,12 @@ msgstr "واحد پولی پیش فرض"
msgid "Setting a default currency enables currency conversion between bills"
msgstr "تنظیم واحد پولی پیش فرض امکان تبدیل ارز بین قبض‌ها رو فراهم می‌کنه"
msgid "Unknown error"
msgstr "خطای ناشناخته"
msgid "Invalid private code."
msgstr "کد خصوصی نامعتبر."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -87,12 +100,6 @@ msgstr "لطفا برای ادامه کپچا رو تایید کن."
msgid "Enter private code to confirm deletion"
msgstr "کد خصوصی رو برای تایید حذف وارد کن"
msgid "Unknown error"
msgstr "خطای ناشناخته"
msgid "Invalid private code."
msgstr "کد خصوصی نامعتبر."
msgid "Get in"
msgstr "بیا تو"
@ -118,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 "واحد پولی"
@ -173,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 ""
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -747,6 +757,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -769,7 +782,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -823,13 +836,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -852,21 +862,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -879,10 +882,29 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "کد خصوصی"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -1031,3 +1053,42 @@ msgstr ""
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -7,16 +7,17 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2023-07-14 07:59+0000\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-07-29 13:07+0000\n"
"Last-Translator: Baptiste <weblate@bitsofnetworks.org>\n"
"Language-Team: French <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/fr/>\n"
"Language: fr\n"
"Language-Team: French <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/fr/>\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.0-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -33,6 +34,12 @@ msgstr ""
msgid "Project name"
msgstr "Nom de projet"
msgid "Current private code"
msgstr "Code daccès actuel"
msgid "Enter existing private code to edit project"
msgstr "Entrez le code d'accès existant pour éditer le projet"
msgid "New private code"
msgstr "Nouveau code daccès"
@ -56,6 +63,12 @@ msgstr ""
"Choisir une devise par défaut permet d'activer la conversion de devises "
"entre les factures"
msgid "Unknown error"
msgstr "Erreur inconnue"
msgid "Invalid private code."
msgstr "Code daccès invalide."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -95,12 +108,6 @@ msgstr "Merci de valider le captcha avant de continuer."
msgid "Enter private code to confirm deletion"
msgstr "Entrez le code d'accès pour confirmer la suppression"
msgid "Unknown error"
msgstr "Erreur inconnue"
msgid "Invalid private code."
msgstr "Code daccès invalide."
msgid "Get in"
msgstr "Entrer"
@ -273,6 +280,9 @@ msgstr "Projet inconnu"
msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès."
msgid "Project settings have been changed successfully."
msgstr "Les paramètres du projet ont bien été enregistrés."
msgid "Unable to parse CSV"
msgstr "Erreur lors de la lecture du fichier CSV"
@ -461,7 +471,7 @@ msgid "Privacy Settings"
msgstr "Vie privée"
msgid "Save changes"
msgstr ""
msgstr "Sauvegarder les modifications"
msgid "This will remove all bills and participants in this project!"
msgstr "Cela supprimera toutes les factures et participant⋅es du projet !"
@ -789,6 +799,9 @@ msgstr "Historique"
msgid "Settings"
msgstr "Options"
msgid "RSS Feed"
msgstr "Flux RSS"
msgid "Other projects :"
msgstr "Autres projets :"
@ -811,7 +824,7 @@ msgstr "Application mobile"
msgid "Documentation"
msgstr "Documentation"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Panneau d'administration"
msgid "Legal information"
@ -865,14 +878,11 @@ msgstr "Pas encore de factures"
msgid "Nothing to list yet."
msgstr "Rien à lister pour le moment."
msgid "You probably want to"
msgstr "Vous souhaitez sûrement"
msgid "Add your first bill"
msgstr "Ajouter votre première facture"
msgid "add a bill"
msgstr "ajouter une facture"
msgid "add participants"
msgstr "ajouter des participant⋅es"
msgid "Add the first participant"
msgstr "Ajouter le premier participant ou la première participante"
msgid "Password reminder"
msgstr "Rappel du code daccès"
@ -896,24 +906,20 @@ msgstr "Changez votre code d'accès"
msgid "Invite people to join this project"
msgstr "Invitez des personnes à rejoindre ce projet"
msgid "Share Identifier & code"
msgstr "Partager l'identifiant et le code"
msgid "Share an invitation link"
msgstr "Partager un lien d'invitation"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Vous pouvez partager l'identifiant de ce projet et le code d'accès par "
"d'autres moyens."
msgid "Identifier:"
msgstr "Identifiant :"
msgid "Share the Link"
msgstr "Partagez le lien"
msgid "You can directly share the following link via your prefered medium"
msgstr "Vous pouvez directement partager le lien suivant"
"Pour inviter des personnes dans ce projet, vous pouvez leur donner le "
"lien d'invitation ci-dessous.<br />Elles pourront ainsi accéder au "
"projet, gérer les participants, ajouter/modifier/supprimer des factures. "
"En revanche, elles ne pourront pas modifier des paramètres importants "
"tels que le code d'accès ou supprimer le projet."
msgid "Scan QR code"
msgstr "Scannez le QR code"
@ -925,13 +931,37 @@ msgid "Send via Emails"
msgstr "Envoyer par email(s)"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Entrez les emails des personnes avec qui vous souhaitez partager ce "
"projet, nous leur enverrons un lien d'invitation."
"Entrez les emails des personnes avec qui vous souhaitez partager ce projet ("
"en séparant les adresses emails avec des virgules). Nous leur enverrons un "
"mail avec le lien d'invitation."
msgid "Share Identifier & code"
msgstr "Partager l'identifiant et le code"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
"Vous pouvez partager l'identifiant de ce projet et le code d'accès par "
"tout moyen de votre choix.<br />Une personne possédant le code d'accès "
"aura un accès complet au projet, y compris pour changer le code d'accès "
"ou l'adresse email associée au projet, voire même supprimer complètement "
"le projet."
msgid "Identifier:"
msgstr "Identifiant :"
msgid "Private code:"
msgstr "Code daccès :"
msgid "the private code was defined when you created the project"
msgstr "le code d'accès a été défini lorsque vous avez créé le projet"
msgid "Who pays?"
msgstr "Qui doit payer ?"
@ -1338,3 +1368,23 @@ msgstr "Période"
#~ msgid "Edit the project"
#~ msgstr "Éditer le projet"
#~ msgid "You probably want to"
#~ msgstr "Vous souhaitez sûrement"
#~ msgid "add participants"
#~ msgstr "ajouter des participant⋅es"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Vous pouvez partager l'identifiant de ce"
#~ " projet et le code d'accès par "
#~ "d'autres moyens."
#~ msgid "Share the Link"
#~ msgstr "Partagez le lien"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Vous pouvez directement partager le lien suivant"

View file

@ -3,9 +3,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2022-11-07 10:07+0000\n"
"Last-Translator: Raanan Katz <raakatz97@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-07-24 07:07+0000\n"
"Last-Translator: Nati Lintzer <nlintzer@gmail.com>\n"
"Language: he\n"
"Language-Team: Hebrew <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/he/>\n"
@ -28,6 +28,13 @@ msgstr "כמות או ביטוי לא תקין. יש להזין רק מספרי
msgid "Project name"
msgstr "שם הפרויקט"
#, fuzzy
msgid "Current private code"
msgstr "קוד פרטי חדש"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "קוד פרטי חדש"
@ -49,6 +56,12 @@ msgstr "מטבע ברירת המחדל"
msgid "Setting a default currency enables currency conversion between bills"
msgstr "בחירת מטבע ברירת מחדל מאפשרת המרת מטבע בין חשבונות"
msgid "Unknown error"
msgstr "שגיאה לא ידועה"
msgid "Invalid private code."
msgstr "קוד פרטי לא תקין."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -86,12 +99,6 @@ msgstr "אנא אמת את ה-Captcha כדי להמשיך."
msgid "Enter private code to confirm deletion"
msgstr "הזן את הקוד הפרטי כדי לאשר מחיקה"
msgid "Unknown error"
msgstr "שגיאה לא ידועה"
msgid "Invalid private code."
msgstr "קוד פרטי לא תקין."
msgid "Get in"
msgstr "היכנס"
@ -172,25 +179,25 @@ 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 ""
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"מצטערים, אך אירעה שגיאה בעת ששלחנו לכם את המייל עם הוראות איפוס הסיסמה. "
"בבקשה תבדקו את הגדרות המייל בשרת או פנו למנהל השרת."
"אנא בדקו את הגדרות המייל בשרת או פנו למנהל השרת:%(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -219,9 +226,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts."
msgstr "יותר מדי ניסיון התחברות כושלים, אנא נסו שם מאוחר יותר."
msgstr "יותר מדי ניסיון התחברות כושלים."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
@ -263,12 +269,15 @@ msgstr "פרויקט לא ידוע"
msgid "Password successfully reset."
msgstr "הסיסמה אופסה בהצלחה."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "תכונה חסרה:%(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
@ -285,7 +294,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"
@ -295,7 +304,7 @@ msgid "Your invitations have been sent"
msgstr "ההזמנות שלך נשלחו"
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
msgstr "מצטערים, אך אירעה שגיאה בעת שליחת ההזמנות."
#, python-format
msgid "%(member)s has been added"
@ -326,20 +335,21 @@ msgid "Participant '%(name)s' has been modified"
msgstr ""
msgid "The bill has been added"
msgstr ""
msgstr "החשבון נוסף"
msgid "Error deleting bill"
msgstr ""
msgstr "שגיאה בעת מחיקת החשבון"
#, fuzzy
msgid "The bill has been deleted"
msgstr ""
msgstr "ההוצאה נמחקה"
msgid "The bill has been modified"
msgstr ""
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgstr "%(lang)s אינה שפה נתמכת"
msgid "Error deleting project history"
msgstr "שגיאה במחיקת הסטוריית הפרויקט"
@ -354,7 +364,7 @@ msgid "Deleted recorded IP addresses in project history."
msgstr ""
msgid "Sorry, we were unable to find the page you've asked for."
msgstr ""
msgstr "מצטערים, לא הצלחנו לגשת לדף שחיפשת."
msgid "The best thing to do is probably to get back to the main page."
msgstr "כנראה שהדבר הטוב ביותר לעשות הוא לחזור לעמוד הראשי."
@ -378,7 +388,7 @@ msgid "?"
msgstr "?"
msgid "Create a new project"
msgstr "צור פרויקט"
msgstr "צור פרויקט חדש"
msgid "Project"
msgstr "פרויקט"
@ -399,7 +409,7 @@ msgid "Actions"
msgstr "פעולות"
msgid "edit"
msgstr ""
msgstr "ערוך"
msgid "Delete project"
msgstr "מחק פרויקט"
@ -419,9 +429,8 @@ msgstr ""
msgid "Edit project"
msgstr ""
#, fuzzy
msgid "Import project"
msgstr "הפרויקטים שלך"
msgstr "ייבא פרוייקטים"
msgid "Download project's data"
msgstr ""
@ -453,45 +462,44 @@ msgstr ""
msgid "This will remove all bills and participants in this project!"
msgstr ""
#, fuzzy
msgid "Import previously exported project"
msgstr "ייבא קובץ JSON מיוצא"
msgstr "ייבא פרוייקט קודם מיוצא"
msgid "Choose file"
msgstr "בחר קובץ"
msgid "Edit this bill"
msgstr ""
msgstr "ערוך את החשבון"
msgid "Add a bill"
msgstr ""
msgstr "הוסף חשבון"
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgid "Everyone"
msgstr ""
msgstr "כולם"
msgid "No one"
msgstr ""
msgstr "אף אחד"
msgid "More options"
msgstr ""
msgstr "יותר אפשרויות"
msgid "Add participant"
msgstr ""
msgstr "הוסף משתתפים"
msgid "Edit this participant"
msgstr ""
msgstr "ערוך את המשתתף"
msgid "john.doe@example.com, mary.moe@site.com"
msgstr ""
msgstr "john.doe@example.com, mary.moe@site.com"
msgid "Download"
msgstr ""
msgstr "הורד"
msgid "Disabled Project History"
msgstr ""
msgstr "היסטורית פרויקט מושבתת"
msgid "Disabled Project History & IP Address Recording"
msgstr ""
@ -747,7 +755,7 @@ msgid "Projects"
msgstr "פרויקטים"
msgid "Start a new project"
msgstr ""
msgstr "התחל פרוייקט חדש"
msgid "History"
msgstr "הסטוריה"
@ -755,11 +763,14 @@ msgstr "הסטוריה"
msgid "Settings"
msgstr "הגדרות"
msgid "Other projects :"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "פרויקטים אחרים:"
msgid "switch to"
msgstr ""
msgstr "שנה ל"
msgid "Dashboard"
msgstr ""
@ -772,12 +783,12 @@ msgid "Code"
msgstr "קוד"
msgid "Mobile Application"
msgstr ""
msgstr "יישום לנייד"
msgid "Documentation"
msgstr "דוקומנטציה"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -787,29 +798,29 @@ msgid "\"I hate money\" is free software"
msgstr "\"אני שונא כסף\" היא תוכנה חינמית"
msgid "you can contribute and improve it!"
msgstr ""
msgstr "אתה יכול לתרום ולשפר אותו!"
#, python-format
msgid "%(amount)s each"
msgstr ""
msgid "you sure?"
msgstr ""
msgstr "האם אתה בטוח?"
msgid "Invite people"
msgstr ""
msgstr "הזמן אנשים"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgstr "הוצאות ישנות"
msgid "You should start by adding participants"
msgstr ""
msgstr "כדאי להתחיל בהוספת משתמשים"
msgid "Add a new bill"
msgstr ""
msgstr "הוסף הוצאה חדשה"
msgid "For what?"
msgstr "עבור מה?"
@ -823,22 +834,20 @@ msgid "Everyone but %(excluded)s"
msgstr ""
msgid "delete"
msgstr ""
msgstr "מחק"
msgid "No bills"
msgstr ""
msgstr "אין הוצאות"
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgstr ""
#, fuzzy
msgid "Add the first participant"
msgstr "ערוך את המשתתף"
msgid "Password reminder"
msgstr "תזכורת סיסמה"
@ -846,7 +855,7 @@ msgstr "תזכורת סיסמה"
msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
msgstr "נשלח לחשבונך לינק לאיפוס הסיסמה, אנא בדוק את תיבת הדוא\"ל שלך."
msgid "Return to home page"
msgstr "חזור לעמוד הבית"
@ -855,42 +864,54 @@ msgid "Your projects"
msgstr "הפרויקטים שלך"
msgid "Reset your password"
msgstr ""
msgstr "אפס את סיסמתך"
msgid "Invite people to join this project"
msgstr ""
msgstr "הזמן אנשים להצטרף לפרויקט"
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
msgstr "סרוק את הברקוד"
msgid "Use a mobile device with a compatible app."
msgstr "השתמש במכשיר נייד עם יישום תואם."
msgid "Send via Emails"
msgstr "שלח באמצעות דוא\"ל"
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 ""
msgid "Share Identifier & code"
msgstr "שתף מזהה וקוד"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "מזהה:"
msgid "Share the Link"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "קוד פרטי"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
msgid "Scan QR code"
msgstr ""
msgid "Use a mobile device with a compatible app."
msgstr ""
msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -903,7 +924,7 @@ msgid "Who?"
msgstr "מי?"
msgid "Balance"
msgstr ""
msgstr "מאזן"
msgid "deactivate"
msgstr ""
@ -918,7 +939,7 @@ msgid "Spent"
msgstr ""
msgid "Expenses by Month"
msgstr ""
msgstr "הוצאות לפי חודש"
msgid "Period"
msgstr "תקופה"
@ -983,3 +1004,43 @@ msgstr "תקופה"
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr "אתה כנראה רוצה"
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr "הוסף משתמשים"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr "אתה יכול לשתף את מזהה הפרויקט והקוד הפרטי באמצעות כל אמצעי תקשורת."
#~ msgid "Share the Link"
#~ msgstr "שתף את הלינק"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "אתה יכול לשתף ישירות את הלינק באמצעי המועדף עליך"
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2020-06-14 14:41+0000\n"
"Last-Translator: raghupalash <singhpalash0@gmail.com>\n"
"Language: hi\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name"
msgstr "परियोजना का नाम"
#, fuzzy
msgid "Current private code"
msgstr "निजी कोड"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy
msgid "New private code"
msgstr "निजी कोड"
@ -51,6 +58,14 @@ msgstr "डिफ़ॉल्ट मुद्रा"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "अज्ञात परियोजना"
#, fuzzy
msgid "Invalid private code."
msgstr "निजी कोड"
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -89,14 +104,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "अज्ञात परियोजना"
#, fuzzy
msgid "Invalid private code."
msgstr "निजी कोड"
msgid "Get in"
msgstr "अंदर जाइये"
@ -271,6 +278,9 @@ msgstr "अज्ञात परियोजना"
msgid "Password successfully reset."
msgstr "पासवर्ड सफलतापूर्वक रीसेट हो गया है।"
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -794,6 +804,9 @@ msgstr "इतिहास"
msgid "Settings"
msgstr "सेटिंग्स"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "अन्य परियोजनाएँ :"
@ -816,7 +829,7 @@ msgstr "मोबाइल एप्लीकेशन"
msgid "Documentation"
msgstr "प्रलेखन"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "व्यवस्थापन डैशबोर्ड"
#, fuzzy
@ -871,14 +884,13 @@ msgstr "कोई बिल नहीं"
msgid "Nothing to list yet."
msgstr "सूचि बनाने के लिए कुछ नहीं।"
msgid "You probably want to"
msgstr "आप शायद यह करना चाहते हैं"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "बिल जोड़ें"
msgid "add participants"
msgstr "प्रतिभागियों को जोड़ें"
#, fuzzy
msgid "Add the first participant"
msgstr "प्रतिभागी जोड़ें"
msgid "Password reminder"
msgstr "पासवर्ड अनुस्मारक"
@ -902,24 +914,15 @@ msgstr "अपना पासवर्ड रीसेट करें"
msgid "Invite people to join this project"
msgstr "इस परियोजना से जुड़ने के लिए लोगों को आमंत्रित करें"
msgid "Share Identifier & code"
msgstr "पहचानकर्ता और कोड साझा करें"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"आप किसी भी संचार माध्यम से परियोजना पहचानकर्ता और निजी कोड साझा कर सकते "
"हैं।"
msgid "Identifier:"
msgstr "पहचानकर्ता:"
msgid "Share the Link"
msgstr "लिंक साझा करें"
msgid "You can directly share the following link via your prefered medium"
msgstr "आप नीचे दिए गए लिंक को सीधे अपने पसंदीदा माध्यम से साझा कर सकते हैं"
msgid "Scan QR code"
msgstr ""
@ -930,17 +933,37 @@ msgstr ""
msgid "Send via Emails"
msgstr "ईमेल के माध्यम से भेजें"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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"
"\t\t बजट प्रबंधन परियोजना के निर्माण के बारे में सूचित करना चाहते हैं "
"और हम उन्हें आपके लिए एक ईमेल भेजेंगे।"
msgid "Share Identifier & code"
msgstr "पहचानकर्ता और कोड साझा करें"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "पहचानकर्ता:"
#, fuzzy
msgid "Private code:"
msgstr "निजी कोड"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "किसे भुगतान करना है?"
@ -1110,3 +1133,24 @@ msgstr "अवधि"
#~ msgid "Edit the project"
#~ msgstr "प्रोजेक्ट संपादित करें"
#~ msgid "You probably want to"
#~ msgstr "आप शायद यह करना चाहते हैं"
#~ msgid "add participants"
#~ msgstr "प्रतिभागियों को जोड़ें"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "आप किसी भी संचार माध्यम से "
#~ "परियोजना पहचानकर्ता और निजी कोड साझा "
#~ "कर सकते हैं।"
#~ msgid "Share the Link"
#~ msgstr "लिंक साझा करें"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "आप नीचे दिए गए लिंक को सीधे अपने पसंदीदा माध्यम से साझा कर सकते हैं"

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\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"
@ -30,6 +30,13 @@ msgstr ""
msgid "Project name"
msgstr "Nama proyek"
#, fuzzy
msgid "Current private code"
msgstr "Kode pribadi baru"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "Kode pribadi baru"
@ -51,6 +58,12 @@ msgstr "Mata Uang Standar"
msgid "Setting a default currency enables currency conversion between bills"
msgstr "Menetapkan mata uang default memungkinkan konversi mata uang antar tagihan"
msgid "Unknown error"
msgstr "Kesalahan tidak diketahui"
msgid "Invalid private code."
msgstr "Kode pribadi tidak valid."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -90,12 +103,6 @@ msgstr "Mohon, silahkan validasi captcha untuk melanjutkan."
msgid "Enter private code to confirm deletion"
msgstr "Masukkan kode pribadi untuk mengkonfirmasi penghapusan"
msgid "Unknown error"
msgstr "Kesalahan tidak diketahui"
msgid "Invalid private code."
msgstr "Kode pribadi tidak valid."
msgid "Get in"
msgstr "Masuk"
@ -268,6 +275,9 @@ msgstr "Proyek tidak diketahui"
msgid "Password successfully reset."
msgstr "Kata sandi berhasil diatur ulang."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -777,6 +787,9 @@ msgstr "Riwayat"
msgid "Settings"
msgstr "Pengaturan"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "Proyek lainnya:"
@ -799,7 +812,7 @@ msgstr "Aplikasi Gawai"
msgid "Documentation"
msgstr "Dokumentasi"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Dasbor Administrasi"
msgid "Legal information"
@ -853,14 +866,13 @@ msgstr "Tidak ada tagihan"
msgid "Nothing to list yet."
msgstr "Belum ada untuk didaftarkan."
msgid "You probably want to"
msgstr "Anda mungkin menginginkan"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "tambah tagihan"
msgid "add participants"
msgstr "tambah partisipan"
#, fuzzy
msgid "Add the first participant"
msgstr "Sunting anggota ini"
msgid "Password reminder"
msgstr "Pengingat kata sandi"
@ -884,26 +896,15 @@ msgstr "Atur ulang kata sandi Anda"
msgid "Invite people to join this project"
msgstr "Undang orang untuk bergabung dalam proyek ini"
msgid "Share Identifier & code"
msgstr "Bagikan ID & kode"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Anda bisa membagikan ID proyek dan kode pribadi dengan cara komunikasi "
"apapun."
msgid "Identifier:"
msgstr "ID:"
msgid "Share the Link"
msgstr "Bagikan tautan"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Anda bisa membagikan tautan secara langsung melalui media yang Anda "
"inginkan"
msgid "Scan QR code"
msgstr ""
@ -914,17 +915,37 @@ msgstr ""
msgid "Send via Emails"
msgstr "Kirim melalui surel"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Spesifikkan daftar alamat surel (dipisah dengan koma) yang akan Anda "
"kirim pemberitahuan tentang\n"
" pembuatan dari manajemen anggaran proyek ini dan kami "
"akan memngirim mereka sebuah surel."
msgid "Share Identifier & code"
msgstr "Bagikan ID & kode"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "ID:"
#, fuzzy
msgid "Private code:"
msgstr "Kode pribadi"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Siapa membayar?"
@ -1121,3 +1142,27 @@ msgstr "Periode"
#~ msgid "Edit the project"
#~ msgstr "Ubah proyek"
#~ msgid "You probably want to"
#~ msgstr "Anda mungkin menginginkan"
#~ msgid "add participants"
#~ msgstr "tambah partisipan"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Anda bisa membagikan ID proyek dan "
#~ "kode pribadi dengan cara komunikasi "
#~ "apapun."
#~ msgid "Share the Link"
#~ msgstr "Bagikan tautan"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ "Anda bisa membagikan tautan secara "
#~ "langsung melalui media yang Anda "
#~ "inginkan"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2022-07-12 15:18+0000\n"
"Last-Translator: Matteo Piotto <piotto@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-08-04 17:05+0000\n"
"Last-Translator: Alessandro Andro <pasandro.pasandro@gmail.com>\n"
"Language-Team: Italian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/it/>\n"
"Language: it\n"
"Language-Team: Italian <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/it/>\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.0-dev\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -23,45 +23,56 @@ msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Quantità o espressione non valida. Solo numeri e operatori + - * / "
"accettati."
"Quantità o espressione non valida. Sono accettati solo numeri e gli "
"operatori + - * / ."
msgid "Project name"
msgstr "Nome del progetto"
#, fuzzy
msgid "Current private code"
msgstr "Codice privato corrente"
msgid "Enter existing private code to edit project"
msgstr "Inserisci il codice privato corrente per modificare il progetto"
msgid "New private code"
msgstr "Codice privato"
msgstr "Nuovo codice privato"
msgid "Enter a new code if you want to change it"
msgstr "Inserisci un nuovo codice se lo vuoi modificare"
msgstr "Se vuoi modificarlo, inserisci un nuovo codice"
msgid "Email"
msgstr "Email"
msgid "Enable project history"
msgstr "Attivare la cronologia del progetto"
msgstr "Abilita la cronologia del progetto"
msgid "Use IP tracking for project history"
msgstr "Utilizzare la localizzazione IP per lo storico del progetto"
msgstr "Utilizzare la localizzazione IP per la cronologia del progetto"
msgid "Default Currency"
msgstr "Valuta predefinita"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
"Impostando una valuta predefinita abilita la conversione della valuta tra"
" le fatture"
"L'impostazione di una valuta predefinita consente la conversione di valuta "
"tra le fatture"
msgid "Unknown error"
msgstr "Errore sconosciuto"
msgid "Invalid private code."
msgstr "Codice privato non valido."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
msgstr ""
"Questo progetto non può essere impostato come 'nessuna valuta' perché "
"Questo progetto non può essere impostato su \"nessuna valuta\" perché "
"contiene fatture in più valute."
msgid "Compatible with Cospend"
msgstr ""
msgstr "Compatibile con Cospend"
msgid "Project identifier"
msgstr "Identificatore del progetto"
@ -81,25 +92,16 @@ msgstr ""
"favore scegli un identificatore nuovo"
msgid "Which is a real currency: Euro or Petro dollar?"
msgstr ""
msgstr "Qual è una valuta reale: Euro o Petrol dollar?"
#, fuzzy
msgid "euro"
msgstr "Periodo"
msgstr "euro"
msgid "Please, validate the captcha to proceed."
msgstr "Si prega di validare il captcha per procedere."
msgid "Enter private code to confirm deletion"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "Progetto non conosciuto"
#, fuzzy
msgid "Invalid private code."
msgstr "Codice privato non valido."
msgstr "Inserisci il codice privato per confermare la cancellazione"
msgid "Get in"
msgstr "Entra"
@ -171,15 +173,12 @@ msgstr "Peso"
msgid "Add"
msgstr "Aggiungi"
#, fuzzy
msgid "The participant name is invalid"
msgstr "L'utente '%(name)s' è stato rimosso"
msgstr "Il nome del partecipante non è valido"
#, fuzzy
msgid "This project already have this participant"
msgstr "Membro già presente in questo progetto"
#, fuzzy
msgid "People to notify"
msgstr "Persone da avvisare"
@ -194,45 +193,43 @@ msgid "Logout"
msgstr "Esci"
msgid "Please check the email configuration of the server."
msgstr ""
msgstr "Per favore verifica la configurazione delle email del server."
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"Spiacenti, si è verificato un errore durante l'invio delle email di "
"invito. Verifica la configurazione dell'email sul server o contatta "
"l'amministratore."
"Verifica la configurazione dell'email sul server o contatta l'amministratore:"
" %(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
msgstr ""
msgstr "{dual_object_0} e {dual_object_1}"
#. Last two items of a list with more than 3 items
msgid "{previous_object}, and {end_object}"
msgstr ""
msgstr "{previous_object}, e {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 "Nessuna 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 "Troppi tentativi di accesso non riusciti. Riprova più tardi."
@ -243,7 +240,7 @@ msgstr ""
"tentativi rimasti."
msgid "Provided token is invalid"
msgstr ""
msgstr "Il token fornito non è valido"
msgid "This private code is not the right one"
msgstr "Questo codice privato non è quello corretto"
@ -258,14 +255,12 @@ msgstr ""
"Abbiamo provato a inviarti un promemoria per email, ma si è verificato un"
" errore. Puoi comunque utilizzare normalmente il progetto."
#, fuzzy
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions."
msgstr ""
"Spiacenti, si è verificato un errore durante l'invio dell'email con le "
"istruzioni per il reset della password. Verifica la configurazione "
"dell'email del server o contatta l'amministratore."
"istruzioni per il reset della password."
msgid "No token provided"
msgstr "Nessun token fornito"
@ -279,17 +274,22 @@ msgstr "Progetto non conosciuto"
msgid "Password successfully reset."
msgstr "Reset della password effettuato."
msgid "Project settings have been changed successfully."
msgstr "Impostazioni del progetto cambiate correttamente."
msgid "Unable to parse CSV"
msgstr ""
msgstr "Impossibile analizzare il CSV"
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "Attributo mancante: %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
"currency"
msgstr ""
"Impossibile aggiungere fatture in più valute a un progetto senza valuta "
"predefinita"
msgid "Project successfully uploaded"
msgstr "Progetto caricato con successo"
@ -298,10 +298,10 @@ msgid "Project successfully deleted"
msgstr "Progetto rimosso con successo"
msgid "Error deleting project"
msgstr ""
msgstr "Errore nell'eliminazione del progetto"
msgid "Unable to logout"
msgstr ""
msgstr "Impossibile effettuare il logout"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -310,48 +310,45 @@ msgstr "Sei stato invitato a condividere le tue spese per %(project)s"
msgid "Your invitations have been sent"
msgstr "I tuoi inviti sono stati spediti"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
"Spiacenti, si è verificato un errore durante l'invio delle email di "
"invito. Verifica la configurazione dell'email sul server o contatta "
"l'amministratore."
"Spiacenti, si è verificato un errore durante l'invio delle email di invito."
#, python-format
msgid "%(member)s has been added"
msgstr "%(member)s è stato aggiunto"
msgid "Error activating participant"
msgstr ""
msgstr "Errore nell'attivazione del partecipante"
#, python-format
msgid "%(name)s is part of this project again"
msgstr "%(name)s fa nuovamente parte di questo progetto"
msgid "Error removing participant"
msgstr ""
msgstr "Errore nella rimozione del partecipante"
#, fuzzy, python-format
#, python-format
msgid ""
"Participant '%(name)s' has been deactivated. It will still appear in the "
"list until its balance reach zero."
msgstr ""
"L'utente '%(name)s' è stato disattivato. Comparirà ancora nella lista "
"Il partecipante '%(name)s' è stato disattivato. Comparirà ancora nella lista "
"utenti finché il suo bilancio sarà superiore a zero."
#, fuzzy, python-format
#, python-format
msgid "Participant '%(name)s' has been removed"
msgstr "L'utente '%(name)s' è stato rimosso"
msgstr "Il partecipante '%(name)s' è stato rimosso"
#, fuzzy, python-format
#, python-format
msgid "Participant '%(name)s' has been modified"
msgstr "L'utente '%(name)s' è stato rimosso"
msgstr "Il partecipante '%(name)s' è stato modificato"
msgid "The bill has been added"
msgstr "L'addebito è stato aggiunto"
msgid "Error deleting bill"
msgstr ""
msgstr "Errore nella cancellazione della spesa"
msgid "The bill has been deleted"
msgstr "La spesa è stata cancellata"
@ -361,22 +358,19 @@ msgstr "L'addebito è stato aggiornato"
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgstr "%(lang)s non è una lingua supportata"
#, fuzzy
msgid "Error deleting project history"
msgstr "Attivare la cronologia del progetto"
msgstr "Errore nel cancellare la cronologia del progetto"
#, fuzzy
msgid "Deleted project history."
msgstr "Cronologia del progetto cancellata."
#, fuzzy
msgid "Error deleting recorded IP addresses"
msgstr "Cancella indirizzi IP conservati"
msgstr "Errore nel cancellare gli indirizzi IP salvati"
msgid "Deleted recorded IP addresses in project history."
msgstr ""
msgstr "Eliminati gli indirizzi IP registrati nella cronologia del progetto."
msgid "Sorry, we were unable to find the page you've asked for."
msgstr "Spiacenti, non abbiamo trovato la pagina che cerchi."
@ -390,9 +384,8 @@ msgstr "Ritorna all'elenco"
msgid "Administration tasks are currently disabled."
msgstr "In questo momento le funzionalità amministrative sono disabilitate."
#, fuzzy
msgid "Authentication"
msgstr "Documentazione"
msgstr "Autenticazione"
msgid "The project you are trying to access do not exist, do you want to"
msgstr "Il progetto cui stai provando ad accedere non esiste, vuoi"
@ -409,9 +402,8 @@ msgstr "Crea un nuovo progetto"
msgid "Project"
msgstr "Progetto"
#, fuzzy
msgid "Number of participants"
msgstr "aggiunti partecipanti"
msgstr "Numro di partecipanti"
msgid "Number of bills"
msgstr "Numero di spese"
@ -428,9 +420,8 @@ msgstr "Azioni"
msgid "edit"
msgstr "modifica"
#, fuzzy
msgid "Delete project"
msgstr "Modifica progetto"
msgstr "Elimina il progetto"
msgid "show"
msgstr "visualizza"
@ -438,20 +429,17 @@ msgstr "visualizza"
msgid "The Dashboard is currently deactivated."
msgstr "Il Cruscotto è attualmente disabilitato."
#, fuzzy
msgid "Download Mobile Application"
msgstr "Applicazione mobile"
msgstr "Scarica l'applicazione per i dispositivi mobili"
#, fuzzy
msgid "Get it on"
msgstr "Entra"
msgid "Edit project"
msgstr "Modifica progetto"
#, fuzzy
msgid "Import project"
msgstr "Modifica progetto"
msgstr "Importa progetto"
msgid "Download project's data"
msgstr "Scarica i dati del progetto"
@ -480,14 +468,13 @@ msgid "Privacy Settings"
msgstr "Impostazioni Privacy"
msgid "Save changes"
msgstr ""
msgstr "Salva le modifiche"
msgid "This will remove all bills and participants in this project!"
msgstr ""
msgstr "Questo eliminerà tutte le spese e i partecipanti a questo progetto!"
#, fuzzy
msgid "Import previously exported project"
msgstr "Importare il file JSON esportato precedentemente"
msgstr "Importa il file JSON esportato precedentemente"
msgid "Choose file"
msgstr "Scegli file"
@ -499,23 +486,22 @@ msgid "Add a bill"
msgstr "Aggiungi un addebito"
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgstr "Sono permesse operazioni semplici, e.g. (18+36.2)/3"
msgid "Everyone"
msgstr "Tutti"
msgid "No one"
msgstr ""
msgstr "Nessuno"
msgid "More options"
msgstr ""
msgstr "Più opzioni"
msgid "Add participant"
msgstr "Aggiungi partecipante"
#, fuzzy
msgid "Edit this participant"
msgstr "Aggiungi partecipante"
msgstr "Modifica questo partecipante"
msgid "john.doe@example.com, mary.moe@site.com"
msgstr "mario.rossi@example.com, maria.bianchi@site.com"
@ -546,11 +532,11 @@ msgstr "Impostazioni Cronologia Aggiornate"
#, python-format
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
msgstr ""
msgstr "Spesa %(name)s: %(property_name)s cambiata da %(before)s a %(after)s"
#, python-format
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
msgstr ""
msgstr "Spesa %(name)s: %(property_name)s cambiata in %(after)s"
msgid "Confirm Remove IP Adresses"
msgstr "Conferma Rimozione Indirizzi IP"
@ -566,9 +552,8 @@ msgstr ""
" La parte residua dello storico del progetto non subirà "
"modifiche. Questa azione non potrà essere annullata."
#, fuzzy
msgid "Confirm deletion"
msgstr "Conferma Cancellazione"
msgstr "Conferma la cancellazione"
msgid "Close"
msgstr "Chiudi"
@ -585,37 +570,36 @@ msgstr ""
#, python-format
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
msgstr ""
msgstr "Spesa %(name)s: aggiunto %(owers_list_str)s alla lista dei proprietari"
#, python-format
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
msgstr ""
msgstr "Spesa %(name)s: rimosso %(owers_list_str)s dalla lista dei proprietari"
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
"Questo progetto ha la cronologia disabilitata. Le nuove azioni non "
"appariranno in basso."
#, fuzzy
msgid "You can enable history on the settings page."
msgstr ""
"La registrazione degli indirizzi IP può essere attivata nella pagina "
"delle impostazioni"
msgstr "È possibile attivare la cronologia nella pagina delle impostazioni."
msgid ""
"The table below reflects actions recorded prior to disabling project "
"history."
msgstr ""
"La tabella seguente riflette le azioni registrate prima della "
"disabilitazione della cronologia del progetto."
#, fuzzy
msgid "You can clear the project history to remove them."
msgstr "Probabilmente qualcuno ha svuotato lo storico del progetto."
msgstr "Puoi cancellare la cronologia del progetto per rimuoverli."
#, fuzzy
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
"Alcune voci qui sotto contengono indirizzi IP, anche se in questo "
"progetto la registrazione degli IP è disabilitata. "
"Alcune voci qui sotto contengono degli indirizzi IP, anche se in questo "
"progetto la registrazione degli IP sia stata disabilitata. "
msgid "Delete stored IP addresses"
msgstr "Cancella indirizzi IP conservati"
@ -626,7 +610,6 @@ msgstr "Nessun indirizzo IP da cancellare"
msgid "Delete Stored IP Addresses"
msgstr "Cancella Indirizzi IP Conservati"
#, fuzzy
msgid "No history to erase"
msgstr "Nessuna cronologia da cancellare"
@ -656,47 +639,47 @@ msgstr "Dall'IP"
msgid "Project %(name)s added"
msgstr "Progetto %(name)s aggiunto"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s added"
msgstr "L'addebito è stato aggiunto"
msgstr "La spesa %(name)s è stata aggiunta"
#, python-format
msgid "Participant %(name)s added"
msgstr ""
msgstr "Il partecipante %(name)s è stato aggiunto"
msgid "Project private code changed"
msgstr "Codice privato del progetto modificato"
#, fuzzy, python-format
#, python-format
msgid "Project renamed to %(new_project_name)s"
msgstr "L'identificatore del progetto è %(new_project_name)s"
msgstr "Il progetto è stato rinominato in %(new_project_name)s"
#, fuzzy, python-format
#, python-format
msgid "Project contact email changed to %(new_email)s"
msgstr "Contatto email del progetto modificato in"
msgstr "Contatto email del progetto modificato in %(new_email)s"
msgid "Project settings modified"
msgstr "Impostazione del progetto modificate"
#, python-format
msgid "Participant %(name)s deactivated"
msgstr ""
msgstr "Il partecipante %(name)s è stato disattivato"
#, python-format
msgid "Participant %(name)s reactivated"
msgstr ""
msgstr "Il partecipante %(name)s è stato riattivato"
#, python-format
msgid "Participant %(name)s renamed to %(new_name)s"
msgstr ""
msgstr "Il partecipante %(name)s è stato rinominato in %(new_name)s"
#, python-format
msgid "Bill %(name)s renamed to %(new_description)s"
msgstr ""
msgstr "La spesa %(name)s è stata rinominata in %(new_description)s"
#, python-format
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
msgstr ""
msgstr "Partecipante %(name)s: peso cambiato da %(old_weight)s a %(new_weight)s"
msgid "Payer"
msgstr "Pagatore"
@ -711,33 +694,33 @@ msgstr "Data"
msgid "Amount in %(currency)s"
msgstr "Importo in %(currency)s"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s modified"
msgstr "L'addebito è stato aggiornato"
msgstr "La spesa %(name)s è stata modificata"
#, python-format
msgid "Participant %(name)s modified"
msgstr ""
msgstr "Il partecipante %(name)s è stato modificato"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s removed"
msgstr "L'utente '%(name)s' è stato rimosso"
msgstr "La spesa %(name)s è stata rimossa"
#, fuzzy, python-format
#, python-format
msgid "Participant %(name)s removed"
msgstr "L'utente '%(name)s' è stato rimosso"
msgstr "Il partecipante '%(name)s' è stato rimosso"
#, fuzzy, python-format
#, python-format
msgid "Project %(name)s changed in an unknown way"
msgstr "modificato in modo sconosciuto"
msgstr "Il progetto %(name)s è stato modificato in modo sconosciuto"
#, fuzzy, python-format
#, python-format
msgid "Bill %(name)s changed in an unknown way"
msgstr "modificato in modo sconosciuto"
msgstr "La spesa %(name)s è stata modificata in modo sconosciuto"
#, fuzzy, python-format
#, python-format
msgid "Participant %(name)s changed in an unknown way"
msgstr "modificato in modo sconosciuto"
msgstr "Il partecipante %(name)s è stato modificato in modo sconosciuto"
msgid "Nothing to list"
msgstr "Niente da elencare"
@ -809,6 +792,9 @@ msgstr "Cronologia"
msgid "Settings"
msgstr "Impostazioni"
msgid "RSS Feed"
msgstr "Feed RSS"
msgid "Other projects :"
msgstr "Altri progetti:"
@ -820,7 +806,7 @@ msgstr "Cruscotto"
#, python-format
msgid "Please retry after %(date)s."
msgstr ""
msgstr "Per favore, riprova dopo %(date)s."
msgid "Code"
msgstr "Codice"
@ -831,12 +817,11 @@ msgstr "Applicazione mobile"
msgid "Documentation"
msgstr "Documentazione"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Cruscotto Amministrazione"
#, fuzzy
msgid "Legal information"
msgstr "Cancella Conferma"
msgstr "Informazioni legali"
msgid "\"I hate money\" is free software"
msgstr "\"I hate money\" è un software libero"
@ -886,14 +871,11 @@ msgstr "Nessun addebito"
msgid "Nothing to list yet."
msgstr "Ancora niente da mostrare."
msgid "You probably want to"
msgstr "Probabilmente vuoi"
msgid "Add your first bill"
msgstr "Aggiungi la tua prima spesa"
msgid "add a bill"
msgstr "aggiungi una spesa"
msgid "add participants"
msgstr "aggiunti partecipanti"
msgid "Add the first participant"
msgstr "Aggiungi il primo partecipante"
msgid "Password reminder"
msgstr "Promemoria password"
@ -917,46 +899,62 @@ msgstr "Reset della tua password"
msgid "Invite people to join this project"
msgstr "Invita persone a collaborare a questo progetto"
msgid "Share Identifier & code"
msgstr "Condividi Identificatore & codice"
msgid "Share an invitation link"
msgstr "Condividi un link d'invito"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"È possibile condividere l'identificativo del progetto e il codice privato"
" con qualsiasi mezzo di comunicazione."
msgid "Identifier:"
msgstr "Identificatore:"
msgid "Share the Link"
msgstr "Condividi il Link"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Puoi condividere direttamente il seguente link attraverso il tuo canale "
"preferito"
"Il modo più semplice per invitare le persone è fornire loro il seguente link "
"di invito.<br />Potranno accedere al progetto, gestire i partecipanti, "
"aggiungere/modificare/cancellare spese. Tuttavia, non avranno accesso a "
"impostazioni importanti come la modifica del codice privato o la "
"cancellazione dell'intero progetto."
msgid "Scan QR code"
msgstr ""
msgstr "Scansiona codice QR"
msgid "Use a mobile device with a compatible app."
msgstr ""
msgstr "Usa un dispositivo mobile con una app compatibile."
msgid "Send via Emails"
msgstr "Inviare via Email"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Specifica un elenco di indirizzi email (separati da virgola) a cui vuoi "
"notificare la\n"
" creazione di questo progetto di gestione budget e "
"manderemo a ciascuno di loro un messaggio per te."
"notificare la creazione di questo progetto. Manderemo loro un messaggio con "
"il link d'invito."
msgid "Share Identifier & code"
msgstr "Condividi Identificatore & codice"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
"È possibile condividere l'identificativo del progetto e il codice privato "
"con qualsiasi mezzo di comunicazione.<br />Chiunque abbia il codice privato "
"avrà accesso all'intero progetto, compresa la modifica di impostazioni quali "
"il codice privato o l'indirizzo e-mail del progetto, o addirittura la "
"cancellazione dell'intero progetto."
msgid "Identifier:"
msgstr "Identificatore:"
msgid "Private code:"
msgstr "Codice privato:"
msgid "the private code was defined when you created the project"
msgstr "il codice privato è stato definito quando hai creato il progetto"
msgid "Who pays?"
msgstr "Chi paga?"
@ -1146,3 +1144,25 @@ msgstr "Periodo"
#~ msgid "Edit the project"
#~ msgstr "Modifica il progetto"
#~ msgid "You probably want to"
#~ msgstr "Probabilmente vuoi"
#~ msgid "add participants"
#~ msgstr "aggiunti partecipanti"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "È possibile condividere l'identificativo del"
#~ " progetto e il codice privato con "
#~ "qualsiasi mezzo di comunicazione."
#~ msgid "Share the Link"
#~ msgstr "Condividi il Link"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ "Puoi condividere direttamente il seguente "
#~ "link attraverso il tuo canale preferito"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2020-11-11 16:28+0000\n"
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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,12 +27,17 @@ msgstr "無効な入力です。数字と「+ - * / 」の演算子しか入力
msgid "Project name"
msgstr "プロジェクトの名前"
#, fuzzy
msgid "Current private code"
msgstr "現在の暗証コード"
msgid "Enter existing private code to edit project"
msgstr "プロジェクトを編集するために、暗証コードを入力してください"
msgid "New private code"
msgstr "暗証コード"
msgstr "暗証コード"
msgid "Enter a new code if you want to change it"
msgstr ""
msgstr "変更するために、新しい暗証コードを入力してください"
msgid "Email"
msgstr "メールアドレス"
@ -47,7 +52,13 @@ msgid "Default Currency"
msgstr "初期設定にする通貨"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgstr "明細通貨変換のため、デフォルトの通貨を設定してください"
msgid "Unknown error"
msgstr "不明エラー"
msgid "Invalid private code."
msgstr "無効な暗証コード。"
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
@ -85,14 +96,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "未知のプロジェクト"
#, fuzzy
msgid "Invalid private code."
msgstr "暗証コード"
msgid "Get in"
msgstr "入る"
@ -172,7 +175,7 @@ msgid "This project already have this participant"
msgstr "プロジェクトはすでにこのメンバーを含めています"
msgid "People to notify"
msgstr ""
msgstr "通知したい人"
msgid "Send the invitations"
msgstr "招待状を送る"
@ -187,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}"
@ -260,9 +264,12 @@ msgstr "未知のプロジェクト"
msgid "Password successfully reset."
msgstr "パスワードを再設定できました。"
msgid "Unable to parse CSV"
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr "CSVを読み込むことができません"
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
@ -282,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"
@ -343,9 +350,8 @@ msgstr ""
msgid "Error deleting project history"
msgstr "プロジェクトの歴史を有効にする"
#, fuzzy
msgid "Deleted project history."
msgstr "プロジェクトの歴史を有効にする"
msgstr "プロジェクトの歴史を削除しました。"
#, fuzzy
msgid "Error deleting recorded IP addresses"
@ -564,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 "
@ -612,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"
@ -631,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 "プロジェクトの設定が修正された"
@ -671,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"
@ -687,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 "表示できるものがない"
@ -767,6 +772,9 @@ msgstr "歴史"
msgid "Settings"
msgstr "設定"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "他のプロジェクト:"
@ -789,7 +797,7 @@ msgstr "携帯アプリ"
msgid "Documentation"
msgstr "書類"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "管理ダッシュボード"
#, fuzzy
@ -844,13 +852,12 @@ msgstr "明細なし"
msgid "Nothing to list yet."
msgstr "表示できるものはありません。"
msgid "You probably want to"
msgstr "…したいかもしれない"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "明細を追加する"
msgid "add participants"
#, fuzzy
msgid "Add the first participant"
msgstr "参加者を追加する"
msgid "Password reminder"
@ -873,22 +880,15 @@ msgstr "パスワードを再設定する"
msgid "Invite people to join this project"
msgstr "他人をこのプロジェクトに招待する"
msgid "Share Identifier & code"
msgstr "名前とコードを共有する"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr "プロジェクト名と私用コードは何の方法でも共有できます。"
msgid "Identifier:"
msgstr "名前:"
msgid "Share the Link"
msgstr "リンクを共有する"
msgid "You can directly share the following link via your prefered medium"
msgstr "好きの手段で以下のリンクを直接に共有できる"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
msgstr ""
@ -900,13 +900,30 @@ msgid "Send via Emails"
msgstr "メールで送る"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 "知らせたいメールアドレスのリスト(カンマ区切り)を指定してください。メールで"
"招待リンクを送信します。"
msgid "Share Identifier & code"
msgstr "名前とコードを共有する"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "名前:"
msgid "Private code:"
msgstr "暗証コード:"
msgid "the private code was defined when you created the project"
msgstr ""
"…を知らせたいメールアドレスのリストを特定する(カンマ区切り)\n"
"彼らにこの予算管理プロジェクトの作成をメールでお知らせします。"
msgid "Who pays?"
msgstr "誰が支払った?"
@ -1067,3 +1084,20 @@ msgstr "期間"
#~ msgid "Edit the project"
#~ msgstr "プロジェクトを編集する"
#~ msgid "You probably want to"
#~ msgstr "…したいかもしれない"
#~ msgid "add participants"
#~ msgstr "参加者を追加する"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr "プロジェクト名と私用コードは何の方法でも共有できます。"
#~ msgid "Share the Link"
#~ msgstr "リンクを共有する"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "好きの手段で以下のリンクを直接に共有できる"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2021-10-17 04:56+0000\n"
"Last-Translator: a-g-rao <athrigrao@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-09-16 10:59+0000\n"
"Last-Translator: Baptiste <weblate@bitsofnetworks.org>\n"
"Language-Team: Kannada <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/kn/>\n"
"Language: kn\n"
"Language-Team: Kannada <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/kn/>\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.0.2\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -31,11 +31,17 @@ msgstr ""
msgid "Project name"
msgstr "ಯೋಜನೆಯ ಹೆಸರು"
msgid "Current private code"
msgstr "ಪ್ರಸ್ತುತ ಸಂಕೇತಪದ"
msgid "Enter existing private code to edit project"
msgstr "ಯೋಜನೆಯನ್ನು ಪರಿಷ್ಕರಿಸಲು ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ"
msgid "New private code"
msgstr "ಹೊಸ ಸಂಕೇತಪದ"
msgid "Enter a new code if you want to change it"
msgstr "ಸಂಕೇತಪದ ಬದಲಾಯಿಸಬೇಕೆಂದರೆ ನಮೂದಿಸಿ."
msgstr "ಸಂಕೇತಪದ ಬದಲಾಯಿಸಬೇಕೆಂದರೆ ನಮೂದಿಸಿ"
msgid "Email"
msgstr "ಮಿನ್ನಂಚೆ"
@ -47,21 +53,31 @@ msgid "Use IP tracking for project history"
msgstr ""
msgid "Default Currency"
msgstr ""
msgstr "ಪುರ್ವನಿಯೋಜಿತ ಕರನ್ಸಿ"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
"ಪುರ್ವನಿಯೋಜಿತ ಕರನ್ಸಿಯನ್ನು ನಮೋದಿಸುವುದರಿಂದ ರಶೀದಿಗಳ ನಡುವೆ ಕರನ್ಸಿ ಪರಿವರ್ತನೆಯನ್ನು "
"ಸಕ್ರಿಯಗೊಳಿಸುತ್ತದೆ"
msgid "Unknown error"
msgstr "ಅಜ್ಞಾತ ದೋಷ"
msgid "Invalid private code."
msgstr "ನಮೂದಿಸಿರುವ ಸಂಕೇತಪದ ಅಮಾನ್ಯವಾಗಿದೆ."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
msgstr ""
"ಅನೇಕ ಕರೆನ್ಸಿಯ ರಶೀದಿ ಇರುವುದರಿಂದ ಈ ಯೋಜನೆಯನ್ನು \"ಯಾವುದೆ ಕರೆನ್ಸಿ ಇಲ್ಲ\" ಯಂದು "
"ನಮೋದಿಸಲು ಆಗುವುದಿಲ್ಲ."
msgid "Compatible with Cospend"
msgstr ""
msgstr "ಖರ್ಚನ್ನು ಜೊತೆಗಾರನೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಬಹುದು"
msgid "Project identifier"
msgstr ""
msgstr "ಯೋಜನೆ ಸಂಕೇತ"
msgid "Private code"
msgstr "ಸಂಕೇತಪದ"
@ -74,25 +90,21 @@ msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
"ಈ ಸಂಕೇತದ (\"%(project)s\") ಇನೊಂದು ಯೋಜನೆ ಆಗಲೆ ಅಸ್ತಿತ್ವದಲ್ಲಿದೆ. ದಯವಿಟ್ಟು ಹೊಸ "
"ಸಂಕೇತವನ್ನು ಆರಿಸಿ"
msgid "Which is a real currency: Euro or Petro dollar?"
msgstr ""
msgstr "ನಿಜವಾದ ಕರೆನ್ಸಿ ಯಾವುದು: ಯುರೋ ಅಥವಾ ಪೆಟ್ರೋ ಡಾಲರ್?"
msgid "euro"
msgstr "ಯೂರೋ"
msgid "Please, validate the captcha to proceed."
msgstr ""
msgstr "ಮುಂದುವರೆಯಲು ದಯವಿಟ್ಟು, ಕ್ಯಾಪ್ಚಾವನ್ನು ಮೌಲ್ಯೀಕರಿಸಿ."
msgid "Enter private code to confirm deletion"
msgstr "ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ"
msgid "Unknown error"
msgstr "ಅಜ್ಞಾತ ದೋಷ"
msgid "Invalid private code."
msgstr "ನಮೂದಿಸಿರುವ ಸಂಕೇತಪದ ಅಮಾನ್ಯವಾಗಿದೆ."
msgid "Get in"
msgstr "ಒಳಹೊಗು"
@ -118,25 +130,26 @@ msgid "Reset password"
msgstr "ಪ್ರವೇಶಪದ ಮರುಹೊಂದಿಸಿ"
msgid "When?"
msgstr ""
msgstr "ಯಾವಾಗ?"
msgid "What?"
msgstr "ಏನು?"
msgid "Who paid?"
msgstr ""
msgstr "ಯಾರು ಪಾವತಿಸಿದ್ದಾರೆ?"
msgid "How much?"
msgstr ""
msgstr "ಎಷ್ಟು?"
msgid "Currency"
msgstr "ನಾಣ್ಯಪದ್ಧತಿ"
#, fuzzy
msgid "External link"
msgstr ""
msgstr "ಬಾಹ್ಯ ಲಿಂಕ್"
msgid "A link to an external document, related to this bill"
msgstr ""
msgstr "ಈ ರಶೀದಿಗೆ ಸಂಬಂಧಿಸಿದ ಬಾಹ್ಯ ಕಡತದ ಲಿಂಕ್"
msgid "For whom?"
msgstr "ಯಾರಿಂದ?"
@ -149,13 +162,13 @@ msgstr "ಕೋರಿಕೆ ಸಲ್ಲಿಸಿ ಹೊಸದನ್ನುಸೇ
#, python-format
msgid "Project default: %(currency)s"
msgstr ""
msgstr "ಯೋಜನೆಯ ಪೂರ್ವನಿಯೋಜಿತ ಕರೆನ್ಸಿ: %(currency)s"
msgid "Name"
msgstr "ಹೆಸರು"
msgid "Weights should be positive"
msgstr ""
msgstr "ತೂಕವು ಧನಾತ್ಮಕವಾಗಿರಬೇಕು"
msgid "Weight"
msgstr "ತೂಕ"
@ -164,33 +177,33 @@ msgid "Add"
msgstr "ಕೂಡು"
msgid "The participant name is invalid"
msgstr ""
msgstr "ಸದಸ್ಯರ ಹೆಸರು ಅಮಾನ್ಯವಾಗಿದೆ"
#, fuzzy
msgid "This project already have this participant"
msgstr "ಈ ಸದಸ್ಯರು ಈಗಾಗಲೆ ಈ ಯೋಜನೆಯ ಸದಸ್ಯರಾಗಿದ್ದಾರೆ"
msgid "People to notify"
msgstr ""
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 ""
msgstr "ದಯವಿಟ್ಟು, ಇಮೇಲ್ ಸಂರಚನೆ ಪರಿಶೀಲಿಸಿ."
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"ದಯವಿಟ್ಟು, ಇಮೇಲ್ ಸಂರಚನೆ ಪರಿಶೀಲಿಸಿ ಅಥವಾ ನಿರ್ವಾಹಕನನ್ನು ಸಂಪರ್ಕಿಸಿ:%(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -209,7 +222,7 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}"
msgid "No Currency"
msgstr ""
msgstr "ಯಾವುದೆ ಕರೆನ್ಸಿ ಇಲ್ಲ"
#. Form error with only one error
msgid "{prefix}: {error}"
@ -259,6 +272,9 @@ msgstr "ಗೊತ್ತಿಲ್ಲದ ಯೋಜನೆ"
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -752,6 +768,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -774,7 +793,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -828,13 +847,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -857,21 +873,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -884,10 +893,28 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Private code:"
msgstr "ಸಂಕೇತಪದ:"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -1027,3 +1054,42 @@ msgstr ""
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2021-07-18 12:32+0000\n"
"Last-Translator: Kemystra <izzmin97@gmail.com>\n"
"Language: ms\n"
@ -27,6 +27,13 @@ msgstr "Nilai atau ungkapan yang sah. Hanya nombor dan operasi + - * / diterima.
msgid "Project name"
msgstr "Nama projek"
#, fuzzy
msgid "Current private code"
msgstr "Kod peribadi baharu"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "Kod peribadi baharu"
@ -51,6 +58,13 @@ msgstr "Mata wang asal"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "Ralat yang tidak dikenalpasti"
msgid "Invalid private code."
msgstr "Kod peribadi tidak sah."
#, fuzzy
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
@ -89,13 +103,6 @@ msgstr ""
msgid "Enter private code to confirm deletion"
msgstr "Masukkan kod peribadi untuk mengesahkan penghapusan"
#, fuzzy
msgid "Unknown error"
msgstr "Ralat yang tidak dikenalpasti"
msgid "Invalid private code."
msgstr "Kod peribadi tidak sah."
msgid "Get in"
msgstr "Masuk"
@ -266,6 +273,9 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -758,6 +768,9 @@ msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
@ -780,7 +793,7 @@ msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr ""
msgid "Legal information"
@ -834,13 +847,10 @@ msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgid "Add your first bill"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
@ -863,21 +873,14 @@ msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
@ -890,10 +893,29 @@ msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
#, fuzzy
msgid "Private code:"
msgstr "Kod peribadi"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
@ -1033,3 +1055,43 @@ msgstr ""
#~ msgid "Edit the project"
#~ msgstr ""
#~ msgid "You probably want to"
#~ msgstr ""
#~ msgid "add a bill"
#~ msgstr ""
#~ msgid "add participants"
#~ msgstr ""
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ msgid "Share the Link"
#~ msgstr ""
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email for you."
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ " creation of this budget "
#~ "management project and we will send "
#~ "them an email with the invitation "
#~ "link."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2022-02-13 16:54+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
"Language: nb_NO\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name"
msgstr "Prosjektnavn"
#, fuzzy
msgid "Current private code"
msgstr "Ny privat kode"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code"
msgstr "Ny privat kode"
@ -50,6 +57,12 @@ msgstr "Forvalgt valuta"
msgid "Setting a default currency enables currency conversion between bills"
msgstr "En forvalg valutainnstilling skrur på konvertering mellom regninger"
msgid "Unknown error"
msgstr "Ukjent feil"
msgid "Invalid private code."
msgstr "Ugyldig privat kode"
#, fuzzy
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
@ -91,12 +104,6 @@ msgstr "Bekreft CAPTCHA-en for å fortsette."
msgid "Enter private code to confirm deletion"
msgstr "Skriv inn privat kode for å bekrefte sletting"
msgid "Unknown error"
msgstr "Ukjent feil"
msgid "Invalid private code."
msgstr "Ugyldig privat kode"
#, fuzzy
msgid "Get in"
msgstr "Til prosjektet"
@ -275,6 +282,9 @@ msgstr "Ukjent prosjekt"
msgid "Password successfully reset."
msgstr "Passord tilbakestilt."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
@ -819,6 +829,9 @@ msgstr "Historikk"
msgid "Settings"
msgstr "Innstillinger"
msgid "RSS Feed"
msgstr ""
#, fuzzy
msgid "Other projects :"
msgstr "Andre prosjekter:"
@ -842,7 +855,7 @@ msgstr "Mobilprogram"
msgid "Documentation"
msgstr "Dokumentasjon"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administrasjonsoversiktspanel"
#, fuzzy
@ -899,14 +912,13 @@ msgstr "Ingen regninger"
msgid "Nothing to list yet."
msgstr "Ingenting å liste opp enda."
msgid "You probably want to"
msgstr "Du ønsker antagelig å"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "legge til en regning"
msgid "add participants"
msgstr "legge til deltagere"
#, fuzzy
msgid "Add the first participant"
msgstr "Legg til deltager"
msgid "Password reminder"
msgstr "Passordpåminner"
@ -930,26 +942,15 @@ msgstr "Tilbakestill passordet ditt"
msgid "Invite people to join this project"
msgstr "Inviter folk til å ta del i dette prosjektet"
#, fuzzy
msgid "Share Identifier & code"
msgstr "Del identifikator og kode"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Du kan dele prosjektidentifikatoren og den private koden slik det måtte "
"passe deg."
msgid "Identifier:"
msgstr "Identifikator:"
#, fuzzy
msgid "Share the Link"
msgstr "Del lenken"
msgid "You can directly share the following link via your prefered medium"
msgstr "Du kan dele denne lenken slik du ønsker"
msgid "Scan QR code"
msgstr ""
@ -963,15 +964,35 @@ msgstr "Send via e-poster"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Angi en (kommainndelt) liste over e-postadresser du ønsker å varsle om\n"
"opprettelsen av dette budsjetthåndteringsprosjektet, og de vil få en "
"e-post om det."
#, fuzzy
msgid "Share Identifier & code"
msgstr "Del identifikator og kode"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "Identifikator:"
#, fuzzy
msgid "Private code:"
msgstr "Privat kode"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Hvem betaler?"
@ -1297,3 +1318,24 @@ msgstr "Periode"
#~ msgid "Edit the project"
#~ msgstr "Rediger prosjektet"
#~ msgid "You probably want to"
#~ msgstr "Du ønsker antagelig å"
#~ msgid "add participants"
#~ msgstr "legge til deltagere"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Du kan dele prosjektidentifikatoren og "
#~ "den private koden slik det måtte "
#~ "passe deg."
#~ msgid "Share the Link"
#~ msgstr "Del lenken"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Du kan dele denne lenken slik du ønsker"

View file

@ -1,18 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2021-02-17 02:50+0000\n"
"Last-Translator: Sander Kooijmans <weblate@gogognome.nl>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\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,12 +31,17 @@ msgstr ""
msgid "Project name"
msgstr "Projectnaam"
#, fuzzy
msgid "Current private code"
msgstr "Huidige Privécode"
msgid "Enter existing private code to edit project"
msgstr "Voer het huidige privécode in om dit project te bewerken"
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"
@ -52,14 +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"
msgid "Unknown error"
msgstr "Onbekende fout"
msgid "Invalid private 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"
@ -77,25 +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 ""
#, fuzzy
msgid "Unknown error"
msgstr "Onbekend project"
#, fuzzy
msgid "Invalid private code."
msgstr "Privécode"
msgstr "Voer de privécode in om de verwijder opdracht te bevestigen"
msgid "Get in"
msgstr "Inloggen"
@ -119,7 +125,7 @@ msgid "Password confirmation"
msgstr "Wachtwoord bevestigen"
msgid "Reset password"
msgstr "Wachtwoord herstellen"
msgstr "Wachtwoord opnieuw instellen"
msgid "When?"
msgstr "Wanneer?"
@ -167,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"
@ -189,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."
"Controleer de e-mailinstellingen van de server of neem contact op met de "
"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"
@ -251,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"
@ -272,12 +272,15 @@ msgstr "Onbekend project"
msgid "Password successfully reset."
msgstr "Wachtwoord is hersteld."
msgid "Unable to parse CSV"
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
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 "
@ -294,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"
@ -344,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"
@ -383,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"
@ -402,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"
@ -421,9 +422,8 @@ msgstr "Acties"
msgid "edit"
msgstr "bewerken"
#, fuzzy
msgid "Delete project"
msgstr "Project aanpassen"
msgstr "Project verwijderen"
msgid "show"
msgstr "tonen"
@ -431,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"
@ -442,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"
@ -791,6 +789,9 @@ msgstr "Geschiedenis"
msgid "Settings"
msgstr "Instellingen"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr "Overige projecten:"
@ -813,7 +814,7 @@ msgstr "Mobiele app"
msgid "Documentation"
msgstr "Documentatie"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Administratie-overzicht"
#, fuzzy
@ -868,14 +869,13 @@ msgstr "Geen rekeningen"
msgid "Nothing to list yet."
msgstr "Nog niks."
msgid "You probably want to"
msgstr "Waarschijnlijk wil je"
msgid "add a bill"
#, fuzzy
msgid "Add your first bill"
msgstr "een rekening toevoegen"
msgid "add participants"
msgstr "deelnemers toevoegen"
#, fuzzy
msgid "Add the first participant"
msgstr "Deelnemer toevoegen"
msgid "Password reminder"
msgstr "Wachtwoordhint"
@ -899,24 +899,15 @@ msgstr "Wachtwoord herstellen"
msgid "Invite people to join this project"
msgstr "Nodig mensen uit voor dit project"
msgid "Share Identifier & code"
msgstr "Identificatie en code delen"
msgid "Share an invitation link"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Je kunt de projectidentificatie en privécode delen via welk "
"communicatieplatform dan ook."
msgid "Identifier:"
msgstr "Identificatie:"
msgid "Share the Link"
msgstr "Link delen"
msgid "You can directly share the following link via your prefered medium"
msgstr "Je kunt de volgende link direct delen"
msgid "Scan QR code"
msgstr ""
@ -927,17 +918,37 @@ msgstr ""
msgid "Send via Emails"
msgstr "Versturen via e-mail"
#, fuzzy
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Stel een (kommagescheiden) lijst op met e-mailadressen van personen die "
"je op de\n"
" hoogte wilt stellen van dit project - wij sturen hen een "
"e-mail."
msgid "Share Identifier & code"
msgstr "Identificatie en code delen"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr "Identificatie:"
#, fuzzy
msgid "Private code:"
msgstr "Privécode"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Wie betaalt?"
@ -1120,3 +1131,23 @@ msgstr "Periode"
#~ msgid "Edit the project"
#~ msgstr "Project bewerken"
#~ msgid "You probably want to"
#~ msgstr "Waarschijnlijk wil je"
#~ msgid "add participants"
#~ msgstr "deelnemers toevoegen"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Je kunt de projectidentificatie en "
#~ "privécode delen via welk communicatieplatform"
#~ " dan ook."
#~ msgid "Share the Link"
#~ msgstr "Link delen"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr "Je kunt de volgende link direct delen"

View file

@ -0,0 +1,932 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-06-15 16:06+0200\n"
"PO-Revision-Date: 2024-07-03 19:09+0000\n"
"Last-Translator: Quentin PAGÈS <quentinantonin@free.fr>\n"
"Language-Team: Occitan <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/oc/>\n"
"Language: oc\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"
#, python-format
msgid "You have just created '%(project)s' to share your expenses"
msgstr "Venètz de crear « %(project)s » per despartir vòstras despensas"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Quantitat o expression non valida. Sonque los nombre e operator +-*/ son "
"acceptats."
msgid "Project name"
msgstr "Nom del projècte"
msgid "Current private code"
msgstr "Còdi daccès actual"
msgid "Enter existing private code to edit project"
msgstr "Picatz lo còdi daccès existissent per editar lo projècte"
msgid "New private code"
msgstr "Còdi privat novèl"
msgid "Enter a new code if you want to change it"
msgstr "Picatz lo còdi novèl se lo volètz cambiar"
msgid "Email"
msgstr "Adreça electronica"
msgid "Enable project history"
msgstr "Activar listoric de projècte"
msgid "Use IP tracking for project history"
msgstr "Reculhir las adreças IP dins listoric de projècte"
msgid "Default Currency"
msgstr "Moneda predeterminada"
msgid "Setting a default currency enables currency conversion between bills"
msgstr ""
msgid "Unknown error"
msgstr "Error desconeguda"
msgid "Invalid private code."
msgstr "Còdi daccès invalid."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
msgstr ""
msgid "Compatible with Cospend"
msgstr "Compatible amb Cospend"
msgid "Project identifier"
msgstr "Identificant del projècte"
msgid "Private code"
msgstr "Còdi privat"
msgid "Create the project"
msgstr "Crear lo projècte"
#, python-format
msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
msgid "Which is a real currency: Euro or Petro dollar?"
msgstr ""
msgid "euro"
msgstr "èuro"
msgid "Please, validate the captcha to proceed."
msgstr "Se vos plai, validatz el captcha per contunhar."
msgid "Enter private code to confirm deletion"
msgstr "Picatz lo còdi daccès per confirmar la supression"
msgid "Get in"
msgstr "Dintrar"
msgid "Admin password"
msgstr "Senhal dadministracion"
msgid "Send me the code by email"
msgstr "Mandatz-me lo còdi per corrièl"
msgid "This project does not exists"
msgstr "Aqueste projècte existís pas"
msgid "Password mismatch"
msgstr ""
msgid "Password"
msgstr "Senhal"
msgid "Password confirmation"
msgstr "Confirmacion del senhla"
msgid "Reset password"
msgstr "Reïnicializar lo senhal"
msgid "When?"
msgstr "Quand ?"
msgid "What?"
msgstr "Qué ?"
msgid "Who paid?"
msgstr "Qual paguèt ?"
msgid "How much?"
msgstr "Quant ?"
msgid "Currency"
msgstr "Moneda"
msgid "External link"
msgstr "Ligam extèrne"
msgid "A link to an external document, related to this bill"
msgstr ""
msgid "For whom?"
msgstr "Per qual ?"
msgid "Submit"
msgstr "Enviar"
msgid "Submit and add a new one"
msgstr ""
#, python-format
msgid "Project default: %(currency)s"
msgstr ""
msgid "Name"
msgstr "Nom"
msgid "Weights should be positive"
msgstr ""
msgid "Weight"
msgstr "Pes"
msgid "Add"
msgstr "Apondre"
msgid "The participant name is invalid"
msgstr "Lo nom del participant es incorrècte"
msgid "This project already have this participant"
msgstr "Aqueste projècte a ja aqueste participant"
msgid "People to notify"
msgstr "Personas de convidar"
msgid "Send the invitations"
msgstr "Enviar las invitacions"
#, python-format
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Logout"
msgstr "Desconnexion"
msgid "Please check the email configuration of the server."
msgstr ""
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
msgstr ""
#. Last two items of a list with more than 3 items
msgid "{previous_object}, and {end_object}"
msgstr ""
#. Two items in a middle of a list with more than 5 objects
msgid "{previous_object}, {next_object}"
msgstr "{previous_object}, {next_object}"
#. First two items of a list with more than 3 items
msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}"
msgid "No Currency"
msgstr "Cap de moneda"
#. Form error with only one error
msgid "{prefix}: {error}"
msgstr "{prefix} : {error}"
#. Form error with a list of errors
msgid "{prefix}:<br />{errors}"
msgstr "{prefix} :<br />{errors}"
msgid "Too many failed login attempts."
msgstr ""
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
msgid "Provided token is invalid"
msgstr "Aqueste geton es invalid"
msgid "This private code is not the right one"
msgstr ""
msgid "A reminder email has just been sent to you"
msgstr ""
msgid ""
"We tried to send you an reminder email, but there was an error. You can "
"still use the project normally."
msgstr ""
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions."
msgstr ""
msgid "No token provided"
msgstr ""
msgid "Invalid token"
msgstr "Geton invalid"
msgid "Unknown project"
msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV"
msgstr ""
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr "Atribut mancant : %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
"currency"
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
msgid "Error deleting project"
msgstr ""
msgid "Unable to logout"
msgstr ""
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
msgstr ""
msgid "Your invitations have been sent"
msgstr ""
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
#, python-format
msgid "%(member)s has been added"
msgstr ""
msgid "Error activating participant"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
msgstr ""
msgid "Error removing participant"
msgstr ""
#, python-format
msgid ""
"Participant '%(name)s' has been deactivated. It will still appear in the "
"list until its balance reach zero."
msgstr ""
#, python-format
msgid "Participant '%(name)s' has been removed"
msgstr ""
#, python-format
msgid "Participant '%(name)s' has been modified"
msgstr ""
msgid "The bill has been added"
msgstr ""
msgid "Error deleting bill"
msgstr ""
msgid "The bill has been deleted"
msgstr ""
msgid "The bill has been modified"
msgstr ""
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgid "Error deleting project history"
msgstr ""
msgid "Deleted project history."
msgstr ""
msgid "Error deleting recorded IP addresses"
msgstr ""
msgid "Deleted recorded IP addresses in project history."
msgstr ""
msgid "Sorry, we were unable to find the page you've asked for."
msgstr ""
msgid "The best thing to do is probably to get back to the main page."
msgstr ""
msgid "Back to the list"
msgstr ""
msgid "Administration tasks are currently disabled."
msgstr ""
msgid "Authentication"
msgstr ""
msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
msgid "create it"
msgstr ""
msgid "?"
msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr "Projècte"
msgid "Number of participants"
msgstr ""
msgid "Number of bills"
msgstr ""
msgid "Newest bill"
msgstr ""
msgid "Oldest bill"
msgstr ""
msgid "Actions"
msgstr ""
msgid "edit"
msgstr "modifica"
msgid "Delete project"
msgstr ""
msgid "show"
msgstr "mostrar"
msgid "The Dashboard is currently deactivated."
msgstr ""
msgid "Download Mobile Application"
msgstr ""
msgid "Get it on"
msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import project"
msgstr ""
msgid "Download project's data"
msgstr ""
msgid "Bill items"
msgstr ""
msgid "Download the list of bills with owner, amount, reason,... "
msgstr ""
msgid "Settle plans"
msgstr ""
msgid "Download the list of transactions needed to settle the current bills."
msgstr ""
msgid "Can't remember the password?"
msgstr ""
msgid "Cancel"
msgstr "Anullar"
msgid "Privacy Settings"
msgstr "Paramètres de confidencialitat"
msgid "Save changes"
msgstr "Enregistrar las modificacions"
msgid "This will remove all bills and participants in this project!"
msgstr ""
msgid "Import previously exported project"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Edit this bill"
msgstr ""
msgid "Add a bill"
msgstr ""
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgid "Everyone"
msgstr "Tot lo monde"
msgid "No one"
msgstr "Degun"
msgid "More options"
msgstr "Mai d'opcions"
msgid "Add participant"
msgstr ""
msgid "Edit this participant"
msgstr ""
msgid "john.doe@example.com, mary.moe@site.com"
msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
#, python-format
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s"
msgstr ""
#, python-format
msgid "Bill %(name)s: %(property_name)s changed to %(after)s"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Confirm deletion"
msgstr "Confirmar la supression"
msgid "Close"
msgstr "Tampar"
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
#, python-format
msgid "Bill %(name)s: added %(owers_list_str)s to owers list"
msgstr ""
#, python-format
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
msgstr ""
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
msgid "You can enable history on the settings page."
msgstr ""
msgid ""
"The table below reflects actions recorded prior to disabling project "
"history."
msgstr ""
msgid "You can clear the project history to remove them."
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
#, python-format
msgid "Project %(name)s added"
msgstr ""
#, python-format
msgid "Bill %(name)s added"
msgstr ""
#, python-format
msgid "Participant %(name)s added"
msgstr ""
msgid "Project private code changed"
msgstr ""
#, python-format
msgid "Project renamed to %(new_project_name)s"
msgstr ""
#, python-format
msgid "Project contact email changed to %(new_email)s"
msgstr ""
msgid "Project settings modified"
msgstr ""
#, python-format
msgid "Participant %(name)s deactivated"
msgstr ""
#, python-format
msgid "Participant %(name)s reactivated"
msgstr ""
#, python-format
msgid "Participant %(name)s renamed to %(new_name)s"
msgstr ""
#, python-format
msgid "Bill %(name)s renamed to %(new_description)s"
msgstr ""
#, python-format
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s"
msgstr ""
msgid "Payer"
msgstr "Pagaire"
msgid "Amount"
msgstr "Soma"
msgid "Date"
msgstr "Data"
#, python-format
msgid "Amount in %(currency)s"
msgstr ""
#, python-format
msgid "Bill %(name)s modified"
msgstr ""
#, python-format
msgid "Participant %(name)s modified"
msgstr ""
#, python-format
msgid "Bill %(name)s removed"
msgstr ""
#, python-format
msgid "Participant %(name)s removed"
msgstr ""
#, python-format
msgid "Project %(name)s changed in an unknown way"
msgstr ""
#, python-format
msgid "Bill %(name)s changed in an unknown way"
msgstr ""
#, python-format
msgid "Participant %(name)s changed in an unknown way"
msgstr ""
msgid "Nothing to list"
msgstr "Pas res a listar"
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
msgid "Try out the demo"
msgstr ""
msgid "You're sharing a house?"
msgstr ""
msgid "Going on holidays with friends?"
msgstr ""
msgid "Simply sharing money with others?"
msgstr ""
msgid "We can help!"
msgstr ""
msgid "Log in to an existing project"
msgstr ""
msgid "Log in"
msgstr "Connexion"
msgid "can't remember your password?"
msgstr ""
msgid "Create"
msgstr "Crear"
msgid ""
"Don\\'t reuse a personal password. Choose a private code and send it to "
"your friends"
msgstr ""
msgid "Account manager"
msgstr "Gestionari de compte"
msgid "Bills"
msgstr "Facturas"
msgid "Settle"
msgstr ""
msgid "Statistics"
msgstr "Estatisticas"
msgid "Languages"
msgstr "Lengas"
msgid "Projects"
msgstr "Projèctes"
msgid "Start a new project"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :"
msgstr ""
msgid "switch to"
msgstr ""
msgid "Dashboard"
msgstr "Panèl dadministracion"
#, python-format
msgid "Please retry after %(date)s."
msgstr ""
msgid "Code"
msgstr "Còdi"
msgid "Mobile Application"
msgstr ""
msgid "Documentation"
msgstr "Documentacion"
msgid "Administration Dashboard"
msgstr "Panèl dadministracion"
msgid "Legal information"
msgstr ""
msgid "\"I hate money\" is free software"
msgstr ""
msgid "you can contribute and improve it!"
msgstr ""
#, python-format
msgid "%(amount)s each"
msgstr "%(amount)s cadun"
msgid "you sure?"
msgstr "O volètz vertadièrament?"
msgid "Invite people"
msgstr "Convidar de monde"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "You should start by adding participants"
msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "For what?"
msgstr "Per qué ?"
#, python-format
msgid "Added on %(date)s"
msgstr ""
#, python-format
msgid "Everyone but %(excluded)s"
msgstr ""
msgid "delete"
msgstr "suprimir"
msgid "No bills"
msgstr "Cap de factura"
msgid "Nothing to list yet."
msgstr "Pas res a listar pel moment."
msgid "Add your first bill"
msgstr ""
msgid "Add the first participant"
msgstr ""
msgid "Password reminder"
msgstr ""
msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
msgid "Return to home page"
msgstr ""
msgid "Your projects"
msgstr ""
msgid "Reset your password"
msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share an invitation link"
msgstr ""
msgid ""
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
msgid "Scan QR code"
msgstr ""
msgid "Use a mobile device with a compatible app."
msgstr ""
msgid "Send via Emails"
msgstr ""
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 ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Private code:"
msgstr "Còdi privat :"
msgid "the private code was defined when you created the project"
msgstr ""
msgid "Who pays?"
msgstr "Qual paga ?"
msgid "To whom?"
msgstr "A qual ?"
msgid "Who?"
msgstr "Qual ?"
msgid "Balance"
msgstr ""
msgid "deactivate"
msgstr ""
msgid "reactivate"
msgstr ""
msgid "Paid"
msgstr "Pagat"
msgid "Spent"
msgstr "Despensat"
msgid "Expenses by Month"
msgstr "Despensas per mes"
msgid "Period"
msgstr "Periòde"

View file

@ -1,19 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n"
"PO-Revision-Date: 2022-09-27 14:19+0000\n"
"Last-Translator: Andrzej Ochodek <andrzej.ochodek@gmail.com>\n"
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-08-24 18:48+0000\n"
"Last-Translator: Eryk Michalak <gnu.ewm@protonmail.com>\n"
"Language-Team: Polish <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/pl/>\n"
"Language: pl\n"
"Language-Team: Polish <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/pl/>\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<10 || n%100>=20) ? 1 : 2\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.0\n"
"Generated-By: Babel 2.9.0\n"
#, python-format
@ -30,6 +30,12 @@ msgstr ""
msgid "Project name"
msgstr "Nazwa projektu"
msgid "Current private code"
msgstr "Bieżący kod prywatny"
msgid "Enter existing private code to edit project"
msgstr "Wprowadź kod prywatny aby edytować projekt"
msgid "New private code"
msgstr "Nowy kod prywatny"
@ -53,6 +59,12 @@ msgstr ""
"Wybranie domyślnej waluty pozwala na konwersję walutową pomiędzy "
"rachunkami"
msgid "Unknown error"
msgstr "Nieznany błąd"
msgid "Invalid private code."
msgstr "Nieprawidłowy kod prywatny."
msgid ""
"This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies."
@ -61,7 +73,7 @@ msgstr ""
" on rachunki w różnych walutach."
msgid "Compatible with Cospend"
msgstr ""
msgstr "Zgodne z Cospend"
msgid "Project identifier"
msgstr "Identyfikator projektu"
@ -92,12 +104,6 @@ msgstr "Rozwiąż captcha, by kontynuować."
msgid "Enter private code to confirm deletion"
msgstr "Wprowadź kod prywatny w celu potwierdzenia usunięcia"
msgid "Unknown error"
msgstr "Nieznany błąd"
msgid "Invalid private code."
msgstr "Nieprawidłowy kod prywatny."
msgid "Get in"
msgstr "Wejdź"
@ -188,16 +194,15 @@ msgid "Logout"
msgstr "Wyloguj"
msgid "Please check the email configuration of the server."
msgstr ""
msgstr "Prosimy sprawdzić konfigurację e-mail serwera."
#, fuzzy, python-format
#, python-format
msgid ""
"Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s"
msgstr ""
"Przepraszamy, wystąpił błąd podczas próby wysłania wiadomości e-mail z "
"zaproszeniem. Sprawdź konfigurację e-mail serwera lub skontaktuj się z "
"administratorem."
"Prosimy sprawdzić konfigurację e-mail serwera lub skontaktować się z "
"administratorem: %(admin_email)s"
#. List with two items only
msgid "{dual_object_0} and {dual_object_1}"
@ -226,9 +231,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts."
msgstr "Zbyt wiele nieudanych prób logowania, spróbuj ponownie później."
msgstr "Zbyt wiele nieudanych prób logowania."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
@ -250,14 +254,12 @@ msgstr ""
"Próbowaliśmy wysłać Ci wiadomość e-mail z przypomnieniem, ale wystąpił "
"błąd. Nadal możesz normalnie korzystać z projektu."
#, fuzzy
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions."
msgstr ""
"Przepraszamy, wystąpił błąd podczas wysyłania wiadomości e-mail z "
"instrukcjami resetowania hasła. Sprawdź konfigurację e-mail serwera lub "
"skontaktuj się z administratorem."
"instrukcjami resetowania hasła."
msgid "No token provided"
msgstr "Nie podano tokena"
@ -271,12 +273,15 @@ msgstr "Nieznany projekt"
msgid "Password successfully reset."
msgstr "Hasło zostało pomyślnie zresetowane."
msgid "Project settings have been changed successfully."
msgstr "Ustawienia projektu zostały pomyślnie zmienione."
msgid "Unable to parse CSV"
msgstr ""
msgstr "Nie udało się odczytać pliku CSV"
#, python-format
msgid "Missing attribute: %(attribute)s"
msgstr ""
msgstr "Brakujący atrybut: %(attribute)s"
msgid ""
"Cannot add bills in multiple currencies to a project without default "
@ -295,7 +300,7 @@ msgid "Error deleting project"
msgstr "Błąd podczas usuwania projektu"
msgid "Unable to logout"
msgstr ""
msgstr "Nie można się wylogować"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
@ -304,12 +309,10 @@ msgstr "Zostałeś zaproszony do podzielenia się swoimi wydatkami w %(project)s
msgid "Your invitations have been sent"
msgstr "Twoje zaproszenia zostały wysłane"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr ""
"Przepraszamy, wystąpił błąd podczas próby wysłania wiadomości e-mail z "
"zaproszeniem. Sprawdź konfigurację e-mail serwera lub skontaktuj się z "
"administratorem."
"zaproszeniem."
#, python-format
msgid "%(member)s has been added"
@ -355,7 +358,7 @@ msgstr "Rachunek został zmieniony"
#, python-format
msgid "%(lang)s is not a supported language"
msgstr ""
msgstr "%(lang)s nie jest obsługowanym językiem"
msgid "Error deleting project history"
msgstr "Błąd podczas usuwania historii projektu"
@ -437,9 +440,8 @@ msgstr "Pobierz na"
msgid "Edit project"
msgstr "Edytuj projekt"
#, fuzzy
msgid "Import project"
msgstr "Edytuj projekt"
msgstr "Importuj projekt"
msgid "Download project's data"
msgstr "Pobierz dane projektu"
@ -466,14 +468,13 @@ msgid "Privacy Settings"
msgstr "Ustawienia prywatności"
msgid "Save changes"
msgstr ""
msgstr "Zapisz zmiany"
msgid "This will remove all bills and participants in this project!"
msgstr "Spowoduje to usunięcie wszystkich rachunków i uczestników tego projektu!"
#, fuzzy
msgid "Import previously exported project"
msgstr "Zaimportuj wcześniej wyeksportowany plik JSON"
msgstr "Zaimportuj wcześniej wyeksportowany projekt"
msgid "Choose file"
msgstr "Wybierz plik"
@ -485,7 +486,7 @@ msgid "Add a bill"
msgstr "Dodaj rachunek"
msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr ""
msgstr "Dozwolone są proste operacje, np. (18+36.2)/3"
msgid "Everyone"
msgstr "Wszyscy"
@ -577,19 +578,20 @@ msgstr "Bill %(name)s: usunięto %(owers_list_str)s z listy właścicieli"
msgid "This project has history disabled. New actions won't appear below."
msgstr ""
"Ten projekt ma wyłączoną historię. Nowe działania nie pojawią się poniżej."
#, fuzzy
msgid "You can enable history on the settings page."
msgstr "Rejestrowanie adresu IP można włączyć na stronie ustawień"
msgstr "Możliwe jest włączenie historii na stronie ustawień."
msgid ""
"The table below reflects actions recorded prior to disabling project "
"history."
msgstr ""
"Poniższa tabela przedstawia działania zarejestrowane przed wyłączeniem "
"historii projektu."
#, fuzzy
msgid "You can clear the project history to remove them."
msgstr "Ktoś prawdopodobnie wyczyścił historię projektu."
msgstr "Możesz wyczyścić historię projektu aby ją usunąć."
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
@ -785,6 +787,9 @@ msgstr "Historia"
msgid "Settings"
msgstr "Ustawienia"
msgid "RSS Feed"
msgstr "Kanał RSS"
msgid "Other projects :"
msgstr "Inne projekty:"
@ -796,7 +801,7 @@ msgstr "Kokpit"
#, python-format
msgid "Please retry after %(date)s."
msgstr ""
msgstr "Prosimy spróbować ponownie po %(date)s."
msgid "Code"
msgstr "Kod"
@ -807,7 +812,7 @@ msgstr "Aplikacja mobilna"
msgid "Documentation"
msgstr "Dokumentacja"
msgid "Administation Dashboard"
msgid "Administration Dashboard"
msgstr "Kokpit administracyjny"
msgid "Legal information"
@ -861,14 +866,11 @@ msgstr "Brak rachunków"
msgid "Nothing to list yet."
msgstr "Nie ma jeszcze żadnej listy."
msgid "You probably want to"
msgstr "Prawdopodobnie chcesz"
msgid "Add your first bill"
msgstr "Dodaj swój pierwszy rachunek"
msgid "add a bill"
msgstr "dodać rachunek"
msgid "add participants"
msgstr "dodać członków"
msgid "Add the first participant"
msgstr "Dodaj pierwszego uczestnika"
msgid "Password reminder"
msgstr "Przypomnienie hasła"
@ -892,46 +894,61 @@ msgstr "Zresetuj swoje hasło"
msgid "Invite people to join this project"
msgstr "Zaproś ludzi do dołączenia do tego projektu"
msgid "Share Identifier & code"
msgstr "Udostępnij identyfikator i kod"
msgid "Share an invitation link"
msgstr "Udostępnij link z zaproszeniem"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
"The easiest way to invite people is to give them the following invitation"
" link.<br />They will be able to access the project, manage participants,"
" 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 ""
"Identyfikator projektu i kod prywatny można udostępniać dowolnymi "
"środkami komunikacji."
msgid "Identifier:"
msgstr "Identyfikator:"
msgid "Share the Link"
msgstr "Udostępnij link"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Możesz bezpośrednio udostępnić poniższy link za pośrednictwem "
"preferowanego medium"
"Najłatwiejszym sposobem zapraszania nowych osób jest podanie im poniższego "
"linku ze zaproszeniem.<br />Osoby te będą mogły uzyskać dostęp do projektu, "
"zarządzać uczestnikami, dodawać/edytować/usuwać rachunki. Nie będą jednak "
"mieli dostępu do ważnych ustawień, takich jak zmiana kodu prywatnego lub "
"usunięcie całego projektu."
msgid "Scan QR code"
msgstr ""
msgstr "Skanuj kod QR"
msgid "Use a mobile device with a compatible app."
msgstr ""
msgstr "Użyj urządzenia mobilnego ze zgodną aplikacją."
msgid "Send via Emails"
msgstr "Wyślij przez maile"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
"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 ""
"Podaj (adresy rozdzielone przecinkami) adresy email, które chcesz "
"powiadomić o \n"
" utworzeniu tego projektu zarządzania budżetem, a my "
"wyślemy Ci wiadomość email."
"Podaj listę adresów e-mail (oddzielonych przecinkami) osób, które chcesz "
"powiadomić o utworzeniu tego projektu. Wyślemy im wiadomość e-mail z linkiem "
"zaproszającym."
msgid "Share Identifier & code"
msgstr "Udostępnij identyfikator i kod"
msgid ""
"You can share the project identifier and the private code by any "
"communication means.<br />Anyone with the private code will have access "
"to the full project, including changing settings such as the private code"
" or project email address, or even deleting the whole project."
msgstr ""
"Identyfikator projektu i kod prywatny można udostępniać za pomocą dowolnych "
"środków komunikacji.<br />Każda osoba posiadająca kod prywatny będzie miała "
"dostęp do całego projektu, w tym do zmiany ustawień, takich jak kod prywatny "
"lub adres e-mail projektu, a nawet do usunięcia całego projektu."
msgid "Identifier:"
msgstr "Identyfikator:"
msgid "Private code:"
msgstr "Kod prywatny:"
msgid "the private code was defined when you created the project"
msgstr "kod prywatny został zdefiniowany podczas tworzenia projektu"
msgid "Who pays?"
msgstr "Kto płaci?"
@ -1117,3 +1134,25 @@ msgstr "Okres"
#~ msgid "Edit the project"
#~ msgstr "Edytuj projekt"
#~ msgid "You probably want to"
#~ msgstr "Prawdopodobnie chcesz"
#~ msgid "add participants"
#~ msgstr "dodać członków"
#~ msgid ""
#~ "You can share the project identifier "
#~ "and the private code by any "
#~ "communication means."
#~ msgstr ""
#~ "Identyfikator projektu i kod prywatny "
#~ "można udostępniać dowolnymi środkami "
#~ "komunikacji."
#~ msgid "Share the Link"
#~ msgstr "Udostępnij link"
#~ msgid "You can directly share the following link via your prefered medium"
#~ msgstr ""
#~ "Możesz bezpośrednio udostępnić poniższy link"
#~ " za pośrednictwem preferowanego medium"

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