Merge branch 'master' into almet/feat/showcase

This commit is contained in:
Glandos 2020-05-06 22:00:10 +02:00 committed by GitHub
commit af1562ce24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 7043 additions and 1059 deletions

11
.gitignore vendored
View file

@ -1,4 +1,3 @@
ihatemoney.cfg
*.pyc
*.egg-info
dist
@ -10,5 +9,11 @@ dist
build
.vscode
.env
.pytest_cache
.pytest_cache
ihatemoney/budget.db
.idea/
.envrc
.DS_Store
.idea
.python-version

13
.isort.cfg Normal file
View file

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

View file

@ -1,9 +1,9 @@
sudo: false
language: python
python:
- "3.5"
- "3.6"
- "3.7"
- "3.8"
script: tox
install:
- pip install tox-travis

View file

@ -9,7 +9,7 @@ This document describes changes between each past release.
- Add support for espanol latino america (es_419)
- Use the external debts lib to solve settlements (#476)
- Remove balance column in statistics view (#323)
- Remove requirements files in favor of setup.cfg pinning (#558)
4.1.3 (2019-09-18)
==================

View file

@ -7,20 +7,27 @@ Adrien CLERC
Alexandre Avenel
Alexis Métaireau
Allan Nordhøy
am97
Andrew Dickinson
Arnaud Bos
Baptiste Jonglez
Benjamin Bouvier
Berteh
bmatticus
Brice Maron
Byron Ullauri
Carey Metcalfe
Daniel Schreiber
DavidRThrashJr
donkers
Edwin Smulders
Elizabeth Sherrock
eMerzh
Feth AREZKI
Frédéric Sureau
Glandos
Heimen Stoffels
James Leong
Jocelyn Delalande
Lucas Verney
Luc Didry
@ -37,5 +44,6 @@ Richard Coates
THANOS SIOURDAKIS
Toover
Xavier Mehrenberger
zorun
The manual drawings are from Coline Billon, they are under CC BY 4.0.

View file

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

View file

@ -11,10 +11,10 @@ ZOPFLIPNG := zopflipng
.PHONY: all
all: install ## Alias for install
.PHONY: install
install: virtualenv $(INSTALL_STAMP) ## Install dependencies
install: virtualenv setup.cfg $(INSTALL_STAMP) ## Install dependencies
$(INSTALL_STAMP):
$(VENV)/bin/pip install -U pip
$(VENV)/bin/pip install -r requirements.txt
$(VENV)/bin/pip install -e .
touch $(INSTALL_STAMP)
.PHONY: virtualenv
@ -23,9 +23,9 @@ $(PYTHON):
$(VIRTUALENV) $(VENV)
.PHONY: install-dev
install-dev: $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
$(DEV_STAMP): $(PYTHON) dev-requirements.txt
$(VENV)/bin/pip install -Ur dev-requirements.txt
install-dev: virtualenv setup.cfg $(INSTALL_STAMP) $(DEV_STAMP) ## Install development dependencies
$(DEV_STAMP): $(PYTHON)
$(VENV)/bin/pip install -Ue .[dev]
touch $(DEV_STAMP)
.PHONY: remove-install-stamp
@ -41,11 +41,19 @@ serve: install ## Run the ihatemoney server
$(PYTHON) -m ihatemoney.manage runserver
.PHONY: test
test: $(DEV_STAMP) ## Run the tests
test: install-dev ## Run the tests
$(VENV)/bin/tox
.PHONY: black
black: install-dev ## Run the tests
$(VENV)/bin/black --target-version=py34 .
.PHONY: isort
isort: install-dev ## Run the tests
$(VENV)/bin/isort -rc .
.PHONY: release
release: $(DEV_STAMP) ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
release: install-dev ## Release a new version (see https://ihatemoney.readthedocs.io/en/latest/contributing.html#how-to-release)
$(VENV)/bin/fullrelease
.PHONY: compress-assets
@ -76,13 +84,6 @@ create-empty-database-revision: ## Create an empty database revision
@read -p "Please enter a message describing this revision: " rev_message; \
$(PYTHON) -m ihatemoney.manage db revision -d ihatemoney/migrations -m "$${rev_message}"
.PHONY: build-requirements
build-requirements: ## Save currently installed packages to requirements.txt
$(VIRTUALENV) $(TEMPDIR)
$(TEMPDIR)/bin/pip install -U pip
$(TEMPDIR)/bin/pip install -Ue "."
$(TEMPDIR)/bin/pip freeze | grep -v -- '-e' > requirements.txt
.PHONY: clean
clean: ## Destroy the virtual environment
rm -rf .venv

View file

@ -5,6 +5,10 @@ I hate money
:target: https://travis-ci.org/spiral-project/ihatemoney
:alt: Travis CI Build Status
.. image:: https://hosted.weblate.org/widgets/i-hate-money/-/i-hate-money/svg-badge.svg
:target: https://hosted.weblate.org/engage/i-hate-money/?utm_source=widget
:alt: Translation status from Weblate
*I hate money* is a web application made to ease shared budget management.
It keeps track of who bought what, when, and for whom; and helps to settle the
bills.
@ -21,7 +25,7 @@ encouraged to do so.
Requirements
============
* **Python**: 3.5, 3.6, 3.7.
* **Python**: 3.6, 3.7, 3.8.
* **Backends**: MySQL, PostgreSQL, SQLite, Memory.
Contributing
@ -31,3 +35,9 @@ Do you wish to contribute to IHateMoney? Fantastic! There's a lot of very
useful help on the official `contributing
<https://ihatemoney.readthedocs.io/en/latest/contributing.html>`_ page.
Translation status
==================
.. image:: https://hosted.weblate.org/widgets/i-hate-money/-/i-hate-money/multi-blue.svg
:target: https://hosted.weblate.org/engage/i-hate-money/?utm_source=widget
:alt: Translation status for each language

View file

@ -1,6 +0,0 @@
zest.releaser
tox
pytest
flake8
Flask-Testing
black ; python_version >= '3.6'

View file

@ -1,6 +1,3 @@
# coding: utf8
import sys, os
templates_path = ["_templates"]
source_suffix = ".rst"
master_doc = "index"

View file

@ -1,14 +1,64 @@
Contributing
############
Setup a dev environment
=======================
.. _how-to-contribute:
You must develop on top of the git master branch::
How to contribute
=================
You would like to contribute? First, thanks a bunch! This project is a small
project with just a few people behind it, so any help is appreciated!
There are different ways to help us, regarding if you are a designer,
a developer or an user.
As a developer
--------------
If you want to contribute code, you can write it and then issue a pull request
on github. To get started, please read :ref:`setup-dev-environment` and
:ref:`contributing-developer`.
As a designer / Front-end developer
-----------------------------------
Feel free to provide mockups, or to involve yourself in the discussions
happening on the GitHub issue tracker. All ideas are welcome. Of course, if you
know how to implement them, feel free to fork and make a pull request.
As a translator
---------------
If you're able to translate Ihatemoney in your own language,
head over to `the website we use for translations <https://hosted.weblate.org/projects/i-hate-money/i-hate-money/>`_
and start translating.
All the heavy lifting will be done automatically, and your strings will
eventually be integrated.
Once a language is ready to be integrated, add it to the
``SUPPORTED_LANGUAGES`` list, in ``ihatemoney/default_settings.py``.
End-user
--------
You are using the application and found a bug? You have some ideas about how to
improve the project? Please tell us `by filling a new issue <https://github.com/spiral-project/ihatemoney/issues>`_.
Or, if you prefer, you can send me an e-mail to `alexis@notmyidea.org` and I
will update the issue tracker with your feedback.
Thanks again!
.. _setup-dev-environment:
Set up a dev environment
========================
You must develop on top of the Git master branch::
git clone https://github.com/spiral-project/ihatemoney.git
Then you need to build your dev environments. Choose your way…
Then you need to build your dev environment. Choose your way…
The quick way
-------------
@ -23,10 +73,8 @@ install dependencies, and run the test server.
The hard way
------------
Alternatively, you can also use the `requirements.txt` file to install the
dependencies yourself. That would be::
Alternatively, you can use pip to install dependencies yourself. That would be::
pip install -r requirements.txt
pip install -e .
And then run the application::
@ -43,28 +91,10 @@ It's as simple as that!
Updating
--------
In case you want to update to newer versions (from git), you can just run the "update" command::
In case you want to update to newer versions (from Git), you can just run the "update" command::
make update
Create database migrations
--------------------------
In case you need to modify the database schema, first update the models in
``ihatemoney/models.py``. Then run the following command to create a new
database revision file::
make create-database-revision
If your changes are simple enough, the generated script will be populated with
the necessary migrations steps. You can edit the generated script. eg: to add
data migrations.
For complex migrations, it is recommended to start from an empty revision file
which can be created with the following command::
make create-empty-database-revision
Useful settings
----------------
@ -80,64 +110,81 @@ Then before running the application, declare its path with ::
export IHATEMONEY_SETTINGS_FILE_PATH="$(pwd)/settings.cfg"
How to contribute
=================
.. _contributing-developer:
You would like to contribute? First, thanks a bunch! This project is a small
project with just a few people behind it, so any help is appreciated!
Contributing as a developer
===========================
There are different ways to help us, regarding if you are a designer,
a developer or an user.
All code contributions should be submitted as Pull Requests on the
`github project <https://github.com/spiral-project/ihatemoney>`_.
As a developer
--------------
Below are some points that you should check to help you prepare your Pull Request.
If you want to contribute code, you can write it and then issue a pull request
on github. Please, think about updating and running the tests before asking for
a pull request as it will help us to maintain the code clean and running.
Running tests
-------------
To do so::
Please, think about updating and running the tests before asking for a pull request
as it will help us to maintain the code clean and running.
To run the tests::
make test
We are using the `black <https://black.readthedocs.io/en/stable/>`_ formatter
for all the python files in this project. Be sure to run it locally on your
files. To do so, just run::
Tests can be edited in ``ihatemoney/tests/tests.py``. If some test cases fail because
of your changes, first check whether your code correctly handle these cases.
If you are confident that your code is correct and that the test cases simply need
to be updated to match your changes, update the test cases and send them as part of
your pull request.
black ihatemoney
If you are introducing a new feature, you need to either add tests to existing classes,
or add a new class (if your new feature is significantly different from existing code).
You can also integrate it with your dev environment (as a *format-on-save*
hook, for instance).
As a designer / Front-end developer
-----------------------------------
Feel free to provide us mockups or to involve yourself into the discussions
hapenning on the github issue tracker. All ideas are welcome. Of course, if you
know how to implement them, feel free to fork and make a pull request.
As a translator
Formatting code
---------------
If you're able to translate Ihatemoney in your own language,
head over to `the website we use for translations <https://hosted.weblate.org/settings/i-hate-money/i-hate-money/>`_
and start translating!
We are using `black <https://black.readthedocs.io/en/stable/>`_ and
`isort <https://timothycrosley.github.io/isort/>`_ formatters for all the Python
files in this project. Be sure to run it locally on your files.
To do so, just run::
All the heavy lifting will be done automatically, and your strings will
eventually be integrated.
make black isort
Once a language is ready to be integrated, add it to the
``SUPPORTED_LANGUAGES`` list, in ``ihatemoney/default_settings.py``.
You can also integrate them with your dev environment (as a *format-on-save*
hook, for instance).
End-user
--------
Creating database migrations
----------------------------
You are using the application and found a bug? You have some ideas about how to
improve the project? Please tell us `by filling a new issue <https://github.com/spiral-project/ihatemoney/issues>`_.
Or, if you prefer, you can send me an email to `alexis@notmyidea.org` and I
will update the issue tracker with your feedback.
In case you need to modify the database schema, first make sure that you have
an up-to-date database by running the dev server at least once (the quick way
or the hard way, see above). The dev server applies all existing migrations
when starting up.
You can now update the models in ``ihatemoney/models.py``. Then run the following
command to create a new database revision file::
make create-database-revision
If your changes are simple enough, the generated script will be populated with
the necessary migrations steps. You can view and edit the generated script, which
is useful to review that the expected model changes have been properly detected.
Usually the auto-detection works well in most cases, but you can of course edit the
script to fix small issues. You could also edit the script to add data migrations.
When you are done with your changes, don't forget to add the migration script to
your final git commit!
If the migration script looks completely wrong, remove the script and start again
with an empty database. The simplest way is to remove or rename the dev database
located at ``/tmp/ihatemoney.db``, and run the dev server at least once.
For complex migrations, it is recommended to start from an empty revision file
which can be created with the following command::
make create-empty-database-revision
You then need to write the migration steps yourself.
Thanks again!
How to build the documentation ?
================================
@ -150,7 +197,7 @@ Install doc dependencies (within the virtualenv, if any)::
pip install -r docs/requirements.txt
And to produce html doc in `docs/_output` folder::
And to produce a HTML doc in the `docs/_output` folder::
cd docs/
make html
@ -173,7 +220,7 @@ In order to prepare a new release, we are following the following steps:
make compress-assets
- Build the translations::
make update-translations
make build-translations

View file

@ -17,7 +17,7 @@ Requirements
«Ihatemoney» depends on:
* **Python**: either 3.5, 3.6 or 3.7 will work.
* **Python**: either 3.6, 3.7 or 3.8 will work.
* **A Backend**: to choose among MySQL, PostgreSQL, SQLite or Memory.
* **Virtualenv** (recommended): `virtualenv` package under Debian/Ubuntu.

View file

@ -1,2 +1,2 @@
Sphinx==1.5.5
docutils==0.13.1
Sphinx==3.0.3
docutils==0.16

View file

@ -1,12 +1,12 @@
# coding: utf8
from flask import request, current_app
from functools import wraps
from flask import current_app, request
from flask_restful import Resource, abort
from werkzeug.security import check_password_hash
from wtforms.fields.core import BooleanField
from ihatemoney.models import db, Project, Person, Bill
from ihatemoney.forms import ProjectForm, EditProjectForm, MemberForm, get_billform_for
from werkzeug.security import check_password_hash
from functools import wraps
from ihatemoney.forms import EditProjectForm, MemberForm, ProjectForm, get_billform_for
from ihatemoney.models import Bill, Person, Project, db
def need_auth(f):

View file

@ -1,17 +1,16 @@
# coding: utf8
from flask import Blueprint
from flask_restful import Api
from flask_cors import CORS
from flask_restful import Api
from ihatemoney.api.common import (
ProjectsHandler,
ProjectHandler,
TokenHandler,
MemberHandler,
ProjectStatsHandler,
MembersHandler,
BillHandler,
BillsHandler,
MemberHandler,
MembersHandler,
ProjectHandler,
ProjectsHandler,
ProjectStatsHandler,
TokenHandler,
)
api = Blueprint("api", __name__, url_prefix="/api")

View file

View file

@ -0,0 +1,46 @@
from cachetools import TTLCache, cached
import requests
class Singleton(type):
_instances = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instances[cls]
class CurrencyConverter(object, metaclass=Singleton):
# Get exchange rates
default = "No Currency"
api_url = "https://api.exchangeratesapi.io/latest?base=USD"
def __init__(self):
pass
@cached(cache=TTLCache(maxsize=1, ttl=86400))
def get_rates(self):
rates = requests.get(self.api_url).json()["rates"]
rates[self.default] = 1.0
return rates
def get_currencies(self):
rates = [rate for rate in self.get_rates()]
rates.sort(key=lambda rate: "" if rate == self.default else rate)
return rates
def exchange_currency(self, amount, source_currency, dest_currency):
if (
source_currency == dest_currency
or source_currency == self.default
or dest_currency == self.default
):
return amount
rates = self.get_rates()
source_rate = rates[source_currency]
dest_rate = rates[dest_currency]
new_amount = (float(amount) / source_rate) * dest_rate
# round to two digits because we are dealing with money
return round(new_amount, 2)

View file

@ -8,4 +8,4 @@ ACTIVATE_DEMO_PROJECT = True
ADMIN_PASSWORD = ""
ALLOW_PUBLIC_PROJECT_CREATION = True
ACTIVATE_ADMIN_DASHBOARD = False
SUPPORTED_LANGUAGES = ["en", "fr", "de", "nl", "es_419"]
SUPPORTED_LANGUAGES = ["en", "fr", "de", "nl", "es_419", "nb_NO", "id"]

View file

@ -1,29 +1,29 @@
import copy
from datetime import datetime
from re import match
import email_validator
from flask import request
from flask_babel import lazy_gettext as _
from flask_wtf.file import FileAllowed, FileField, FileRequired
from flask_wtf.form import FlaskForm
from wtforms.fields.core import SelectField, SelectMultipleField
from jinja2 import Markup
from werkzeug.security import check_password_hash, generate_password_hash
from wtforms.fields.core import Label, SelectField, SelectMultipleField
from wtforms.fields.html5 import DateField, DecimalField, URLField
from wtforms.fields.simple import PasswordField, SubmitField, StringField
from wtforms.fields.simple import BooleanField, PasswordField, StringField, SubmitField
from wtforms.validators import (
Email,
DataRequired,
ValidationError,
Email,
EqualTo,
NumberRange,
Optional,
ValidationError,
)
from flask_wtf.file import FileField, FileAllowed, FileRequired
from flask_babel import lazy_gettext as _
from flask import request
from werkzeug.security import generate_password_hash
from datetime import datetime
from re import match
from jinja2 import Markup
import email_validator
from ihatemoney.models import Project, Person
from ihatemoney.utils import slugify, eval_arithmetic_expression
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import LoggingMode, Person, Project
from ihatemoney.utils import eval_arithmetic_expression, slugify
def strip_filter(string):
@ -33,6 +33,18 @@ def strip_filter(string):
return string
def get_editprojectform_for(project, **kwargs):
"""Return an instance of EditProjectForm configured for a particular project.
"""
form = EditProjectForm(**kwargs)
choices = copy.copy(form.default_currency.choices)
choices.sort(
key=lambda rates: "" if rates[0] == project.default_currency else rates[0]
)
form.default_currency.choices = choices
return form
def get_billform_for(project, set_default=True, **kwargs):
"""Return an instance of BillForm configured for a particular project.
@ -41,6 +53,23 @@ def get_billform_for(project, set_default=True, **kwargs):
"""
form = BillForm(**kwargs)
if form.original_currency.data == "None":
form.original_currency.data = project.default_currency
if form.original_currency.data != CurrencyConverter.default:
choices = copy.copy(form.original_currency.choices)
choices.remove((CurrencyConverter.default, CurrencyConverter.default))
choices.sort(
key=lambda rates: "" if rates[0] == project.default_currency else rates[0]
)
form.original_currency.choices = choices
else:
form.original_currency.render_kw = {"default": True}
form.original_currency.data = CurrencyConverter.default
form.original_currency.label = Label(
"original_currency", "Currency (Default: %s)" % (project.default_currency)
)
active_members = [(m.id, m.name) for m in project.active_members]
form.payed_for.choices = form.payer.choices = active_members
@ -89,6 +118,28 @@ class EditProjectForm(FlaskForm):
name = StringField(_("Project name"), validators=[DataRequired()])
password = StringField(_("Private code"), validators=[DataRequired()])
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
project_history = BooleanField(_("Enable project history"))
ip_recording = BooleanField(_("Use IP tracking for project history"))
currency_helper = CurrencyConverter()
default_currency = SelectField(
_("Default Currency"),
choices=[
(currency_name, currency_name)
for currency_name in currency_helper.get_currencies()
],
validators=[DataRequired()],
)
@property
def logging_preference(self):
"""Get the LoggingMode object corresponding to current form data."""
if not self.project_history.data:
return LoggingMode.DISABLED
else:
if self.ip_recording.data:
return LoggingMode.RECORD_IP
else:
return LoggingMode.ENABLED
def save(self):
"""Create a new project with the information given by this form.
@ -100,22 +151,33 @@ class EditProjectForm(FlaskForm):
id=self.id.data,
password=generate_password_hash(self.password.data),
contact_email=self.contact_email.data,
logging_preference=self.logging_preference,
default_currency=self.default_currency.data,
)
return project
def update(self, project):
"""Update the project with the information from the form"""
project.name = self.name.data
project.password = generate_password_hash(self.password.data)
# Only update password if changed to prevent spurious log entries
if not check_password_hash(project.password, self.password.data):
project.password = generate_password_hash(self.password.data)
project.contact_email = self.contact_email.data
project.logging_preference = self.logging_preference
project.default_currency = self.default_currency.data
return project
class UploadForm(FlaskForm):
file = FileField(
"JSON", validators=[FileRequired(), FileAllowed(["json", "JSON"], "JSON only!")]
"JSON",
validators=[FileRequired(), FileAllowed(["json", "JSON"], "JSON only!")],
description=_("Import previously exported JSON file"),
)
submit = SubmitField(_("Import"))
class ProjectForm(EditProjectForm):
@ -123,6 +185,14 @@ class ProjectForm(EditProjectForm):
password = PasswordField(_("Private code"), validators=[DataRequired()])
submit = SubmitField(_("Create the project"))
def save(self):
# WTForms Boolean Fields don't insert the default value when the
# request doesn't include any value the way that other fields do,
# so we'll manually do it here
self.project_history.data = LoggingMode.default() != LoggingMode.DISABLED
self.ip_recording.data = LoggingMode.default() == LoggingMode.RECORD_IP
return super().save()
def validate_id(form, field):
form.id.data = slugify(field.data)
if (form.id.data == "dashboard") or Project.query.get(form.id.data):
@ -171,6 +241,15 @@ class BillForm(FlaskForm):
what = StringField(_("What?"), validators=[DataRequired()])
payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int)
amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()])
currency_helper = CurrencyConverter()
original_currency = SelectField(
_("Currency"),
choices=[
(currency_name, currency_name)
for currency_name in currency_helper.get_currencies()
],
validators=[DataRequired()],
)
external_link = URLField(
_("External link"),
validators=[Optional()],
@ -189,6 +268,10 @@ class BillForm(FlaskForm):
bill.external_link = self.external_link.data
bill.date = self.date.data
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
bill.original_currency = self.original_currency.data
bill.converted_amount = self.currency_helper.exchange_currency(
bill.amount, bill.original_currency, project.default_currency
)
return bill
def fake_form(self, bill, project):
@ -198,6 +281,10 @@ class BillForm(FlaskForm):
bill.external_link = ""
bill.date = self.date
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
bill.original_currency = CurrencyConverter.default
bill.converted_amount = self.currency_helper.exchange_currency(
bill.amount, bill.original_currency, project.default_currency
)
return bill
@ -206,6 +293,7 @@ class BillForm(FlaskForm):
self.amount.data = bill.amount
self.what.data = bill.what
self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency
self.date.data = bill.date
self.payed_for.data = [int(ower.id) for ower in bill.owers]

139
ihatemoney/history.py Normal file
View file

@ -0,0 +1,139 @@
from flask_babel import gettext as _
from sqlalchemy_continuum import Operation, parent_class
from ihatemoney.models import BillVersion, Person, PersonVersion, ProjectVersion
def get_history_queries(project):
"""Generate queries for each type of version object for a given project."""
person_changes = PersonVersion.query.filter_by(project_id=project.id)
project_changes = ProjectVersion.query.filter_by(id=project.id)
bill_changes = (
BillVersion.query.with_entities(BillVersion.id.label("bill_version_id"))
.join(Person, BillVersion.payer_id == Person.id)
.filter(Person.project_id == project.id)
)
sub_query = bill_changes.subquery()
bill_changes = BillVersion.query.filter(BillVersion.id.in_(sub_query))
return person_changes, project_changes, bill_changes
def history_sort_key(history_item_dict):
"""
Return the key necessary to sort history entries. First order sort is time
of modification, but for simultaneous modifications we make the re-name
modification occur last so that the simultaneous entries make sense using
the old name.
"""
second_order = 0
if "prop_changed" in history_item_dict:
changed_property = history_item_dict["prop_changed"]
if changed_property == "name" or changed_property == "what":
second_order = 1
return history_item_dict["time"], second_order
def describe_version(version_obj):
"""Use the base model str() function to describe a version object"""
return parent_class(type(version_obj)).__str__(version_obj)
def describe_owers_change(version, human_readable_names):
"""Compute the set difference to get added/removed owers lists."""
before_owers = {version.id: version for version in version.previous.owers}
after_owers = {version.id: version for version in version.owers}
added_ids = set(after_owers).difference(set(before_owers))
removed_ids = set(before_owers).difference(set(after_owers))
if not human_readable_names:
return added_ids, removed_ids
added = [describe_version(after_owers[ower_id]) for ower_id in added_ids]
removed = [describe_version(before_owers[ower_id]) for ower_id in removed_ids]
return added, removed
def get_history(project, human_readable_names=True):
"""
Fetch history for all models associated with a given project.
:param human_readable_names Whether to replace id numbers with readable names
:return A sorted list of dicts with history information
"""
person_query, project_query, bill_query = get_history_queries(project)
history = []
for version_list in [person_query.all(), project_query.all(), bill_query.all()]:
for version in version_list:
object_type = {
"Person": _("Participant"),
"Bill": _("Bill"),
"Project": _("Project"),
}[parent_class(type(version)).__name__]
# Use the old name if applicable
if version.previous:
object_str = describe_version(version.previous)
else:
object_str = describe_version(version)
common_properties = {
"time": version.transaction.issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
"operation_type": version.operation_type,
"object_type": object_type,
"object_desc": object_str,
"ip": version.transaction.remote_addr,
}
if version.operation_type == Operation.UPDATE:
# Only iterate the changeset if the previous version
# Was logged
if version.previous:
changeset = version.changeset
if isinstance(version, BillVersion):
if version.owers != version.previous.owers:
added, removed = describe_owers_change(
version, human_readable_names
)
if added:
changeset["owers_added"] = (None, added)
if removed:
changeset["owers_removed"] = (None, removed)
# Remove converted_amount if amount changed in the same way.
if (
"amount" in changeset
and "converted_amount" in changeset
and changeset["amount"] == changeset["converted_amount"]
):
del changeset["converted_amount"]
for (prop, (val_before, val_after),) in changeset.items():
if human_readable_names:
if prop == "payer_id":
prop = "payer"
if val_after is not None:
val_after = describe_version(version.payer)
if version.previous and val_before is not None:
val_before = describe_version(
version.previous.payer
)
else:
val_after = None
next_event = common_properties.copy()
next_event["prop_changed"] = prop
next_event["val_before"] = val_before
next_event["val_after"] = val_after
history.append(next_event)
else:
history.append(common_properties)
else:
history.append(common_properties)
return sorted(history, key=history_sort_key, reverse=True)

View file

@ -1,16 +1,16 @@
#!/usr/bin/env python
import getpass
import os
import random
import sys
import getpass
from flask_script import Manager, Command, Option
from flask_migrate import Migrate, MigrateCommand
from flask_script import Command, Manager, Option
from werkzeug.security import generate_password_hash
from ihatemoney.models import Project, db
from ihatemoney.run import create_app
from ihatemoney.models import db, Project
from ihatemoney.utils import create_jinja_env
@ -51,7 +51,7 @@ class GenerateConfig(Command):
def run(self, config_file):
env = create_jinja_env("conf-templates", strict_rendering=True)
template = env.get_template("%s.j2" % config_file)
template = env.get_template(f"{config_file}.j2")
bin_path = os.path.dirname(sys.executable)
pkg_path = os.path.abspath(os.path.dirname(__file__))

View file

@ -12,6 +12,18 @@ msgstr ""
msgid "Email"
msgstr ""
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr ""
@ -106,6 +118,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -139,6 +160,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -150,7 +177,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -204,9 +231,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -228,6 +252,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -237,6 +264,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -258,6 +291,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -288,6 +324,177 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -335,6 +542,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -392,6 +602,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
@ -418,9 +634,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -489,15 +702,15 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""

View file

@ -41,7 +41,7 @@ def run_migrations_offline():
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
context.configure(url=url, include_object=include_object)
with context.begin_transaction():
context.run_migrations()
@ -75,6 +75,7 @@ def run_migrations_online():
context.configure(
connection=connection,
target_metadata=target_metadata,
include_object=include_object,
process_revision_directives=process_revision_directives,
**current_app.extensions["migrate"].configure_args
)
@ -86,6 +87,12 @@ def run_migrations_online():
connection.close()
def include_object(object, name, type_, reflected, compare_to):
if name == "sqlite_sequence":
return False
return True
if context.is_offline_mode():
run_migrations_offline()
else:

View file

@ -0,0 +1,232 @@
"""autologger
Revision ID: 2dcb0c0048dc
Revises: 6c6fb2b7f229
Create Date: 2020-04-10 18:12:41.285590
"""
# revision identifiers, used by Alembic.
revision = "2dcb0c0048dc"
down_revision = "6c6fb2b7f229"
from alembic import op
import sqlalchemy as sa
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"bill_version",
sa.Column("id", sa.Integer(), autoincrement=False, nullable=False),
sa.Column("payer_id", sa.Integer(), autoincrement=False, nullable=True),
sa.Column("amount", sa.Float(), autoincrement=False, nullable=True),
sa.Column("date", sa.Date(), autoincrement=False, nullable=True),
sa.Column("creation_date", sa.Date(), autoincrement=False, nullable=True),
sa.Column("what", sa.UnicodeText(), autoincrement=False, nullable=True),
sa.Column(
"external_link", sa.UnicodeText(), autoincrement=False, nullable=True
),
sa.Column("archive", sa.Integer(), autoincrement=False, nullable=True),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint("id", "transaction_id"),
)
op.create_index(
op.f("ix_bill_version_end_transaction_id"),
"bill_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_bill_version_operation_type"),
"bill_version",
["operation_type"],
unique=False,
)
op.create_index(
op.f("ix_bill_version_transaction_id"),
"bill_version",
["transaction_id"],
unique=False,
)
op.create_table(
"billowers_version",
sa.Column("bill_id", sa.Integer(), autoincrement=False, nullable=False),
sa.Column("person_id", sa.Integer(), autoincrement=False, nullable=False),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint("bill_id", "person_id", "transaction_id"),
)
op.create_index(
op.f("ix_billowers_version_end_transaction_id"),
"billowers_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_billowers_version_operation_type"),
"billowers_version",
["operation_type"],
unique=False,
)
op.create_index(
op.f("ix_billowers_version_transaction_id"),
"billowers_version",
["transaction_id"],
unique=False,
)
op.create_table(
"person_version",
sa.Column("id", sa.Integer(), autoincrement=False, nullable=False),
sa.Column(
"project_id", sa.String(length=64), autoincrement=False, nullable=True
),
sa.Column("name", sa.UnicodeText(), autoincrement=False, nullable=True),
sa.Column("weight", sa.Float(), autoincrement=False, nullable=True),
sa.Column("activated", sa.Boolean(), autoincrement=False, nullable=True),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint("id", "transaction_id"),
)
op.create_index(
op.f("ix_person_version_end_transaction_id"),
"person_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_person_version_operation_type"),
"person_version",
["operation_type"],
unique=False,
)
op.create_index(
op.f("ix_person_version_transaction_id"),
"person_version",
["transaction_id"],
unique=False,
)
op.create_table(
"project_version",
sa.Column("id", sa.String(length=64), autoincrement=False, nullable=False),
sa.Column("name", sa.UnicodeText(), autoincrement=False, nullable=True),
sa.Column(
"password", sa.String(length=128), autoincrement=False, nullable=True
),
sa.Column(
"contact_email", sa.String(length=128), autoincrement=False, nullable=True
),
sa.Column(
"logging_preference",
sa.Enum("DISABLED", "ENABLED", "RECORD_IP", name="loggingmode"),
server_default="ENABLED",
autoincrement=False,
nullable=True,
),
sa.Column(
"transaction_id", sa.BigInteger(), autoincrement=False, nullable=False
),
sa.Column("end_transaction_id", sa.BigInteger(), nullable=True),
sa.Column("operation_type", sa.SmallInteger(), nullable=False),
sa.PrimaryKeyConstraint("id", "transaction_id"),
)
op.create_index(
op.f("ix_project_version_end_transaction_id"),
"project_version",
["end_transaction_id"],
unique=False,
)
op.create_index(
op.f("ix_project_version_operation_type"),
"project_version",
["operation_type"],
unique=False,
)
op.create_index(
op.f("ix_project_version_transaction_id"),
"project_version",
["transaction_id"],
unique=False,
)
op.create_table(
"transaction",
sa.Column("issued_at", sa.DateTime(), nullable=True),
sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column("remote_addr", sa.String(length=50), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
bind = op.get_bind()
if bind.engine.name == "sqlite":
with op.batch_alter_table("project", recreate="always") as batch_op:
batch_op.add_column(
sa.Column(
"logging_preference",
sa.Enum("DISABLED", "ENABLED", "RECORD_IP", name="loggingmode"),
server_default="ENABLED",
nullable=False,
),
)
else:
op.add_column(
"project",
sa.Column(
"logging_preference",
sa.Enum("DISABLED", "ENABLED", "RECORD_IP", name="loggingmode"),
server_default="ENABLED",
nullable=False,
),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
bind = op.get_bind()
if bind.engine.name == "sqlite":
with op.batch_alter_table("project", recreate="always") as batch_op:
batch_op.drop_column("logging_preference")
else:
op.drop_column("project", "logging_preference")
op.drop_table("transaction")
op.drop_index(
op.f("ix_project_version_transaction_id"), table_name="project_version"
)
op.drop_index(
op.f("ix_project_version_operation_type"), table_name="project_version"
)
op.drop_index(
op.f("ix_project_version_end_transaction_id"), table_name="project_version"
)
op.drop_table("project_version")
op.drop_index(op.f("ix_person_version_transaction_id"), table_name="person_version")
op.drop_index(op.f("ix_person_version_operation_type"), table_name="person_version")
op.drop_index(
op.f("ix_person_version_end_transaction_id"), table_name="person_version"
)
op.drop_table("person_version")
op.drop_index(
op.f("ix_billowers_version_transaction_id"), table_name="billowers_version"
)
op.drop_index(
op.f("ix_billowers_version_operation_type"), table_name="billowers_version"
)
op.drop_index(
op.f("ix_billowers_version_end_transaction_id"), table_name="billowers_version"
)
op.drop_table("billowers_version")
op.drop_index(op.f("ix_bill_version_transaction_id"), table_name="bill_version")
op.drop_index(op.f("ix_bill_version_operation_type"), table_name="bill_version")
op.drop_index(op.f("ix_bill_version_end_transaction_id"), table_name="bill_version")
op.drop_table("bill_version")
# ### end Alembic commands ###

View file

@ -0,0 +1,73 @@
"""Add currencies
Revision ID: 927ed575acbd
Revises: cb038f79982e
Create Date: 2020-04-25 14:49:41.136602
"""
# revision identifiers, used by Alembic.
revision = "927ed575acbd"
down_revision = "cb038f79982e"
from alembic import op
import sqlalchemy as sa
from ihatemoney.currency_convertor import CurrencyConverter
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("bill", sa.Column("converted_amount", sa.Float(), nullable=True))
op.add_column(
"bill",
sa.Column(
"original_currency",
sa.String(length=3),
server_default=CurrencyConverter.default,
nullable=True,
),
)
op.add_column(
"bill_version",
sa.Column("converted_amount", sa.Float(), autoincrement=False, nullable=True),
)
op.add_column(
"bill_version",
sa.Column(
"original_currency", sa.String(length=3), autoincrement=False, nullable=True
),
)
op.add_column(
"project",
sa.Column(
"default_currency",
sa.String(length=3),
server_default=CurrencyConverter.default,
nullable=True,
),
)
op.add_column(
"project_version",
sa.Column(
"default_currency", sa.String(length=3), autoincrement=False, nullable=True
),
)
# ### end Alembic commands ###
op.execute(
"""
UPDATE bill
SET converted_amount = amount
WHERE converted_amount IS NULL
"""
)
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("project_version", "default_currency")
op.drop_column("project", "default_currency")
op.drop_column("bill_version", "original_currency")
op.drop_column("bill_version", "converted_amount")
op.drop_column("bill", "original_currency")
op.drop_column("bill", "converted_amount")
# ### end Alembic commands ###

View file

@ -0,0 +1,60 @@
"""sqlite_autoincrement
Revision ID: cb038f79982e
Revises: 2dcb0c0048dc
Create Date: 2020-04-13 17:40:02.426957
"""
# revision identifiers, used by Alembic.
revision = "cb038f79982e"
down_revision = "2dcb0c0048dc"
from alembic import op
import sqlalchemy as sa
def upgrade():
bind = op.get_bind()
if bind.engine.name == "sqlite":
alter_table_batches = [
op.batch_alter_table(
"person", recreate="always", table_kwargs={"sqlite_autoincrement": True}
),
op.batch_alter_table(
"bill", recreate="always", table_kwargs={"sqlite_autoincrement": True}
),
op.batch_alter_table(
"billowers",
recreate="always",
table_kwargs={"sqlite_autoincrement": True},
),
]
for batch_op in alter_table_batches:
with batch_op:
pass
def downgrade():
bind = op.get_bind()
if bind.engine.name == "sqlite":
alter_table_batches = [
op.batch_alter_table(
"person",
recreate="always",
table_kwargs={"sqlite_autoincrement": False},
),
op.batch_alter_table(
"bill", recreate="always", table_kwargs={"sqlite_autoincrement": False}
),
op.batch_alter_table(
"billowers",
recreate="always",
table_kwargs={"sqlite_autoincrement": False},
),
]
for batch_op in alter_table_batches:
with batch_op:
pass

View file

@ -1,17 +1,49 @@
from collections import defaultdict
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy, BaseQuery
from flask import g, current_app
from debts import settle
from sqlalchemy import orm
from sqlalchemy.sql import func
from flask import current_app, g
from flask_sqlalchemy import BaseQuery, SQLAlchemy
from itsdangerous import (
TimedJSONWebSignatureSerializer,
URLSafeSerializer,
BadSignature,
SignatureExpired,
TimedJSONWebSignatureSerializer,
URLSafeSerializer,
)
import sqlalchemy
from sqlalchemy import orm
from sqlalchemy.sql import func
from sqlalchemy_continuum import make_versioned, version_class
from sqlalchemy_continuum.plugins import FlaskPlugin
from ihatemoney.patch_sqlalchemy_continuum import PatchedBuilder
from ihatemoney.versioning import (
ConditionalVersioningManager,
LoggingMode,
get_ip_if_allowed,
version_privacy_predicate,
)
make_versioned(
user_cls=None,
manager=ConditionalVersioningManager(
# Conditionally Disable the versioning based on each
# project's privacy preferences
tracking_predicate=version_privacy_predicate,
# Patch in a fix to a SQLAchemy-Continuum Bug.
# See patch_sqlalchemy_continuum.py
builder=PatchedBuilder(),
),
plugins=[
FlaskPlugin(
# Redirect to our own function, which respects user preferences
# on IP address collection
remote_addr_factory=get_ip_if_allowed,
# Suppress the plugin's attempt to grab a user id,
# which imports the flask_login module (causing an error)
current_user_id_factory=lambda: None,
)
],
)
db = SQLAlchemy()
@ -22,14 +54,24 @@ class Project(db.Model):
def get_by_name(self, name):
return Project.query.filter(Project.name == name).one()
# Direct SQLAlchemy-Continuum to track changes to this model
__versioned__ = {}
id = db.Column(db.String(64), primary_key=True)
name = db.Column(db.UnicodeText)
password = db.Column(db.String(128))
contact_email = db.Column(db.String(128))
logging_preference = db.Column(
db.Enum(LoggingMode),
default=LoggingMode.default(),
nullable=False,
server_default=LoggingMode.default().name,
)
members = db.relationship("Person", backref="project")
query_class = ProjectQuery
default_currency = db.Column(db.String(3))
@property
def _to_serialize(self):
@ -37,7 +79,9 @@ class Project(db.Model):
"id": self.id,
"name": self.name,
"contact_email": self.contact_email,
"logging_preference": self.logging_preference.value,
"members": [],
"default_currency": self.default_currency,
}
balance = self.balance
@ -86,7 +130,10 @@ class Project(db.Model):
{
"member": member,
"paid": sum(
[bill.amount for bill in self.get_member_bills(member.id).all()]
[
bill.converted_amount
for bill in self.get_member_bills(member.id).all()
]
),
"spent": sum(
[
@ -109,7 +156,7 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills().all():
monthly[bill.date.year][bill.date.month] += bill.amount
monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@property
@ -277,8 +324,11 @@ class Project(db.Model):
return None
return data["project_id"]
def __str__(self):
return self.name
def __repr__(self):
return "<Project %s>" % self.name
return f"<Project {self.name}>"
class Person(db.Model):
@ -301,6 +351,11 @@ class Person(db.Model):
query_class = PersonQuery
# Direct SQLAlchemy-Continuum to track changes to this model
__versioned__ = {}
__table_args__ = {"sqlite_autoincrement": True}
id = db.Column(db.Integer, primary_key=True)
project_id = db.Column(db.String(64), db.ForeignKey("project.id"))
bills = db.relationship("Bill", backref="payer")
@ -331,14 +386,15 @@ class Person(db.Model):
return self.name
def __repr__(self):
return "<Person %s for project %s>" % (self.name, self.project.name)
return f"<Person {self.name} for project {self.project.name}>"
# We need to manually define a join table for m2m relations
billowers = db.Table(
"billowers",
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id")),
db.Column("person_id", db.Integer, db.ForeignKey("person.id")),
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
db.Column("person_id", db.Integer, db.ForeignKey("person.id"), primary_key=True),
sqlite_autoincrement=True,
)
@ -365,6 +421,11 @@ class Bill(db.Model):
query_class = BillQuery
# Direct SQLAlchemy-Continuum to track changes to this model
__versioned__ = {}
__table_args__ = {"sqlite_autoincrement": True}
id = db.Column(db.Integer, primary_key=True)
payer_id = db.Column(db.Integer, db.ForeignKey("person.id"))
@ -376,6 +437,9 @@ class Bill(db.Model):
what = db.Column(db.UnicodeText)
external_link = db.Column(db.UnicodeText)
original_currency = db.Column(db.String(3))
converted_amount = db.Column(db.Float)
archive = db.Column(db.Integer, db.ForeignKey("archive.id"))
@property
@ -389,9 +453,11 @@ class Bill(db.Model):
"creation_date": self.creation_date,
"what": self.what,
"external_link": self.external_link,
"original_currency": self.original_currency,
"converted_amount": self.converted_amount,
}
def pay_each(self):
def pay_each_default(self, amount):
"""Compute what each share has to pay"""
if self.owers:
weights = (
@ -399,15 +465,20 @@ class Bill(db.Model):
.join(billowers, Bill)
.filter(Bill.id == self.id)
).scalar()
return self.amount / weights
return amount / weights
else:
return 0
def __str__(self):
return self.what
def pay_each(self):
return self.pay_each_default(self.converted_amount)
def __repr__(self):
return "<Bill of %s from %s for %s>" % (
self.amount,
self.payer,
", ".join([o.name for o in self.owers]),
return (
f"<Bill of {self.amount} from {self.payer} for "
f"{', '.join([o.name for o in self.owers])}>"
)
@ -426,3 +497,10 @@ class Archive(db.Model):
def __repr__(self):
return "<Archive>"
sqlalchemy.orm.configure_mappers()
PersonVersion = version_class(Person)
ProjectVersion = version_class(Project)
BillVersion = version_class(Bill)

View file

@ -0,0 +1,138 @@
"""
A temporary work-around to patch SQLAlchemy-continuum per:
https://github.com/kvesteri/sqlalchemy-continuum/pull/242
Source code reproduced under their license:
Copyright (c) 2012, Konsta Vesterinen
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The names of the contributors may not be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
import sqlalchemy as sa
from sqlalchemy_continuum import Operation
from sqlalchemy_continuum.builder import Builder
from sqlalchemy_continuum.expression_reflector import VersionExpressionReflector
from sqlalchemy_continuum.relationship_builder import RelationshipBuilder
from sqlalchemy_continuum.utils import adapt_columns, option
class PatchedRelationShipBuilder(RelationshipBuilder):
def association_subquery(self, obj):
"""
Returns an EXISTS clause that checks if an association exists for given
SQLAlchemy declarative object. This query is used by
many_to_many_criteria method.
Example query:
.. code-block:: sql
EXISTS (
SELECT 1
FROM article_tag_version
WHERE article_id = 3
AND tag_id = tags_version.id
AND operation_type != 2
AND EXISTS (
SELECT 1
FROM article_tag_version as article_tag_version2
WHERE article_tag_version2.tag_id = article_tag_version.tag_id
AND article_tag_version2.tx_id <=5
AND article_tag_version2.article_id = 3
GROUP BY article_tag_version2.tag_id
HAVING
MAX(article_tag_version2.tx_id) =
article_tag_version.tx_id
)
)
:param obj: SQLAlchemy declarative object
"""
tx_column = option(obj, "transaction_column_name")
join_column = self.property.primaryjoin.right.name
object_join_column = self.property.primaryjoin.left.name
reflector = VersionExpressionReflector(obj, self.property)
association_table_alias = self.association_version_table.alias()
association_cols = [
association_table_alias.c[association_col.name]
for _, association_col in self.remote_to_association_column_pairs
]
association_exists = sa.exists(
sa.select([1])
.where(
sa.and_(
association_table_alias.c[tx_column] <= getattr(obj, tx_column),
association_table_alias.c[join_column]
== getattr(obj, object_join_column),
*[
association_col
== self.association_version_table.c[association_col.name]
for association_col in association_cols
]
)
)
.group_by(*association_cols)
.having(
sa.func.max(association_table_alias.c[tx_column])
== self.association_version_table.c[tx_column]
)
.correlate(self.association_version_table)
)
return sa.exists(
sa.select([1])
.where(
sa.and_(
reflector(self.property.primaryjoin),
association_exists,
self.association_version_table.c.operation_type != Operation.DELETE,
adapt_columns(self.property.secondaryjoin),
)
)
.correlate(self.local_cls, self.remote_cls)
)
class PatchedBuilder(Builder):
def build_relationships(self, version_classes):
"""
Builds relationships for all version classes.
:param version_classes: list of generated version classes
"""
for cls in version_classes:
if not self.manager.option(cls, "versioning"):
continue
for prop in sa.inspect(cls).iterate_properties:
if prop.key == "versions":
continue
builder = PatchedRelationShipBuilder(self.manager, cls, prop)
builder()

View file

@ -2,13 +2,15 @@ import os
import os.path
import warnings
from flask import Flask, g, request, session, render_template
from flask import Flask, g, render_template, request, session
from flask_babel import Babel
from flask_mail import Mail
from flask_migrate import Migrate, upgrade, stamp
from flask_migrate import Migrate, stamp, upgrade
from werkzeug.middleware.proxy_fix import ProxyFix
from ihatemoney import default_settings
from ihatemoney.api.v1 import api as apiv1
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import db
from ihatemoney.utils import (
IhmJSONEncoder,
@ -19,8 +21,6 @@ from ihatemoney.utils import (
)
from ihatemoney.web import main as web_interface
from ihatemoney import default_settings
def setup_database(app):
"""Prepare the database. Create tables, run migrations etc."""
@ -138,6 +138,9 @@ def create_app(
# Configure the a, root="main"pplication
setup_database(app)
# Setup Currency Cache
CurrencyConverter()
mail = Mail()
mail.init_app(app)
app.mail = mail

View file

@ -162,6 +162,11 @@ body {
margin-bottom: 20px;
margin-left: 25px;
}
@media (max-width: 400px) {
.home .card {
min-width: unset;
}
}
/* Other */
#bills {
@ -192,7 +197,7 @@ footer {
padding: 45px 50px;
}
@media (min-width: 768px) {
@media (min-width: 1024px) {
footer {
padding-left: calc(25% + 50px);
}
@ -332,13 +337,16 @@ footer .footer-left {
background: url("../images/see.png") no-repeat right;
}
#bill_table, #monthly_stats {
#bill_table,
#monthly_stats,
#history_table {
margin-top: 30px;
margin-bottom: 30px;
}
@media (min-width: 768px) {
.split_bills, #table_overflow.statistics {
.split_bills,
#table_overflow.statistics {
/* The table is shifted to left, so add the spacer width on the right to match */
width: calc(100% + 15px);
}
@ -368,6 +376,36 @@ footer .footer-left {
background: url("../images/see.png") no-repeat right;
}
.history_icon > .delete,
.history_icon > .add,
.history_icon > .edit {
font-size: 0px;
display: block;
width: 16px;
height: 16px;
margin: 2px;
margin-right: 10px;
margin-top: 3px;
float: left;
}
.history_icon > .delete {
background: url("../images/delete.png") no-repeat right;
}
.history_icon > .edit {
background: url("../images/edit.png") no-repeat right;
}
.history_icon > .add {
background: url("../images/add.png") no-repeat right;
}
.history_text {
display: table-cell;
}
.balance .balance-value {
text-align: right;
}
@ -529,6 +567,13 @@ footer .icon svg {
fill: white;
}
.icon.icon-red {
fill: #dc3545;
}
.btn:hover .icon.icon-red {
fill: white !important;
}
/* align the first column */
#monthly_stats tr *:first-child {
text-align: right;
@ -538,3 +583,17 @@ footer .icon svg {
.hiddenpswp {
display: none;
}
#history_warnings {
margin-top: 30px;
}
/* edit settings */
.edit-project form {
margin-top: 1em;
margin-bottom: 3em;
}
.edit-project .custom-file {
margin-bottom: 2em;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 B

View file

@ -0,0 +1,8 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 807 807"><g
transform="translate(-20,-20)"
id="g8"><polygon
id="polygon6"
points="173,20 423,269 673,20 827,173 577,423 827,673 673,827 423,577 173,827 20,673 269,423 20,173 "
class="fil0" /></g></svg>

After

Width:  |  Height:  |  Size: 291 B

View file

@ -5,3 +5,7 @@ function selectCheckboxes(value){
els[i].checked = value;
}
}
function localizeTime(utcTimestamp) {
return new Date(utcTimestamp).toLocaleString()
}

View file

@ -10,4 +10,5 @@
<div class="col-xs-12 col-sm-5 col-md-4 offset-md-2">
<a href='{{ url_for("main.home") }}'>{{ _("The best thing to do is probably to get back to the main page.")}}</a>
</div>
</main>
{% endblock %}

View file

@ -5,18 +5,43 @@
{
$(this).html("<a style='color:red; ' href='{{ url_for('.delete_project') }}' >{{_("you sure?")}}</a>");
});
$('.custom-file-input').on('change', function(event) {
var filename = [].slice.call(this.files).map(function (file) { return file.name}).join(',')
var $labelElement = $(this).parents('.custom-file').find('.custom-file-label')
$labelElement.text(filename)
})
{% endblock %}
{% block content %}
<h2>{{ _("Edit project") }}</h2>
<p>
<form class="form-horizontal" method="post">
{{ forms.edit_project(edit_form) }}
</form>
</p>
<div class="container edit-project">
<h2>{{ _("Download project's data") }}</h2>
<p>
<h2>{{ _("Edit project") }}</h2>
<form class="form-horizontal" method="post">
{{ forms.edit_project(edit_form) }}
</form>
<h2>{{ _("Import JSON") }}</h2>
<form class="form-horizontal" method="post" enctype="multipart/form-data">
{{ import_form.hidden_tag() }}
<div class="custom-file">
<div class="form-group">
{{ import_form.file(class="custom-file-input") }}
<small class="form-text text-muted">
{{ import_form.file.description }}
</small>
</div>
<label class="custom-file-label" for="customFile">{{ _('Choose file') }}</label>
</div>
<div class="actions">
{{ import_form.submit(class="btn btn-primary") }}
</div>
</form>
<h2>{{ _("Download project's data") }}</h2>
<div class="list-group download-project">
<div class="list-group-item list-group-item-action">
<h5 class="d-flex w-100 justify-content-between">
@ -51,5 +76,5 @@
<p class="mb-1 text-muted">{{ _('Download the list of transactions needed to settle the current bills.') }}</p>
</div>
</div>
</p>
</div>
{% endblock %}

View file

@ -20,6 +20,16 @@
</div>
{% endmacro %}
{% macro checkbox(field) %}
<div class="controls{% if inline %} col-9{% endif %}">
{{ field(id=field.name) }}
<label for="{{ field.name }}">{{ field.label() }}</label>
{% if field.description %}
<small id="{{field.name}}_description"" class="form-text text-muted">{{ field.description }}</small>
{% endif %}
</div>
{% endmacro %}
{% macro submit(field, cancel=False, home=False) -%}
<div class="actions">
<button type="submit" class="btn btn-primary">{{ field.name }}</button>
@ -65,6 +75,7 @@
{{ input(form.name) }}
{{ input(form.password) }}
{{ input(form.contact_email) }}
{{ input(form.default_currency) }}
{% if not home %}
{{ submit(form.submit, home=True) }}
{% endif %}
@ -78,6 +89,15 @@
{{ input(form.name) }}
{{ input(form.password) }}
{{ input(form.contact_email) }}
<div class="form-group">
<label for="privacy_checkboxes">{{ _("Privacy Settings") }}</label>
<div id="privacy_checkboxes" class="card card-body bg-light">
{{ checkbox(form.project_history) }}
{{ checkbox(form.ip_recording) }}
</div>
</div>
{{ input(form.default_currency) }}
<div class="actions">
<button class="btn btn-primary">{{ _("Edit the project") }}</button>
<a id="delete-project" style="color:red; margin-left:10px; cursor:pointer; ">{{ _("delete") }}</a>
@ -104,6 +124,9 @@
{{ input(form.what, inline=True) }}
{{ input(form.payer, inline=True, class="form-control custom-select") }}
{{ input(form.amount, inline=True) }}
{% if not form.original_currency.render_kw %}
{{ input(form.original_currency, inline=True) }}
{% endif %}
{{ input(form.external_link, inline=True) }}
<div class="form-group row">

View file

@ -0,0 +1,261 @@
{% extends "sidebar_table_layout.html" %}
{% macro change_to_logging_preference(event) %}
{% if event.val_after == LoggingMode.DISABLED %}
{% if event.val_before == LoggingMode.ENABLED %}
{{ _("Disabled Project History") }}
{% else %}
{{ _("Disabled Project History & IP Address Recording") }}
{% endif %}
{% elif event.val_after == LoggingMode.ENABLED %}
{% if event.val_before == LoggingMode.DISABLED %}
{{ _("Enabled Project History") }}
{% elif event.val_before == LoggingMode.RECORD_IP %}
{{ _("Disabled IP Address Recording") }}
{% else %}
{{ _("Enabled Project History") }}
{% endif %}
{% elif event.val_after == LoggingMode.RECORD_IP %}
{% if event.val_before == LoggingMode.DISABLED %}
{{ _("Enabled Project History & IP Address Recording") }}
{% elif event.val_before == LoggingMode.ENABLED %}
{{ _("Enabled IP Address Recording") }}
{% else %}
{{ _("Enabled Project History & IP Address Recording") }}
{% endif %}
{% else %}
{# Should be unreachable #}
{{ _("History Settings Changed") }}
{% endif %}
{% endmacro %}
{% macro describe_object(event) %}{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em>{% endmacro %}
{% macro simple_property_change(event, localized_property_name, from=True) %}
{{ describe_object(event) }}:
{{ localized_property_name }} {{ _("changed") }}
{% if from %}{{ _("from") }} <em class="font-italic">{{ event.val_before }}</em>{% endif %}
{{ _("to") }} <em class="font-italic">{{ event.val_after }}</em>
{% endmacro %}
{% macro clear_history_modals() %}
<!-- Modal -->
<div id="confirm-ip-delete" class="modal fade show" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ _('Confirm Remove IP Adresses') }}</h3>
<a href="#" class="close" data-dismiss="modal">&times;</a>
</div>
<div class="modal-body">
<p>{{ _("Are you sure you want to delete all recorded IP addresses from this project?
The rest of the project history will be unaffected. This action cannot be undone.") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button>
<form action="{{ url_for(".strip_ip_addresses") }}" method="post">
<input type="submit" class="btn btn-danger" value="{{ _("Confirm Delete") }}" name="{{ _("Confirm Delete") }}"/>
</form>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div id="confirm-erase" class="modal fade show" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ _('Delete Confirmation') }}</h3>
<a href="#" class="close" data-dismiss="modal">&times;</a>
</div>
<div class="modal-body">
<p>{{ _("Are you sure you want to erase all history for this project? This action cannot be undone.") }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">{{ _("Close") }}</button>
<form action="{{ url_for(".erase_history") }}" method="post">
<input type="submit" class="btn btn-danger" value="{{ _("Confirm Delete") }}" name="{{ _("Confirm Delete") }}"/>
</form>
</div>
</div>
</div>
</div>
{% endmacro %}
{% macro owers_changed(event, add) %}
{{ describe_object(event) }}: {% if add %}{{ _("Added") }}{% else %}{{ _("Removed") }}{% endif %}
{% if event.val_after|length > 1 %}
{% for name in event.val_after %}
<em class="font-italic">{{ name }}</em>{% if event.val_after|length > 2 and loop.index != event.val_after|length %},{% endif %}
{% if loop.index == event.val_after|length - 1 %} {{ _("and") }} {% endif %}
{% endfor %}
{% else %}
<em class="font-italic">{{ event.val_after[0] }}</em>
{% endif %}
{% if add %}{{ _("to") }}{% else %}{{ _("from") }}{% endif %}
{{ _("owers list") }}
{% endmacro %}
{% block sidebar %}
<div id="table_overflow">
<table class="balance table">
<thead>
<tr class="d-none d-md-table-row">
<th>{{ _("Who?") }}</th>
<th class="balance-value">{{ _("Balance") }}</th>
</tr>
</thead>
{% set balance = g.project.balance %}
{% for member in g.project.members | sort(attribute='name') if member.activated or balance[member.id]|round(2) != 0 %}
<tr id="bal-member-{{ member.id }}" action={% if member.activated %}delete{% else %}reactivate{% endif %}>
<td class="balance-name">{{ member.name }}</td>
<td class="balance-value {% if balance[member.id]|round(2) > 0 %}positive{% elif balance[member.id]|round(2) < 0 %}negative{% endif %}">
{% if balance[member.id]|round(2) > 0 %}+{% endif %}{{ "%.2f" | format(balance[member.id]) }}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
{% block content %}
{% if current_log_pref == LoggingMode.DISABLED or (current_log_pref != LoggingMode.RECORD_IP and any_ip_addresses) %}
<div id="history_warnings" class="card card-body bg-light">
{% if current_log_pref == LoggingMode.DISABLED %}
<p>{% set url = url_for(".edit_project") %}
{% trans %}
<i>This project has history disabled. New actions won't appear below. You can enable history on the</i>
<a href="{{ url }}">settings page</a>
{% endtrans %}
</p>
{% if history %}
<p>
{% trans %}
<i>The table below reflects actions recorded prior to disabling project history. You can
<a href="#" data-toggle="modal" data-keyboard="false" data-target="#confirm-erase">clear project history</a> to remove them.</i></p>
{% endtrans %}
{% endif %}
{% endif %}
{% if current_log_pref != LoggingMode.RECORD_IP and any_ip_addresses %}
<p>
<i>{{ _("Some entries below contain IP addresses, even though this project has IP recording disabled. ") }}
<a href="#" data-toggle="modal" data-keyboard="false" data-target="#confirm-ip-delete">{{ _("Delete stored IP addresses") }}</a></i>
</p>
{% endif %}
</div>
{% endif %}
{{ clear_history_modals() }}
<span class="float-right mt-3" {% if not history %} data-toggle="tooltip" title="{{_('No history to erase')}}" {% endif %}>
<a href="#" class="btn btn-outline-danger float-right {% if not history %} disabled {% endif %}" data-toggle="modal" data-keyboard="false" data-target="#confirm-erase">
<i class="icon icon-red plus">{{ static_include("images/x.svg") | safe }}</i>
{{ _("Clear Project History") }}
</a>
</span>
<span class="float-right mt-3" {% if not any_ip_addresses %}data-placement="top" data-toggle="tooltip" title="{{_('No IP Addresses to erase')}}" {% endif %}>
<a href="#" class="btn btn-outline-danger float-right mr-2 {% if not any_ip_addresses %} disabled {% endif %}" data-toggle="modal" data-keyboard="false" data-target="#confirm-ip-delete">
<i class="icon icon-red plus">{{ static_include("images/x.svg") | safe }}</i>
{{ _("Delete Stored IP Addresses") }}
</a>
</span>
<div class="clearfix"></div>
{% if history %}
<table id="history_table" class="split_bills table table-striped">
<thead><tr>
<th style="width: 15%">{{ _("Time") }}</th>
<th style="width: 65%">{{ _("Event") }}</th>
<th style="width: 20%">
<span data-toggle="tooltip" title="{% if current_log_pref != LoggingMode.RECORD_IP %}
{{_('IP address recording can be enabled on the settings page') }}
{% else %}
{{_('IP address recording can be disabled on the settings page') }}
{% endif %}">
{{ _("From IP") }}</span></th>
</tr></thead>
<tbody>
{% for event in history %}
<tr>
<td><script>document.write(localizeTime("{{ event.time }}"));</script></td>
<td >
<div class="history_icon">
<i {% if event.operation_type == OperationType.INSERT %}
class="add"
{% elif event.operation_type == OperationType.UPDATE %}
class="edit"
{% elif event.operation_type == OperationType.DELETE %}
class="delete"
{% endif %}
></i>
</div>
<div class="history_text">
{% if event.operation_type == OperationType.INSERT %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em> {{ _("added") }}
{% elif event.operation_type == OperationType.UPDATE %}
{% if event.object_type == _("Project") %}
{% if event.prop_changed == "password" %}
{{ _("Project private code changed") }}
{% elif event.prop_changed == "logging_preference" %}
{{ change_to_logging_preference(event) }}
{% elif event.prop_changed == "name" %}
{{ _("Project renamed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% elif event.prop_changed == "contact_email" %}
{{ _("Project contact email changed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% else %}
{{ _("Project settings modified") }}
{% endif %}
{% elif event.prop_changed == "activated" %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em>
{% if event.val_after == False %}{{ _("deactivated") }}{% else %}{{ _("reactivated") }}{% endif %}
{% elif event.prop_changed == "name" or event.prop_changed == "what" %}
{{ describe_object(event) }} {{ _("renamed to") }} <em class="font-italic">{{ event.val_after }}</em>
{% elif event.prop_changed == "weight" %}
{{ simple_property_change(event, _("Weight")) }}
{% elif event.prop_changed == "external_link" %}
{{ describe_object(event) }}: {{ _("External link changed to") }}
<a href="{{ event.val_after }}" class="font-italic">{{ event.val_after }}</a>
{% elif event.prop_changed == "owers_added" %}
{{ owers_changed(event, True)}}
{% elif event.prop_changed == "owers_removed" %}
{{ owers_changed(event, False)}}
{% elif event.prop_changed == "payer" %}
{{ simple_property_change(event, _("Payer"))}}
{% elif event.prop_changed == "amount" %}
{{ simple_property_change(event, _("Amount")) }}
{% elif event.prop_changed == "date" %}
{{ simple_property_change(event, _("Date")) }}
{% elif event.prop_changed == "original_currency" %}
{{ simple_property_change(event, _("Currency")) }}
{% elif event.prop_changed == "converted_amount" %}
{{ simple_property_change(event, _("Amount in %(currency)s", currency=g.project.default_currency)) }}
{% else %}
{{ describe_object(event) }} {{ _("modified") }}
{% endif %}
{% elif event.operation_type == OperationType.DELETE %}
{{ event.object_type }} <em class="font-italic">{{ event.object_desc }}</em> {{ _("removed") }}
{% else %}
{# Should be unreachable #}
{{ describe_object(event) }} {{ _("changed in a unknown way") }}
{% endif %}
</div>
</td>
<td>{% if event.ip %}{{ event.ip }}{% else %} -- {% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="py-3 d-flex justify-content-center empty-bill">
<div class="card d-inline-flex p-2">
<div class="card-body text-center text-muted">
<i class="icon icon-white hand-holding-heart">{{ static_include("images/hand-holding-heart.svg") | safe }}</i>
<h3>{{ _('Nothing to list')}}</h3>
<p>
{{ _("Someone probably cleared the project history.") }}
</p>
</div>
</div></div>
{% endif %}
{% endblock %}

View file

@ -47,8 +47,8 @@
<li class="nav-item{% if current_view == 'list_bills' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.list_bills") }}">{{ _("Bills") }}</a></li>
<li class="nav-item{% if current_view == 'settle_bill' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.settle_bill") }}">{{ _("Settle") }}</a></li>
<li class="nav-item{% if current_view == 'statistics' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.statistics") }}">{{ _("Statistics") }}</a></li>
<li class="nav-item{% if current_view == 'history' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.history") }}">{{ _("History") }}</a></li>
<li class="nav-item{% if current_view == 'edit_project' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.edit_project") }}">{{ _("Settings") }}</a></li>
<li class="nav-item{% if current_view == 'upload_json' %} active{% endif %}"><a class="nav-link" href="{{ url_for("main.upload_json") }}">{{ _("Import") }}</a></li>
{% endblock %}
{% endif %}
</ul>
@ -127,9 +127,13 @@
</div>
<div class="messages">
{% for message in get_flashed_messages() %}
<div class="flash alert alert-success">{{ message }}</div>
{% endfor %}
{% for category, message in get_flashed_messages(with_categories=true) %}
{% if category == "message" %}{# Default category for flash(msg) #}
<div class="flash alert alert-success">{{ message }}</div>
{% else %}
<div class="flash alert alert-{{ category }}">{{ message }}</div>
{% endif %}
{% endfor %}
</div>
{% block footer %}

View file

@ -117,7 +117,19 @@
<div class="clearfix"></div>
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
<thead><tr><th>{{ _("When?") }}</th><th>{{ _("Who paid?") }}</<th><th>{{ _("For what?") }}</th><th>{{ _("For whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Actions") }}</th></tr></thead>
<thead>
<tr><th>{{ _("When?") }}
</th><th>{{ _("Who paid?") }}
</th><th>{{ _("For what?") }}
</th><th>{{ _("For whom?") }}
</th><th>{{ _("How much?") }}
{% if g.project.default_currency != "No Currency" %}
</th><th>{{ _("Amount in %(currency)s", currency=g.project.default_currency) }}
{%- else -%}
</th><th>{{ _("Amount") }}
{% endif %}
</th><th>{{ _("Actions") }}</th></tr>
</thead>
<tbody>
{% for bill in bills.items %}
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
@ -136,7 +148,14 @@
{%- else -%}
{{ bill.owers|join(', ', 'name') }}
{%- endif %}</td>
<td>{{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each()) }} {{ _("each") }})</td>
<td>
{% if bill.original_currency != "No Currency" %}
{{ "%0.2f"|format(bill.amount) }} {{bill.original_currency}} ({{ "%0.2f"|format(bill.pay_each_default(bill.amount)) }} {{bill.original_currency}} {{ _(" each") }})
{%- else -%}
{{ "%0.2f"|format(bill.amount) }} ({{ "%0.2f"|format(bill.pay_each_default(bill.amount)) }} {{ _(" each") }})
{% endif %}
</td>
<td>{{ "%0.2f"|format(bill.converted_amount) }}</td>
<td class="bill-actions">
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
<a class="delete" href="{{ url_for(".delete_bill", bill_id=bill.id) }}" title="{{ _("delete") }}">{{ _('delete') }}</a>

View file

@ -9,7 +9,7 @@
<th class="balance-value">{{ _("Balance") }}</th>
</tr>
</thead>
{% for stat in members_stats| sort(attribute='member.name') %}
{% for stat in members_stats|sort(attribute='member.name') %}
<tr>
<td class="balance-name">{{ stat.member.name }}</td>
<td class="balance-value {% if stat.balance|round(2) > 0 %}positive{% elif stat.balance|round(2) < 0 %}negative{% endif %}">
@ -27,7 +27,7 @@
<table id="bill_table" class="split_bills table table-striped ml-md-n3">
<thead><tr><th class="d-md-none">{{ _("Who?") }}</th><th>{{ _("Paid") }}</th><th>{{ _("Spent") }}</th></tr></thead>
<tbody>
{% for stat in members_stats %}
{% for stat in members_stats|sort(attribute='member.name') %}
<tr>
<td class="d-md-none">{{ stat.member.name }}</td>
<td>{{ "%0.2f"|format(stat.paid) }}</td>

View file

@ -1,10 +0,0 @@
{% extends "layout.html" %}
{% block content %}
<h2>{{ _("Import JSON") }}</h2>
<p>
<form class="form-horizontal" method="post" enctype="multipart/form-data">
{{ forms.upload_json(form) }}
</form>
</p>
{% endblock %}

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -1,16 +1,18 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-24 18:27+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: cs\n"
"Language-Team: none\n"
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)\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"
"X-Generator: Translate Toolkit 2.4.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -26,6 +28,18 @@ msgstr ""
msgid "Email"
msgstr ""
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr ""
@ -120,6 +134,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -153,6 +176,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -164,7 +193,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -218,9 +247,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -242,6 +268,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -251,6 +280,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -272,6 +307,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -302,6 +340,177 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -349,6 +558,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -406,6 +618,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
@ -432,9 +650,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -503,14 +718,14 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""

