Adding bill types and automatic settling between people (#1290)

* Bill types added in Bill and Project Model, Implemented in BillForm
* import and export bill feature updated with bill type, tests modified to reflect the behavior
* eliminating unnecessary bill type
* typo fixed, test cases fixed for the current bill types
* button added
* settle button added
* new changes
* test cases added
* bchen-reimbursement
* tests for different bill types
* test cases fixed
* fixed reimbursement test case
* Replaced assertEqual with assert
* Fixed missing bill_type in unit tests
* Removed commented code
* Reverted unnecessary string edit
* Changed bill_type to an Enum
* Added test checking correct bill_type validation
* Fixed  billtype displaying in all caps
* Removed 'Transfer' bill type
* Added migration rule and set default bill_type in alembic
* bill_type is now an optional parameter in the BillForm
* Use enum name instead of value as SQL server_default

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

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

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

---------

Co-authored-by: Ruitao Li <ruital@andrew.cmu.edu>
Co-authored-by: MelodyZhangYiqun <98992024+MelodyZhangYiqun@users.noreply.github.com>
Co-authored-by: Ruitao Li <49292515+FlowingCloudRTL@users.noreply.github.com>
Co-authored-by: MelodyZhangYiqun <yiqunz@andrew.cmu.edu>
Co-authored-by: Brandan Chen <bychen@andrew.cmu.edu>
Co-authored-by: Emilie Zhou <54161959+ez157@users.noreply.github.com>
Co-authored-by: Tom <tom.roussel@esat.kuleuven.be>
This commit is contained in:
TomRoussel 2024-03-16 12:20:48 +01:00 committed by GitHub
parent ba117ba0a6
commit 720f0e52dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 553 additions and 33 deletions

View file

@ -39,7 +39,7 @@ from wtforms.validators import (
) )
from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.models import Bill, LoggingMode, Person, Project from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
from ihatemoney.utils import ( from ihatemoney.utils import (
em_surround, em_surround,
eval_arithmetic_expression, eval_arithmetic_expression,
@ -364,6 +364,7 @@ class BillForm(FlaskForm):
payed_for = SelectMultipleField( payed_for = SelectMultipleField(
_("For whom?"), validators=[DataRequired()], coerce=int _("For whom?"), validators=[DataRequired()], coerce=int
) )
bill_type = SelectField(_("Bill Type"), choices=BillType.choices(), coerce=BillType, default=BillType.EXPENSE)
submit = SubmitField(_("Submit")) submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one")) submit2 = SubmitField(_("Submit and add a new one"))
@ -377,12 +378,14 @@ class BillForm(FlaskForm):
payer_id=self.payer.data, payer_id=self.payer.data,
project_default_currency=project.default_currency, project_default_currency=project.default_currency,
what=self.what.data, what=self.what.data,
bill_type=self.bill_type.data,
) )
def save(self, bill, project): def save(self, bill, project):
bill.payer_id = self.payer.data bill.payer_id = self.payer.data
bill.amount = self.amount.data bill.amount = self.amount.data
bill.what = self.what.data bill.what = self.what.data
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data bill.external_link = self.external_link.data
bill.date = self.date.data bill.date = self.date.data
bill.owers = Person.query.get_by_ids(self.payed_for.data, project) bill.owers = Person.query.get_by_ids(self.payed_for.data, project)
@ -396,6 +399,7 @@ class BillForm(FlaskForm):
self.payer.data = bill.payer_id self.payer.data = bill.payer_id
self.amount.data = bill.amount self.amount.data = bill.amount
self.what.data = bill.what self.what.data = bill.what
self.bill_type.data = bill.bill_type
self.external_link.data = bill.external_link self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency self.original_currency.data = bill.original_currency
self.date.data = bill.date self.date.data = bill.date

View file

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

View file

