Add price editor

This commit is contained in:
Midgard 2022-04-22 01:15:54 +02:00
parent fc630e9061
commit 09e2d704cd
Signed by: midgard
GPG key ID: 511C112F1331BBB4
6 changed files with 229 additions and 6 deletions

View file

@ -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')

View file

@ -18,6 +18,7 @@ class OrderItem(db.Model):
dish_id = db.Column(db.String(64), nullable=True) dish_id = db.Column(db.String(64), nullable=True)
dish_name = db.Column(db.String(120), nullable=True) dish_name = db.Column(db.String(120), nullable=True)
price = db.Column(db.Integer, 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) paid = db.Column(db.Boolean, default=False, nullable=True)
comment = db.Column(db.Text(), nullable=True) comment = db.Column(db.Text(), nullable=True)
hlds_data_version = db.Column(db.String(40), nullable=True) hlds_data_version = db.Column(db.String(40), nullable=True)

View file

@ -307,7 +307,12 @@
{%- endif %} {%- endif %}
</div> </div>
<div class="price_aligned">{{ item.price|euro }}</div> <div class="price_aligned">
{{ item.price|euro }}
{% if item.price_modified %}
<span class="glyphicon glyphicon-pencil" style="opacity: 0.5" title="Edited"></span>
{% endif %}
</div>
<div class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</div> <div class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</div>
</li> </li>
{% endfor %} {% endfor %}
@ -328,6 +333,14 @@
<button name="action" value="mark_unpaid" class="btn btn-sm">Mark unpaid</button> <button name="action" value="mark_unpaid" class="btn btn-sm">Mark unpaid</button>
<button disabled name="action" value="tab" class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button> <button disabled name="action" value="tab" class="btn btn-sm"><span class="glyphicon glyphicon-piggy-bank"></span> Tab (TODO)</button>
<button disabled name="action" value="qr" class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button> <button disabled name="action" value="qr" class="btn btn-sm"><span class="glyphicon glyphicon-qrcode"></span> QR code (TODO)</button>
{% if order.is_closed() %}
&nbsp; <span style="border-left: 1px solid var(--gray0); display: inline-block;">&nbsp;</span>&nbsp;
<a href="{{ url_for('order_bp.prices', order_id=order.id) }}" class="btn btn-sm">
<span class="glyphicon glyphicon-pencil"></span> Edit prices
</a>
{% endif %}
</div> </div>
</form> </form>
</div> </div>

View file

@ -0,0 +1,130 @@
{% extends "layout.html" %}
{% set active_page = "orders" -%}
{% import "utils.html" as util %}
{% block metas %}
{{ super() }}
<meta name="robots" content="noindex, nofollow">
{% endblock %}
{% block container %}
<header>
<h2 id="order-title">Edit prices</h2>
<div>Only applied to <a href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}">order {{ order.id }}</a>. To permanently change prices for {{ order.location_name }}, edit the <a href="https://git.zeus.gent/haldis/menus/-/blob/master/{{order.location_id}}.hlds">HLDS location definition</a>.</div>
</header>
<form action="{{ url_for('order_bp.prices', order_id=order.id) }}" method="post">
<div class="col-md-6" id="per_dish">
<h3>Per dish</h3>
<div class="noscript">This functionality requires JavaScript.</div>
<div class="script">
<table class="table table-condensed">
<thead>
<tr><th colspan="2">Dish</th><th>Price</th></tr>
</thead>
<tbody>
{% 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 -%}
<tr>
{% if loop.first %}
<td rowspan="{{dish_comment_groups | length }}">
<span class="quantity">{{ dish_quantity }}</span> ×
{{ dish_name }}
</td>
{% endif %}
<td>
<span class="quantity">{{ items | length }}</span> ×
{% if comment %}{{ comment }}
{% else %}<i>No comment</i>
{% endif %}
</td>
<td>
{% set price = items[0].price | euro("") %}
{% set item_ids = items | map(attribute="id") %}
<input type="text" data-for-items="{{ item_ids | join(",") }}" value="{{ price }}">
</td>
</tr>
{% endfor %}
{%- endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6" id="per_person">
<h3>Per person</h3>
<table class="table table-condensed">
<thead>
<tr><th>Name</th><th>Items</th></tr>
</thead>
<tbody>
{% for user_name, order_items in order.group_by_user() -%}
<tr>
<td>{{ user_name }}</td>
<td class="items">
<ul>
{% for item in order_items %}
<li class="{{ 'paid' if item.paid }}">
<input type="text" value="{{ item.price|euro("") }}" name="item_{{ item.id }}" id="item_{{ item.id }}">
<span class="item_description">{{ item.dish_name }}{{ "; " + item.comment if item.comment }}</span>
</li>
{% endfor %}
</ul>
</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
<div>
<a href="{{ url_for('order_bp.order_from_id', order_id=order.id) }}" class="btn btn-sm">Cancel</a>
<button class="btn btn-sm btn-primary">Apply</button>
</div>
</form>
{% endblock %}
{% block styles %}
{{ super() }}
<style>
.script {
display: none;
}
#per_dish ul, #per_person ul {
list-style-type: none;
padding: 0;
}
#per_dish input, #per_person input {
width: 3em;
}
</style>
{% endblock %}
{% block scripts %}
{{ super() }}
<script type="text/javascript">
"use strict";
$(window).on("load", () => {
$(".noscript").css("display", "none");
$(".script").css("display", "unset");
$("#per_dish input").on("change", e => {
console.log(e.target);
for (let item_id of e.target.dataset.forItems.split(",")) {
$("#item_" + item_id).val(e.target.value);
}
});
});
</script>
{% endblock %}

View file

@ -1,16 +1,25 @@
"Script which contains several utils for Haldis" "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 Convert cents to string formatted euro
""" """
euro, cents = divmod(value, 100) euro, cents = divmod(value, 100)
if cents: if cents:
return f"{euro}.{cents:02}" return f"{unit}{euro}.{cents:02}"
return f"{euro}" 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): def price_range_string(price_range, include_upper=False):

View file

@ -1,5 +1,6 @@
"Script to generate the order related views of Haldis" "Script to generate the order related views of Haldis"
import random import random
import re
import typing import typing
from datetime import datetime from datetime import datetime
@ -11,7 +12,7 @@ from forms import AnonOrderItemForm, OrderForm, OrderItemForm
from hlds.definitions import location_definition_version, location_definitions from hlds.definitions import location_definition_version, location_definitions
from models import Order, OrderItem, User, db from models import Order, OrderItem, User, db
from notification import post_order_to_webhook 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 from werkzeug.wrappers import Response
order_bp = Blueprint("order_bp", "order") order_bp = Blueprint("order_bp", "order")
@ -323,6 +324,54 @@ def close_order(order_id: int) -> typing.Optional[Response]:
return None return None
@order_bp.route("/<order_id>/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]: def select_user(items) -> typing.Optional[User]:
"Select a random user from those who are signed up for the order" "Select a random user from those who are signed up for the order"
user = None user = None