View file

@ -1,25 +1,26 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-12 09:58+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2020-02-12 10:50+0000\n"
"Last-Translator: flolilo <flolilo@mailbox.org>\n"
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/de/>\n"
"Language: de\n"
"Language-Team: German <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/de/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.11-dev\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Kein gültiger Betrag oder Ausdruck. Es werden nur Zahlen und die Operatoren +"
" - * / akzeptiert."
"Kein gültiger Betrag oder Ausdruck. Es werden nur Zahlen und die "
"Operatoren + - * / akzeptiert."
msgid "Project name"
msgstr "Projektname"
@ -30,6 +31,18 @@ msgstr "Privater Code"
msgid "Email"
msgstr "E-Mail"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Projektkennung"
@ -41,8 +54,8 @@ msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
"Ein Projekt mit der Kennung (\"%(project)s\") existiert bereits. Bitte wähle "
"eine andere Kennung"
"Ein Projekt mit der Kennung (\"%(project)s\") existiert bereits. Bitte "
"wähle eine andere Kennung"
msgid "Get in"
msgstr "Eintreten"
@ -80,6 +93,12 @@ msgstr "Von"
msgid "Amount paid"
msgstr "Betrag"
msgid "External link"
msgstr ""
msgid "A link to an external document, related to this bill"
msgstr ""
msgid "For whom?"
msgstr "Für wen?"
@ -120,6 +139,15 @@ msgstr "Einladung senden"
msgid "The email %(email)s is not valid"
msgstr "Die E-Mail-Adresse(n) %(email)s ist/sind nicht gültig"
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr "Projekt"
msgid "Too many failed login attempts, please retry later."
msgstr "Zu viele fehlgeschlagene Anmeldeversuche, bitte versuche es später."
@ -130,8 +158,7 @@ msgstr ""
"verbleibend."
msgid "You either provided a bad token or no project identifier."
msgstr ""
"Du hast entweder einen ungültigen Token oder keine Projekt-ID angegeben."
msgstr "Du hast entweder einen ungültigen Token oder keine Projekt-ID angegeben."
msgid "This private code is not the right one"
msgstr "Der private Code ist nicht korrekt"
@ -158,6 +185,12 @@ msgstr "Unbekanntes Projekt"
msgid "Password successfully reset."
msgstr "Passwort erfolgreich zurückgesetzt."
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr "Projekt erfolgreich gelöscht"
@ -169,8 +202,8 @@ msgid "Your invitations have been sent"
msgstr "Deine Einladungen wurden versendet"
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s wurde(n) hinzugefügt"
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
@ -225,9 +258,6 @@ msgstr "?"
msgid "Create a new project"
msgstr "Neues Projekt erstellen"
msgid "Project"
msgstr "Projekt"
msgid "Number of members"
msgstr "Anzahl der Teilnehmer"
@ -249,6 +279,9 @@ msgstr "Bearbeiten"
msgid "delete"
msgstr "Löschen"
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr "Das Dashboard ist aktuell deaktiviert."
@ -258,6 +291,12 @@ msgstr "Bist du sicher?"
msgid "Edit project"
msgstr "Projekt bearbeiten"
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr "Projektdaten herunterladen"
@ -279,6 +318,9 @@ msgstr "Passwort vergessen?"
msgid "Cancel"
msgstr "Abbrechen"
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr "Projekt bearbeiten"
@ -309,6 +351,177 @@ msgstr "Einladung versenden"
msgid "Download"
msgstr "Herunterladen"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr "Wer?"
msgid "Balance"
msgstr "Bilanz"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr "Verwalten deine geteilten <br />Ausgaben ganz einfach"
@ -343,8 +556,9 @@ msgid ""
"This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!"
msgstr ""
"Dieser Zugangscode wird an deine Freunde gesendet. Es wird als Klartext auf "
"dem Server gespeichert. Bitte verwenden daher kein persönliches Passwort!"
"Dieser Zugangscode wird an deine Freunde gesendet. Es wird als Klartext "
"auf dem Server gespeichert. Bitte verwenden daher kein persönliches "
"Passwort!"
msgid "Account manager"
msgstr "Konten"
@ -358,6 +572,9 @@ msgstr "Bilanz"
msgid "Statistics"
msgstr "Statistik"
msgid "History"
msgstr ""
msgid "Settings"
msgstr "Einstellungen"
@ -415,6 +632,12 @@ msgstr "Du kannst anfangen, Teilnehmer hinzuzufügen"
msgid "Add a new bill"
msgstr "Neue Ausgabe"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr "Wann?"
@ -485,8 +708,8 @@ msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
"Du kannst die Projekt-ID und den privaten Code auf jedem Kommunikationsweg "
"weitergeben."
"Du kannst die Projekt-ID und den privaten Code auf jedem "
"Kommunikationsweg weitergeben."
msgid "Identifier:"
msgstr "ID:"
@ -495,8 +718,7 @@ msgid "Share the Link"
msgstr "Link teilen"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Du kannst den folgenden Link direkt über dein bevorzugtes Medium teilen"
msgstr "Du kannst den folgenden Link direkt über dein bevorzugtes Medium teilen"
msgid "Send via Emails"
msgstr "Per E-Mail versenden"
@ -507,10 +729,10 @@ msgid ""
" creation of this budget management project and we will "
"send them an email for you."
msgstr ""
"Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du über "
"die\n"
"\t\t\tErstellung dieses Projekts informieren möchtest, und wir senden ihnen "
"eine E-Mail."
"Gib eine (durch Kommas getrennte) Liste von E-Mail-Adressen an, die du "
"über die\n"
"\t\t\tErstellung dieses Projekts informieren möchtest, und wir senden "
"ihnen eine E-Mail."
msgid "Who pays?"
msgstr "Wer zahlt?"
@ -518,14 +740,21 @@ msgstr "Wer zahlt?"
msgid "To whom?"
msgstr "An wen?"
msgid "Who?"
msgstr "Wer?"
msgid "Paid"
msgstr "Bezahlt"
msgid "Spent"
msgstr "Ausgegeben"
msgid "Balance"
msgstr "Bilanz"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""
#~ msgid "Someone probably"
#~ msgstr ""
#~ msgid "cleared the project history."
#~ msgstr ""

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 21:48+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language: el\n"
@ -12,7 +12,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.7.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -28,6 +28,18 @@ msgstr ""
msgid "Email"
msgstr ""
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr ""
@ -122,6 +134,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -155,6 +176,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -166,7 +193,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -220,9 +247,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -244,6 +268,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -253,6 +280,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -274,6 +307,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -304,6 +340,177 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -351,6 +558,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -408,6 +618,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
@ -434,9 +650,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -505,97 +718,14 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
#~ msgid ""
#~ "Not a valid amount or expression.Only"
#~ " numbers and + - * / "
#~ "operatorsare accepted."
#~ msgstr ""
#~ msgid "What do you want to download ?"
#~ msgstr ""
#~ msgid "bills"
#~ msgstr ""
#~ msgid "transactions"
#~ msgstr ""
#~ msgid "Export file format"
#~ msgstr ""
#~ msgid "Edit this project"
#~ msgstr ""
#~ msgid "Download this project's data"
#~ msgstr ""
#~ msgid "Type user name here"
#~ msgstr ""
#~ msgid "No, thanks"
#~ msgstr ""
#~ msgid "Manage your shared <br>expenses, easily"
#~ msgstr ""
#~ msgid "Log to an existing project"
#~ msgstr ""
#~ msgid "log in"
#~ msgstr ""
#~ msgid "or create a new one"
#~ msgstr ""
#~ msgid "let's get started"
#~ msgstr ""
#~ msgid "options"
#~ msgstr ""
#~ msgid "Project settings"
#~ msgstr ""
#~ msgid "This is a free software"
#~ msgstr ""
#~ msgid "Invite people to join this project!"
#~ msgstr ""
#~ msgid "Added on"
#~ msgstr ""
#~ msgid "Nothing to list yet. You probably want to"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ "creation of this budget management "
#~ "project and we will send them an"
#~ " email for you."
#~ msgstr ""
#~ msgid ""
#~ "If you prefer, you can share the project identifier and the shared\n"
#~ "password by other communication means. "
#~ "Or even directly share the following "
#~ "link:"
#~ msgstr ""
#~ msgid "A link to reset your password has been sent to your email."
#~ msgstr ""
msgid "Period"
msgstr ""