@ -1,4 +1,5 @@
from collections import defaultdict from collections import defaultdict
from enum import Enum
import datetime import datetime
import itertools import itertools
@ -21,7 +22,7 @@ from sqlalchemy_continuum.plugins import FlaskPlugin
from ihatemoney.currency_convertor import CurrencyConverter from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.monkeypath_continuum import PatchedTransactionFactory from ihatemoney.monkeypath_continuum import PatchedTransactionFactory
from ihatemoney.utils import generate_password_hash, get_members, same_bill from ihatemoney.utils import generate_password_hash, get_members, same_bill, FormEnum
from ihatemoney.versioning import ( from ihatemoney.versioning import (
ConditionalVersioningManager, ConditionalVersioningManager,
LoggingMode, LoggingMode,
@ -50,6 +51,15 @@ make_versioned(
], ],
) )
class BillType(Enum):
EXPENSE = "Expense"
REIMBURSEMENT = "Reimbursement"
@classmethod
def choices(cls):
return [(choice, choice.value) for choice in cls]
db = SQLAlchemy() db = SQLAlchemy()
@ -112,22 +122,31 @@ class Project(db.Model):
- dict mapping each member to how much he/she should be paid by - dict mapping each member to how much he/she should be paid by
others (i.e. how much he/she has paid for bills) others (i.e. how much he/she has paid for bills)
balance spent paid
""" """
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3)) balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
for bill in self.get_bills_unordered().all(): for bill in self.get_bills_unordered().all():
should_receive[bill.payer.id] += bill.converted_amount
total_weight = sum(ower.weight for ower in bill.owers) total_weight = sum(ower.weight for ower in bill.owers)
for ower in bill.owers:
should_pay[ower.id] += ( if bill.bill_type == BillType.EXPENSE:
ower.weight * bill.converted_amount / total_weight should_receive[bill.payer.id] += bill.converted_amount
) for ower in bill.owers:
should_pay[ower.id] += (ower.weight * bill.converted_amount / total_weight)
if bill.bill_type == BillType.REIMBURSEMENT:
should_receive[bill.payer.id] += bill.converted_amount
for ower in bill.owers:
should_receive[ower.id] -= bill.converted_amount
for person in self.members: for person in self.members:
balance = should_receive[person.id] - should_pay[person.id] balance = should_receive[person.id] - should_pay[person.id]
balances[person.id] = balance balances[person.id] = balance
return balances, should_pay, should_receive return (
balances,
should_pay,
should_receive,
)
@property @property
def balance(self): def balance(self):
@ -160,7 +179,8 @@ class Project(db.Model):
""" """
monthly = defaultdict(lambda: defaultdict(float)) monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all(): for bill in self.get_bills_unordered().all():
monthly[bill.date.year][bill.date.month] += bill.converted_amount if bill.bill_type == BillType.EXPENSE:
monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly return monthly
@property @property
@ -336,6 +356,7 @@ class Project(db.Model):
pretty_bills.append( pretty_bills.append(
{ {
"what": bill.what, "what": bill.what,
"bill_type": bill.bill_type.value,
"amount": round(bill.amount, 2), "amount": round(bill.amount, 2),
"currency": bill.original_currency, "currency": bill.original_currency,
"date": str(bill.date), "date": str(bill.date),
@ -407,6 +428,7 @@ class Project(db.Model):
new_bill = Bill( new_bill = Bill(
amount=b["amount"], amount=b["amount"],
date=parse(b["date"]), date=parse(b["date"]),
bill_type=b["bill_type"],
external_link="", external_link="",
original_currency=b["currency"], original_currency=b["currency"],
owers=Person.query.get_by_names(b["owers"], self), owers=Person.query.get_by_names(b["owers"], self),
@ -537,14 +559,15 @@ class Project(db.Model):
db.session.commit() db.session.commit()
operations = ( operations = (
("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping"), ("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping", "Expense"),
("Alice", 20, ("Amina", "Alice"), "Beer !"), ("Alice", 20, ("Amina", "Alice"), "Beer !", "Expense"),
("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP"), ("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP", "Expense"),
) )
for payer, amount, owers, what in operations: for (payer, amount, owers, what, bill_type) in operations:
db.session.add( db.session.add(
Bill( Bill(
amount=amount, amount=amount,
bill_type=bill_type,
original_currency=project.default_currency, original_currency=project.default_currency,
owers=[members[name] for name in owers], owers=[members[name] for name in owers],
payer_id=members[payer].id, payer_id=members[payer].id,
@ -677,6 +700,7 @@ class Bill(db.Model):
date = db.Column(db.Date, default=datetime.datetime.now) date = db.Column(db.Date, default=datetime.datetime.now)
creation_date = db.Column(db.Date, default=datetime.datetime.now) creation_date = db.Column(db.Date, default=datetime.datetime.now)
what = db.Column(db.UnicodeText) what = db.Column(db.UnicodeText)
bill_type = db.Column(db.Enum(BillType))
external_link = db.Column(db.UnicodeText) external_link = db.Column(db.UnicodeText)
original_currency = db.Column(db.String(3)) original_currency = db.Column(db.String(3))
@ -696,6 +720,7 @@ class Bill(db.Model):
payer_id: int = None, payer_id: int = None,
project_default_currency: str = "", project_default_currency: str = "",
what: str = "", what: str = "",
bill_type: str = "Expense",
): ):
super().__init__() super().__init__()
self.amount = amount self.amount = amount
@ -705,6 +730,7 @@ class Bill(db.Model):
self.owers = owers self.owers = owers
self.payer_id = payer_id self.payer_id = payer_id
self.what = what self.what = what
self.bill_type = BillType(bill_type)
self.converted_amount = self.currency_helper.exchange_currency( self.converted_amount = self.currency_helper.exchange_currency(
self.amount, self.original_currency, project_default_currency self.amount, self.original_currency, project_default_currency
) )
@ -719,6 +745,7 @@ class Bill(db.Model):
"date": self.date, "date": self.date,
"creation_date": self.creation_date, "creation_date": self.creation_date,
"what": self.what, "what": self.what,
"bill_type": self.bill_type.value,
"external_link": self.external_link, "external_link": self.external_link,
"original_currency": self.original_currency, "original_currency": self.original_currency,
"converted_amount": self.converted_amount, "converted_amount": self.converted_amount,

View file

@ -165,6 +165,7 @@
{% include "display_errors.html" %} {% include "display_errors.html" %}
{{ form.hidden_tag() }} {{ form.hidden_tag() }}
{{ input(form.date, inline=True) }} {{ input(form.date, inline=True) }}
{{ input(form.bill_type, inline=True) }}
{{ input(form.what, inline=True) }} {{ input(form.what, inline=True) }}
{{ input(form.payer, inline=True, class="form-control custom-select") }} {{ input(form.payer, inline=True, class="form-control custom-select") }}
<div data-toggle="tooltip" data-placement="top" title='{{ _("Simple operations are allowed, e.g. (18+36.2)/3") }}'> <div data-toggle="tooltip" data-placement="top" title='{{ _("Simple operations are allowed, e.g. (18+36.2)/3") }}'>

View file

@ -9,13 +9,20 @@
{% block content %} {% block content %}
<table id="bill_table" class="split_bills table table-striped"> <table id="bill_table" class="split_bills table table-striped">
<thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th></tr></thead> <thead><tr><th>{{ _("Who pays?") }}</th><th>{{ _("To whom?") }}</th><th>{{ _("How much?") }}</th><th>{{ _("Settled?") }}</th></tr></thead>
<tbody> <tbody>
{% for bill in bills %} {% for bill in bills %}
<tr receiver={{bill.receiver.id}}> <tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td> <td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</td> <td>{{ bill.receiver }}</td>
<td>{{ bill.amount|currency }}</td> <td>{{ bill.amount|currency }}</td>
<td>
<span id="settle-bill" class="ml-auto pb-2">
<a href="{{ url_for('.settle', amount = bill.amount, ower_id = bill.ower.id, payer_id = bill.receiver.id) }}" class="btn btn-primary">
{{ ("Settle") }}
</a>
</span>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View file

@ -407,6 +407,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -431,6 +432,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1}, {"activated": True, "id": 2, "name": "jeanne", "weight": 1},
], ],
"bill_type": "Expense",
"amount": 25.0, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -462,6 +464,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -479,6 +482,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "beer", "what": "beer",
"payer": "2", "payer": "2",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -500,6 +504,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1}, {"activated": True, "id": 2, "name": "jeanne", "weight": 1},
], ],
"bill_type": "Expense",
"amount": 25.0, "amount": 25.0,
"date": "2011-09-10", "date": "2011-09-10",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
@ -554,6 +559,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": input_amount, "amount": input_amount,
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -578,6 +584,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1}, {"activated": True, "id": 2, "name": "jeanne", "weight": 1},
], ],
"bill_type": "Expense",
"amount": expected_amount, "amount": expected_amount,
"date": "2011-08-10", "date": "2011-08-10",
"id": id, "id": id,
@ -611,6 +618,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": amount, "amount": amount,
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -658,6 +666,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
}, },
@ -682,6 +691,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1}, {"activated": True, "id": 2, "name": "jeanne", "weight": 1},
], ],
"bill_type": "Expense",
"amount": 25.0, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -706,6 +716,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "30", "amount": "30",
"external_link": "https://raclette.fr", "external_link": "https://raclette.fr",
"original_currency": "CAD", "original_currency": "CAD",
@ -727,6 +738,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1.0}, {"activated": True, "id": 2, "name": "jeanne", "weight": 1.0},
], ],
"bill_type": "Expense",
"amount": 30.0, "amount": 30.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -747,6 +759,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "Pierogi", "what": "Pierogi",
"payer": "1", "payer": "1",
"payed_for": ["2", "3"], "payed_for": ["2", "3"],
"bill_type": "Expense",
"amount": "80", "amount": "80",
"original_currency": "PLN", "original_currency": "PLN",
}, },
@ -791,6 +804,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -855,6 +869,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1", "2"], "payed_for": ["1", "2"],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -877,6 +892,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1}, {"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeannedy familly", "weight": 4}, {"activated": True, "id": 2, "name": "jeannedy familly", "weight": 4},
], ],
"bill_type": "Expense",
"amount": 25.0, "amount": 25.0,
"date": "2011-08-10", "date": "2011-08-10",
"id": 1, "id": 1,
@ -962,6 +978,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1"], "payed_for": ["1"],
"bill_type": "Expense",
"amount": "0", "amount": "0",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
@ -990,8 +1007,73 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage", "what": "fromage",
"payer": "1", "payer": "1",
"payed_for": ["1"], "payed_for": ["1"],
"bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12", "amount": "9347242149381274732472348728748723473278472843.12",
}, },
headers=self.get_auth("raclette"), headers=self.get_auth("raclette"),
) )
self.assertStatus(400, req) self.assertStatus(400, req)
def test_validate_bill_type(self):
self.api_create("raclette")
self.api_add_member("raclette", "zorglub")
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "wrong_bill_type",
"amount": "50",
},
headers=self.get_auth("raclette")
)
self.assertStatus(400, req)
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"bill_type": "Expense",
"amount": "50",
},
headers=self.get_auth("raclette")
)
self.assertStatus(201, req)
def test_default_bill_type(self):
self.api_create("raclette")
self.api_add_member("raclette", "zorglub")
# Post a bill without adding a bill type
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
"amount": "50",
},
headers=self.get_auth("raclette")
)
self.assertStatus(201, req)
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
# Bill type should now be "Expense"
got = json.loads(req.data.decode("utf-8"))
assert got["bill_type"] == "Expense"

