mirror of
https://github.com/spiral-project/ihatemoney.git
synced 2025-05-06 13:01:50 +02:00
added tagging functionality and filtering by date, amount, payer, and tag
This commit is contained in:
parent
37156cf976
commit
e018e8a19a
6 changed files with 97 additions and 3 deletions
|
@ -1,7 +1,7 @@
|
||||||
from flask_wtf.form import FlaskForm
|
from flask_wtf.form import FlaskForm
|
||||||
from wtforms.fields.core import SelectField, SelectMultipleField
|
from wtforms.fields.core import SelectField, SelectMultipleField
|
||||||
from wtforms.fields.html5 import DateField, DecimalField, URLField
|
from wtforms.fields.html5 import DateField, DecimalField, URLField
|
||||||
from wtforms.fields.simple import PasswordField, SubmitField, StringField, BooleanField
|
from wtforms.fields.simple import PasswordField, SubmitField, StringField, BooleanField, HiddenField
|
||||||
from wtforms.validators import (
|
from wtforms.validators import (
|
||||||
Email,
|
Email,
|
||||||
DataRequired,
|
DataRequired,
|
||||||
|
@ -199,6 +199,7 @@ class ResetPasswordForm(FlaskForm):
|
||||||
class BillForm(FlaskForm):
|
class BillForm(FlaskForm):
|
||||||
date = DateField(_("Date"), validators=[DataRequired()], default=datetime.now)
|
date = DateField(_("Date"), validators=[DataRequired()], default=datetime.now)
|
||||||
what = StringField(_("What?"), validators=[DataRequired()])
|
what = StringField(_("What?"), validators=[DataRequired()])
|
||||||
|
tag = HiddenField(_("Tag"), default="")
|
||||||
payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int)
|
payer = SelectField(_("Payer"), validators=[DataRequired()], coerce=int)
|
||||||
amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()])
|
amount = CalculatorStringField(_("Amount paid"), validators=[DataRequired()])
|
||||||
external_link = URLField(
|
external_link = URLField(
|
||||||
|
@ -216,6 +217,9 @@ class BillForm(FlaskForm):
|
||||||
bill.payer_id = self.payer.data
|
bill.payer_id = self.payer.data
|
||||||
bill.amount = self.amount.data
|
bill.amount = self.amount.data
|
||||||
bill.what = self.what.data
|
bill.what = self.what.data
|
||||||
|
tag = list(set(part[1:] for part in bill.what.split() if part.startswith('#')))
|
||||||
|
if tag:
|
||||||
|
bill.tag = tag[0]
|
||||||
bill.external_link = self.external_link.data
|
bill.external_link = self.external_link.data
|
||||||
bill.date = self.date.data
|
bill.date = self.date.data
|
||||||
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
|
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for.data]
|
||||||
|
@ -225,6 +229,7 @@ class BillForm(FlaskForm):
|
||||||
bill.payer_id = self.payer
|
bill.payer_id = self.payer
|
||||||
bill.amount = self.amount
|
bill.amount = self.amount
|
||||||
bill.what = self.what
|
bill.what = self.what
|
||||||
|
bill.tag = self.tag
|
||||||
bill.external_link = ""
|
bill.external_link = ""
|
||||||
bill.date = self.date
|
bill.date = self.date
|
||||||
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
|
bill.owers = [Person.query.get(ower, project) for ower in self.payed_for]
|
||||||
|
@ -235,6 +240,7 @@ class BillForm(FlaskForm):
|
||||||
self.payer.data = bill.payer_id
|
self.payer.data = bill.payer_id
|
||||||
self.amount.data = bill.amount
|
self.amount.data = bill.amount
|
||||||
self.what.data = bill.what
|
self.what.data = bill.what
|
||||||
|
self.tag.data = bill.tag
|
||||||
self.external_link.data = bill.external_link
|
self.external_link.data = bill.external_link
|
||||||
self.date.data = bill.date
|
self.date.data = bill.date
|
||||||
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
self.payed_for.data = [int(ower.id) for ower in bill.owers]
|
||||||
|
|
|
@ -434,6 +434,7 @@ class Bill(db.Model):
|
||||||
date = db.Column(db.Date, default=datetime.now)
|
date = db.Column(db.Date, default=datetime.now)
|
||||||
creation_date = db.Column(db.Date, default=datetime.now)
|
creation_date = db.Column(db.Date, default=datetime.now)
|
||||||
what = db.Column(db.UnicodeText)
|
what = db.Column(db.UnicodeText)
|
||||||
|
tag = db.Column(db.UnicodeText)
|
||||||
external_link = db.Column(db.UnicodeText)
|
external_link = db.Column(db.UnicodeText)
|
||||||
|
|
||||||
archive = db.Column(db.Integer, db.ForeignKey("archive.id"))
|
archive = db.Column(db.Integer, db.ForeignKey("archive.id"))
|
||||||
|
@ -448,6 +449,7 @@ class Bill(db.Model):
|
||||||
"date": self.date,
|
"date": self.date,
|
||||||
"creation_date": self.creation_date,
|
"creation_date": self.creation_date,
|
||||||
"what": self.what,
|
"what": self.what,
|
||||||
|
"tag": self.tag,
|
||||||
"external_link": self.external_link,
|
"external_link": self.external_link,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -174,6 +174,14 @@ body {
|
||||||
width: 5em;
|
width: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#bill-search {
|
||||||
|
color: white;
|
||||||
|
background: #8f9296;
|
||||||
|
margin-top: 10px;
|
||||||
|
border-radius: .25rem;
|
||||||
|
padding: .375rem .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.invites textarea {
|
.invites textarea {
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
|
@ -17,6 +17,44 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// remove duplicate tags
|
||||||
|
var usedTags = {};
|
||||||
|
$("select[name='tag-search-select'] > option").each(function() {
|
||||||
|
if (usedTags[this.text]) {
|
||||||
|
$(this).remove();
|
||||||
|
} else {
|
||||||
|
usedTags[this.text] = this.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// add default values to payer select
|
||||||
|
$('#payer-search-select').prepend(new Option('', '', true, true));
|
||||||
|
|
||||||
|
// add default values to date select
|
||||||
|
$('#date-search-select').prepend(new Option('', '', true, true));
|
||||||
|
|
||||||
|
$('#btn-bill-filter').click(function() {
|
||||||
|
var dateValue = $('#date-search-select').val();
|
||||||
|
var payerValue = $('#payer-search-select option:selected').val();
|
||||||
|
var amountValue = $('#amount-search-select').val();
|
||||||
|
if (amountValue.substr(amountValue.length - 3) === '.00') {
|
||||||
|
amountValue = amountValue.substr(0, amountValue.length - 3)
|
||||||
|
}
|
||||||
|
var amountWithZero = amountValue + ".0"
|
||||||
|
var tagValue = $('#tag-search-select').val();
|
||||||
|
|
||||||
|
var matching = $('#bill_table tbody tr').filter(function(){
|
||||||
|
return (payerValue != "" && $(this).attr('payer') !== payerValue) || $(this).attr('date') !== dateValue || (amountValue != "" && $(this).attr('amount') !== amountValue && $(this).attr('amount') !== amountWithZero) || (tagValue != "" && $(this).attr('tag') !== tagValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
matching.hide();
|
||||||
|
$('#bill_table tbody tr').not(matching).show(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#btn-bill-showall').click(function() {
|
||||||
|
$('#bill_table tbody tr').show(200);
|
||||||
|
});
|
||||||
|
|
||||||
var highlight_owers = function(){
|
var highlight_owers = function(){
|
||||||
var ower_ids = $(this).attr("owers").split(',');
|
var ower_ids = $(this).attr("owers").split(',');
|
||||||
var payer_id = $(this).attr("payer");
|
var payer_id = $(this).attr("payer");
|
||||||
|
@ -110,11 +148,38 @@
|
||||||
{% if bills.total > 0 %}
|
{% if bills.total > 0 %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
|
<div id="bill-search">
|
||||||
|
<span>Filter by:</span>
|
||||||
|
<form class="form-inline mt-2">
|
||||||
|
<div class="form-group mr-sm-2">
|
||||||
|
<label for="date-select" class="mr-sm-2">{{ _("Date") }}</label>
|
||||||
|
{{ bill_form.date(id="date-search-select", class="form-control custom-select", placeholder='') | safe }}
|
||||||
|
<label for="payer-select" class="mr-sm-2" style="margin-left: 8px">{{ _("Payer") }}</label>
|
||||||
|
{{ bill_form.payer(id="payer-search-select", class="form-control custom-select", placeholder='') | safe }}
|
||||||
|
<label for="amount-select" class="mr-sm-2" style="margin-left: 8px">{{ _("Amount") }}</label>
|
||||||
|
{{ bill_form.amount(id="amount-search-select", class="form-control", style="width: 100px", placeholder='') | safe }}
|
||||||
|
<label for="tag-select" class="mr-sm-2" style="margin-left: 8px">{{ _("Tag") }}</label>
|
||||||
|
<select name="tag-search-select" id="tag-search-select" style="height: 38px; margin-right: 8px">
|
||||||
|
<option value=""></option>
|
||||||
|
{% for bill in bills.items %}
|
||||||
|
{% if bill.tag %}
|
||||||
|
<option value="{{bill.tag}}">{{bill.tag}}</option>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button id="btn-bill-filter" type="button" class="btn btn-secondary mr-sm-2">{{ _("Apply Filter") }}</button>
|
||||||
|
<button id="btn-bill-showall" type="button" class="btn btn-secondary">{{ _("Show All") }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<table id="bill_table" class="col table table-striped table-hover table-responsive-sm">
|
<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></thead>
|
<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></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for bill in bills.items %}
|
{% for bill in bills.items %}
|
||||||
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
|
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}" date="{{bill.date}}" amount="{{bill.amount}}" tag="{{bill.tag}}">
|
||||||
<td>
|
<td>
|
||||||
<span data-toggle="tooltip" data-placement="top"
|
<span data-toggle="tooltip" data-placement="top"
|
||||||
title="{{ _('Added on %(date)s', date=bill.creation_date if bill.creation_date else bill.date) }}">
|
title="{{ _('Added on %(date)s', date=bill.creation_date if bill.creation_date else bill.date) }}">
|
||||||
|
|
|
@ -1215,6 +1215,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"date": "2017-01-01",
|
"date": "2017-01-01",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
|
"tag": "",
|
||||||
"amount": 13.33,
|
"amount": 13.33,
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -1223,6 +1224,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
|
"tag": "",
|
||||||
"amount": 200.0,
|
"amount": 200.0,
|
||||||
"payer_name": "fred",
|
"payer_name": "fred",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -1231,6 +1233,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "fromage a raclette",
|
"what": "fromage a raclette",
|
||||||
|
"tag": "",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"payer_name": "alexis",
|
"payer_name": "alexis",
|
||||||
"payer_weight": 2.0,
|
"payer_weight": 2.0,
|
||||||
|
@ -1290,6 +1293,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
data={
|
data={
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
|
"tag": "",
|
||||||
"payer": 2,
|
"payer": 2,
|
||||||
"payed_for": [1, 3],
|
"payed_for": [1, 3],
|
||||||
"amount": "200",
|
"amount": "200",
|
||||||
|
@ -1300,6 +1304,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"date": "2017-01-01",
|
"date": "2017-01-01",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
|
"tag": "",
|
||||||
"amount": 13.33,
|
"amount": 13.33,
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -1308,6 +1313,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{ # This expense does not have to be present twice.
|
{ # This expense does not have to be present twice.
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "red wine",
|
"what": "red wine",
|
||||||
|
"tag": "",
|
||||||
"amount": 200.0,
|
"amount": 200.0,
|
||||||
"payer_name": "fred",
|
"payer_name": "fred",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
|
@ -1316,6 +1322,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{
|
{
|
||||||
"date": "2016-12-31",
|
"date": "2016-12-31",
|
||||||
"what": "fromage a raclette",
|
"what": "fromage a raclette",
|
||||||
|
"tag": "",
|
||||||
"amount": 10.0,
|
"amount": 10.0,
|
||||||
"payer_name": "alexis",
|
"payer_name": "alexis",
|
||||||
"payer_weight": 2.0,
|
"payer_weight": 2.0,
|
||||||
|
@ -1380,6 +1387,7 @@ class BudgetTestCase(IhatemoneyTestCase):
|
||||||
{ # amount missing
|
{ # amount missing
|
||||||
"date": "2017-01-01",
|
"date": "2017-01-01",
|
||||||
"what": "refund",
|
"what": "refund",
|
||||||
|
"tag": "",
|
||||||
"payer_name": "tata",
|
"payer_name": "tata",
|
||||||
"payer_weight": 1.0,
|
"payer_weight": 1.0,
|
||||||
"owers": ["fred"],
|
"owers": ["fred"],
|
||||||
|
@ -1784,6 +1792,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
got = json.loads(req.data.decode("utf-8"))
|
got = json.loads(req.data.decode("utf-8"))
|
||||||
|
del got["tag"]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
datetime.date.today(),
|
datetime.date.today(),
|
||||||
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
||||||
|
@ -1858,6 +1867,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
||||||
)
|
)
|
||||||
del got["creation_date"]
|
del got["creation_date"]
|
||||||
|
del got["tag"]
|
||||||
self.assertDictEqual(expected, got)
|
self.assertDictEqual(expected, got)
|
||||||
|
|
||||||
# delete a bill
|
# delete a bill
|
||||||
|
@ -1934,6 +1944,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
||||||
)
|
)
|
||||||
del got["creation_date"]
|
del got["creation_date"]
|
||||||
|
del got["tag"]
|
||||||
self.assertDictEqual(expected, got)
|
self.assertDictEqual(expected, got)
|
||||||
|
|
||||||
# should raise errors
|
# should raise errors
|
||||||
|
@ -2075,6 +2086,7 @@ class APITestCase(IhatemoneyTestCase):
|
||||||
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
|
||||||
)
|
)
|
||||||
del got["creation_date"]
|
del got["creation_date"]
|
||||||
|
del got["tag"]
|
||||||
self.assertDictEqual(expected, got)
|
self.assertDictEqual(expected, got)
|
||||||
|
|
||||||
# getting it should return a 404
|
# getting it should return a 404
|
||||||
|
|
|
@ -426,7 +426,7 @@ def import_project(file, project):
|
||||||
json_file = json.load(file)
|
json_file = json.load(file)
|
||||||
|
|
||||||
# Check if JSON is correct
|
# Check if JSON is correct
|
||||||
attr = ["what", "payer_name", "payer_weight", "amount", "date", "owers"]
|
attr = ["what", "tag", "payer_name", "payer_weight", "amount", "date", "owers"]
|
||||||
attr.sort()
|
attr.sort()
|
||||||
for e in json_file:
|
for e in json_file:
|
||||||
if len(e) != len(attr):
|
if len(e) != len(attr):
|
||||||
|
@ -483,6 +483,7 @@ def import_project(file, project):
|
||||||
bill = Bill()
|
bill = Bill()
|
||||||
form = get_billform_for(project)
|
form = get_billform_for(project)
|
||||||
form.what = b["what"]
|
form.what = b["what"]
|
||||||
|
form.tag = b["tag"]
|
||||||
form.amount = b["amount"]
|
form.amount = b["amount"]
|
||||||
form.date = parse(b["date"])
|
form.date = parse(b["date"])
|
||||||
form.payer = id_dict[b["payer_name"]]
|
form.payer = id_dict[b["payer_name"]]
|
||||||
|
|
Loading…
Reference in a new issue