View file

@ -1,19 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-20 11:52+0200\n"
"PO-Revision-Date: 2019-09-25 22:28+0000\n"
"Last-Translator: Diego Caraballo <diegocaraballo84@gmail.com>\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2020-05-03 15:20+0000\n"
"Last-Translator: Fabian Rodriguez <fabian@fabianrodriguez.com>\n"
"Language-Team: Spanish (Latin America) <https://hosted.weblate.org/projects/"
"i-hate-money/i-hate-money/es_419/>\n"
"Language: es_419\n"
"Language-Team: Spanish (Latin America) "
"<https://hosted.weblate.org/projects/i-hate-money/i-hate-money/es_419/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.7.0\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.1-dev\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -31,6 +31,18 @@ msgstr "Código privado"
msgid "Email"
msgstr "Correo Electrónico"
msgid "Enable project history"
msgstr "Habilitar historial del proyecto"
msgid "Use IP tracking for project history"
msgstr "Registrar la IPs para el historial del proyecto"
msgid "Import previously exported JSON file"
msgstr "Importar archivo JSON previamente exportado"
msgid "Import"
msgstr "Importar"
msgid "Project identifier"
msgstr "Identificador de proyecto"
@ -127,6 +139,15 @@ msgstr "Enviar invitaciones"
msgid "The email %(email)s is not valid"
msgstr "El correo electrónico %(email)s no es válido"
msgid "Participant"
msgstr "Participante"
msgid "Bill"
msgstr "Factura"
msgid "Project"
msgstr "Proyecto"
msgid "Too many failed login attempts, please retry later."
msgstr ""
"Demasiados intentos fallidos de inicio de sesión, vuelva a intentarlo más"
@ -164,6 +185,12 @@ msgstr "Proyecto desconocido"
msgid "Password successfully reset."
msgstr "Contraseña restablecida con éxito."
msgid "Project successfully uploaded"
msgstr "El proyecto se subió exitosamente"
msgid "Invalid JSON"
msgstr "JSON inválido"
msgid "Project successfully deleted"
msgstr "Proyecto eliminado correctamente"
@ -175,8 +202,8 @@ msgid "Your invitations have been sent"
msgstr "Sus invitaciones han sido enviadas"
#, python-format
msgid "%(member)s had been added"
msgstr "se han añadido %(member)s"
msgid "%(member)s has been added"
msgstr "Se añadieron %(member)s"
#, python-format
msgid "%(name)s is part of this project again"
@ -231,9 +258,6 @@ msgstr "?"
msgid "Create a new project"
msgstr "Crear un nuevo proyecto"
msgid "Project"
msgstr "Proyecto"
msgid "Number of members"
msgstr "Número de miembros"
@ -255,6 +279,9 @@ msgstr "Editar"
msgid "delete"
msgstr "Eliminar"
msgid "see"
msgstr "ver"
msgid "The Dashboard is currently deactivated."
msgstr "El panel está desactivado actualmente."
@ -264,6 +291,12 @@ msgstr "¿Estás seguro?"
msgid "Edit project"
msgstr "Editar proyecto"
msgid "Import JSON"
msgstr "Importar JSON"
msgid "Choose file"
msgstr "Escoger un archivo"
msgid "Download project's data"
msgstr "Descargar datos del proyecto"
@ -287,6 +320,9 @@ msgstr "¿No recuerdas la contraseña?"
msgid "Cancel"
msgstr "Cancelar"
msgid "Privacy Settings"
msgstr "Ajustes de privacidad"
msgid "Edit the project"
msgstr "Editar el proyecto"
@ -309,7 +345,7 @@ msgid "Edit this member"
msgstr "Editar este miembro"
msgid "john.doe@example.com, mary.moe@site.com"
msgstr "john.doe@example.com, mary.moe@site.com"
msgstr "juan.perez@example.com, ana.rodriguez@site.com"
msgid "Send the invitations"
msgstr "Enviar las invitaciones"
@ -317,6 +353,199 @@ msgstr "Enviar las invitaciones"
msgid "Download"
msgstr "Descargar"
msgid "Disabled Project History"
msgstr "Historial de proyecto activo"
msgid "Disabled Project History & IP Address Recording"
msgstr "Historial de proyecto y registros de dirección IP inactivos"
msgid "Enabled Project History"
msgstr "Historial de proyecto activo"
msgid "Disabled IP Address Recording"
msgstr "Registro de direcciones IP activo"
msgid "Enabled Project History & IP Address Recording"
msgstr "Historial de proyecto y registros de dirección IP activos"
msgid "Enabled IP Address Recording"
msgstr "Se activó el registros de dirección IP"
msgid "History Settings Changed"
msgstr "Se cambiaron los ajustes del historial"
msgid "changed"
msgstr "cambió"
msgid "from"
msgstr "de"
msgid "to"
msgstr "a"
msgid "Confirm Remove IP Adresses"
msgstr "Confirmar eliminación de direcciones IP"
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
"Por favor confirme la eliminación completa del registro de direcciones IP "
"del proyecto.\n"
" El resto de historial del proyecto no será afectado. Este "
"cambio es irreversible."
msgid "Close"
msgstr "Cerrar"
msgid "Confirm Delete"
msgstr "Confirmar eliminación"
msgid "Delete Confirmation"
msgstr "Confirmación de eliminación"
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
"Por favor confirme la eliminación completa del historial del proyecto. Esta "
"acción es irreversible."
msgid "Added"
msgstr "Agregado"
msgid "Removed"
msgstr "Eliminado"
msgid "and"
msgstr "y"
msgid "owers list"
msgstr "lista de deudores"
msgid "Who?"
msgstr "¿Quién?"
msgid "Balance"
msgstr "Balance"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
"\n"
" <i>El historial de este proyecto ha sido desactivado. Nuevas "
"operaciones no apareceran a continuacion. El historial se puede agregar</i> "
"\n"
" <a href=\"%(url)s\">en la página de ajustes</a>\n"
" "
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
"\n"
" <i>Este registro muestra la actividad previa a la desactivación "
"del historial del proyecto. Use la opción \n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" data-"
"target=\"#confirm-erase\">Eliminar historial del proyecto</a> para "
"borrarlo.</i></p>\n"
" "
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
"Algunos registros contienen direcciones IP, a pesar de que el registro de "
"direcciones IP del proyecto no está activo. "
msgid "Delete stored IP addresses"
msgstr "Borrar las direcciones IP registradas"
msgid "No history to erase"
msgstr "No hay historial para borrar"
msgid "Clear Project History"
msgstr "Borrar el historial del proyecto"
msgid "No IP Addresses to erase"
msgstr "No hay direcciones IP para borrar"
msgid "Delete Stored IP Addresses"
msgstr "Borrar direcciones IP registradas"
msgid "Time"
msgstr "Hora"
msgid "Event"
msgstr "Evento"
msgid "IP address recording can be enabled on the settings page"
msgstr "El registro de direcciones IP se puede activar en la página de ajustes"
msgid "IP address recording can be disabled on the settings page"
msgstr ""
"El registro de direcciones IP se puede desactivar en la página de ajustes"
msgid "From IP"
msgstr "IP de origen"
msgid "added"
msgstr "agregado"
msgid "Project private code changed"
msgstr "Se cambió el código privado del proyecto"
msgid "Project renamed to"
msgstr "Se cambió el nombre del proyecto a"
msgid "Project contact email changed to"
msgstr "Se cambió el correo electrónico de contacto a"
msgid "Project settings modified"
msgstr "Ajustes del proyecto modificados"
msgid "deactivated"
msgstr "desactivado"
msgid "reactivated"
msgstr "reactivado"
msgid "renamed to"
msgstr "se cambió de nombre a"
msgid "External link changed to"
msgstr "Se cambió el enlace externo por"
msgid "Amount"
msgstr "Monto"
msgid "modified"
msgstr "modificado"
msgid "removed"
msgstr "removido"
msgid "changed in a unknown way"
msgstr "se cambió de manera desconocida"
msgid "Nothing to list"
msgstr "Nada por listar"
msgid "Someone probably cleared the project history."
msgstr "Es probable que alguien borrara el historial del proyecto."
msgid "Manage your shared <br />expenses, easily"
msgstr "Gestione sus gastos compartidos <br />fácilmente"
@ -366,6 +595,9 @@ msgstr "Resolver"
msgid "Statistics"
msgstr "Estadísticas"
msgid "History"
msgstr "Historial"
msgid "Settings"
msgstr "Configuración"
@ -423,6 +655,12 @@ msgstr "Deberías comenzar agregando participantes"
msgid "Add a new bill"
msgstr "Añadir una nueva factura"
msgid "Newer bills"
msgstr "Nuevas facturas"
msgid "Older bills"
msgstr "Facturas anteriores"
msgid "When?"
msgstr "¿Cuando?"
@ -449,9 +687,6 @@ msgstr "Todo el mundo menos %(excluded)s"
msgid "each"
msgstr "Cada"
msgid "see"
msgstr "ver"
msgid "No bills"
msgstr "Sin facturas"
@ -478,7 +713,7 @@ msgstr ""
"correos electrónicos."
msgid "Return to home page"
msgstr "Regresar a la página principal"
msgstr "Regresar al inicio"
msgid "Your projects"
msgstr "Sus proyectos"
@ -530,15 +765,17 @@ msgstr "¿Quién paga?"
msgid "To whom?"
msgstr "¿A quién?"
msgid "Who?"
msgstr "¿Quién?"
msgid "Paid"
msgstr "Pagado"
msgid "Spent"
msgstr "Gastado"
msgid "Balance"
msgstr "Balance"
msgid "Expenses by Month"
msgstr "Gastos por mes"
msgid "Period"
msgstr "Período"
#~ msgid "%(member)s had been added"
#~ msgstr "se han añadido %(member)s"

