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.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

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 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)
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
)
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,6 +179,7 @@ class Project(db.Model):
"""
monthly = defaultdict(lambda: defaultdict(float))
for bill in self.get_bills_unordered().all():
if bill.bill_type == BillType.EXPENSE:
monthly[bill.date.year][bill.date.month] += bill.converted_amount
return monthly
@ -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,

View file

@ -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") }}
<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 %}
<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>
{% for bill in bills %}
<tr receiver={{bill.receiver.id}}>
<td>{{ bill.ower }}</td>
<td>{{ bill.receiver }}</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>
{% endfor %}
</tbody>

View file

@ -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"

View file

@ -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",
},
)
@ -1229,6 +1434,68 @@ class TestBudget(IhatemoneyTestCase):
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"
},
)

View file

@ -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",
},

View file

@ -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")

View file

@ -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",
},
)

View file

@ -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

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`
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("/<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")
def history():
"""Query for the version entries associated with this project."""