From 165427bf8f0861e16bc46121aaa1522e99d0514d Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 13 Apr 2019 11:14:59 +0200 Subject: [PATCH 1/3] WIP: deal with product packing --- copanier/models.py | 15 ++++++++++++++- copanier/static/app.css | 4 ++++ copanier/templates/delivery.html | 13 +++++++++++-- copanier/templates/edit_delivery.html | 2 ++ copanier/templates/place_order.html | 6 ++++-- tests/test_views.py | 4 ++-- 6 files changed, 37 insertions(+), 7 deletions(-) diff --git a/copanier/models.py b/copanier/models.py index 1b32c12..ceef0f1 100644 --- a/copanier/models.py +++ b/copanier/models.py @@ -44,7 +44,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,6 +96,7 @@ class Product(Base): description: str = "" url: str = "" img: str = "" + packing: int = None @property def label(self): @@ -159,6 +161,10 @@ 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) + @classmethod def init_fs(cls): cls.get_root().mkdir(parents=True, exist_ok=True) @@ -198,3 +204,10 @@ class Delivery(Base): if product.ref in order.products: total += order.products[product.ref].wanted 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 diff --git a/copanier/static/app.css b/copanier/static/app.css index 19c9731..087f87d 100644 --- a/copanier/static/app.css +++ b/copanier/static/app.css @@ -363,6 +363,10 @@ article.delivery { article.delivery th.person { max-width: 7rem; } +td.missing, +th.missing { + background-color: #db7734; +} hr { background-color: #dbdbdb; border: none; diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index bde0e29..0133af1 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -10,6 +10,9 @@ Référence Produit Prix + {% if delivery.has_packing %} + Lot + {% endif %} Total {% for email, order in delivery.orders.items() %} @@ -26,7 +29,10 @@ {{ product.ref }} {{ product.label }} {{ product.price }} € - {{ delivery.product_wanted(product) }} + {% if delivery.has_packing %} + {{ product.packing or '—'}} + {% endif %} + {{ delivery.product_wanted(product) }}{% if delivery.product_missing(product) %} ({{ delivery.product_missing(product) }}){% endif %} {% for email, order in delivery.orders.items() %} {% if product.ref in order.products %} {{ order.products[product.ref].wanted }} @@ -37,7 +43,10 @@ {% endfor %} Total—— - {{ delivery.total }} € + {% if delivery.has_packing %} + — + {% endif %} + {{ delivery.total }} € {% for email, order in delivery.orders.items() %} {{ order.total(delivery.products) }} € {% endfor %} diff --git a/copanier/templates/edit_delivery.html b/copanier/templates/edit_delivery.html index 456da85..cd3dd31 100644 --- a/copanier/templates/edit_delivery.html +++ b/copanier/templates/edit_delivery.html @@ -52,6 +52,8 @@
Conditionnement d'une unité: 1kg, 33cl…
description
Plus de détails sur le produit.
+
packing
+
Contionnement final pour grouper les commandes, le cas échéant, en nombre d'unités.
url
Une URL éventuelle pointant sur une page présentant le produit.
img
diff --git a/copanier/templates/place_order.html b/copanier/templates/place_order.html index dd56c10..468445c 100644 --- a/copanier/templates/place_order.html +++ b/copanier/templates/place_order.html @@ -6,7 +6,7 @@ {% include "includes/delivery_head.html" %}
- + {% for product in delivery.products %} - + + {{ product.packing or "—" }}{% if delivery.product_missing(product) %} (manque {{ delivery.product_missing(product) }}){% endif %} + {% endfor %}
ProduitPrixQuantité
ProduitPrixLotQuantité
{{ product.label }} @@ -16,7 +16,9 @@ {% endwith %} {% endif %}

{{ product.price }} €
{{ product.price }} €

Total: {{ order.total(delivery.products) if order else 0 }} €

diff --git a/tests/test_views.py b/tests/test_views.py index 98e4aac..24a3ff2 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -101,6 +101,6 @@ async def test_export_products(client, delivery): resp = await client.get(f"/livraison/{delivery.id}/exporter/produits") wb = load_workbook(filename=BytesIO(resp.body)) assert list(wb.active.values) == [ - ("name", "ref", "price", "unit", "description", "url", "img"), - ("Lait", "123", 1.5, None, None, None, None), + ("name", "ref", "price", "unit", "description", "url", "img", "packing"), + ("Lait", "123", 1.5, None, None, None, None, None), ] From 425222a0a2cb36cbe22bc201af47bda12eb1f7c0 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 13 Apr 2019 23:48:37 +0200 Subject: [PATCH 2/3] Allow to define ProductOrder.ajustment when product needs adjustment --- copanier/__init__.py | 20 +++++-- copanier/models.py | 36 ++++++++--- copanier/reports.py | 23 +++++-- copanier/static/app.css | 5 +- copanier/templates/delivery.html | 14 ++--- copanier/templates/edit_delivery.html | 12 ++-- copanier/templates/emails/order_summary.txt | 4 +- .../templates/includes/delivery_head.html | 2 +- .../templates/includes/delivery_list.html | 2 +- copanier/templates/includes/order_button.html | 9 +++ .../templates/includes/order_summary.html | 4 +- copanier/templates/place_order.html | 19 +++++- setup.cfg | 14 ++++- tests/test_models.py | 17 ++++++ tests/test_reports.py | 26 ++++++++ tests/test_views.py | 60 +++++++++++++++++-- 16 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 copanier/templates/includes/order_button.html create mode 100644 tests/test_reports.py diff --git a/copanier/__init__.py b/copanier/__init__.py index 733d353..141d501 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: diff --git a/copanier/models.py b/copanier/models.py index ceef0f1..f0d87f3 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): @@ -109,7 +108,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 @@ -117,14 +120,15 @@ 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 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 ) @@ -133,6 +137,9 @@ class Delivery(Base): __root__ = "delivery" __lock__ = threading.Lock() + CLOSED = 0 + OPEN = 1 + ADJUSTMENT = 2 producer: str from_date: datetime_field @@ -145,6 +152,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) @@ -165,6 +180,10 @@ class Delivery(Base): 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) @@ -202,7 +221,7 @@ 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): @@ -211,3 +230,6 @@ class Delivery(Base): 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..dde7e2a 100644 --- a/copanier/reports.py +++ b/copanier/reports.py @@ -10,7 +10,15 @@ 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( @@ -19,10 +27,11 @@ def summary(delivery): product.label, 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) @@ -36,12 +45,14 @@ def full(delivery): row = [product.ref, product.label, 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 087f87d..4e1ceac 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%; diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index 0133af1..8ac5824 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block body %} -

