{{ delivery.producer }} — Ajuster le produit «{{ product.name }}»
+Conditionnement {{ product.packing }} x {{ product.unit }}
+Total commandé {{ delivery.product_wanted(product) }}
+Manquant {{ delivery.product_missing(product) }}
+ +diff --git a/copanier/__init__.py b/copanier/__init__.py
index 733d353..2c1f5e1 100644
--- a/copanier/__init__.py
+++ b/copanier/__init__.py
@@ -80,7 +80,7 @@ async def auth_required(request, response):
# Should be handler Roll side?
# In dev mode, we serve the static, but we don't have yet a way to mark static
# route as unprotected.
- if request.path.startswith('/static/'):
+ if request.path.startswith("/static/"):
return
if request.route.payload and not request.route.payload.get("unprotected"):
token = request.cookies.get("token")
@@ -198,7 +198,7 @@ async def import_products(request, response, id):
response.message(err, status="error")
response.redirect = path
return
- elif data.filename.endswith('.xlsx'):
+ elif data.filename.endswith(".xlsx"):
try:
imports.products_from_xlsx(delivery, data)
except ValueError as err:
@@ -258,15 +258,25 @@ async def place_order(request, response, id):
response.redirect = delivery_url
return
if request.method == "POST":
+ if not (user and user.is_staff) and delivery.status == delivery.CLOSED:
+ response.message("La livraison est fermée", "error")
+ response.redirect = delivery_url
+ return
form = request.form
order = Order(paid=form.bool("paid", False))
for product in delivery.products:
try:
- quantity = form.int(product.ref, 0)
+ wanted = form.int(f"wanted:{product.ref}", 0)
except HttpError:
continue
- if quantity:
- order.products[product.ref] = ProductOrder(wanted=quantity)
+ try:
+ adjustment = form.int(f"adjustment:{product.ref}", 0)
+ except HttpError:
+ adjustment = 0
+ if wanted or adjustment:
+ order.products[product.ref] = ProductOrder(
+ wanted=wanted, adjustment=adjustment
+ )
if not delivery.orders:
delivery.orders = {}
if not order.products:
@@ -344,6 +354,35 @@ async def xls_full_report(request, response, id):
response.xlsx(reports.full(delivery))
+@app.route("/livraison/{id}/ajuster/{ref}", methods=["GET", "POST"])
+async def adjust_product(request, response, id, ref):
+ delivery = Delivery.load(id)
+ delivery_url = f"/livraison/{delivery.id}"
+ user = session.user.get(None)
+ if not user or not user.is_staff:
+ response.message("Désolé, c'est dangereux par ici", "warning")
+ response.redirect = delivery_url
+ return
+ for product in delivery.products:
+ if product.ref == ref:
+ break
+ else:
+ response.message(f"Référence inconnue: {ref}")
+ response.redirect = delivery_url
+ return
+ if request.method == "POST":
+ form = request.form
+ for email, order in delivery.orders.items():
+ choice = order[product]
+ choice.adjustment = form.int(email, 0)
+ order[product] = choice
+ delivery.persist()
+ response.message(f"Le produit «{product.ref}» a bien été ajusté!")
+ response.redirect = delivery_url
+ else:
+ response.html("adjust_product.html", {"delivery": delivery, "product": product})
+
+
def configure():
config.init()
diff --git a/copanier/models.py b/copanier/models.py
index 1b32c12..447ea78 100644
--- a/copanier/models.py
+++ b/copanier/models.py
@@ -33,7 +33,6 @@ def price_field(value):
@dataclass
class Base:
-
@classmethod
def create(cls, data=None, **kwargs):
if isinstance(data, Base):
@@ -44,7 +43,8 @@ class Base:
for name, field_ in self.__dataclass_fields__.items():
value = getattr(self, name)
type_ = field_.type
- if not isinstance(value, Base): # Do not recast our classes.
+ # Do not recast our classes.
+ if not isinstance(value, Base) and value is not None:
try:
setattr(self, name, self.cast(type_, value))
except (TypeError, ValueError):
@@ -95,9 +95,9 @@ class Product(Base):
description: str = ""
url: str = ""
img: str = ""
+ packing: int = None
- @property
- def label(self):
+ def __str__(self):
out = self.name
if self.unit:
out += f" ({self.unit})"
@@ -107,7 +107,11 @@ class Product(Base):
@dataclass
class ProductOrder(Base):
wanted: int
- ordered: int = 0
+ adjustment: int = 0
+
+ @property
+ def quantity(self):
+ return self.wanted + self.adjustment
@dataclass
@@ -115,14 +119,20 @@ class Order(Base):
products: Dict[str, ProductOrder] = field(default_factory=dict)
paid: bool = False
- def get_quantity(self, product):
- choice = self.products.get(product.ref)
- return choice.wanted if choice else 0
+ def __getitem__(self, ref):
+ if isinstance(ref, Product):
+ ref = ref.ref
+ return self.products.get(ref, ProductOrder(wanted=0))
+
+ def __setitem__(self, ref, value):
+ if isinstance(ref, Product):
+ ref = ref.ref
+ self.products[ref] = value
def total(self, products):
products = {p.ref: p for p in products}
return round(
- sum(p.wanted * products[ref].price for ref, p in self.products.items()), 2
+ sum(p.quantity * products[ref].price for ref, p in self.products.items()), 2
)
@@ -131,6 +141,9 @@ class Delivery(Base):
__root__ = "delivery"
__lock__ = threading.Lock()
+ CLOSED = 0
+ OPEN = 1
+ ADJUSTMENT = 2
producer: str
from_date: datetime_field
@@ -143,6 +156,14 @@ class Delivery(Base):
orders: Dict[str, Order] = field(default_factory=dict)
id: str = field(default_factory=lambda *a, **k: uuid.uuid4().hex)
+ @property
+ def status(self):
+ if self.is_open:
+ return self.OPEN
+ if self.needs_adjustment:
+ return self.ADJUSTMENT
+ return self.CLOSED
+
@property
def total(self):
return round(sum(o.total(self.products) for o in self.orders.values()), 2)
@@ -159,6 +180,14 @@ class Delivery(Base):
def is_passed(self):
return not self.is_foreseen
+ @property
+ def has_packing(self):
+ return any(p.packing for p in self.products)
+
+ @property
+ def needs_adjustment(self):
+ return self.has_packing and any(self.product_missing(p) for p in self.products)
+
@classmethod
def init_fs(cls):
cls.get_root().mkdir(parents=True, exist_ok=True)
@@ -196,5 +225,15 @@ class Delivery(Base):
total = 0
for order in self.orders.values():
if product.ref in order.products:
- total += order.products[product.ref].wanted
+ total += order.products[product.ref].quantity
return total
+
+ def product_missing(self, product):
+ if not product.packing:
+ return 0
+ wanted = self.product_wanted(product)
+ orphan = wanted % product.packing
+ return product.packing - orphan if orphan else 0
+
+ def has_order(self, person):
+ return person.email in self.orders
diff --git a/copanier/reports.py b/copanier/reports.py
index ec25008..7bb959e 100644
--- a/copanier/reports.py
+++ b/copanier/reports.py
@@ -10,19 +10,28 @@ def summary(delivery):
wb = Workbook()
ws = wb.active
ws.title = f"{delivery.producer} {delivery.from_date.date()}"
- ws.append(["ref", "produit", "prix", "unités", "total"])
+ headers = [
+ "ref",
+ "produit",
+ "prix unitaire",
+ "quantité commandée",
+ "unité",
+ "total",
+ ]
+ ws.append(headers)
for product in delivery.products:
wanted = delivery.product_wanted(product)
ws.append(
[
product.ref,
- product.label,
+ str(product),
product.price,
wanted,
+ product.unit,
round(product.price * wanted, 2),
]
)
- ws.append(["", "", "", "Total", delivery.total])
+ ws.append(["", "", "", "", "Total", delivery.total])
return save_virtual_workbook(wb)
@@ -33,15 +42,17 @@ def full(delivery):
headers = ["ref", "produit", "prix"] + [e for e in delivery.orders] + ["total"]
ws.append(headers)
for product in delivery.products:
- row = [product.ref, product.label, product.price]
+ row = [product.ref, str(product), product.price]
for order in delivery.orders.values():
wanted = order.products.get(product.ref)
- row.append(wanted.wanted if wanted else 0)
+ row.append(wanted.quantity if wanted else 0)
row.append(delivery.product_wanted(product))
ws.append(row)
- footer = ["Total", "", ""] + [
- o.total(delivery.products) for o in delivery.orders.values()
- ] + [delivery.total]
+ footer = (
+ ["Total", "", ""]
+ + [o.total(delivery.products) for o in delivery.orders.values()]
+ + [delivery.total]
+ )
ws.append(footer)
return save_virtual_workbook(wb)
diff --git a/copanier/static/app.css b/copanier/static/app.css
index 19c9731..1b548ac 100644
--- a/copanier/static/app.css
+++ b/copanier/static/app.css
@@ -236,7 +236,6 @@ textarea {
line-height: 1rem;
background-color: #fff;
border: .05rem solid #bbc;
- border-radius: .1rem;
box-sizing: border-box;
}
@@ -276,6 +275,9 @@ input:focus,
select:focus {
box-shadow: 0 0 .1rem var(--primary-color);
}
+input[readonly] {
+ background-color: #eee;
+}
table {
border-collapse: collapse;
@@ -355,6 +357,7 @@ td.with-input {
}
td.with-input input {
width: 100%;
+ text-align: center;
}
article.delivery {
width: 100%;
@@ -363,6 +366,13 @@ article.delivery {
article.delivery th.person {
max-width: 7rem;
}
+td.missing,
+th.missing {
+ background-color: #db7734;
+}
+.missing a {
+ color: #ddd;
+}
hr {
background-color: #dbdbdb;
border: none;
diff --git a/copanier/templates/adjust_product.html b/copanier/templates/adjust_product.html
new file mode 100644
index 0000000..b46b16d
--- /dev/null
+++ b/copanier/templates/adjust_product.html
@@ -0,0 +1,23 @@
+{% extends "base.html" %}
+
+{% block body %}
+ Conditionnement {{ product.packing }} x {{ product.unit }} Total commandé {{ delivery.product_wanted(product) }} Manquant {{ delivery.product_missing(product) }}{{ delivery.producer }} — Ajuster le produit «{{ product.name }}»
+
Référence | Produit | Prix | + {% if delivery.has_packing %} +Conditionnement | + {% endif %}Total | {% for email, order in delivery.orders.items() %}@@ -24,20 +27,28 @@ {% for product in delivery.products %} | ||||
---|---|---|---|---|---|---|---|---|---|
{{ product.ref }} | -{{ product.label }} | +{{ product }} | {{ product.price }} € | -{{ delivery.product_wanted(product) }} | - {% for email, order in delivery.orders.items() %} - {% if product.ref in order.products %} -{{ order.products[product.ref].wanted }} | - {% else %} -— | + {% if delivery.has_packing %} +{{ product.packing or '—'}} | + {% endif %} ++ {{ delivery.product_wanted(product) }} + {% if request.user.is_staff %}{% endif %} + {% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }}) + {% if request.user.is_staff %}{% endif %} {% endif %} + | + {% for email, order in delivery.orders.items() %} +{{ order[product.ref].quantity or "—" }} | {% endfor %}
Total | — | — | -{{ delivery.total }} € | + {% if delivery.has_packing %} +— | + {% endif %} +{{ delivery.total }} € | {% for email, order in delivery.orders.items() %}{{ order.total(delivery.products) }} € | {% endfor %} @@ -48,7 +59,7 @@