View file

@ -428,6 +428,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": jeanne_id, "payer": jeanne_id,
"payed_for": [jeanne_id], "payed_for": [jeanne_id],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
) )
@ -479,6 +480,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": zorglub.id, "payer": zorglub.id,
"payed_for": [zorglub.id], "payed_for": [zorglub.id],
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
) )
@ -646,6 +648,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "25", "amount": "25",
}, },
) )
@ -661,6 +664,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -684,6 +688,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "19", "amount": "19",
}, },
) )
@ -695,6 +700,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[1], "payer": members_ids[1],
"payed_for": members_ids[0], "payed_for": members_ids[0],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -706,10 +712,23 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[1], "payer": members_ids[1],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "17", "amount": "17",
}, },
) )
#transfer bill should not affect balances at all
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "Transfer",
"payer": members_ids[1],
"payed_for": members_ids[0],
"bill_type": "Transfer",
"amount": "500",
},
)
balance = self.get_project("raclette").balance balance = self.get_project("raclette").balance
assert set(balance.values()) == set([19.0, -19.0]) assert set(balance.values()) == set([19.0, -19.0])
@ -721,6 +740,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "-25", "amount": "-25",
}, },
) )
@ -735,6 +755,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "25,02", "amount": "25,02",
}, },
) )
@ -749,6 +770,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "42", "amount": "42",
"external_link": "https://example.com/fromage", "external_link": "https://example.com/fromage",
}, },
@ -764,12 +786,179 @@ class TestBudget(IhatemoneyTestCase):
"what": "mauvais fromage à raclette", "what": "mauvais fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "42000", "amount": "42000",
"external_link": "javascript:alert('Tu bluffes, Martoni.')", "external_link": "javascript:alert('Tu bluffes, Martoni.')",
}, },
) )
assert "Invalid URL" in resp.data.decode("utf-8") assert "Invalid URL" in resp.data.decode("utf-8")
def test_reimbursement_bill(self):
self.post_project("rent")
# add two participants
self.client.post("/rent/members/add", data={"name": "bob"})
self.client.post("/rent/members/add", data={"name": "alice"})
members_ids = [m.id for m in self.get_project("rent").members]
# create a bill to test reimbursement
self.client.post(
"/rent/add",
data={
"date": "2022-12-12",
"what": "december rent",
"payer": members_ids[0], #bob
"payed_for": members_ids, #bob and alice
"bill_type": "Expense",
"amount": "1000",
},
)
#check balance
balance = self.get_project("rent").balance
assert set(balance.values()), set([500 == -500])
#check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 1000
assert alice_paid == 0
# test reimbursement bill
self.client.post(
"/rent/add",
data={
"date": "2022-12-13",
"what": "reimbursement for rent",
"payer": members_ids[1], #alice
"payed_for": members_ids[0], #bob
"bill_type": "Reimbursement",
"amount": "500",
},
)
balance = self.get_project("rent").balance
assert set(balance.values()), set([0 == 0])
#check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 500
assert alice_paid == 500
def test_transfer_bill(self):
self.post_project("random")
# add two participants
self.client.post("/random/members/add", data={"name": "zorglub"})
self.client.post("/random/members/add", data={"name": "fred"})
members_ids = [m.id for m in self.get_project("random").members]
self.client.post(
"/random/add",
data={
"date": "2022-10-10",
"what": "Rent",
"payer": members_ids[0], #zorglub
"payed_for": members_ids, #zorglub + fred
"bill_type": "Expense",
"amount": "1000",
},
)
# test transfer bill (should not affect anything whatsoever)
self.client.post(
"/random/add",
data={
"date": "2022-10-10",
"what": "Transfer of 500 to fred",
"payer": members_ids[0], #zorglub
"payed_for": members_ids[1], #fred
"bill_type": "Transfer",
"amount": "500",
},
)
balance = self.get_project("random").balance
assert set(balance.values()), set([500 == -500])
def test_reimbursement_bill(self):
self.post_project("rent")
# add two participants
self.client.post("/rent/members/add", data={"name": "bob"})
self.client.post("/rent/members/add", data={"name": "alice"})
members_ids = [m.id for m in self.get_project("rent").members]
# create a bill to test reimbursement
self.client.post(
"/rent/add",
data={
"date": "2022-12-12",
"what": "december rent",
"payer": members_ids[0], #bob
"payed_for": members_ids, #bob and alice
"bill_type": "Expense",
"amount": "1000",
},
)
#check balance
balance = self.get_project("rent").balance
assert set(balance.values()), set([500 == -500])
#check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 1000
assert alice_paid == 0
# test reimbursement bill
self.client.post(
"/rent/add",
data={
"date": "2022-12-13",
"what": "reimbursement for rent",
"payer": members_ids[1], #alice
"payed_for": members_ids[0], #bob
"bill_type": "Reimbursement",
"amount": "500",
},
)
balance = self.get_project("rent").balance
assert set(balance.values()), set([0 == 0])
#check paid
bob_paid = self.get_project("rent").full_balance[2][members_ids[0]]
alice_paid = self.get_project("rent").full_balance[2][members_ids[1]]
assert bob_paid == 500
assert alice_paid == 500
def test_transfer_bill(self):
self.post_project("random")
# add two participants
self.client.post("/random/members/add", data={"name": "zorglub"})
self.client.post("/random/members/add", data={"name": "fred"})
members_ids = [m.id for m in self.get_project("random").members]
self.client.post(
"/random/add",
data={
"date": "2022-10-10",
"what": "Rent",
"payer": members_ids[0], #zorglub
"payed_for": members_ids, #zorglub + fred
"bill_type": "Expense",
"amount": "1000",
},
)
# test transfer bill (should not affect anything whatsoever)
self.client.post(
"/random/add",
data={
"date": "2022-10-10",
"what": "Transfer of 500 to fred",
"payer": members_ids[0], #zorglub
"payed_for": members_ids[1], #fred
"bill_type": "Transfer",
"amount": "500",
},
)
balance = self.get_project("random").balance
assert set(balance.values()), set([500 == -500])
def test_weighted_balance(self): def test_weighted_balance(self):
self.post_project("raclette") self.post_project("raclette")
@ -789,6 +978,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": members_ids[0], "payer": members_ids[0],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -800,6 +990,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "pommes de terre", "what": "pommes de terre",
"payer": members_ids[1], "payer": members_ids[1],
"payed_for": members_ids, "payed_for": members_ids,
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -864,6 +1055,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "24.36", "amount": "24.36",
}, },
) )
@ -875,6 +1067,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "19.12", "amount": "19.12",
}, },
) )
@ -886,6 +1079,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen", "what": "delicatessen",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "22", "amount": "22",
}, },
) )
@ -1019,6 +1213,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -1030,6 +1225,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -1041,6 +1237,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen", "what": "delicatessen",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -1089,6 +1286,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 2, "payer": 2,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "30", "amount": "30",
}, },
) )
@ -1114,6 +1312,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "ice cream", "what": "ice cream",
"payer": 2, "payer": 2,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -1129,6 +1328,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "champomy", "what": "champomy",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -1144,6 +1344,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "smoothie", "what": "smoothie",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -1160,6 +1361,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "more champomy", "what": "more champomy",
"payer": 2, "payer": 2,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "30", "amount": "30",
}, },
) )
@ -1192,6 +1394,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -1203,6 +1406,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -1214,6 +1418,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen", "what": "delicatessen",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -1228,6 +1433,68 @@ class TestBudget(IhatemoneyTestCase):
for m, a in members.items(): for m, a in members.items():
assert abs(a - balance[m.id]) < 0.01 assert abs(a - balance[m.id]) < 0.01
return return
def test_settle_button(self):
self.post_project("raclette")
# add participants
self.client.post("/raclette/members/add", data={"name": "zorglub"})
self.client.post("/raclette/members/add", data={"name": "jeanne"})
self.client.post("/raclette/members/add", data={"name": "tata"})
# Add a participant with a balance at 0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})
# create bills
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "red wine",
"payer": 2,
"payed_for": [1],
"bill_type": "Expense",
"amount": "20",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10",
},
)
project = self.get_project("raclette")
transactions = project.get_transactions_to_settle_bill()
count = 0
for t in transactions:
count+=1
self.client.get("/raclette/settle"+"/"+str(t["amount"])+"/"+str(t["ower"].id)+"/"+str(t["receiver"].id))
temp_transactions = project.get_transactions_to_settle_bill()
#test if the one has disappeared
assert len(temp_transactions) == len(transactions)-count
#test if theres a new one with bill_type reimbursement
bill = project.get_newest_bill()
assert bill.bill_type == models.BillType.REIMBURSEMENT
return
def test_settle_zero(self): def test_settle_zero(self):
self.post_project("raclette") self.post_project("raclette")
@ -1245,6 +1512,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -1256,6 +1524,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1, 3], "payed_for": [1, 3],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -1267,6 +1536,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund", "what": "refund",
"payer": 3, "payer": 3,
"payed_for": [2], "payed_for": [2],
"bill_type": "Expense",
"amount": "13.33", "amount": "13.33",
}, },
) )
@ -1299,6 +1569,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3, 4], "payed_for": [1, 2, 3, 4],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -1317,6 +1588,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "roblochon", "what": "roblochon",
"payer": 2, "payer": 2,
"payed_for": [1, 3, 4], "payed_for": [1, 3, 4],
"bill_type": "Expense",
"amount": "100.0", "amount": "100.0",
} }
# Try to access bill of another project # Try to access bill of another project
@ -1422,6 +1694,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -1433,6 +1706,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1, 3], "payed_for": [1, 3],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -1444,6 +1718,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund", "what": "refund",
"payer": 3, "payer": 3,
"payed_for": [2], "payed_for": [2],
"bill_type": "Expense",
"amount": "13.33", "amount": "13.33",
}, },
) )
@ -1469,6 +1744,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund from EUR", "what": "refund from EUR",
"payer": 3, "payer": 3,
"payed_for": [2], "payed_for": [2],
"bill_type": "Expense",
"amount": "20", "amount": "20",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -1492,6 +1768,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "Poutine", "what": "Poutine",
"payer": 3, "payer": 3,
"payed_for": [2], "payed_for": [2],
"bill_type": "Expense",
"amount": "18", "amount": "18",
"original_currency": "CAD", "original_currency": "CAD",
}, },
@ -1548,6 +1825,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -1583,6 +1861,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -1595,6 +1874,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "aspirine", "what": "aspirine",
"payer": 2, "payer": 2,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "5.0", "amount": "5.0",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -1629,6 +1909,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "0", "amount": "0",
"original_currency": "XXX", "original_currency": "XXX",
}, },
@ -1673,6 +1954,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12", "amount": "9347242149381274732472348728748723473278472843.12",
"original_currency": "EUR", "original_currency": "EUR",
}, },
@ -1723,6 +2005,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"amount": "12", "amount": "12",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
self.client.post( self.client.post(
@ -1734,6 +2017,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2], "payed_for": [1, 2],
"amount": "15", "amount": "15",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
self.client.post( self.client.post(
@ -1745,6 +2029,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2], "payed_for": [1, 2],
"amount": "10", "amount": "10",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
@ -1807,6 +2092,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"amount": "12", "amount": "12",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
self.client.post( self.client.post(
@ -1818,6 +2104,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2], "payed_for": [1, 2],
"amount": "15", "amount": "15",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
self.client.post( self.client.post(
@ -1829,6 +2116,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2], "payed_for": [1, 2],
"amount": "10", "amount": "10",
"original_currency": "EUR", "original_currency": "EUR",
"bill_type": "Expense"
}, },
) )
@ -1907,6 +2195,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1], "payed_for": [1],
"amount": "12", "amount": "12",
"original_currency": "XXX", "original_currency": "XXX",
"bill_type": "Expense"
}, },
follow_redirects=True, follow_redirects=True,
) )
@ -1967,6 +2256,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1, "payer": 1,
"payed_for": [1], "payed_for": [1],
"amount": "12", "amount": "12",
"bill_type": "Expense",
"original_currency": "XXX", "original_currency": "XXX",
}, },
follow_redirects=True, follow_redirects=True,
@ -2074,6 +2364,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1], "payer": members_ids[1],
"payed_for": members_ids, "payed_for": members_ids,
"amount": "25", "amount": "25",
"bill_type": "Expense"
}, },
) )
@ -2091,6 +2382,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids_tartif[2], "payer": members_ids_tartif[2],
"payed_for": members_ids_tartif, "payed_for": members_ids_tartif,
"amount": "24", "amount": "24",
"bill_type": "Expense"
}, },
) )
@ -2125,6 +2417,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1], "payer": members_ids[1],
"payed_for": members_ids[1:], "payed_for": members_ids[1:],
"amount": "25", "amount": "25",
"bill_type": "Expense"
}, },
) )

