From fc630e90616a7a088a1345da0e54da478cb12e34 Mon Sep 17 00:00:00 2001 From: Midgard Date: Sun, 27 Mar 2022 19:33:14 +0200 Subject: [PATCH 1/4] Support marking items as paid --- app/models/orderitem.py | 9 +++++++ app/templates/order.html | 25 +++++++++++++----- app/views/order.py | 56 ++++++++++++++++++++++++++-------------- 3 files changed, 64 insertions(+), 26 deletions(-) diff --git a/app/models/orderitem.py b/app/models/orderitem.py index b865b51..6e85e6c 100644 --- a/app/models/orderitem.py +++ b/app/models/orderitem.py @@ -73,3 +73,12 @@ class OrderItem(db.Model): if user and (user.is_admin() or user == self.order.courier): return True return False + + # pylint: disable=W0613 + def can_modify_payment(self, order_id: int, user_id: int) -> bool: + if int(self.order_id) != int(order_id): + return False + user = User.query.filter(User.id == user_id).first() + if user and (user.is_admin() or user == self.order.courier): + return True + return False diff --git a/app/templates/order.html b/app/templates/order.html index 8ac6f4e..60e1cf5 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -267,6 +267,7 @@

Items per person

+
@@ -276,12 +277,20 @@ @@ -331,8 +328,6 @@ On selected: - - {% if order.is_closed() %}      From 5a9d9c1d31cf149b6d3acc9d109c172103c644b6 Mon Sep 17 00:00:00 2001 From: Midgard Date: Fri, 22 Apr 2022 01:25:01 +0200 Subject: [PATCH 4/4] Update prices per person on key up After user feedback --- app/templates/order_prices.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/templates/order_prices.html b/app/templates/order_prices.html index bbb9ca5..d5eec77 100644 --- a/app/templates/order_prices.html +++ b/app/templates/order_prices.html @@ -119,12 +119,14 @@ $(".noscript").css("display", "none"); $(".script").css("display", "unset"); - $("#per_dish input").on("change", e => { + function updatePerPersonPrices(e) { console.log(e.target); for (let item_id of e.target.dataset.forItems.split(",")) { $("#item_" + item_id).val(e.target.value); } - }); + }; + $("#per_dish input").on("change", updatePerPersonPrices); + $("#per_dish input").on("keyup", updatePerPersonPrices); }); {% endblock %}
TotalNameItems
{% set paid = order_items | map(attribute="paid") | all %} - + {% set can_modify_payment = True %} + {% for order_item in order_items %} + {% if not order_item.can_modify_payment(order.id, current_user.id) %} + {% set can_modify_payment = False %} + {% endif %} + {% endfor %} + - {{ order_items | map(attribute="price") | sum | euro }} + + {{ order_items | map(attribute="price") | sum | euro }} + - {% if paid %}paid{% endif %} + {% if paid %}{% endif %} {{ user_name }} @@ -315,10 +324,12 @@ + {% endblock %} diff --git a/app/views/order.py b/app/views/order.py index 49ca679..ad2ccad 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -225,29 +225,47 @@ def order_item_create(order_id: int) -> typing.Any: return redirect(url_for("order_bp.order_from_id", order_id=order_id)) -@order_bp.route("///user_paid", methods=["POST"]) +@order_bp.route("//users_paid", methods=["POST"]) @login_required # pylint: disable=R1710 -def items_user_paid(order_id: int, user_name: str) -> typing.Optional[Response]: - "Indicate payment status for a user in an order" - user = User.query.filter(User.username == user_name).first() - items: typing.List[OrderItem] = [] - if user: - items = OrderItem.query.filter( - (OrderItem.user_id == user.id) & (OrderItem.order_id == order_id) - ).all() +def items_user_paid(order_id: int) -> typing.Optional[Response]: + user_names = request.form.getlist("user_names") + if request.form.get("action") == "mark_paid": + return set_items_paid(order_id, user_names, True) + elif request.form.get("action") == "mark_unpaid": + return set_items_paid(order_id, user_names, False) else: - items = OrderItem.query.filter( - (OrderItem.user_name == user_name) & (OrderItem.order_id == order_id) - ).all() - current_order = Order.query.filter(Order.id == order_id).first() - if current_order.courier_id == current_user.id or current_user.admin: + abort(404) + +def set_items_paid(order_id: int, user_names: typing.Iterable[str], paid: bool): + total_paid_items = 0 + total_failed_items = 0 + for user_name in user_names: + user = User.query.filter(User.username == user_name).first() + items: typing.List[OrderItem] = [] + if user: + items = OrderItem.query.filter( + (OrderItem.user_id == user.id) & (OrderItem.order_id == order_id) + ).all() + else: + items = OrderItem.query.filter( + (OrderItem.user_name == user_name) & (OrderItem.order_id == order_id) + ).all() + for item in items: - item.paid = True - db.session.commit() - flash("Paid %d items for %s" % (len(items), item.for_name), "success") - return redirect(url_for("order_bp.order_from_id", order_id=order_id)) - abort(404) + if item.can_modify_payment(order_id, current_user.id): + if item.paid != paid: + item.paid = paid + total_paid_items += 1 + else: + total_failed_items += 1 + + db.session.commit() + if total_failed_items == 0: + flash("Marked %d items as paid" % (total_paid_items,), "success") + else: + flash("Failed to mark %d items as paid (succeeded in marking %d items as paid)" % (total_failed_items, total_paid_items), "error") + return redirect(url_for("order_bp.order_from_id", order_id=order_id)) @order_bp.route("///delete", methods=["POST"]) From 09e2d704cdfdc6c95411c17054d183750d0adef7 Mon Sep 17 00:00:00 2001 From: Midgard Date: Fri, 22 Apr 2022 01:15:54 +0200 Subject: [PATCH 2/4] Add price editor --- ...013fe95bea_create_price_modified_column.py | 21 +++ app/models/orderitem.py | 1 + app/templates/order.html | 15 +- app/templates/order_prices.html | 130 ++++++++++++++++++ app/utils.py | 17 ++- app/views/order.py | 51 ++++++- 6 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 app/migrations/versions/55013fe95bea_create_price_modified_column.py create mode 100644 app/templates/order_prices.html diff --git a/app/migrations/versions/55013fe95bea_create_price_modified_column.py b/app/migrations/versions/55013fe95bea_create_price_modified_column.py new file mode 100644 index 0000000..d4010c1 --- /dev/null +++ b/app/migrations/versions/55013fe95bea_create_price_modified_column.py @@ -0,0 +1,21 @@ +"""Create price_modified column + +Revision ID: 55013fe95bea +Revises: 9159a6fed021 +Create Date: 2022-04-22 01:00:03.729596 + +""" + +# revision identifiers, used by Alembic. +revision = '55013fe95bea' +down_revision = '9159a6fed021' + +from alembic import op +import sqlalchemy as sa + +def upgrade(): + op.add_column('order_item', sa.Column('price_modified', sa.DateTime(), nullable=True)) + + +def downgrade(): + op.drop_column('order_item', 'price_modified') diff --git a/app/models/orderitem.py b/app/models/orderitem.py index 6e85e6c..623d751 100644 --- a/app/models/orderitem.py +++ b/app/models/orderitem.py @@ -18,6 +18,7 @@ class OrderItem(db.Model): dish_id = db.Column(db.String(64), nullable=True) dish_name = db.Column(db.String(120), nullable=True) price = db.Column(db.Integer, nullable=True) + price_modified = db.Column(db.DateTime, nullable=True) paid = db.Column(db.Boolean, default=False, nullable=True) comment = db.Column(db.Text(), nullable=True) hlds_data_version = db.Column(db.String(40), nullable=True) diff --git a/app/templates/order.html b/app/templates/order.html index 60e1cf5..02fdccd 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -307,7 +307,12 @@ {%- endif %} -
{{ item.price|euro }}
+
+ {{ item.price|euro }} + {% if item.price_modified %} + + {% endif %} +
{{ item.dish_name }}{{ "; " + item.comment if item.comment }}
{% endfor %} @@ -328,6 +333,14 @@ + + {% if order.is_closed() %} +      + + Edit prices + + {% endif %} + diff --git a/app/templates/order_prices.html b/app/templates/order_prices.html new file mode 100644 index 0000000..bbb9ca5 --- /dev/null +++ b/app/templates/order_prices.html @@ -0,0 +1,130 @@ +{% extends "layout.html" %} +{% set active_page = "orders" -%} + +{% import "utils.html" as util %} + +{% block metas %} + {{ super() }} + +{% endblock %} + +{% block container %} +
+