View file

@ -7,9 +7,9 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-10-01 21:48+0200\n"
"PO-Revision-Date: 2019-10-07 22:56+0000\n"
"Last-Translator: Alexis Metaireau <alexis@notmyidea.org>\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2020-04-25 11:14+0000\n"
"Last-Translator: Rémy Hubscher <hubscher.remy@gmail.com>\n"
"Language-Team: French <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/fr/>\n"
"Language: fr\n"
@ -17,15 +17,15 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 3.9-dev\n"
"Generated-By: Babel 2.7.0\n"
"X-Generator: Weblate 4.0.2-dev\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Ceci n'est pas pas un montant ou une expression valide. Seuls les nombres"
" et les opérateurs + - * / sont acceptés."
"Ceci n'est pas un montant ou une expression valide. Seuls les nombres et "
"les opérateurs + - * / sont acceptés."
msgid "Project name"
msgstr "Nom de projet"
@ -36,6 +36,18 @@ msgstr "Code daccès"
msgid "Email"
msgstr "Email"
msgid "Enable project history"
msgstr "Activer l'historique de projet"
msgid "Use IP tracking for project history"
msgstr "Collecter les adresses IP dans l'historique de projet"
msgid "Import previously exported JSON file"
msgstr "Importer un fichier JSON précédemment exporté"
msgid "Import"
msgstr "Importer"
msgid "Project identifier"
msgstr "Identifiant du projet"
@ -132,6 +144,15 @@ msgstr "Envoyer les invitations"
msgid "The email %(email)s is not valid"
msgstr "Lemail %(email)s est invalide"
msgid "Participant"
msgstr "Participant"
msgid "Bill"
msgstr "Facture"
msgid "Project"
msgstr "Projet"
msgid "Too many failed login attempts, please retry later."
msgstr "Trop d'échecs dauthentification successifs, veuillez réessayer plus tard."
@ -167,6 +188,12 @@ msgstr "Project inconnu"
msgid "Password successfully reset."
msgstr "Le mot de passe a été changé avec succès."
msgid "Project successfully uploaded"
msgstr "Le projet a été correctement importé"
msgid "Invalid JSON"
msgstr "Le fichier JSON est invalide"
msgid "Project successfully deleted"
msgstr "Projet supprimé"
@ -178,8 +205,8 @@ msgid "Your invitations have been sent"
msgstr "Vos invitations ont bien été envoyées"
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s a bien été ajouté"
msgid "%(member)s has been added"
msgstr "%(member)s a été ajouté"
#, python-format
msgid "%(name)s is part of this project again"
@ -234,9 +261,6 @@ msgstr " ?"
msgid "Create a new project"
msgstr "Créer un nouveau projet"
msgid "Project"
msgstr "Projets"
msgid "Number of members"
msgstr "Nombre de membres"
@ -258,6 +282,9 @@ msgstr "éditer"
msgid "delete"
msgstr "supprimer"
msgid "see"
msgstr "voir"
msgid "The Dashboard is currently deactivated."
msgstr "Le tableau de bord est actuellement désactivée."
@ -267,6 +294,12 @@ msgstr "cest sûr ?"
msgid "Edit project"
msgstr "Éditer le projet"
msgid "Import JSON"
msgstr "Import JSON"
msgid "Choose file"
msgstr "Choisir un fichier"
msgid "Download project's data"
msgstr "Télécharger les données du projet"
@ -288,6 +321,9 @@ msgstr "Vous ne vous souvenez plus du code daccès ?"
msgid "Cancel"
msgstr "Annuler"
msgid "Privacy Settings"
msgstr "Vie privée"
msgid "Edit the project"
msgstr "Éditer le projet"
@ -318,6 +354,202 @@ msgstr "Envoyer les invitations"
msgid "Download"
msgstr "Télécharger"
msgid "Disabled Project History"
msgstr "Désactiver l'historique du projet"
msgid "Disabled Project History & IP Address Recording"
msgstr "Désactiver l'historique du projet et l'enregistrement des adresses IP"
msgid "Enabled Project History"
msgstr "Activer l'historique du projet"
msgid "Disabled IP Address Recording"
msgstr "Désactiver l'enregistrement des adresses IP"
msgid "Enabled Project History & IP Address Recording"
msgstr "Activer l'historique du projet et lenregistrement des adresses IP"
msgid "Enabled IP Address Recording"
msgstr "Activer l'enregistrement des adresses IP"
msgid "History Settings Changed"
msgstr "Paramètres d'historique modifiés"
msgid "changed"
msgstr "modifié"
msgid "from"
msgstr "du"
msgid "to"
msgstr "au"
msgid "Confirm Remove IP Adresses"
msgstr "Confirmer la suppression des adresses IP"
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
"Êtes-vous sur de vouloir supprimer toutes les adresses IP enregistrées pour "
"ce projet ?\n"
"Le reste de l'historique ne sera pas affecté. Cette action n'est pas "
"réversible."
msgid "Close"
msgstr "Fermer"
msgid "Confirm Delete"
msgstr "Confirmer la suppression"
msgid "Delete Confirmation"
msgstr "Confirmation de suppression"
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
"Êtes-vous sur de vouloir supprimer tout l'historique du projet ? Cette "
"action n'est pas réversible."
msgid "Added"
msgstr "Ajouté"
msgid "Removed"
msgstr "Supprimé"
msgid "and"
msgstr "et"
msgid "owers list"
msgstr "Liste des débiteurs"
msgid "Who?"
msgstr "Qui ?"
msgid "Balance"
msgstr "Solde"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
"\n"
" <i>L'historique de ce projet a été désactivé. Les nouvelles "
"actions n'apparaîtront pas ci-dessous. Vous pouvez réactiver l'historique"
" du projet dans les </i>\n"
" <a href=\"%(url)s\">paramètres du projet</a>\n"
" "
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
"\n"
" <i>Le tableau ci-dessous liste les actions enregistrées avant"
" la désactivation de l'historique du projet. Vous pouvez\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> cliquer ici pour"
" les supprimer.</i></p>\n"
" "
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
"Certaines entrées de l'historique contiennent une adresse IP, bien que ce"
" projet ait désactivé l'enregistrement des adresses IP. "
msgid "Delete stored IP addresses"
msgstr "Supprimer toutes les adresses IP enregistrées"
msgid "No history to erase"
msgstr "Aucun historique à supprimer"
msgid "Clear Project History"
msgstr "Supprimer les entrées de l'historique du projet"
msgid "No IP Addresses to erase"
msgstr "Aucune adresse IP à supprimer"
msgid "Delete Stored IP Addresses"
msgstr "Supprimer les adresses IP enregistrées"
msgid "Time"
msgstr "Heure"
msgid "Event"
msgstr "Évènement"
msgid "IP address recording can be enabled on the settings page"
msgstr ""
"L'enregistrement des adresses IP peut-être activé dans les paramètres de "
"la page"
msgid "IP address recording can be disabled on the settings page"
msgstr ""
"L'enregistrement des adresses IP peut-être désactivé dans les paramètres "
"de la page"
msgid "From IP"
msgstr "Depuis l'IP"
msgid "added"
msgstr "ajouté"
msgid "Project private code changed"
msgstr "Le mot de passe du projet a été modifié"
msgid "Project renamed to"
msgstr "Le projet a été renommé"
msgid "Project contact email changed to"
msgstr "L'adresse email de contact du projet a été modifié en"
msgid "Project settings modified"
msgstr "Les paramètres du projet ont été modifiés"
msgid "deactivated"
msgstr "désactivé"
msgid "reactivated"
msgstr "réactivé"
msgid "renamed to"
msgstr "renommé en"
msgid "External link changed to"
msgstr "Le lien d'accès a été modifié en"
msgid "Amount"
msgstr "Montant"
msgid "modified"
msgstr "modifié"
msgid "removed"
msgstr "supprimé"
msgid "changed in a unknown way"
msgstr "modifié d'une manière inconnue"
msgid "Nothing to list"
msgstr "Rien à afficher"
msgid "Someone probably cleared the project history."
msgstr "Quelqu'un a probablement vidé l'historique du projet."
msgid "Manage your shared <br />expenses, easily"
msgstr "Gérez vos dépenses <br />partagées, facilement"
@ -367,6 +599,9 @@ msgstr "Remboursements"
msgid "Statistics"
msgstr "Statistiques"
msgid "History"
msgstr "Historique"
msgid "Settings"
msgstr "Options"
@ -424,6 +659,12 @@ msgstr "Vous devriez commencer par ajouter des participants"
msgid "Add a new bill"
msgstr "Nouvelle facture"
msgid "Newer bills"
msgstr "Nouvelles factures"
msgid "Older bills"
msgstr "Ancienne factures"
msgid "When?"
msgstr "Quand ?"
@ -450,9 +691,6 @@ msgstr "Tout le monde sauf %(excluded)s"
msgid "each"
msgstr "chacun"
msgid "see"
msgstr "voir"
msgid "No bills"
msgstr "Pas encore de factures"
@ -527,27 +765,17 @@ msgstr "Qui doit payer ?"
msgid "To whom?"
msgstr "Pour qui ?"
msgid "Who?"
msgstr "Qui ?"
msgid "Paid"
msgstr "A payé"
msgid "Spent"
msgstr "A dépensé"
msgid "Balance"
msgstr "Solde"
msgid "Import"
msgstr "Importer"
msgid "Project successfully uploaded"
msgstr "Le projet a été correctement importé"
msgid "Invalid JSON"
msgstr "Le fichier JSON est invalide"
msgid "Expenses by Month"
msgstr "Dépenses par mois"
msgid "Period"
msgstr "Période"
#~ msgid ""
#~ "The project identifier is used to "
@ -695,3 +923,72 @@ msgstr "Le fichier JSON est invalide"
#~ msgid "A link to reset your password has been sent to your email."
#~ msgstr "Un lien pour changer votre mot de passe vous a été envoyé par mail."
#~ msgid "%(member)s had been added"
#~ msgstr "%(member)s a bien été ajouté"
#~ msgid "Disabled Project History"
#~ msgstr "Historisation du projet désactivée"
#~ msgid "Disabled Project History & IP Address Recording"
#~ msgstr "Historisation du projet et enregistrement des adresses IP désactivés"
#~ msgid "Enabled Project History"
#~ msgstr "Historisation du projet activée"
#~ msgid "Disabled IP Address Recording"
#~ msgstr "Enregistrement des adresses IP désactivé"
#~ msgid "Enabled Project History & IP Address Recording"
#~ msgstr "Historisation du projet et enregistrement des adresses IP activés"
#~ msgid "Enabled IP Address Recording"
#~ msgstr "Enregistrement des adresses IP activé"
#~ msgid "History Settings Changed"
#~ msgstr "Changement des paramètres dhistorisation"
#~ msgid "changed"
#~ msgstr "modifié"
#~ msgid "from"
#~ msgstr "depuis"
#~ msgid "to"
#~ msgstr "vers"
#~ msgid "Confirm Remove IP Adresses"
#~ msgstr "Confirmer la suppression des adresses IP"
#~ msgid ""
#~ "Are you sure you want to delete"
#~ " all recorded IP addresses from this"
#~ " project?\n"
#~ " The rest of the project"
#~ " history will be unaffected. This "
#~ "action cannot be undone."
#~ msgstr ""
#~ "Êtes vous sûr de supprimer toutes "
#~ "les adresses IP enregistrées dans ce "
#~ "projet ?\n"
#~ "Le reste de lhistorique du projet "
#~ "restera inchangé. Cette action est "
#~ "irréversible."
#~ msgid "Close"
#~ msgstr "Fermer"
#~ msgid "Confirm Delete"
#~ msgstr "Confirmer la suppression"
#~ msgid "Delete Confirmation"
#~ msgstr "Confirmation de suppression"
#~ msgid ""
#~ "Are you sure you want to erase "
#~ "all history for this project? This "
#~ "action cannot be undone."
#~ msgstr ""
#~ "Êtes vous sûr de supprimer la "
#~ "totalité de lhistorique de ce projet"
#~ " ? Cette action est irréversible."

