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 11304 additions and 4032 deletions

View file

@ -14,9 +14,7 @@ CONTRIBUTORS
docker-compose.* docker-compose.*
Dockerfile Dockerfile
docs docs
LICENSE
Makefile Makefile
MANIFEST.in MANIFEST.in
README.md
SECURITY.md SECURITY.md
tox.ini 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: on:
push: push:
tags: ['*'] tags: ['*']
branches: [ master ] branches: [ 'main', 'stable-*' ]
pull_request: pull_request:
branches: [ master ] branches: [ 'main', 'stable-*' ]
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -18,8 +18,8 @@ jobs:
- name: Test image # Checks we are able to run the container. - name: Test image # Checks we are able to run the container.
run: docker compose -f docker-compose.test.yml run sut run: docker compose -f docker-compose.test.yml run sut
build: build_upload:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
needs: test needs: test
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
steps: 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 *.pyc
*.egg-info *.egg-info
*.mo
dist dist
.venv .venv
docs/_build/ docs/_build/
@ -16,4 +17,5 @@ ihatemoney/budget.db
.DS_Store .DS_Store
.idea .idea
.python-version .python-version
.coverage*
prof

View file

@ -1,7 +1,11 @@
version: 2 version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
python: python:
version: "3.7"
install: install:
- method: pip - method: pip
path: . 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. 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 ### 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) ## 6.0.0 (2023-07-13)

View file

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

View file