View file

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

View file

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

View file

@ -126,6 +126,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -137,6 +138,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -148,6 +150,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen", "what": "delicatessen",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )
@ -181,6 +184,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette", "what": "fromage à raclette",
"payer": 1, "payer": 1,
"payed_for": [1, 2, 3], "payed_for": [1, 2, 3],
"bill_type": "Expense",
"amount": "10.0", "amount": "10.0",
}, },
) )
@ -192,6 +196,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine", "what": "red wine",
"payer": 2, "payer": 2,
"payed_for": [1], "payed_for": [1],
"bill_type": "Expense",
"amount": "20", "amount": "20",
}, },
) )
@ -203,6 +208,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen", "what": "delicatessen",
"payer": 1, "payer": 1,
"payed_for": [1, 2], "payed_for": [1, 2],
"bill_type": "Expense",
"amount": "10", "amount": "10",
}, },
) )

View file

@ -221,6 +221,7 @@ def csv2list_of_dicts(csv_to_convert):
r["amount"] = float(r["amount"]) r["amount"] = float(r["amount"])
r["payer_weight"] = float(r["payer_weight"]) r["payer_weight"] = float(r["payer_weight"])
r["owers"] = [o.strip() for o in r["owers"].split(",")] r["owers"] = [o.strip() for o in r["owers"].split(",")]
r["bill_type"] = str(r["bill_type"])
result.append(r) result.append(r)
return result return result
@ -304,7 +305,16 @@ def get_members(file):
def same_bill(bill1, bill2): def same_bill(bill1, bill2):
attr = ["what", "payer_name", "payer_weight", "amount", "currency", "date", "owers"] attr = [
"what",
"bill_type",
"payer_name",
"payer_weight",
"amount",
"currency",
"date",
"owers",
]
for a in attr: for a in attr:
if bill1[a] != bill2[a]: if bill1[a] != bill2[a]:
return False return False