Binary file not shown.

View file

@ -1,24 +1,26 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-15 09:12+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2019-11-16 10:04+0000\n"
"Last-Translator: Muhammad Fauzi <fauzi.padlaw@gmail.com>\n"
"Language-Team: Indonesian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/id/>\n"
"Language: id\n"
"Language-Team: Indonesian <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/id/>\n"
"Plural-Forms: nplurals=1; plural=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"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 3.10-dev\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Jumlah atau operator tidak valid. Hanya angka dan opertaor +-* yang diterima."
"Jumlah atau operator tidak valid. Hanya angka dan opertaor +-* yang "
"diterima."
msgid "Project name"
msgstr "Nama proyek"
@ -29,6 +31,18 @@ msgstr "Kode pribadi"
msgid "Email"
msgstr "Surel"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Pengidentifikasi proyek"
@ -40,8 +54,8 @@ msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
"Sebuah proyek dengan ID ini (\"%(project)s\") sudah ada. Silakan pilih ID "
"baru"
"Sebuah proyek dengan ID ini (\"%(project)s\") sudah ada. Silakan pilih ID"
" baru"
msgid "Get in"
msgstr "Masuk"
@ -125,6 +139,15 @@ msgstr "Kirim undangan"
msgid "The email %(email)s is not valid"
msgstr "Surel %(email)s tidak valid"
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr "Proyek"
msgid "Too many failed login attempts, please retry later."
msgstr "Terlalu banyak percobaan masuk, silakan coba lagi nanti."
@ -158,6 +181,12 @@ msgstr "Proyek tidak diketahui"
msgid "Password successfully reset."
msgstr "Kata sandi berhasil diatur ulang."
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr "Proyek berhasil dihapus"
@ -169,8 +198,8 @@ msgid "Your invitations have been sent"
msgstr "Undangan Anda telah dikirim"
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s telah ditambahkan"
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
@ -225,9 +254,6 @@ msgstr "?"
msgid "Create a new project"
msgstr "Buat proyek baru"
msgid "Project"
msgstr "Proyek"
msgid "Number of members"
msgstr "Jumlah anggota"
@ -249,6 +275,9 @@ msgstr "ubah"
msgid "delete"
msgstr "hapus"
msgid "see"
msgstr "lihat"
msgid "The Dashboard is currently deactivated."
msgstr "Dasbor sekarang ini sedang dinonaktifkan."
@ -258,6 +287,12 @@ msgstr "Anda yakin?"
msgid "Edit project"
msgstr "Ubah proyek"
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr "Unduh data proyek"
@ -279,6 +314,9 @@ msgstr "Tidak bisa mengingat kata sandi?"
msgid "Cancel"
msgstr "Batalkan"
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr "Ubah proyek"
@ -309,6 +347,177 @@ msgstr "Kirim undangan"
msgid "Download"
msgstr "Unduh"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr "Siapa?"
msgid "Balance"
msgstr "Saldo"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr "Atur pembagian harga <br />Anda, dengan mudah"
@ -343,8 +552,8 @@ msgid ""
"This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!"
msgstr ""
"Kode akses ini akan dikirimkan ke teman Anda. Kode ini disimpan dalam bentuk "
"teks biasa dalam server, jadi jangan gunakan password Anda!"
"Kode akses ini akan dikirimkan ke teman Anda. Kode ini disimpan dalam "
"bentuk teks biasa dalam server, jadi jangan gunakan password Anda!"
msgid "Account manager"
msgstr "Pengatur akun"
@ -358,6 +567,9 @@ msgstr "Atur"
msgid "Statistics"
msgstr "Statistik"
msgid "History"
msgstr ""
msgid "Settings"
msgstr "Pengaturan"
@ -415,6 +627,12 @@ msgstr "Anda harus mulai dengan menambahkan partisipan"
msgid "Add a new bill"
msgstr "Tambah tagihan baru"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr "Kapan?"
@ -441,9 +659,6 @@ msgstr "Semua orang kecuali %(excluded)s"
msgid "each"
msgstr "setiap"
msgid "see"
msgstr "lihat"
msgid "No bills"
msgstr "Tidak ada tagihan"
@ -466,8 +681,8 @@ msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
"Tautan atur ulang kata sandi telah dikirim kepada Anda, silakan cek email "
"Anda."
"Tautan atur ulang kata sandi telah dikirim kepada Anda, silakan cek email"
" Anda."
msgid "Return to home page"
msgstr "Kembali ke halaman depan"
@ -499,7 +714,8 @@ msgstr "Bagikan tautan"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Anda bisa membagikan tautan secara langsung melalui media yang Anda inginkan"
"Anda bisa membagikan tautan secara langsung melalui media yang Anda "
"inginkan"
msgid "Send via Emails"
msgstr "Kirim melalui surel"
@ -510,10 +726,10 @@ msgid ""
" creation of this budget management project and we will "
"send them an email for you."
msgstr ""
"Spesifikkan daftar alamat surel (dipisah dengan koma) yang akan Anda kirim "
"pemberitahuan tentang\n"
" pembuatan dari manajemen anggaran proyek ini dan kami akan "
"memngirim mereka sebuah surel."
"Spesifikkan daftar alamat surel (dipisah dengan koma) yang akan Anda "
"kirim pemberitahuan tentang\n"
" pembuatan dari manajemen anggaran proyek ini dan kami "
"akan memngirim mereka sebuah surel."
msgid "Who pays?"
msgstr "Siapa membayar?"
@ -521,14 +737,24 @@ msgstr "Siapa membayar?"
msgid "To whom?"
msgstr "Kepada siapa?"
msgid "Who?"
msgstr "Siapa?"
msgid "Paid"
msgstr "Dibayar"
msgid "Spent"
msgstr "Dihabiskan"
msgid "Balance"
msgstr "Saldo"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""
#~ msgid "%(member)s had been added"
#~ msgstr "%(member)s telah ditambahkan"
#~ msgid "Someone probably"
#~ msgstr ""
#~ msgid "cleared the project history."
#~ msgstr ""