{{ delivery.producer }} {% if delivery.is_open %}Gérer ma commande{% endif %}

+

{{ delivery.producer }} {% include "includes/order_button.html" %}

{% include "includes/delivery_head.html" %}
@@ -11,7 +11,7 @@ {% if delivery.has_packing %} - + {% endif %} {% for email, order in delivery.orders.items() %} @@ -32,13 +32,9 @@ {% if delivery.has_packing %} {% endif %} - {{ delivery.product_wanted(product) }}{% if delivery.product_missing(product) %} ({{ delivery.product_missing(product) }}){% endif %} + {{ delivery.product_wanted(product) }}{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }}){% endif %} {% for email, order in delivery.orders.items() %} - {% if product.ref in order.products %} - - {% else %} - - {% endif %} + {% endfor %} {% endfor %} @@ -57,7 +53,7 @@
  • - Rapport résumé + Bon de commande
  • Rapport complet diff --git a/copanier/templates/edit_delivery.html b/copanier/templates/edit_delivery.html index cd3dd31..e2d10b8 100644 --- a/copanier/templates/edit_delivery.html +++ b/copanier/templates/edit_delivery.html @@ -8,27 +8,27 @@ {% endif %}
    diff --git a/copanier/templates/emails/order_summary.txt b/copanier/templates/emails/order_summary.txt index 4e7ce9e..ec93e81 100644 --- a/copanier/templates/emails/order_summary.txt +++ b/copanier/templates/emails/order_summary.txt @@ -5,8 +5,8 @@ Voici le résumé de votre commande «{{ delivery.producer }}» Produit | Prix unitaire | Quantité {% for product in delivery.products %} -{% if order.get_quantity(product) %} -{{ product.name }} | {{ product.price }} € | {{ order.get_quantity(product) }} +{% if order[product].quantity %} +{{ product.name }} | {{ product.price }} € | {{ order[product].quantity }} {% endif %} {% endfor %} diff --git a/copanier/templates/includes/delivery_head.html b/copanier/templates/includes/delivery_head.html index d4b0855..d2f0920 100644 --- a/copanier/templates/includes/delivery_head.html +++ b/copanier/templates/includes/delivery_head.html @@ -2,6 +2,6 @@
  • Produits {{ delivery.description }}
  • Lieu {{ delivery.where }}
  • Date de livraison
  • -
  • {% if delivery.is_open %}Date limite de commande {% else %}Fermée{% endif %}
  • +
  • {% if delivery.status == delivery.OPEN %}Date limite de commande {% elif delivery.status == delivery.ADJUSTMENT %}Ajustement en cours{% else %}Fermée{% endif %}
  • {% if delivery.instructions %}
  • À savoir {{ delivery.instructions }}
  • {% endif %}