View file

@ -8,6 +8,7 @@ Basically, this blueprint takes care of the authentication and provides
some shortcuts to make your life better when coding (see `pull_project` some shortcuts to make your life better when coding (see `pull_project`
and `add_project_id` for a quick overview) and `add_project_id` for a quick overview)
""" """
import datetime
from functools import wraps from functools import wraps
import hashlib import hashlib
import json import json
@ -57,7 +58,7 @@ from ihatemoney.forms import (
get_billform_for, get_billform_for,
) )
from ihatemoney.history import get_history, get_history_queries, purge_history from ihatemoney.history import get_history, get_history_queries, purge_history
from ihatemoney.models import Bill, LoggingMode, Person, Project, db from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project, db
from ihatemoney.utils import ( from ihatemoney.utils import (
Redirect303, Redirect303,
csv2list_of_dicts, csv2list_of_dicts,
@ -471,6 +472,7 @@ def import_project():
# Check data # Check data
attr = [ attr = [
"amount", "amount",
"bill_type",
"currency", "currency",
"date", "date",
"owers", "owers",
@ -848,6 +850,27 @@ def settle_bill():
return render_template("settle_bills.html", bills=bills, current_view="settle_bill") return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
@main.route("/<project_id>/settle/<amount>/<int:ower_id>/<int:payer_id>")
def settle(amount, ower_id, payer_id):
# FIXME: Test this bill belongs to this project !
new_reinbursement = Bill(
amount=float(amount),
date=datetime.datetime.today(),
owers=[Person.query.get(payer_id)],
payer_id=ower_id,
project_default_currency=g.project.default_currency,
bill_type=BillType.REIMBURSEMENT,
what=_("Settlement"),
)
session.update()
db.session.add(new_reinbursement)
db.session.commit()
return redirect(url_for(".settle_bill"))
@main.route("/<project_id>/history") @main.route("/<project_id>/history")
def history(): def history():
"""Query for the version entries associated with this project.""" """Query for the version entries associated with this project."""