Compare commits
82 commits
4db6ef807b
...
34d9253f9f
Author | SHA1 | Date | |
---|---|---|---|
34d9253f9f | |||
![]() |
354a482e60 | ||
![]() |
73a90d7dbc | ||
bd689f931a | |||
67938eabbc | |||
662ff97795 | |||
![]() |
cfc2ffa671 | ||
![]() |
3a007714bf | ||
4f9cad88bd | |||
![]() |
c2db991ffd | ||
![]() |
794f26f767 | ||
![]() |
85eccb74b2 | ||
4b96e89422 | |||
![]() |
dcb61b62b1 | ||
![]() |
e00c39a62c | ||
![]() |
2f099674ed | ||
![]() |
752c80d29f | ||
![]() |
61ea1f54d2 | ||
![]() |
299c384908 | ||
![]() |
4e9ff9b1ac | ||
![]() |
2aa410c68f | ||
![]() |
7505cbe25a | ||
![]() |
83a60b1289 | ||
![]() |
ce20f9adea | ||
2b21795e94 | |||
![]() |
0b09476865 | ||
![]() |
62fbeee15c | ||
![]() |
56e2ff6900 | ||
![]() |
35ac04be20 | ||
![]() |
a9f211d3f6 | ||
e568bb05cc | |||
86eb9b8662 | |||
6e31a9c8b5 | |||
![]() |
cf77b4c346 | ||
![]() |
6582e2c0c3 | ||
![]() |
6e0a3689b8 | ||
![]() |
710aee9711 | ||
![]() |
eb6e156c32 | ||
![]() |
9ef46e2c5d | ||
4e7496e49d | |||
e5dfbf2f37 | |||
![]() |
3ac1bb8afe | ||
![]() |
a5f83de5ce | ||
![]() |
050de4e8f6 | ||
![]() |
f9a96b0e0d | ||
![]() |
a74cd97286 | ||
![]() |
a3009126dc | ||
![]() |
eef67cf84c | ||
![]() |
a3d4e4250d | ||
![]() |
ae1cc309d7 | ||
![]() |
510c8db07f | ||
![]() |
843f2df877 | ||
![]() |
178fc94cef | ||
![]() |
312dfef14b | ||
![]() |
a0409a296a | ||
![]() |
73f014e90e | ||
![]() |
4af4c10b1f | ||
![]() |
c399611660 | ||
![]() |
f090d81358 | ||
![]() |
d00f8063ed | ||
![]() |
720f0e52dd | ||
![]() |
ba117ba0a6 | ||
2bb535070a | |||
![]() |
1dcb0ba78b | ||
![]() |
fa4a881ae1 | ||
![]() |
edefb51cfb | ||
![]() |
9f7ecf6614 | ||
![]() |
7fd00344e2 | ||
![]() |
6ec3ba6f77 | ||
![]() |
511ba86c4c | ||
![]() |
54a5b0e63e | ||
![]() |
2ce1ea4bf2 | ||
![]() |
8bce025c15 | ||
![]() |
bb30813ec4 | ||
![]() |
ecf9a7b590 | ||
![]() |
b74ac1077c | ||
![]() |
76e8b3baf0 | ||
![]() |
8cd60c1bf5 | ||
![]() |
2d7c6486e9 | ||
![]() |
db7c9ea2b3 | ||
![]() |
d1382a691b | ||
![]() |
d02ec152f7 |
|
@ -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
|
@ -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
|
12
.github/workflows/dockerhub.yml
vendored
|
@ -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:
|
||||||
|
|
103
.github/workflows/test-docs.yml
vendored
|
@ -1,103 +0,0 @@
|
||||||
name: Test & Docs
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ master ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ master ]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
# Use postgresql and MariaDB versions of Debian buster
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:11
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: ihatemoney
|
|
||||||
POSTGRES_DB: ihatemoney_ci
|
|
||||||
options:
|
|
||||||
--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
|
|
||||||
mariadb:
|
|
||||||
image: mariadb:10.3
|
|
||||||
env:
|
|
||||||
MARIADB_ROOT_PASSWORD: ihatemoney
|
|
||||||
MARIADB_DATABASE: ihatemoney_ci
|
|
||||||
options: >-
|
|
||||||
--health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
|
|
||||||
ports:
|
|
||||||
- 3306:3306
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
|
|
||||||
dependencies: [normal]
|
|
||||||
database: [sqlite]
|
|
||||||
# Test other databases only with one version of Python (Debian buster has 3.7)
|
|
||||||
include:
|
|
||||||
- python-version: 3.7
|
|
||||||
dependencies: normal
|
|
||||||
database: postgresql
|
|
||||||
- python-version: 3.7
|
|
||||||
dependencies: normal
|
|
||||||
database: mariadb
|
|
||||||
# Try a few variants with the minimal versions supported
|
|
||||||
- python-version: 3.7
|
|
||||||
dependencies: minimal
|
|
||||||
database: sqlite
|
|
||||||
- python-version: 3.7
|
|
||||||
dependencies: minimal
|
|
||||||
database: postgresql
|
|
||||||
- python-version: 3.7
|
|
||||||
dependencies: minimal
|
|
||||||
database: mariadb
|
|
||||||
- python-version: 3.9
|
|
||||||
dependencies: minimal
|
|
||||||
database: sqlite
|
|
||||||
- python-version: "3.10"
|
|
||||||
dependencies: minimal
|
|
||||||
database: sqlite
|
|
||||||
- python-version: "3.11"
|
|
||||||
dependencies: minimal
|
|
||||||
database: sqlite
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python-version }}
|
|
||||||
cache: 'pip'
|
|
||||||
cache-dependency-path: '**/pyproject.toml'
|
|
||||||
- name: Change dependencies to minimal supported versions
|
|
||||||
run: sed -i -e 's/>=/==/g; s/~=.*==\(.*\)/==\1/g; s/~=/==/g;' pyproject.toml
|
|
||||||
if: matrix.dependencies == 'minimal'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
python -m pip install tox
|
|
||||||
# Run tox using the version of Python in `PATH`
|
|
||||||
- name: Run Tox with sqlite
|
|
||||||
run: tox -e py
|
|
||||||
if: matrix.database == 'sqlite'
|
|
||||||
env:
|
|
||||||
TESTING_SQLALCHEMY_DATABASE_URI: 'sqlite:///budget.db'
|
|
||||||
- name: Run Tox with postgresql
|
|
||||||
run: tox -e py
|
|
||||||
if: matrix.database == 'postgresql'
|
|
||||||
env:
|
|
||||||
TESTING_SQLALCHEMY_DATABASE_URI: 'postgresql+psycopg2://postgres:ihatemoney@localhost:5432/ihatemoney_ci'
|
|
||||||
- name: Run Tox with mariadb
|
|
||||||
run: tox -e py
|
|
||||||
if: matrix.database == 'mariadb'
|
|
||||||
env:
|
|
||||||
TESTING_SQLALCHEMY_DATABASE_URI: 'mysql+pymysql://root:ihatemoney@localhost:3306/ihatemoney_ci'
|
|
||||||
- name: Run Lint & Docs
|
|
||||||
run: tox -e lint_docs
|
|
||||||
if: matrix.python-version == '3.11'
|
|
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info
|
*.egg-info
|
||||||
|
*.mo
|
||||||
dist
|
dist
|
||||||
.venv
|
.venv
|
||||||
docs/_build/
|
docs/_build/
|
||||||
|
|
13
.isort.cfg
|
@ -1,13 +0,0 @@
|
||||||
[settings]
|
|
||||||
# Needed for black compatibility
|
|
||||||
multi_line_output=3
|
|
||||||
include_trailing_comma=True
|
|
||||||
force_grid_wrap=0
|
|
||||||
line_length=88
|
|
||||||
combine_as_imports=True
|
|
||||||
|
|
||||||
# If set, imports will be sorted within their section independent to the import_type.
|
|
||||||
force_sort_within_sections=True
|
|
||||||
|
|
||||||
# skip
|
|
||||||
skip_glob=.local,**/migrations/**,**/node_modules/**,**/node-forge/**
|
|
34
CHANGELOG.md
|
@ -2,10 +2,40 @@
|
||||||
|
|
||||||
This document describes changes between each past release.
|
This document describes changes between each past release.
|
||||||
|
|
||||||
## 6.1.2 (unreleased)
|
## 6.2.0 (unreleased)
|
||||||
|
|
||||||
|
- Add support for python 3.12 (#757)
|
||||||
|
- Migrate from setup.cfg to pyproject.toml (#1243)
|
||||||
|
- Update to wtforms 3.1 (#1248)
|
||||||
|
- Document [repository rules](https://ihatemoney.readthedocs.io/en/latest/contributing.html#repository-rules) (#1253)
|
||||||
|
- Add "reimbursement" bills and allow to create them directly from the "Settle" page (#1290)
|
||||||
|
- Remove support for python 3.7
|
||||||
|
- Replace the black linter by ruff
|
||||||
|
- Replace virtualenv and pip by uv
|
||||||
|
- Remove tox
|
||||||
|
|
||||||
|
## 6.1.5 (2024-03-19)
|
||||||
|
|
||||||
|
- Fix README and changelog not being displayed on PyPI
|
||||||
|
- Fix ability to change project settings when project has existing currency (#1292)
|
||||||
|
- Update translations for Dutch and German
|
||||||
|
|
||||||
|
|
||||||
- Nothing changed yet.
|
## 6.1.4 (2023-12-14)
|
||||||
|
|
||||||
|
- Fix missing markdown include in manifest (#1274)
|
||||||
|
- Update translations for Chinese, Turkish, Czech, Spanish (Latin America), Swedish
|
||||||
|
|
||||||
|
|
||||||
|
## 6.1.3 (2023-11-23)
|
||||||
|
|
||||||
|
- Revert update to flask and werkzeug 2.3 because of a regression (see #1272)
|
||||||
|
|
||||||
|
|
||||||
|
## 6.1.2 (2023-11-19)
|
||||||
|
|
||||||
|
- Fix password generation command line crash (#1242)
|
||||||
|
- Update to flask and werkzeug 2.3 (#1244)
|
||||||
|
|
||||||
|
|
||||||
## 6.1.1 (2023-10-04)
|
## 6.1.1 (2023-10-04)
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
include *.rst
|
|
||||||
recursive-include ihatemoney *.rst *.py *.yaml *.po *.mo *.html *.css *.js *.eot *.svg *.woff *.txt *.png *.webp *.ini *.cfg *.j2 *.jpg *.gif *.ico *.xml
|
|
||||||
include LICENSE CONTRIBUTORS CHANGELOG.rst
|
|
71
Makefile
|
@ -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 pyproject.toml $(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 pyproject.toml $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
|
|
||||||
$(DEV_STAMP): $(PYTHON)
|
|
||||||
$(VENV)/bin/pip install -Ue .[dev]
|
|
||||||
touch $(DEV_STAMP)
|
|
||||||
|
|
||||||
.PHONY: remove-install-stamp
|
|
||||||
remove-install-stamp:
|
|
||||||
rm $(INSTALL_STAMP)
|
|
||||||
|
|
||||||
.PHONY: update
|
|
||||||
update: remove-install-stamp install ## Update the dependencies
|
|
||||||
|
|
||||||
.PHONY: serve
|
.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}'
|
||||||
|
|
33
README.md
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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).
|
||||||
|
@ -274,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
|
||||||
|
|
|
@ -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}
|
||||||
|
|
11
hatch_build.py
Normal 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()
|
|
@ -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
|
@ -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()
|
|
@ -3,6 +3,7 @@ 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
|
||||||
|
@ -14,6 +15,7 @@ APPLICATION_ROOT = "/"
|
||||||
ENABLE_CAPTCHA = False
|
ENABLE_CAPTCHA = False
|
||||||
LEGAL_LINK = ""
|
LEGAL_LINK = ""
|
||||||
SUPPORTED_LANGUAGES = [
|
SUPPORTED_LANGUAGES = [
|
||||||
|
"az",
|
||||||
"ca",
|
"ca",
|
||||||
"cs",
|
"cs",
|
||||||
"de",
|
"de",
|
||||||
|
@ -46,3 +48,4 @@ SUPPORTED_LANGUAGES = [
|
||||||
"uk",
|
"uk",
|
||||||
"zh_Hans",
|
"zh_Hans",
|
||||||
]
|
]
|
||||||
|
SHOWCASE_LANGUAGES = ["en", "fr"]
|
||||||
|
|
|
@ -39,7 +39,7 @@ 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,
|
||||||
|
@ -90,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):
|
||||||
|
@ -195,7 +194,7 @@ class EditProjectForm(FlaskForm):
|
||||||
raise ValidationError(msg)
|
raise ValidationError(msg)
|
||||||
if (
|
if (
|
||||||
project is not None
|
project is not None
|
||||||
and field.data != CurrencyConverter.no_currency
|
and field.data != project.default_currency
|
||||||
and project.has_bills()
|
and project.has_bills()
|
||||||
):
|
):
|
||||||
msg = _(
|
msg = _(
|
||||||
|
@ -364,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"))
|
||||||
|
|
||||||
|
@ -377,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)
|
||||||
|
@ -396,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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ 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
|
||||||
|
@ -93,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()
|
||||||
|
|
|
@ -759,7 +759,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -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())
|
|
@ -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
|
||||||
|
@ -50,6 +51,16 @@ make_versioned(
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BillType(Enum):
|
||||||
|
EXPENSE = "Expense"
|
||||||
|
REIMBURSEMENT = "Reimbursement"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def choices(cls):
|
||||||
|
return [(choice.value, choice.value) for choice in cls]
|
||||||
|
|
||||||
|
|
||||||
db = SQLAlchemy()
|
db = SQLAlchemy()
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,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):
|
||||||
|
@ -160,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
|
||||||
|
|
||||||
|
@ -186,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 []
|
||||||
|
|
||||||
|
@ -202,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
|
||||||
|
@ -336,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),
|
||||||
|
@ -407,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),
|
||||||
|
@ -537,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,
|
||||||
|
@ -677,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))
|
||||||
|
@ -696,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
|
||||||
|
@ -705,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
|
||||||
)
|
)
|
||||||
|
@ -719,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,
|
||||||
|
|
|
@ -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(
|
||||||
|
|
BIN
ihatemoney/static/showcase/en/1.webp
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
ihatemoney/static/showcase/en/2.webp
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
ihatemoney/static/showcase/en/3.webp
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
ihatemoney/static/showcase/en/4.webp
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
ihatemoney/static/showcase/en/5.webp
Normal file
After Width: | Height: | Size: 75 KiB |
BIN
ihatemoney/static/showcase/en/6.webp
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
ihatemoney/static/showcase/en/7.webp
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
ihatemoney/static/showcase/en/8.webp
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
ihatemoney/static/showcase/en/9.webp
Normal file
After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 130 KiB |
|
@ -200,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>
|
||||||
|
|
|
@ -1,90 +1,81 @@
|
||||||
{% extends "layout.html" %}
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<header id="header" class="row">
|
<header id="header" class="row">
|
||||||
<div class="col-xs-12 col-sm-5 offset-md-2">
|
<div class="col-xs-12 col-sm-5 offset-md-2">
|
||||||
<h2>{{ _("Manage your shared <br />expenses, easily") }}</h2>
|
<h2>
|
||||||
|
{{ _("Manage your shared <br />expenses, easily") }}
|
||||||
|
</h2>
|
||||||
{% if is_demo_project_activated %}
|
{% if is_demo_project_activated %}
|
||||||
<a href="{{ url_for('.demo') }}" class="tryout btn">
|
<a href="{{ url_for(".demo") }}" class="tryout btn">{{ _("Try out the demo") }}</a>
|
||||||
{{ _("Try out the demo") }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if g.lang == 'fr' %}
|
{% if g.lang == 'fr' or g.lang == 'en' %}
|
||||||
ou
|
{{ _("or") }}
|
||||||
<span class="side-to-side">
|
<span class="side-to-side">
|
||||||
<a class="showcase btn" onclick="javascript:showGallery(); return false;">Voir la BD explicative</a>
|
<a class="showcase btn"
|
||||||
<img class="showcaseimg" src="{{ url_for("static", filename='images/indicate.svg') }}" />
|
onclick="javascript:showGallery(); return false;">{{ _("See the explanatory comics") }}</a>
|
||||||
|
<img class="showcaseimg"
|
||||||
|
src="{{ url_for("static", filename='images/indicate.svg') }}" />
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-sm-4">
|
<div class="col-xs-12 col-sm-4">
|
||||||
<table class="additional-content"><tr>
|
<table class="additional-content">
|
||||||
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
{{ _("You're sharing a house?") }}<br />
|
{{ _("You're sharing a house?") }}
|
||||||
{{ _("Going on holidays with friends?") }}<br />
|
<br />
|
||||||
{{ _("Simply sharing money with others?") }} <br />
|
{{ _("Going on holidays with friends?") }}
|
||||||
|
<br />
|
||||||
|
{{ _("Simply sharing money with others?") }}
|
||||||
|
<br />
|
||||||
<strong>{{ _("We can help!") }}</strong>
|
<strong>{{ _("We can help!") }}</strong>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<img class="shareimg" src="{{ url_for("static", filename='images/share.svg') }}" />
|
<img class="shareimg"
|
||||||
|
src="{{ url_for("static", filename='images/share.svg') }}" />
|
||||||
</td>
|
</td>
|
||||||
</tr></table>
|
</tr>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main class="row home">
|
<main class="row home">
|
||||||
<div class="card-deck ml-auto mr-auto">
|
<div class="card-deck ml-auto mr-auto">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">{{ _("Log in to an existing project") }}</div>
|
||||||
{{ _("Log in to an existing project") }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form
|
<form id="authentication-form"
|
||||||
id="authentication-form"
|
|
||||||
class="form-horizontal"
|
class="form-horizontal"
|
||||||
action="{{ url_for('.authenticate') }}"
|
action="{{ url_for(".authenticate") }}"
|
||||||
method="post">
|
method="post">
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
<legend></legend>
|
<legend></legend>
|
||||||
{{ forms.authenticate(auth_form, home=True) }}
|
{{ forms.authenticate(auth_form, home=True) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn btn-primary btn-block" type="submit">
|
<button class="btn btn-primary btn-block" type="submit">{{ _("Log in") }}</button>
|
||||||
{{ _("Log in") }}
|
<a class="password-reminder btn btn-link"
|
||||||
</button>
|
href="{{ url_for(".remind_password") }}">{{ _("can't remember your password?") }}</a>
|
||||||
<a
|
|
||||||
class="password-reminder btn btn-link"
|
|
||||||
href="{{ url_for('.remind_password') }}"
|
|
||||||
>{{ _("can't remember your password?") }}</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">{{ _("Create a new project") }}</div>
|
||||||
{{ _("Create a new project") }}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if is_public_project_creation_allowed %}
|
{% if is_public_project_creation_allowed %}
|
||||||
<form
|
<form id="creation-form"
|
||||||
id="creation-form"
|
|
||||||
class="form-horizontal"
|
class="form-horizontal"
|
||||||
action="{{ url_for('.create_project') }}"
|
action="{{ url_for(".create_project") }}"
|
||||||
method="post"
|
method="post">
|
||||||
>
|
|
||||||
<fieldset class="form-group">
|
<fieldset class="form-group">
|
||||||
{{ forms.create_project(project_form, home=True) }}
|
{{ forms.create_project(project_form, home=True) }}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="btn btn-primary btn-block" type="submit">
|
<button class="btn btn-primary btn-block" type="submit">{{ _("Create") }}</button>
|
||||||
{{ _("Create") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('.create_project') }}">
|
<a href="{{ url_for(".create_project") }}">{{ _("Create a new project") }}</a>
|
||||||
{{ _("Create a new project") }}
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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') }}">
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column h-100">
|
<body class="d-flex flex-column h-100">
|
||||||
{% if g.lang == 'fr' %}{% include "showcase.html" %}{% endif %}
|
{% if display_showcase %}{% include "showcase.html" %}{% endif %}
|
||||||
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
|
<nav class="navbar navbar-expand-lg fixed-top navbar-dark bg-dark">
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarToggler" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarToggler" aria-controls="navbarToggler" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
@ -168,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 %}
|
||||||
|
|
|
@ -64,10 +64,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="identifier">
|
<div class="identifier">
|
||||||
{% if g.lang == 'fr' %}
|
{% if display_showcase %}
|
||||||
<a class="btn btn-secondary btn-block" href="" onclick="javascript:showGallery(); return false;">
|
<a class="btn btn-secondary btn-block" href="" onclick="javascript:showGallery(); return false;">
|
||||||
<i class="icon icon-white high before-text">{{ static_include("images/read.svg") | safe }}</i>
|
<i class="icon icon-white high before-text">{{ static_include("images/read.svg") | safe }}</i>
|
||||||
Voir la BD explicative
|
{{ _("See the explanatory comics") }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary btn-block" href="{{ url_for('.invite') }}">
|
<a class="btn btn-secondary btn-block" href="{{ url_for('.invite') }}">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
<div id="pswp" class="hiddenpswp" tabindex="-1" role="dialog" aria-hidden="true">
|
<div id="pswp"
|
||||||
|
class="hiddenpswp"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
aria-hidden="true">
|
||||||
<div class="pswp__bg"></div>
|
<div class="pswp__bg"></div>
|
||||||
<div class="pswp__scroll-wrap">
|
<div class="pswp__scroll-wrap">
|
||||||
<div class="pswp__container">
|
<div class="pswp__container">
|
||||||
|
@ -9,7 +13,7 @@
|
||||||
<div class="pswp__ui pswp__ui--hidden">
|
<div class="pswp__ui pswp__ui--hidden">
|
||||||
<div class="pswp__top-bar">
|
<div class="pswp__top-bar">
|
||||||
<div class="pswp__counter"></div>
|
<div class="pswp__counter"></div>
|
||||||
<button class="pswp__button pswp__button--close" title="Fermer (Esc)"></button>
|
<button class="pswp__button pswp__button--close" title="{{ _("Close (Esc)") }}">
|
||||||
<div class="pswp__preloader">
|
<div class="pswp__preloader">
|
||||||
<div class="pswp__preloader__icn">
|
<div class="pswp__preloader__icn">
|
||||||
<div class="pswp__preloader__cut">
|
<div class="pswp__preloader__cut">
|
||||||
|
@ -18,13 +22,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="pswp__button pswp__button--arrow--left"
|
||||||
<button class="pswp__button pswp__button--arrow--left" title="Suivant (flèche droite)">
|
title="{{ _("Next (right arrow)") }}"></button>
|
||||||
</button>
|
<button class="pswp__button pswp__button--arrow--right"
|
||||||
|
title="{{ _("Previous (left arrow)") }}"></button>
|
||||||
<button class="pswp__button pswp__button--arrow--right" title="Précédent (flèche gauche)">
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="pswp__caption">
|
<div class="pswp__caption">
|
||||||
<div class="pswp__caption__center"></div>
|
<div class="pswp__caption__center"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,18 +33,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var pswpElement = document.getElementById('pswp');
|
var pswpElement = document.getElementById("pswp");
|
||||||
var items = JSON.parse('[{"h": 2450, "src": "{{ url_for("static", filename="showcase/1.webp") }}", "w": 2450}, {"h": 2509, "src": "{{ url_for("static", filename="showcase/2.webp") }}", "w": 2221}, {"h": 2536, "src": "{{ url_for("static", filename="showcase/3.webp") }}", "w": 2101}, {"h": 2722, "src": "{{ url_for("static", filename="showcase/4.webp") }}", "w": 2348}, {"h": 2745, "src": "{{ url_for("static", filename="showcase/5.webp") }}", "w": 1804}, {"h": 3307, "src": "{{ url_for("static", filename="showcase/6.webp") }}", "w": 2897}, {"h": 2321, "src": "{{ url_for("static", filename="showcase/7.webp") }}", "w": 2239}, {"h": 2470, "src": "{{ url_for("static", filename="showcase/8.webp") }}", "w": 2419}, {"h": 3307, "src": "{{ url_for("static", filename="showcase/9.webp") }}", "w": 2602}]');
|
|
||||||
var options = {
|
var options = {
|
||||||
index: 0,
|
index: 0,
|
||||||
loop: false,
|
loop: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function showGallery(){
|
function showGallery(){
|
||||||
|
let items = [
|
||||||
|
{"h": 2450, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/1.webp') }}", "w": 2450},
|
||||||
|
{"h": 2509, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/2.webp') }}", "w": 2221},
|
||||||
|
{"h": 2536, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/3.webp') }}", "w": 2101},
|
||||||
|
{"h": 2722, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/4.webp') }}", "w": 2348},
|
||||||
|
{"h": 2745, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/5.webp') }}", "w": 1804},
|
||||||
|
{"h": 3307, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/6.webp') }}", "w": 2897},
|
||||||
|
{"h": 2321, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/7.webp') }}", "w": 2239},
|
||||||
|
{"h": 2470, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/8.webp') }}", "w": 2419},
|
||||||
|
{"h": 3307, "src": "{{ url_for('static', filename='showcase/' + g.lang + '/9.webp') }}", "w": 2602}
|
||||||
|
];
|
||||||
|
|
||||||
/* the CSS and JS for photoswipe is loaded dynamically
|
// CSS and JS are loaded dynamically when the gallery opens
|
||||||
* so that they are not loaded if the gallery is not open */
|
|
||||||
|
|
||||||
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/default-skin/default-skin.css") }}">');
|
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/default-skin/default-skin.css") }}">');
|
||||||
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/photoswipe.css") }}">');
|
$('head').append('<link rel="stylesheet" type="text/css" href="{{ url_for("static", filename="photoswipe/photoswipe.css") }}">');
|
||||||
|
|
||||||
|
@ -63,5 +72,4 @@ function showGallery(){
|
||||||
};
|
};
|
||||||
document.body.appendChild(script);
|
document.body.appendChild(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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 -%}
|
||||||
|
|
|
@ -9,7 +9,6 @@ from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAPI(IhatemoneyTestCase):
|
class TestAPI(IhatemoneyTestCase):
|
||||||
|
|
||||||
"""Tests the API"""
|
"""Tests the API"""
|
||||||
|
|
||||||
def api_create(
|
def api_create(
|
||||||
|
@ -43,7 +42,7 @@ class TestAPI(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", "")
|
||||||
)
|
)
|
||||||
|
@ -407,6 +406,7 @@ class TestAPI(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",
|
||||||
},
|
},
|
||||||
|
@ -431,6 +431,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "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,
|
||||||
|
@ -462,6 +463,7 @@ class TestAPI(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",
|
||||||
},
|
},
|
||||||
|
@ -479,6 +481,7 @@ class TestAPI(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",
|
||||||
},
|
},
|
||||||
|
@ -500,6 +503,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "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",
|
||||||
|
@ -554,6 +558,7 @@ class TestAPI(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"),
|
||||||
|
@ -578,6 +583,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "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,
|
||||||
|
@ -611,6 +617,7 @@ class TestAPI(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"),
|
||||||
|
@ -658,6 +665,7 @@ class TestAPI(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",
|
||||||
},
|
},
|
||||||
|
@ -682,6 +690,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "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,
|
||||||
|
@ -706,6 +715,7 @@ class TestAPI(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",
|
||||||
|
@ -727,6 +737,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
|
||||||
{"activated": True, "id": 2, "name": "jeanne", "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,
|
||||||
|
@ -747,6 +758,7 @@ class TestAPI(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",
|
||||||
},
|
},
|
||||||
|
@ -791,6 +803,7 @@ class TestAPI(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"),
|
||||||
|
@ -855,6 +868,7 @@ class TestAPI(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"),
|
||||||
|
@ -877,6 +891,7 @@ class TestAPI(IhatemoneyTestCase):
|
||||||
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
|
||||||
{"activated": True, "id": 2, "name": "jeannedy 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,
|
||||||
|
@ -962,6 +977,7 @@ class TestAPI(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"),
|
||||||
|
@ -990,8 +1006,71 @@ class TestAPI(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")
|
||||||
|
|
||||||
|
req = self.client.post(
|
||||||
|
"/api/projects/raclette/bills",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["1"],
|
||||||
|
"bill_type": "wrong_bill_type",
|
||||||
|
"amount": "50",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertStatus(400, req)
|
||||||
|
|
||||||
|
req = self.client.post(
|
||||||
|
"/api/projects/raclette/bills",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["1"],
|
||||||
|
"bill_type": "Expense",
|
||||||
|
"amount": "50",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertStatus(201, req)
|
||||||
|
|
||||||
|
def test_default_bill_type(self):
|
||||||
|
self.api_create("raclette")
|
||||||
|
self.api_add_member("raclette", "zorglub")
|
||||||
|
|
||||||
|
# Post a bill without adding a bill type
|
||||||
|
req = self.client.post(
|
||||||
|
"/api/projects/raclette/bills",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage",
|
||||||
|
"payer": "1",
|
||||||
|
"payed_for": ["1"],
|
||||||
|
"amount": "50",
|
||||||
|
},
|
||||||
|
headers=self.get_auth("raclette"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertStatus(201, req)
|
||||||
|
|
||||||
|
req = self.client.get(
|
||||||
|
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
|
||||||
|
)
|
||||||
|
self.assertStatus(200, req)
|
||||||
|
|
||||||
|
# Bill type should now be "Expense"
|
||||||
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
|
assert got["bill_type"] == "Expense"
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import datetime
|
from datetime import datetime, timedelta, date
|
||||||
import re
|
import re
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import unquote, urlparse, urlunparse
|
||||||
|
|
||||||
from flask import session, url_for
|
from flask import session, url_for
|
||||||
from libfaketime import fake_time
|
|
||||||
import pytest
|
import pytest
|
||||||
from werkzeug.security import check_password_hash
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
@ -239,7 +238,10 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
url, data={"password": "pass", "password_confirmation": "pass"}
|
url, data={"password": "pass", "password_confirmation": "pass"}
|
||||||
)
|
)
|
||||||
resp = self.login("raclette", password="pass")
|
resp = self.login("raclette", password="pass")
|
||||||
assert "<title>Account manager - raclette</title>" in resp.data.decode("utf-8")
|
assert (
|
||||||
|
"<title>I Hate Money — Account manager - raclette</title>"
|
||||||
|
in resp.data.decode("utf-8")
|
||||||
|
)
|
||||||
# Test empty and null tokens
|
# Test empty and null tokens
|
||||||
resp = self.client.get("/reset-password")
|
resp = self.client.get("/reset-password")
|
||||||
assert "No token provided" in resp.data.decode("utf-8")
|
assert "No token provided" in resp.data.decode("utf-8")
|
||||||
|
@ -428,6 +430,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": jeanne_id,
|
"payer": jeanne_id,
|
||||||
"payed_for": [jeanne_id],
|
"payed_for": [jeanne_id],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -479,6 +482,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": zorglub.id,
|
"payer": zorglub.id,
|
||||||
"payed_for": [zorglub.id],
|
"payed_for": [zorglub.id],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -646,6 +650,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -661,6 +666,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -684,6 +690,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "19",
|
"amount": "19",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -695,6 +702,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[1],
|
"payer": members_ids[1],
|
||||||
"payed_for": members_ids[0],
|
"payed_for": members_ids[0],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -706,6 +714,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[1],
|
"payer": members_ids[1],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "17",
|
"amount": "17",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -721,6 +730,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "-25",
|
"amount": "-25",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -735,6 +745,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "25,02",
|
"amount": "25,02",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -749,6 +760,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "42",
|
"amount": "42",
|
||||||
"external_link": "https://example.com/fromage",
|
"external_link": "https://example.com/fromage",
|
||||||
},
|
},
|
||||||
|
@ -764,12 +776,63 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "mauvais fromage à raclette",
|
"what": "mauvais fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "42000",
|
"amount": "42000",
|
||||||
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
|
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert "Invalid URL" in resp.data.decode("utf-8")
|
assert "Invalid URL" in resp.data.decode("utf-8")
|
||||||
|
|
||||||
|
def test_reimbursement_bill(self):
|
||||||
|
self.post_project("rent")
|
||||||
|
|
||||||
|
# add two participants
|
||||||
|
self.client.post("/rent/members/add", data={"name": "bob"})
|
||||||
|
self.client.post("/rent/members/add", data={"name": "alice"})
|
||||||
|
|
||||||
|
members_ids = [m.id for m in self.get_project("rent").members]
|
||||||
|
# create a bill to test reimbursement
|
||||||
|
self.client.post(
|
||||||
|
"/rent/add",
|
||||||
|
data={
|
||||||
|
"date": "2022-12-12",
|
||||||
|
"what": "december rent",
|
||||||
|
"payer": members_ids[0], # bob
|
||||||
|
"payed_for": members_ids, # bob and alice
|
||||||
|
"bill_type": "Expense",
|
||||||
|
"amount": "1000",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# check balance
|
||||||
|
balance = self.get_project("rent").balance
|
||||||
|
assert set(balance.values()), set([500 == -500])
|
||||||
|
# check paid
|
||||||
|
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
|
||||||
|
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
|
||||||
|
assert bob_paid == 1000
|
||||||
|
assert alice_paid == 0
|
||||||
|
|
||||||
|
# test reimbursement bill
|
||||||
|
self.client.post(
|
||||||
|
"/rent/add",
|
||||||
|
data={
|
||||||
|
"date": "2022-12-13",
|
||||||
|
"what": "reimbursement for rent",
|
||||||
|
"payer": members_ids[1], # alice
|
||||||
|
"payed_for": members_ids[0], # bob
|
||||||
|
"bill_type": "Reimbursement",
|
||||||
|
"amount": "500",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
balance = self.get_project("rent").balance
|
||||||
|
assert set(balance.values()), set([0 == 0])
|
||||||
|
# check paid
|
||||||
|
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
|
||||||
|
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
|
||||||
|
assert bob_paid == 500
|
||||||
|
assert alice_paid == 500
|
||||||
|
|
||||||
def test_weighted_balance(self):
|
def test_weighted_balance(self):
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
@ -789,6 +852,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": members_ids[0],
|
"payer": members_ids[0],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -800,6 +864,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "pommes de terre",
|
"what": "pommes de terre",
|
||||||
"payer": members_ids[1],
|
"payer": members_ids[1],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -864,6 +929,7 @@ class TestBudget(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": "24.36",
|
"amount": "24.36",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -875,6 +941,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "19.12",
|
"amount": "19.12",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -886,6 +953,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "delicatessen",
|
"what": "delicatessen",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "22",
|
"amount": "22",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -964,9 +1032,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
assert """<thead>
|
assert """<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Project</th>
|
<th>Project</th>
|
||||||
<th>Number of participants</th>""" in resp.data.decode(
|
<th>Number of participants</th>""" in resp.data.decode("utf-8")
|
||||||
"utf-8"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_dashboard_project_deletion(self):
|
def test_dashboard_project_deletion(self):
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
@ -1019,6 +1085,7 @@ class TestBudget(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1030,6 +1097,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1041,6 +1109,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "delicatessen",
|
"what": "delicatessen",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1077,7 +1146,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
assert re.search(re.compile(regex2, re.DOTALL), response.data.decode("utf-8"))
|
assert re.search(re.compile(regex2, re.DOTALL), response.data.decode("utf-8"))
|
||||||
|
|
||||||
# Check monthly expenses again: it should have a single month and the correct amount
|
# Check monthly expenses again: it should have a single month and the correct amount
|
||||||
august = datetime.date(year=2011, month=8, day=1)
|
august = date(year=2011, month=8, day=1)
|
||||||
assert project.active_months_range() == [august]
|
assert project.active_months_range() == [august]
|
||||||
assert dict(project.monthly_stats[2011]) == {8: 40.0}
|
assert dict(project.monthly_stats[2011]) == {8: 40.0}
|
||||||
|
|
||||||
|
@ -1089,15 +1158,16 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "30",
|
"amount": "30",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
months = [
|
months = [
|
||||||
datetime.date(year=2011, month=12, day=1),
|
date(year=2011, month=12, day=1),
|
||||||
datetime.date(year=2011, month=11, day=1),
|
date(year=2011, month=11, day=1),
|
||||||
datetime.date(year=2011, month=10, day=1),
|
date(year=2011, month=10, day=1),
|
||||||
datetime.date(year=2011, month=9, day=1),
|
date(year=2011, month=9, day=1),
|
||||||
datetime.date(year=2011, month=8, day=1),
|
date(year=2011, month=8, day=1),
|
||||||
]
|
]
|
||||||
amounts_2011 = {
|
amounts_2011 = {
|
||||||
12: 30.0,
|
12: 30.0,
|
||||||
|
@ -1114,6 +1184,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "ice cream",
|
"what": "ice cream",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1129,6 +1200,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "champomy",
|
"what": "champomy",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1144,10 +1216,11 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "smoothie",
|
"what": "smoothie",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
months.append(datetime.date(year=2011, month=7, day=1))
|
months.append(date(year=2011, month=7, day=1))
|
||||||
amounts_2011[7] = 20.0
|
amounts_2011[7] = 20.0
|
||||||
assert project.active_months_range() == months
|
assert project.active_months_range() == months
|
||||||
assert dict(project.monthly_stats[2011]) == amounts_2011
|
assert dict(project.monthly_stats[2011]) == amounts_2011
|
||||||
|
@ -1160,10 +1233,11 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "more champomy",
|
"what": "more champomy",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "30",
|
"amount": "30",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
months.insert(0, datetime.date(year=2012, month=1, day=1))
|
months.insert(0, date(year=2012, month=1, day=1))
|
||||||
amounts_2012 = {1: 30.0}
|
amounts_2012 = {1: 30.0}
|
||||||
assert project.active_months_range() == months
|
assert project.active_months_range() == months
|
||||||
assert dict(project.monthly_stats[2011]) == amounts_2011
|
assert dict(project.monthly_stats[2011]) == amounts_2011
|
||||||
|
@ -1192,6 +1266,7 @@ class TestBudget(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1203,6 +1278,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1214,6 +1290,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "delicatessen",
|
"what": "delicatessen",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1229,6 +1306,76 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
assert abs(a - balance[m.id]) < 0.01
|
assert abs(a - balance[m.id]) < 0.01
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def test_settle_button(self):
|
||||||
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
# add participants
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "zorglub"})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "jeanne"})
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "tata"})
|
||||||
|
# Add a participant with a balance at 0 :
|
||||||
|
self.client.post("/raclette/members/add", data={"name": "pépé"})
|
||||||
|
|
||||||
|
# create bills
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "fromage à raclette",
|
||||||
|
"payer": 1,
|
||||||
|
"payed_for": [1, 2, 3],
|
||||||
|
"bill_type": "Expense",
|
||||||
|
"amount": "10.0",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "red wine",
|
||||||
|
"payer": 2,
|
||||||
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
|
"amount": "20",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
"/raclette/add",
|
||||||
|
data={
|
||||||
|
"date": "2011-08-10",
|
||||||
|
"what": "delicatessen",
|
||||||
|
"payer": 1,
|
||||||
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
|
"amount": "10",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
project = self.get_project("raclette")
|
||||||
|
transactions = project.get_transactions_to_settle_bill()
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for t in transactions:
|
||||||
|
count += 1
|
||||||
|
self.client.get(
|
||||||
|
"/raclette/settle"
|
||||||
|
+ "/"
|
||||||
|
+ str(t["amount"])
|
||||||
|
+ "/"
|
||||||
|
+ str(t["ower"].id)
|
||||||
|
+ "/"
|
||||||
|
+ str(t["receiver"].id)
|
||||||
|
)
|
||||||
|
temp_transactions = project.get_transactions_to_settle_bill()
|
||||||
|
# test if the one has disappeared
|
||||||
|
assert len(temp_transactions) == len(transactions) - count
|
||||||
|
|
||||||
|
# test if theres a new one with bill_type reimbursement
|
||||||
|
bill = project.get_newest_bill()
|
||||||
|
assert bill.bill_type == models.BillType.REIMBURSEMENT
|
||||||
|
return
|
||||||
|
|
||||||
def test_settle_zero(self):
|
def test_settle_zero(self):
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
|
|
||||||
|
@ -1245,6 +1392,7 @@ class TestBudget(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1256,6 +1404,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 3],
|
"payed_for": [1, 3],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1267,6 +1416,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
"payer": 3,
|
"payer": 3,
|
||||||
"payed_for": [2],
|
"payed_for": [2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "13.33",
|
"amount": "13.33",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1299,6 +1449,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2, 3, 4],
|
"payed_for": [1, 2, 3, 4],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10.0",
|
"amount": "10.0",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1317,6 +1468,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "roblochon",
|
"what": "roblochon",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 3, 4],
|
"payed_for": [1, 3, 4],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "100.0",
|
"amount": "100.0",
|
||||||
}
|
}
|
||||||
# Try to access bill of another project
|
# Try to access bill of another project
|
||||||
|
@ -1422,6 +1574,7 @@ class TestBudget(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1433,6 +1586,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 3],
|
"payed_for": [1, 3],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1444,6 +1598,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
"payer": 3,
|
"payer": 3,
|
||||||
"payed_for": [2],
|
"payed_for": [2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "13.33",
|
"amount": "13.33",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1469,6 +1624,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "refund from EUR",
|
"what": "refund from EUR",
|
||||||
"payer": 3,
|
"payer": 3,
|
||||||
"payed_for": [2],
|
"payed_for": [2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
},
|
},
|
||||||
|
@ -1492,6 +1648,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "Poutine",
|
"what": "Poutine",
|
||||||
"payer": 3,
|
"payer": 3,
|
||||||
"payed_for": [2],
|
"payed_for": [2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "18",
|
"amount": "18",
|
||||||
"original_currency": "CAD",
|
"original_currency": "CAD",
|
||||||
},
|
},
|
||||||
|
@ -1548,6 +1705,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10.0",
|
"amount": "10.0",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
},
|
},
|
||||||
|
@ -1583,6 +1741,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10.0",
|
"amount": "10.0",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
},
|
},
|
||||||
|
@ -1595,6 +1754,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "aspirine",
|
"what": "aspirine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "5.0",
|
"amount": "5.0",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
},
|
},
|
||||||
|
@ -1629,6 +1789,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "0",
|
"amount": "0",
|
||||||
"original_currency": "XXX",
|
"original_currency": "XXX",
|
||||||
},
|
},
|
||||||
|
@ -1673,6 +1834,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"what": "fromage à raclette",
|
"what": "fromage à raclette",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "9347242149381274732472348728748723473278472843.12",
|
"amount": "9347242149381274732472348728748723473278472843.12",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
},
|
},
|
||||||
|
@ -1708,7 +1870,6 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"""
|
"""
|
||||||
Tests that the RSS feed output content is expected.
|
Tests that the RSS feed output content is expected.
|
||||||
"""
|
"""
|
||||||
with fake_time("2023-07-25 12:00:00"):
|
|
||||||
self.post_project("raclette", default_currency="EUR")
|
self.post_project("raclette", default_currency="EUR")
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "peter"})
|
self.client.post("/raclette/members/add", data={"name": "peter"})
|
||||||
|
@ -1723,6 +1884,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2, 3],
|
"payed_for": [1, 2, 3],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
|
@ -1734,6 +1896,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "15",
|
"amount": "15",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
|
@ -1745,6 +1908,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1752,12 +1916,10 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
token = project.generate_token("feed")
|
token = project.generate_token("feed")
|
||||||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||||
|
|
||||||
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
|
content = resp.data.decode()
|
||||||
<rss version="2.0"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
assert (
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
f"""<channel>
|
||||||
>
|
|
||||||
<channel>
|
|
||||||
<title>I Hate Money — raclette</title>
|
<title>I Hate Money — raclette</title>
|
||||||
<description>Latest bills from raclette</description>
|
<description>Latest bills from raclette</description>
|
||||||
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
|
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
|
||||||
|
@ -1767,32 +1929,18 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
<guid isPermaLink="false">1</guid>
|
<guid isPermaLink="false">1</guid>
|
||||||
<dc:creator>george</dc:creator>
|
<dc:creator>george</dc:creator>
|
||||||
<description>December 31, 2016 - george, peter, steven : €4.00</description>
|
<description>December 31, 2016 - george, peter, steven : €4.00</description>
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
"""
|
||||||
</item>
|
in content
|
||||||
<item>
|
)
|
||||||
<title>charcuterie - €15.00</title>
|
|
||||||
<guid isPermaLink="false">2</guid>
|
assert """<title>charcuterie - €15.00</title>""" in content
|
||||||
<dc:creator>peter</dc:creator>
|
assert """<title>vin blanc - €10.00</title>""" in content
|
||||||
<description>December 30, 2016 - george, peter : €7.50</description>
|
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>vin blanc - €10.00</title>
|
|
||||||
<guid isPermaLink="false">3</guid>
|
|
||||||
<dc:creator>peter</dc:creator>
|
|
||||||
<description>December 29, 2016 - george, peter : €5.00</description>
|
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
|
||||||
</item>
|
|
||||||
</channel>
|
|
||||||
</rss>""" # noqa: E501
|
|
||||||
assert resp.data.decode() == expected_rss_content
|
|
||||||
|
|
||||||
def test_rss_feed_history_disabled(self):
|
def test_rss_feed_history_disabled(self):
|
||||||
"""
|
"""
|
||||||
Tests that RSS feeds is correctly rendered even if the project
|
Tests that RSS feeds is correctly rendered even if the project
|
||||||
history is disabled.
|
history is disabled.
|
||||||
"""
|
"""
|
||||||
with fake_time("2023-07-25 12:00:00"):
|
|
||||||
self.post_project("raclette", default_currency="EUR", project_history=False)
|
self.post_project("raclette", default_currency="EUR", project_history=False)
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
self.client.post("/raclette/members/add", data={"name": "peter"})
|
self.client.post("/raclette/members/add", data={"name": "peter"})
|
||||||
|
@ -1807,6 +1955,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2, 3],
|
"payed_for": [1, 2, 3],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
|
@ -1818,6 +1967,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "15",
|
"amount": "15",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.client.post(
|
self.client.post(
|
||||||
|
@ -1829,6 +1979,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
"original_currency": "EUR",
|
"original_currency": "EUR",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1836,44 +1987,12 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
token = project.generate_token("feed")
|
token = project.generate_token("feed")
|
||||||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||||
|
|
||||||
expected_rss_content = f"""<?xml version="1.0" encoding="utf-8"?>
|
content = resp.data.decode()
|
||||||
<rss version="2.0"
|
assert """<title>charcuterie - €15.00</title>""" in content
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
assert """<title>vin blanc - €10.00</title>""" in content
|
||||||
xmlns:atom="http://www.w3.org/2005/Atom"
|
|
||||||
>
|
|
||||||
<channel>
|
|
||||||
<title>I Hate Money — raclette</title>
|
|
||||||
<description>Latest bills from raclette</description>
|
|
||||||
<atom:link href="http://localhost/raclette/feed/{token}.xml" rel="self" type="application/rss+xml" />
|
|
||||||
<link>http://localhost/raclette/</link>
|
|
||||||
<item>
|
|
||||||
<title>fromage à raclette - €12.00</title>
|
|
||||||
<guid isPermaLink="false">1</guid>
|
|
||||||
<dc:creator>george</dc:creator>
|
|
||||||
<description>December 31, 2016 - george, peter, steven : €4.00</description>
|
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>charcuterie - €15.00</title>
|
|
||||||
<guid isPermaLink="false">2</guid>
|
|
||||||
<dc:creator>peter</dc:creator>
|
|
||||||
<description>December 30, 2016 - george, peter : €7.50</description>
|
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<title>vin blanc - €10.00</title>
|
|
||||||
<guid isPermaLink="false">3</guid>
|
|
||||||
<dc:creator>peter</dc:creator>
|
|
||||||
<description>December 29, 2016 - george, peter : €5.00</description>
|
|
||||||
<pubDate>Tue, 25 Jul 2023 00:00:00 +0000</pubDate>
|
|
||||||
</item>
|
|
||||||
</channel>
|
|
||||||
</rss>""" # noqa: E501
|
|
||||||
assert resp.data.decode() == expected_rss_content
|
|
||||||
|
|
||||||
def test_rss_if_modified_since_header(self):
|
def test_rss_if_modified_since_header(self):
|
||||||
# Project creation
|
# Project creation
|
||||||
with fake_time("2023-07-26 13:00:00"):
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
project = self.get_project("raclette")
|
project = self.get_project("raclette")
|
||||||
|
@ -1881,22 +2000,33 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
|
|
||||||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.headers.get("Last-Modified") == "Wed, 26 Jul 2023 13:00:00 UTC"
|
assert "Last-Modified" in resp.headers.keys()
|
||||||
|
last_modified = resp.headers.get("Last-Modified")
|
||||||
|
|
||||||
|
# Get a date 1 hour before the last modified date
|
||||||
|
before = datetime.strptime(
|
||||||
|
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
|
||||||
|
) - timedelta(hours=1)
|
||||||
|
before_str = before.strftime("%a, %d %b %Y %H:%M:%S %Z")
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={"If-Modified-Since": "Tue, 26 Jul 2023 12:00:00 UTC"},
|
headers={"If-Modified-Since": before_str},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
after = datetime.strptime(
|
||||||
|
last_modified, "%a, %d %b %Y %H:%M:%S %Z"
|
||||||
|
) + timedelta(hours=1)
|
||||||
|
after_str = after.strftime("%a, %d %b %Y %H:%M:%S %Z")
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={"If-Modified-Since": "Tue, 26 Jul 2023 14:00:00 UTC"},
|
headers={"If-Modified-Since": after_str},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 304
|
assert resp.status_code == 304
|
||||||
|
|
||||||
# Add bill
|
# Add bill
|
||||||
with fake_time("2023-07-27 13:00:00"):
|
|
||||||
self.login("raclette")
|
self.login("raclette")
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
"/raclette/add",
|
"/raclette/add",
|
||||||
|
@ -1907,6 +2037,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
"original_currency": "XXX",
|
"original_currency": "XXX",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
@ -1915,38 +2046,34 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={"If-Modified-Since": "Tue, 27 Jul 2023 12:00:00 UTC"},
|
headers={"If-Modified-Since": before_str},
|
||||||
)
|
)
|
||||||
assert resp.headers.get("Last-Modified") == "Thu, 27 Jul 2023 13:00:00 UTC"
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={"If-Modified-Since": "Tue, 27 Jul 2023 14:00:00 UTC"},
|
headers={"If-Modified-Since": after_str},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 304
|
assert resp.status_code == 304
|
||||||
|
|
||||||
def test_rss_etag_headers(self):
|
def test_rss_etag_headers(self):
|
||||||
# Project creation
|
# Project creation
|
||||||
with fake_time("2023-07-26 13:00:00"):
|
|
||||||
self.post_project("raclette")
|
self.post_project("raclette")
|
||||||
self.client.post("/raclette/members/add", data={"name": "george"})
|
self.client.post("/raclette/members/add", data={"name": "george"})
|
||||||
project = self.get_project("raclette")
|
project = self.get_project("raclette")
|
||||||
token = project.generate_token("feed")
|
token = project.generate_token("feed")
|
||||||
|
|
||||||
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
resp = self.client.get(f"/raclette/feed/{token}.xml")
|
||||||
assert resp.headers.get("ETag") == build_etag(
|
etag = resp.headers.get("ETag")
|
||||||
project.id, "2023-07-26T13:00:00"
|
|
||||||
)
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={
|
headers={
|
||||||
"If-None-Match": build_etag(project.id, "2023-07-26T12:00:00"),
|
"If-None-Match": etag,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 304
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
|
@ -1954,10 +2081,9 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"),
|
"If-None-Match": build_etag(project.id, "2023-07-26T13:00:00"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 304
|
assert resp.status_code == 200
|
||||||
|
|
||||||
# Add bill
|
# Add bill
|
||||||
with fake_time("2023-07-27 13:00:00"):
|
|
||||||
self.login("raclette")
|
self.login("raclette")
|
||||||
resp = self.client.post(
|
resp = self.client.post(
|
||||||
"/raclette/add",
|
"/raclette/add",
|
||||||
|
@ -1967,26 +2093,26 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
"amount": "12",
|
"amount": "12",
|
||||||
|
"bill_type": "Expense",
|
||||||
"original_currency": "XXX",
|
"original_currency": "XXX",
|
||||||
},
|
},
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert "The bill has been added" in resp.data.decode()
|
assert "The bill has been added" in resp.data.decode()
|
||||||
|
etag = resp.headers.get("ETag")
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={
|
headers={"If-None-Match": etag},
|
||||||
"If-None-Match": build_etag(project.id, "2023-07-27T12:00:00"),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
assert resp.headers.get("ETag") == build_etag(project.id, "2023-07-27T13:00:00")
|
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
new_etag = resp.headers.get("ETag")
|
||||||
|
|
||||||
resp = self.client.get(
|
resp = self.client.get(
|
||||||
f"/raclette/feed/{token}.xml",
|
f"/raclette/feed/{token}.xml",
|
||||||
headers={
|
headers={
|
||||||
"If-None-Match": build_etag(project.id, "2023-07-27T13:00:00"),
|
"If-None-Match": new_etag,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assert resp.status_code == 304
|
assert resp.status_code == 304
|
||||||
|
@ -2074,6 +2200,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": members_ids[1],
|
"payer": members_ids[1],
|
||||||
"payed_for": members_ids,
|
"payed_for": members_ids,
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2091,6 +2218,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": members_ids_tartif[2],
|
"payer": members_ids_tartif[2],
|
||||||
"payed_for": members_ids_tartif,
|
"payed_for": members_ids_tartif,
|
||||||
"amount": "24",
|
"amount": "24",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -2125,6 +2253,7 @@ class TestBudget(IhatemoneyTestCase):
|
||||||
"payer": members_ids[1],
|
"payer": members_ids[1],
|
||||||
"payed_for": members_ids[1:],
|
"payed_for": members_ids[1:],
|
||||||
"amount": "25",
|
"amount": "25",
|
||||||
|
"bill_type": "Expense",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,32 @@
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from jinja2 import FileSystemBytecodeCache
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from ihatemoney.babel_utils import compile_catalogs
|
||||||
from ihatemoney.currency_convertor import CurrencyConverter
|
from ihatemoney.currency_convertor import CurrencyConverter
|
||||||
from ihatemoney.run import create_app, db
|
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
|
@pytest.fixture
|
||||||
def app(request: pytest.FixtureRequest):
|
def app(request: pytest.FixtureRequest, jinja_cache_directory):
|
||||||
"""Create the Flask app with database"""
|
"""Create the Flask app with database"""
|
||||||
app = create_app(request.cls)
|
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():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
request.cls.app = app
|
request.cls.app = app
|
||||||
|
|
|
@ -212,6 +212,7 @@ class TestHistory(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,
|
||||||
|
@ -228,6 +229,7 @@ class TestHistory(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,
|
||||||
|
@ -371,6 +373,7 @@ class TestHistory(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,
|
||||||
|
@ -391,6 +394,7 @@ class TestHistory(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,
|
||||||
|
@ -477,6 +481,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -487,6 +492,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -505,6 +511,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -545,6 +552,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -567,6 +575,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -627,6 +636,7 @@ class TestHistory(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",
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
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"
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,13 @@ def import_data(request: pytest.FixtureRequest):
|
||||||
"amount": 13.33,
|
"amount": 13.33,
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
"bill_type": "Expense",
|
||||||
"owers": ["jeanne"],
|
"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": "jeanne",
|
"payer_name": "jeanne",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -28,6 +30,7 @@ def import_data(request: pytest.FixtureRequest):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
|
"bill_type": "Expense",
|
||||||
"what": "fromage a raclette",
|
"what": "fromage a raclette",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"payer_name": "zorglub",
|
"payer_name": "zorglub",
|
||||||
|
@ -48,6 +51,7 @@ 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,
|
||||||
|
@ -56,6 +60,7 @@ class CommonTestCase(object):
|
||||||
{
|
{
|
||||||
"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": "jeanne",
|
"payer_name": "jeanne",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -63,7 +68,8 @@ class CommonTestCase(object):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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,
|
||||||
|
@ -108,6 +114,7 @@ class CommonTestCase(object):
|
||||||
assert b["currency"] == d["currency"]
|
assert b["currency"] == d["currency"]
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert 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"]]
|
||||||
|
@ -150,6 +157,7 @@ class CommonTestCase(object):
|
||||||
assert b["currency"] == "XXX"
|
assert b["currency"] == "XXX"
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert 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"]]
|
||||||
|
@ -208,6 +216,7 @@ class CommonTestCase(object):
|
||||||
assert b["currency"] == "EUR"
|
assert b["currency"] == "EUR"
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert 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"]]
|
||||||
|
@ -247,6 +256,7 @@ class CommonTestCase(object):
|
||||||
assert b["currency"] == "XXX"
|
assert b["currency"] == "XXX"
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert 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"]]
|
||||||
|
@ -271,6 +281,7 @@ class CommonTestCase(object):
|
||||||
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",
|
||||||
|
@ -303,6 +314,7 @@ class CommonTestCase(object):
|
||||||
assert b["currency"] == d["currency"]
|
assert b["currency"] == d["currency"]
|
||||||
assert b["payer_weight"] == d["payer_weight"]
|
assert b["payer_weight"] == d["payer_weight"]
|
||||||
assert 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"]]
|
||||||
|
@ -326,6 +338,7 @@ 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": ["jeanne"],
|
"owers": ["jeanne"],
|
||||||
|
@ -353,7 +366,8 @@ class TestExport(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",
|
||||||
|
@ -364,6 +378,7 @@ class TestExport(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],
|
||||||
|
@ -375,6 +390,7 @@ class TestExport(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],
|
||||||
|
@ -387,6 +403,7 @@ class TestExport(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",
|
||||||
|
@ -396,6 +413,7 @@ class TestExport(IhatemoneyTestCase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
|
@ -405,7 +423,8 @@ class TestExport(IhatemoneyTestCase):
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
|
@ -418,10 +437,10 @@ class TestExport(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",
|
||||||
"2017-01-01,refund,XXX,13.33,tata,1.0,jeanne",
|
"2017-01-01,refund,Reimbursement,XXX,13.33,tata,1.0,jeanne",
|
||||||
'2016-12-31,red wine,XXX,200.0,jeanne,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, jeanne, 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")
|
||||||
|
|
||||||
|
@ -481,7 +500,8 @@ class TestExport(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",
|
||||||
|
@ -494,6 +514,7 @@ class TestExport(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",
|
||||||
|
@ -506,6 +527,7 @@ class TestExport(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",
|
||||||
|
@ -519,6 +541,7 @@ class TestExport(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",
|
||||||
|
@ -528,6 +551,7 @@ class TestExport(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"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": "jeanne",
|
"payer_name": "jeanne",
|
||||||
|
@ -537,6 +561,7 @@ class TestExport(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"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",
|
||||||
|
@ -549,10 +574,10 @@ class TestExport(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",
|
||||||
"2017-01-01,refund,13.33,EUR,tata,1.0,jeanne",
|
"2017-01-01,refund,Reimbursement,13.33,EUR,tata,1.0,jeanne",
|
||||||
'2016-12-31,poutine from Québec,100.0,CAD,jeanne,1.0,"zorglub, tata"',
|
'2016-12-31,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, jeanne, 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")
|
||||||
|
|
||||||
|
@ -643,6 +668,7 @@ class TestExport(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",
|
||||||
|
@ -653,8 +679,8 @@ class TestExport(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")
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,17 @@ import smtplib
|
||||||
import socket
|
import socket
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
from werkzeug.security import check_password_hash
|
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
|
||||||
|
|
||||||
|
@ -126,6 +130,7 @@ class TestModels(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -137,6 +142,7 @@ class TestModels(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -148,6 +154,7 @@ class TestModels(IhatemoneyTestCase):
|
||||||
"what": "delicatessen",
|
"what": "delicatessen",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -181,6 +188,7 @@ class TestModels(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",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -192,6 +200,7 @@ class TestModels(IhatemoneyTestCase):
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1],
|
"payed_for": [1],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "20",
|
"amount": "20",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -203,6 +212,7 @@ class TestModels(IhatemoneyTestCase):
|
||||||
"what": "delicatessen",
|
"what": "delicatessen",
|
||||||
"payer": 1,
|
"payer": 1,
|
||||||
"payed_for": [1, 2],
|
"payed_for": [1, 2],
|
||||||
|
"bill_type": "Expense",
|
||||||
"amount": "10",
|
"amount": "10",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -223,6 +233,65 @@ class TestModels(IhatemoneyTestCase):
|
||||||
pay_each_expected = 10 / 3
|
pay_each_expected = 10 / 3
|
||||||
assert 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 TestEmailFailure(IhatemoneyTestCase):
|
class TestEmailFailure(IhatemoneyTestCase):
|
||||||
def test_creation_email_failure_smtp(self):
|
def test_creation_email_failure_smtp(self):
|
||||||
|
@ -395,9 +464,7 @@ class TestCurrencyConverter:
|
||||||
|
|
||||||
def test_failing_remote(self):
|
def test_failing_remote(self):
|
||||||
rates = {}
|
rates = {}
|
||||||
with patch("requests.Response.json", new=lambda _: {}), pytest.warns(
|
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)
|
||||||
|
|
1155
ihatemoney/translations/az/LC_MESSAGES/messages.po
Normal file
|
@ -782,7 +782,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -785,7 +785,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -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-29 14:24+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
|
||||||
|
@ -222,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}"
|
||||||
|
@ -826,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"
|
||||||
|
@ -1082,4 +1082,3 @@ msgstr "Període"
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
#~ "Pots compartir directament l'enllaç següent"
|
#~ "Pots compartir directament l'enllaç següent"
|
||||||
#~ " a través del teu mitjà preferit"
|
#~ " a través del teu mitjà preferit"
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,8 @@ 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-29 14:24+0200\n"
|
"POT-Creation-Date: 2023-07-29 14:24+0200\n"
|
||||||
"PO-Revision-Date: 2023-08-20 00:58+0000\n"
|
"PO-Revision-Date: 2024-02-24 00:03+0000\n"
|
||||||
"Last-Translator: Ettore Atalan <atalanttore@googlemail.com>\n"
|
"Last-Translator: Peter <peteramried@web.de>\n"
|
||||||
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
|
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
|
||||||
"i-hate-money/de/>\n"
|
"i-hate-money/de/>\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
|
@ -12,7 +12,7 @@ msgstr ""
|
||||||
"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"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 5.0-dev\n"
|
"X-Generator: Weblate 5.5-dev\n"
|
||||||
"Generated-By: Babel 2.9.0\n"
|
"Generated-By: Babel 2.9.0\n"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
|
@ -35,7 +35,7 @@ msgid "Current private code"
|
||||||
msgstr "Aktueller privater Code"
|
msgstr "Aktueller privater Code"
|
||||||
|
|
||||||
msgid "Enter existing private code to edit project"
|
msgid "Enter existing private code to edit project"
|
||||||
msgstr ""
|
msgstr "Geben Sie Ihren privaten Code ein, um das Projekt zu bearbeiten"
|
||||||
|
|
||||||
msgid "New private code"
|
msgid "New private code"
|
||||||
msgstr "Neuer privater Code"
|
msgstr "Neuer privater Code"
|
||||||
|
@ -197,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}"
|
||||||
|
@ -235,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."
|
||||||
|
@ -284,15 +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."
|
msgid "Project settings have been changed successfully."
|
||||||
msgstr ""
|
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 "
|
||||||
|
@ -311,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"
|
||||||
|
@ -320,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"
|
||||||
|
@ -371,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"
|
||||||
|
@ -486,7 +480,7 @@ 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"
|
||||||
|
@ -497,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"
|
||||||
|
@ -590,12 +585,14 @@ msgstr "Rechnung %(name)s: %(owers_list_str)s zur Eigentümerliste hinzugefügt"
|
||||||
msgid "Bill %(name)s: removed %(owers_list_str)s from owers list"
|
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 "
|
||||||
|
@ -816,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"
|
||||||
|
@ -827,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"
|
||||||
|
@ -881,9 +878,8 @@ msgstr "Keine Ausgaben"
|
||||||
msgid "Nothing to list yet."
|
msgid "Nothing to list yet."
|
||||||
msgstr "Noch nichts aufzulisten."
|
msgstr "Noch nichts aufzulisten."
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Add your first bill"
|
msgid "Add your first bill"
|
||||||
msgstr "eine Ausgabe hinzufügen"
|
msgstr "Mach deine erste Rechnung"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "Add the first participant"
|
msgid "Add the first participant"
|
||||||
|
@ -912,7 +908,7 @@ msgid "Invite people to join this project"
|
||||||
msgstr "Lade Leute ein, diesem Projekt beizutreten"
|
msgstr "Lade Leute ein, diesem Projekt beizutreten"
|
||||||
|
|
||||||
msgid "Share an invitation link"
|
msgid "Share an invitation link"
|
||||||
msgstr ""
|
msgstr "Einladung verschicken"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The easiest way to invite people is to give them the following invitation"
|
"The easiest way to invite people is to give them the following invitation"
|
||||||
|
@ -920,26 +916,29 @@ msgid ""
|
||||||
" add/edit/delete bills. However, they will not have access to important "
|
" add/edit/delete bills. However, they will not have access to important "
|
||||||
"settings such as changing the private code or deleting the whole project."
|
"settings such as changing the private code or deleting the whole project."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"Am einfachsten lädt man jemanden ein, indem man folgenden Einladungs-Link "
|
||||||
|
"teilt. <br /> Sie werden dann in der Lage sein dem Projekt beizutreten, zu "
|
||||||
|
"bearbeiten und Rechnungen hinzuzufügen, bearbeiten und löschen. Sie werden "
|
||||||
|
"allerdings keinen Zugriff auf wichtige Einstellungen bekommen, wie Privaten "
|
||||||
|
"Code zu ändern oder gar das ganze Projekt zu löschen."
|
||||||
|
|
||||||
msgid "Scan QR code"
|
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"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Specify a list of email adresses (separated by comma) of people you want "
|
"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 "
|
"to notify about the creation of this project. We will send them an email "
|
||||||
"with the invitation link."
|
"with the invitation link."
|
||||||
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"
|
msgid "Share Identifier & code"
|
||||||
msgstr "Teile die ID & den Code"
|
msgstr "Teile die ID & den Code"
|
||||||
|
@ -954,12 +953,11 @@ msgstr ""
|
||||||
msgid "Identifier:"
|
msgid "Identifier:"
|
||||||
msgstr "ID:"
|
msgstr "ID:"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Private code:"
|
msgid "Private code:"
|
||||||
msgstr "Privater Code"
|
msgstr "Privater Code:"
|
||||||
|
|
||||||
msgid "the private code was defined when you created the project"
|
msgid "the private code was defined when you created the project"
|
||||||
msgstr ""
|
msgstr "Der Private Code wurde beim erstellen des Projekts von Ihnen festgelegt"
|
||||||
|
|
||||||
msgid "Who pays?"
|
msgid "Who pays?"
|
||||||
msgstr "Wer zahlt?"
|
msgstr "Wer zahlt?"
|
||||||
|
|
|
@ -811,7 +811,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
|
|
@ -821,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
|
||||||
|
|
|
@ -818,7 +818,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"
|
||||||
|
|
|
@ -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-29 14:24+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,18 +29,17 @@ msgstr ""
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "Nombre del Proyecto"
|
msgstr "Nombre del Proyecto"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Current private code"
|
msgid "Current private code"
|
||||||
msgstr "Nuevo código privado"
|
msgstr "código privado actual"
|
||||||
|
|
||||||
msgid "Enter existing private code to edit project"
|
msgid "Enter existing private code to edit project"
|
||||||
msgstr ""
|
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"
|
||||||
|
@ -50,31 +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"
|
msgid "Unknown error"
|
||||||
msgstr "Error desconocido"
|
msgstr "Error desconocido"
|
||||||
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "Código privado inválido."
|
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"
|
||||||
|
@ -94,16 +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 "Get in"
|
msgid "Get in"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
@ -124,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"
|
||||||
|
@ -161,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"
|
||||||
|
@ -196,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}"
|
||||||
|
@ -223,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"
|
||||||
|
@ -284,33 +276,33 @@ msgid "Password successfully reset."
|
||||||
msgstr "Contraseña restablecida con éxito."
|
msgstr "Contraseña restablecida con éxito."
|
||||||
|
|
||||||
msgid "Project settings have been changed successfully."
|
msgid "Project settings have been changed successfully."
|
||||||
msgstr ""
|
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"
|
||||||
|
@ -319,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 ""
|
||||||
|
@ -360,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"
|
||||||
|
@ -370,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"
|
||||||
|
@ -450,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"
|
||||||
|
@ -481,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"
|
||||||
|
@ -500,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"
|
||||||
|
@ -524,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"
|
||||||
|
@ -592,19 +580,21 @@ msgstr "Factura %(name)s: removida %(owers_list_str)s de la lista de dueños"
|
||||||
|
|
||||||
msgid "This project has history disabled. New actions won't appear below."
|
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 "
|
||||||
|
@ -801,7 +791,7 @@ msgid "Settings"
|
||||||
msgstr "Configuración"
|
msgstr "Configuración"
|
||||||
|
|
||||||
msgid "RSS Feed"
|
msgid "RSS Feed"
|
||||||
msgstr ""
|
msgstr "rss Feed"
|
||||||
|
|
||||||
msgid "Other projects :"
|
msgid "Other projects :"
|
||||||
msgstr "Otros proyectos :"
|
msgstr "Otros proyectos :"
|
||||||
|
@ -814,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"
|
||||||
|
@ -825,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"
|
||||||
|
@ -879,13 +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."
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Add your first bill"
|
msgid "Add your first bill"
|
||||||
msgstr "agregar una factura"
|
msgstr "Añade tu primera factura"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Add the first participant"
|
msgid "Add the first participant"
|
||||||
msgstr "Editar este participante"
|
msgstr "Agregar el primer participante"
|
||||||
|
|
||||||
msgid "Password reminder"
|
msgid "Password reminder"
|
||||||
msgstr "Recordar contraseña"
|
msgstr "Recordar contraseña"
|
||||||
|
@ -910,7 +898,7 @@ 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 an invitation link"
|
msgid "Share an invitation link"
|
||||||
msgstr ""
|
msgstr "Compartir un enlace de invitación"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"The easiest way to invite people is to give them the following invitation"
|
"The easiest way to invite people is to give them the following invitation"
|
||||||
|
@ -918,26 +906,29 @@ msgid ""
|
||||||
" add/edit/delete bills. However, they will not have access to important "
|
" add/edit/delete bills. However, they will not have access to important "
|
||||||
"settings such as changing the private code or deleting the whole project."
|
"settings such as changing the private code or deleting the whole project."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"La forma más sencilla de invitar personas es dándoles el siguiente enlace de "
|
||||||
|
"invitación.<br />Podrán acceder al proyecto, administrar participantes, "
|
||||||
|
"agregar/editar/eliminar facturas. Sin embargo, no tendrán acceso a "
|
||||||
|
"configuraciones importantes como cambiar el código privado o eliminar todo "
|
||||||
|
"el proyecto."
|
||||||
|
|
||||||
msgid "Scan QR code"
|
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"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Specify a list of email adresses (separated by comma) of people you want "
|
"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 "
|
"to notify about the creation of this project. We will send them an email "
|
||||||
"with the invitation link."
|
"with the invitation link."
|
||||||
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"
|
msgid "Share Identifier & code"
|
||||||
msgstr "Compartir identificador y código"
|
msgstr "Compartir identificador y código"
|
||||||
|
@ -948,16 +939,20 @@ msgid ""
|
||||||
"to the full project, including changing settings such as the private code"
|
"to the full project, including changing settings such as the private code"
|
||||||
" or project email address, or even deleting the whole project."
|
" or project email address, or even deleting the whole project."
|
||||||
msgstr ""
|
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:"
|
msgid "Identifier:"
|
||||||
msgstr "Identificador:"
|
msgstr "Identificador:"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Private code:"
|
msgid "Private code:"
|
||||||
msgstr "Código privado"
|
msgstr "Código privado:"
|
||||||
|
|
||||||
msgid "the private code was defined when you created the project"
|
msgid "the private code was defined when you created the project"
|
||||||
msgstr ""
|
msgstr "el código privado se definió cuando creaste el proyecto"
|
||||||
|
|
||||||
msgid "Who pays?"
|
msgid "Who pays?"
|
||||||
msgstr "¿Quién paga?"
|
msgstr "¿Quién paga?"
|
||||||
|
@ -1170,4 +1165,3 @@ msgstr "Período"
|
||||||
#~ "Puedes compartir directamente el siguiente "
|
#~ "Puedes compartir directamente el siguiente "
|
||||||
#~ "enlace a través de tu medio "
|
#~ "enlace a través de tu medio "
|
||||||
#~ "preferido"
|
#~ "preferido"
|
||||||
|
|
||||||
|
|
|
@ -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-29 14:24+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
|
||||||
|
@ -125,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 "واحد پولی"
|
||||||
|
@ -180,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 ""
|
||||||
|
@ -782,7 +782,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
@ -1092,4 +1092,3 @@ msgstr ""
|
||||||
#~ "them an email with the invitation "
|
#~ "them an email with the invitation "
|
||||||
#~ "link."
|
#~ "link."
|
||||||
#~ msgstr ""
|
#~ msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -824,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"
|
||||||
|
|
|
@ -788,7 +788,7 @@ msgstr "יישום לנייד"
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "דוקומנטציה"
|
msgstr "דוקומנטציה"
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -829,7 +829,7 @@ msgstr "मोबाइल एप्लीकेशन"
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "प्रलेखन"
|
msgstr "प्रलेखन"
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr "व्यवस्थापन डैशबोर्ड"
|
msgstr "व्यवस्थापन डैशबोर्ड"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
|
|
@ -812,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"
|
||||||
|
|
|
@ -817,7 +817,7 @@ msgstr "Applicazione mobile"
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "Documentazione"
|
msgstr "Documentazione"
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr "Cruscotto Amministrazione"
|
msgstr "Cruscotto Amministrazione"
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -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-29 14:24+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,19 +27,17 @@ msgstr "無効な入力です。数字と「+ - * / 」の演算子しか入力
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "プロジェクトの名前"
|
msgstr "プロジェクトの名前"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Current private code"
|
msgid "Current private code"
|
||||||
msgstr "暗証コード"
|
msgstr "現在の暗証コード"
|
||||||
|
|
||||||
msgid "Enter existing private code to edit project"
|
msgid "Enter existing private code to edit project"
|
||||||
msgstr ""
|
msgstr "プロジェクトを編集するために、暗証コードを入力してください"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
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 "メールアドレス"
|
||||||
|
@ -54,15 +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 "明細通貨変換のため、デフォルトの通貨を設定してください"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "未知のプロジェクト"
|
msgstr "不明エラー"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "暗証コード"
|
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 "
|
||||||
|
@ -179,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 "招待状を送る"
|
||||||
|
@ -194,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}"
|
||||||
|
@ -271,7 +268,7 @@ msgid "Project settings have been changed successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Unable to parse CSV"
|
msgid "Unable to parse CSV"
|
||||||
msgstr ""
|
msgstr "CSVを読み込むことができません"
|
||||||
|
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Missing attribute: %(attribute)s"
|
msgid "Missing attribute: %(attribute)s"
|
||||||
|
@ -292,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"
|
||||||
|
@ -353,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"
|
||||||
|
@ -574,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 "
|
||||||
|
@ -622,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"
|
||||||
|
@ -641,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 "プロジェクトの設定が修正された"
|
||||||
|
@ -681,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"
|
||||||
|
@ -697,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 "表示できるものがない"
|
||||||
|
@ -802,7 +797,7 @@ msgstr "携帯アプリ"
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr "書類"
|
msgstr "書類"
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr "管理ダッシュボード"
|
msgstr "管理ダッシュボード"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
|
@ -904,14 +899,12 @@ msgstr ""
|
||||||
msgid "Send via Emails"
|
msgid "Send via Emails"
|
||||||
msgstr "メールで送る"
|
msgstr "メールで送る"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Specify a list of email adresses (separated by comma) of people you want "
|
"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 "
|
"to notify about the creation of this project. We will send them an email "
|
||||||
"with the invitation link."
|
"with the invitation link."
|
||||||
msgstr ""
|
msgstr "知らせたいメールアドレスのリスト(カンマ区切り)を指定してください。メールで"
|
||||||
"…を知らせたいメールアドレスのリストを特定する(カンマ区切り)\n"
|
"招待リンクを送信します。"
|
||||||
"彼らにこの予算管理プロジェクトの作成をメールでお知らせします。"
|
|
||||||
|
|
||||||
msgid "Share Identifier & code"
|
msgid "Share Identifier & code"
|
||||||
msgstr "名前とコードを共有する"
|
msgstr "名前とコードを共有する"
|
||||||
|
@ -926,9 +919,8 @@ msgstr ""
|
||||||
msgid "Identifier:"
|
msgid "Identifier:"
|
||||||
msgstr "名前:"
|
msgstr "名前:"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Private code:"
|
msgid "Private code:"
|
||||||
msgstr "暗証コード"
|
msgstr "暗証コード:"
|
||||||
|
|
||||||
msgid "the private code was defined when you created the project"
|
msgid "the private code was defined when you created the project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1109,4 +1101,3 @@ msgstr "期間"
|
||||||
|
|
||||||
#~ msgid "You can directly share the following link via your prefered medium"
|
#~ msgid "You can directly share the following link via your prefered medium"
|
||||||
#~ msgstr "好きの手段で以下のリンクを直接に共有できる"
|
#~ msgstr "好きの手段で以下のリンクを直接に共有できる"
|
||||||
|
|
||||||
|
|
|
@ -793,7 +793,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -793,7 +793,7 @@ msgstr ""
|
||||||
msgid "Documentation"
|
msgid "Documentation"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Administation Dashboard"
|
msgid "Administration Dashboard"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Legal information"
|
msgid "Legal information"
|
||||||
|
|
|
@ -855,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
|
||||||
|
|
|
@ -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-29 14:24+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,19 +31,17 @@ msgstr ""
|
||||||
msgid "Project name"
|
msgid "Project name"
|
||||||
msgstr "Projectnaam"
|
msgstr "Projectnaam"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Current private code"
|
msgid "Current private code"
|
||||||
msgstr "Privécode"
|
msgstr "Huidige Privécode"
|
||||||
|
|
||||||
msgid "Enter existing private code to edit project"
|
msgid "Enter existing private code to edit project"
|
||||||
msgstr ""
|
msgstr "Voer het huidige privécode in om dit project te bewerken"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "New private code"
|
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"
|
||||||
|
@ -59,22 +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"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Unknown error"
|
msgid "Unknown error"
|
||||||
msgstr "Onbekend project"
|
msgstr "Onbekende fout"
|
||||||
|
|
||||||
#, fuzzy
|
|
||||||
msgid "Invalid private code."
|
msgid "Invalid private code."
|
||||||
msgstr "Privé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"
|
||||||
|
@ -92,17 +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"
|
||||||
|
|
||||||
msgid "Get in"
|
msgid "Get in"
|
||||||
msgstr "Inloggen"
|
msgstr "Inloggen"
|
||||||
|
@ -126,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?"
|
||||||
|
@ -174,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"
|
||||||
|
@ -196,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."
|
"beheerder: %(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}"
|
||||||
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"
|
||||||
|
@ -258,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"
|
||||||
|
@ -283,11 +276,11 @@ msgid "Project settings have been changed successfully."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Unable to parse CSV"
|
msgid "Unable to parse CSV"
|
||||||
msgstr ""
|
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 "
|
||||||
|
@ -304,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"
|
||||||
|
@ -354,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"
|
||||||
|
@ -393,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"
|
||||||
|
@ -412,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"
|
||||||
|
@ -431,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"
|
||||||
|
@ -441,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"
|
||||||
|
@ -452,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"
|
||||||
|
@ -826,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
|
||||||
|
@ -1163,4 +1151,3 @@ msgstr "Periode"
|
||||||
|
|
||||||
#~ msgid "You can directly share the following link via your prefered medium"
|
#~ msgid "You can directly share the following link via your prefered medium"
|
||||||
#~ msgstr "Je kunt de volgende link direct delen"
|
#~ msgstr "Je kunt de volgende link direct delen"
|
||||||
|
|
||||||
|
|