diff --git a/ihatemoney/models.py b/ihatemoney/models.py index 10615d42..8c28c9c0 100644 --- a/ihatemoney/models.py +++ b/ihatemoney/models.py @@ -114,12 +114,13 @@ class Project(db.Model): 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) - for ower in bill.owers: - should_pay[ower.id] += ( - ower.weight * bill.converted_amount / total_weight - ) + if not bill.archive: + should_receive[bill.payer.id] += bill.converted_amount + total_weight = sum(ower.weight for ower in bill.owers) + for ower in bill.owers: + should_pay[ower.id] += ( + ower.weight * bill.converted_amount / total_weight + ) for person in self.members: balance = should_receive[person.id] - should_pay[person.id] @@ -678,7 +679,8 @@ class Bill(db.Model): original_currency = db.Column(db.String(3)) converted_amount = db.Column(db.Float) - archive = db.Column(db.Integer, db.ForeignKey("archive.id")) + # archive = db.Column(db.Integer, db.ForeignKey("archive.id")) + archive = db.Column(db.Boolean) currency_helper = CurrencyConverter() @@ -692,6 +694,7 @@ class Bill(db.Model): payer_id: int = None, project_default_currency: str = "", what: str = "", + archive: bool = False ): super().__init__() self.amount = amount @@ -704,6 +707,7 @@ class Bill(db.Model): self.converted_amount = self.currency_helper.exchange_currency( self.amount, self.original_currency, project_default_currency ) + self.archive = archive @property def _to_serialize(self): @@ -750,6 +754,9 @@ class Bill(db.Model): f"{', '.join([o.name for o in self.owers])}>" ) + def set_archived(self): + self.archive = True + class Archive(db.Model): id = db.Column(db.Integer, primary_key=True) diff --git a/ihatemoney/static/css/main.css b/ihatemoney/static/css/main.css index 4c61fbea..64bdbd8c 100644 --- a/ihatemoney/static/css/main.css +++ b/ihatemoney/static/css/main.css @@ -295,6 +295,7 @@ footer .footer-left { .bill-actions > form > .delete, .bill-actions > .edit, +.bill-actions > form > .archive, .bill-actions > .show { font-size: 0px; display: block; @@ -314,6 +315,10 @@ footer .footer-left { background: url("../images/edit.png") no-repeat right; } +.bill-actions > form > .archive { + background: url("../images/archive.png") no-repeat right; +} + .bill-actions > .show { background: url("../images/show.png") no-repeat right; } diff --git a/ihatemoney/static/images/archive.png b/ihatemoney/static/images/archive.png new file mode 100644 index 00000000..c7ac1c70 Binary files /dev/null and b/ihatemoney/static/images/archive.png differ diff --git a/ihatemoney/templates/list_bills.html b/ihatemoney/templates/list_bills.html index 52116d5e..95097c2e 100644 --- a/ihatemoney/templates/list_bills.html +++ b/ihatemoney/templates/list_bills.html @@ -117,6 +117,7 @@ {% endif %} + {{ static_include("images/plus.svg") | safe }} {{ _("Add a new bill") }} @@ -126,6 +127,7 @@ {% if bills.total > 0 %} +

Active Bills

{% for (weights, bill) in bills.items %} + {% if bill.archive == false %} + {% endif %} {% endfor %}
{{ _("When?") }} @@ -137,6 +139,7 @@
+
+ {{ csrf_form.csrf_token }} + +
{% if bill.external_link %} {{ _('show') }} {% endif %}
diff --git a/ihatemoney/templates/list_bills_archived.html b/ihatemoney/templates/list_bills_archived.html new file mode 100644 index 00000000..f1081378 --- /dev/null +++ b/ihatemoney/templates/list_bills_archived.html @@ -0,0 +1,180 @@ +{% extends "sidebar_table_layout.html" %} + +{%- macro weighted_bill_amount(bill, weights, currency=bill.original_currency, amount=bill.amount) %} + {{ amount|currency(currency) }} + {%- if weights != 1.0 %} + ({{ _("%(amount)s each", amount=(amount / weights)|currency(currency)) }}) + {%- endif -%} +{% endmacro -%} + +{% block title %} - {{ g.project.name }}{% endblock %} +{% block js %} + {% if add_bill %} $('#new-bill > a').click(); {% endif %} + + // ask for confirmation before removing an user + $('.action.delete').each(function(){ + var link = $(this).find('button'); + link.click(function(){ + if ($(this).hasClass("confirm")){ + return true; + } + $(this).html("{{_("you sure?")}}"); + $(this).addClass("confirm"); + return false; + }); + }); + + var highlight_owers = function(){ + var ower_ids = $(this).attr("owers").split(','); + var payer_id = $(this).attr("payer"); + $.each(ower_ids, function(i, val){ + $('#bal-member-'+val).addClass("ower_line"); + }); + $("#bal-member-"+payer_id).addClass("payer_line"); + }; + + var unhighlight_owers = function(){ + $('[id^="bal-member-"]').removeClass("ower_line payer_line"); + }; + + $('#bill_table tbody tr').hover(highlight_owers, unhighlight_owers); + +{% endblock %} + +{% block sidebar %} + +
+ {% if g.lang == 'fr' %} + + {{ static_include("images/read.svg") | safe }} + Voir la BD explicative + + {% endif %} + + {{ static_include("images/paper-plane.svg") | safe }} + {{ _("Invite people") }} + +
+{% endblock %} + +{% block content %} + +
+{% if bills.pages > 1 %} + +{% endif %} + + + + {{ static_include("images/plus.svg") | safe }} + {{ _("Add a new bill") }} + + +
+ + {% if bills.total > 0 %} +