diff --git a/copanier/templates/includes/delivery_list.html b/copanier/templates/includes/delivery_list.html index bb0c27b..1687bd6 100644 --- a/copanier/templates/includes/delivery_list.html +++ b/copanier/templates/includes/delivery_list.html @@ -2,7 +2,7 @@
Produit PrixLotConditionnementTotal{{ product.packing or '—'}}{{ order.products[product.ref].wanted }}{{ order[product.ref].quantity or "—" }}
{% for product in delivery.products %} - {% if order.get_quantity(product) %} + {% if order[product].quantity %} - + {% endif %} {% endfor %} diff --git a/copanier/templates/place_order.html b/copanier/templates/place_order.html index 468445c..475f1e8 100644 --- a/copanier/templates/place_order.html +++ b/copanier/templates/place_order.html @@ -6,7 +6,14 @@ {% include "includes/delivery_head.html" %}
ProduitPrix unitaireQuantité
{{ product.label }}{{ product.price }} €{{ order.get_quantity(product) }}{{ product.price }} €{{ order[product].quantity }}
- + + + + {% if delivery.has_packing %} + + {% endif %} + + {% if delivery.status == delivery.ADJUSTMENT %}{% endif %} {% for product in delivery.products %} - {{ product.packing or "—" }}{% if delivery.product_missing(product) %} (manque {{ delivery.product_missing(product) }}){% endif %} - + {% if delivery.has_packing %} + {{ product.packing or "—" }}{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }}){% endif %} + {% endif %} + + {% if delivery.status == delivery.ADJUSTMENT %} + + {% endif %} + {% endfor %}
ProduitPrixLotQuantité
ProduitPrixConditionnementQuantitéAjustement
{{ product.label }} @@ -17,8 +24,14 @@ {% endif %}

{{ product.price }} €

Total: {{ order.total(delivery.products) if order else 0 }} €