@ -26,6 +26,7 @@ ENV DEBUG="False" \
SHOW_ADMIN_EMAIL="True" \ SHOW_ADMIN_EMAIL="True" \
SQLALCHEMY_DATABASE_URI="sqlite:////database/ihatemoney.db" \ SQLALCHEMY_DATABASE_URI="sqlite:////database/ihatemoney.db" \
SQLALCHEMY_TRACK_MODIFICATIONS="False" \ SQLALCHEMY_TRACK_MODIFICATIONS="False" \
APPLICATION_ROOT="/" \
ENABLE_CAPTCHA="False" \ ENABLE_CAPTCHA="False" \
LEGAL_LINK="" 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 VIRTUALENV = uv venv
SPHINX_BUILDDIR = docs/_build
VENV := $(shell realpath $${VIRTUAL_ENV-.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 DEV_STAMP = $(VENV)/.dev_env_installed.stamp
INSTALL_STAMP = $(VENV)/.install.stamp INSTALL_STAMP = $(VENV)/.install.stamp
TEMPDIR := $(shell mktemp -d) TEMPDIR := $(shell mktemp -d)
ZOPFLIPNG := zopflipng ZOPFLIPNG := zopflipng
MAGICK_MOGRIFY := mogrify 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 .PHONY: virtualenv
virtualenv: $(PYTHON) virtualenv: $(PYTHON)
$(PYTHON): $(PYTHON):
$(VIRTUALENV) $(VENV) $(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 .PHONY: serve
serve: install ## Run the ihatemoney server serve: build-translations ## Run the ihatemoney server
@echo 'Running ihatemoney on http://localhost:5000' @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 .PHONY: test
test: install-dev ## Run the tests test:
$(VENV)/bin/tox uv run --extra dev --extra database pytest .
.PHONY: black .PHONY: lint
black: install-dev ## Run the tests lint:
$(VENV)/bin/black --target-version=py37 . uv tool run ruff check .
uv tool run vermin --no-tips --violations -t=3.8- .
.PHONY: isort .PHONY: format
isort: install-dev ## Run the tests format:
$(VENV)/bin/isort . uv tool run ruff format .
.PHONY: release .PHONY: release
release: install-dev ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release) release: # Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
$(VENV)/bin/fullrelease uv run --extra dev fullreleas
.PHONY: compress-showcase .PHONY: compress-showcase
compress-showcase: compress-showcase:
@ -72,27 +52,30 @@ compress-assets: compress-showcase ## Compress static assets
.PHONY: build-translations .PHONY: build-translations
build-translations: ## Build the 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 .PHONY: extract-translations
update-translations: ## Extract new translations from source code extract-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 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
$(VENV)/bin/pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/ uv run --extra dev pybabel update -i ihatemoney/messages.pot -d ihatemoney/translations/
.PHONY: create-database-revision .PHONY: create-database-revision
create-database-revision: ## Create a new database revision create-database-revision: ## Create a new database revision
@read -p "Please enter a message describing this revision: " rev_message; \ @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 .PHONY: create-empty-database-revision
create-empty-database-revision: ## Create an empty database revision create-empty-database-revision: ## Create an empty database revision
@read -p "Please enter a message describing this revision: " rev_message; \ @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 .PHONY: clean
clean: ## Destroy the virtual environment clean: ## Destroy the virtual environment
rm -rf .venv 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 .PHONY: help
help: ## Show the help indications 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}' @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 ## 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), - **Backends**: SQLite, PostgreSQL, MariaDB (version 10.3.2 or above),
Memory. 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 ## Contributing
Do you wish to contribute to IHateMoney? Fantastic! There's a lot of Do you wish to contribute to IHateMoney? Fantastic! There's a lot of

View file

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

View file

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

View file

@ -35,6 +35,7 @@ services:
- SHOW_ADMIN_EMAIL=True - SHOW_ADMIN_EMAIL=True
- SQLALCHEMY_DATABASE_URI=sqlite:////database/ihatemoney.db - SQLALCHEMY_DATABASE_URI=sqlite:////database/ihatemoney.db
- SQLALCHEMY_TRACK_MODIFICATIONS=False - SQLALCHEMY_TRACK_MODIFICATIONS=False
- APPLICATION_ROOT=/
- ENABLE_CAPTCHA=False - ENABLE_CAPTCHA=False
- LEGAL_LINK= - LEGAL_LINK=
- PORT=8000 - 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 $ curl --basic -u demo:demo https://ihatemoney.org/api/projects/demo/token
{"token": "WyJ0ZXN0Il0.Rt04fNMmxp9YslCRq8hB6jE9s1Q"} {"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 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 $ 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 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 ### Projects
@ -67,8 +68,8 @@ A project needs the following arguments:
- `name`: the project name (string) - `name`: the project name (string)
- `id`: the project identifier (string without special chars or - `id`: the project identifier (string without special chars or
spaces) spaces)
- `password`: the project password / secret code (string) - `password`: the project password / private code (string)
- `contact_email`: the contact email (string) - `contact_email`: the contact email, used to recover the private code (string)
Optional arguments: Optional arguments:
@ -83,7 +84,9 @@ Here is the command:
-d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org' -d 'name=yay&id=yay&password=yay&contact_email=yay@notmyidea.org'
"yay" "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 #### 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\ $ curl --basic -u yay:yay -X PUT\
https://ihatemoney.org/api/projects/yay -d\ 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 #### 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": 31, "name": "Arnaud"},
{"weight": 1, "activated": true, "id": 32, "name": "Alexis"}, {"weight": 1, "activated": true, "id": 32, "name": "Alexis"},
{"weight": 1, "activated": true, "id": 33, "name": "Olivier"}, {"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`: 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 "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, "paid": 5,
"spent": 15.5, "spent": 15.5,
"balance": -10.5 "balance": -10.5

View file

@ -1,9 +1,12 @@
import datetime
templates_path = ["_templates"] templates_path = ["_templates"]
source_suffix = ".rst" source_suffix = ".rst"
master_doc = "index" master_doc = "index"
project = "I hate money" 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" version = "5.0"
release = "5.0" release = "5.0"

View file

@ -127,11 +127,11 @@ ADMIN_PASSWORD needs to be set.
## APPLICATION_ROOT ## APPLICATION_ROOT
If empty, ihatemoney will be served at domain root (e.g: By default, ihatemoney will be served at domain root (e.g:
*http://domain.tld*), if set to `"somestring"`, it will be served from a *http://domain.tld*), if set to `"/somestring"`, it will be served from a
"folder" (e.g: *http://domain.tld/somestring*). "folder" (e.g: *http://domain.tld/somestring*).
- **Default value:** `""` (empty string) - **Default value:** `"/"`
## BABEL_DEFAULT_TIMEZONE ## BABEL_DEFAULT_TIMEZONE
@ -173,6 +173,14 @@ URL you want.
- **Default value:** `""` (empty string) - **Default value:** `""` (empty string)
- **Production value:** The URL of your chosing. - **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 ## Configuring email sending
By default, Ihatemoney sends emails using a local SMTP server, but it's 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_USERNAME** : default **None**
- **MAIL_PASSWORD** : default **None** - **MAIL_PASSWORD** : default **None**
- **DEFAULT_MAIL_SENDER** : 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 # 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 ## How to contribute
You would like to contribute? First, thanks a bunch! This project is a You would like to contribute? First, thanks a bunch! This project is a
@ -46,6 +78,15 @@ Thanks again!
(setup-dev-environment)= (setup-dev-environment)=
## Set up a 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: You must develop on top of the Git master branch:
git clone https://github.com/spiral-project/ihatemoney.git 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. Python files in this project. Be sure to run it locally on your files.
To do so, just run: To do so, just run:
make black isort make lint
You can also integrate them with your dev environment (as a You can also integrate them with your dev environment (as a
*format-on-save* hook, for instance). *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. 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 ? ## How to build the documentation ?
The documentation is using The documentation is using
@ -261,10 +315,9 @@ In order to issue a new release, follow the following steps:
make compress-assets make compress-assets
- Build the translations: - Extract the translations:
make update-translations make extract-translations
make build-translations
- If you're not completely sure of yourself at this point, you can - 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 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} ::: {note}
The above command will prompt for version number, handle 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. 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 This is probably the simplest way to get something running. Once you
have Docker installed on your system, just issue : 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>. 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 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 At the prompt, enter a password to use for the admin dashboard. The
command will print the hashed password string. 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 ACTIVATE_ADMIN_DASHBOARD=True \
-e ADMIN_PASSWORD=<hashed_password_string> \ -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` Additional gunicorn parameters can be passed using the docker `CMD`
parameter. For example, use the following command to add more gunicorn parameter. For example, use the following command to add more gunicorn
workers: workers:
docker run -d -p 8000:8000 ihatemoney/ihatemoney -w 3 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)= (cloud)=
## On a Cloud Provider ## 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: «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 (>= - **A database backend**: choose among SQLite, PostgreSQL, MariaDB (>=
10.3.2). 10.3.2).
- **Virtual environment** (recommended): [python3-venv]{.title-ref} - **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 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 malicious member and to manage changes in membership (e.g. ensuring that
a previous member can no longer access the project). But these 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! your project!
## Security model ## 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\") - **project identifier** (equivalent to a \"login\")
- **private code** (equivalent to a \"password\") - **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 - access the project through the web interface or the API
- add, modify or remove participants
- add, modify or remove bills - add, modify or remove bills
- view statistics of the project
- view project history - view project history
- change basic settings of the project - change basic settings of the project
- change the email address associated to the project - change the email address associated to the project
- change the private code of 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 Somebody with the **auth token** can manipulate the project through the API:
essentially the same thing:
- access the project - access the project
- add, modify or remove participants
- add, modify or remove bills - add, modify or remove bills
- change basic settings of the project - view statistics of the project
- change the email address associated to the project - delete the project
- change the private code of 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, allow to login on the web interface without knowing the private code,
see below. 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 ## Giving access to a project
There are two main ways to give access to a project to a new person: 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 code. In particular, somebody that is logged-in through the invitation
link will not be able to change the private code, because the web link will not be able to change the private code, because the web
interface requires a confirmation of the existing private code to change 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 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 ## Removing access to a project
If a person should no longer be able to access a project, the only way 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 This will prevent anybody from logging in with the old private code.
won\'t work anymore, and anybody with the old token will no longer be However, anybody with an existing session cookie will still have
able to access the project through the API. 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 ## Recovering access to a project
If the private code is no longer known, the creator of the project can 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 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 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 Note, however, that somebody with the private code could have changed
the email address in the settings at any time. 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 against mistakes: a malicious member can easily remove all entries from
the history! 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 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] [python: **.py]
[jinja2: **/templates/**.html] [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. # service over plain HTTP.
SESSION_COOKIE_SECURE = True 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 # You can activate an optional CAPTCHA if you want to. It can be helpful
# to filter spammer bots. # 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 # 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. # about GDPR, or how you handle the data of your users.
# Set this variable to the URL you want. # 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 return rates
def get_currencies(self, with_no_currency=True): def get_currencies(self, with_no_currency=True):
rates = [ currencies = [
rate "AED",
for rate in self.get_rates() "AFN",
if with_no_currency or rate != self.no_currency "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) if with_no_currency:
return rates currencies.append(self.no_currency)
return currencies
def exchange_currency(self, amount, source_currency, dest_currency): def exchange_currency(self, amount, source_currency, dest_currency):
if ( if (

View file

@ -3,14 +3,19 @@ DEBUG = SQLACHEMY_ECHO = False
SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/ihatemoney.db" SQLALCHEMY_DATABASE_URI = "sqlite:////tmp/ihatemoney.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = "tralala" SECRET_KEY = "tralala"
SITE_NAME = "I Hate Money"
MAIL_DEFAULT_SENDER = "Budget manager <admin@example.com>" MAIL_DEFAULT_SENDER = "Budget manager <admin@example.com>"
SHOW_ADMIN_EMAIL = True SHOW_ADMIN_EMAIL = True
ACTIVATE_DEMO_PROJECT = True ACTIVATE_DEMO_PROJECT = True
ACTIVATE_ADMIN_DASHBOARD = False
ADMIN_PASSWORD = "" ADMIN_PASSWORD = ""
ALLOW_PUBLIC_PROJECT_CREATION = True ALLOW_PUBLIC_PROJECT_CREATION = True
ACTIVATE_ADMIN_DASHBOARD = False
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
APPLICATION_ROOT = "/"
ENABLE_CAPTCHA = False
LEGAL_LINK = ""
SUPPORTED_LANGUAGES = [ SUPPORTED_LANGUAGES = [
"az",
"ca", "ca",
"cs", "cs",
"de", "de",
@ -43,5 +48,3 @@ SUPPORTED_LANGUAGES = [
"uk", "uk",
"zh_Hans", "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.file import FileAllowed, FileField, FileRequired
from flask_wtf.form import FlaskForm from flask_wtf.form import FlaskForm
from markupsafe import Markup 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 ( from wtforms.fields import (
BooleanField, BooleanField,
DateField, DateField,
@ -39,10 +39,11 @@ from wtforms.validators import (
) )
from ihatemoney.currency_convertor import CurrencyConverter 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 ( from ihatemoney.utils import (
em_surround, em_surround,
eval_arithmetic_expression, eval_arithmetic_expression,
generate_password_hash,
render_localized_currency, render_localized_currency,
slugify, slugify,
) )
@ -66,6 +67,9 @@ def get_billform_for(project, set_default=True, **kwargs):
if form.original_currency.data is None: if form.original_currency.data is None:
form.original_currency.data = project.default_currency 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 show_no_currency = form.original_currency.data == CurrencyConverter.no_currency
form.original_currency.choices = [ form.original_currency.choices = [
@ -86,7 +90,6 @@ def get_billform_for(project, set_default=True, **kwargs):
class CommaDecimalField(DecimalField): class CommaDecimalField(DecimalField):
"""A class to deal with comma in Decimal Field""" """A class to deal with comma in Decimal Field"""
def process_formdata(self, value): def process_formdata(self, value):
@ -121,6 +124,11 @@ class CalculatorStringField(StringField):
class EditProjectForm(FlaskForm): class EditProjectForm(FlaskForm):
name = StringField(_("Project name"), validators=[DataRequired()]) 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 # If empty -> don't change the password
password = PasswordField( password = PasswordField(
_("New private code"), _("New private code"),
@ -154,6 +162,13 @@ class EditProjectForm(FlaskForm):
for currency_name in self.currency_helper.get_currencies() 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 @property
def logging_preference(self): def logging_preference(self):
"""Get the LoggingMode object corresponding to current form data.""" """Get the LoggingMode object corresponding to current form data."""
@ -172,12 +187,20 @@ class EditProjectForm(FlaskForm):
and field.data == CurrencyConverter.no_currency and field.data == CurrencyConverter.no_currency
and project.has_multiple_currencies() and project.has_multiple_currencies()
): ):
raise ValidationError( msg = _(
_(
"This project cannot be set to 'no currency'" "This project cannot be set to 'no currency'"
" because it contains bills in multiple currencies." " 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): def update(self, project):
"""Update the project with the information from the form""" """Update the project with the information from the form"""
@ -212,7 +235,9 @@ class ImportProjectForm(FlaskForm):
class ProjectForm(EditProjectForm): class ProjectForm(EditProjectForm):
id = StringField(_("Project identifier"), validators=[DataRequired()]) 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()]) password = PasswordField(_("Private code"), validators=[DataRequired()])
submit = SubmitField(_("Create the project")) submit = SubmitField(_("Create the project"))
@ -338,6 +363,12 @@ class BillForm(FlaskForm):
payed_for = SelectMultipleField( payed_for = SelectMultipleField(
_("For whom?"), validators=[DataRequired()], coerce=int _("For whom?"), validators=[DataRequired()], coerce=int
) )
bill_type = SelectField(
_("Bill Type"),
choices=BillType.choices(),
coerce=BillType,
default=BillType.EXPENSE,
)
submit = SubmitField(_("Submit")) submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one")) submit2 = SubmitField(_("Submit and add a new one"))
@ -351,12 +382,14 @@ class BillForm(FlaskForm):
payer_id=self.payer.data, payer_id=self.payer.data,
project_default_currency=project.default_currency, project_default_currency=project.default_currency,
what=self.what.data, what=self.what.data,
bill_type=self.bill_type.data,
) )
def save(self, bill, project): def save(self, bill, project):
bill.payer_id = self.payer.data bill.payer_id = self.payer.data
bill.amount = self.amount.data bill.amount = self.amount.data
bill.what = self.what.data bill.what = self.what.data
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data bill.external_link = self.external_link.data
bill.date = self.date.data bill.date = self.date.data
bill.owers = Person.query.get_by_ids(self.payed_for.data, project) 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.payer.data = bill.payer_id
self.amount.data = bill.amount self.amount.data = bill.amount
self.what.data = bill.what self.what.data = bill.what
self.bill_type.data = bill.bill_type
self.external_link.data = bill.external_link self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency self.original_currency.data = bill.original_currency
self.date.data = bill.date self.date.data = bill.date
@ -391,6 +425,17 @@ class BillForm(FlaskForm):
# See https://github.com/python-babel/babel/issues/821 # See https://github.com/python-babel/babel/issues/821
raise ValidationError(f"Result is too high: {field.data}") 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): class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter]) name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
@ -412,7 +457,6 @@ class MemberForm(FlaskForm):
and Person.query.filter( and Person.query.filter(
Person.name == field.data, Person.name == field.data,
Person.project == self.project, Person.project == self.project,
Person.activated,
).all() ).all()
): # NOQA ): # NOQA
raise ValidationError(_("This project already have this participant")) raise ValidationError(_("This project already have this participant"))

View file

@ -38,6 +38,9 @@ def history_sort_key(history_item_dict):
def describe_version(version_obj): def describe_version(version_obj):
"""Use the base model str() function to describe a version object""" """Use the base model str() function to describe a version object"""
if not version_obj:
return ""
else:
return parent_class(type(version_obj)).__str__(version_obj) return parent_class(type(version_obj)).__str__(version_obj)
@ -83,10 +86,26 @@ def get_history(project, human_readable_names=True):
"time": version.transaction.issued_at, "time": version.transaction.issued_at,
"operation_type": version.operation_type, "operation_type": version.operation_type,
"object_type": object_type, "object_type": object_type,
"bill_details": None,
"object_desc": object_str, "object_desc": object_str,
"ip": version.transaction.remote_addr, "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: if version.operation_type == Operation.UPDATE:
# Only iterate the changeset if the previous version # Only iterate the changeset if the previous version
# Was logged # Was logged

View file

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

View file

@ -10,6 +10,12 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "" msgstr ""
msgid "Current private code"
msgstr ""
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code" msgid "New private code"
msgstr "" msgstr ""
@ -31,6 +37,12 @@ msgstr ""
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid private code."
msgstr ""
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -66,12 +78,6 @@ msgstr ""
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr ""
msgid "Invalid private code."
msgstr ""
msgid "Get in" msgid "Get in"
msgstr "" msgstr ""
@ -235,6 +241,9 @@ msgstr ""
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -725,6 +734,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -747,7 +759,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -801,13 +813,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -830,21 +839,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -857,10 +859,28 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" 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 from collections import defaultdict
import datetime import datetime
from enum import Enum
import itertools import itertools
from dateutil.parser import parse from dateutil.parser import parse
@ -18,11 +19,10 @@ from sqlalchemy import orm
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy_continuum import make_versioned, version_class from sqlalchemy_continuum import make_versioned, version_class
from sqlalchemy_continuum.plugins import FlaskPlugin from sqlalchemy_continuum.plugins import FlaskPlugin
from werkzeug.security import generate_password_hash
from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory 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 ( from ihatemoney.versioning import (
ConditionalVersioningManager, ConditionalVersioningManager,
LoggingMode, 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() db = SQLAlchemy()
@ -113,22 +123,33 @@ class Project(db.Model):
- dict mapping each member to how much he/she should be paid by - dict mapping each member to how much he/she should be paid by
others (i.e. how much he/she has paid for bills) 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)) balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
for bill in self.get_bills_unordered().all(): 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) total_weight = sum(ower.weight for ower in bill.owers)
if bill.bill_type == BillType.EXPENSE:
should_receive[bill.payer.id] += bill.converted_amount
for ower in bill.owers: for ower in bill.owers:
should_pay[ower.id] += ( should_pay[ower.id] += (
ower.weight * bill.converted_amount / total_weight 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: for person in self.members:
balance = should_receive[person.id] - should_pay[person.id] balance = should_receive[person.id] - should_pay[person.id]
balances[person.id] = balance balances[person.id] = balance
return balances, should_pay, should_receive return (
balances,
should_pay,
should_receive,
)
@property @property
def balance(self): def balance(self):
@ -161,6 +182,7 @@ class Project(db.Model):
""" """
monthly = defaultdict(lambda: defaultdict(float)) monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all(): for bill in self.get_bills_unordered().all():
if bill.bill_type == BillType.EXPENSE:
monthly[bill.date.year][bill.date.month] += bill.converted_amount monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly return monthly
@ -187,7 +209,6 @@ class Project(db.Model):
) )
return pretty_transactions return pretty_transactions
# cache value for better performance
members = {person.id: person for person in self.members} members = {person.id: person for person in self.members}
settle_plan = settle(self.balance.items()) or [] settle_plan = settle(self.balance.items()) or []
@ -203,22 +224,6 @@ class Project(db.Model):
return prettify(transactions, pretty_output) 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): def has_bills(self):
"""return if the project do have bills or not""" """return if the project do have bills or not"""
return self.get_bills_unordered().count() > 0 return self.get_bills_unordered().count() > 0
@ -337,6 +342,7 @@ class Project(db.Model):
pretty_bills.append( pretty_bills.append(
{ {
"what": bill.what, "what": bill.what,
"bill_type": bill.bill_type.value,
"amount": round(bill.amount, 2), "amount": round(bill.amount, 2),
"currency": bill.original_currency, "currency": bill.original_currency,
"date": str(bill.date), "date": str(bill.date),
@ -408,6 +414,7 @@ class Project(db.Model):
new_bill = Bill( new_bill = Bill(
amount=b["amount"], amount=b["amount"],
date=parse(b["date"]), date=parse(b["date"]),
bill_type=b["bill_type"],
external_link="", external_link="",
original_currency=b["currency"], original_currency=b["currency"],
owers=Person.query.get_by_names(b["owers"], self), owers=Person.query.get_by_names(b["owers"], self),
@ -453,7 +460,8 @@ class Project(db.Model):
"""Generate a timed and serialized JsonWebToken """Generate a timed and serialized JsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed), :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": if token_type == "reset":
@ -476,9 +484,10 @@ class Project(db.Model):
:param token: Serialized TimedJsonWebToken :param token: Serialized TimedJsonWebToken
:param token_type: Either "auth" for authentication (invalidated when project code changed), :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),
:param project_id: Project ID. Used for token_type "auth" to use the password as serializer or "feed" for project feeds (invalidated when project code changed)
secret key. :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" :param max_age: Token expiration time (in seconds). Only used with token_type "reset"
""" """
loads_kwargs = {} loads_kwargs = {}
@ -536,14 +545,15 @@ class Project(db.Model):
db.session.commit() db.session.commit()
operations = ( operations = (
("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping"), ("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping", "Expense"),
("Alice", 20, ("Amina", "Alice"), "Beer !"), ("Alice", 20, ("Amina", "Alice"), "Beer !", "Expense"),
("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP"), ("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( db.session.add(
Bill( Bill(
amount=amount, amount=amount,
bill_type=bill_type,
original_currency=project.default_currency, original_currency=project.default_currency,
owers=[members[name] for name in owers], owers=[members[name] for name in owers],
payer_id=members[payer].id, payer_id=members[payer].id,
@ -676,6 +686,7 @@ class Bill(db.Model):
date = db.Column(db.Date, default=datetime.datetime.now) date = db.Column(db.Date, default=datetime.datetime.now)
creation_date = db.Column(db.Date, default=datetime.datetime.now) creation_date = db.Column(db.Date, default=datetime.datetime.now)
what = db.Column(db.UnicodeText) what = db.Column(db.UnicodeText)
bill_type = db.Column(db.Enum(BillType))
external_link = db.Column(db.UnicodeText) external_link = db.Column(db.UnicodeText)
original_currency = db.Column(db.String(3)) original_currency = db.Column(db.String(3))
@ -695,6 +706,7 @@ class Bill(db.Model):
payer_id: int = None, payer_id: int = None,
project_default_currency: str = "", project_default_currency: str = "",
what: str = "", what: str = "",
bill_type: str = "Expense",
): ):
super().__init__() super().__init__()
self.amount = amount self.amount = amount
@ -704,6 +716,7 @@ class Bill(db.Model):
self.owers = owers self.owers = owers
self.payer_id = payer_id self.payer_id = payer_id
self.what = what self.what = what
self.bill_type = BillType(bill_type)
self.converted_amount = self.currency_helper.exchange_currency( self.converted_amount = self.currency_helper.exchange_currency(
self.amount, self.original_currency, project_default_currency self.amount, self.original_currency, project_default_currency
) )
@ -718,6 +731,7 @@ class Bill(db.Model):
"date": self.date, "date": self.date,
"creation_date": self.creation_date, "creation_date": self.creation_date,
"what": self.what, "what": self.what,
"bill_type": self.bill_type.value,
"external_link": self.external_link, "external_link": self.external_link,
"original_currency": self.original_currency, "original_currency": self.original_currency,
"converted_amount": self.converted_amount, "converted_amount": self.converted_amount,

View file

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

View file

@ -424,6 +424,15 @@ footer .footer-left {
display: table-cell; 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 { .balance .balance-value {
text-align: right; text-align: right;

View file

@ -100,6 +100,7 @@
</div> </div>
{{ input(form.default_currency) }} {{ input(form.default_currency) }}
{{ input(form.current_password) }}
<div class="actions"> <div class="actions">
<button class="btn btn-primary">{{ _("Save changes") }}</button> <button class="btn btn-primary">{{ _("Save changes") }}</button>
</div> </div>
@ -178,7 +179,11 @@
<a class="badge badge-secondary" href="#" id="selectnone" onclick="selectCheckboxes(false)">{{_("No one")}}</a> <a class="badge badge-secondary" href="#" id="selectnone" onclick="selectCheckboxes(false)">{{_("No one")}}</a>
</p> </p>
<div class="d-flex flex-column flex-wrap overflow-auto" style="max-height: 20em;"> <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%;"> <p class="form-check text-break" style="max-width: 50%;">
<label for="payed_for-{{key}}" class="form-check-label"> <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}}"/> <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" %} {% if g.project.default_currency != "XXX" %}
{{ input(form.original_currency, inline=True, class="form-control custom-select") }} {{ input(form.original_currency, inline=True, class="form-control custom-select") }}
{% endif %} {% endif %}
{{ input(form.bill_type, inline=True) }}
{{ input(form.external_link, inline=True) }} {{ input(form.external_link, inline=True) }}
</details> </details>
</fieldset> </fieldset>
@ -230,7 +236,8 @@
{{ input(form.weight) }} {{ input(form.weight) }}
</fieldset> </fieldset>
<div class="actions"> <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> </div>
{% endmacro %} {% endmacro %}

View file

@ -91,6 +91,21 @@
{% endif %} {% endif %}
{% endmacro %} {% 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 %} {% block sidebar %}
<div id="table_overflow"> <div id="table_overflow">
{{ balance_table(show_weight=False, show_header=True) }} {{ balance_table(show_weight=False, show_header=True) }}
@ -177,9 +192,12 @@
{% trans %}Project {{ name }} added{% endtrans %} {% trans %}Project {{ name }} added{% endtrans %}
{% elif event.object_type == "Bill" %} {% elif event.object_type == "Bill" %}
{% trans %}Bill {{ name }} added{% endtrans %} {% trans %}Bill {{ name }} added{% endtrans %}
{{ bill_details(event.bill_details) }}
{% elif event.object_type == "Person" %} {% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} added{% endtrans %} {% trans %}Participant {{ name }} added{% endtrans %}
{% endif %} {% endif %}
{% elif event.operation_type == OperationType.UPDATE %} {% elif event.operation_type == OperationType.UPDATE %}
{% if event.object_type == "Project" %} {% if event.object_type == "Project" %}
{% if event.prop_changed == "password" %} {% if event.prop_changed == "password" %}
@ -195,24 +213,12 @@
{% else %} {% else %}
{{ _("Project settings modified") }} {{ _("Project settings modified") }}
{% endif %} {% endif %}
{% elif event.prop_changed == "activated" %} {% elif event.object_type == "Bill" %}
{% if event.val_after == False %} {% if event.prop_changed == "what" %}
{% 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" %}
{% set new_description=event.val_after|em_surround %} {% set new_description=event.val_after|em_surround %}
{% trans %}Bill {{ name }} renamed to {{ new_description }}{% endtrans %} {% 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" %} {% 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) }} {{ 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" %} {% elif event.prop_changed == "owers_added" %}
{{ owers_changed(event, True)}} {{ owers_changed(event, True)}}
{% elif event.prop_changed == "owers_removed" %} {% elif event.prop_changed == "owers_removed" %}
@ -228,18 +234,37 @@
{% elif event.prop_changed == "converted_amount" %} {% elif event.prop_changed == "converted_amount" %}
{{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }} {{ bill_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %} {% else %}
{% if event.object_type == "Bill" %}
{% trans %}Bill {{ name }} modified{% endtrans %} {% trans %}Bill {{ name }} modified{% endtrans %}
{% endif %}
{{ bill_details(event.bill_details, before=True) }}
{% elif event.object_type == "Person" %} {% 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 %} {% trans %}Participant {{ name }} modified{% endtrans %}
{% endif %} {% endif %}
{% endif %} {% endif %}
{% elif event.operation_type == OperationType.DELETE %} {% elif event.operation_type == OperationType.DELETE %}
{% if event.object_type == "Bill" %} {% if event.object_type == "Bill" %}
{% trans %}Bill {{ name }} removed{% endtrans %} {% trans %}Bill {{ name }} removed{% endtrans %}
{{ bill_details(event.bill_details) }}
{% elif event.object_type == "Person" %} {% elif event.object_type == "Person" %}
{% trans %}Participant {{ name }} removed{% endtrans %} {% trans %}Participant {{ name }} removed{% endtrans %}
{% endif %} {% endif %}
{% else %} {% else %}
{# Should be unreachable #} {# Should be unreachable #}
{% if event.object_type == "Project" %} {% if event.object_type == "Project" %}

View file

@ -20,7 +20,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html class="h-100"> <html class="h-100">
<head> <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 http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}">
@ -103,6 +103,7 @@
{% if g.project %} {% 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.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.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 %} {% endif %}
{% if session['projects'] and not ((session['projects'] | length) == 1 and g.project and g.project.id in session['projects']) %} {% 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'] %} {% if session['is_admin'] %}
<li><a class="dropdown-item" href="{{ url_for("main.dashboard") }}">{{ _("Dashboard") }}</a></li> <li><a class="dropdown-item" href="{{ url_for("main.dashboard") }}">{{ _("Dashboard") }}</a></li>
{% endif %} {% endif %}
{% if g.logout_form %}
<li> <li>
<form action="{{ url_for("main.exit") }}" method="post"> <form action="{{ url_for("main.exit") }}" method="post">
{{ g.logout_form.hidden_tag() }} {{ g.logout_form.hidden_tag() }}
{{ g.logout_form.submit(class="dropdown-item") }} {{ g.logout_form.submit(class="dropdown-item") }}
</form> </form>
</li> </li>
{% endif %}
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
@ -165,7 +168,7 @@
<i class="icon book">{{ static_include("images/book.svg") | safe }}</i> <i class="icon book">{{ static_include("images/book.svg") | safe }}</i>
</a> </a>
{% if g.show_admin_dashboard_link %} {% 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> <i class="icon admin">{{ static_include("images/cog.svg") | safe }}</i>
</a> </a>
{% endif %} {% endif %}

View file

@ -11,6 +11,11 @@
{% block js %} {% block js %}
{% if add_bill %} $('#new-bill > a').click(); {% endif %} {% 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 // ask for confirmation before removing an user
$('.action.delete').each(function(){ $('.action.delete').each(function(){
var link = $(this).find('button'); var link = $(this).find('button');
@ -44,6 +49,9 @@
{% endblock %} {% 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 %} {% block sidebar %}
<div class="sidebar_content"> <div class="sidebar_content">
@ -98,7 +106,7 @@
</ul> </ul>
{% endif %} {% 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 %}> <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> <i class="icon icon-white before-text">{{ static_include("images/plus.svg") | safe }}</i>
{{ _("Add a new bill") }} {{ _("Add a new bill") }}
</a> </a>
@ -161,13 +169,14 @@
<h3>{{ _('No bills')}}</h3> <h3>{{ _('No bills')}}</h3>
<p> <p>
{{ _("Nothing to list yet.")}}<br /> {{ _("Nothing to list yet.")}}<br />
{{ _("You probably want to") }} {%- if g.project.members %}
{%- if g.project.members %} <a href="{{ url_for('.add_bill') }}" data-toggle="modal" data-target="#bill-form"> <a href="{{ url_for('.add_bill') }}" data-toggle="modal" data-target="#bill-form">
{{- _("add a bill") -}} {{- _("Add your first bill") -}}
</a> ? </a>
{% else %} <a href="{{ url_for('.add_member') }}"> {% else %}
{{- _('add participants') -}} <a href="{{ url_for('.add_member') }}">
</a> ? {{- _("Add the first participant") -}}
</a>
{%- endif -%} {%- endif -%}
</p> </p>
</div> </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> <tbody>
<tr> <tr>
<td> <td>
<h3>{{ _('Share Identifier & code') }}</h3> <h3>{{ _('Share an invitation link') }}</h3>
</td> </td>
<td> <td>
{{ _("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.") }}</br>
<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>
<a href="{{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}"> <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()) }} {{ url_for(".join_project", _external=True, project_id=g.project.id, token=g.project.generate_token()) }}
</a> </a>
@ -40,14 +30,26 @@
<h3>{{ _('Send via Emails') }}</h3> <h3>{{ _('Send via Emails') }}</h3>
</td> </td>
<td> <td>
<p>{{ _("Specify a (comma separated) list of email adresses you want to notify about the <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>
creation of this budget management project and we will send them an email for you.") }}</p>
{% include "display_errors.html" %} {% include "display_errors.html" %}
<form class="invites form-horizontal" method="post" accept-charset="utf-8"> <form class="invites form-horizontal" method="post" accept-charset="utf-8">
{{ forms.invites(form) }} {{ forms.invites(form) }}
</form> </form>
</td> </td>
</tr> </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> </tbody>
</table> </table>

View file

@ -9,13 +9,22 @@
{% block content %} {% block content %}
<table id="bill_table" class="split_bills table table-striped"> <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> <tbody>
{% for bill in bills %} {% for bill in bills %}
<tr receiver={{bill.receiver.id}}> <tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td> <td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</td> <td>{{ bill.receiver }}</td>
<td>{{ bill.amount|currency }}</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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -11,7 +11,7 @@
</tr> </tr>
</thead> </thead>
{%- endif %} {%- 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 %}"> <tr id="bal-member-{{ member.id }}" action="{% if member.activated %}delete{% else %}reactivate{% endif %}">
<td class="balance-name">{{ member.name }} <td class="balance-name">{{ member.name }}
{%- if show_weight -%} {%- if show_weight -%}

View file

@ -1,14 +1,14 @@
import base64 import base64
import datetime import datetime
import json import json
import unittest
import pytest
from ihatemoney.tests.common.help_functions import em_surround from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
class APITestCase(IhatemoneyTestCase): class TestAPI(IhatemoneyTestCase):
"""Tests the API""" """Tests the API"""
def api_create( def api_create(
@ -42,7 +42,7 @@ class APITestCase(IhatemoneyTestCase):
def get_auth(self, username, password=None): def get_auth(self, username, password=None):
password = password or username password = password or username
base64string = ( base64string = (
base64.encodebytes(f"{username}:{password}".encode("utf-8")) base64.encodebytes(f"{username}:{password}".encode("utf-8")) # noqa: E231
.decode("utf-8") .decode("utf-8")
.replace("\n", "") .replace("\n", "")
) )
@ -57,7 +57,7 @@ class APITestCase(IhatemoneyTestCase):
resp = self.client.options( resp = self.client.options(
"/api/projects/raclette", headers=self.get_auth("raclette") "/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): def test_basic_auth(self):
# create a project # create a project
@ -94,32 +94,32 @@ class APITestCase(IhatemoneyTestCase):
}, },
) )
self.assertTrue(400, resp.status_code) assert 400 == resp.status_code
self.assertEqual( assert '{"contact_email": ["Invalid email address."]}\n' == resp.data.decode(
'{"contact_email": ["Invalid email address."]}\n', resp.data.decode("utf-8") "utf-8"
) )
# create it # create it
with self.app.mail.record_messages() as outbox: with self.app.mail.record_messages() as outbox:
resp = self.api_create("raclette") resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code) assert 201 == resp.status_code
# Check that email messages have been sent. # Check that email messages have been sent.
self.assertEqual(len(outbox), 1) assert len(outbox) == 1
self.assertEqual(outbox[0].recipients, ["raclette@notmyidea.org"]) assert outbox[0].recipients == ["raclette@notmyidea.org"]
# create it twice should return a 400 # create it twice should return a 400
resp = self.api_create("raclette") resp = self.api_create("raclette")
self.assertTrue(400, resp.status_code) assert 400 == resp.status_code
self.assertIn("id", json.loads(resp.data.decode("utf-8"))) assert "id" in json.loads(resp.data.decode("utf-8"))
# get information about it # get information about it
resp = self.client.get( resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette") "/api/projects/raclette", headers=self.get_auth("raclette")
) )
self.assertTrue(200, resp.status_code) assert 200 == resp.status_code
expected = { expected = {
"members": [], "members": [],
"name": "raclette", "name": "raclette",
@ -129,9 +129,9 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1, "logging_preference": 1,
} }
decoded_resp = json.loads(resp.data.decode("utf-8")) 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( resp = self.client.put(
"/api/projects/raclette", "/api/projects/raclette",
data={ data={
@ -143,14 +143,43 @@ class APITestCase(IhatemoneyTestCase):
}, },
headers=self.get_auth("raclette"), 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( resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette") "/api/projects/raclette", headers=self.get_auth("raclette")
) )
self.assertEqual(200, resp.status_code) assert 200 == resp.status_code
expected = { expected = {
"name": "The raclette party", "name": "The raclette party",
"contact_email": "yeah@notmyidea.org", "contact_email": "yeah@notmyidea.org",
@ -160,7 +189,7 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1, "logging_preference": 1,
} }
decoded_resp = json.loads(resp.data.decode("utf-8")) decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected) assert decoded_resp == expected
# password change is possible via API # password change is possible via API
resp = self.client.put( resp = self.client.put(
@ -168,18 +197,19 @@ class APITestCase(IhatemoneyTestCase):
data={ data={
"contact_email": "yeah@notmyidea.org", "contact_email": "yeah@notmyidea.org",
"default_currency": "XXX", "default_currency": "XXX",
"current_password": "raclette",
"password": "tartiflette", "password": "tartiflette",
"name": "The raclette party", "name": "The raclette party",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertEqual(200, resp.status_code) assert 200 == resp.status_code
resp = self.client.get( resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette") "/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
) )
self.assertEqual(200, resp.status_code) assert 200 == resp.status_code
# delete should work # delete should work
resp = self.client.delete( resp = self.client.delete(
@ -190,21 +220,21 @@ class APITestCase(IhatemoneyTestCase):
resp = self.client.get( resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette") "/api/projects/raclette", headers=self.get_auth("raclette")
) )
self.assertEqual(401, resp.status_code) assert 401 == resp.status_code
def test_token_creation(self): def test_token_creation(self):
"""Test that token of project is generated""" """Test that token of project is generated"""
# Create project # Create project
resp = self.api_create("raclette") resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code) assert 201 == resp.status_code
# Get token # Get token
resp = self.client.get( resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette") "/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")) decoded_resp = json.loads(resp.data.decode("utf-8"))
@ -213,8 +243,22 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/token", "/api/projects/raclette/token",
headers={"Authorization": f"Basic {decoded_resp['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): def test_token_login(self):
resp = self.api_create("raclette") resp = self.api_create("raclette")
@ -225,7 +269,7 @@ class APITestCase(IhatemoneyTestCase):
decoded_resp = json.loads(resp.data.decode("utf-8")) decoded_resp = json.loads(resp.data.decode("utf-8"))
resp = self.client.get(f"/raclette/join/{decoded_resp['token']}") resp = self.client.get(f"/raclette/join/{decoded_resp['token']}")
# Test that we are redirected. # Test that we are redirected.
self.assertEqual(302, resp.status_code) assert 302 == resp.status_code
def test_member(self): def test_member(self):
# create a project # create a project
@ -237,7 +281,7 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8")) assert "[]\n" == req.data.decode("utf-8")
# add a member # add a member
req = self.client.post( req = self.client.post(
@ -248,7 +292,7 @@ class APITestCase(IhatemoneyTestCase):
# the id of the new member should be returned # the id of the new member should be returned
self.assertStatus(201, req) 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 # the list of participants should contain one member
req = self.client.get( req = self.client.get(
@ -256,7 +300,7 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(200, req) 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. # Try to add another member with the same name.
req = self.client.post( req = self.client.post(
@ -269,7 +313,7 @@ class APITestCase(IhatemoneyTestCase):
# edit the participant # edit the participant
req = self.client.put( req = self.client.put(
"/api/projects/raclette/members/1", "/api/projects/raclette/members/1",
data={"name": "Fred", "weight": 2}, data={"name": "Jeanne", "weight": 2},
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
@ -281,14 +325,14 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data.decode("utf-8"))["name"]) assert "Jeanne" == json.loads(req.data.decode("utf-8"))["name"]
self.assertEqual(2, json.loads(req.data.decode("utf-8"))["weight"]) assert 2 == json.loads(req.data.decode("utf-8"))["weight"]
# edit this member with same information # edit this member with same information
# (test PUT idemopotence) # (test PUT idempotence)
req = self.client.put( req = self.client.put(
"/api/projects/raclette/members/1", "/api/projects/raclette/members/1",
data={"name": "Fred"}, data={"name": "Jeanne"},
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
@ -297,7 +341,7 @@ class APITestCase(IhatemoneyTestCase):
# de-activate the participant # de-activate the participant
req = self.client.put( req = self.client.put(
"/api/projects/raclette/members/1", "/api/projects/raclette/members/1",
data={"name": "Fred", "activated": False}, data={"name": "Jeanne", "activated": False},
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertStatus(200, req) self.assertStatus(200, req)
@ -306,12 +350,12 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/members/1", headers=self.get_auth("raclette") "/api/projects/raclette/members/1", headers=self.get_auth("raclette")
) )
self.assertStatus(200, req) 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 # re-activate the participant
req = self.client.put( req = self.client.put(
"/api/projects/raclette/members/1", "/api/projects/raclette/members/1",
data={"name": "Fred", "activated": True}, data={"name": "Jeanne", "activated": True},
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
@ -319,7 +363,7 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/members/1", headers=self.get_auth("raclette") "/api/projects/raclette/members/1", headers=self.get_auth("raclette")
) )
self.assertStatus(200, req) 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 # delete a member
@ -335,7 +379,7 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8")) assert "[]\n" == req.data.decode("utf-8")
def test_bills(self): def test_bills(self):
# create a project # create a project
@ -343,7 +387,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants # add participants
self.api_add_member("raclette", "zorglub") self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred") self.api_add_member("raclette", "jeanne")
self.api_add_member("raclette", "quentin") self.api_add_member("raclette", "quentin")
# get the list of bills (should be empty) # get the list of bills (should be empty)
@ -352,7 +396,7 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8")) assert "[]\n" == req.data.decode("utf-8")
# add a bill # add a bill
req = self.client.post( req = self.client.post(
@ -362,6 +406,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -370,7 +415,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id # should return the id
self.assertStatus(201, req) 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 # get this bill details
req = self.client.get( req = self.client.get(
@ -384,8 +429,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1, "payer_id": 1,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"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, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -395,19 +441,19 @@ class APITestCase(IhatemoneyTestCase):
} }
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
self.assertEqual( assert (
datetime.date.today(), datetime.date.today()
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(), == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
) )
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# the list of bills should length 1 # the list of bills should length 1
req = self.client.get( req = self.client.get(
"/api/projects/raclette/bills", headers=self.get_auth("raclette") "/api/projects/raclette/bills", headers=self.get_auth("raclette")
) )
self.assertStatus(200, req) 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 # edit with errors should return an error
req = self.client.put( req = self.client.put(
@ -417,6 +463,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -424,9 +471,7 @@ class APITestCase(IhatemoneyTestCase):
) )
self.assertStatus(400, req) self.assertStatus(400, req)
self.assertEqual( assert '{"date": ["This field is required."]}\n' == req.data.decode("utf-8")
'{"date": ["This field is required."]}\n', req.data.decode("utf-8")
)
# edit a bill # edit a bill
req = self.client.put( req = self.client.put(
@ -436,6 +481,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "beer", "what": "beer",
"payer": "2", "payer": "2",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -455,8 +501,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 2, "payer_id": 2,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"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, "amount": 25.0,
"date": "2011-09-10", "date": "2011-09-10",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
@ -466,12 +513,12 @@ class APITestCase(IhatemoneyTestCase):
} }
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
self.assertEqual( assert (
creation_date, creation_date
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(), == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
) )
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# delete a bill # delete a bill
req = self.client.delete( req = self.client.delete(
@ -491,7 +538,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants # add participants
self.api_add_member("raclette", "zorglub") self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred") self.api_add_member("raclette", "jeanne")
# valid amounts # valid amounts
input_expected = [ input_expected = [
@ -511,6 +558,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": input_amount, "amount": input_amount,
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -518,7 +566,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id # should return the id
self.assertStatus(201, req) 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 # get this bill's details
req = self.client.get( req = self.client.get(
@ -533,8 +581,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1, "payer_id": 1,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"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, "amount": expected_amount,
"date": "2011-08-10", "date": "2011-08-10",
"id": id, "id": id,
@ -544,12 +593,12 @@ class APITestCase(IhatemoneyTestCase):
} }
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
self.assertEqual( assert (
datetime.date.today(), datetime.date.today()
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(), == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
) )
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# should raise errors # should raise errors
erroneous_amounts = [ erroneous_amounts = [
@ -568,28 +617,30 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": amount, "amount": amount,
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertStatus(400, req) self.assertStatus(400, req)
@pytest.mark.skip(reason="Currency conversion is broken")
def test_currencies(self): def test_currencies(self):
# check /currencies for list of supported currencies # check /currencies for list of supported currencies
resp = self.client.get("/api/currencies") resp = self.client.get("/api/currencies")
self.assertTrue(201, resp.status_code) assert 200 == resp.status_code
self.assertIn("XXX", json.loads(resp.data.decode("utf-8"))) assert "XXX" in json.loads(resp.data.decode("utf-8"))
# create project with a default currency # create project with a default currency
resp = self.api_create("raclette", default_currency="EUR") resp = self.api_create("raclette", default_currency="EUR")
self.assertTrue(201, resp.status_code) assert 201 == resp.status_code
# get information about it # get information about it
resp = self.client.get( resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette") "/api/projects/raclette", headers=self.get_auth("raclette")
) )
self.assertTrue(200, resp.status_code) assert 200 == resp.status_code
expected = { expected = {
"members": [], "members": [],
"name": "raclette", "name": "raclette",
@ -599,11 +650,11 @@ class APITestCase(IhatemoneyTestCase):
"logging_preference": 1, "logging_preference": 1,
} }
decoded_resp = json.loads(resp.data.decode("utf-8")) decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected) assert decoded_resp == expected
# Add participants # Add participants
self.api_add_member("raclette", "zorglub") self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred") self.api_add_member("raclette", "jeanne")
self.api_add_member("raclette", "quentin") self.api_add_member("raclette", "quentin")
# Add a bill without explicit currency # Add a bill without explicit currency
@ -614,6 +665,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -622,7 +674,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id # should return the id
self.assertStatus(201, req) 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 # get this bill details
req = self.client.get( req = self.client.get(
@ -636,8 +688,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1, "payer_id": 1,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"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, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -647,12 +700,12 @@ class APITestCase(IhatemoneyTestCase):
} }
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
self.assertEqual( assert (
datetime.date.today(), datetime.date.today()
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(), == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
) )
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# Change bill amount and currency # Change bill amount and currency
req = self.client.put( req = self.client.put(
@ -662,6 +715,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "30", "amount": "30",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
"original_currency": "CAD", "original_currency": "CAD",
@ -681,8 +735,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1, "payer_id": 1,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0}, {"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, "amount": 30.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -693,7 +748,7 @@ class APITestCase(IhatemoneyTestCase):
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# Add a bill with yet another currency # Add a bill with yet another currency
req = self.client.post( req = self.client.post(
@ -703,6 +758,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "Pierogi", "what": "Pierogi",
"payer": "1", "payer": "1",
"payed_for": ["2", "3"], "payed_for": ["2", "3"],
"bill_type": "Expense",
"amount": "80", "amount": "80",
"original_currency": "PLN", "original_currency": "PLN",
}, },
@ -711,7 +767,7 @@ class APITestCase(IhatemoneyTestCase):
# should return the id # should return the id
self.assertStatus(201, req) 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 # Try to remove default project currency, it should fail
req = self.client.put( req = self.client.put(
@ -719,15 +775,16 @@ class APITestCase(IhatemoneyTestCase):
data={ data={
"contact_email": "yeah@notmyidea.org", "contact_email": "yeah@notmyidea.org",
"default_currency": "XXX", "default_currency": "XXX",
"current_password": "raclette",
"password": "raclette", "password": "raclette",
"name": "The raclette party", "name": "The raclette party",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertStatus(400, req) self.assertStatus(400, req)
self.assertIn("This project cannot be set", req.data.decode("utf-8")) assert "This project cannot be set" in req.data.decode("utf-8")
self.assertIn( assert "because it contains bills in multiple currencies" in req.data.decode(
"because it contains bills in multiple currencies", req.data.decode("utf-8") "utf-8"
) )
def test_statistics(self): def test_statistics(self):
@ -736,7 +793,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants # add participants
self.api_add_member("raclette", "zorglub") self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred") self.api_add_member("raclette", "jeanne")
# add a bill # add a bill
req = self.client.post( req = self.client.post(
@ -746,6 +803,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -756,8 +814,7 @@ class APITestCase(IhatemoneyTestCase):
"/api/projects/raclette/statistics", headers=self.get_auth("raclette") "/api/projects/raclette/statistics", headers=self.get_auth("raclette")
) )
self.assertStatus(200, req) self.assertStatus(200, req)
self.assertEqual( assert [
[
{ {
"balance": 12.5, "balance": 12.5,
"member": { "member": {
@ -774,15 +831,13 @@ class APITestCase(IhatemoneyTestCase):
"member": { "member": {
"activated": True, "activated": True,
"id": 2, "id": 2,
"name": "fred", "name": "jeanne",
"weight": 1.0, "weight": 1.0,
}, },
"paid": 0, "paid": 0,
"spent": 12.5, "spent": 12.5,
}, },
], ] == json.loads(req.data.decode("utf-8"))
json.loads(req.data.decode("utf-8")),
)
def test_username_xss(self): def test_username_xss(self):
# create a project # create a project
@ -794,7 +849,7 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "<script>") self.api_add_member("raclette", "<script>")
result = self.client.get("/raclette/") 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): def test_weighted_bills(self):
# create a project # create a project
@ -802,7 +857,7 @@ class APITestCase(IhatemoneyTestCase):
# add participants # add participants
self.api_add_member("raclette", "zorglub") 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") self.api_add_member("raclette", "quentin")
# add a bill # add a bill
@ -813,6 +868,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -833,8 +889,9 @@ class APITestCase(IhatemoneyTestCase):
"payer_id": 1, "payer_id": 1,
"owers": [ "owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"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, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -843,12 +900,12 @@ class APITestCase(IhatemoneyTestCase):
"original_currency": "XXX", "original_currency": "XXX",
} }
got = json.loads(req.data.decode("utf-8")) got = json.loads(req.data.decode("utf-8"))
self.assertEqual( assert (
creation_date, creation_date
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(), == datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date()
) )
del got["creation_date"] del got["creation_date"]
self.assertDictEqual(expected, got) assert expected == got
# getting it should return a 404 # getting it should return a 404
req = self.client.get( req = self.client.get(
@ -867,7 +924,7 @@ class APITestCase(IhatemoneyTestCase):
{ {
"activated": True, "activated": True,
"id": 2, "id": 2,
"name": "freddy familly", "name": "jeannedy familly",
"weight": 4.0, "weight": 4.0,
"balance": -20.0, "balance": -20.0,
}, },
@ -888,7 +945,7 @@ class APITestCase(IhatemoneyTestCase):
self.assertStatus(200, req) self.assertStatus(200, req)
decoded_req = json.loads(req.data.decode("utf-8")) 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): def test_log_created_from_api_call(self):
# create a project # create a project
@ -899,15 +956,13 @@ class APITestCase(IhatemoneyTestCase):
self.api_add_member("raclette", "zorglub") self.api_add_member("raclette", "zorglub")
resp = self.client.get("/raclette/history", follow_redirects=True) resp = self.client.get("/raclette/history", follow_redirects=True)
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Participant {em_surround('zorglub')} added" in resp.data.decode(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8") "utf-8"
) )
self.assertIn( assert f"Project {em_surround('raclette')} added" in resp.data.decode("utf-8")
f"Project {em_surround('raclette')} added", 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.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def test_amount_is_null(self): def test_amount_is_null(self):
self.api_create("raclette") self.api_create("raclette")
@ -922,6 +977,7 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1"], "payed_for": ["1"],
"bill_type": "Expense",
"amount": "0", "amount": "0",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -950,12 +1006,71 @@ class APITestCase(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1"], "payed_for": ["1"],
"bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12", "amount": "9347242149381274732472348728748723473278472843.12",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertStatus(400, req) self.assertStatus(400, req)
def test_validate_bill_type(self):
self.api_create("raclette")
self.api_add_member("raclette", "zorglub")
if __name__ == "__main__": req = self.client.post(
unittest.main() "/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 import os
from unittest.mock import MagicMock
from flask_testing import TestCase import pytest
from werkzeug.security import generate_password_hash
from ihatemoney import models from ihatemoney import models
from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.utils import generate_password_hash
from ihatemoney.run import create_app, db
class BaseTestCase(TestCase): @pytest.mark.usefixtures("client", "converter")
class BaseTestCase:
SECRET_KEY = "TEST SESSION" SECRET_KEY = "TEST SESSION"
SQLALCHEMY_DATABASE_URI = os.environ.get( SQLALCHEMY_DATABASE_URI = os.environ.get(
"TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://" "TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://"
) )
ENABLE_CAPTCHA = False ENABLE_CAPTCHA = False
PASSWORD_HASH_METHOD = "pbkdf2:sha1:1"
def create_app(self): PASSWORD_HASH_SALT_LENGTH = 1
# 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()
def login(self, project, password=None, test_client=None): def login(self, project, password=None, test_client=None):
password = password or project password = password or project
@ -56,6 +32,7 @@ class BaseTestCase(TestCase):
default_currency="XXX", default_currency="XXX",
name=None, name=None,
password=None, password=None,
project_history=True,
): ):
"""Create a fake project""" """Create a fake project"""
name = name or id name = name or id
@ -69,6 +46,7 @@ class BaseTestCase(TestCase):
"password": password, "password": password,
"contact_email": f"{id}@notmyidea.org", "contact_email": f"{id}@notmyidea.org",
"default_currency": default_currency, "default_currency": default_currency,
"project_history": project_history,
}, },
follow_redirects=follow_redirects, follow_redirects=follow_redirects,
) )
@ -79,7 +57,7 @@ class BaseTestCase(TestCase):
data=data, data=data,
# follow_redirects=True, # 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): def create_project(self, id, default_currency="XXX", name=None, password=None):
name = name or str(id) name = name or str(id)
@ -105,11 +83,9 @@ class IhatemoneyTestCase(BaseTestCase):
def assertStatus(self, expected, resp, url=None): def assertStatus(self, expected, resp, url=None):
if url is None: if url is None:
url = resp.request.path url = resp.request.path
return self.assertEqual( assert (
expected, expected == resp.status_code
resp.status_code, ), f"{url} expected {expected}, got {resp.status_code}"
f"{url} expected {expected}, got {resp.status_code}",
)
def enable_admin(self, password="adminpass"): def enable_admin(self, password="adminpass"):
self.app.config["ACTIVATE_ADMIN_DASHBOARD"] = True 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 import history, models
from ihatemoney.tests.common.help_functions import em_surround 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 from ihatemoney.versioning import LoggingMode
class HistoryTestCase(IhatemoneyTestCase): @pytest.fixture
def setUp(self): def demo(client):
super().setUp() client.post(
self.post_project("demo") "/create",
self.login("demo") 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): def test_simple_create_logentry_no_ip(self):
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8")) assert f"Project {em_surround('demo')} added" in resp.data.decode("utf-8")
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1) assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) 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 # Change only logging_preferences
new_data = { new_data = {
"name": "demo", "name": "demo",
"contact_email": "demo@notmyidea.org", "contact_email": "demo@notmyidea.org",
"current_password": current_password,
"password": "demo", "password": "demo",
"default_currency": "XXX", "default_currency": "XXX",
} }
@ -35,142 +53,136 @@ class HistoryTestCase(IhatemoneyTestCase):
# Disable History # Disable History
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True) resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertNotIn("alert-danger", resp.data.decode("utf-8")) assert "alert-danger" not in resp.data.decode("utf-8")
resp = self.client.get("/demo/edit") resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
if logging_preference == LoggingMode.DISABLED: 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: else:
self.assertIn( assert '<input checked id="project_history"' in resp.data.decode("utf-8")
'<input checked id="project_history"', resp.data.decode("utf-8")
)
if logging_preference == LoggingMode.RECORD_IP: 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: 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): def assert_empty_history_logging_disabled(self):
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertIn( assert (
"This project has history disabled. New actions won't appear below.", "This project has history disabled. New actions won't appear below."
resp.data.decode("utf-8"), in resp.data.decode("utf-8")
) )
self.assertIn("Nothing to list", resp.data.decode("utf-8")) assert "Nothing to list" in resp.data.decode("utf-8")
self.assertNotIn( assert (
"The table below reflects actions recorded prior to disabling project history.", "The table below reflects actions recorded prior to disabling project history."
resp.data.decode("utf-8"), not in resp.data.decode("utf-8")
) )
self.assertNotIn( assert "Some entries below contain IP addresses," not in resp.data.decode(
"Some entries below contain IP addresses,", resp.data.decode("utf-8") "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 "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): def test_project_edit(self):
new_data = { new_data = {
"name": "demo2", "name": "demo2",
"contact_email": "demo2@notmyidea.org", "contact_email": "demo2@notmyidea.org",
"current_password": "demo",
"password": "123456", "password": "123456",
"project_history": "y", "project_history": "y",
"default_currency": "USD", # Currency changed from default "default_currency": "USD", # Currency changed from default
} }
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True) 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8")) assert f"Project {em_surround('demo')} added" in resp.data.decode("utf-8")
self.assertIn( assert (
f"Project contact email changed to {em_surround('demo2@notmyidea.org')}", f"Project contact email changed to {em_surround('demo2@notmyidea.org')}"
resp.data.decode("utf-8"), in resp.data.decode("utf-8")
) )
self.assertIn("Project private code changed", resp.data.decode("utf-8")) assert "Project private code changed" in resp.data.decode("utf-8")
self.assertIn( assert f"Project renamed to {em_surround('demo2')}" in resp.data.decode("utf-8")
f"Project renamed to {em_surround('demo2')}", resp.data.decode("utf-8") assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
) "utf-8"
self.assertLess( ).index("Project contact email changed to ")
resp.data.decode("utf-8").index("Project renamed "), assert resp.data.decode("utf-8").index("Project renamed ") < resp.data.decode(
resp.data.decode("utf-8").index("Project contact email changed to "), "utf-8"
) ).index("Project private code changed")
self.assertLess( assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
resp.data.decode("utf-8").index("Project renamed "), assert "127.0.0.1" not in resp.data.decode("utf-8")
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"))
def test_project_privacy_edit(self): def test_project_privacy_edit(self):
resp = self.client.get("/demo/edit") resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert (
'<input checked id="project_history" name="project_history" type="checkbox" value="y">', '<input checked id="project_history" name="project_history" type="checkbox" value="y">'
resp.data.decode("utf-8"), 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn("Disabled Project History\n", resp.data.decode("utf-8")) assert "Disabled Project History\n" in resp.data.decode("utf-8")
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2) assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert "Enabled Project History & IP Address Recording" in resp.data.decode(
"Enabled Project History & IP Address Recording", resp.data.decode("utf-8") "utf-8"
) )
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2) assert 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("127.0.0.1") == 1
self.change_privacy_to(LoggingMode.ENABLED) self.change_privacy_to("demo", LoggingMode.ENABLED)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn("Disabled IP Address Recording\n", resp.data.decode("utf-8")) assert "Disabled IP Address Recording\n" in resp.data.decode("utf-8")
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2) assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2) assert resp.data.decode("utf-8").count("127.0.0.1") == 2
def test_project_privacy_edit2(self): 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn("Enabled IP Address Recording\n", resp.data.decode("utf-8")) assert "Enabled IP Address Recording\n" in resp.data.decode("utf-8")
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1) assert resp.data.decode("utf-8").count("<td> -- </td>") == 1
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert "Disabled Project History & IP Address Recording" in resp.data.decode(
"Disabled Project History & IP Address Recording", resp.data.decode("utf-8") "utf-8"
) )
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1) assert 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("127.0.0.1") == 2
self.change_privacy_to(LoggingMode.ENABLED) self.change_privacy_to("demo", LoggingMode.ENABLED)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn("Enabled Project History\n", resp.data.decode("utf-8")) assert "Enabled Project History\n" in resp.data.decode("utf-8")
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2) assert resp.data.decode("utf-8").count("<td> -- </td>") == 2
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2) assert resp.data.decode("utf-8").count("127.0.0.1") == 2
def do_misc_database_operations(self, logging_mode): def do_misc_database_operations(self, logging_mode):
new_data = { new_data = {
"name": "demo2", "name": "demo2",
"contact_email": "demo2@notmyidea.org", "contact_email": "demo2@notmyidea.org",
"current_password": "demo",
"password": "123456", "password": "123456",
"default_currency": "USD", "default_currency": "USD",
} }
@ -182,13 +194,13 @@ class HistoryTestCase(IhatemoneyTestCase):
new_data["ip_recording"] = "y" new_data["ip_recording"] = "y"
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True) 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 # adds a member to this project
resp = self.client.post( resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True "/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 user_id = models.Person.query.one().id
@ -200,11 +212,12 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": user_id, "payer": user_id,
"payed_for": [user_id], "payed_for": [user_id],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
bill_id = models.Bill.query.one().id bill_id = models.Bill.query.one().id
@ -216,48 +229,46 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": user_id, "payer": user_id,
"payed_for": [user_id], "payed_for": [user_id],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
# delete the bill # delete the bill
resp = self.client.post(f"/demo/delete/{bill_id}", follow_redirects=True) 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 # delete user using POST method
resp = self.client.post( resp = self.client.post(
f"/demo/members/{user_id}/delete", follow_redirects=True 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): def test_disable_clear_no_new_records(self):
# Disable logging # 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 # Ensure we can't clear history with a GET or with a password-less POST
resp = self.client.get("/demo/erase_history") 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) resp = self.client.post("/demo/erase_history", follow_redirects=True)
self.assertIn( assert "Error deleting project history" in resp.data.decode("utf-8")
"Error deleting project history",
resp.data.decode("utf-8"),
)
# List history # List history
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert (
"This project has history disabled. New actions won't appear below.", "This project has history disabled. New actions won't appear below."
resp.data.decode("utf-8"), in resp.data.decode("utf-8")
) )
self.assertIn( assert (
"The table below reflects actions recorded prior to disabling project history.", "The table below reflects actions recorded prior to disabling project history."
resp.data.decode("utf-8"), in resp.data.decode("utf-8")
) )
self.assertNotIn("Nothing to list", resp.data.decode("utf-8")) assert "Nothing to list" not in resp.data.decode("utf-8")
self.assertNotIn( assert "Some entries below contain IP addresses," not in resp.data.decode(
"Some entries below contain IP addresses,", resp.data.decode("utf-8") "utf-8"
) )
# Clear Existing Entries # Clear Existing Entries
@ -266,7 +277,7 @@ class HistoryTestCase(IhatemoneyTestCase):
data={"password": "demo"}, data={"password": "demo"},
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assert_empty_history_logging_disabled() self.assert_empty_history_logging_disabled()
# Do lots of database operations & check that there's still no history # 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): def test_clear_ip_records(self):
# Enable IP Recording # 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 # Do lots of database operations to generate IP address entries
self.do_misc_database_operations(LoggingMode.RECORD_IP) self.do_misc_database_operations(LoggingMode.RECORD_IP)
# Disable IP Recording # Disable IP Recording
self.change_privacy_to(LoggingMode.ENABLED) self.change_privacy_to("123456", LoggingMode.ENABLED)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertNotIn( assert (
"This project has history disabled. New actions won't appear below.", "This project has history disabled. New actions won't appear below."
resp.data.decode("utf-8"), not in resp.data.decode("utf-8")
) )
self.assertNotIn( assert (
"The table below reflects actions recorded prior to disabling project history.", "The table below reflects actions recorded prior to disabling project history."
resp.data.decode("utf-8"), not in resp.data.decode("utf-8")
) )
self.assertNotIn("Nothing to list", resp.data.decode("utf-8")) assert "Nothing to list" not in resp.data.decode("utf-8")
self.assertIn( assert "Some entries below contain IP addresses," in resp.data.decode("utf-8")
"Some entries below contain IP addresses,", 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
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
# Generate more operations to confirm additional IP info isn't recorded # Generate more operations to confirm additional IP info isn't recorded
self.do_misc_database_operations(LoggingMode.ENABLED) self.do_misc_database_operations(LoggingMode.ENABLED)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12) assert resp.data.decode("utf-8").count("127.0.0.1") == 12
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 7) 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 # Ensure we can't clear IP data with a GET or with a password-less POST
resp = self.client.get("/demo/strip_ip_addresses") 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) resp = self.client.post("/demo/strip_ip_addresses", follow_redirects=True)
self.assertIn( assert "Error deleting recorded IP addresses" in resp.data.decode("utf-8")
"Error deleting recorded IP addresses",
resp.data.decode("utf-8"),
)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 12) assert resp.data.decode("utf-8").count("127.0.0.1") == 12
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 7) assert resp.data.decode("utf-8").count("<td> -- </td>") == 7
# Clear IP Data # Clear IP Data
resp = self.client.post( resp = self.client.post(
@ -330,33 +336,33 @@ class HistoryTestCase(IhatemoneyTestCase):
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertNotIn( assert (
"This project has history disabled. New actions won't appear below.", "This project has history disabled. New actions won't appear below."
resp.data.decode("utf-8"), not in resp.data.decode("utf-8")
) )
self.assertNotIn( assert (
"The table below reflects actions recorded prior to disabling project history.", "The table below reflects actions recorded prior to disabling project history."
resp.data.decode("utf-8"), not in resp.data.decode("utf-8")
) )
self.assertNotIn("Nothing to list", resp.data.decode("utf-8")) assert "Nothing to list" not in resp.data.decode("utf-8")
self.assertNotIn( assert "Some entries below contain IP addresses," not in resp.data.decode(
"Some entries below contain IP addresses,", resp.data.decode("utf-8") "utf-8"
) )
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 0) assert 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("<td> -- </td>") == 19
def test_logs_for_common_actions(self): def test_logs_for_common_actions(self):
# adds a member to this project # adds a member to this project
resp = self.client.post( resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True "/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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Participant {em_surround('zorglub')} added" in resp.data.decode(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8") "utf-8"
) )
# create a bill # create a bill
@ -367,17 +373,17 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Bill {em_surround('fromage à raclette')} added" in resp.data.decode(
f"Bill {em_surround('fromage à raclette')} added", "utf-8"
resp.data.decode("utf-8"),
) )
# edit the bill # edit the bill
@ -388,48 +394,42 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "new thing", "what": "new thing",
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Bill {em_surround('fromage à raclette')} added" in resp.data.decode(
f"Bill {em_surround('fromage à raclette')} added", "utf-8"
resp.data.decode("utf-8"),
) )
self.assertRegex( assert re.search(
resp.data.decode("utf-8"),
r"Bill %s:\s* Amount changed\s* from %s\s* to %s" r"Bill %s:\s* Amount changed\s* from %s\s* to %s"
% ( % (
em_surround("fromage à raclette", regex_escape=True), em_surround("fromage à raclette", regex_escape=True),
em_surround("25.0", regex_escape=True), em_surround("25.0", regex_escape=True),
em_surround("10.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"), resp.data.decode("utf-8"),
) )
self.assertLess( assert "Bill %s renamed to %s" % (
resp.data.decode("utf-8").index( 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" f"Bill {em_surround('fromage à raclette')} renamed to"
), ) < resp.data.decode("utf-8").index("Amount changed")
resp.data.decode("utf-8").index("Amount changed"),
)
# delete the bill # delete the bill
resp = self.client.post("/demo/delete/1", follow_redirects=True) 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Bill {em_surround('new thing')} removed" in resp.data.decode("utf-8")
f"Bill {em_surround('new thing')} removed", resp.data.decode("utf-8")
)
# edit user # edit user
resp = self.client.post( resp = self.client.post(
@ -437,39 +437,35 @@ class HistoryTestCase(IhatemoneyTestCase):
data={"weight": 2, "name": "new name"}, data={"weight": 2, "name": "new name"},
follow_redirects=True, follow_redirects=True,
) )
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertRegex( assert re.search(
resp.data.decode("utf-8"),
r"Participant %s:\s* weight changed\s* from %s\s* to %s" r"Participant %s:\s* weight changed\s* from %s\s* to %s"
% ( % (
em_surround("zorglub", regex_escape=True), em_surround("zorglub", regex_escape=True),
em_surround("1.0", regex_escape=True), em_surround("1.0", regex_escape=True),
em_surround("2.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"), resp.data.decode("utf-8"),
) )
self.assertLess( assert "Participant %s renamed to %s" % (
resp.data.decode("utf-8").index( 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" f"Participant {em_surround('zorglub')} renamed"
), ) < resp.data.decode("utf-8").index("weight changed")
resp.data.decode("utf-8").index("weight changed"),
)
# delete user using POST method # delete user using POST method
resp = self.client.post("/demo/members/1/delete", follow_redirects=True) 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") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertIn( assert f"Participant {em_surround('new name')} removed" in resp.data.decode(
f"Participant {em_surround('new name')} removed", resp.data.decode("utf-8") "utf-8"
) )
def test_double_bill_double_person_edit_second(self): def test_double_bill_double_person_edit_second(self):
@ -485,6 +481,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1", "what": "Bill 1",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
) )
@ -495,15 +492,16 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 2", "what": "Bill 2",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
# Should be 5 history entries at this point # Should be 5 history entries at this point
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5) assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) assert "127.0.0.1" not in resp.data.decode("utf-8")
# Edit ONLY the amount on the first bill # Edit ONLY the amount on the first bill
self.client.post( self.client.post(
@ -513,33 +511,33 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1", "what": "Bill 1",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "88", "amount": "88",
}, },
) )
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertRegex( assert re.search(
resp.data.decode("utf-8"),
r"Bill {}:\s* Amount changed\s* from {}\s* to {}".format( r"Bill {}:\s* Amount changed\s* from {}\s* to {}".format(
em_surround("Bill 1", regex_escape=True), em_surround("Bill 1", regex_escape=True),
em_surround("25.0", regex_escape=True), em_surround("25.0", regex_escape=True),
em_surround("88.0", regex_escape=True), em_surround("88.0", regex_escape=True),
), ),
resp.data.decode("utf-8"),
) )
self.assertNotRegex( assert not re.search(
resp.data.decode("utf-8"),
r"Removed\s* {}\s* and\s* {}\s* from\s* owers list".format( r"Removed\s* {}\s* and\s* {}\s* from\s* owers list".format(
em_surround("User 1", regex_escape=True), em_surround("User 1", regex_escape=True),
em_surround("User 2", regex_escape=True), em_surround("User 2", regex_escape=True),
), ),
resp.data.decode("utf-8"), resp.data.decode("utf-8"),
) ), resp.data.decode("utf-8")
# Should be 6 history entries at this point # Should be 6 history entries at this point
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6) assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) assert "127.0.0.1" not in resp.data.decode("utf-8")
def test_bill_add_remove_add(self): def test_bill_add_remove_add(self):
# add two members # add two members
@ -554,6 +552,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 1", "what": "Bill 1",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
) )
@ -562,13 +561,11 @@ class HistoryTestCase(IhatemoneyTestCase):
self.client.post("/demo/delete/1", follow_redirects=True) self.client.post("/demo/delete/1", follow_redirects=True)
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5) assert resp.data.decode("utf-8").count("<td> -- </td>") == 5
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) assert "127.0.0.1" not in resp.data.decode("utf-8")
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8")) assert f"Bill {em_surround('Bill 1')} added" in resp.data.decode("utf-8")
self.assertIn( assert f"Bill {em_surround('Bill 1')} removed" in resp.data.decode("utf-8")
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
# Add a new bill # Add a new bill
self.client.post( self.client.post(
@ -578,22 +575,21 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "Bill 2", "what": "Bill 2",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
resp = self.client.get("/demo/history") resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200) assert resp.status_code == 200
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6) assert resp.data.decode("utf-8").count("<td> -- </td>") == 6
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8")) assert "127.0.0.1" not in resp.data.decode("utf-8")
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8")) assert f"Bill {em_surround('Bill 1')} added" in resp.data.decode("utf-8")
self.assertEqual( assert (
resp.data.decode("utf-8").count(f"Bill {em_surround('Bill 1')} added"), 1 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 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): def test_double_bill_double_person_edit_second_no_web(self):
u1 = models.Person(project_id="demo", name="User 1") u1 = models.Person(project_id="demo", name="User 1")
@ -614,7 +610,7 @@ class HistoryTestCase(IhatemoneyTestCase):
models.db.session.commit() models.db.session.commit()
history_list = history.get_history(self.get_project("demo")) history_list = history.get_history(self.get_project("demo"))
self.assertEqual(len(history_list), 5) assert len(history_list) == 5
# Change just the amount # Change just the amount
b1.amount = 5 b1.amount = 5
@ -623,8 +619,8 @@ class HistoryTestCase(IhatemoneyTestCase):
history_list = history.get_history(self.get_project("demo")) history_list = history.get_history(self.get_project("demo"))
for entry in history_list: for entry in history_list:
if "prop_changed" in entry: if "prop_changed" in entry:
self.assertNotIn("owers", entry["prop_changed"]) assert "owers" not in entry["prop_changed"]
self.assertEqual(len(history_list), 6) assert len(history_list) == 6
def test_delete_history_with_project(self): def test_delete_history_with_project(self):
self.post_project("raclette", password="party") self.post_project("raclette", password="party")
@ -640,6 +636,7 @@ class HistoryTestCase(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "10", "amount": "10",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -656,8 +653,4 @@ class HistoryTestCase(IhatemoneyTestCase):
# History should be equal to project creation # History should be equal to project creation
history_list = history.get_history(self.get_project("raclette")) history_list = history.get_history(self.get_project("raclette"))
self.assertEqual(len(history_list), 1) assert len(history_list) == 1
if __name__ == "__main__":
unittest.main()

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-04-14 21:10+0000\n"
"Last-Translator: Hasidul Islam <ihasidul@gmail.com>\n" "Last-Translator: Hasidul Islam <ihasidul@gmail.com>\n"
"Language: bn\n" "Language: bn\n"
@ -27,6 +27,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "প্রজেক্টের নাম" msgstr "প্রজেক্টের নাম"
#, fuzzy
msgid "Current private code"
msgstr "নতুন ব্যক্তিগত কোড"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code" msgid "New private code"
msgstr "নতুন ব্যক্তিগত কোড" msgstr "নতুন ব্যক্তিগত কোড"
@ -48,6 +55,12 @@ msgstr "ডিফল্ট মুদ্রা"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr "অজানা ত্রুটি"
msgid "Invalid private code."
msgstr "অবৈধ ব্যক্তিগত কোড."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -87,12 +100,6 @@ msgstr "অনুগ্রহ করে, এগিয়ে যেতে ক্
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "মুছে ফেলা নিশ্চিত করতে ব্যক্তিগত কোড লিখুন" msgstr "মুছে ফেলা নিশ্চিত করতে ব্যক্তিগত কোড লিখুন"
msgid "Unknown error"
msgstr "অজানা ত্রুটি"
msgid "Invalid private code."
msgstr "অবৈধ ব্যক্তিগত কোড."
msgid "Get in" msgid "Get in"
msgstr "প্রবেশ করুন" msgstr "প্রবেশ করুন"
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -747,6 +757,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -769,7 +782,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -823,13 +836,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -852,21 +862,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -879,10 +882,29 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -989,3 +1011,43 @@ msgstr ""
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2020-08-01 10:41+0000\n"
"Last-Translator: Oymate <dhruboadittya96@gmail.com>\n" "Last-Translator: Oymate <dhruboadittya96@gmail.com>\n"
"Language: bn_BD\n" "Language: bn_BD\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "প্রকল্পের নাম" msgstr "প্রকল্পের নাম"
#, fuzzy
msgid "Current private code"
msgstr "ব্যক্তিগত কোড"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy #, fuzzy
msgid "New private code" msgid "New private code"
msgstr "ব্যক্তিগত কোড" msgstr "ব্যক্তিগত কোড"
@ -51,6 +58,13 @@ msgstr "ডিফল্ট মুদ্রা"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr "অজানা ত্রুট"
#, fuzzy
msgid "Invalid private code."
msgstr "ব্যক্তিগত কোড"
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -86,13 +100,6 @@ msgstr ""
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "মুছে ফেলার জন্য ব্যক্তিগত কোড লিখুন" msgstr "মুছে ফেলার জন্য ব্যক্তিগত কোড লিখুন"
msgid "Unknown error"
msgstr "অজানা ত্রুট"
#, fuzzy
msgid "Invalid private code."
msgstr "ব্যক্তিগত কোড"
msgid "Get in" msgid "Get in"
msgstr "ভিতরে আস" msgstr "ভিতরে আস"
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -750,6 +760,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -772,7 +785,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -826,13 +839,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -855,21 +865,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -882,10 +885,29 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -1094,3 +1116,43 @@ msgstr ""
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-09-12 15:25+0000\n" "PO-Revision-Date: 2024-07-03 19:09+0000\n"
"Last-Translator: Maite Guix <maite.guix@gmail.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nom del projecte" 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" msgid "New private code"
msgstr "Codi privat nou" msgstr "Codi privat nou"
@ -52,6 +59,12 @@ msgstr ""
"L'establiment d'una moneda predeterminada permet la conversió de moneda " "L'establiment d'una moneda predeterminada permet la conversió de moneda "
"entre factures" "entre factures"
msgid "Unknown error"
msgstr "Error desconegut"
msgid "Invalid private code."
msgstr "Codi privat no vàlid."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -91,12 +104,6 @@ msgstr "Si us plau, valida el captcha per a continuar."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Introdueix el codi privat per a confirmar la supressió" 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" msgid "Get in"
msgstr "Entrar-hi" msgstr "Entrar-hi"
@ -215,7 +222,7 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}" msgstr "{start_object}, {next_object}"
msgid "No Currency" msgid "No Currency"
msgstr "Sense moneda" msgstr "Cap moneda"
#. Form error with only one error #. Form error with only one error
msgid "{prefix}: {error}" msgid "{prefix}: {error}"
@ -273,6 +280,9 @@ msgstr "Projecte desconegut"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "La contrasenya s'ha restablert correctament." msgstr "La contrasenya s'ha restablert correctament."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -791,6 +801,9 @@ msgstr "Historial"
msgid "Settings" msgid "Settings"
msgstr "Configuració" msgstr "Configuració"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "Altres projectes:" msgstr "Altres projectes:"
@ -813,7 +826,7 @@ msgstr "Aplicació mòbil"
msgid "Documentation" msgid "Documentation"
msgstr "Documentació" msgstr "Documentació"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Panell d'administració" msgstr "Panell d'administració"
msgid "Legal information" msgid "Legal information"
@ -867,14 +880,13 @@ msgstr "Sense factures"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Res a enumerar encara." msgstr "Res a enumerar encara."
msgid "You probably want to" #, fuzzy
msgstr "Probablement vulguis" msgid "Add your first bill"
msgid "add a bill"
msgstr "afegir una factura" msgstr "afegir una factura"
msgid "add participants" #, fuzzy
msgstr "afegir participants" msgid "Add the first participant"
msgstr "Editar aquest participant"
msgid "Password reminder" msgid "Password reminder"
msgstr "Recordatori de contrasenya" msgstr "Recordatori de contrasenya"
@ -898,26 +910,15 @@ msgstr "Restablir la contrasenya"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Convidar a persones a unir-se a aquest projecte" msgstr "Convidar a persones a unir-se a aquest projecte"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Compartir l'identificador i el codi" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" 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" msgid "Scan QR code"
msgstr "" msgstr ""
@ -928,17 +929,37 @@ msgstr ""
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Enviar per correu electrònic" msgstr "Enviar per correu electrònic"
#, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Especifica una llista (separada per comes) dels correus electrònic als " "Especifica una llista (separada per comes) dels correus electrònic als "
"que vols notificar la\n" "que vols notificar la\n"
" creació d'aquest projecte de gestió pressupostària i els " " creació d'aquest projecte de gestió pressupostària i els "
"hi enviarem un correu electrònic." "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?" msgid "Who pays?"
msgstr "Qui ha de pagar?" msgstr "Qui ha de pagar?"
@ -1039,3 +1060,25 @@ msgstr "Període"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Editar el projecte" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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: 2023-07-09 19:50+0000\n" "PO-Revision-Date: 2024-02-24 00:03+0000\n"
"Last-Translator: Sebastian Lay <mail@sebastian-lay.de>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -31,6 +31,12 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Projektname" 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" msgid "New private code"
msgstr "Neuer privater Code" msgstr "Neuer privater Code"
@ -54,6 +60,12 @@ msgstr ""
"Das Festlegen einer Standardwährung ermöglicht die Währungsumrechnung " "Das Festlegen einer Standardwährung ermöglicht die Währungsumrechnung "
"zwischen Rechnungen" "zwischen Rechnungen"
msgid "Unknown error"
msgstr "Unbekannter Fehler"
msgid "Invalid private code."
msgstr "ungültiger privater Code."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -62,7 +74,7 @@ msgstr ""
"es Rechnungen unterschiedlicher Währungen enthält." "es Rechnungen unterschiedlicher Währungen enthält."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "Kompatibel mit Cospend"
msgid "Project identifier" msgid "Project identifier"
msgstr "Projektkennung" msgstr "Projektkennung"
@ -93,12 +105,6 @@ msgstr "Bitte bestätige das Captcha, um fortzufahren."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Geben Sie Ihren privaten Code ein, um die Löschung zu bestätigen" 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" msgid "Get in"
msgstr "Eintreten" msgstr "Eintreten"
@ -191,16 +197,15 @@ msgid "Logout"
msgstr "Ausloggen" msgstr "Ausloggen"
msgid "Please check the email configuration of the server." 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 "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails " "Bitte überprüfe die E-Mail Konfiguration des Servers oder kontaktiere einen "
"auf. Bitte überprüfe die E-Mail Konfiguration des Servers oder " "Administrator: %(admin_email)s"
"kontaktiere einen Administrator."
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -229,11 +234,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." msgid "Too many failed login attempts."
msgstr "" msgstr "Zu viele fehlgeschlagene Anmeldeversuche."
"Zu viele fehlgeschlagene Anmeldeversuche, bitte versuche es später "
"nochmal."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." 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." msgid "Password successfully reset."
msgstr "Passwort erfolgreich zurückgesetzt." msgstr "Passwort erfolgreich zurückgesetzt."
#, fuzzy
msgid "Project settings have been changed successfully."
msgstr "Einstellungen wurden erfolgreich übernommen."
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
#, python-format #, fuzzy, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "Fehlendes Attribut: %(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "Cannot add bills in multiple currencies to a project without default "
@ -302,7 +308,7 @@ msgid "Error deleting project"
msgstr "Fehler bei Projektlöschung" msgstr "Fehler bei Projektlöschung"
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "Verlassen nicht möglich"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" 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" msgid "Your invitations have been sent"
msgstr "Deine Einladungen wurden versendet" msgstr "Deine Einladungen wurden versendet"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails." msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr "" msgstr ""
"Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails " "Entschuldigung, es trat ein Fehler beim Versenden der Einladungsmails auf."
"auf. Bitte überprüfe die E-Mail Konfiguration des Servers oder "
"kontaktiere einen Administrator."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
@ -362,7 +365,7 @@ msgstr "Die Ausgabe wurde bearbeitet"
#, python-format #, python-format
msgid "%(lang)s is not a supported language" msgid "%(lang)s is not a supported language"
msgstr "" msgstr "%(lang)s ist keine unterstützte Sprache"
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "Projekthistorie konnte nicht gelöscht werden" msgstr "Projekthistorie konnte nicht gelöscht werden"
@ -442,9 +445,8 @@ msgstr "Mach mit"
msgid "Edit project" msgid "Edit project"
msgstr "Projekt bearbeiten" msgstr "Projekt bearbeiten"
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "Projekt bearbeiten" msgstr "Projekt importieren"
msgid "Download project's data" msgid "Download project's data"
msgstr "Projektdaten herunterladen" msgstr "Projektdaten herunterladen"
@ -471,14 +473,14 @@ msgid "Privacy Settings"
msgstr "Datenschutzeinstellungen" msgstr "Datenschutzeinstellungen"
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr "Änderungen speichern"
msgid "This will remove all bills and participants in this project!" msgid "This will remove all bills and participants in this project!"
msgstr "Dies wird alle Ausgaben und Mitglieder dieses Projektes löschen!" msgstr "Dies wird alle Ausgaben und Mitglieder dieses Projektes löschen!"
#, fuzzy #, fuzzy
msgid "Import previously exported project" msgid "Import previously exported project"
msgstr "Zuvor exportierte JSON-Datei importieren" msgstr "Zuvor exportierte Projekt-Datei importieren"
msgid "Choose file" msgid "Choose file"
msgstr "Datei auswählen" msgstr "Datei auswählen"
@ -489,8 +491,9 @@ msgstr "Ausgabe bearbeiten"
msgid "Add a bill" msgid "Add a bill"
msgstr "Ausgabe hinzufügen" msgstr "Ausgabe hinzufügen"
#, fuzzy
msgid "Simple operations are allowed, e.g. (18+36.2)/3" 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" msgid "Everyone"
msgstr "Jeder" 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" 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" 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." msgid "This project has history disabled. New actions won't appear below."
msgstr "" msgstr ""
"Dieses Projekt hat den Projektverlauf deaktiviert. Neue Aktionen werden "
"unten nicht mehr angezeigt."
#, fuzzy
msgid "You can enable history on the settings page." 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 "" msgid ""
"The table below reflects actions recorded prior to disabling project " "The table below reflects actions recorded prior to disabling project "
@ -794,6 +799,9 @@ msgstr "Verlauf"
msgid "Settings" msgid "Settings"
msgstr "Einstellungen" msgstr "Einstellungen"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "Andere Projekte:" msgstr "Andere Projekte:"
@ -805,7 +813,7 @@ msgstr "Dashboard"
#, python-format #, python-format
msgid "Please retry after %(date)s." msgid "Please retry after %(date)s."
msgstr "" msgstr "Bitte nach %(date)s erneut versuchen."
msgid "Code" msgid "Code"
msgstr "Code" msgstr "Code"
@ -816,7 +824,7 @@ msgstr "Handy-Applikation"
msgid "Documentation" msgid "Documentation"
msgstr "Dokumentation" msgstr "Dokumentation"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Dashboard Administration" msgstr "Dashboard Administration"
msgid "Legal information" msgid "Legal information"
@ -859,7 +867,7 @@ msgstr "Hinzugefügt am %(date)s"
#, python-format #, python-format
msgid "Everyone but %(excluded)s" msgid "Everyone but %(excluded)s"
msgstr "Jeder außer %(excluded)s" msgstr "Alle außer %(excluded)s"
msgid "delete" msgid "delete"
msgstr "Löschen" msgstr "Löschen"
@ -870,14 +878,12 @@ msgstr "Keine Ausgaben"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Noch nichts aufzulisten." msgstr "Noch nichts aufzulisten."
msgid "You probably want to" msgid "Add your first bill"
msgstr "Du willst wahrscheinlich" msgstr "Mach deine erste Rechnung"
msgid "add a bill" #, fuzzy
msgstr "eine Ausgabe hinzufügen" msgid "Add the first participant"
msgstr "Diesen Benutzer bearbeiten"
msgid "add participants"
msgstr "Teilnehmer hinzufügen"
msgid "Password reminder" msgid "Password reminder"
msgstr "Passwort-Erinnerung" msgstr "Passwort-Erinnerung"
@ -901,44 +907,57 @@ msgstr "Setze dein Passwort zurück"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Lade Leute ein, diesem Projekt beizutreten" msgstr "Lade Leute ein, diesem Projekt beizutreten"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Teile die ID & den Code" msgstr "Einladung verschicken"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" msgstr ""
"Du kannst die Projekt-ID und den privaten Code auf jedem " "Am einfachsten lädt man jemanden ein, indem man folgenden Einladungs-Link "
"Kommunikationsweg weitergeben." "teilt. <br /> Sie werden dann in der Lage sein dem Projekt beizutreten, zu "
"bearbeiten und Rechnungen hinzuzufügen, bearbeiten und löschen. Sie werden "
msgid "Identifier:" "allerdings keinen Zugriff auf wichtige Einstellungen bekommen, wie Privaten "
msgstr "ID:" "Code zu ändern oder gar das ganze Projekt zu löschen."
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr "QR-Code scannen"
msgid "Use a mobile device with a compatible app." msgid "Use a mobile device with a compatible app."
msgstr "" msgstr "Benutzen sie ein Smartphone mit Kompatibler App."
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Per E-Mail versenden" msgstr "Per E-Mail versenden"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du " "Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du über "
"über die\n" "die Erstellung dieses Projekts informieren möchtest, und wir werden ihnen "
"\t\t\tErstellung dieses Projekts informieren möchtest, und wir senden " "eine E-Mail senden."
"ihnen eine E-Mail."
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?" msgid "Who pays?"
msgstr "Wer zahlt?" msgstr "Wer zahlt?"
@ -1145,3 +1164,23 @@ msgstr "Zeitraum"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Projekt bearbeiten" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-08-01 08:34+0000\n"
"Last-Translator: Eugenia Russell <eugenia.russell2019@gmail.com>\n" "Last-Translator: Eugenia Russell <eugenia.russell2019@gmail.com>\n"
"Language: el\n" "Language: el\n"
@ -27,6 +27,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Τίτλος εργασίας" msgstr "Τίτλος εργασίας"
#, fuzzy
msgid "Current private code"
msgstr "Ιδιωτικός κωδικός"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy #, fuzzy
msgid "New private code" msgid "New private code"
msgstr "Ιδιωτικός κωδικός" msgstr "Ιδιωτικός κωδικός"
@ -49,6 +56,14 @@ msgstr "Προεπιλεγμένο Νόμισμα"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "Άγνωστο πρότζεκτ"
#, fuzzy
msgid "Invalid private code."
msgstr "Ιδιωτικός κωδικός"
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -89,14 +104,6 @@ msgstr ""
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Εισαγάγετε ιδιωτικό κωδικό για επιβεβαίωση της διαγραφής" msgstr "Εισαγάγετε ιδιωτικό κωδικό για επιβεβαίωση της διαγραφής"
#, fuzzy
msgid "Unknown error"
msgstr "Άγνωστο πρότζεκτ"
#, fuzzy
msgid "Invalid private code."
msgstr "Ιδιωτικός κωδικός"
msgid "Get in" msgid "Get in"
msgstr "Συνδεθείτε" msgstr "Συνδεθείτε"
@ -269,6 +276,9 @@ msgstr "Άγνωστο πρότζεκτ"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Επαναφορά του κωδικού επιτυχώς." msgstr "Επαναφορά του κωδικού επιτυχώς."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -776,6 +786,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -798,7 +811,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
#, fuzzy #, fuzzy
@ -853,14 +866,12 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" #, fuzzy
msgstr "" msgid "Add the first participant"
msgstr "Προσθήκη συμμετέχοντος"
msgid "add participants"
msgstr ""
msgid "Password reminder" msgid "Password reminder"
msgstr "" msgstr ""
@ -882,21 +893,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -909,10 +913,29 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -1097,3 +1120,43 @@ msgstr "Περίοδος"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Επεξεργαστείτε το έργο" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-10-01 20:35+0000\n"
"Last-Translator: phlostically <phlostically@mailinator.com>\n" "Last-Translator: phlostically <phlostically@mailinator.com>\n"
"Language: eo\n" "Language: eo\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nomo de projekto" 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" msgid "New private code"
msgstr "Nova privata kodo" msgstr "Nova privata kodo"
@ -50,6 +57,12 @@ msgstr "Implicita valuto"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr "Nekonata eraro"
msgid "Invalid private code."
msgstr "Nevalida privata kodo."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -90,12 +103,6 @@ msgstr ""
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "" msgstr ""
msgid "Unknown error"
msgstr "Nekonata eraro"
msgid "Invalid private code."
msgstr "Nevalida privata kodo."
msgid "Get in" msgid "Get in"
msgstr "Eniri" msgstr "Eniri"
@ -270,6 +277,9 @@ msgstr "Nekonata projekto"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Pasvorto sukcese restarigita." msgstr "Pasvorto sukcese restarigita."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -786,6 +796,9 @@ msgstr "Historio"
msgid "Settings" msgid "Settings"
msgstr "Agordoj" msgstr "Agordoj"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "Aliaj projektoj:" msgstr "Aliaj projektoj:"
@ -808,7 +821,7 @@ msgstr "Poŝaparata programo"
msgid "Documentation" msgid "Documentation"
msgstr "Dokumentaro" msgstr "Dokumentaro"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Administra panelo" msgstr "Administra panelo"
#, fuzzy #, fuzzy
@ -863,14 +876,13 @@ msgstr "Neniu fakturo"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Nenio listigebla ankoraŭ." msgstr "Nenio listigebla ankoraŭ."
msgid "You probably want to" #, fuzzy
msgstr "Vi probable volas" msgid "Add your first bill"
msgid "add a bill"
msgstr "aldoni fakturon" msgstr "aldoni fakturon"
msgid "add participants" #, fuzzy
msgstr "aldoni partoprenantojn" msgid "Add the first participant"
msgstr "Aldono partoprenanton"
msgid "Password reminder" msgid "Password reminder"
msgstr "Rememorigilo pri pasvorto" msgstr "Rememorigilo pri pasvorto"
@ -894,22 +906,15 @@ msgstr "Restarigi vian pasvorton"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Inviti homojn aliĝi al ĉi tiu projekto" msgstr "Inviti homojn aliĝi al ĉi tiu projekto"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Konigi identigilon kaj kodon" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "Vi povas iel ajn konigi la projektan identigilon kaj la privatan kodon." " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:" msgstr ""
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr ""
@ -920,17 +925,37 @@ msgstr ""
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Sendi retpoŝte" msgstr "Sendi retpoŝte"
#, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Specifu (kome apartigitan) liston de tiuj retpoŝtaj adresoj, kiujn vi " "Specifu (kome apartigitan) liston de tiuj retpoŝtaj adresoj, kiujn vi "
"volas sciigi pri la\n" "volas sciigi pri la\n"
" kreado de ĉi tiu buĝet-administra projekto, kaj ni sendos" " kreado de ĉi tiu buĝet-administra projekto, kaj ni sendos"
" al ili retpoŝtajn mesaĝojn por vi." " 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?" msgid "Who pays?"
msgstr "Kiu pagas?" msgstr "Kiu pagas?"
@ -1096,3 +1121,21 @@ msgstr "Periodo"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Redakti la projekton" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2023-11-27 05:02+0000\n"
"Last-Translator: Santiago José Gutiérrez Llanos " "Last-Translator: Wilfredo Gomez <thepageguy@mailfence.com>\n"
"<gutierrezapata17@gmail.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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -30,11 +29,17 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nombre del Proyecto" 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" 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" 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" msgid "Email"
msgstr "Correo Electrónico" msgstr "Correo Electrónico"
@ -43,25 +48,31 @@ msgid "Enable project history"
msgstr "Habilitar historial del proyecto" msgstr "Habilitar historial del proyecto"
msgid "Use IP tracking for project history" 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" msgid "Default Currency"
msgstr "Moneda por defecto" msgstr "Moneda por defecto"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
"Establecer una moneda predeterminada permite la conversión de divisas " "Establecer una moneda predeterminada permite la conversión de moneda entre "
"entre facturas" "billetes"
msgid "Unknown error"
msgstr "Error desconocido"
msgid "Invalid private code."
msgstr "Código privado no válido."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
msgstr "" msgstr ""
"Este proyecto no se puede establecer en 'ninguna moneda' porque contiene " "Este proyecto no se puede configurar como \"sin moneda\" porque contiene "
"facturas en varias monedas." "billetes en varias monedas."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "Compatible con Cospend"
msgid "Project identifier" msgid "Project identifier"
msgstr "Identificador de proyecto" msgstr "Identificador de proyecto"
@ -81,22 +92,16 @@ msgstr ""
"favor, elija un nuevo identificador" "favor, elija un nuevo identificador"
msgid "Which is a real currency: Euro or Petro dollar?" 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" msgid "euro"
msgstr "Euro" msgstr "euro"
msgid "Please, validate the captcha to proceed." 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" msgid "Enter private code to confirm deletion"
msgstr "Introduzca el código privado para confirmar la eliminación" msgstr "Ingrese el código privado para confirmar la eliminación"
msgid "Unknown error"
msgstr "Error desconocido"
msgid "Invalid private code."
msgstr "Código privado inválido."
msgid "Get in" msgid "Get in"
msgstr "Entrar" msgstr "Entrar"
@ -117,7 +122,7 @@ msgid "Password"
msgstr "Contraseña" msgstr "Contraseña"
msgid "Password confirmation" msgid "Password confirmation"
msgstr "Confirmar contraseña" msgstr "confirmación de contraseña"
msgid "Reset password" msgid "Reset password"
msgstr "Restablecer contraseña" msgstr "Restablecer contraseña"
@ -154,7 +159,7 @@ msgstr "Enviar y agregar uno nuevo"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
msgstr "moneda predeterminada del projecto: %(currency)s" msgstr "Projecto por defecto: %(currency)s"
msgid "Name" msgid "Name"
msgstr "Nombre" msgstr "Nombre"
@ -189,15 +194,15 @@ msgstr "Cerrar sesión"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr ""
"Por favor verifique la configuración de correo electrónico del servidor."
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"Lo sentimos, hubo un error cuando intentamos enviarle correos de " "Verifique la configuración de correo electrónico del servidor o comuníquese "
"invitación. Por favor, revise la configuración de correo en el servidor o" "con el administrador: %(admin_email)s"
" contactese con el administrador."
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -216,53 +221,47 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}" msgstr "{start_object}, {next_object}"
msgid "No Currency" msgid "No Currency"
msgstr "no moneda" msgstr "No Moneda"
#. Form error with only one error #. Form error with only one error
msgid "{prefix}: {error}" msgid "{prefix}: {error}"
msgstr "{prefijo}: {error}" msgstr "{prefix}: {error}"
#. Form error with a list of errors #. Form error with a list of errors
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "{prefijo}:<br />{errores}" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." msgid "Too many failed login attempts."
msgstr "" msgstr "Demasiados intentos fallidos de inicio de sesión."
"Demasiados intentos fallidos de inicio de sesión, vuelva a intentarlo más"
" tarde."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "" msgstr ""
"Esta contraseña de administrador no es la correcta. Solo quedan %(num)d " "Esta contraseña de administrador no es la correcta. Solo quedan %(num)d "
"intentos." "intentos restantes."
msgid "Provided token is invalid" 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" msgid "This private code is not the right one"
msgstr "Este código privado no es el correcto" msgstr "Este código privado no es el correcto"
msgid "A reminder email has just been sent to you" 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 "" msgid ""
"We tried to send you an reminder email, but there was an error. You can " "We tried to send you an reminder email, but there was an error. You can "
"still use the project normally." "still use the project normally."
msgstr "" msgstr ""
"Te hemos intentado enviar un correo electrónico recordatorio pero ha " "Intentamos enviarte un correo electrónico de recordatorio, pero hubo un "
"habido un error. Todavía puedes usar el proyecto habitualmente." "error. Aún puedes usar el proyecto normalmente."
#, fuzzy
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions." "instructions."
msgstr "" msgstr ""
"Lo sentimos, hubo un error al enviarle un correo electrónico con las " "Lo sentimos, hubo un error al enviarle un correo electrónico con "
"instrucciones de restablecimiento de contraseña. Compruebe la " "instrucciones para restablecer la contraseña."
"configuración de correo electrónico del servidor o póngase en contacto "
"con el administrador."
msgid "No token provided" msgid "No token provided"
msgstr "No se proporciono ningún token" msgstr "No se proporciono ningún token"
@ -276,31 +275,34 @@ msgstr "Proyecto desconocido"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Contraseña restablecida con éxito." 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" msgid "Unable to parse CSV"
msgstr "" msgstr "No se puede analizar CSV"
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "Atributo faltante: %(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "Cannot add bills in multiple currencies to a project without default "
"currency" "currency"
msgstr "" msgstr ""
"No se pueden agregar facturas en varias monedas a un proyecto sin la " "No se pueden agregar billetes en varias monedas a un proyecto sin moneda "
"moneda predeterminada" "predeterminada"
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "El proyecto se subió exitosamente" msgstr "Proyecto cargado exitosamente"
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "Proyecto eliminado correctamente" msgstr "Proyecto eliminado correctamente"
msgid "Error deleting project" msgid "Error deleting project"
msgstr "Error al borrar poryecto" msgstr "Error al borrar proyecto"
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "No se puede cerrar sesión"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" 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" msgid "Your invitations have been sent"
msgstr "Sus invitaciones han sido enviadas" msgstr "Sus invitaciones han sido enviadas"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails." msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr "" msgstr ""
"Lo sentimos, hubo un error cuando intentamos enviarle correos de " "Lo sentimos, hubo un error al intentar enviar los correos electrónicos de "
"invitación. Por favor, revise la configuración de correo en el servidor o" "invitación."
" contactese con el administrador."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
msgstr "Se añadieron %(member)s" msgstr "%(member)s ha sido añadido"
msgid "Error activating participant" msgid "Error activating participant"
msgstr "Error activando participante" msgstr "Error al activar el participante"
#, python-format #, python-format
msgid "%(name)s is part of this project again" 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" msgid "Error removing participant"
msgstr "Error eliminando participante" msgstr "Error al eliminar el participante"
#, python-format #, python-format
msgid "" msgid ""
@ -350,7 +350,7 @@ msgid "The bill has been added"
msgstr "La factura ha sido agregada" msgstr "La factura ha sido agregada"
msgid "Error deleting bill" msgid "Error deleting bill"
msgstr "Error eliminando factura" msgstr "Error al eliminar la factura"
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "La factura ha sido eliminada" msgstr "La factura ha sido eliminada"
@ -360,7 +360,7 @@ msgstr "La factura ha sido modificada"
#, python-format #, python-format
msgid "%(lang)s is not a supported language" msgid "%(lang)s is not a supported language"
msgstr "" msgstr "%(lang)s no es un idioma admitido"
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "Error al eliminar el historial del proyecto" msgstr "Error al eliminar el historial del proyecto"
@ -440,9 +440,8 @@ msgstr "Conseguir en"
msgid "Edit project" msgid "Edit project"
msgstr "Editar proyecto" msgstr "Editar proyecto"
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "Editar proyecto" msgstr "Importar proyecto"
msgid "Download project's data" msgid "Download project's data"
msgstr "Descargar datos del proyecto" msgstr "Descargar datos del proyecto"
@ -471,14 +470,13 @@ msgid "Privacy Settings"
msgstr "Ajustes de privacidad" msgstr "Ajustes de privacidad"
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr "Guardar cambios"
msgid "This will remove all bills and participants in this project!" msgid "This will remove all bills and participants in this project!"
msgstr "Esto va a remover todas las facturas y participantes en este proyecto!" msgstr "Esto va a remover todas las facturas y participantes en este proyecto!"
#, fuzzy
msgid "Import previously exported project" msgid "Import previously exported project"
msgstr "Importar archivo JSON previamente exportado" msgstr "Importar proyecto previamente exportado"
msgid "Choose file" msgid "Choose file"
msgstr "Escoger un archivo" msgstr "Escoger un archivo"
@ -490,7 +488,7 @@ msgid "Add a bill"
msgstr "Agregar una factura" msgstr "Agregar una factura"
msgid "Simple operations are allowed, e.g. (18+36.2)/3" 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" msgid "Everyone"
msgstr "Todos" msgstr "Todos"
@ -514,13 +512,13 @@ msgid "Download"
msgstr "Descargar" msgstr "Descargar"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "Historial de proyecto activo" msgstr "Historial de proyectos deshabilitado"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "Historial de proyecto y registros de dirección IP inactivos" msgstr "Historial de proyecto y registros de dirección IP inactivos"
msgid "Enabled Project History" msgid "Enabled Project History"
msgstr "Historial de proyecto activo" msgstr "Historial de proyectos habilitado"
msgid "Disabled IP Address Recording" msgid "Disabled IP Address Recording"
msgstr "Registro de direcciones IP activo" 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." msgid "This project has history disabled. New actions won't appear below."
msgstr "" 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." 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 "" msgid ""
"The table below reflects actions recorded prior to disabling project " "The table below reflects actions recorded prior to disabling project "
"history." "history."
msgstr "" 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." 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 "" msgid ""
"Some entries below contain IP addresses, even though this project has IP " "Some entries below contain IP addresses, even though this project has IP "
@ -790,6 +790,9 @@ msgstr "Historial"
msgid "Settings" msgid "Settings"
msgstr "Configuración" msgstr "Configuración"
msgid "RSS Feed"
msgstr "rss Feed"
msgid "Other projects :" msgid "Other projects :"
msgstr "Otros proyectos :" msgstr "Otros proyectos :"
@ -801,7 +804,7 @@ msgstr "Tablero"
#, python-format #, python-format
msgid "Please retry after %(date)s." msgid "Please retry after %(date)s."
msgstr "" msgstr "Vuelva a intentarlo después de %(date)s."
msgid "Code" msgid "Code"
msgstr "Código" msgstr "Código"
@ -812,7 +815,7 @@ msgstr "Aplicación móvil"
msgid "Documentation" msgid "Documentation"
msgstr "Documentación" msgstr "Documentación"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Panel de administración" msgstr "Panel de administración"
msgid "Legal information" msgid "Legal information"
@ -866,14 +869,11 @@ msgstr "Sin facturas"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Aún no hay nada que listar." msgstr "Aún no hay nada que listar."
msgid "You probably want to" msgid "Add your first bill"
msgstr "Probablemente quieras" msgstr "Añade tu primera factura"
msgid "add a bill" msgid "Add the first participant"
msgstr "agregar una factura" msgstr "Agregar el primer participante"
msgid "add participants"
msgstr "agregar participantes"
msgid "Password reminder" msgid "Password reminder"
msgstr "Recordar contraseña" msgstr "Recordar contraseña"
@ -897,46 +897,62 @@ msgstr "Restablecer su contraseña"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Invita a personas a unirse a este proyecto" msgstr "Invita a personas a unirse a este proyecto"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Compartir identificador y código" msgstr "Compartir un enlace de invitación"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" msgstr ""
"Puede compartir el identificador del proyecto y el código privado por " "La forma más sencilla de invitar personas es dándoles el siguiente enlace de "
"cualquier medio de comunicación." "invitación.<br />Podrán acceder al proyecto, administrar participantes, "
"agregar/editar/eliminar facturas. Sin embargo, no tendrán acceso a "
msgid "Identifier:" "configuraciones importantes como cambiar el código privado o eliminar todo "
msgstr "Identificador:" "el proyecto."
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr "Escanear código QR"
msgid "Use a mobile device with a compatible app." 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" msgid "Send via Emails"
msgstr "Enviar por correo electrónico" msgstr "Enviar por correo electrónico"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Especifique una lista (separada por comas) de las direcciones de correo " "Especifique una lista de direcciones de correo electrónico (separadas por "
"electrónico a las que desea notificar acerca de la\n" "comas) de las personas a las que desea notificar sobre la creación de este "
"creación de este proyecto de gestión presupuestaria y les enviaremos un " "proyecto. Les enviaremos un correo electrónico con el enlace de invitación."
"correo electrónico para usted."
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?" msgid "Who pays?"
msgstr "¿Quién paga?" msgstr "¿Quién paga?"
@ -1126,3 +1142,26 @@ msgstr "Período"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Editar el proyecto" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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: 2023-03-19 21:40+0000\n" "PO-Revision-Date: 2024-05-23 03:01+0000\n"
"Last-Translator: Sai Mohammad-Hossein Emami <emami@outlook.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -27,6 +27,13 @@ msgstr "مقدار یا عبارت نامعتبر. فقط اعداد و عملی
msgid "Project name" msgid "Project name"
msgstr "نام پروژه" msgstr "نام پروژه"
#, fuzzy
msgid "Current private code"
msgstr "کد خصوصی جدید"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code" msgid "New private code"
msgstr "کد خصوصی جدید" msgstr "کد خصوصی جدید"
@ -48,6 +55,12 @@ msgstr "واحد پولی پیش فرض"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "تنظیم واحد پولی پیش فرض امکان تبدیل ارز بین قبض‌ها رو فراهم می‌کنه" msgstr "تنظیم واحد پولی پیش فرض امکان تبدیل ارز بین قبض‌ها رو فراهم می‌کنه"
msgid "Unknown error"
msgstr "خطای ناشناخته"
msgid "Invalid private code."
msgstr "کد خصوصی نامعتبر."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -87,12 +100,6 @@ msgstr "لطفا برای ادامه کپچا رو تایید کن."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "کد خصوصی رو برای تایید حذف وارد کن" msgstr "کد خصوصی رو برای تایید حذف وارد کن"
msgid "Unknown error"
msgstr "خطای ناشناخته"
msgid "Invalid private code."
msgstr "کد خصوصی نامعتبر."
msgid "Get in" msgid "Get in"
msgstr "بیا تو" msgstr "بیا تو"
@ -118,16 +125,16 @@ msgid "Reset password"
msgstr "بازنشانی گذرواژه" msgstr "بازنشانی گذرواژه"
msgid "When?" msgid "When?"
msgstr "" msgstr "چه زمانی؟"
msgid "What?" msgid "What?"
msgstr "چی؟" msgstr "چی؟"
msgid "Who paid?" msgid "Who paid?"
msgstr "" msgstr "چه کسی پرداخت کرد؟"
msgid "How much?" msgid "How much?"
msgstr "" msgstr "چقدر؟"
msgid "Currency" msgid "Currency"
msgstr "واحد پولی" msgstr "واحد پولی"
@ -173,14 +180,14 @@ msgid "People to notify"
msgstr "افرادی که براشون نوتیفیکیشن ارسال میشه" msgstr "افرادی که براشون نوتیفیکیشن ارسال میشه"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "" msgstr "ارسال دعوت نامه ها"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "ایمیل %(email)s نامعتبره" msgstr "ایمیل %(email)s نامعتبره"
msgid "Logout" msgid "Logout"
msgstr "" msgstr "خروج"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr ""
@ -256,6 +263,9 @@ msgstr ""
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -747,6 +757,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -769,7 +782,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -823,13 +836,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -852,21 +862,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -879,10 +882,29 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -1031,3 +1053,42 @@ msgstr ""
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-07-14 10:01+0200\n" "POT-Creation-Date: 2023-07-29 14:24+0200\n"
"PO-Revision-Date: 2023-07-14 07:59+0000\n" "PO-Revision-Date: 2023-07-29 13:07+0000\n"
"Last-Translator: Baptiste <weblate@bitsofnetworks.org>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -33,6 +34,12 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nom de projet" 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" msgid "New private code"
msgstr "Nouveau code daccès" msgstr "Nouveau code daccès"
@ -56,6 +63,12 @@ msgstr ""
"Choisir une devise par défaut permet d'activer la conversion de devises " "Choisir une devise par défaut permet d'activer la conversion de devises "
"entre les factures" "entre les factures"
msgid "Unknown error"
msgstr "Erreur inconnue"
msgid "Invalid private code."
msgstr "Code daccès invalide."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -95,12 +108,6 @@ msgstr "Merci de valider le captcha avant de continuer."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Entrez le code d'accès pour confirmer la suppression" 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" msgid "Get in"
msgstr "Entrer" msgstr "Entrer"
@ -273,6 +280,9 @@ msgstr "Projet inconnu"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès." 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" msgid "Unable to parse CSV"
msgstr "Erreur lors de la lecture du fichier CSV" msgstr "Erreur lors de la lecture du fichier CSV"
@ -461,7 +471,7 @@ msgid "Privacy Settings"
msgstr "Vie privée" msgstr "Vie privée"
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr "Sauvegarder les modifications"
msgid "This will remove all bills and participants in this project!" msgid "This will remove all bills and participants in this project!"
msgstr "Cela supprimera toutes les factures et participant⋅es du projet !" msgstr "Cela supprimera toutes les factures et participant⋅es du projet !"
@ -789,6 +799,9 @@ msgstr "Historique"
msgid "Settings" msgid "Settings"
msgstr "Options" msgstr "Options"
msgid "RSS Feed"
msgstr "Flux RSS"
msgid "Other projects :" msgid "Other projects :"
msgstr "Autres projets :" msgstr "Autres projets :"
@ -811,7 +824,7 @@ msgstr "Application mobile"
msgid "Documentation" msgid "Documentation"
msgstr "Documentation" msgstr "Documentation"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Panneau d'administration" msgstr "Panneau d'administration"
msgid "Legal information" msgid "Legal information"
@ -865,14 +878,11 @@ msgstr "Pas encore de factures"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Rien à lister pour le moment." msgstr "Rien à lister pour le moment."
msgid "You probably want to" msgid "Add your first bill"
msgstr "Vous souhaitez sûrement" msgstr "Ajouter votre première facture"
msgid "add a bill" msgid "Add the first participant"
msgstr "ajouter une facture" msgstr "Ajouter le premier participant ou la première participante"
msgid "add participants"
msgstr "ajouter des participant⋅es"
msgid "Password reminder" msgid "Password reminder"
msgstr "Rappel du code daccès" msgstr "Rappel du code daccès"
@ -896,24 +906,20 @@ msgstr "Changez votre code d'accès"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Invitez des personnes à rejoindre ce projet" msgstr "Invitez des personnes à rejoindre ce projet"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Partager l'identifiant et le code" msgstr "Partager un lien d'invitation"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" msgstr ""
"Vous pouvez partager l'identifiant de ce projet et le code d'accès par " "Pour inviter des personnes dans ce projet, vous pouvez leur donner le "
"d'autres moyens." "lien d'invitation ci-dessous.<br />Elles pourront ainsi accéder au "
"projet, gérer les participants, ajouter/modifier/supprimer des factures. "
msgid "Identifier:" "En revanche, elles ne pourront pas modifier des paramètres importants "
msgstr "Identifiant :" "tels que le code d'accès ou supprimer le projet."
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "Scannez le QR code" msgstr "Scannez le QR code"
@ -925,13 +931,37 @@ msgid "Send via Emails"
msgstr "Envoyer par email(s)" msgstr "Envoyer par email(s)"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Entrez les emails des personnes avec qui vous souhaitez partager ce " "Entrez les emails des personnes avec qui vous souhaitez partager ce projet ("
"projet, nous leur enverrons un lien d'invitation." "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?" msgid "Who pays?"
msgstr "Qui doit payer ?" msgstr "Qui doit payer ?"
@ -1338,3 +1368,23 @@ msgstr "Période"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Éditer le projet" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-11-07 10:07+0000\n" "PO-Revision-Date: 2023-07-24 07:07+0000\n"
"Last-Translator: Raanan Katz <raakatz97@gmail.com>\n" "Last-Translator: Nati Lintzer <nlintzer@gmail.com>\n"
"Language: he\n" "Language: he\n"
"Language-Team: Hebrew <https://hosted.weblate.org/projects/i-hate-money/i" "Language-Team: Hebrew <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/he/>\n" "-hate-money/he/>\n"
@ -28,6 +28,13 @@ msgstr "כמות או ביטוי לא תקין. יש להזין רק מספרי
msgid "Project name" msgid "Project name"
msgstr "שם הפרויקט" msgstr "שם הפרויקט"
#, fuzzy
msgid "Current private code"
msgstr "קוד פרטי חדש"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code" msgid "New private code"
msgstr "קוד פרטי חדש" msgstr "קוד פרטי חדש"
@ -49,6 +56,12 @@ msgstr "מטבע ברירת המחדל"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "בחירת מטבע ברירת מחדל מאפשרת המרת מטבע בין חשבונות" msgstr "בחירת מטבע ברירת מחדל מאפשרת המרת מטבע בין חשבונות"
msgid "Unknown error"
msgstr "שגיאה לא ידועה"
msgid "Invalid private code."
msgstr "קוד פרטי לא תקין."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -86,12 +99,6 @@ msgstr "אנא אמת את ה-Captcha כדי להמשיך."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "הזן את הקוד הפרטי כדי לאשר מחיקה" msgstr "הזן את הקוד הפרטי כדי לאשר מחיקה"
msgid "Unknown error"
msgstr "שגיאה לא ידועה"
msgid "Invalid private code."
msgstr "קוד פרטי לא תקין."
msgid "Get in" msgid "Get in"
msgstr "היכנס" msgstr "היכנס"
@ -172,25 +179,25 @@ msgid "People to notify"
msgstr "אנשים להתריע להם" msgstr "אנשים להתריע להם"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "" msgstr "שלח את ההזמנות"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "כתובת הדוא\"ל %(email)s אינה תקינה" msgstr "כתובת הדוא\"ל %(email)s אינה תקינה"
msgid "Logout" msgid "Logout"
msgstr "" msgstr "להתנתק"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr ""
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"מצטערים, אך אירעה שגיאה בעת ששלחנו לכם את המייל עם הוראות איפוס הסיסמה. " "מצטערים, אך אירעה שגיאה בעת ששלחנו לכם את המייל עם הוראות איפוס הסיסמה. "
"בבקשה תבדקו את הגדרות המייל בשרת או פנו למנהל השרת." "אנא בדקו את הגדרות המייל בשרת או פנו למנהל השרת:%(admin_email)s"
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -219,9 +226,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." msgid "Too many failed login attempts."
msgstr "יותר מדי ניסיון התחברות כושלים, אנא נסו שם מאוחר יותר." msgstr "יותר מדי ניסיון התחברות כושלים."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
@ -263,12 +269,15 @@ msgstr "פרויקט לא ידוע"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "הסיסמה אופסה בהצלחה." msgstr "הסיסמה אופסה בהצלחה."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "תכונה חסרה:%(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "Cannot add bills in multiple currencies to a project without default "
@ -285,7 +294,7 @@ msgid "Error deleting project"
msgstr "שגיאה במחיקת הפרויקט" msgstr "שגיאה במחיקת הפרויקט"
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "לא ניתן להתנתק"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
@ -295,7 +304,7 @@ msgid "Your invitations have been sent"
msgstr "ההזמנות שלך נשלחו" msgstr "ההזמנות שלך נשלחו"
msgid "Sorry, there was an error while trying to send the invitation emails." msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr "" msgstr "מצטערים, אך אירעה שגיאה בעת שליחת ההזמנות."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
@ -326,20 +335,21 @@ msgid "Participant '%(name)s' has been modified"
msgstr "" msgstr ""
msgid "The bill has been added" msgid "The bill has been added"
msgstr "" msgstr "החשבון נוסף"
msgid "Error deleting bill" msgid "Error deleting bill"
msgstr "" msgstr "שגיאה בעת מחיקת החשבון"
#, fuzzy
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "" msgstr "ההוצאה נמחקה"
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "" msgstr ""
#, python-format #, python-format
msgid "%(lang)s is not a supported language" msgid "%(lang)s is not a supported language"
msgstr "" msgstr "%(lang)s אינה שפה נתמכת"
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "שגיאה במחיקת הסטוריית הפרויקט" msgstr "שגיאה במחיקת הסטוריית הפרויקט"
@ -354,7 +364,7 @@ msgid "Deleted recorded IP addresses in project history."
msgstr "" msgstr ""
msgid "Sorry, we were unable to find the page you've asked for." 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." msgid "The best thing to do is probably to get back to the main page."
msgstr "כנראה שהדבר הטוב ביותר לעשות הוא לחזור לעמוד הראשי." msgstr "כנראה שהדבר הטוב ביותר לעשות הוא לחזור לעמוד הראשי."
@ -378,7 +388,7 @@ msgid "?"
msgstr "?" msgstr "?"
msgid "Create a new project" msgid "Create a new project"
msgstr "צור פרויקט" msgstr "צור פרויקט חדש"
msgid "Project" msgid "Project"
msgstr "פרויקט" msgstr "פרויקט"
@ -399,7 +409,7 @@ msgid "Actions"
msgstr "פעולות" msgstr "פעולות"
msgid "edit" msgid "edit"
msgstr "" msgstr "ערוך"
msgid "Delete project" msgid "Delete project"
msgstr "מחק פרויקט" msgstr "מחק פרויקט"
@ -419,9 +429,8 @@ msgstr ""
msgid "Edit project" msgid "Edit project"
msgstr "" msgstr ""
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "הפרויקטים שלך" msgstr "ייבא פרוייקטים"
msgid "Download project's data" msgid "Download project's data"
msgstr "" msgstr ""
@ -453,45 +462,44 @@ msgstr ""
msgid "This will remove all bills and participants in this project!" msgid "This will remove all bills and participants in this project!"
msgstr "" msgstr ""
#, fuzzy
msgid "Import previously exported project" msgid "Import previously exported project"
msgstr "ייבא קובץ JSON מיוצא" msgstr "ייבא פרוייקט קודם מיוצא"
msgid "Choose file" msgid "Choose file"
msgstr "בחר קובץ" msgstr "בחר קובץ"
msgid "Edit this bill" msgid "Edit this bill"
msgstr "" msgstr "ערוך את החשבון"
msgid "Add a bill" msgid "Add a bill"
msgstr "" msgstr "הוסף חשבון"
msgid "Simple operations are allowed, e.g. (18+36.2)/3" msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr "" msgstr ""
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr "כולם"
msgid "No one" msgid "No one"
msgstr "" msgstr "אף אחד"
msgid "More options" msgid "More options"
msgstr "" msgstr "יותר אפשרויות"
msgid "Add participant" msgid "Add participant"
msgstr "" msgstr "הוסף משתתפים"
msgid "Edit this participant" msgid "Edit this participant"
msgstr "" msgstr "ערוך את המשתתף"
msgid "john.doe@example.com, mary.moe@site.com" msgid "john.doe@example.com, mary.moe@site.com"
msgstr "" msgstr "john.doe@example.com, mary.moe@site.com"
msgid "Download" msgid "Download"
msgstr "" msgstr "הורד"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "" msgstr "היסטורית פרויקט מושבתת"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "" msgstr ""
@ -747,7 +755,7 @@ msgid "Projects"
msgstr "פרויקטים" msgstr "פרויקטים"
msgid "Start a new project" msgid "Start a new project"
msgstr "" msgstr "התחל פרוייקט חדש"
msgid "History" msgid "History"
msgstr "הסטוריה" msgstr "הסטוריה"
@ -755,11 +763,14 @@ msgstr "הסטוריה"
msgid "Settings" msgid "Settings"
msgstr "הגדרות" msgstr "הגדרות"
msgid "Other projects :" msgid "RSS Feed"
msgstr "" msgstr ""
msgid "Other projects :"
msgstr "פרויקטים אחרים:"
msgid "switch to" msgid "switch to"
msgstr "" msgstr "שנה ל"
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
@ -772,12 +783,12 @@ msgid "Code"
msgstr "קוד" msgstr "קוד"
msgid "Mobile Application" msgid "Mobile Application"
msgstr "" msgstr "יישום לנייד"
msgid "Documentation" msgid "Documentation"
msgstr "דוקומנטציה" msgstr "דוקומנטציה"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -787,29 +798,29 @@ msgid "\"I hate money\" is free software"
msgstr "\"אני שונא כסף\" היא תוכנה חינמית" msgstr "\"אני שונא כסף\" היא תוכנה חינמית"
msgid "you can contribute and improve it!" msgid "you can contribute and improve it!"
msgstr "" msgstr "אתה יכול לתרום ולשפר אותו!"
#, python-format #, python-format
msgid "%(amount)s each" msgid "%(amount)s each"
msgstr "" msgstr ""
msgid "you sure?" msgid "you sure?"
msgstr "" msgstr "האם אתה בטוח?"
msgid "Invite people" msgid "Invite people"
msgstr "" msgstr "הזמן אנשים"
msgid "Newer bills" msgid "Newer bills"
msgstr "" msgstr ""
msgid "Older bills" msgid "Older bills"
msgstr "" msgstr "הוצאות ישנות"
msgid "You should start by adding participants" msgid "You should start by adding participants"
msgstr "" msgstr "כדאי להתחיל בהוספת משתמשים"
msgid "Add a new bill" msgid "Add a new bill"
msgstr "" msgstr "הוסף הוצאה חדשה"
msgid "For what?" msgid "For what?"
msgstr "עבור מה?" msgstr "עבור מה?"
@ -823,22 +834,20 @@ msgid "Everyone but %(excluded)s"
msgstr "" msgstr ""
msgid "delete" msgid "delete"
msgstr "" msgstr "מחק"
msgid "No bills" msgid "No bills"
msgstr "" msgstr "אין הוצאות"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" #, fuzzy
msgstr "" msgid "Add the first participant"
msgstr "ערוך את המשתתף"
msgid "add participants"
msgstr ""
msgid "Password reminder" msgid "Password reminder"
msgstr "תזכורת סיסמה" msgstr "תזכורת סיסמה"
@ -846,7 +855,7 @@ msgstr "תזכורת סיסמה"
msgid "" msgid ""
"A link to reset your password has been sent to you, please check your " "A link to reset your password has been sent to you, please check your "
"emails." "emails."
msgstr "" msgstr "נשלח לחשבונך לינק לאיפוס הסיסמה, אנא בדוק את תיבת הדוא\"ל שלך."
msgid "Return to home page" msgid "Return to home page"
msgstr "חזור לעמוד הבית" msgstr "חזור לעמוד הבית"
@ -855,42 +864,54 @@ msgid "Your projects"
msgstr "הפרויקטים שלך" msgstr "הפרויקטים שלך"
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr "אפס את סיסמתך"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr "הזמן אנשים להצטרף לפרויקט"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" 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 "" msgid ""
"You can share the project identifier and the private code by any " "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 "" msgstr ""
msgid "Identifier:" msgid "Identifier:"
msgstr "מזהה:" msgstr "מזהה:"
msgid "Share the Link" #, fuzzy
msgstr "" msgid "Private code:"
msgstr "קוד פרטי"
msgid "You can directly share the following link via your prefered medium" msgid "the private code was defined when you created the 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 (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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -903,7 +924,7 @@ msgid "Who?"
msgstr "מי?" msgstr "מי?"
msgid "Balance" msgid "Balance"
msgstr "" msgstr "מאזן"
msgid "deactivate" msgid "deactivate"
msgstr "" msgstr ""
@ -918,7 +939,7 @@ msgid "Spent"
msgstr "" msgstr ""
msgid "Expenses by Month" msgid "Expenses by Month"
msgstr "" msgstr "הוצאות לפי חודש"
msgid "Period" msgid "Period"
msgstr "תקופה" msgstr "תקופה"
@ -983,3 +1004,43 @@ msgstr "תקופה"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2020-06-14 14:41+0000\n"
"Last-Translator: raghupalash <singhpalash0@gmail.com>\n" "Last-Translator: raghupalash <singhpalash0@gmail.com>\n"
"Language: hi\n" "Language: hi\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "परियोजना का नाम" msgstr "परियोजना का नाम"
#, fuzzy
msgid "Current private code"
msgstr "निजी कोड"
msgid "Enter existing private code to edit project"
msgstr ""
#, fuzzy #, fuzzy
msgid "New private code" msgid "New private code"
msgstr "निजी कोड" msgstr "निजी कोड"
@ -51,6 +58,14 @@ msgstr "डिफ़ॉल्ट मुद्रा"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "अज्ञात परियोजना"
#, fuzzy
msgid "Invalid private code."
msgstr "निजी कोड"
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -89,14 +104,6 @@ msgstr ""
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "" msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "अज्ञात परियोजना"
#, fuzzy
msgid "Invalid private code."
msgstr "निजी कोड"
msgid "Get in" msgid "Get in"
msgstr "अंदर जाइये" msgstr "अंदर जाइये"
@ -271,6 +278,9 @@ msgstr "अज्ञात परियोजना"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "पासवर्ड सफलतापूर्वक रीसेट हो गया है।" msgstr "पासवर्ड सफलतापूर्वक रीसेट हो गया है।"
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -794,6 +804,9 @@ msgstr "इतिहास"
msgid "Settings" msgid "Settings"
msgstr "सेटिंग्स" msgstr "सेटिंग्स"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "अन्य परियोजनाएँ :" msgstr "अन्य परियोजनाएँ :"
@ -816,7 +829,7 @@ msgstr "मोबाइल एप्लीकेशन"
msgid "Documentation" msgid "Documentation"
msgstr "प्रलेखन" msgstr "प्रलेखन"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "व्यवस्थापन डैशबोर्ड" msgstr "व्यवस्थापन डैशबोर्ड"
#, fuzzy #, fuzzy
@ -871,14 +884,13 @@ msgstr "कोई बिल नहीं"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "सूचि बनाने के लिए कुछ नहीं।" msgstr "सूचि बनाने के लिए कुछ नहीं।"
msgid "You probably want to" #, fuzzy
msgstr "आप शायद यह करना चाहते हैं" msgid "Add your first bill"
msgid "add a bill"
msgstr "बिल जोड़ें" msgstr "बिल जोड़ें"
msgid "add participants" #, fuzzy
msgstr "प्रतिभागियों को जोड़ें" msgid "Add the first participant"
msgstr "प्रतिभागी जोड़ें"
msgid "Password reminder" msgid "Password reminder"
msgstr "पासवर्ड अनुस्मारक" msgstr "पासवर्ड अनुस्मारक"
@ -902,24 +914,15 @@ msgstr "अपना पासवर्ड रीसेट करें"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "इस परियोजना से जुड़ने के लिए लोगों को आमंत्रित करें" msgstr "इस परियोजना से जुड़ने के लिए लोगों को आमंत्रित करें"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "पहचानकर्ता और कोड साझा करें" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" 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" msgid "Scan QR code"
msgstr "" msgstr ""
@ -930,17 +933,37 @@ msgstr ""
msgid "Send via Emails" msgid "Send via Emails"
msgstr "ईमेल के माध्यम से भेजें" msgstr "ईमेल के माध्यम से भेजें"
#, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"उन ईमेल पतों की एक (अल्पविराम से अलग की गयी) सूची निर्दिष्ट करें जिन्हे " "उन ईमेल पतों की एक (अल्पविराम से अलग की गयी) सूची निर्दिष्ट करें जिन्हे "
"आप इस \n" "आप इस \n"
"\t\t बजट प्रबंधन परियोजना के निर्माण के बारे में सूचित करना चाहते हैं " "\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?" msgid "Who pays?"
msgstr "किसे भुगतान करना है?" msgstr "किसे भुगतान करना है?"
@ -1110,3 +1133,24 @@ msgstr "अवधि"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "प्रोजेक्ट संपादित करें" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-04-11 17:12+0000\n"
"Last-Translator: Santiago José Gutiérrez Llanos " "Last-Translator: Santiago José Gutiérrez Llanos "
"<gutierrezapata17@gmail.com>\n" "<gutierrezapata17@gmail.com>\n"
@ -30,6 +30,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nama proyek" 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" msgid "New private code"
msgstr "Kode pribadi baru" msgstr "Kode pribadi baru"
@ -51,6 +58,12 @@ msgstr "Mata Uang Standar"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "Menetapkan mata uang default memungkinkan konversi mata uang antar tagihan" 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 "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -90,12 +103,6 @@ msgstr "Mohon, silahkan validasi captcha untuk melanjutkan."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Masukkan kode pribadi untuk mengkonfirmasi penghapusan" 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" msgid "Get in"
msgstr "Masuk" msgstr "Masuk"
@ -268,6 +275,9 @@ msgstr "Proyek tidak diketahui"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Kata sandi berhasil diatur ulang." msgstr "Kata sandi berhasil diatur ulang."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -777,6 +787,9 @@ msgstr "Riwayat"
msgid "Settings" msgid "Settings"
msgstr "Pengaturan" msgstr "Pengaturan"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "Proyek lainnya:" msgstr "Proyek lainnya:"
@ -799,7 +812,7 @@ msgstr "Aplikasi Gawai"
msgid "Documentation" msgid "Documentation"
msgstr "Dokumentasi" msgstr "Dokumentasi"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Dasbor Administrasi" msgstr "Dasbor Administrasi"
msgid "Legal information" msgid "Legal information"
@ -853,14 +866,13 @@ msgstr "Tidak ada tagihan"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Belum ada untuk didaftarkan." msgstr "Belum ada untuk didaftarkan."
msgid "You probably want to" #, fuzzy
msgstr "Anda mungkin menginginkan" msgid "Add your first bill"
msgid "add a bill"
msgstr "tambah tagihan" msgstr "tambah tagihan"
msgid "add participants" #, fuzzy
msgstr "tambah partisipan" msgid "Add the first participant"
msgstr "Sunting anggota ini"
msgid "Password reminder" msgid "Password reminder"
msgstr "Pengingat kata sandi" msgstr "Pengingat kata sandi"
@ -884,26 +896,15 @@ msgstr "Atur ulang kata sandi Anda"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Undang orang untuk bergabung dalam proyek ini" msgstr "Undang orang untuk bergabung dalam proyek ini"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Bagikan ID & kode" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" 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" msgid "Scan QR code"
msgstr "" msgstr ""
@ -914,17 +915,37 @@ msgstr ""
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Kirim melalui surel" msgstr "Kirim melalui surel"
#, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Spesifikkan daftar alamat surel (dipisah dengan koma) yang akan Anda " "Spesifikkan daftar alamat surel (dipisah dengan koma) yang akan Anda "
"kirim pemberitahuan tentang\n" "kirim pemberitahuan tentang\n"
" pembuatan dari manajemen anggaran proyek ini dan kami " " pembuatan dari manajemen anggaran proyek ini dan kami "
"akan memngirim mereka sebuah surel." "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?" msgid "Who pays?"
msgstr "Siapa membayar?" msgstr "Siapa membayar?"
@ -1121,3 +1142,27 @@ msgstr "Periode"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Ubah proyek" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-07-12 15:18+0000\n" "PO-Revision-Date: 2023-08-04 17:05+0000\n"
"Last-Translator: Matteo Piotto <piotto@gmail.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -23,45 +23,56 @@ msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted." "accepted."
msgstr "" msgstr ""
"Quantità o espressione non valida. Solo numeri e operatori + - * / " "Quantità o espressione non valida. Sono accettati solo numeri e gli "
"accettati." "operatori + - * / ."
msgid "Project name" msgid "Project name"
msgstr "Nome del progetto" 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" msgid "New private code"
msgstr "Codice privato" msgstr "Nuovo codice privato"
msgid "Enter a new code if you want to change it" 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" msgid "Email"
msgstr "Email" msgstr "Email"
msgid "Enable project history" msgid "Enable project history"
msgstr "Attivare la cronologia del progetto" msgstr "Abilita la cronologia del progetto"
msgid "Use IP tracking for project history" 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" msgid "Default Currency"
msgstr "Valuta predefinita" msgstr "Valuta predefinita"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
"Impostando una valuta predefinita abilita la conversione della valuta tra" "L'impostazione di una valuta predefinita consente la conversione di valuta "
" le fatture" "tra le fatture"
msgid "Unknown error"
msgstr "Errore sconosciuto"
msgid "Invalid private code."
msgstr "Codice privato non valido."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
msgstr "" 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." "contiene fatture in più valute."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "Compatibile con Cospend"
msgid "Project identifier" msgid "Project identifier"
msgstr "Identificatore del progetto" msgstr "Identificatore del progetto"
@ -81,25 +92,16 @@ msgstr ""
"favore scegli un identificatore nuovo" "favore scegli un identificatore nuovo"
msgid "Which is a real currency: Euro or Petro dollar?" msgid "Which is a real currency: Euro or Petro dollar?"
msgstr "" msgstr "Qual è una valuta reale: Euro o Petrol dollar?"
#, fuzzy
msgid "euro" msgid "euro"
msgstr "Periodo" msgstr "euro"
msgid "Please, validate the captcha to proceed." msgid "Please, validate the captcha to proceed."
msgstr "Si prega di validare il captcha per procedere." msgstr "Si prega di validare il captcha per procedere."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "" msgstr "Inserisci il codice privato per confermare la cancellazione"
#, fuzzy
msgid "Unknown error"
msgstr "Progetto non conosciuto"
#, fuzzy
msgid "Invalid private code."
msgstr "Codice privato non valido."
msgid "Get in" msgid "Get in"
msgstr "Entra" msgstr "Entra"
@ -171,15 +173,12 @@ msgstr "Peso"
msgid "Add" msgid "Add"
msgstr "Aggiungi" msgstr "Aggiungi"
#, fuzzy
msgid "The participant name is invalid" 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" msgid "This project already have this participant"
msgstr "Membro già presente in questo progetto" msgstr "Membro già presente in questo progetto"
#, fuzzy
msgid "People to notify" msgid "People to notify"
msgstr "Persone da avvisare" msgstr "Persone da avvisare"
@ -194,45 +193,43 @@ msgid "Logout"
msgstr "Esci" msgstr "Esci"
msgid "Please check the email configuration of the server." 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 "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"Spiacenti, si è verificato un errore durante l'invio delle email di " "Verifica la configurazione dell'email sul server o contatta l'amministratore:"
"invito. Verifica la configurazione dell'email sul server o contatta " " %(admin_email)s"
"l'amministratore."
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" 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 #. Last two items of a list with more than 3 items
msgid "{previous_object}, and {end_object}" 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 #. Two items in a middle of a list with more than 5 objects
msgid "{previous_object}, {next_object}" msgid "{previous_object}, {next_object}"
msgstr "" msgstr "{previous_object}, {next_object}"
#. First two items of a list with more than 3 items #. First two items of a list with more than 3 items
msgid "{start_object}, {next_object}" msgid "{start_object}, {next_object}"
msgstr "" msgstr "{start_object}, {next_object}"
msgid "No Currency" msgid "No Currency"
msgstr "Nessuna valuta" msgstr "Nessuna valuta"
#. Form error with only one error #. Form error with only one error
msgid "{prefix}: {error}" msgid "{prefix}: {error}"
msgstr "" msgstr "{prefix}: {error}"
#. Form error with a list of errors #. Form error with a list of errors
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." msgid "Too many failed login attempts."
msgstr "Troppi tentativi di accesso non riusciti. Riprova più tardi." msgstr "Troppi tentativi di accesso non riusciti. Riprova più tardi."
@ -243,7 +240,7 @@ msgstr ""
"tentativi rimasti." "tentativi rimasti."
msgid "Provided token is invalid" msgid "Provided token is invalid"
msgstr "" msgstr "Il token fornito non è valido"
msgid "This private code is not the right one" msgid "This private code is not the right one"
msgstr "Questo codice privato non è quello corretto" msgstr "Questo codice privato non è quello corretto"
@ -258,14 +255,12 @@ msgstr ""
"Abbiamo provato a inviarti un promemoria per email, ma si è verificato un" "Abbiamo provato a inviarti un promemoria per email, ma si è verificato un"
" errore. Puoi comunque utilizzare normalmente il progetto." " errore. Puoi comunque utilizzare normalmente il progetto."
#, fuzzy
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions." "instructions."
msgstr "" msgstr ""
"Spiacenti, si è verificato un errore durante l'invio dell'email con le " "Spiacenti, si è verificato un errore durante l'invio dell'email con le "
"istruzioni per il reset della password. Verifica la configurazione " "istruzioni per il reset della password."
"dell'email del server o contatta l'amministratore."
msgid "No token provided" msgid "No token provided"
msgstr "Nessun token fornito" msgstr "Nessun token fornito"
@ -279,17 +274,22 @@ msgstr "Progetto non conosciuto"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Reset della password effettuato." msgstr "Reset della password effettuato."
msgid "Project settings have been changed successfully."
msgstr "Impostazioni del progetto cambiate correttamente."
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr "Impossibile analizzare il CSV"
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "Attributo mancante: %(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "Cannot add bills in multiple currencies to a project without default "
"currency" "currency"
msgstr "" msgstr ""
"Impossibile aggiungere fatture in più valute a un progetto senza valuta "
"predefinita"
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "Progetto caricato con successo" msgstr "Progetto caricato con successo"
@ -298,10 +298,10 @@ msgid "Project successfully deleted"
msgstr "Progetto rimosso con successo" msgstr "Progetto rimosso con successo"
msgid "Error deleting project" msgid "Error deleting project"
msgstr "" msgstr "Errore nell'eliminazione del progetto"
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "Impossibile effettuare il logout"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" 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" msgid "Your invitations have been sent"
msgstr "I tuoi inviti sono stati spediti" msgstr "I tuoi inviti sono stati spediti"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails." msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr "" msgstr ""
"Spiacenti, si è verificato un errore durante l'invio delle email di " "Spiacenti, si è verificato un errore durante l'invio delle email di invito."
"invito. Verifica la configurazione dell'email sul server o contatta "
"l'amministratore."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
msgstr "%(member)s è stato aggiunto" msgstr "%(member)s è stato aggiunto"
msgid "Error activating participant" msgid "Error activating participant"
msgstr "" msgstr "Errore nell'attivazione del partecipante"
#, python-format #, python-format
msgid "%(name)s is part of this project again" msgid "%(name)s is part of this project again"
msgstr "%(name)s fa nuovamente parte di questo progetto" msgstr "%(name)s fa nuovamente parte di questo progetto"
msgid "Error removing participant" msgid "Error removing participant"
msgstr "" msgstr "Errore nella rimozione del partecipante"
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Participant '%(name)s' has been deactivated. It will still appear in the " "Participant '%(name)s' has been deactivated. It will still appear in the "
"list until its balance reach zero." "list until its balance reach zero."
msgstr "" 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." "utenti finché il suo bilancio sarà superiore a zero."
#, fuzzy, python-format #, python-format
msgid "Participant '%(name)s' has been removed" 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" 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" msgid "The bill has been added"
msgstr "L'addebito è stato aggiunto" msgstr "L'addebito è stato aggiunto"
msgid "Error deleting bill" msgid "Error deleting bill"
msgstr "" msgstr "Errore nella cancellazione della spesa"
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "La spesa è stata cancellata" msgstr "La spesa è stata cancellata"
@ -361,22 +358,19 @@ msgstr "L'addebito è stato aggiornato"
#, python-format #, python-format
msgid "%(lang)s is not a supported language" msgid "%(lang)s is not a supported language"
msgstr "" msgstr "%(lang)s non è una lingua supportata"
#, fuzzy
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "Attivare la cronologia del progetto" msgstr "Errore nel cancellare la cronologia del progetto"
#, fuzzy
msgid "Deleted project history." msgid "Deleted project history."
msgstr "Cronologia del progetto cancellata." msgstr "Cronologia del progetto cancellata."
#, fuzzy
msgid "Error deleting recorded IP addresses" 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." 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." msgid "Sorry, we were unable to find the page you've asked for."
msgstr "Spiacenti, non abbiamo trovato la pagina che cerchi." msgstr "Spiacenti, non abbiamo trovato la pagina che cerchi."
@ -390,9 +384,8 @@ msgstr "Ritorna all'elenco"
msgid "Administration tasks are currently disabled." msgid "Administration tasks are currently disabled."
msgstr "In questo momento le funzionalità amministrative sono disabilitate." msgstr "In questo momento le funzionalità amministrative sono disabilitate."
#, fuzzy
msgid "Authentication" msgid "Authentication"
msgstr "Documentazione" msgstr "Autenticazione"
msgid "The project you are trying to access do not exist, do you want to" 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" msgstr "Il progetto cui stai provando ad accedere non esiste, vuoi"
@ -409,9 +402,8 @@ msgstr "Crea un nuovo progetto"
msgid "Project" msgid "Project"
msgstr "Progetto" msgstr "Progetto"
#, fuzzy
msgid "Number of participants" msgid "Number of participants"
msgstr "aggiunti partecipanti" msgstr "Numro di partecipanti"
msgid "Number of bills" msgid "Number of bills"
msgstr "Numero di spese" msgstr "Numero di spese"
@ -428,9 +420,8 @@ msgstr "Azioni"
msgid "edit" msgid "edit"
msgstr "modifica" msgstr "modifica"
#, fuzzy
msgid "Delete project" msgid "Delete project"
msgstr "Modifica progetto" msgstr "Elimina il progetto"
msgid "show" msgid "show"
msgstr "visualizza" msgstr "visualizza"
@ -438,20 +429,17 @@ msgstr "visualizza"
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "Il Cruscotto è attualmente disabilitato." msgstr "Il Cruscotto è attualmente disabilitato."
#, fuzzy
msgid "Download Mobile Application" msgid "Download Mobile Application"
msgstr "Applicazione mobile" msgstr "Scarica l'applicazione per i dispositivi mobili"
#, fuzzy
msgid "Get it on" msgid "Get it on"
msgstr "Entra" msgstr "Entra"
msgid "Edit project" msgid "Edit project"
msgstr "Modifica progetto" msgstr "Modifica progetto"
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "Modifica progetto" msgstr "Importa progetto"
msgid "Download project's data" msgid "Download project's data"
msgstr "Scarica i dati del progetto" msgstr "Scarica i dati del progetto"
@ -480,14 +468,13 @@ msgid "Privacy Settings"
msgstr "Impostazioni Privacy" msgstr "Impostazioni Privacy"
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr "Salva le modifiche"
msgid "This will remove all bills and participants in this project!" 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" msgid "Import previously exported project"
msgstr "Importare il file JSON esportato precedentemente" msgstr "Importa il file JSON esportato precedentemente"
msgid "Choose file" msgid "Choose file"
msgstr "Scegli file" msgstr "Scegli file"
@ -499,23 +486,22 @@ msgid "Add a bill"
msgstr "Aggiungi un addebito" msgstr "Aggiungi un addebito"
msgid "Simple operations are allowed, e.g. (18+36.2)/3" 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" msgid "Everyone"
msgstr "Tutti" msgstr "Tutti"
msgid "No one" msgid "No one"
msgstr "" msgstr "Nessuno"
msgid "More options" msgid "More options"
msgstr "" msgstr "Più opzioni"
msgid "Add participant" msgid "Add participant"
msgstr "Aggiungi partecipante" msgstr "Aggiungi partecipante"
#, fuzzy
msgid "Edit this participant" msgid "Edit this participant"
msgstr "Aggiungi partecipante" msgstr "Modifica questo partecipante"
msgid "john.doe@example.com, mary.moe@site.com" msgid "john.doe@example.com, mary.moe@site.com"
msgstr "mario.rossi@example.com, maria.bianchi@site.com" msgstr "mario.rossi@example.com, maria.bianchi@site.com"
@ -546,11 +532,11 @@ msgstr "Impostazioni Cronologia Aggiornate"
#, python-format #, python-format
msgid "Bill %(name)s: %(property_name)s changed from %(before)s to %(after)s" 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 #, python-format
msgid "Bill %(name)s: %(property_name)s changed to %(after)s" 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" msgid "Confirm Remove IP Adresses"
msgstr "Conferma Rimozione Indirizzi IP" msgstr "Conferma Rimozione Indirizzi IP"
@ -566,9 +552,8 @@ msgstr ""
" La parte residua dello storico del progetto non subirà " " La parte residua dello storico del progetto non subirà "
"modifiche. Questa azione non potrà essere annullata." "modifiche. Questa azione non potrà essere annullata."
#, fuzzy
msgid "Confirm deletion" msgid "Confirm deletion"
msgstr "Conferma Cancellazione" msgstr "Conferma la cancellazione"
msgid "Close" msgid "Close"
msgstr "Chiudi" msgstr "Chiudi"
@ -585,37 +570,36 @@ msgstr ""
#, python-format #, python-format
msgid "Bill %(name)s: added %(owers_list_str)s to owers list" 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 #, python-format
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list" 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." msgid "This project has history disabled. New actions won't appear below."
msgstr "" msgstr ""
"Questo progetto ha la cronologia disabilitata. Le nuove azioni non "
"appariranno in basso."
#, fuzzy
msgid "You can enable history on the settings page." msgid "You can enable history on the settings page."
msgstr "" msgstr "È possibile attivare la cronologia nella pagina delle impostazioni."
"La registrazione degli indirizzi IP può essere attivata nella pagina "
"delle impostazioni"
msgid "" msgid ""
"The table below reflects actions recorded prior to disabling project " "The table below reflects actions recorded prior to disabling project "
"history." "history."
msgstr "" 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." 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 "" msgid ""
"Some entries below contain IP addresses, even though this project has IP " "Some entries below contain IP addresses, even though this project has IP "
"recording disabled. " "recording disabled. "
msgstr "" msgstr ""
"Alcune voci qui sotto contengono indirizzi IP, anche se in questo " "Alcune voci qui sotto contengono degli indirizzi IP, anche se in questo "
"progetto la registrazione degli IP è disabilitata. " "progetto la registrazione degli IP sia stata disabilitata. "
msgid "Delete stored IP addresses" msgid "Delete stored IP addresses"
msgstr "Cancella indirizzi IP conservati" msgstr "Cancella indirizzi IP conservati"
@ -626,7 +610,6 @@ msgstr "Nessun indirizzo IP da cancellare"
msgid "Delete Stored IP Addresses" msgid "Delete Stored IP Addresses"
msgstr "Cancella Indirizzi IP Conservati" msgstr "Cancella Indirizzi IP Conservati"
#, fuzzy
msgid "No history to erase" msgid "No history to erase"
msgstr "Nessuna cronologia da cancellare" msgstr "Nessuna cronologia da cancellare"
@ -656,47 +639,47 @@ msgstr "Dall'IP"
msgid "Project %(name)s added" msgid "Project %(name)s added"
msgstr "Progetto %(name)s aggiunto" msgstr "Progetto %(name)s aggiunto"
#, fuzzy, python-format #, python-format
msgid "Bill %(name)s added" msgid "Bill %(name)s added"
msgstr "L'addebito è stato aggiunto" msgstr "La spesa %(name)s è stata aggiunta"
#, python-format #, python-format
msgid "Participant %(name)s added" msgid "Participant %(name)s added"
msgstr "" msgstr "Il partecipante %(name)s è stato aggiunto"
msgid "Project private code changed" msgid "Project private code changed"
msgstr "Codice privato del progetto modificato" msgstr "Codice privato del progetto modificato"
#, fuzzy, python-format #, python-format
msgid "Project renamed to %(new_project_name)s" 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" 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" msgid "Project settings modified"
msgstr "Impostazione del progetto modificate" msgstr "Impostazione del progetto modificate"
#, python-format #, python-format
msgid "Participant %(name)s deactivated" msgid "Participant %(name)s deactivated"
msgstr "" msgstr "Il partecipante %(name)s è stato disattivato"
#, python-format #, python-format
msgid "Participant %(name)s reactivated" msgid "Participant %(name)s reactivated"
msgstr "" msgstr "Il partecipante %(name)s è stato riattivato"
#, python-format #, python-format
msgid "Participant %(name)s renamed to %(new_name)s" msgid "Participant %(name)s renamed to %(new_name)s"
msgstr "" msgstr "Il partecipante %(name)s è stato rinominato in %(new_name)s"
#, python-format #, python-format
msgid "Bill %(name)s renamed to %(new_description)s" msgid "Bill %(name)s renamed to %(new_description)s"
msgstr "" msgstr "La spesa %(name)s è stata rinominata in %(new_description)s"
#, python-format #, python-format
msgid "Participant %(name)s: weight changed from %(old_weight)s to %(new_weight)s" 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" msgid "Payer"
msgstr "Pagatore" msgstr "Pagatore"
@ -711,33 +694,33 @@ msgstr "Data"
msgid "Amount in %(currency)s" msgid "Amount in %(currency)s"
msgstr "Importo in %(currency)s" msgstr "Importo in %(currency)s"
#, fuzzy, python-format #, python-format
msgid "Bill %(name)s modified" msgid "Bill %(name)s modified"
msgstr "L'addebito è stato aggiornato" msgstr "La spesa %(name)s è stata modificata"
#, python-format #, python-format
msgid "Participant %(name)s modified" msgid "Participant %(name)s modified"
msgstr "" msgstr "Il partecipante %(name)s è stato modificato"
#, fuzzy, python-format #, python-format
msgid "Bill %(name)s removed" 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" 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" 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" 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" 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" msgid "Nothing to list"
msgstr "Niente da elencare" msgstr "Niente da elencare"
@ -809,6 +792,9 @@ msgstr "Cronologia"
msgid "Settings" msgid "Settings"
msgstr "Impostazioni" msgstr "Impostazioni"
msgid "RSS Feed"
msgstr "Feed RSS"
msgid "Other projects :" msgid "Other projects :"
msgstr "Altri progetti:" msgstr "Altri progetti:"
@ -820,7 +806,7 @@ msgstr "Cruscotto"
#, python-format #, python-format
msgid "Please retry after %(date)s." msgid "Please retry after %(date)s."
msgstr "" msgstr "Per favore, riprova dopo %(date)s."
msgid "Code" msgid "Code"
msgstr "Codice" msgstr "Codice"
@ -831,12 +817,11 @@ msgstr "Applicazione mobile"
msgid "Documentation" msgid "Documentation"
msgstr "Documentazione" msgstr "Documentazione"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Cruscotto Amministrazione" msgstr "Cruscotto Amministrazione"
#, fuzzy
msgid "Legal information" msgid "Legal information"
msgstr "Cancella Conferma" msgstr "Informazioni legali"
msgid "\"I hate money\" is free software" msgid "\"I hate money\" is free software"
msgstr "\"I hate money\" è un software libero" msgstr "\"I hate money\" è un software libero"
@ -886,14 +871,11 @@ msgstr "Nessun addebito"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Ancora niente da mostrare." msgstr "Ancora niente da mostrare."
msgid "You probably want to" msgid "Add your first bill"
msgstr "Probabilmente vuoi" msgstr "Aggiungi la tua prima spesa"
msgid "add a bill" msgid "Add the first participant"
msgstr "aggiungi una spesa" msgstr "Aggiungi il primo partecipante"
msgid "add participants"
msgstr "aggiunti partecipanti"
msgid "Password reminder" msgid "Password reminder"
msgstr "Promemoria password" msgstr "Promemoria password"
@ -917,46 +899,62 @@ msgstr "Reset della tua password"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Invita persone a collaborare a questo progetto" msgstr "Invita persone a collaborare a questo progetto"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Condividi Identificatore & codice" msgstr "Condividi un link d'invito"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" msgstr ""
"È possibile condividere l'identificativo del progetto e il codice privato" "Il modo più semplice per invitare le persone è fornire loro il seguente link "
" con qualsiasi mezzo di comunicazione." "di invito.<br />Potranno accedere al progetto, gestire i partecipanti, "
"aggiungere/modificare/cancellare spese. Tuttavia, non avranno accesso a "
msgid "Identifier:" "impostazioni importanti come la modifica del codice privato o la "
msgstr "Identificatore:" "cancellazione dell'intero progetto."
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr "Scansiona codice QR"
msgid "Use a mobile device with a compatible app." msgid "Use a mobile device with a compatible app."
msgstr "" msgstr "Usa un dispositivo mobile con una app compatibile."
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Inviare via Email" msgstr "Inviare via Email"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Specifica un elenco di indirizzi email (separati da virgola) a cui vuoi " "Specifica un elenco di indirizzi email (separati da virgola) a cui vuoi "
"notificare la\n" "notificare la creazione di questo progetto. Manderemo loro un messaggio con "
" creazione di questo progetto di gestione budget e " "il link d'invito."
"manderemo a ciascuno di loro un messaggio per te."
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?" msgid "Who pays?"
msgstr "Chi paga?" msgstr "Chi paga?"
@ -1146,3 +1144,25 @@ msgstr "Periodo"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Modifica il progetto" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-11-11 16:28+0000\n" "PO-Revision-Date: 2024-06-24 15:09+0000\n"
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -27,12 +27,17 @@ msgstr "無効な入力です。数字と「+ - * / 」の演算子しか入力
msgid "Project name" msgid "Project name"
msgstr "プロジェクトの名前" msgstr "プロジェクトの名前"
#, fuzzy msgid "Current private code"
msgstr "現在の暗証コード"
msgid "Enter existing private code to edit project"
msgstr "プロジェクトを編集するために、暗証コードを入力してください"
msgid "New private code" msgid "New private code"
msgstr "暗証コード" msgstr "暗証コード"
msgid "Enter a new code if you want to change it" msgid "Enter a new code if you want to change it"
msgstr "" msgstr "変更するために、新しい暗証コードを入力してください"
msgid "Email" msgid "Email"
msgstr "メールアドレス" msgstr "メールアドレス"
@ -47,7 +52,13 @@ msgid "Default Currency"
msgstr "初期設定にする通貨" msgstr "初期設定にする通貨"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr "明細通貨変換のため、デフォルトの通貨を設定してください"
msgid "Unknown error"
msgstr "不明エラー"
msgid "Invalid private code."
msgstr "無効な暗証コード。"
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "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" msgid "Enter private code to confirm deletion"
msgstr "" msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "未知のプロジェクト"
#, fuzzy
msgid "Invalid private code."
msgstr "暗証コード"
msgid "Get in" msgid "Get in"
msgstr "入る" msgstr "入る"
@ -172,7 +175,7 @@ msgid "This project already have this participant"
msgstr "プロジェクトはすでにこのメンバーを含めています" msgstr "プロジェクトはすでにこのメンバーを含めています"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "通知したい人"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "招待状を送る" msgstr "招待状を送る"
@ -187,11 +190,12 @@ msgstr "ログアウト"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr ""
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "申し訳ございませんが、招待メールを送ったとき、エラーが発生しました。メールアドレスを再度チェックするかまたは管理者に連絡ください。" msgstr "申し訳ございませんが、エラーが発生しました。メールアドレスを再度チェックする"
"か、または管理者( %(admin_email)sに連絡ください"
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -260,9 +264,12 @@ msgstr "未知のプロジェクト"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "パスワードを再設定できました。" msgstr "パスワードを再設定できました。"
msgid "Unable to parse CSV" msgid "Project settings have been changed successfully."
msgstr "" msgstr ""
msgid "Unable to parse CSV"
msgstr "CSVを読み込むことができません"
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr ""
@ -282,7 +289,7 @@ msgid "Error deleting project"
msgstr "" msgstr ""
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "ログアウトできません"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
@ -343,9 +350,8 @@ msgstr ""
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "プロジェクトの歴史を有効にする" msgstr "プロジェクトの歴史を有効にする"
#, fuzzy
msgid "Deleted project history." msgid "Deleted project history."
msgstr "プロジェクトの歴史を有効にする" msgstr "プロジェクトの歴史を削除しました。"
#, fuzzy #, fuzzy
msgid "Error deleting recorded IP addresses" msgid "Error deleting recorded IP addresses"
@ -564,9 +570,8 @@ msgstr ""
msgid "This project has history disabled. New actions won't appear below." msgid "This project has history disabled. New actions won't appear below."
msgstr "" msgstr ""
#, fuzzy
msgid "You can enable history on the settings page." msgid "You can enable history on the settings page."
msgstr "設定ページでIPアドレス記録を編集可能にすることができる" msgstr "設定ページでIPアドレス記録を編集可能にすることができる"
msgid "" msgid ""
"The table below reflects actions recorded prior to disabling project " "The table below reflects actions recorded prior to disabling project "
@ -612,13 +617,13 @@ msgstr "設定ページでIPアドレス記録を編集不可にすることが
msgid "From IP" msgid "From IP"
msgstr "IPから" msgstr "IPから"
#, fuzzy, python-format #, python-format
msgid "Project %(name)s added" msgid "Project %(name)s added"
msgstr "プロジェクトの名前" msgstr "プロジェクト%(name)sが作成されました"
#, fuzzy, python-format #, python-format
msgid "Bill %(name)s added" msgid "Bill %(name)s added"
msgstr "明細が追加されました" msgstr "%(name)s明細が追加されました"
#, python-format #, python-format
msgid "Participant %(name)s added" msgid "Participant %(name)s added"
@ -631,9 +636,9 @@ msgstr "プロジェクトの私用コードが変更された"
msgid "Project renamed to %(new_project_name)s" msgid "Project renamed to %(new_project_name)s"
msgstr "プロジェクト名は%(new_project_name)s" msgstr "プロジェクト名は%(new_project_name)s"
#, fuzzy, python-format #, python-format
msgid "Project contact email changed to %(new_email)s" msgid "Project contact email changed to %(new_email)s"
msgstr "プロジェクトの連絡メールが…に変更された" msgstr "プロジェクトの連絡メールが%(new_email)sに変更されました"
msgid "Project settings modified" msgid "Project settings modified"
msgstr "プロジェクトの設定が修正された" msgstr "プロジェクトの設定が修正された"
@ -671,9 +676,9 @@ msgstr "日付"
msgid "Amount in %(currency)s" msgid "Amount in %(currency)s"
msgstr "%(currency)sでの金額" msgstr "%(currency)sでの金額"
#, fuzzy, python-format #, python-format
msgid "Bill %(name)s modified" msgid "Bill %(name)s modified"
msgstr "明細が変更されました" msgstr "%(name)s明細が変更されました"
#, python-format #, python-format
msgid "Participant %(name)s modified" msgid "Participant %(name)s modified"
@ -687,17 +692,17 @@ msgstr "ユーザー%(name)sが既に取り除かれました"
msgid "Participant %(name)s removed" msgid "Participant %(name)s removed"
msgstr "ユーザー%(name)sが既に取り除かれました" msgstr "ユーザー%(name)sが既に取り除かれました"
#, fuzzy, python-format #, python-format
msgid "Project %(name)s changed in an unknown way" 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" 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" msgid "Participant %(name)s changed in an unknown way"
msgstr "未知の方法で変更された" msgstr "参加者%(name)sが不明な方法で変更されました"
msgid "Nothing to list" msgid "Nothing to list"
msgstr "表示できるものがない" msgstr "表示できるものがない"
@ -767,6 +772,9 @@ msgstr "歴史"
msgid "Settings" msgid "Settings"
msgstr "設定" msgstr "設定"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "他のプロジェクト:" msgstr "他のプロジェクト:"
@ -789,7 +797,7 @@ msgstr "携帯アプリ"
msgid "Documentation" msgid "Documentation"
msgstr "書類" msgstr "書類"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "管理ダッシュボード" msgstr "管理ダッシュボード"
#, fuzzy #, fuzzy
@ -844,13 +852,12 @@ msgstr "明細なし"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "表示できるものはありません。" msgstr "表示できるものはありません。"
msgid "You probably want to" #, fuzzy
msgstr "…したいかもしれない" msgid "Add your first bill"
msgid "add a bill"
msgstr "明細を追加する" msgstr "明細を追加する"
msgid "add participants" #, fuzzy
msgid "Add the first participant"
msgstr "参加者を追加する" msgstr "参加者を追加する"
msgid "Password reminder" msgid "Password reminder"
@ -873,22 +880,15 @@ msgstr "パスワードを再設定する"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "他人をこのプロジェクトに招待する" msgstr "他人をこのプロジェクトに招待する"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "名前とコードを共有する" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "プロジェクト名と私用コードは何の方法でも共有できます。" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:" msgstr ""
msgstr "名前:"
msgid "Share the Link"
msgstr "リンクを共有する"
msgid "You can directly share the following link via your prefered medium"
msgstr "好きの手段で以下のリンクを直接に共有できる"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr ""
@ -900,13 +900,30 @@ msgid "Send via Emails"
msgstr "メールで送る" msgstr "メールで送る"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
"…を知らせたいメールアドレスのリストを特定する(カンマ区切り)\n"
"彼らにこの予算管理プロジェクトの作成をメールでお知らせします。"
msgid "Who pays?" msgid "Who pays?"
msgstr "誰が支払った?" msgstr "誰が支払った?"
@ -1067,3 +1084,20 @@ msgstr "期間"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "プロジェクトを編集する" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-17 04:56+0000\n" "PO-Revision-Date: 2023-09-16 10:59+0000\n"
"Last-Translator: a-g-rao <athrigrao@gmail.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -31,11 +31,17 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "ಯೋಜನೆಯ ಹೆಸರು" msgstr "ಯೋಜನೆಯ ಹೆಸರು"
msgid "Current private code"
msgstr "ಪ್ರಸ್ತುತ ಸಂಕೇತಪದ"
msgid "Enter existing private code to edit project"
msgstr "ಯೋಜನೆಯನ್ನು ಪರಿಷ್ಕರಿಸಲು ಚಾಲ್ತಿಯಲ್ಲಿರುವ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ"
msgid "New private code" msgid "New private code"
msgstr "ಹೊಸ ಸಂಕೇತಪದ" msgstr "ಹೊಸ ಸಂಕೇತಪದ"
msgid "Enter a new code if you want to change it" msgid "Enter a new code if you want to change it"
msgstr "ಸಂಕೇತಪದ ಬದಲಾಯಿಸಬೇಕೆಂದರೆ ನಮೂದಿಸಿ." msgstr "ಸಂಕೇತಪದ ಬದಲಾಯಿಸಬೇಕೆಂದರೆ ನಮೂದಿಸಿ"
msgid "Email" msgid "Email"
msgstr "ಮಿನ್ನಂಚೆ" msgstr "ಮಿನ್ನಂಚೆ"
@ -47,21 +53,31 @@ msgid "Use IP tracking for project history"
msgstr "" msgstr ""
msgid "Default Currency" msgid "Default Currency"
msgstr "" msgstr "ಪುರ್ವನಿಯೋಜಿತ ಕರನ್ಸಿ"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
"ಪುರ್ವನಿಯೋಜಿತ ಕರನ್ಸಿಯನ್ನು ನಮೋದಿಸುವುದರಿಂದ ರಶೀದಿಗಳ ನಡುವೆ ಕರನ್ಸಿ ಪರಿವರ್ತನೆಯನ್ನು "
"ಸಕ್ರಿಯಗೊಳಿಸುತ್ತದೆ"
msgid "Unknown error"
msgstr "ಅಜ್ಞಾತ ದೋಷ"
msgid "Invalid private code."
msgstr "ನಮೂದಿಸಿರುವ ಸಂಕೇತಪದ ಅಮಾನ್ಯವಾಗಿದೆ."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
msgstr "" msgstr ""
"ಅನೇಕ ಕರೆನ್ಸಿಯ ರಶೀದಿ ಇರುವುದರಿಂದ ಈ ಯೋಜನೆಯನ್ನು \"ಯಾವುದೆ ಕರೆನ್ಸಿ ಇಲ್ಲ\" ಯಂದು "
"ನಮೋದಿಸಲು ಆಗುವುದಿಲ್ಲ."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "ಖರ್ಚನ್ನು ಜೊತೆಗಾರನೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಬಹುದು"
msgid "Project identifier" msgid "Project identifier"
msgstr "" msgstr "ಯೋಜನೆ ಸಂಕೇತ"
msgid "Private code" msgid "Private code"
msgstr "ಸಂಕೇತಪದ" msgstr "ಸಂಕೇತಪದ"
@ -74,25 +90,21 @@ msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please " "A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier" "choose a new identifier"
msgstr "" msgstr ""
"ಈ ಸಂಕೇತದ (\"%(project)s\") ಇನೊಂದು ಯೋಜನೆ ಆಗಲೆ ಅಸ್ತಿತ್ವದಲ್ಲಿದೆ. ದಯವಿಟ್ಟು ಹೊಸ "
"ಸಂಕೇತವನ್ನು ಆರಿಸಿ"
msgid "Which is a real currency: Euro or Petro dollar?" msgid "Which is a real currency: Euro or Petro dollar?"
msgstr "" msgstr "ನಿಜವಾದ ಕರೆನ್ಸಿ ಯಾವುದು: ಯುರೋ ಅಥವಾ ಪೆಟ್ರೋ ಡಾಲರ್?"
msgid "euro" msgid "euro"
msgstr "ಯೂರೋ" msgstr "ಯೂರೋ"
msgid "Please, validate the captcha to proceed." msgid "Please, validate the captcha to proceed."
msgstr "" msgstr "ಮುಂದುವರೆಯಲು ದಯವಿಟ್ಟು, ಕ್ಯಾಪ್ಚಾವನ್ನು ಮೌಲ್ಯೀಕರಿಸಿ."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ" msgstr "ತೆಗೆದುಹಾಕಲು ನಿಮ್ಮ ಸಂಕೇತಪದವನ್ನು ನಮೂದಿಸಿ"
msgid "Unknown error"
msgstr "ಅಜ್ಞಾತ ದೋಷ"
msgid "Invalid private code."
msgstr "ನಮೂದಿಸಿರುವ ಸಂಕೇತಪದ ಅಮಾನ್ಯವಾಗಿದೆ."
msgid "Get in" msgid "Get in"
msgstr "ಒಳಹೊಗು" msgstr "ಒಳಹೊಗು"
@ -118,25 +130,26 @@ msgid "Reset password"
msgstr "ಪ್ರವೇಶಪದ ಮರುಹೊಂದಿಸಿ" msgstr "ಪ್ರವೇಶಪದ ಮರುಹೊಂದಿಸಿ"
msgid "When?" msgid "When?"
msgstr "" msgstr "ಯಾವಾಗ?"
msgid "What?" msgid "What?"
msgstr "ಏನು?" msgstr "ಏನು?"
msgid "Who paid?" msgid "Who paid?"
msgstr "" msgstr "ಯಾರು ಪಾವತಿಸಿದ್ದಾರೆ?"
msgid "How much?" msgid "How much?"
msgstr "" msgstr "ಎಷ್ಟು?"
msgid "Currency" msgid "Currency"
msgstr "ನಾಣ್ಯಪದ್ಧತಿ" msgstr "ನಾಣ್ಯಪದ್ಧತಿ"
#, fuzzy
msgid "External link" msgid "External link"
msgstr "" msgstr "ಬಾಹ್ಯ ಲಿಂಕ್"
msgid "A link to an external document, related to this bill" msgid "A link to an external document, related to this bill"
msgstr "" msgstr "ಈ ರಶೀದಿಗೆ ಸಂಬಂಧಿಸಿದ ಬಾಹ್ಯ ಕಡತದ ಲಿಂಕ್"
msgid "For whom?" msgid "For whom?"
msgstr "ಯಾರಿಂದ?" msgstr "ಯಾರಿಂದ?"
@ -149,13 +162,13 @@ msgstr "ಕೋರಿಕೆ ಸಲ್ಲಿಸಿ ಹೊಸದನ್ನುಸೇ
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
msgstr "" msgstr "ಯೋಜನೆಯ ಪೂರ್ವನಿಯೋಜಿತ ಕರೆನ್ಸಿ: %(currency)s"
msgid "Name" msgid "Name"
msgstr "ಹೆಸರು" msgstr "ಹೆಸರು"
msgid "Weights should be positive" msgid "Weights should be positive"
msgstr "" msgstr "ತೂಕವು ಧನಾತ್ಮಕವಾಗಿರಬೇಕು"
msgid "Weight" msgid "Weight"
msgstr "ತೂಕ" msgstr "ತೂಕ"
@ -164,33 +177,33 @@ msgid "Add"
msgstr "ಕೂಡು" msgstr "ಕೂಡು"
msgid "The participant name is invalid" msgid "The participant name is invalid"
msgstr "" msgstr "ಸದಸ್ಯರ ಹೆಸರು ಅಮಾನ್ಯವಾಗಿದೆ"
#, fuzzy
msgid "This project already have this participant" msgid "This project already have this participant"
msgstr "ಈ ಸದಸ್ಯರು ಈಗಾಗಲೆ ಈ ಯೋಜನೆಯ ಸದಸ್ಯರಾಗಿದ್ದಾರೆ" msgstr "ಈ ಸದಸ್ಯರು ಈಗಾಗಲೆ ಈ ಯೋಜನೆಯ ಸದಸ್ಯರಾಗಿದ್ದಾರೆ"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "ಸೂಚಿಸಬೇಕಾದ ಜನರು"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "" msgstr "ಆಮಂತ್ರಣಗಳನ್ನು ಕಳುಹಿಸಿ"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "ಮಿನ್ನಂಚೆ %(email)s ಸಮಂಜಸವಾಗಿಲ್ಲ" msgstr "ಮಿನ್ನಂಚೆ %(email)s ಸಮಂಜಸವಾಗಿಲ್ಲ"
msgid "Logout" msgid "Logout"
msgstr "" msgstr "ನಿರ್ಗಮಿಸಿ"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr "ದಯವಿಟ್ಟು, ಇಮೇಲ್ ಸಂರಚನೆ ಪರಿಶೀಲಿಸಿ."
#, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"ದಯವಿಟ್ಟು, ಇಮೇಲ್ ಸಂರಚನೆ ಪರಿಶೀಲಿಸಿ ಅಥವಾ ನಿರ್ವಾಹಕನನ್ನು ಸಂಪರ್ಕಿಸಿ:%(admin_email)s"
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -209,7 +222,7 @@ msgid "{start_object}, {next_object}"
msgstr "{start_object}, {next_object}" msgstr "{start_object}, {next_object}"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr "ಯಾವುದೆ ಕರೆನ್ಸಿ ಇಲ್ಲ"
#. Form error with only one error #. Form error with only one error
msgid "{prefix}: {error}" msgid "{prefix}: {error}"
@ -259,6 +272,9 @@ msgstr "ಗೊತ್ತಿಲ್ಲದ ಯೋಜನೆ"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -752,6 +768,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -774,7 +793,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -828,13 +847,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -857,21 +873,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -884,10 +893,28 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -1027,3 +1054,42 @@ msgstr ""
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2021-07-18 12:32+0000\n"
"Last-Translator: Kemystra <izzmin97@gmail.com>\n" "Last-Translator: Kemystra <izzmin97@gmail.com>\n"
"Language: ms\n" "Language: ms\n"
@ -27,6 +27,13 @@ msgstr "Nilai atau ungkapan yang sah. Hanya nombor dan operasi + - * / diterima.
msgid "Project name" msgid "Project name"
msgstr "Nama projek" 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" msgid "New private code"
msgstr "Kod peribadi baharu" msgstr "Kod peribadi baharu"
@ -51,6 +58,13 @@ msgstr "Mata wang asal"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" msgstr ""
#, fuzzy
msgid "Unknown error"
msgstr "Ralat yang tidak dikenalpasti"
msgid "Invalid private code."
msgstr "Kod peribadi tidak sah."
#, fuzzy #, fuzzy
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "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" msgid "Enter private code to confirm deletion"
msgstr "Masukkan kod peribadi untuk mengesahkan penghapusan" 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" msgid "Get in"
msgstr "Masuk" msgstr "Masuk"
@ -266,6 +273,9 @@ msgstr ""
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr ""
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -758,6 +768,9 @@ msgstr ""
msgid "Settings" msgid "Settings"
msgstr "" msgstr ""
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr ""
@ -780,7 +793,7 @@ msgstr ""
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr ""
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "" msgstr ""
msgid "Legal information" msgid "Legal information"
@ -834,13 +847,10 @@ msgstr ""
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr ""
msgid "You probably want to" msgid "Add your first bill"
msgstr "" msgstr ""
msgid "add a bill" msgid "Add the first participant"
msgstr ""
msgid "add participants"
msgstr "" msgstr ""
msgid "Password reminder" msgid "Password reminder"
@ -863,21 +873,14 @@ msgstr ""
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " link.<br />They will be able to access the project, manage participants,"
msgstr "" " add/edit/delete bills. However, they will not have access to important "
"settings such as changing the private code or deleting the whole project."
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
msgid "Scan QR code" msgid "Scan QR code"
@ -890,10 +893,29 @@ msgid "Send via Emails"
msgstr "" msgstr ""
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you." 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 "" msgstr ""
msgid "Who pays?" msgid "Who pays?"
@ -1033,3 +1055,43 @@ msgstr ""
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "" #~ 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 "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2022-02-13 16:54+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n" "Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
"Language: nb_NO\n" "Language: nb_NO\n"
@ -29,6 +29,13 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Prosjektnavn" msgstr "Prosjektnavn"
#, fuzzy
msgid "Current private code"
msgstr "Ny privat kode"
msgid "Enter existing private code to edit project"
msgstr ""
msgid "New private code" msgid "New private code"
msgstr "Ny privat kode" msgstr "Ny privat kode"
@ -50,6 +57,12 @@ msgstr "Forvalgt valuta"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "En forvalg valutainnstilling skrur på konvertering mellom regninger" msgstr "En forvalg valutainnstilling skrur på konvertering mellom regninger"
msgid "Unknown error"
msgstr "Ukjent feil"
msgid "Invalid private code."
msgstr "Ugyldig privat kode"
#, fuzzy #, fuzzy
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "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" msgid "Enter private code to confirm deletion"
msgstr "Skriv inn privat kode for å bekrefte sletting" msgstr "Skriv inn privat kode for å bekrefte sletting"
msgid "Unknown error"
msgstr "Ukjent feil"
msgid "Invalid private code."
msgstr "Ugyldig privat kode"
#, fuzzy #, fuzzy
msgid "Get in" msgid "Get in"
msgstr "Til prosjektet" msgstr "Til prosjektet"
@ -275,6 +282,9 @@ msgstr "Ukjent prosjekt"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Passord tilbakestilt." msgstr "Passord tilbakestilt."
msgid "Project settings have been changed successfully."
msgstr ""
msgid "Unable to parse CSV" msgid "Unable to parse CSV"
msgstr "" msgstr ""
@ -819,6 +829,9 @@ msgstr "Historikk"
msgid "Settings" msgid "Settings"
msgstr "Innstillinger" msgstr "Innstillinger"
msgid "RSS Feed"
msgstr ""
#, fuzzy #, fuzzy
msgid "Other projects :" msgid "Other projects :"
msgstr "Andre prosjekter:" msgstr "Andre prosjekter:"
@ -842,7 +855,7 @@ msgstr "Mobilprogram"
msgid "Documentation" msgid "Documentation"
msgstr "Dokumentasjon" msgstr "Dokumentasjon"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Administrasjonsoversiktspanel" msgstr "Administrasjonsoversiktspanel"
#, fuzzy #, fuzzy
@ -899,14 +912,13 @@ msgstr "Ingen regninger"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Ingenting å liste opp enda." msgstr "Ingenting å liste opp enda."
msgid "You probably want to" #, fuzzy
msgstr "Du ønsker antagelig å" msgid "Add your first bill"
msgid "add a bill"
msgstr "legge til en regning" msgstr "legge til en regning"
msgid "add participants" #, fuzzy
msgstr "legge til deltagere" msgid "Add the first participant"
msgstr "Legg til deltager"
msgid "Password reminder" msgid "Password reminder"
msgstr "Passordpåminner" msgstr "Passordpåminner"
@ -930,26 +942,15 @@ msgstr "Tilbakestill passordet ditt"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Inviter folk til å ta del i dette prosjektet" msgstr "Inviter folk til å ta del i dette prosjektet"
#, fuzzy msgid "Share an invitation link"
msgid "Share Identifier & code" msgstr ""
msgstr "Del identifikator og kode"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" 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" msgid "Scan QR code"
msgstr "" msgstr ""
@ -963,15 +964,35 @@ msgstr "Send via e-poster"
#, fuzzy #, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Angi en (kommainndelt) liste over e-postadresser du ønsker å varsle om\n" "Angi en (kommainndelt) liste over e-postadresser du ønsker å varsle om\n"
"opprettelsen av dette budsjetthåndteringsprosjektet, og de vil få en " "opprettelsen av dette budsjetthåndteringsprosjektet, og de vil få en "
"e-post om det." "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?" msgid "Who pays?"
msgstr "Hvem betaler?" msgstr "Hvem betaler?"
@ -1297,3 +1318,24 @@ msgstr "Periode"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Rediger prosjektet" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\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-02-17 02:50+0000\n" "PO-Revision-Date: 2024-03-17 12:01+0000\n"
"Last-Translator: Sander Kooijmans <weblate@gogognome.nl>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -31,12 +31,17 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Projectnaam" 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" msgid "New private code"
msgstr "Privécode" msgstr "Nieuwe privécode"
msgid "Enter a new code if you want to change it" 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" msgid "Email"
msgstr "E-mailadres" msgstr "E-mailadres"
@ -52,14 +57,24 @@ msgstr "Standaard munteenheid"
msgid "Setting a default currency enables currency conversion between bills" msgid "Setting a default currency enables currency conversion between bills"
msgstr "" 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 "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
msgstr "" msgstr ""
"Dit project kan niet op 'geen valuta' worden gezet, omdat het facturen in "
"meerdere valuta's bevat."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "Compatibel met mede besteden"
msgid "Project identifier" msgid "Project identifier"
msgstr "Project-id" msgstr "Project-id"
@ -77,25 +92,16 @@ msgid ""
msgstr "Er is al een project genaamd (\"%(project)s\"). Kies een andere naam" msgstr "Er is al een project genaamd (\"%(project)s\"). Kies een andere naam"
msgid "Which is a real currency: Euro or Petro dollar?" 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" msgid "euro"
msgstr "Periode" msgstr "euro"
msgid "Please, validate the captcha to proceed." msgid "Please, validate the captcha to proceed."
msgstr "" msgstr "Valideer de captcha om door te gaan."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "" msgstr "Voer de privécode in om de verwijder opdracht te bevestigen"
#, fuzzy
msgid "Unknown error"
msgstr "Onbekend project"
#, fuzzy
msgid "Invalid private code."
msgstr "Privécode"
msgid "Get in" msgid "Get in"
msgstr "Inloggen" msgstr "Inloggen"
@ -119,7 +125,7 @@ msgid "Password confirmation"
msgstr "Wachtwoord bevestigen" msgstr "Wachtwoord bevestigen"
msgid "Reset password" msgid "Reset password"
msgstr "Wachtwoord herstellen" msgstr "Wachtwoord opnieuw instellen"
msgid "When?" msgid "When?"
msgstr "Wanneer?" msgstr "Wanneer?"
@ -167,16 +173,14 @@ msgstr "Gewicht"
msgid "Add" msgid "Add"
msgstr "Toevoegen" msgstr "Toevoegen"
#, fuzzy
msgid "The participant name is invalid" 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" msgid "This project already have this participant"
msgstr "Deze deelnemer is al lid van het project" msgstr "Deze deelnemer is al lid van het project"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "Mensen om op de hoogte te stellen"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "Uitnodigingen versturen" msgstr "Uitnodigingen versturen"
@ -189,54 +193,52 @@ msgid "Logout"
msgstr "Uitloggen" msgstr "Uitloggen"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr "Controleer de e-mailconfiguratie van de server."
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" 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 "
" Controleer de e-mailinstellingen van de server of neem contact op met de" "beheerder: %(admin_email)s"
" beheerder."
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" 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 #. Last two items of a list with more than 3 items
msgid "{previous_object}, and {end_object}" 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 #. Two items in a middle of a list with more than 5 objects
msgid "{previous_object}, {next_object}" msgid "{previous_object}, {next_object}"
msgstr "" msgstr "{previous_object}, {next_object}"
#. First two items of a list with more than 3 items #. First two items of a list with more than 3 items
msgid "{start_object}, {next_object}" msgid "{start_object}, {next_object}"
msgstr "" msgstr "{start_object}, {next_object}"
msgid "No Currency" msgid "No Currency"
msgstr "Geen munteenheid" msgstr "Geen valuta"
#. Form error with only one error #. Form error with only one error
msgid "{prefix}: {error}" msgid "{prefix}: {error}"
msgstr "" msgstr "{prefix}: {error}"
#. Form error with a list of errors #. Form error with a list of errors
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." msgid "Too many failed login attempts."
msgstr "Te vaak onjuist ingelogd. Probeer het later opnieuw." msgstr "Te veel mislukte inlogpogingen."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." 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." msgstr "Het admin-wachtwoord is onjuist. Je kunt het nog %(num)d keer proberen."
msgid "Provided token is invalid" msgid "Provided token is invalid"
msgstr "" msgstr "Het opgegeven token is ongeldig"
msgid "This private code is not the right one" msgid "This private code is not the right one"
msgstr "Deze privécode is onjuist" msgstr "Deze privécode is onjuist"
@ -251,14 +253,12 @@ msgstr ""
"We hebben geprobeerd een herinneringsmail te versturen, maar er is iets " "We hebben geprobeerd een herinneringsmail te versturen, maar er is iets "
"fout gegaan. Je kunt het project nog steeds normaal gebruiken." "fout gegaan. Je kunt het project nog steeds normaal gebruiken."
#, fuzzy
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions." "instructions."
msgstr "" msgstr ""
"Sorry, er is iets fout gegaan bij het verzenden van een e-mail met " "Sorry, er is iets fout gegaan bij het verzenden van een e-mail met "
"instructies om je wachtwoord te herstellen. Controleer de " "instructies om je wachtwoord te herstellen."
"e-mailinstellingen van de server of neem contact op met de beheerder."
msgid "No token provided" msgid "No token provided"
msgstr "Geen toegangssleutel opgegeven" msgstr "Geen toegangssleutel opgegeven"
@ -272,12 +272,15 @@ msgstr "Onbekend project"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Wachtwoord is hersteld." msgstr "Wachtwoord is hersteld."
msgid "Unable to parse CSV" msgid "Project settings have been changed successfully."
msgstr "" msgstr ""
msgid "Unable to parse CSV"
msgstr "Kan CSV niet parseren"
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "Ontbrekende attribute: %(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "Cannot add bills in multiple currencies to a project without default "
@ -294,7 +297,7 @@ msgid "Error deleting project"
msgstr "" msgstr ""
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "Kan niet uitloggen"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" 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" msgstr "De rekening is toegevoegd"
msgid "Error deleting bill" msgid "Error deleting bill"
msgstr "" msgstr "Fout bij verwijderen factuur"
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "De rekening is verwijderd" msgstr "De factuur is verwijderd"
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "De rekening is aangepast" msgstr "De rekening is aangepast"
@ -383,9 +386,8 @@ msgstr "Terug naar de lijst"
msgid "Administration tasks are currently disabled." msgid "Administration tasks are currently disabled."
msgstr "Beheerderstaken zijn momenteel uitgeschakeld." msgstr "Beheerderstaken zijn momenteel uitgeschakeld."
#, fuzzy
msgid "Authentication" msgid "Authentication"
msgstr "Documentatie" msgstr "Authenticatie"
msgid "The project you are trying to access do not exist, do you want to" 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" msgstr "Het project dat je probeert te benaderen bestaat niet. Wil je"
@ -402,18 +404,17 @@ msgstr "Nieuw project aanmaken"
msgid "Project" msgid "Project"
msgstr "Project" msgstr "Project"
#, fuzzy
msgid "Number of participants" msgid "Number of participants"
msgstr "deelnemers toevoegen" msgstr "Aantal deelnemers"
msgid "Number of bills" msgid "Number of bills"
msgstr "Aantal rekeningen" msgstr "Aantal facturen"
msgid "Newest bill" msgid "Newest bill"
msgstr "Nieuwste rekening" msgstr "Nieuwste factuur"
msgid "Oldest bill" msgid "Oldest bill"
msgstr "Oudste rekening" msgstr "Oudste factuur"
msgid "Actions" msgid "Actions"
msgstr "Acties" msgstr "Acties"
@ -421,9 +422,8 @@ msgstr "Acties"
msgid "edit" msgid "edit"
msgstr "bewerken" msgstr "bewerken"
#, fuzzy
msgid "Delete project" msgid "Delete project"
msgstr "Project aanpassen" msgstr "Project verwijderen"
msgid "show" msgid "show"
msgstr "tonen" msgstr "tonen"
@ -431,9 +431,8 @@ msgstr "tonen"
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "De overzichtspagina is momenteel uitgeschakeld." msgstr "De overzichtspagina is momenteel uitgeschakeld."
#, fuzzy
msgid "Download Mobile Application" msgid "Download Mobile Application"
msgstr "Mobiele app" msgstr "Mobiele applicatie downloaden"
#, fuzzy #, fuzzy
msgid "Get it on" msgid "Get it on"
@ -442,9 +441,8 @@ msgstr "Inloggen"
msgid "Edit project" msgid "Edit project"
msgstr "Project aanpassen" msgstr "Project aanpassen"
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "Project aanpassen" msgstr "Project importeren"
msgid "Download project's data" msgid "Download project's data"
msgstr "Projectgegevens downloaden" msgstr "Projectgegevens downloaden"
@ -791,6 +789,9 @@ msgstr "Geschiedenis"
msgid "Settings" msgid "Settings"
msgstr "Instellingen" msgstr "Instellingen"
msgid "RSS Feed"
msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "Overige projecten:" msgstr "Overige projecten:"
@ -813,7 +814,7 @@ msgstr "Mobiele app"
msgid "Documentation" msgid "Documentation"
msgstr "Documentatie" msgstr "Documentatie"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Administratie-overzicht" msgstr "Administratie-overzicht"
#, fuzzy #, fuzzy
@ -868,14 +869,13 @@ msgstr "Geen rekeningen"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Nog niks." msgstr "Nog niks."
msgid "You probably want to" #, fuzzy
msgstr "Waarschijnlijk wil je" msgid "Add your first bill"
msgid "add a bill"
msgstr "een rekening toevoegen" msgstr "een rekening toevoegen"
msgid "add participants" #, fuzzy
msgstr "deelnemers toevoegen" msgid "Add the first participant"
msgstr "Deelnemer toevoegen"
msgid "Password reminder" msgid "Password reminder"
msgstr "Wachtwoordhint" msgstr "Wachtwoordhint"
@ -899,24 +899,15 @@ msgstr "Wachtwoord herstellen"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Nodig mensen uit voor dit project" msgstr "Nodig mensen uit voor dit project"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Identificatie en code delen" msgstr ""
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" 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" msgid "Scan QR code"
msgstr "" msgstr ""
@ -927,17 +918,37 @@ msgstr ""
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Versturen via e-mail" msgstr "Versturen via e-mail"
#, fuzzy
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Stel een (kommagescheiden) lijst op met e-mailadressen van personen die " "Stel een (kommagescheiden) lijst op met e-mailadressen van personen die "
"je op de\n" "je op de\n"
" hoogte wilt stellen van dit project - wij sturen hen een " " hoogte wilt stellen van dit project - wij sturen hen een "
"e-mail." "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?" msgid "Who pays?"
msgstr "Wie betaalt?" msgstr "Wie betaalt?"
@ -1120,3 +1131,23 @@ msgstr "Periode"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Project bewerken" #~ 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 "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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-09-27 14:19+0000\n" "PO-Revision-Date: 2023-08-24 18:48+0000\n"
"Last-Translator: Andrzej Ochodek <andrzej.ochodek@gmail.com>\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: 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" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\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" "Generated-By: Babel 2.9.0\n"
#, python-format #, python-format
@ -30,6 +30,12 @@ msgstr ""
msgid "Project name" msgid "Project name"
msgstr "Nazwa projektu" 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" msgid "New private code"
msgstr "Nowy kod prywatny" msgstr "Nowy kod prywatny"
@ -53,6 +59,12 @@ msgstr ""
"Wybranie domyślnej waluty pozwala na konwersję walutową pomiędzy " "Wybranie domyślnej waluty pozwala na konwersję walutową pomiędzy "
"rachunkami" "rachunkami"
msgid "Unknown error"
msgstr "Nieznany błąd"
msgid "Invalid private code."
msgstr "Nieprawidłowy kod prywatny."
msgid "" msgid ""
"This project cannot be set to 'no currency' because it contains bills in " "This project cannot be set to 'no currency' because it contains bills in "
"multiple currencies." "multiple currencies."
@ -61,7 +73,7 @@ msgstr ""
" on rachunki w różnych walutach." " on rachunki w różnych walutach."
msgid "Compatible with Cospend" msgid "Compatible with Cospend"
msgstr "" msgstr "Zgodne z Cospend"
msgid "Project identifier" msgid "Project identifier"
msgstr "Identyfikator projektu" msgstr "Identyfikator projektu"
@ -92,12 +104,6 @@ msgstr "Rozwiąż captcha, by kontynuować."
msgid "Enter private code to confirm deletion" msgid "Enter private code to confirm deletion"
msgstr "Wprowadź kod prywatny w celu potwierdzenia usunięcia" 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" msgid "Get in"
msgstr "Wejdź" msgstr "Wejdź"
@ -188,16 +194,15 @@ msgid "Logout"
msgstr "Wyloguj" msgstr "Wyloguj"
msgid "Please check the email configuration of the server." msgid "Please check the email configuration of the server."
msgstr "" msgstr "Prosimy sprawdzić konfigurację e-mail serwera."
#, fuzzy, python-format #, python-format
msgid "" msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator: %(admin_email)s" "administrator: %(admin_email)s"
msgstr "" msgstr ""
"Przepraszamy, wystąpił błąd podczas próby wysłania wiadomości e-mail z " "Prosimy sprawdzić konfigurację e-mail serwera lub skontaktować się z "
"zaproszeniem. Sprawdź konfigurację e-mail serwera lub skontaktuj się z " "administratorem: %(admin_email)s"
"administratorem."
#. List with two items only #. List with two items only
msgid "{dual_object_0} and {dual_object_1}" msgid "{dual_object_0} and {dual_object_1}"
@ -226,9 +231,8 @@ msgstr "{prefix}: {error}"
msgid "{prefix}:<br />{errors}" msgid "{prefix}:<br />{errors}"
msgstr "{prefix}:<br />{errors}" msgstr "{prefix}:<br />{errors}"
#, fuzzy
msgid "Too many failed login attempts." 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 #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." 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ł " "Próbowaliśmy wysłać Ci wiadomość e-mail z przypomnieniem, ale wystąpił "
"błąd. Nadal możesz normalnie korzystać z projektu." "błąd. Nadal możesz normalnie korzystać z projektu."
#, fuzzy
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions." "instructions."
msgstr "" msgstr ""
"Przepraszamy, wystąpił błąd podczas wysyłania wiadomości e-mail z " "Przepraszamy, wystąpił błąd podczas wysyłania wiadomości e-mail z "
"instrukcjami resetowania hasła. Sprawdź konfigurację e-mail serwera lub " "instrukcjami resetowania hasła."
"skontaktuj się z administratorem."
msgid "No token provided" msgid "No token provided"
msgstr "Nie podano tokena" msgstr "Nie podano tokena"
@ -271,12 +273,15 @@ msgstr "Nieznany projekt"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "Hasło zostało pomyślnie zresetowane." 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" msgid "Unable to parse CSV"
msgstr "" msgstr "Nie udało się odczytać pliku CSV"
#, python-format #, python-format
msgid "Missing attribute: %(attribute)s" msgid "Missing attribute: %(attribute)s"
msgstr "" msgstr "Brakujący atrybut: %(attribute)s"
msgid "" msgid ""
"Cannot add bills in multiple currencies to a project without default " "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" msgstr "Błąd podczas usuwania projektu"
msgid "Unable to logout" msgid "Unable to logout"
msgstr "" msgstr "Nie można się wylogować"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" 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" msgid "Your invitations have been sent"
msgstr "Twoje zaproszenia zostały wysłane" msgstr "Twoje zaproszenia zostały wysłane"
#, fuzzy
msgid "Sorry, there was an error while trying to send the invitation emails." msgid "Sorry, there was an error while trying to send the invitation emails."
msgstr "" msgstr ""
"Przepraszamy, wystąpił błąd podczas próby wysłania wiadomości e-mail z " "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 " "zaproszeniem."
"administratorem."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
@ -355,7 +358,7 @@ msgstr "Rachunek został zmieniony"
#, python-format #, python-format
msgid "%(lang)s is not a supported language" msgid "%(lang)s is not a supported language"
msgstr "" msgstr "%(lang)s nie jest obsługowanym językiem"
msgid "Error deleting project history" msgid "Error deleting project history"
msgstr "Błąd podczas usuwania historii projektu" msgstr "Błąd podczas usuwania historii projektu"
@ -437,9 +440,8 @@ msgstr "Pobierz na"
msgid "Edit project" msgid "Edit project"
msgstr "Edytuj projekt" msgstr "Edytuj projekt"
#, fuzzy
msgid "Import project" msgid "Import project"
msgstr "Edytuj projekt" msgstr "Importuj projekt"
msgid "Download project's data" msgid "Download project's data"
msgstr "Pobierz dane projektu" msgstr "Pobierz dane projektu"
@ -466,14 +468,13 @@ msgid "Privacy Settings"
msgstr "Ustawienia prywatności" msgstr "Ustawienia prywatności"
msgid "Save changes" msgid "Save changes"
msgstr "" msgstr "Zapisz zmiany"
msgid "This will remove all bills and participants in this project!" msgid "This will remove all bills and participants in this project!"
msgstr "Spowoduje to usunięcie wszystkich rachunków i uczestników tego projektu!" msgstr "Spowoduje to usunięcie wszystkich rachunków i uczestników tego projektu!"
#, fuzzy
msgid "Import previously exported project" msgid "Import previously exported project"
msgstr "Zaimportuj wcześniej wyeksportowany plik JSON" msgstr "Zaimportuj wcześniej wyeksportowany projekt"
msgid "Choose file" msgid "Choose file"
msgstr "Wybierz plik" msgstr "Wybierz plik"
@ -485,7 +486,7 @@ msgid "Add a bill"
msgstr "Dodaj rachunek" msgstr "Dodaj rachunek"
msgid "Simple operations are allowed, e.g. (18+36.2)/3" msgid "Simple operations are allowed, e.g. (18+36.2)/3"
msgstr "" msgstr "Dozwolone są proste operacje, np. (18+36.2)/3"
msgid "Everyone" msgid "Everyone"
msgstr "Wszyscy" 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." msgid "This project has history disabled. New actions won't appear below."
msgstr "" 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." 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 "" msgid ""
"The table below reflects actions recorded prior to disabling project " "The table below reflects actions recorded prior to disabling project "
"history." "history."
msgstr "" msgstr ""
"Poniższa tabela przedstawia działania zarejestrowane przed wyłączeniem "
"historii projektu."
#, fuzzy
msgid "You can clear the project history to remove them." 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 "" msgid ""
"Some entries below contain IP addresses, even though this project has IP " "Some entries below contain IP addresses, even though this project has IP "
@ -785,6 +787,9 @@ msgstr "Historia"
msgid "Settings" msgid "Settings"
msgstr "Ustawienia" msgstr "Ustawienia"
msgid "RSS Feed"
msgstr "Kanał RSS"
msgid "Other projects :" msgid "Other projects :"
msgstr "Inne projekty:" msgstr "Inne projekty:"
@ -796,7 +801,7 @@ msgstr "Kokpit"
#, python-format #, python-format
msgid "Please retry after %(date)s." msgid "Please retry after %(date)s."
msgstr "" msgstr "Prosimy spróbować ponownie po %(date)s."
msgid "Code" msgid "Code"
msgstr "Kod" msgstr "Kod"
@ -807,7 +812,7 @@ msgstr "Aplikacja mobilna"
msgid "Documentation" msgid "Documentation"
msgstr "Dokumentacja" msgstr "Dokumentacja"
msgid "Administation Dashboard" msgid "Administration Dashboard"
msgstr "Kokpit administracyjny" msgstr "Kokpit administracyjny"
msgid "Legal information" msgid "Legal information"
@ -861,14 +866,11 @@ msgstr "Brak rachunków"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "Nie ma jeszcze żadnej listy." msgstr "Nie ma jeszcze żadnej listy."
msgid "You probably want to" msgid "Add your first bill"
msgstr "Prawdopodobnie chcesz" msgstr "Dodaj swój pierwszy rachunek"
msgid "add a bill" msgid "Add the first participant"
msgstr "dodać rachunek" msgstr "Dodaj pierwszego uczestnika"
msgid "add participants"
msgstr "dodać członków"
msgid "Password reminder" msgid "Password reminder"
msgstr "Przypomnienie hasła" msgstr "Przypomnienie hasła"
@ -892,46 +894,61 @@ msgstr "Zresetuj swoje hasło"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Zaproś ludzi do dołączenia do tego projektu" msgstr "Zaproś ludzi do dołączenia do tego projektu"
msgid "Share Identifier & code" msgid "Share an invitation link"
msgstr "Udostępnij identyfikator i kod" msgstr "Udostępnij link z zaproszeniem"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "The easiest way to invite people is to give them the following invitation"
"communication means." " 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 "" msgstr ""
"Identyfikator projektu i kod prywatny można udostępniać dowolnymi " "Najłatwiejszym sposobem zapraszania nowych osób jest podanie im poniższego "
"środkami komunikacji." "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 "
msgid "Identifier:" "mieli dostępu do ważnych ustawień, takich jak zmiana kodu prywatnego lub "
msgstr "Identyfikator:" "usunięcie całego projektu."
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"
msgid "Scan QR code" msgid "Scan QR code"
msgstr "" msgstr "Skanuj kod QR"
msgid "Use a mobile device with a compatible app." msgid "Use a mobile device with a compatible app."
msgstr "" msgstr "Użyj urządzenia mobilnego ze zgodną aplikacją."
msgid "Send via Emails" msgid "Send via Emails"
msgstr "Wyślij przez maile" msgstr "Wyślij przez maile"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a list of email adresses (separated by comma) of people you want "
"about the\n" "to notify about the creation of this project. We will send them an email "
" creation of this budget management project and we will " "with the invitation link."
"send them an email for you."
msgstr "" msgstr ""
"Podaj (adresy rozdzielone przecinkami) adresy email, które chcesz " "Podaj listę adresów e-mail (oddzielonych przecinkami) osób, które chcesz "
"powiadomić o \n" "powiadomić o utworzeniu tego projektu. Wyślemy im wiadomość e-mail z linkiem "
" utworzeniu tego projektu zarządzania budżetem, a my " "zaproszającym."
"wyślemy Ci wiadomość email."
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?" msgid "Who pays?"
msgstr "Kto płaci?" msgstr "Kto płaci?"
@ -1117,3 +1134,25 @@ msgstr "Okres"
#~ msgid "Edit the project" #~ msgid "Edit the project"
#~ msgstr "Edytuj projekt" #~ 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