Edit prices

+
Only applied to order {{ order.id }}. To permanently change prices for {{ order.location_name }}, edit the HLDS location definition.
+
+ +
+
+

Per dish

+
This functionality requires JavaScript.
+
+ + + + + + + {% for dish_name, dish_quantity, dish_comment_groups in order.group_by_dish() -%} + {% set has_comments = dish_comment_groups | length > 1 or (dish_comment_groups | map("first") | any) -%} + {% for comment, items in dish_comment_groups -%} + + + {% if loop.first %} + + {% endif %} + + + + + + + {% endfor %} + {%- endfor %} + +
DishPrice
+ {{ dish_quantity }} × + {{ dish_name }} + + {{ items | length }} × + {% if comment %}{{ comment }} + {% else %}No comment + {% endif %} + + {% set price = items[0].price | euro("") %} + {% set item_ids = items | map(attribute="id") %} + € +
+
+
+ +
+

Per person

+ + + + + + {% for user_name, order_items in order.group_by_user() -%} + + + + + + {%- endfor %} + +
NameItems
{{ user_name }} +
    + {% for item in order_items %} +
  • + € + {{ item.dish_name }}{{ "; " + item.comment if item.comment }} +
  • + {% endfor %} +
+
+
+ +
+ Cancel + +
+
+ +{% endblock %} + +{% block styles %} + {{ super() }} + +{% endblock %} + +{% block scripts %} + {{ super() }} + +{% endblock %} diff --git a/app/utils.py b/app/utils.py index 91c9793..80556f2 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,16 +1,25 @@ "Script which contains several utils for Haldis" -from typing import Iterable +import re +from typing import Iterable, Optional -def euro_string(value: int) -> str: +def euro_string(value: int, unit="€ ") -> str: """ Convert cents to string formatted euro """ euro, cents = divmod(value, 100) if cents: - return f"€ {euro}.{cents:02}" - return f"€ {euro}" + return f"{unit}{euro}.{cents:02}" + return f"{unit}{euro}" + + +def parse_euro_string(value: str) -> Optional[int]: + m = re.fullmatch("(?:€ ?)?([0-9]+)(?:[.,]([0-9]+))?", value) + if not m: + return None + cents_02 = "{:0<2.2}".format(m.group(2)) if m.group(2) else "00" + return int(m.group(1)) * 100 + int(cents_02) def price_range_string(price_range, include_upper=False): diff --git a/app/views/order.py b/app/views/order.py index ad2ccad..cd3847a 100644 --- a/app/views/order.py +++ b/app/views/order.py @@ -1,5 +1,6 @@ "Script to generate the order related views of Haldis" import random +import re import typing from datetime import datetime @@ -11,7 +12,7 @@ from forms import AnonOrderItemForm, OrderForm, OrderItemForm from hlds.definitions import location_definition_version, location_definitions from models import Order, OrderItem, User, db from notification import post_order_to_webhook -from utils import ignore_none +from utils import ignore_none, parse_euro_string from werkzeug.wrappers import Response order_bp = Blueprint("order_bp", "order") @@ -323,6 +324,54 @@ def close_order(order_id: int) -> typing.Optional[Response]: return None +@order_bp.route("//prices", methods=["GET", "POST"]) +@login_required +def prices(order_id: int) -> typing.Optional[Response]: + order = Order.query.filter(Order.id == order_id).first() + if order is None: + abort(404) + if ( + current_user.is_anonymous() or + not (current_user.is_admin() or current_user.id == order.courier_id) + ): + flash("Only the courier can edit prices.", "error") + return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + if not order.is_closed(): + flash("Cannot modify prices until the order is closed.", "error") + return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + + if request.method == "GET": + return render_template( + "order_prices.html", + order=order, + ) + else: + new_prices = {} + + for key, value in request.form.items(): + m = re.fullmatch("item_([0-9]+)", key) + if not m: + continue + item_id = int(m.group(1)) + + price = parse_euro_string(value) + if not price: + flash(f"Could not recognize '{value}' as a price") + continue + + new_prices[item_id] = price + + for item in order.items: + new_price = new_prices.get(item.id) + if new_price is not None and new_price != item.price: + item.price = new_price + item.price_modified = datetime.now() + db.session.commit() + + return redirect(url_for("order_bp.order_from_id", order_id=order_id)) + + + def select_user(items) -> typing.Optional[User]: "Select a random user from those who are signed up for the order" user = None From ced04acb2e2ec63fea5e0af5ccc66e682a10430a Mon Sep 17 00:00:00 2001 From: Midgard Date: Fri, 22 Apr 2022 01:17:32 +0200 Subject: [PATCH 3/4] Remove TODO buttons --- app/templates/order.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/templates/order.html b/app/templates/order.html index 02fdccd..7285ce8 100644 --- a/app/templates/order.html +++ b/app/templates/order.html @@ -316,9 +316,6 @@
{{ item.dish_name }}{{ "; " + item.comment if item.comment }}
{% endfor %} -
  • - -