diff --git a/setup.cfg b/setup.cfg index 7162f61..82e7e1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,13 +7,25 @@ packages = find: include_package_data = True install_requires = Jinja2==2.10 - minicli==0.4.4 openpyxl==2.6.1 PyJWT==1.7.1 PyYAML==5.1 roll==0.10.1 ujson==1.35 +[options.extras_require] +dev = + hupper==1.6.1 + minicli==0.4.4 + usine==0.2.2 +test = + pyquery==1.4.0 + pytest==4.3.1 + pytest-asyncio==0.10.0 +prod = + gunicorn==19.9.0 + uvloop==0.12.2 + [options.entry_points] console_scripts = diff --git a/tests/test_models.py b/tests/test_models.py index f11051c..d34a5e1 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -36,6 +36,13 @@ def test_delivery_is_open_when_order_before_is_in_the_future(delivery): assert delivery.is_open +def test_delivery_status(delivery): + delivery.order_before = now() + timedelta(hours=1) + assert delivery.status == delivery.OPEN + delivery.order_before = now() - timedelta(days=1) + assert delivery.status == delivery.CLOSED + + def test_can_create_product(): product = Product(name="Lait 1.5L", ref="123", price=1.5) assert product.ref == "123" @@ -103,3 +110,13 @@ def test_person_is_staff_if_no_staff_in_config(monkeypatch): monkeypatch.setattr(config, 'STAFF', []) person = Person(email="foo@bar.fr") assert person.is_staff + + +def test_productorder_quantity(): + choice = ProductOrder(wanted=3) + assert choice.wanted == 3 + assert choice.quantity == 3 + choice = ProductOrder(wanted=3, adjustment=2) + assert choice.quantity == 5 + choice = ProductOrder(wanted=3, adjustment=-1) + assert choice.quantity == 2 diff --git a/tests/test_reports.py b/tests/test_reports.py new file mode 100644 index 0000000..833ff7d --- /dev/null +++ b/tests/test_reports.py @@ -0,0 +1,26 @@ +from io import BytesIO + +from openpyxl import load_workbook + +from copanier import reports +from copanier.models import Order, Product, ProductOrder + + +def test_export_products(client, delivery): + delivery.products[0].packing = 6 + delivery.products.append( + Product(ref="456", name="yaourt", price="3.5", packing=4, unit="pot 125ml") + ) + delivery.products.append(Product(ref="789", name="fromage", price="9.2")) + delivery.orders["foo@bar.org"] = Order( + products={"123": ProductOrder(wanted=1), "456": ProductOrder(wanted=4)} + ) + delivery.persist() + wb = load_workbook(filename=BytesIO(reports.summary(delivery))) + assert list(wb.active.values) == [ + ("ref", "produit", "prix unitaire", "quantité commandée", "unité", "total"), + ("123", "Lait", 1.5, 1, None, 1.5), + ("456", "yaourt (pot 125ml)", 3.5, 4, "pot 125ml", 14), + ("789", "fromage", 9.2, 0, None, 0), + (None, None, None, None, "Total", 15.5), + ] diff --git a/tests/test_views.py b/tests/test_views.py index 24a3ff2..b7007e6 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,9 +1,11 @@ +from datetime import datetime, timedelta from io import BytesIO import pytest from openpyxl import load_workbook +from pyquery import PyQuery as pq -from copanier.models import Delivery, Order, ProductOrder +from copanier.models import Delivery, Order, ProductOrder, Product pytestmark = pytest.mark.asyncio @@ -52,7 +54,7 @@ async def test_create_delivery(client): async def test_place_order_with_session(client, delivery): delivery.persist() - body = {"123": "3"} + body = {"wanted:123": "3"} resp = await client.post(f"/livraison/{delivery.id}/commander", body=body) assert resp.status == 302 delivery = Delivery.load(id=delivery.id) @@ -79,7 +81,7 @@ async def test_place_empty_order_should_delete_previous(client, delivery): async def test_place_order_with_empty_string(client, delivery): delivery.persist() - body = {"123": ""} # User deleted the field value. + body = {"wanted:123": ""} # User deleted the field value. resp = await client.post(f"/livraison/{delivery.id}/commander", body=body) assert resp.status == 302 delivery = Delivery.load(id=delivery.id) @@ -88,7 +90,7 @@ async def test_place_order_with_empty_string(client, delivery): async def test_change_paid_status_when_placing_order(client, delivery): delivery.persist() - body = {"123": "3", "paid": 1} + body = {"wanted:123": "3", "paid": 1} resp = await client.post(f"/livraison/{delivery.id}/commander", body=body) assert resp.status == 302 delivery = Delivery.load(id=delivery.id) @@ -96,6 +98,56 @@ async def test_change_paid_status_when_placing_order(client, delivery): assert delivery.orders["foo@bar.org"].paid is True +async def test_get_place_order_with_closed_subscription(client, delivery): + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.orders["foo@bar.org"] = Order(products={"123": ProductOrder(wanted=1)}) + delivery.persist() + assert delivery.status == delivery.CLOSED + resp = await client.get(f"/livraison/{delivery.id}/commander") + doc = pq(resp.body) + assert doc('[name="wanted:123"]').attr("readonly") + assert not doc('[name="adjustment:123"]') + + +async def test_get_place_order_with_adjustment_status(client, delivery): + resp = await client.get(f"/livraison/{delivery.id}/commander") + doc = pq(resp.body) + assert not doc('[name="wanted:123"]').attr("readonly") + assert not doc('[name="adjustment:123"]') + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.products[0].packing = 6 + delivery.products.append(Product(ref="456", name="yaourt", price="3.5", packing=4)) + delivery.products.append(Product(ref="789", name="fromage", price="9.2")) + delivery.orders["foo@bar.org"] = Order( + products={"123": ProductOrder(wanted=1), "456": ProductOrder(wanted=4)} + ) + delivery.persist() + assert delivery.status == delivery.ADJUSTMENT + resp = await client.get(f"/livraison/{delivery.id}/commander") + doc = pq(resp.body) + assert doc('[name="wanted:123"]').attr("readonly") + assert doc('[name="adjustment:123"]') + assert not doc('[name="adjustment:123"]').attr("readonly") + assert doc('[name="wanted:456"]').attr("readonly") + assert doc('[name="adjustment:456"]') + # Already adjusted. + assert doc('[name="adjustment:456"]').attr("readonly") + assert doc('[name="adjustment:789"]') + # Needs no adjustment. + assert doc('[name="adjustment:789"]').attr("readonly") + + +async def test_cannot_place_order_on_closed_delivery(client, delivery, monkeypatch): + monkeypatch.setattr("copanier.config.STAFF", ["someone@else.org"]) + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.persist() + body = {"wanted:123": "3"} + resp = await client.post(f"/livraison/{delivery.id}/commander", body=body) + assert resp.status == 302 + delivery = Delivery.load(id=delivery.id) + assert not delivery.orders + + async def test_export_products(client, delivery): delivery.persist() resp = await client.get(f"/livraison/{delivery.id}/exporter/produits") From e9ec23875456620b0324a91c39f99e09af311ed3 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sun, 14 Apr 2019 16:00:07 +0200 Subject: [PATCH 3/3] Very basic "adjust product" page --- copanier/__init__.py | 29 +++++++++++ copanier/models.py | 8 +++- copanier/reports.py | 4 +- copanier/static/app.css | 3 ++ copanier/templates/adjust_product.html | 23 +++++++++ copanier/templates/delivery.html | 10 +++- .../templates/includes/order_summary.html | 2 +- copanier/templates/place_order.html | 2 +- tests/test_views.py | 48 +++++++++++++++++++ 9 files changed, 121 insertions(+), 8 deletions(-) create mode 100644 copanier/templates/adjust_product.html diff --git a/copanier/__init__.py b/copanier/__init__.py index 141d501..2c1f5e1 100644 --- a/copanier/__init__.py +++ b/copanier/__init__.py @@ -354,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 f0d87f3..447ea78 100644 --- a/copanier/models.py +++ b/copanier/models.py @@ -97,8 +97,7 @@ class Product(Base): img: str = "" packing: int = None - @property - def label(self): + def __str__(self): out = self.name if self.unit: out += f" ({self.unit})" @@ -125,6 +124,11 @@ class Order(Base): 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( diff --git a/copanier/reports.py b/copanier/reports.py index dde7e2a..7bb959e 100644 --- a/copanier/reports.py +++ b/copanier/reports.py @@ -24,7 +24,7 @@ def summary(delivery): ws.append( [ product.ref, - product.label, + str(product), product.price, wanted, product.unit, @@ -42,7 +42,7 @@ 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.quantity if wanted else 0) diff --git a/copanier/static/app.css b/copanier/static/app.css index 4e1ceac..1b548ac 100644 --- a/copanier/static/app.css +++ b/copanier/static/app.css @@ -370,6 +370,9 @@ 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 %} +
+