View file

@ -0,0 +1,729 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-28 10:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 2.5.1\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
msgid "Project name"
msgstr ""
msgid "Private code"
msgstr ""
msgid "Email"
msgstr ""
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr ""
msgid "Create the project"
msgstr ""
#, python-format
msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
msgid "Get in"
msgstr ""
msgid "Admin password"
msgstr ""
msgid "Send me the code by email"
msgstr ""
msgid "This project does not exists"
msgstr ""
msgid "Password mismatch"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password confirmation"
msgstr ""
msgid "Reset password"
msgstr ""
msgid "Date"
msgstr ""
msgid "What?"
msgstr ""
msgid "Payer"
msgstr ""
msgid "Amount paid"
msgstr ""
msgid "External link"
msgstr ""
msgid "A link to an external document, related to this bill"
msgstr ""
msgid "For whom?"
msgstr ""
msgid "Submit"
msgstr ""
msgid "Submit and add a new one"
msgstr ""
msgid "Bills can't be null"
msgstr ""
msgid "Name"
msgstr ""
msgid "Weights should be positive"
msgstr ""
msgid "Weight"
msgstr ""
msgid "Add"
msgstr ""
msgid "User name incorrect"
msgstr ""
msgid "This project already have this member"
msgstr ""
msgid "People to notify"
msgstr ""
msgid "Send invites"
msgstr ""
#, python-format
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
msgid "You either provided a bad token or no project identifier."
msgstr ""
msgid "This private code is not the right one"
msgstr ""
#, python-format
msgid "You have just created '%(project)s' to share your expenses"
msgstr ""
#, python-format
msgid "%(msg_compl)sThe project identifier is %(project)s"
msgstr ""
msgid "No token provided"
msgstr ""
msgid "Invalid token"
msgstr ""
msgid "Unknown project"
msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
msgstr ""
msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
msgstr ""
#, python-format
msgid ""
"User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero."
msgstr ""
#, python-format
msgid "User '%(name)s' has been removed"
msgstr ""
#, python-format
msgid "User '%(name)s' has been edited"
msgstr ""
msgid "The bill has been added"
msgstr ""
msgid "The bill has been deleted"
msgstr ""
msgid "The bill has been modified"
msgstr ""
msgid "Sorry, we were unable to find the page you've asked for."
msgstr ""
msgid "The best thing to do is probably to get back to the main page."
msgstr ""
msgid "Back to the list"
msgstr ""
msgid "Administration tasks are currently disabled."
msgstr ""
msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
msgid "create it"
msgstr ""
msgid "?"
msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Number of members"
msgstr ""
msgid "Number of bills"
msgstr ""
msgid "Newest bill"
msgstr ""
msgid "Oldest bill"
msgstr ""
msgid "Actions"
msgstr ""
msgid "edit"
msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
msgid "you sure?"
msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
msgid "Bill items"
msgstr ""
msgid "Download the list of bills with owner, amount, reason,... "
msgstr ""
msgid "Settle plans"
msgstr ""
msgid "Download the list of transactions needed to settle the current bills."
msgstr ""
msgid "Can't remember the password?"
msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
msgid "Edit this bill"
msgstr ""
msgid "Add a bill"
msgstr ""
msgid "Select all"
msgstr ""
msgid "Select none"
msgstr ""
msgid "Add participant"
msgstr ""
msgid "Edit this member"
msgstr ""
msgid "john.doe@example.com, mary.moe@site.com"
msgstr ""
msgid "Send the invitations"
msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
msgid "Try out the demo"
msgstr ""
msgid "You're sharing a house?"
msgstr ""
msgid "Going on holidays with friends?"
msgstr ""
msgid "Simply sharing money with others?"
msgstr ""
msgid "We can help!"
msgstr ""
msgid "Log in to an existing project"
msgstr ""
msgid "Log in"
msgstr ""
msgid "can't remember your password?"
msgstr ""
msgid "Create"
msgstr ""
msgid ""
"This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!"
msgstr ""
msgid "Account manager"
msgstr ""
msgid "Bills"
msgstr ""
msgid "Settle"
msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
msgid "Languages"
msgstr ""
msgid "Projects"
msgstr ""
msgid "Start a new project"
msgstr ""
msgid "Other projects :"
msgstr ""
msgid "switch to"
msgstr ""
msgid "Dashboard"
msgstr ""
msgid "Logout"
msgstr ""
msgid "Code"
msgstr ""
msgid "Mobile Application"
msgstr ""
msgid "Documentation"
msgstr ""
msgid "Administation Dashboard"
msgstr ""
msgid "\"I hate money\" is a free software"
msgstr ""
msgid "you can contribute and improve it!"
msgstr ""
msgid "deactivate"
msgstr ""
msgid "reactivate"
msgstr ""
msgid "Invite people"
msgstr ""
msgid "You should start by adding participants"
msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
msgid "Who paid?"
msgstr ""
msgid "For what?"
msgstr ""
msgid "How much?"
msgstr ""
#, python-format
msgid "Added on %(date)s"
msgstr ""
msgid "Everyone"
msgstr ""
#, python-format
msgid "Everyone but %(excluded)s"
msgstr ""
msgid "each"
msgstr ""
msgid "No bills"
msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgstr ""
msgid "Password reminder"
msgstr ""
msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
msgid "Return to home page"
msgstr ""
msgid "Your projects"
msgstr ""
msgid "Reset your password"
msgstr ""
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr ""
msgid "You can directly share the following link via your prefered medium"
msgstr ""
msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
msgstr ""
msgid "Who pays?"
msgstr ""
msgid "To whom?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""

View file

@ -1,19 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-09-30 23:53+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2019-11-12 09:04+0000\n"
"Last-Translator: Allan Nordhøy <epost@anotheragency.no>\n"
"Language-Team: Norwegian Bokmål <https://hosted.weblate.org/projects/"
"i-hate-money/i-hate-money/nb_NO/>\n"
"Language: nb_NO\n"
"Language-Team: Norwegian Bokmål <https://hosted.weblate.org/projects/i"
"-hate-money/i-hate-money/nb_NO/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.10-dev\n"
"Generated-By: Babel 2.7.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -31,6 +31,18 @@ msgstr "Privat kode"
msgid "Email"
msgstr "E-post"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Prosjektidentifikator"
@ -129,6 +141,15 @@ msgstr "Send invitasjoner"
msgid "The email %(email)s is not valid"
msgstr "E-posten \"%(email)s\" er ikke gyldig"
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr "Prosjekt"
msgid "Too many failed login attempts, please retry later."
msgstr "For mange mislykkede innloggingsforsøk, prøv igjen senere."
@ -165,6 +186,12 @@ msgstr "Ukjent prosjekt"
msgid "Password successfully reset."
msgstr "Passord tilbakestilt."
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
#, fuzzy
msgid "Project successfully deleted"
msgstr "Prosjekt slettet"
@ -179,8 +206,8 @@ msgid "Your invitations have been sent"
msgstr "Invitasjonene dine har blitt sendt"
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s lagt til"
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
@ -237,9 +264,6 @@ msgstr "?"
msgid "Create a new project"
msgstr "Opprett et nytt prosjekt"
msgid "Project"
msgstr "Prosjekt"
msgid "Number of members"
msgstr "Antall medlemmer"
@ -261,6 +285,10 @@ msgstr "rediger"
msgid "delete"
msgstr "slett"
#, fuzzy
msgid "see"
msgstr "se"
msgid "The Dashboard is currently deactivated."
msgstr "Oversikten er for tiden avskrudd."
@ -270,6 +298,12 @@ msgstr "er du sikker?"
msgid "Edit project"
msgstr "Rediger prosjekt"
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr "Last ned prosjektets data"
@ -294,6 +328,9 @@ msgstr "Husker du ikke passordet?"
msgid "Cancel"
msgstr "Avbryt"
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr "Rediger prosjektet"
@ -325,6 +362,178 @@ msgstr "Send ut invitasjonene"
msgid "Download"
msgstr "Last nd"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr "Hvem?"
#, fuzzy
msgid "Balance"
msgstr "Kontobalanse"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr "Håndter delte <br />utgifter, enkelt"
@ -376,6 +585,9 @@ msgstr "Gjør opp"
msgid "Statistics"
msgstr "Statistikk"
msgid "History"
msgstr ""
msgid "Settings"
msgstr "Innstillinger"
@ -436,6 +648,12 @@ msgstr "Du kan starte ved å legge til deltagere"
msgid "Add a new bill"
msgstr "Legg til en ny regning"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr "Når?"
@ -462,10 +680,6 @@ msgstr "Alle, unntagen %(excluded)s"
msgid "each"
msgstr "hver"
#, fuzzy
msgid "see"
msgstr "se"
msgid "No bills"
msgstr "Ingen regninger"
@ -546,18 +760,17 @@ msgstr "Hvem betaler?"
msgid "To whom?"
msgstr "Til hvem?"
msgid "Who?"
msgstr "Hvem?"
msgid "Paid"
msgstr "Betalt"
msgid "Spent"
msgstr "Forbrukt"
#, fuzzy
msgid "Balance"
msgstr "Kontobalanse"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""
#~ msgid ""
#~ "The project identifier is used to "
@ -672,3 +885,6 @@ msgstr "Kontobalanse"
#~ "En lenke for å tilbakestille passordet"
#~ " har blitt sent til deg per "
#~ "e-post."
#~ msgid "%(member)s had been added"
#~ msgstr "%(member)s lagt til"

View file

@ -1,19 +1,19 @@
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2019-09-30 23:53+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2019-10-07 22:56+0000\n"
"Last-Translator: Heimen Stoffels <vistausss@outlook.com>\n"
"Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/nl/>\n"
"Language: nl\n"
"Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/i"
"-hate-money/nl/>\n"
"Plural-Forms: nplurals=2; plural=n != 1\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 3.9-dev\n"
"Generated-By: Babel 2.7.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -29,6 +29,18 @@ msgstr "Privécode"
msgid "Email"
msgstr "E-mailadres"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Project-id"
@ -123,6 +135,15 @@ msgstr "Uitnodigingen versturen"
msgid "The email %(email)s is not valid"
msgstr "Het e-mailadres '%(email)s' is onjuist"
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr "Project"
msgid "Too many failed login attempts, please retry later."
msgstr "Te vaak onjuist ingelogd. Probeer het later opnieuw."
@ -158,6 +179,12 @@ msgstr "Onbekend project"
msgid "Password successfully reset."
msgstr "Wachtwoord is hersteld."
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr "Project is verwijderd"
@ -169,8 +196,8 @@ msgid "Your invitations have been sent"
msgstr "Je uitnodigingen zijn verstuurd"
#, python-format
msgid "%(member)s had been added"
msgstr "%(member)s is toegevoegd"
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
@ -225,9 +252,6 @@ msgstr "?"
msgid "Create a new project"
msgstr "Nieuw project aanmaken"
msgid "Project"
msgstr "Project"
msgid "Number of members"
msgstr "Aantal deelnemers"
@ -249,6 +273,9 @@ msgstr "bewerken"
msgid "delete"
msgstr "verwijderen"
msgid "see"
msgstr "bekijk"
msgid "The Dashboard is currently deactivated."
msgstr "De overzichtspagina is momenteel uitgeschakeld."
@ -258,6 +285,12 @@ msgstr "weet je het zeker?"
msgid "Edit project"
msgstr "Project aanpassen"
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr "Projectgegevens downloaden"
@ -281,6 +314,9 @@ msgstr "Ben je je wachtwoord vergeten?"
msgid "Cancel"
msgstr "Annuleren"
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr "Project bewerken"
@ -311,6 +347,177 @@ msgstr "Uitnodigingen versturen"
msgid "Download"
msgstr "Downloaden"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr "Wie?"
msgid "Balance"
msgstr "Saldo"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr "Beheer eenvoudig je <br />gedeelde uitgaven"
@ -360,6 +567,9 @@ msgstr "Schikken"
msgid "Statistics"
msgstr "Statistieken"
msgid "History"
msgstr ""
msgid "Settings"
msgstr "Instellingen"
@ -417,6 +627,12 @@ msgstr "Begin met het toevoegen van deelnemers"
msgid "Add a new bill"
msgstr "Nieuwe rekening toevoegen"
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr "Wanneer?"
@ -443,9 +659,6 @@ msgstr "Iedereen, behalve %(excluded)s"
msgid "each"
msgstr "per persoon"
msgid "see"
msgstr "bekijk"
msgid "No bills"
msgstr "Geen rekeningen"
@ -522,128 +735,14 @@ msgstr "Wie betaalt?"
msgid "To whom?"
msgstr "Aan wie?"
msgid "Who?"
msgstr "Wie?"
msgid "Paid"
msgstr "Betaald"
msgid "Spent"
msgstr "Uitgegeven"
msgid "Balance"
msgstr "Saldo"
msgid "Expenses by Month"
msgstr ""
#~ msgid ""
#~ "The project identifier is used to "
#~ "log in and for the URL of "
#~ "the project. We tried to generate "
#~ "an identifier for you but a "
#~ "project with this identifier already "
#~ "exists. Please create a new identifier"
#~ " that you will be able to "
#~ "remember"
#~ msgstr ""
#~ "De project-id wordt gebruikt om in"
#~ " te loggen en als url van het"
#~ " project. We hebben geprobeerd om een"
#~ " id voor je te genereren, maar "
#~ "er is al een project met deze "
#~ "id. Creëer een nieuwe id die je"
#~ " makkelijk kunt onthouden."
#~ msgid ""
#~ "Not a valid amount or expression.Only"
#~ " numbers and + - * / "
#~ "operatorsare accepted."
#~ msgstr ""
#~ "Geen geldig bedrag of geldige expressie."
#~ " Alleen getallen en + - * / "
#~ "zijn toegestaan."
#~ msgid "What do you want to download ?"
#~ msgstr "Wat wil je downloaden?"
#~ msgid "bills"
#~ msgstr "rekeningen"
#~ msgid "transactions"
#~ msgstr "transacties"
#~ msgid "Export file format"
#~ msgstr "Bestandsformaat voor exporteren"
#~ msgid "Edit this project"
#~ msgstr "Dit project bewerken"
#~ msgid "Download this project's data"
#~ msgstr "Projectgegevens downloaden"
#~ msgid "Type user name here"
#~ msgstr "Typ hier de gebruikersnaam"
#~ msgid "No, thanks"
#~ msgstr "Nee, bedankt"
#~ msgid "Manage your shared <br>expenses, easily"
#~ msgstr "Beheer eenvoudig je gedeelde <br>uitgaven"
#~ msgid "Log to an existing project"
#~ msgstr "Log in op een bestaand project"
#~ msgid "log in"
#~ msgstr "inloggen"
#~ msgid "or create a new one"
#~ msgstr "of creëer een nieuwe"
#~ msgid "let's get started"
#~ msgstr "aan de slag"
#~ msgid "options"
#~ msgstr "opties"
#~ msgid "Project settings"
#~ msgstr "Projectinstellingen"
#~ msgid "This is a free software"
#~ msgstr "Dit is vrije software"
#~ msgid "Invite people to join this project!"
#~ msgstr "Nodig mensen uit voor dit project!"
#~ msgid "Added on"
#~ msgstr "Toegevoegd op"
#~ msgid "Nothing to list yet. You probably want to"
#~ msgstr "Er kan nog geen opsomming worden gemaakt. Voeg"
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ "creation of this budget management "
#~ "project and we will send them an"
#~ " email for you."
#~ msgstr ""
#~ "Geef een kommagescheiden lijst van "
#~ "e-mailadressen op. Deze mensen worden op"
#~ " de\n"
#~ "hoogte gebracht van het bestaan van "
#~ "dit project en wij sturen hen een"
#~ " e-mail."
#~ msgid ""
#~ "If you prefer, you can share the project identifier and the shared\n"
#~ "password by other communication means. "
#~ "Or even directly share the following "
#~ "link:"
#~ msgstr ""
#~ "Als je wilt, dan kun je de project-id en het gedeelde wachtwoord\n"
#~ "delen via andere kanalen. Of deel gewoon de volgende link:"
#~ msgid "A link to reset your password has been sent to your email."
#~ msgstr ""
#~ "Er is een link met "
#~ "wachtwoordherstelinstructies naar je e-mailadres "
#~ "verstuurd."
msgid "Period"
msgstr ""

View file

