mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-04-28 17:32:38 +02:00
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:
parent
ba117ba0a6
commit
720f0e52dd
12 changed files with 553 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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())
|
|
@ -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,
|
||||
|
|
|
@ -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") }}'>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Reference in a new issue