{{ delivery.producer }} — Ajuster le produit «{{ product.name }}»

+

Conditionnement {{ product.packing }} x {{ product.unit }}

+

Total commandé {{ delivery.product_wanted(product) }}

+

Manquant {{ delivery.product_missing(product) }}

+ + + + {% for email, order in delivery.orders.items() %} + + + + + + {% endfor %} +
PersonneCommandeAjustement
{{ email }}{{ order[product].wanted }}
+ + +
+{% endblock body %} diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index 8ac5824..2ac0e1e 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -27,12 +27,18 @@ {% for product in delivery.products %} {{ product.ref }} - {{ product.label }} + {{ product }} {{ product.price }} € {% if delivery.has_packing %} {{ product.packing or '—'}} {% endif %} - {{ delivery.product_wanted(product) }}{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }}){% 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 %} diff --git a/copanier/templates/includes/order_summary.html b/copanier/templates/includes/order_summary.html index 03064c1..cbb1bba 100644 --- a/copanier/templates/includes/order_summary.html +++ b/copanier/templates/includes/order_summary.html @@ -3,7 +3,7 @@ {% for product in delivery.products %} {% if order[product].quantity %} - {{ product.label }} + {{ product }} {{ product.price }} €{{ order[product].quantity }} {% endif %} diff --git a/copanier/templates/place_order.html b/copanier/templates/place_order.html index 475f1e8..b5d5a11 100644 --- a/copanier/templates/place_order.html +++ b/copanier/templates/place_order.html @@ -16,7 +16,7 @@ {% if delivery.status == delivery.ADJUSTMENT %}Ajustement{% endif %} {% for product in delivery.products %} - {{ product.label }} + {{ product }} {% if product.description or product.img %} {% with unique_id=loop.index %} {% include "includes/modal_product.html" %} diff --git a/tests/test_views.py b/tests/test_views.py index b7007e6..aaf0c0d 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -148,6 +148,54 @@ async def test_cannot_place_order_on_closed_delivery(client, delivery, monkeypat assert not delivery.orders +async def test_get_adjust_product(client, delivery): + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.products[0].packing = 6 + delivery.orders["foo@bar.org"] = Order( + products={"123": ProductOrder(wanted=2, adjustment=1)} + ) + delivery.persist() + assert delivery.status == delivery.ADJUSTMENT + resp = await client.get(f"/livraison/{delivery.id}/ajuster/123") + doc = pq(resp.body) + assert doc('[name="foo@bar.org"]') + assert doc('[name="foo@bar.org"]').attr("value") == '1' + + +async def test_post_adjust_product(client, delivery): + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.products[0].packing = 6 + delivery.orders["foo@bar.org"] = Order( + products={"123": ProductOrder(wanted=2)} + ) + delivery.persist() + assert delivery.status == delivery.ADJUSTMENT + body = {"foo@bar.org": "1"} + resp = await client.post(f"/livraison/{delivery.id}/ajuster/123", body=body) + assert resp.status == 302 + delivery = Delivery.load(id=delivery.id) + assert delivery.orders["foo@bar.org"].products["123"].wanted == 2 + assert delivery.orders["foo@bar.org"].products["123"].adjustment == 1 + + +async def test_only_staff_can_adjust_product(client, delivery, monkeypatch): + delivery.order_before = datetime.now() - timedelta(days=1) + delivery.products[0].packing = 6 + delivery.orders["foo@bar.org"] = Order( + products={"123": ProductOrder(wanted=2)} + ) + delivery.persist() + monkeypatch.setattr("copanier.config.STAFF", ["someone@else.org"]) + resp = await client.get(f"/livraison/{delivery.id}/ajuster/123") + assert resp.status == 302 + body = {"foo@bar.org": "1"} + resp = await client.post(f"/livraison/{delivery.id}/ajuster/123", body=body) + assert resp.status == 302 + delivery = Delivery.load(id=delivery.id) + assert delivery.orders["foo@bar.org"].products["123"].wanted == 2 + assert delivery.orders["foo@bar.org"].products["123"].adjustment == 0 + + async def test_export_products(client, delivery): delivery.persist() resp = await client.get(f"/livraison/{delivery.id}/exporter/produits")