@ -0,0 +1,768 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-26 19:50+0200\n"
"PO-Revision-Date: 2020-04-28 07:11+0000\n"
"Last-Translator: Vsevolod <sevauserg.com@gmail.com>\n"
"Language-Team: Russian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/ru/>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.0.2\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
"Недопустимая сумма выражения. Принимаются только цифры и операторы + - * / ."
msgid "Project name"
msgstr "Имя проекта"
msgid "Private code"
msgstr "Приватный код"
msgid "Email"
msgstr "Email"
msgid "Enable project history"
msgstr "Включить историю проекта"
msgid "Use IP tracking for project history"
msgstr "Использовать отслеживание по IP для истории проекта"
msgid "Import previously exported JSON file"
msgstr "Импортировать ранее экспортированный JSON файл"
msgid "Import"
msgstr "Импортировать"
msgid "Project identifier"
msgstr "Идентификатор проекта"
msgid "Create the project"
msgstr "Создать проект"
#, python-format
msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
"Проект с идентификатором (\"%(project)s\") уже существует. Пожалуйста, "
"выберете новый идентификатор"
msgid "Get in"
msgstr "Войти"
msgid "Admin password"
msgstr "Пароль администратора"
msgid "Send me the code by email"
msgstr "Отправить код мне на Email"
msgid "This project does not exists"
msgstr "Такой проект не существует"
msgid "Password mismatch"
msgstr "Пароли не совпадают"
msgid "Password"
msgstr "Пароль"
msgid "Password confirmation"
msgstr "Подтвердите пароль"
msgid "Reset password"
msgstr "Восстановить пароль"
msgid "Date"
msgstr "Дата"
msgid "What?"
msgstr "Что?"
msgid "Payer"
msgstr "Плательщик"
msgid "Amount paid"
msgstr "Уплаченная сумма"
msgid "External link"
msgstr "Внешняя ссылка"
msgid "A link to an external document, related to this bill"
msgstr "Ссылка на внешний документ, относящийся к этому счёту"
msgid "For whom?"
msgstr "Кому?"
msgid "Submit"
msgstr "Отправить"
msgid "Submit and add a new one"
msgstr "Отправить и добавить новый"
msgid "Bills can't be null"
msgstr "Счета не могут быть нулевыми"
msgid "Name"
msgstr "Имя"
msgid "Weights should be positive"
msgstr "Вес должен быть положительным"
msgid "Weight"
msgstr "Вес"
msgid "Add"
msgstr "Добавить"
msgid "User name incorrect"
msgstr "Неправильное имя пользователя"
msgid "This project already have this member"
msgstr "В этом проекте уже есть такой участник"
msgid "People to notify"
msgstr "Люди для уведомления"
msgid "Send invites"
msgstr "Отправить приглашения"
#, python-format
msgid "The email %(email)s is not valid"
msgstr "Email %(email)s не правильный"
msgid "Participant"
msgstr "Участник"
msgid "Bill"
msgstr "Счёт"
msgid "Project"
msgstr "Проект"
msgid "Too many failed login attempts, please retry later."
msgstr "Слишком много неудачных попыток входа, попробуйте позже."
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
"Этот пароль администратора неправильный. Осталось только %(num)d попыток."
msgid "You either provided a bad token or no project identifier."
msgstr ""
"Вы либо предоставили неверный токен, либо не указали идентификатор проекта."
msgid "This private code is not the right one"
msgstr "Этот приватный код не подходит"
#, python-format
msgid "You have just created '%(project)s' to share your expenses"
msgstr "Вы только что создали '%(project)s' , чтобы разделить расходы"
#, python-format
msgid "%(msg_compl)sThe project identifier is %(project)s"
msgstr "%(msg_compl)sИдентификатор проекта это %(project)s"
msgid "No token provided"
msgstr "Не предоставлен токен"
msgid "Invalid token"
msgstr "Неправильный токен"
msgid "Unknown project"
msgstr "Неизвестный проект"
msgid "Password successfully reset."
msgstr "Пароль успешно восстановлен."
msgid "Project successfully uploaded"
msgstr "Проект успешно загружен"
msgid "Invalid JSON"
msgstr "Неправильный JSON"
msgid "Project successfully deleted"
msgstr "Проект удалён"
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
msgstr "Вас пригласили разделить расходы в проект %(project)s"
msgid "Your invitations have been sent"
msgstr "Ваш код приглашения был отправлен"
#, python-format
msgid "%(member)s has been added"
msgstr "%(member)s был добавлен"
#, python-format
msgid "%(name)s is part of this project again"
msgstr "%(name)s снова часть этого проекта"
#, python-format
msgid ""
"User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero."
msgstr ""
"Пользователь '%(name)s' был деактивирован. Он будет отображаться в списке "
"пользователей до тех пор, пока его баланс не станет равным нулю."
#, python-format
msgid "User '%(name)s' has been removed"
msgstr "Пользователь '%(name)s' был удалён"
#, python-format
msgid "User '%(name)s' has been edited"
msgstr "Пользователь '%(name)s' был изменён"
msgid "The bill has been added"
msgstr "Счёт был добавлен"
msgid "The bill has been deleted"
msgstr "Счёт был удалён"
msgid "The bill has been modified"
msgstr "Счёт был изменён"
msgid "Sorry, we were unable to find the page you've asked for."
msgstr "К сожалению, нам не удалось найти страницу, которую вы запросили."
msgid "The best thing to do is probably to get back to the main page."
msgstr "Лучше всего вернуться на главную страницу."
msgid "Back to the list"
msgstr "Вернутся к списку"
msgid "Administration tasks are currently disabled."
msgstr "Задачи администратора в данный момент отключены."
msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
"Проект, к которому вы пытаетесь получить доступ, не существует, вы хотите"
msgid "create it"
msgstr "создать его"
msgid "?"
msgstr "?"
msgid "Create a new project"
msgstr "Создать новый проект"
msgid "Number of members"
msgstr "Число участников"
msgid "Number of bills"
msgstr "Число счетов"
msgid "Newest bill"
msgstr "Новейший счёт"
msgid "Oldest bill"
msgstr "Старейший счёт"
msgid "Actions"
msgstr "Действия"
msgid "edit"
msgstr "изменить"
msgid "delete"
msgstr "удалить"
msgid "see"
msgstr "просмотреть"
msgid "The Dashboard is currently deactivated."
msgstr "Панель инструментов в данный момент отключена."
msgid "you sure?"
msgstr "вы уверены?"
msgid "Edit project"
msgstr "Изменить проект"
msgid "Import JSON"
msgstr "Импортировать JSON"
msgid "Choose file"
msgstr "Выбрать файл"
msgid "Download project's data"
msgstr "Скачать данные проекта"
msgid "Bill items"
msgstr "Пункты счета"
msgid "Download the list of bills with owner, amount, reason,... "
msgstr "Скачать список счетов с владельцем, суммой, причиной, .. "
msgid "Settle plans"
msgstr "Урегулировать планы"
msgid "Download the list of transactions needed to settle the current bills."
msgstr "Скачать список переводов нужных, чтобы урегулировать данные счета."
msgid "Can't remember the password?"
msgstr "Не помните пароль?"
msgid "Cancel"
msgstr "Отменить"
msgid "Privacy Settings"
msgstr "Настройки приватности"
msgid "Edit the project"
msgstr "Изменить проект"
msgid "Edit this bill"
msgstr "Изменить счёт"
msgid "Add a bill"
msgstr "Добавить счёт"
msgid "Select all"
msgstr "Выбрать всё"
msgid "Select none"
msgstr "Отменить выбор"
msgid "Add participant"
msgstr "Добавить участника"
msgid "Edit this member"
msgstr "Изменить этого участника"
msgid "john.doe@example.com, mary.moe@site.com"
msgstr "john.doe@example.com, mary.moe@site.com"
msgid "Send the invitations"
msgstr "Отправить приглашения"
msgid "Download"
msgstr "Скачать"
msgid "Disabled Project History"
msgstr "История отключенных проектов"
msgid "Disabled Project History & IP Address Recording"
msgstr "Отключенная история проекта и запись IP-адреса"
msgid "Enabled Project History"
msgstr "Включить историю проекта"
msgid "Disabled IP Address Recording"
msgstr "Выключить запись IP-адрессов"
msgid "Enabled Project History & IP Address Recording"
msgstr "Включить историю проекта и запись IP адрессов"
msgid "Enabled IP Address Recording"
msgstr "Включить запись IP адрессов"
msgid "History Settings Changed"
msgstr "Настройки истории изменены"
msgid "changed"
msgstr "изменены"
msgid "from"
msgstr "от"
msgid "to"
msgstr "кому"
msgid "Confirm Remove IP Adresses"
msgstr "Подтвердите удаление IP-адресов"
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
"Вы уверены, что хотите удалить все записанные IP-адреса из этого проекта?\n"
" Остальная часть истории проекта не будет затронута. Это "
"действие не может быть отменено."
msgid "Close"
msgstr "Закрыть"
msgid "Confirm Delete"
msgstr "Подтвердить удаление"
msgid "Delete Confirmation"
msgstr "Подтверждение удаления"
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
"Вы уверены, что хотите стереть историю проекта? Это действие нельзя отменить."
msgid "Added"
msgstr "Добавлен"
msgid "Removed"
msgstr "Удалён"
msgid "and"
msgstr "и"
msgid "owers list"
msgstr "список владельцев"
msgid "Who?"
msgstr "Кто?"
msgid "Balance"
msgstr "Баланс"
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
"\n"
" <i>У этого проекта история отключена. Новые действия не появятся "
"ниже. Вы можете включить историю в</i>\n"
" <a href=\"%(url)s\">настройках</a>\n"
" "
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
"\n"
" <i>В таблице ниже отражены действия, записанные до отключения "
"истории проекта. Вы можете\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" data-"
"target=\"#confirm-erase\">очистить историю проекта</a> to remove "
"them.</i></p>\n"
" "
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
"Некоторые записи ниже содержат IP-адреса, хотя в этом проекте запись IP "
"отключена. "
msgid "Delete stored IP addresses"
msgstr "Удалить сохраненные IP-адреса"
msgid "No history to erase"
msgstr "Нечего стирать"
msgid "Clear Project History"
msgstr "Стереть историю проекта"
msgid "No IP Addresses to erase"
msgstr "Нечего стирать"
msgid "Delete Stored IP Addresses"
msgstr "Удалить сохраненные IP-адреса"
msgid "Time"
msgstr "Время"
msgid "Event"
msgstr "Событие"
msgid "IP address recording can be enabled on the settings page"
msgstr "Запись IP-адреса может быть включена на странице настроек"
msgid "IP address recording can be disabled on the settings page"
msgstr "Запись IP-адреса может быть отключена на странице настроек"
msgid "From IP"
msgstr "От IP"
msgid "added"
msgstr "добавлен"
msgid "Project private code changed"
msgstr "Приватный код проекта изменен"
msgid "Project renamed to"
msgstr "Проект переименован в"
msgid "Project contact email changed to"
msgstr "Контактная почта проекта изменена на"
msgid "Project settings modified"
msgstr "Настройки проекта изменены"
msgid "deactivated"
msgstr "отключено"
msgid "reactivated"
msgstr "реактивирован"
msgid "renamed to"
msgstr "переименован в"
msgid "External link changed to"
msgstr "Внешняя ссылка изменена на"
msgid "Amount"
msgstr "Количество"
msgid "modified"
msgstr "изменено"
msgid "removed"
msgstr "удалено"
msgid "changed in a unknown way"
msgstr "изменилось неизвестным образом"
msgid "Nothing to list"
msgstr "Нечего перечислять"
msgid "Someone probably cleared the project history."
msgstr "Кто-то скорее всего стёр историю проекта."
msgid "Manage your shared <br />expenses, easily"
msgstr "Управляйте своими общими <br />расходами проще"
msgid "Try out the demo"
msgstr "Попробуйте"
msgid "You're sharing a house?"
msgstr "Вы живете в одном доме с другими людьми?"
msgid "Going on holidays with friends?"
msgstr "Собираетесь в отпуск с друзьями?"
msgid "Simply sharing money with others?"
msgstr "Просто делиться деньгами с другими?"
msgid "We can help!"
msgstr "Мы поможем!"
msgid "Log in to an existing project"
msgstr "Войти в существующий проект"
msgid "Log in"
msgstr "Войти"
msgid "can't remember your password?"
msgstr "не помните пароль?"
msgid "Create"
msgstr "Создать"
msgid ""
"This access code will be sent to your friends. It is stored as-is by the "
"server, so don\\'t reuse a personal password!"
msgstr ""
"Этот код доступа будет отправлен вашим друзьям. Он хранится на сервере как "
"есть, поэтому не используйте личный пароль!"
msgid "Account manager"
msgstr "Менеджер аккаунтов"
msgid "Bills"
msgstr "Счета"
msgid "Settle"
msgstr "Отрегулировать"
msgid "Statistics"
msgstr "Статистика"
msgid "History"
msgstr "История"
msgid "Settings"
msgstr "Настройки"
msgid "Languages"
msgstr "Языки"
msgid "Projects"
msgstr "Проекты"
msgid "Start a new project"
msgstr "Начать новый проект"
msgid "Other projects :"
msgstr "Остальные проекты :"
msgid "switch to"
msgstr "сменён на"
msgid "Dashboard"
msgstr "Панель инструментов"
msgid "Logout"
msgstr "Выйти"
msgid "Code"
msgstr "Код"
msgid "Mobile Application"
msgstr "Мобильное приложение"
msgid "Documentation"
msgstr "Документация"
msgid "Administation Dashboard"
msgstr "Панель инструментов администратора"
msgid "\"I hate money\" is a free software"
msgstr "\" I hate money \" - бесплатная программа"
msgid "you can contribute and improve it!"
msgstr "вы можете способствовать развитию и улучшать её!"
msgid "deactivate"
msgstr "отключить"
msgid "reactivate"
msgstr "включить"
msgid "Invite people"
msgstr "Пригласить людей"
msgid "You should start by adding participants"
msgstr "Вам стоит начать с добавления пользователей"
msgid "Add a new bill"
msgstr "Добавить новый счёт"
msgid "Newer bills"
msgstr "Новые счета"
msgid "Older bills"
msgstr "Старые счета"
msgid "When?"
msgstr "Когда?"
msgid "Who paid?"
msgstr "Кто заплатил?"
msgid "For what?"
msgstr "За что?"
msgid "How much?"
msgstr "Сколько?"
#, python-format
msgid "Added on %(date)s"
msgstr "Добавлено %(date)s"
msgid "Everyone"
msgstr "Каждый"
#, python-format
msgid "Everyone but %(excluded)s"
msgstr "Каждый, кроме %(excluded)s"
msgid "each"
msgstr "каждый"
msgid "No bills"
msgstr "Нет счетов"
msgid "Nothing to list yet."
msgstr "Нечего перечислять еще."
msgid "You probably want to"
msgstr "Возможно вы хотите"
msgid "add a bill"
msgstr "добавить счёт"
msgid "add participants"
msgstr "добавить пользователя"
msgid "Password reminder"
msgstr "Напоминание пароля"
msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
"Ссылка для восстановления пароля отправлена, пожалуйста, проверьте Email."
msgid "Return to home page"
msgstr "Вернуться на главную страницу"
msgid "Your projects"
msgstr "Ваши проекты"
msgid "Reset your password"
msgstr "Восстановить пароль"
msgid "Invite people to join this project"
msgstr "Пригласить людей присоединиться к этому проекту"
msgid "Share Identifier & code"
msgstr "Поделиться идентификатором и кодом"
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
"Вы можете поделиться идентификатором проекта и личным кодом любым способом "
"связи."
msgid "Identifier:"
msgstr "Идентификатор:"
msgid "Share the Link"
msgstr "Поделиться ссылкой"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
"Вы можете напрямую поделиться следующей ссылкой через любой способ связи"
msgid "Send via Emails"
msgstr "Отправить по почте"
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
msgstr ""
"Укажите (разделенный запятыми) список адресов электронной почты, которые вы "
"хотите уведомить о\n"
" создание этого проекта управления бюджетом, и мы вышлем им "
"письмо."
msgid "Who pays?"
msgstr "Кто платит?"
msgid "To whom?"
msgstr "Кому?"
msgid "Paid"
msgstr "Оплачено"
msgid "Spent"
msgstr "Потрачено"
msgid "Expenses by Month"
msgstr "Расходы по месяцам"
msgid "Period"
msgstr "Период"

View file

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 21:48+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2019-08-07 13:24+0000\n"
"Last-Translator: Mesut Akcan <makcan@gmail.com>\n"
"Language: tr\n"
@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.7.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -31,6 +31,18 @@ msgstr "Özel kod"
msgid "Email"
msgstr "E-posta"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Proje tanımlayıcısı"
@ -127,6 +139,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -160,6 +181,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -171,7 +198,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -225,9 +252,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -249,6 +273,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -258,6 +285,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -279,6 +312,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -309,6 +345,177 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -356,6 +563,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -413,6 +623,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
@ -439,9 +655,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -510,91 +723,14 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
#~ msgid "What do you want to download ?"
#~ msgstr ""
#~ msgid "bills"
#~ msgstr ""
#~ msgid "transactions"
#~ msgstr ""
#~ msgid "Export file format"
#~ msgstr ""
#~ msgid "Edit this project"
#~ msgstr ""
#~ msgid "Download this project's data"
#~ msgstr ""
#~ msgid "Type user name here"
#~ msgstr ""
#~ msgid "No, thanks"
#~ msgstr ""
#~ msgid "Manage your shared <br>expenses, easily"
#~ msgstr ""
#~ msgid "Log to an existing project"
#~ msgstr ""
#~ msgid "log in"
#~ msgstr ""
#~ msgid "or create a new one"
#~ msgstr ""
#~ msgid "let's get started"
#~ msgstr ""
#~ msgid "options"
#~ msgstr ""
#~ msgid "Project settings"
#~ msgstr ""
#~ msgid "This is a free software"
#~ msgstr ""
#~ msgid "Invite people to join this project!"
#~ msgstr ""
#~ msgid "Added on"
#~ msgstr ""
#~ msgid "Nothing to list yet. You probably want to"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ "creation of this budget management "
#~ "project and we will send them an"
#~ " email for you."
#~ msgstr ""
#~ msgid ""
#~ "If you prefer, you can share the project identifier and the shared\n"
#~ "password by other communication means. "
#~ "Or even directly share the following "
#~ "link:"
#~ msgstr ""
#~ msgid "A link to reset your password has been sent to your email."
#~ msgstr ""
msgid "Period"
msgstr ""

Binary file not shown.

View file

@ -1,19 +1,20 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-12-05 15:35+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2019-12-08 16:26+0000\n"
"Last-Translator: Tymofij Lytvynenko <till.svit@gmail.com>\n"
"Language-Team: Ukrainian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/uk/>\n"
"Language: uk\n"
"Language-Team: Ukrainian <https://hosted.weblate.org/projects/i-hate-"
"money/i-hate-money/uk/>\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<="
"4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 3.10-dev\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -29,6 +30,18 @@ msgstr "Приватний код"
msgid "Email"
msgstr "Е-пошта"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "Ідентифікатор проєкту"
@ -123,6 +136,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -156,6 +178,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -167,7 +195,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -221,9 +249,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -245,6 +270,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -254,6 +282,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -275,6 +309,9 @@ msgstr ""
msgid "Cancel"
msgstr ""
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -305,6 +342,177 @@ msgstr ""
msgid "Download"
msgstr ""
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -352,6 +560,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -409,6 +620,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr ""
@ -435,9 +652,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -506,14 +720,14 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr ""
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
msgid "Period"
msgstr ""

View file

