Skip to content

Commit

Permalink
FIXUP: some more work
Browse files Browse the repository at this point in the history
  • Loading branch information
almet committed Sep 29, 2024
1 parent 14cc9b9 commit f968c98
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 42 deletions.
68 changes: 49 additions & 19 deletions ihatemoney/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ class EditProjectForm(FlaskForm):
_("New private code"),
description=_("Enter a new code if you want to change it"),
)
contact_email = StringField(_("Email"), validators=[DataRequired(), Email()])
contact_email = StringField(_("Email"), validators=[
DataRequired(), Email()])
project_history = BooleanField(_("Enable project history"))
ip_recording = BooleanField(_("Use IP tracking for project history"))
currency_helper = CurrencyConverter()
Expand Down Expand Up @@ -228,7 +229,8 @@ class ImportProjectForm(FlaskForm):
"File",
validators=[
FileRequired(),
FileAllowed(["json", "JSON", "csv", "CSV"], "Incorrect file format"),
FileAllowed(["json", "JSON", "csv", "CSV"],
"Incorrect file format"),
],
description=_("Compatible with Cospend"),
)
Expand Down Expand Up @@ -349,9 +351,11 @@ class ResetPasswordForm(FlaskForm):


class BillForm(FlaskForm):
date = DateField(_("When?"), validators=[DataRequired()], default=datetime.now)
date = DateField(_("When?"), validators=[
DataRequired()], default=datetime.now)
what = StringField(_("What?"), validators=[DataRequired()])
payer = SelectField(_("Who paid?"), validators=[DataRequired()], coerce=int)
payer = SelectField(_("Who paid?"), validators=[
DataRequired()], coerce=int)
amount = CalculatorStringField(_("How much?"), validators=[DataRequired()])
currency_helper = CurrencyConverter()
original_currency = SelectField(_("Currency"), validators=[DataRequired()])
Expand All @@ -373,29 +377,48 @@ class BillForm(FlaskForm):
submit = SubmitField(_("Submit"))
submit2 = SubmitField(_("Submit and add a new one"))

def parse_hashtags(self, project, what):
"""Handles the hashtags which can be optionally specified in the 'what'
field, using the `grocery #hash #otherhash` syntax.
Returns: the new "what" field (with hashtags stripped-out) and the list
of tags.
"""

hashtags = findall(r"#(\w+)", what)

if not hashtags:
return what, []

for tag in hashtags:
what = what.replace(f"#{tag}", "")

return what, hashtags

