diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py
index 7585a521..6a787e21 100644
--- a/ihatemoney/forms.py
+++ b/ihatemoney/forms.py
@@ -39,7 +39,7 @@ from wtforms.validators import (
)
from ihatemoney.currency_convertor import CurrencyConverter
-from ihatemoney.models import Bill, LoggingMode, Person, Project
+from ihatemoney.models import Bill, BillType, LoggingMode, Person, Project
from ihatemoney.utils import (
em_surround,
eval_arithmetic_expression,
@@ -364,6 +364,7 @@ class BillForm(FlaskForm):
payed_for = SelectMultipleField(
_("For whom?"), validators=[DataRequired()], coerce=int
)
+ bill_type = SelectField(_("Bill Type"), choices=BillType.choices(), coerce=BillType, default=BillType.EXPENSE)
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))
@@ -377,12 +378,14 @@ class BillForm(FlaskForm):
payer_id=self.payer.data,
project_default_currency=project.default_currency,
what=self.what.data,
+ bill_type=self.bill_type.data,
)
def save(self, bill, project):
bill.payer_id = self.payer.data
bill.amount = self.amount.data
bill.what = self.what.data
+ bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data
bill.date = self.date.data
bill.owers = Person.query.get_by_ids(self.payed_for.data, project)
@@ -396,6 +399,7 @@ class BillForm(FlaskForm):
self.payer.data = bill.payer_id
self.amount.data = bill.amount
self.what.data = bill.what
+ self.bill_type.data = bill.bill_type
self.external_link.data = bill.external_link
self.original_currency.data = bill.original_currency
self.date.data = bill.date
diff --git a/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py b/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py
new file mode 100644
index 00000000..9b1fc520
--- /dev/null
+++ b/ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py
@@ -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())
\ No newline at end of file
diff --git a/ihatemoney/models.py b/ihatemoney/models.py
index c1aeaa2a..fcc17264 100644
--- a/ihatemoney/models.py
+++ b/ihatemoney/models.py
@@ -1,4 +1,5 @@
from collections import defaultdict
+from enum import Enum
import datetime
import itertools
@@ -21,7 +22,7 @@ from sqlalchemy_continuum.plugins import FlaskPlugin
from ihatemoney.currency_convertor import CurrencyConverter
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 (
ConditionalVersioningManager,
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()
@@ -112,22 +122,31 @@ class Project(db.Model):
- dict mapping each member to how much he/she should be paid by
others (i.e. how much he/she has paid for bills)
+ balance spent paid
"""
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
-
for bill in self.get_bills_unordered().all():
- should_receive[bill.payer.id] += bill.converted_amount
total_weight = sum(ower.weight for ower in bill.owers)
- for ower in bill.owers:
- should_pay[ower.id] += (
- ower.weight * bill.converted_amount / total_weight
- )
+
+ if bill.bill_type == BillType.EXPENSE:
+ should_receive[bill.payer.id] += bill.converted_amount
+ for ower in bill.owers:
+ should_pay[ower.id] += (ower.weight * bill.converted_amount / total_weight)
+
+ if bill.bill_type == BillType.REIMBURSEMENT:
+ should_receive[bill.payer.id] += bill.converted_amount
+ for ower in bill.owers:
+ should_receive[ower.id] -= bill.converted_amount
for person in self.members:
balance = should_receive[person.id] - should_pay[person.id]
balances[person.id] = balance
- return balances, should_pay, should_receive
+ return (
+ balances,
+ should_pay,
+ should_receive,
+ )
@property
def balance(self):
@@ -160,7 +179,8 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all():
- monthly[bill.date.year][bill.date.month] += bill.converted_amount
+ if bill.bill_type == BillType.EXPENSE:
+ monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@property
@@ -336,6 +356,7 @@ class Project(db.Model):
pretty_bills.append(
{
"what": bill.what,
+ "bill_type": bill.bill_type.value,
"amount": round(bill.amount, 2),
"currency": bill.original_currency,
"date": str(bill.date),
@@ -407,6 +428,7 @@ class Project(db.Model):
new_bill = Bill(
amount=b["amount"],
date=parse(b["date"]),
+ bill_type=b["bill_type"],
external_link="",
original_currency=b["currency"],
owers=Person.query.get_by_names(b["owers"], self),
@@ -537,14 +559,15 @@ class Project(db.Model):
db.session.commit()
operations = (
- ("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping"),
- ("Alice", 20, ("Amina", "Alice"), "Beer !"),
- ("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP"),
+ ("Georg", 200, ("Amina", "Georg", "Alice"), "Food shopping", "Expense"),
+ ("Alice", 20, ("Amina", "Alice"), "Beer !", "Expense"),
+ ("Amina", 50, ("Amina", "Alice", "Georg"), "AMAP", "Expense"),
)
- for payer, amount, owers, what in operations:
+ for (payer, amount, owers, what, bill_type) in operations:
db.session.add(
Bill(
amount=amount,
+ bill_type=bill_type,
original_currency=project.default_currency,
owers=[members[name] for name in owers],
payer_id=members[payer].id,
@@ -677,6 +700,7 @@ class Bill(db.Model):
date = db.Column(db.Date, default=datetime.datetime.now)
creation_date = db.Column(db.Date, default=datetime.datetime.now)
what = db.Column(db.UnicodeText)
+ bill_type = db.Column(db.Enum(BillType))
external_link = db.Column(db.UnicodeText)
original_currency = db.Column(db.String(3))
@@ -696,6 +720,7 @@ class Bill(db.Model):
payer_id: int = None,
project_default_currency: str = "",
what: str = "",
+ bill_type: str = "Expense",
):
super().__init__()
self.amount = amount
@@ -705,6 +730,7 @@ class Bill(db.Model):
self.owers = owers
self.payer_id = payer_id
self.what = what
+ self.bill_type = BillType(bill_type)
self.converted_amount = self.currency_helper.exchange_currency(
self.amount, self.original_currency, project_default_currency
)
@@ -719,6 +745,7 @@ class Bill(db.Model):
"date": self.date,
"creation_date": self.creation_date,
"what": self.what,
+ "bill_type": self.bill_type.value,
"external_link": self.external_link,
"original_currency": self.original_currency,
"converted_amount": self.converted_amount,
diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html
index cb586807..9b299d65 100644
--- a/ihatemoney/templates/forms.html
+++ b/ihatemoney/templates/forms.html
@@ -165,6 +165,7 @@
{% include "display_errors.html" %}
{{ form.hidden_tag() }}
{{ input(form.date, inline=True) }}
+ {{ input(form.bill_type, inline=True) }}
{{ input(form.what, inline=True) }}
{{ input(form.payer, inline=True, class="form-control custom-select") }}
diff --git a/ihatemoney/templates/settle_bills.html b/ihatemoney/templates/settle_bills.html
index 601156c6..d23f9c06 100644
--- a/ihatemoney/templates/settle_bills.html
+++ b/ihatemoney/templates/settle_bills.html
@@ -9,13 +9,20 @@
{% block content %}
- {{ _("Who pays?") }} | {{ _("To whom?") }} | {{ _("How much?") }} |
+ {{ _("Who pays?") }} | {{ _("To whom?") }} | {{ _("How much?") }} | {{ _("Settled?") }} |
{% for bill in bills %}
{{ bill.ower }} |
{{ bill.receiver }} |
{{ bill.amount|currency }} |
+
+
+
+ {{ ("Settle") }}
+
+
+ |
{% endfor %}
diff --git a/ihatemoney/tests/api_test.py b/ihatemoney/tests/api_test.py
index 260d6d45..ff94ebe7 100644
--- a/ihatemoney/tests/api_test.py
+++ b/ihatemoney/tests/api_test.py
@@ -407,6 +407,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@@ -431,6 +432,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
+ "bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@@ -462,6 +464,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@@ -479,6 +482,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "beer",
"payer": "2",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@@ -500,6 +504,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
+ "bill_type": "Expense",
"amount": 25.0,
"date": "2011-09-10",
"external_link": "https://raclette.fr",
@@ -554,6 +559,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": input_amount,
},
headers=self.get_auth("raclette"),
@@ -578,6 +584,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
+ "bill_type": "Expense",
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
@@ -611,6 +618,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": amount,
},
headers=self.get_auth("raclette"),
@@ -658,6 +666,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
"external_link": "https://raclette.fr",
},
@@ -682,6 +691,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1},
],
+ "bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@@ -706,6 +716,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "30",
"external_link": "https://raclette.fr",
"original_currency": "CAD",
@@ -727,6 +738,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1.0},
{"activated": True, "id": 2, "name": "jeanne", "weight": 1.0},
],
+ "bill_type": "Expense",
"amount": 30.0,
"date": "2011-08-10",
"id": 1,
@@ -747,6 +759,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "Pierogi",
"payer": "1",
"payed_for": ["2", "3"],
+ "bill_type": "Expense",
"amount": "80",
"original_currency": "PLN",
},
@@ -791,6 +804,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@@ -855,6 +869,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
+ "bill_type": "Expense",
"amount": "25",
},
headers=self.get_auth("raclette"),
@@ -877,6 +892,7 @@ class TestAPI(IhatemoneyTestCase):
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "jeannedy familly", "weight": 4},
],
+ "bill_type": "Expense",
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
@@ -962,6 +978,7 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
+ "bill_type": "Expense",
"amount": "0",
},
headers=self.get_auth("raclette"),
@@ -990,8 +1007,73 @@ class TestAPI(IhatemoneyTestCase):
"what": "fromage",
"payer": "1",
"payed_for": ["1"],
+ "bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
+
+ def test_validate_bill_type(self):
+ self.api_create("raclette")
+ self.api_add_member("raclette", "zorglub")
+
+
+ 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"
+
diff --git a/ihatemoney/tests/budget_test.py b/ihatemoney/tests/budget_test.py
index f66cab1c..9ddeec8a 100644
--- a/ihatemoney/tests/budget_test.py
+++ b/ihatemoney/tests/budget_test.py
@@ -428,6 +428,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": jeanne_id,
"payed_for": [jeanne_id],
+ "bill_type": "Expense",
"amount": "25",
},
)
@@ -479,6 +480,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": zorglub.id,
"payed_for": [zorglub.id],
+ "bill_type": "Expense",
"amount": "25",
},
)
@@ -646,6 +648,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "25",
},
)
@@ -661,6 +664,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -684,6 +688,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "19",
},
)
@@ -695,6 +700,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[1],
"payed_for": members_ids[0],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -706,10 +712,23 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[1],
"payed_for": members_ids,
+ "bill_type": "Expense",
"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
assert set(balance.values()) == set([19.0, -19.0])
@@ -721,6 +740,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "-25",
},
)
@@ -735,6 +755,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "25,02",
},
)
@@ -749,6 +770,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "42",
"external_link": "https://example.com/fromage",
},
@@ -764,12 +786,179 @@ class TestBudget(IhatemoneyTestCase):
"what": "mauvais fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "42000",
"external_link": "javascript:alert('Tu bluffes, Martoni.')",
},
)
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):
self.post_project("raclette")
@@ -789,6 +978,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": members_ids[0],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -800,6 +990,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "pommes de terre",
"payer": members_ids[1],
"payed_for": members_ids,
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -864,6 +1055,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "24.36",
},
)
@@ -875,6 +1067,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "19.12",
},
)
@@ -886,6 +1079,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "22",
},
)
@@ -1019,6 +1213,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -1030,6 +1225,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -1041,6 +1237,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -1089,6 +1286,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 2,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "30",
},
)
@@ -1114,6 +1312,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "ice cream",
"payer": 2,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -1129,6 +1328,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "champomy",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -1144,6 +1344,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "smoothie",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -1160,6 +1361,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "more champomy",
"payer": 2,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "30",
},
)
@@ -1192,6 +1394,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -1203,6 +1406,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -1214,6 +1418,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -1228,6 +1433,68 @@ class TestBudget(IhatemoneyTestCase):
for m, a in members.items():
assert abs(a - balance[m.id]) < 0.01
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):
self.post_project("raclette")
@@ -1245,6 +1512,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -1256,6 +1524,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -1267,6 +1536,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund",
"payer": 3,
"payed_for": [2],
+ "bill_type": "Expense",
"amount": "13.33",
},
)
@@ -1299,6 +1569,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3, 4],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -1317,6 +1588,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "roblochon",
"payer": 2,
"payed_for": [1, 3, 4],
+ "bill_type": "Expense",
"amount": "100.0",
}
# Try to access bill of another project
@@ -1422,6 +1694,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -1433,6 +1706,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -1444,6 +1718,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund",
"payer": 3,
"payed_for": [2],
+ "bill_type": "Expense",
"amount": "13.33",
},
)
@@ -1469,6 +1744,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "refund from EUR",
"payer": 3,
"payed_for": [2],
+ "bill_type": "Expense",
"amount": "20",
"original_currency": "EUR",
},
@@ -1492,6 +1768,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "Poutine",
"payer": 3,
"payed_for": [2],
+ "bill_type": "Expense",
"amount": "18",
"original_currency": "CAD",
},
@@ -1548,6 +1825,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10.0",
"original_currency": "EUR",
},
@@ -1583,6 +1861,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10.0",
"original_currency": "EUR",
},
@@ -1595,6 +1874,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "aspirine",
"payer": 2,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "5.0",
"original_currency": "EUR",
},
@@ -1629,6 +1909,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "0",
"original_currency": "XXX",
},
@@ -1673,6 +1954,7 @@ class TestBudget(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "9347242149381274732472348728748723473278472843.12",
"original_currency": "EUR",
},
@@ -1723,6 +2005,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
self.client.post(
@@ -1734,6 +2017,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
self.client.post(
@@ -1745,6 +2029,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
@@ -1807,6 +2092,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2, 3],
"amount": "12",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
self.client.post(
@@ -1818,6 +2104,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "15",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
self.client.post(
@@ -1829,6 +2116,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1, 2],
"amount": "10",
"original_currency": "EUR",
+ "bill_type": "Expense"
},
)
@@ -1907,6 +2195,7 @@ class TestBudget(IhatemoneyTestCase):
"payed_for": [1],
"amount": "12",
"original_currency": "XXX",
+ "bill_type": "Expense"
},
follow_redirects=True,
)
@@ -1967,6 +2256,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": 1,
"payed_for": [1],
"amount": "12",
+ "bill_type": "Expense",
"original_currency": "XXX",
},
follow_redirects=True,
@@ -2074,6 +2364,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1],
"payed_for": members_ids,
"amount": "25",
+ "bill_type": "Expense"
},
)
@@ -2091,6 +2382,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids_tartif[2],
"payed_for": members_ids_tartif,
"amount": "24",
+ "bill_type": "Expense"
},
)
@@ -2125,6 +2417,7 @@ class TestBudget(IhatemoneyTestCase):
"payer": members_ids[1],
"payed_for": members_ids[1:],
"amount": "25",
+ "bill_type": "Expense"
},
)
diff --git a/ihatemoney/tests/history_test.py b/ihatemoney/tests/history_test.py
index 52ce2874..828a23ca 100644
--- a/ihatemoney/tests/history_test.py
+++ b/ihatemoney/tests/history_test.py
@@ -212,6 +212,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
+ "bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
@@ -228,6 +229,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
+ "bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
@@ -371,6 +373,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "25",
},
follow_redirects=True,
@@ -391,6 +394,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "new thing",
"payer": 1,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "10",
},
follow_redirects=True,
@@ -477,6 +481,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "25",
},
)
@@ -487,6 +492,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -505,6 +511,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "88",
},
)
@@ -545,6 +552,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "25",
},
)
@@ -567,6 +575,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -627,6 +636,7 @@ class TestHistory(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "10",
"original_currency": "EUR",
},
diff --git a/ihatemoney/tests/import_test.py b/ihatemoney/tests/import_test.py
index 8bc43825..604c20f6 100644
--- a/ihatemoney/tests/import_test.py
+++ b/ihatemoney/tests/import_test.py
@@ -16,11 +16,13 @@ def import_data(request: pytest.FixtureRequest):
"amount": 13.33,
"payer_name": "tata",
"payer_weight": 1.0,
+ "bill_type": "Expense",
"owers": ["jeanne"],
},
{
"date": "2016-12-31",
"what": "red wine",
+ "bill_type": "Expense",
"amount": 200.0,
"payer_name": "jeanne",
"payer_weight": 1.0,
@@ -28,6 +30,7 @@ def import_data(request: pytest.FixtureRequest):
},
{
"date": "2016-12-31",
+ "bill_type": "Expense",
"what": "fromage a raclette",
"amount": 10.0,
"payer_name": "zorglub",
@@ -48,6 +51,7 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
+ "bill_type": "Expense",
"amount": 13.33,
"payer_name": "tata",
"payer_weight": 1.0,
@@ -56,6 +60,7 @@ class CommonTestCase(object):
{
"date": "2016-12-31",
"what": "red wine",
+ "bill_type": "Expense",
"amount": 200.0,
"payer_name": "jeanne",
"payer_weight": 1.0,
@@ -63,7 +68,8 @@ class CommonTestCase(object):
},
{
"date": "2016-12-31",
- "what": "fromage a raclette",
+ "what": "a raclette",
+ "bill_type": "Expense",
"amount": 10.0,
"payer_name": "zorglub",
"payer_weight": 2.0,
@@ -108,6 +114,7 @@ class CommonTestCase(object):
assert b["currency"] == d["currency"]
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
+ assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@@ -150,6 +157,7 @@ class CommonTestCase(object):
assert b["currency"] == "XXX"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
+ assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@@ -208,6 +216,7 @@ class CommonTestCase(object):
assert b["currency"] == "EUR"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
+ assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@@ -247,6 +256,7 @@ class CommonTestCase(object):
assert b["currency"] == "XXX"
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
+ assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@@ -271,6 +281,7 @@ class CommonTestCase(object):
data={
"date": "2016-12-31",
"what": "red wine",
+ "bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "200",
@@ -303,6 +314,7 @@ class CommonTestCase(object):
assert b["currency"] == d["currency"]
assert b["payer_weight"] == d["payer_weight"]
assert b["date"] == d["date"]
+ assert b["bill_type"] == d["bill_type"]
list_project = [ower for ower in b["owers"]]
list_project.sort()
list_json = [ower for ower in d["owers"]]
@@ -326,6 +338,7 @@ class CommonTestCase(object):
{
"date": "2017-01-01",
"what": "refund",
+ "bill_type": "Reimbursement",
"payer_name": "tata",
"payer_weight": 1.0,
"owers": ["jeanne"],
@@ -353,7 +366,8 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
- "what": "fromage à raclette",
+ "bill_type": "Expense",
+ "what": "à raclette",
"payer": 1,
"payed_for": [1, 2, 3, 4],
"amount": "10.0",
@@ -364,6 +378,7 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
+ "bill_type": "Expense",
"what": "red wine",
"payer": 2,
"payed_for": [1, 3],
@@ -375,6 +390,7 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2017-01-01",
+ "bill_type": "Reimbursement",
"what": "refund",
"payer": 3,
"payed_for": [2],
@@ -387,6 +403,7 @@ class TestExport(IhatemoneyTestCase):
expected = [
{
"date": "2017-01-01",
+ "bill_type": "Reimbursement",
"what": "refund",
"amount": 13.33,
"currency": "XXX",
@@ -396,6 +413,7 @@ class TestExport(IhatemoneyTestCase):
},
{
"date": "2016-12-31",
+ "bill_type": "Expense",
"what": "red wine",
"amount": 200.0,
"currency": "XXX",
@@ -405,7 +423,8 @@ class TestExport(IhatemoneyTestCase):
},
{
"date": "2016-12-31",
- "what": "fromage \xe0 raclette",
+ "bill_type": "Expense",
+ "what": "\xe0 raclette",
"amount": 10.0,
"currency": "XXX",
"payer_name": "zorglub",
@@ -418,10 +437,10 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
- "date,what,amount,currency,payer_name,payer_weight,owers",
- "2017-01-01,refund,XXX,13.33,tata,1.0,jeanne",
- '2016-12-31,red wine,XXX,200.0,jeanne,1.0,"zorglub, tata"',
- '2016-12-31,fromage à raclette,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
+ "date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
+ "2017-01-01,refund,Reimbursement,XXX,13.33,tata,1.0,jeanne",
+ '2016-12-31,red wine,Expense,XXX,200.0,jeanne,1.0,"zorglub, tata"',
+ '2016-12-31,à raclette,Expense,10.0,XXX,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
]
received_lines = resp.data.decode("utf-8").split("\n")
@@ -481,7 +500,8 @@ class TestExport(IhatemoneyTestCase):
"/raclette/add",
data={
"date": "2016-12-31",
- "what": "fromage à raclette",
+ "what": "à raclette",
+ "bill_type": "Expense",
"payer": 1,
"payed_for": [1, 2, 3, 4],
"amount": "10.0",
@@ -494,6 +514,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "poutine from Québec",
+ "bill_type": "Expense",
"payer": 2,
"payed_for": [1, 3],
"amount": "100",
@@ -506,6 +527,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2017-01-01",
"what": "refund",
+ "bill_type": "Reimbursement",
"payer": 3,
"payed_for": [2],
"amount": "13.33",
@@ -519,6 +541,7 @@ class TestExport(IhatemoneyTestCase):
{
"date": "2017-01-01",
"what": "refund",
+ "bill_type": "Reimbursement",
"amount": 13.33,
"currency": "EUR",
"payer_name": "tata",
@@ -528,6 +551,7 @@ class TestExport(IhatemoneyTestCase):
{
"date": "2016-12-31",
"what": "poutine from Qu\xe9bec",
+ "bill_type": "Expense",
"amount": 100.0,
"currency": "CAD",
"payer_name": "jeanne",
@@ -536,7 +560,8 @@ class TestExport(IhatemoneyTestCase):
},
{
"date": "2016-12-31",
- "what": "fromage \xe0 raclette",
+ "what": "\xe0 raclette",
+ "bill_type": "Expense",
"amount": 10.0,
"currency": "EUR",
"payer_name": "zorglub",
@@ -549,10 +574,10 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
- "date,what,amount,currency,payer_name,payer_weight,owers",
- "2017-01-01,refund,13.33,EUR,tata,1.0,jeanne",
- '2016-12-31,poutine from Québec,100.0,CAD,jeanne,1.0,"zorglub, tata"',
- '2016-12-31,fromage à raclette,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
+ "date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
+ "2017-01-01,refund,Reimbursement,13.33,EUR,tata,1.0,jeanne",
+ '2016-12-31,poutine from Québec,Expense,100.0,CAD,jeanne,1.0,"zorglub, tata"',
+ '2016-12-31,à raclette,Expense,10.0,EUR,zorglub,2.0,"zorglub, jeanne, tata, pépé"',
]
received_lines = resp.data.decode("utf-8").split("\n")
@@ -643,6 +668,7 @@ class TestExport(IhatemoneyTestCase):
data={
"date": "2016-12-31",
"what": "=COS(36)",
+ "bill_type": "Expense",
"payer": 1,
"payed_for": [1],
"amount": "10.0",
@@ -653,8 +679,8 @@ class TestExport(IhatemoneyTestCase):
# generate csv export of bills
resp = self.client.get("/raclette/export/bills.csv")
expected = [
- "date,what,amount,currency,payer_name,payer_weight,owers",
- "2016-12-31,'=COS(36),10.0,EUR,zorglub,1.0,zorglub",
+ "date,what,bill_type,amount,currency,payer_name,payer_weight,owers",
+ "2016-12-31,'=COS(36),Expense,10.0,EUR,zorglub,1.0,zorglub",
]
received_lines = resp.data.decode("utf-8").split("\n")
diff --git a/ihatemoney/tests/main_test.py b/ihatemoney/tests/main_test.py
index ecafe831..843385f9 100644
--- a/ihatemoney/tests/main_test.py
+++ b/ihatemoney/tests/main_test.py
@@ -126,6 +126,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -137,6 +138,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -148,6 +150,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
@@ -181,6 +184,7 @@ class TestModels(IhatemoneyTestCase):
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
+ "bill_type": "Expense",
"amount": "10.0",
},
)
@@ -192,6 +196,7 @@ class TestModels(IhatemoneyTestCase):
"what": "red wine",
"payer": 2,
"payed_for": [1],
+ "bill_type": "Expense",
"amount": "20",
},
)
@@ -203,6 +208,7 @@ class TestModels(IhatemoneyTestCase):
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
+ "bill_type": "Expense",
"amount": "10",
},
)
diff --git a/ihatemoney/utils.py b/ihatemoney/utils.py
index 281cdf51..ef4818ad 100644
--- a/ihatemoney/utils.py
+++ b/ihatemoney/utils.py
@@ -221,6 +221,7 @@ def csv2list_of_dicts(csv_to_convert):
r["amount"] = float(r["amount"])
r["payer_weight"] = float(r["payer_weight"])
r["owers"] = [o.strip() for o in r["owers"].split(",")]
+ r["bill_type"] = str(r["bill_type"])
result.append(r)
return result
@@ -304,7 +305,16 @@ def get_members(file):
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:
if bill1[a] != bill2[a]:
return False
diff --git a/ihatemoney/web.py b/ihatemoney/web.py
index bbb19d3d..3433fb91 100644
--- a/ihatemoney/web.py
+++ b/ihatemoney/web.py
@@ -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`
and `add_project_id` for a quick overview)
"""
+import datetime
from functools import wraps
import hashlib
import json
@@ -57,7 +58,7 @@ from ihatemoney.forms import (
get_billform_for,
)
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 (
Redirect303,
csv2list_of_dicts,
@@ -471,6 +472,7 @@ def import_project():
# Check data
attr = [
"amount",
+ "bill_type",
"currency",
"date",
"owers",
@@ -848,6 +850,27 @@ def settle_bill():
return render_template("settle_bills.html", bills=bills, current_view="settle_bill")
+@main.route("//settle///")
+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("//history")
def history():
"""Query for the version entries associated with this project."""