diff --git a/budget/forms.py b/budget/forms.py index 2dde57da..7d6eb51e 100644 --- a/budget/forms.py +++ b/budget/forms.py @@ -152,27 +152,35 @@ class BillForm(Form): class MemberForm(Form): name = TextField(_("Name"), validators=[Required()]) + weight = CommaDecimalField(_("Weight"), default=1) submit = SubmitField(_("Add")) - def __init__(self, project, *args, **kwargs): + def __init__(self, project, edit=False, *args, **kwargs): super(MemberForm, self).__init__(*args, **kwargs) self.project = project + self.edit = edit def validate_name(form, field): if field.data == form.name.default: raise ValidationError(_("User name incorrect")) - if Person.query.filter(Person.name == field.data)\ - .filter(Person.project == form.project)\ - .filter(Person.activated == True).all(): + if (not form.edit and Person.query.filter( + Person.name == field.data, + Person.project == form.project, + Person.activated == True).all()): raise ValidationError(_("This project already have this member")) def save(self, project, person): # if the user is already bound to the project, just reactivate him person.name = self.name.data person.project = project + person.weight = self.weight.data return person + def fill(self, member): + self.name.data = member.name + self.weight.data = member.weight + class InviteForm(Form): emails = TextAreaField(_("People to notify")) diff --git a/budget/migrations/versions/26d6a218c329_.py b/budget/migrations/versions/26d6a218c329_.py new file mode 100644 index 00000000..859b9af0 --- /dev/null +++ b/budget/migrations/versions/26d6a218c329_.py @@ -0,0 +1,26 @@ +"""Add Person.weight column + +Revision ID: 26d6a218c329 +Revises: b9a10d5d63ce +Create Date: 2016-06-15 09:22:04.069447 + +""" + +# revision identifiers, used by Alembic. +revision = '26d6a218c329' +down_revision = 'b9a10d5d63ce' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column('person', sa.Column('weight', sa.Float(), nullable=True)) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_column('person', 'weight') + ### end Alembic commands ### diff --git a/budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py b/budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py new file mode 100644 index 00000000..5542146c --- /dev/null +++ b/budget/migrations/versions/f629c8ef4ab0_initialize_all_members_weights_to_1.py @@ -0,0 +1,39 @@ +"""Initialize all members weights to 1 + +Revision ID: f629c8ef4ab0 +Revises: 26d6a218c329 +Create Date: 2016-06-15 09:40:30.400862 + +""" + +# revision identifiers, used by Alembic. +revision = 'f629c8ef4ab0' +down_revision = '26d6a218c329' + +from alembic import op +import sqlalchemy as sa + +# Snapshot of the person table +person_helper = sa.Table( + 'person', sa.MetaData(), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('project_id', sa.String(length=64), nullable=True), + sa.Column('name', sa.UnicodeText(), nullable=True), + sa.Column('activated', sa.Boolean(), nullable=True), + sa.Column('weight', sa.Float(), nullable=True), + sa.ForeignKeyConstraint(['project_id'], ['project.id'], ), + sa.PrimaryKeyConstraint('id') +) + + +def upgrade(): + op.execute( + person_helper.update() + .where(person_helper.c.weight == None) + .values(weight=1) + ) + + +def downgrade(): + # Downgrade path is not possible, because information has been lost. + pass diff --git a/budget/models.py b/budget/models.py index 727200f1..852b3e19 100644 --- a/budget/models.py +++ b/budget/models.py @@ -40,8 +40,9 @@ class Project(db.Model): bills = Bill.query.filter(Bill.owers.contains(person)) for bill in bills.all(): if person != bill.payer: - should_pay[person] += bill.pay_each() - should_receive[bill.payer] += bill.pay_each() + share = bill.pay_each() * person.weight + should_pay[person] += share + should_receive[bill.payer] += share for person in self.members: balance = should_receive[person] - should_pay[person] @@ -49,6 +50,10 @@ class Project(db.Model): return balances + @property + def uses_weights(self): + return len([i for i in self.members if i.weight != 1]) > 0 + def get_transactions_to_settle_bill(self): """Return a list of transactions that could be made to settle the bill""" #cache value for better performance @@ -152,13 +157,14 @@ class Person(db.Model): query_class = PersonQuery - _to_serialize = ("id", "name", "activated") + _to_serialize = ("id", "name", "weight", "activated") id = db.Column(db.Integer, primary_key=True) project_id = db.Column(db.String(64), db.ForeignKey("project.id")) bills = db.relationship("Bill", backref="payer") name = db.Column(db.UnicodeText) + weight = db.Column(db.Float, default=1) activated = db.Column(db.Boolean, default=True) def has_bills(self): @@ -217,9 +223,10 @@ class Bill(db.Model): archive = db.Column(db.Integer, db.ForeignKey("archive.id")) def pay_each(self): - """Compute what each person has to pay""" + """Compute what each share has to pay""" if self.owers: - return self.amount / len(self.owers) + # FIXME: SQL might dot that more efficiently + return self.amount / sum(i.weight for i in self.owers) else: return 0 diff --git a/budget/run.py b/budget/run.py index 51670f2b..807ad12a 100644 --- a/budget/run.py +++ b/budget/run.py @@ -9,6 +9,8 @@ from raven.contrib.flask import Sentry from web import main, db, mail from api import api from utils import PrefixedWSGI +from utils import minimal_round + app = Flask(__name__) @@ -37,6 +39,9 @@ configure() app.register_blueprint(main) app.register_blueprint(api) +# custom jinja2 filters +app.jinja_env.filters['minimal_round'] = minimal_round + # db db.init_app(app) db.app = app diff --git a/budget/static/css/main.css b/budget/static/css/main.css index 97a3e195..f3fe8a0b 100644 --- a/budget/static/css/main.css +++ b/budget/static/css/main.css @@ -176,6 +176,11 @@ tr.payer_line .balance-name{ color: red; } +.edit button, .edit button:hover { + background: url('../images/edit.png') left no-repeat; + +} + .reactivate button, .reactivate button:hover { background: url('../images/reactivate.png') left no-repeat; color: white; @@ -189,6 +194,18 @@ tr.payer_line .balance-name{ position: absolute; } +.light { + opacity: 0.3; +} + +.extra-info { + display: none; +} + +tr:hover .extra-info { + display: inline; +} + .modal-body { max-height:455px; } @@ -208,4 +225,3 @@ tr.payer_line .balance-name{ .row-fluid > .offset3{margin-left:25.5%;} .row-fluid > .offset2{margin-left:17%;} .row-fluid > .offset1{margin-left:8.5%;} - diff --git a/budget/templates/edit_member.html b/budget/templates/edit_member.html new file mode 100644 index 00000000..5f097f9f --- /dev/null +++ b/budget/templates/edit_member.html @@ -0,0 +1,17 @@ +{% extends "layout.html" %} + +{% block js %} + $('#cancel-form').click(function(){location.href={{ url_for(".list_bills") }};}); +{% endblock %} + + +{% block top_menu %} +{{ _("Back to the list") }} +{% endblock %} + +{% block content %} + +
+{% endblock %} diff --git a/budget/templates/forms.html b/budget/templates/forms.html index ec735159..07e5b3d2 100644 --- a/budget/templates/forms.html +++ b/budget/templates/forms.html @@ -95,6 +95,20 @@ {{ form.name(placeholder=_("Type user name here")) }} {% endmacro %} +{% macro edit_member(form, title=True) %} + +