@ -1,19 +1,20 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-01 21:48+0200\n"
"POT-Creation-Date: 2020-04-25 13:02+0200\n"
"PO-Revision-Date: 2020-02-09 12:01+0000\n"
"Last-Translator: Muge Niu <mugeniu12138@gmail.com>\n"
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
"i-hate-money/i-hate-money/zh_Hans/>\n"
"Language: zh_HANS-CN\n"
"Language: zh_HANS_CN\n"
"Language-Team: Chinese (Simplified) "
"<https://hosted.weblate.org/projects/i-hate-money/i-hate-money/zh_Hans/>"
"\n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 3.11-dev\n"
"Generated-By: Babel 2.7.0\n"
"Generated-By: Babel 2.8.0\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
@ -29,6 +30,18 @@ msgstr "共享密钥"
msgid "Email"
msgstr "邮箱"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr ""
msgid "Project identifier"
msgstr "账目名称"
@ -123,6 +136,15 @@ msgstr ""
msgid "The email %(email)s is not valid"
msgstr ""
msgid "Participant"
msgstr ""
msgid "Bill"
msgstr ""
msgid "Project"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
@ -156,6 +178,12 @@ msgstr ""
msgid "Password successfully reset."
msgstr ""
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
@ -167,7 +195,7 @@ msgid "Your invitations have been sent"
msgstr ""
#, python-format
msgid "%(member)s had been added"
msgid "%(member)s has been added"
msgstr ""
#, python-format
@ -221,9 +249,6 @@ msgstr ""
msgid "Create a new project"
msgstr ""
msgid "Project"
msgstr ""
msgid "Number of members"
msgstr ""
@ -245,6 +270,9 @@ msgstr ""
msgid "delete"
msgstr ""
msgid "see"
msgstr ""
msgid "The Dashboard is currently deactivated."
msgstr ""
@ -254,6 +282,12 @@ msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr ""
msgid "Download project's data"
msgstr ""
@ -275,6 +309,9 @@ msgstr ""
msgid "Cancel"
msgstr "取消"
msgid "Privacy Settings"
msgstr ""
msgid "Edit the project"
msgstr ""
@ -305,6 +342,177 @@ msgstr ""
msgid "Download"
msgstr "下载"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr ""
msgid "Removed"
msgstr ""
msgid "and"
msgstr ""
msgid "owers list"
msgstr ""
msgid "Who?"
msgstr "谁?"
msgid "Balance"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr ""
msgid "Event"
msgstr ""
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr ""
msgid "modified"
msgstr ""
msgid "removed"
msgstr ""
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
@ -352,6 +560,9 @@ msgstr ""
msgid "Statistics"
msgstr ""
msgid "History"
msgstr ""
msgid "Settings"
msgstr ""
@ -409,6 +620,12 @@ msgstr ""
msgid "Add a new bill"
msgstr ""
msgid "Newer bills"
msgstr ""
msgid "Older bills"
msgstr ""
msgid "When?"
msgstr "什么时候?"
@ -435,9 +652,6 @@ msgstr ""
msgid "each"
msgstr ""
msgid "see"
msgstr ""
msgid "No bills"
msgstr ""
@ -506,107 +720,14 @@ msgstr ""
msgid "To whom?"
msgstr ""
msgid "Who?"
msgstr "谁?"
msgid "Paid"
msgstr ""
msgid "Spent"
msgstr ""
msgid "Balance"
msgid "Expenses by Month"
msgstr ""
#~ msgid ""
#~ "The project identifier is used to "
#~ "log in and for the URL of "
#~ "the project. We tried to generate "
#~ "an identifier for you but a "
#~ "project with this identifier already "
#~ "exists. Please create a new identifier"
#~ " that you will be able to "
#~ "remember"
#~ msgstr ""
#~ msgid ""
#~ "Not a valid amount or expression.Only"
#~ " numbers and + - * / "
#~ "operatorsare accepted."
#~ msgstr ""
#~ msgid "What do you want to download ?"
#~ msgstr "你想下载什么?"
#~ msgid "bills"
#~ msgstr ""
#~ msgid "transactions"
#~ msgstr ""
#~ msgid "Export file format"
#~ msgstr ""
#~ msgid "Edit this project"
#~ msgstr ""
#~ msgid "Download this project's data"
#~ msgstr ""
#~ msgid "Type user name here"
#~ msgstr ""
#~ msgid "No, thanks"
#~ msgstr ""
#~ msgid "Manage your shared <br>expenses, easily"
#~ msgstr ""
#~ msgid "Log to an existing project"
#~ msgstr ""
#~ msgid "log in"
#~ msgstr ""
#~ msgid "or create a new one"
#~ msgstr ""
#~ msgid "let's get started"
#~ msgstr ""
#~ msgid "options"
#~ msgstr ""
#~ msgid "Project settings"
#~ msgstr ""
#~ msgid "This is a free software"
#~ msgstr ""
#~ msgid "Invite people to join this project!"
#~ msgstr ""
#~ msgid "Added on"
#~ msgstr ""
#~ msgid "Nothing to list yet. You probably want to"
#~ msgstr ""
#~ msgid ""
#~ "Specify a (comma separated) list of "
#~ "email adresses you want to notify "
#~ "about the\n"
#~ "creation of this budget management "
#~ "project and we will send them an"
#~ " email for you."
#~ msgstr ""
#~ msgid ""
#~ "If you prefer, you can share the project identifier and the shared\n"
#~ "password by other communication means. "
#~ "Or even directly share the following "
#~ "link:"
#~ msgstr ""
#~ msgid "A link to reset your password has been sent to your email."
#~ msgstr ""
msgid "Period"
msgstr ""

View file

@ -1,19 +1,18 @@
import re
import os
import ast
import operator
from io import BytesIO, StringIO
import jinja2
from json import dumps, JSONEncoder
from flask import redirect, current_app
from babel import Locale
from werkzeug.routing import HTTPException, RoutingException
from datetime import datetime, timedelta
import csv
from datetime import datetime, timedelta
from enum import Enum
from io import BytesIO, StringIO
from json import JSONEncoder, dumps
import operator
import os
import re
from babel import Locale
from flask import current_app, redirect, render_template
from flask_babel import get_locale
import jinja2
from werkzeug.routing import HTTPException, RoutingException
def slugify(value):
@ -99,7 +98,7 @@ def static_include(filename):
def locale_from_iso(iso_code):
return Locale(iso_code)
return Locale.parse(iso_code)
def list_of_dicts2json(dict_to_convert):
@ -257,3 +256,40 @@ def same_bill(bill1, bill2):
if bill1[a] != bill2[a]:
return False
return True
class FormEnum(Enum):
"""Extend builtin Enum class to be seamlessly compatible with WTForms"""
@classmethod
def choices(cls):
return [(choice, choice.name) for choice in cls]
@classmethod
def coerce(cls, item):
"""Coerce a str or int representation into an Enum object"""
if isinstance(item, cls):
return item
# If item is not already a Enum object then it must be
# a string or int corresponding to an ID (e.g. '0' or 1)
# Either int() or cls() will correctly throw a TypeError if this
# is not the case
return cls(int(item))
def __str__(self):
return str(self.value)
def render_localized_template(template_name_prefix, **context):
"""Like render_template(), but selects the right template according to the
current user language. Fallback to English if a template for the
current language does not exist.
"""
fallback = "en"
templates = [
f"{template_name_prefix}.{lang}.j2"
for lang in (get_locale().language, fallback)
]
# render_template() supports a list of templates to try in order
return render_template(templates, **context)

94
ihatemoney/versioning.py Normal file
View file

@ -0,0 +1,94 @@
from flask import g
from sqlalchemy.orm.attributes import get_history
from sqlalchemy_continuum import VersioningManager
from sqlalchemy_continuum.plugins.flask import fetch_remote_addr
from ihatemoney.utils import FormEnum
class LoggingMode(FormEnum):
"""Represents a project's history preferences."""
DISABLED = 0
ENABLED = 1
RECORD_IP = 2
@classmethod
def default(cls):
return cls.ENABLED
class ConditionalVersioningManager(VersioningManager):
"""Conditionally enable version tracking based on the given predicate."""
def __init__(self, tracking_predicate, *args, **kwargs):
"""Create version entry iff tracking_predicate() returns True."""
super().__init__(*args, **kwargs)
self.tracking_predicate = tracking_predicate
def before_flush(self, session, flush_context, instances):
if self.tracking_predicate():
return super().before_flush(session, flush_context, instances)
else:
# At least one call to unit_of_work() needs to be made against the
# session object to prevent a KeyError later. This doesn't create
# a version or transaction entry
self.unit_of_work(session)
def after_flush(self, session, flush_context):
if self.tracking_predicate():
return super().after_flush(session, flush_context)
else:
# At least one call to unit_of_work() needs to be made against the
# session object to prevent a KeyError later. This doesn't create
# a version or transaction entry
self.unit_of_work(session)
def version_privacy_predicate():
"""Evaluate if the project of the current session has enabled logging."""
logging_enabled = False
try:
if g.project.logging_preference != LoggingMode.DISABLED:
logging_enabled = True
# If logging WAS enabled prior to this transaction,
# we log this one last transaction
old_logging_mode = get_history(g.project, "logging_preference")[2]
if old_logging_mode and old_logging_mode[0] != LoggingMode.DISABLED:
logging_enabled = True
except AttributeError:
# g.project doesn't exist, it's being created or this action is outside
# the scope of a project. Use the default logging mode to decide
if LoggingMode.default() != LoggingMode.DISABLED:
logging_enabled = True
return logging_enabled
def get_ip_if_allowed():
"""
Get the remote address (IP address) of the current Flask context, if the
project's privacy settings allow it. Behind the scenes, this calls back to
the FlaskPlugin from SQLAlchemy-Continuum in order to maintain forward
compatibility
"""
ip_logging_allowed = False
try:
if g.project.logging_preference == LoggingMode.RECORD_IP:
ip_logging_allowed = True
# If ip recording WAS enabled prior to this transaction,
# we record the IP for this one last transaction
old_logging_mode = get_history(g.project, "logging_preference")[2]
if old_logging_mode and old_logging_mode[0] == LoggingMode.RECORD_IP:
ip_logging_allowed = True
except AttributeError:
# g.project doesn't exist, it's being created or this action is outside
# the scope of a project. Use the default logging mode to decide
if LoggingMode.default() == LoggingMode.RECORD_IP:
ip_logging_allowed = True
if ip_logging_allowed:
return fetch_remote_addr()
else:
return None

View file

@ -8,55 +8,59 @@ Basically, this blueprint takes care of the authentication and provides
some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview)
"""
from datetime import datetime
from functools import wraps
import json
import os
from functools import wraps
from smtplib import SMTPRecipientsRefused
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from flask import (
abort,
Blueprint,
abort,
current_app,
flash,
g,
redirect,
render_template,
request,
session,
url_for,
send_file,
send_from_directory,
session,
url_for,
)
from flask_babel import get_locale, gettext as _
from flask_babel import gettext as _
from flask_mail import Message
from sqlalchemy import orm
from sqlalchemy_continuum import Operation
from werkzeug.exceptions import NotFound
from werkzeug.security import check_password_hash, generate_password_hash
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.forms import (
AdminAuthenticationForm,
AuthenticationForm,
EditProjectForm,
InviteForm,
MemberForm,
PasswordReminder,
ResetPasswordForm,
ProjectForm,
get_billform_for,
ResetPasswordForm,
UploadForm,
get_billform_for,
get_editprojectform_for,
)
from ihatemoney.models import db, Project, Person, Bill
from ihatemoney.history import get_history, get_history_queries
from ihatemoney.models import Bill, LoggingMode, Person, Project, db
from ihatemoney.utils import (
Redirect303,
list_of_dicts2json,
list_of_dicts2csv,
LoginThrottler,
Redirect303,
get_members,
list_of_dicts2csv,
list_of_dicts2json,
render_localized_template,
same_bill,
)
from datetime import datetime
from dateutil.relativedelta import relativedelta
main = Blueprint("main", __name__)
@ -235,6 +239,8 @@ def authenticate(project_id=None):
# add the project on the top of the list
session["projects"].insert(0, (project_id, project.name))
session[project_id] = True
# Set session to permanent to make language choice persist
session.permanent = True
session.update()
setattr(g, "project", project)
return redirect(url_for(".list_bills"))
@ -297,9 +303,7 @@ def create_project():
project=g.project.name,
)
message_body = render_template(
"reminder_mail.%s.j2" % get_locale().language
)
message_body = render_localized_template("reminder_mail")
msg = Message(
message_title, body=message_body, recipients=[project.contact_email]
@ -307,19 +311,10 @@ def create_project():
try:
current_app.mail.send(msg)
except SMTPRecipientsRefused:
msg_compl = "Problem sending mail. "
# TODO: destroy the project and cancel instead?
else:
msg_compl = ""
flash(_("Error while sending reminder email"), category="danger")
# redirect the user to the next step (invite)
flash(
_(
"%(msg_compl)sThe project identifier is %(project)s",
msg_compl=msg_compl,
project=project.id,
)
)
flash(_("The project identifier is %(project)s", project=project.id))
return redirect(url_for(".list_bills", project_id=project.id))
return render_template("create_project.html", form=form)
@ -333,11 +328,12 @@ def remind_password():
# get the project
project = Project.query.get(form.id.data)
# send a link to reset the password
password_reminder = "password_reminder.%s.j2" % get_locale().language
current_app.mail.send(
Message(
"password recovery",
body=render_template(password_reminder, project=project),
body=render_localized_template(
"password_reminder", project=project
),
recipients=[project.contact_email],
)
)
@ -381,37 +377,55 @@ def reset_password():
@main.route("/<project_id>/edit", methods=["GET", "POST"])
def edit_project():
edit_form = EditProjectForm()
if request.method == "POST":
if edit_form.validate():
project = edit_form.update(g.project)
db.session.add(project)
db.session.commit()
edit_form = get_editprojectform_for(g.project)
import_form = UploadForm()
# Import form
if import_form.validate_on_submit():
try:
import_project(import_form.file.data.stream, g.project)
flash(_("Project successfully uploaded"))
return redirect(url_for(".list_bills"))
return redirect(url_for("main.list_bills"))
except ValueError:
flash(_("Invalid JSON"), category="danger")
# Edit form
if edit_form.validate_on_submit():
project = edit_form.update(g.project)
# Update converted currency
if project.default_currency != CurrencyConverter.default:
for bill in project.get_bills():
if bill.original_currency == CurrencyConverter.default:
bill.original_currency = project.default_currency
bill.converted_amount = CurrencyConverter().exchange_currency(
bill.amount, bill.original_currency, project.default_currency
)
db.session.add(bill)
db.session.add(project)
db.session.commit()
return redirect(url_for("main.list_bills"))
else:
edit_form.name.data = g.project.name
if g.project.logging_preference != LoggingMode.DISABLED:
edit_form.project_history.data = True
if g.project.logging_preference == LoggingMode.RECORD_IP:
edit_form.ip_recording.data = True
edit_form.contact_email.data = g.project.contact_email
return render_template(
"edit_project.html", edit_form=edit_form, current_view="edit_project"
"edit_project.html",
edit_form=edit_form,
import_form=import_form,
current_view="edit_project",
)
@main.route("/<project_id>/upload_json", methods=["GET", "POST"])
def upload_json():
form = UploadForm()
if form.validate_on_submit():
try:
import_project(form.file.data.stream, g.project)
flash(_("Project successfully uploaded"))
except ValueError:
flash(_("Invalid JSON"), category="error")
return redirect(url_for("main.list_bills"))
return render_template("upload_json.html", form=form)
def import_project(file, project):
json_file = json.load(file)
@ -477,6 +491,7 @@ def import_project(file, project):
form.date = parse(b["date"])
form.payer = id_dict[b["payer_name"]]
form.payed_for = owers_id
form.original_currency = b.get("original_currency")
db.session.add(form.fake_form(bill, project))
@ -510,7 +525,7 @@ def export_project(file, format):
return send_file(
file2export,
attachment_filename="%s-%s.%s" % (g.project.id, file, format),
attachment_filename=f"{g.project.id}-{file}.{format}",
as_attachment=True,
)
@ -542,6 +557,7 @@ def demo():
name="demonstration",
password=generate_password_hash("demo"),
contact_email="demo@notmyidea.org",
default_currency="EUR",
)
db.session.add(project)
db.session.commit()
@ -558,11 +574,7 @@ def invite():
if request.method == "POST":
if form.validate():
# send the email
message_body = render_template(
"invitation_mail.%s.j2" % get_locale().language
)
message_body = render_localized_template("invitation_mail")
message_title = _(
"You have been invited to share your " "expenses for %(project)s",
project=g.project.name,
@ -610,7 +622,7 @@ def add_member():
if form.validate():
member = form.save(g.project, Person())
db.session.commit()
flash(_("%(member)s had been added", member=member.name))
flash(_("%(member)s has been added", member=member.name))
return redirect(url_for(".list_bills"))
return render_template("add_member.html", form=form)
@ -740,6 +752,45 @@ def settle_bill():
return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
@main.route("/<project_id>/history")
def history():
"""Query for the version entries associated with this project."""
history = get_history(g.project, human_readable_names=True)
any_ip_addresses = any(event["ip"] for event in history)
return render_template(
"history.html",
current_view="history",
history=history,
any_ip_addresses=any_ip_addresses,
LoggingMode=LoggingMode,
OperationType=Operation,
current_log_pref=g.project.logging_preference,
)
@main.route("/<project_id>/erase_history", methods=["POST"])
def erase_history():
"""Erase all history entries associated with this project."""
for query in get_history_queries(g.project):
query.delete(synchronize_session="fetch")
db.session.commit()
return redirect(url_for(".history"))
@main.route("/<project_id>/strip_ip_addresses", methods=["POST"])
def strip_ip_addresses():
"""Strip ip addresses from history entries associated with this project."""
for query in get_history_queries(g.project):
for version_object in query.all():
version_object.transaction.remote_addr = None
db.session.commit()
return redirect(url_for(".history"))
@main.route("/<project_id>/statistics")
def statistics():
"""Compute what each member has paid and spent and display it"""

View file

@ -1,27 +0,0 @@
alembic==1.2.0
aniso8601==8.0.0
Babel==2.7.0
blinker==1.4
Click==7.0
debts==0.4
dnspython==1.16.0
email-validator==1.0.4
Flask==1.1.1
Flask-Babel==0.12.2
Flask-Cors==3.0.8
Flask-Mail==0.9.1
Flask-Migrate==2.5.2
Flask-RESTful==0.3.7
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.1
Flask-WTF==0.14.2
idna==2.8
itsdangerous==1.1.0
Jinja2==2.10.1
Mako==1.1.0
MarkupSafe==1.1.1
python-dateutil==2.8.0
pytz==2019.2
SQLAlchemy==1.3.8
Werkzeug==0.16.0
WTForms==2.2.1

View file

@ -11,9 +11,9 @@ license = Custom BSD Beerware
classifiers =
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: WSGI :: Application
@ -22,28 +22,34 @@ packages = find:
include_package_data = True
zip_safe = False
install_requires =
flask
flask-wtf
flask-sqlalchemy<3.0
flask-mail
Flask-Migrate
Flask-script
flask-babel
flask-restful
jinja2
blinker
flask-cors
itsdangerous
email_validator
debts
blinker==1.4
cachetools==4.1.0
debts==0.5
email_validator==1.0.5
Flask-Babel==1.0.0
Flask-Cors==3.0.8
Flask-Mail==0.9.1
Flask-Migrate==2.5.3
Flask-RESTful==0.3.8
Flask-Script==2.0.6
Flask-SQLAlchemy==2.4.1
Flask-WTF==0.14.3
WTForms==2.2.1
Flask==1.1.2
itsdangerous==1.1.0
Jinja2==2.11.2
requests==2.22.0
SQLAlchemy-Continuum==1.3.9
[options.extras_require]
dev =
zest.releaser
tox
pytest
flake8
Flask-Testing
black==19.10b0 ; python_version >= '3.6'
flake8==3.7.9
Flask-Testing==0.8.0
isort==4.3.21
pytest==5.4.1
tox==3.14.6
zest.releaser==6.20.1
[options.entry_points]
console_scripts =

17
tox.ini
View file

@ -1,5 +1,5 @@
[tox]
envlist = py37,py36,py35,docs,flake8,black
envlist = py38,py37,py36,docs,flake8,black
skip_missing_interpreters = True
[testenv]
@ -9,8 +9,7 @@ commands =
py.test --pyargs ihatemoney.tests.tests
deps =
-rdev-requirements.txt
-rrequirements.txt
-e.[dev]
# To be sure we are importing ihatemoney pkg from pip-installed version
changedir = /tmp
@ -22,15 +21,13 @@ deps =
changedir = {toxinidir}
[testenv:black]
commands = black --check --target-version=py34 .
deps =
-rdev-requirements.txt
commands =
black --check --target-version=py34 .
isort -c -rc .
changedir = {toxinidir}
[testenv:flake8]
commands = flake8 ihatemoney
deps =
-rdev-requirements.txt
changedir = {toxinidir}
[flake8]
@ -42,6 +39,6 @@ extend-ignore =
[travis]
python =
3.5: py35
3.6: py36, docs, black, flake8
3.6: py36
3.7: py37
3.8: py38, docs, black, flake8