From 720f0e52ddbacf2a8ee4f1426978548f36b6cf77 Mon Sep 17 00:00:00 2001 From: TomRoussel <21120212+TomRoussel@users.noreply.github.com> Date: Sat, 16 Mar 2024 12:20:48 +0100 Subject: [PATCH] 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 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 Co-authored-by: Brandan Chen Co-authored-by: Emilie Zhou <54161959+ez157@users.noreply.github.com> Co-authored-by: Tom --- ihatemoney/forms.py | 6 +- ...b38559992_new_bill_type_attribute_added.py | 31 ++ ihatemoney/models.py | 53 +++- ihatemoney/templates/forms.html | 1 + ihatemoney/templates/settle_bills.html | 9 +- ihatemoney/tests/api_test.py | 82 +++++ ihatemoney/tests/budget_test.py | 295 +++++++++++++++++- ihatemoney/tests/history_test.py | 10 + ihatemoney/tests/import_test.py | 56 +++- ihatemoney/tests/main_test.py | 6 + ihatemoney/utils.py | 12 +- ihatemoney/web.py | 25 +- 12 files changed, 553 insertions(+), 33 deletions(-) create mode 100644 ihatemoney/migrations/versions/7a9b38559992_new_bill_type_attribute_added.py 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 %} - + {% for bill in bills %} + {% 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."""
{{ _("Who pays?") }}{{ _("To whom?") }}{{ _("How much?") }}
{{ _("Who pays?") }}{{ _("To whom?") }}{{ _("How much?") }}{{ _("Settled?") }}
{{ bill.ower }} {{ bill.receiver }} {{ bill.amount|currency }} + + + {{ ("Settle") }} + + +