def export(self, project):
return Bill(
"""This is triggered on bill creation.
"""
what, hashtags = self.parse_hashtags(project, self.what.data)

bill = Bill(
amount=float(self.amount.data),
date=self.date.data,
external_link=self.external_link.data,
original_currency=str(self.original_currency.data),
owers=Person.query.get_by_ids(self.payed_for.data, project),
payer_id=self.payer.data,
project_default_currency=project.default_currency,
what=self.what.data,
what=what,
bill_type=self.bill_type.data,
)
bill.set_tags(hashtags, project)
return bill

def save(self, bill, project):
what, hashtags = self.parse_hashtags(project, self.what.data)
bill.payer_id = self.payer.data
bill.amount = self.amount.data
# Get the list of tags from the 'what' field
hashtags = findall(r"#(\w+)", self.what.data)
if hashtags:
bill.tags = [Tag(name=tag) for tag in hashtags]
for tag in hashtags:
self.what.data = self.what.data.replace(f"#{tag}", "")
bill.what = self.what.data
bill.what = what
bill.bill_type = BillType(self.bill_type.data)
bill.external_link = self.external_link.data
bill.date = self.date.data
Expand All @@ -404,19 +427,22 @@ def save(self, bill, project):
bill.converted_amount = self.currency_helper.exchange_currency(
bill.amount, bill.original_currency, project.default_currency
)
bill.set_tags(hashtags, project)
return bill

def fill(self, bill, project):
self.payer.data = bill.payer_id
self.amount.data = bill.amount
self.what.data = bill.what
hashtags = ' '.join([f'#{tag.name}' for tag in bill.tags])
self.what.data = bill.what.strip() + f' {hashtags}'
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
self.payed_for.data = [int(ower.id) for ower in bill.owers]

self.original_currency.label = Label("original_currency", _("Currency"))
self.original_currency.label = Label(
"original_currency", _("Currency"))
self.original_currency.description = _(
"Project default: %(currency)s",
currency=render_localized_currency(
Expand Down Expand Up @@ -445,10 +471,13 @@ def validate_original_currency(self, field):


class MemberForm(FlaskForm):
name = StringField(_("Name"), validators=[DataRequired()], filters=[strip_filter])
name = StringField(_("Name"), validators=[
DataRequired()], filters=[strip_filter])

weight_validators = [NumberRange(min=0.1, message=_("Weights should be positive"))]
weight = CommaDecimalField(_("Weight"), default=1, validators=weight_validators)
weight_validators = [NumberRange(
min=0.1, message=_("Weights should be positive"))]
weight = CommaDecimalField(
_("Weight"), default=1, validators=weight_validators)
submit = SubmitField(_("Add"))

def __init__(self, project, edit=False, *args, **kwargs):
Expand All @@ -467,7 +496,8 @@ def validate_name(self, field):
Person.activated,
).all()
): # NOQA
raise ValidationError(_("This project already have this participant"))
raise ValidationError(
_("This project already have this participant"))

def save(self, project, person):
# if the user is already bound to the project, just reactivate him
Expand Down
66 changes: 57 additions & 9 deletions ihatemoney/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ def full_balance(self):
balance spent paid
"""
balances, should_pay, should_receive = (defaultdict(int) for time in (1, 2, 3))
balances, should_pay, should_receive = (
defaultdict(int) for time in (1, 2, 3))
for bill in self.get_bills_unordered().all():
total_weight = sum(ower.weight for ower in bill.owers)

Expand Down Expand Up @@ -181,11 +182,28 @@ def monthly_stats(self):
:rtype dict:
"""
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

@property
def tags_monthly_stats(self):
"""
:return: a dict of years mapping to a dict of months mapping to the amount
:rtype dict:
"""
tags_monthly = defaultdict(
lambda: defaultdict(lambda: defaultdict(float)))

for bill in self.get_bills_unordered().all():
if bill.bill_type == BillType.EXPENSE:
for tag in bill.tags:
tags_monthly[bill.date.year][bill.date.month][tag.name] += bill.converted_amount
return tags_monthly

@property
def uses_weights(self):
return len([i for i in self.members if i.weight != 1]) > 0
Expand Down Expand Up @@ -322,7 +340,8 @@ def active_months_range(self):
year=newest_date.year, month=newest_date.month, day=1
)
# Infinite iterator towards the past
all_months = (newest_month - relativedelta(months=i) for i in itertools.count())
all_months = (newest_month - relativedelta(months=i)
for i in itertools.count())
# Stop when reaching one month before the first date
months = itertools.takewhile(
lambda x: x > oldest_date - relativedelta(months=1), all_months
Expand Down Expand Up @@ -497,7 +516,8 @@ def verify_token(token, token_type="auth", project_id=None, max_age=3600):
)
loads_kwargs["max_age"] = max_age
else:
project = Project.query.get(project_id) if project_id is not None else None
project = Project.query.get(
project_id) if project_id is not None else None
password = project.password if project is not None else ""
serializer = URLSafeSerializer(
current_app.config["SECRET_KEY"] + password, salt=token_type
Expand Down Expand Up @@ -643,13 +663,28 @@ def __repr__(self):
# We need to manually define a join table for m2m relations
billowers = db.Table(
"billowers",
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
db.Column("person_id", db.Integer, db.ForeignKey("person.id"), primary_key=True),
db.Column("bill_id", db.Integer, db.ForeignKey(
"bill.id"), primary_key=True),
db.Column("person_id", db.Integer, db.ForeignKey(
"person.id"), primary_key=True),
sqlite_autoincrement=True,
)


class Tag(db.Model):
class TagQuery(BaseQuery):
def get_or_create(self, name, project):
exists = (
Tag.query.filter(Tag.name == name)
.filter(Tag.project_id == project.id)
.one_or_none()
)
if exists:
return exists
return Tag(name=name, project_id=project.id)

query_class = TagQuery

__versionned__ = {}

__table_args__ = {"sqlite_autoincrement": True}
Expand All @@ -662,11 +697,15 @@ class Tag(db.Model):
def __str__(self):
return self.name

def __repr__(self):
return self.name


# We need to manually define a join table for m2m relations
billtags = db.Table(
"billtags",
db.Column("bill_id", db.Integer, db.ForeignKey("bill.id"), primary_key=True),
db.Column("bill_id", db.Integer, db.ForeignKey(
"bill.id"), primary_key=True),
db.Column("tag_id", db.Integer, db.ForeignKey("tag.id"), primary_key=True),
sqlite_autoincrement=True,
)
Expand Down Expand Up @@ -776,15 +815,24 @@ def pay_each_default(self, amount):
else:
return 0

def __str__(self):
return self.what

def pay_each(self):
"""Warning: this is slow, if you need to compute this for many bills, do
it differently (see balance_full function)
"""
return self.pay_each_default(self.converted_amount)

def set_tags(self, tags, project):
object_tags = []
for tag_name in tags:
tag = Tag.query.get_or_create(name=tag_name, project=project)
db.session.add(tag)
object_tags.append(tag)
self.tags = object_tags
db.session.commit()

def __str__(self):
return self.what

def __repr__(self):
return (
f"<Bill of {self.amount} from {self.payer} for "
Expand Down
23 changes: 17 additions & 6 deletions ihatemoney/templates/list_bills.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,18 @@ <h3 class="modal-title">{{ _('Add a bill') }}</h3>
{% if bills.total > 0 %}
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
<thead>
<tr><th>{{ _("When?") }}
</th><th>{{ _("Who paid?") }}
</th><th>{{ _("For what?") }}
</th><th>{{ _("For whom?") }}
</th><th>{{ _("How much?") }}
</th><th>{{ _("Actions") }}</th></tr>
<tr><th>{{ _("When?") }}</th>
<th>{{ _("Who paid?") }}</th>
<th>{{ _("For what?") }}</th>
<th>{{ _("For whom?") }}</th>
<th>{{ _("How much?") }}</th>
<th data-toggle="tooltip"
data-placement="top"
title="{{ _('You can add tags to your bills by appending a #hashtag') }}">
{{ _("Tags") }}
</th>
<th>{{ _("Actions") }}</th>
</tr>
</thead>
<tbody>
{% for (weights, bill) in bills.items %}
Expand All @@ -147,6 +153,11 @@ <h3 class="modal-title">{{ _('Add a bill') }}</h3>
{{ weighted_bill_amount(bill, weights) }}
</span>
</td>
<td>
{% for tag in bill.tags %}
#{{ tag.name }}
{% endfor %}
</td>
<td class="bill-actions d-flex align-items-center">
<a class="edit" href="{{ url_for(".edit_bill", bill_id=bill.id) }}" title="{{ _("edit") }}">{{ _('edit') }}</a>
<form class="delete-bill" action="{{ url_for(".delete_bill", bill_id=bill.id) }}" method="POST">
Expand Down
18 changes: 17 additions & 1 deletion ihatemoney/templates/statistics.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,28 @@
</table>
<h2>{{ _("Expenses by Month") }}</h2>
<table id="monthly_stats" class="table table-striped">
<thead><tr><th>{{ _("Period") }}</th><th>{{ _("Spent") }}</th></tr></thead>
<thead>
<tr>
<th>{{ _("Period") }}</th>
<th>{{ _("Spent") }}</th>
{% for tag in tags %}
<th>#{{ tag.name }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for month in months %}
<tr>
<td>{{ month|dateformat("MMMM yyyy") }}</td>
<td>{{ monthly_stats[month.year][month.month]|currency }}</td>
{% for tag in tags %}
{% if tag.name in tags_monthly_stats[month.year][month.month] %}
<td>{{ tags_monthly_stats[month.year][month.month][tag.name]|currency }}</td>
{% else %}
<td> - </td>
{% endif %}
{% endfor %}

</tr>
{% endfor %}
</tbody>
Expand Down
Loading

0 comments on commit f968c98

Please sign in to comment.