Archived Bills

+ + + + + + {% for (weights, bill) in bills.items %} + {% if bill.archive == true %} + + + + + + + + + {% endif %} + {% endfor %} + +
{{ _("When?") }} + {{ _("Who paid?") }} + {{ _("For what?") }} + {{ _("For whom?") }} + {{ _("How much?") }} + {{ _("Actions") }}
+ + {{ bill.date }} + + {{ bill.payer }}{{ bill.what }}{% if bill.owers|length == g.project.members|length -%} + {{ _("Everyone") }} + {%- elif bill.owers|length > g.project.members|length / 2 + 1 -%} + {{ _("Everyone but %(excluded)s", excluded=g.project.members|reject('in', bill.owers)|join(', ', 'name')) }} + {%- else -%} + {{ bill.owers|join(', ', 'name') }} + {%- endif %} + + {{ weighted_bill_amount(bill, weights) }} + + + {{ _('edit') }} +
+ {{ csrf_form.csrf_token }} + +
+
+ {{ csrf_form.csrf_token }} + +
+ {% if bill.external_link %} + {{ _('show') }} + {% endif %} +
+ {% else %} +
+
+
+ {{ static_include("images/bill.svg") | safe }} +

{{ _('No bills')}}

+

+ {{ _("Nothing to list yet.")}}
+ {{ _("You probably want to") }} + {%- if g.project.members %} + {{- _("add a bill") -}} + ? + {% else %} + {{- _('add participants') -}} + ? + {%- endif -%} +

+
+
+ {% endif %} +{% endblock %} diff --git a/ihatemoney/web.py b/ihatemoney/web.py index c2f19c06..dc08b9c0 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -645,6 +645,31 @@ def list_bills(): current_view="list_bills", ) +@main.route("//archived") +def list_bills_archived(): + bill_form = get_billform_for(g.project) + # Used for CSRF validation + csrf_form = EmptyForm() + # set the last selected payer as default choice if exists + if "last_selected_payer" in session: + bill_form.payer.data = session["last_selected_payer"] + + # Each item will be a (weight_sum, Bill) tuple. + # TODO: improve this awkward result using column_property: + # https://docs.sqlalchemy.org/en/14/orm/mapped_sql_expr.html. + weighted_bills = g.project.get_bill_weights_ordered().paginate( + per_page=100, error_out=True + ) + + return render_template( + "list_bills_archived.html", + bills=weighted_bills, + member_form=MemberForm(g.project), + bill_form=bill_form, + csrf_form=csrf_form, + add_bill=request.values.get("add_bill", False), + current_view="list_bills", + ) @main.route("//members/add", methods=["GET", "POST"]) def add_member(): @@ -766,6 +791,29 @@ def delete_bill(bill_id): return redirect(url_for(".list_bills")) +@main.route("//archive/", methods=["POST"]) +def archive_bill(bill_id): + form = EmptyForm() + if not form.validate(): + flash(format_form_errors(form, _("Error archiving bill")), category="danger") + return redirect(url_for(".list_bills")) + + bill = Bill.query.get(g.project, bill_id) + if not bill: + return redirect(url_for(".list_bills")) + + if bill.archive == True: + bill.archive = False + else: + bill.archive = True + db.session.commit() + + if (bill.archive == True): + flash(_("The bill has been archived")) + return redirect(url_for(".list_bills_archived")) + else: + flash(_("The bill has been unarchived")) + return redirect(url_for(".list_bills")) @main.route("//edit/", methods=["GET", "POST"]) def edit_bill(bill_id):