Merge branch 'packing' into 'master'

Product.packing management

See merge request ybon/copanier!1
This commit is contained in:
Yohan 2019-04-14 16:27:09 +02:00
commit 35e68bd2ac
17 changed files with 371 additions and 57 deletions

View file

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

View file

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

View file

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

View file

@ -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;

View file

@ -0,0 +1,23 @@
{% extends "base.html" %}
{% block body %}
<article>
<h3><a href="/livraison/{{ delivery.id }}">{{ delivery.producer }}</a> — Ajuster le produit «{{ product.name }}»</h3>
<p><strong>Conditionnement</strong> {{ product.packing }} x {{ product.unit }}</p>
<p><strong>Total commandé</strong> {{ delivery.product_wanted(product) }}</p>
<p><strong>Manquant</strong> {{ delivery.product_missing(product) }}</p>
<form method="post">
<table>
<tr><th>Personne</th><th class="amount">Commande</th><th class="amount">Ajustement</th></tr>
{% for email, order in delivery.orders.items() %}
<tr>
<td>{{ email }}</td>
<td>{{ order[product].wanted }}</td>
<td class="with-input"><input type="number" min=0 name="{{ email }}" value="{{ order[product].adjustment }}"></td>
</tr>
{% endfor %}
</table>
<input type="submit" value="Enregistrer">
</form>
</article>
{% endblock body %}

View file

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% block body %}
<h3>{{ delivery.producer }} {% if delivery.is_open %}<a class="button" href="/livraison/{{ delivery.id }}/commander">Gérer ma commande</a>{% endif %}</h3>
<h3>{{ delivery.producer }} {% include "includes/order_button.html" %}</h3>
{% include "includes/delivery_head.html" %}
<article class="delivery">
<table class="delivery">
@ -10,6 +10,9 @@
<th class="ref">Référence</th>
<th class="product">Produit</th>
<th class="price">Prix</th>
{% if delivery.has_packing %}
<th class="packing">Conditionnement</th>
{% endif %}
<th class="amount">Total</th>
{% for email, order in delivery.orders.items() %}
<th class="person{% if delivery.is_passed and not order.paid %} not-paid{% endif %}">
@ -24,20 +27,28 @@
{% for product in delivery.products %}
<tr>
<td class="ref">{{ product.ref }}</td>
<th class="product">{{ product.label }}</th>
<th class="product">{{ product }}</th>
<td>{{ product.price }} €</td>
<th>{{ delivery.product_wanted(product) }}</th>
{% for email, order in delivery.orders.items() %}
{% if product.ref in order.products %}
<td>{{ order.products[product.ref].wanted }}</td>
{% else %}
<td></td>
{% if delivery.has_packing %}
<td class="packing">{{ product.packing or '—'}}</td>
{% endif %}
<th{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} class="missing" title="Les commandes individuelles ne correspondent pas aux conditionnements"{% endif %}>
{{ delivery.product_wanted(product) }}
{% if request.user.is_staff %}<a href="/livraison/{{ delivery.id }}/ajuster/{{ product.ref }}">{% endif %}
{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} ({{ delivery.product_missing(product) }})
{% if request.user.is_staff %}</a>{% endif %}
{% endif %}
</th>
{% for email, order in delivery.orders.items() %}
<td>{{ order[product.ref].quantity or "—" }}</td>
{% endfor %}
</tr>
{% endfor %}
<tr><th class="total"><i class="icon-pricetags"></i> Total</th><td></td><td></td>
<td class="total">{{ delivery.total }} €</td>
{% if delivery.has_packing %}
<td></td>
{% endif %}
<th class="total">{{ delivery.total }} €</th>
{% for email, order in delivery.orders.items() %}
<td>{{ order.total(delivery.products) }} €</td>
{% endfor %}
@ -48,7 +59,7 @@
<hr>
<ul class="toolbox">
<li>
<a href="/livraison/{{ delivery.id }}/rapport.xlsx"><i class="icon-magnifying-glass"></i> Rapport résumé</a>
<a href="/livraison/{{ delivery.id }}/rapport.xlsx"><i class="icon-ribbon"></i> Bon de commande</a>
</li>
<li>
<a href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-grid"></i> Rapport complet</a>

View file

@ -8,27 +8,27 @@
{% endif %}
<form method="post">
<label>
<h5>Producteur</h5>
<p>Producteur</p>
<input type="text" name="producer" value="{{ delivery.producer or '' }}" required>
</label>
<label>
<h5>Description des produits</h5>
<p>Description des produits</p>
<input type="text" name="description" value="{{ delivery.description or '' }}" required>
</label>
<label>
<h5>Lieu</h5>
<p>Lieu</p>
<input type="text" name="where" value="{{ delivery.where or '' }}" required>
</label>
<label>
<h5>Date de livraison</h5>
<p>Date de livraison</p>
<input type="date" name="date" value="{{ delivery.from_date.date() if delivery.from_date else '' }}" required> de <input type="time" name="from_time" value="{{ delivery.from_date.time() if delivery.from_date else '' }}" required> à <input type="time" name="to_time" value="{{ delivery.to_date.time() if delivery.to_date else '' }}" required>
</label>
<label>
<h5>Date de limite de commande</h5>
<p>Date de limite de commande</p>
<input type="date" name="order_before" value="{{ delivery.order_before.date() if delivery.order_before else '' }}" required>
</label>
<label>
<h5>Instructions particulières</h5>
<p>Instructions particulières</p>
<input type="text" name="instructions" value="{{ delivery.instructions or '' }}">
</label>
<div>
@ -52,6 +52,8 @@
<dd>Conditionnement d'une unité: 1kg, 33cl…</dd>
<dt>description</dt>
<dd>Plus de détails sur le produit.</dd>
<dt>packing</dt>
<dd>Contionnement final pour grouper les commandes, le cas échéant, en nombre d'unités.</dd>
<dt>url</dt>
<dd>Une URL éventuelle pointant sur une page présentant le produit.</dd>
<dt>img</dt>

View file

@ -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 %}

View file

@ -2,6 +2,6 @@
<li><i class="icon-basket"></i> <strong>Produits</strong> {{ delivery.description }}</li>
<li><i class="icon-streetsign"></i> <strong>Lieu</strong> {{ delivery.where }}</li>
<li><i class="icon-clock"></i> <strong>Date de livraison</strong> <time datetime="{{ delivery.from_date }}">{{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</time></li>
<li><i class="icon-hourglass"></i> {% if delivery.is_open %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before|date }}</time>{% else %}<strong>Fermée</strong>{% endif %}</li>
<li><i class="icon-hourglass"></i> {% if delivery.status == delivery.OPEN %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before|date }}</time>{% elif delivery.status == delivery.ADJUSTMENT %}<strong>Ajustement en cours</strong>{% else %}<strong>Fermée</strong>{% endif %}</li>
{% if delivery.instructions %}<li><i class="icon-lightbulb"></i> <strong>À savoir</strong> {{ delivery.instructions }}</li>{% endif %}
</ul>

View file

@ -2,7 +2,7 @@
<ul class="delivery">
{% for delivery in deliveries %}
<li>
<h3><a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.producer }}</a> {% if delivery.is_open %}<a class="button" href="/livraison/{{ delivery.id }}/commander">{% if request.user.email in delivery.orders %}Gérer ma commande{% else %}Commander{% endif %}</a>{% endif %}</h3>
<h3><a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.producer }}</a> {% include "includes/order_button.html" %}</h3>
{% include "includes/delivery_head.html" %}
</li>
<hr>

View file

@ -0,0 +1,9 @@
{% if delivery.status != delivery.CLOSED %}
<a class="button" href="/livraison/{{ delivery.id }}/commander">
{% if delivery.status == delivery.ADJUSTMENT %}
Ajuster ma commande
{% elif delivery.status == delivery.OPEN %}
{% if request.user.email in delivery.orders %}Gérer ma commande{% else %}Commander{% endif %}
{% endif %}
</a>
{% endif %}

View file

@ -1,10 +1,10 @@
<table class="order">
<tr><th class="product">Produit</th><th class="price">Prix unitaire</th><th class="amount">Quantité</th></tr>
{% for product in delivery.products %}
{% if order.get_quantity(product) %}
{% if order[product].quantity %}
<tr>
<th class="product" style="text-align: left;">{{ product.label }}</th>
<td>{{ product.price }} €</td><td>{{ order.get_quantity(product) }}</td>
<th class="product" style="text-align: left;">{{ product }}</th>
<td>{{ product.price }} €</td><td>{{ order[product].quantity }}</td>
</tr>
{% endif %}
{% endfor %}

View file

@ -6,17 +6,32 @@
{% include "includes/delivery_head.html" %}
<form method="post">
<table class="order">
<tr><th class="product">Produit</th><th class="price">Prix</th><th class="amount">Quantité</th></tr>
<tr>
<th class="product">Produit</th>
<th class="price">Prix</th>
{% if delivery.has_packing %}
<th class="packing">Conditionnement</th>
{% endif %}
<th class="amount">Quantité</th>
{% if delivery.status == delivery.ADJUSTMENT %}<th class="amount">Ajustement</th>{% endif %}</tr>
{% for product in delivery.products %}
<tr>
<th class="product">{{ product.label }}
<th class="product">{{ product }}
{% if product.description or product.img %}
{% with unique_id=loop.index %}
{% include "includes/modal_product.html" %}
{% endwith %}
{% endif %}</p>
</th>
<td>{{ product.price }} €</td><td class="with-input"><input type="number" name="{{ product.ref }}" value="{{ order.get_quantity(product) }}"></td></tr>
<td>{{ product.price }} €</td>
{% if delivery.has_packing %}
<td{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} class="missing" title="Les commandes individuelles ne correspondent pas aux conditionnements"{% endif %}>{{ product.packing or "—" }}{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} ({{ delivery.product_missing(product) }}){% endif %}</td>
{% endif %}
<td class="with-input"><input {% if delivery.status != delivery.OPEN %}type="text" readonly{% else%}type="number"{% endif%} min=0 name="wanted:{{ product.ref }}" value="{{ order[product].wanted }}"></td>
{% if delivery.status == delivery.ADJUSTMENT %}
<td class="with-input"><input type="number" name="adjustment:{{ product.ref }}" value="{{ order[product].adjustment }}" {% if not delivery.product_missing(product) %}readonly{% endif %}></td>
{% endif %}
</tr>
{% endfor %}
</table>
<p>Total: {{ order.total(delivery.products) if order else 0 }} €</p>

View file

@ -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 =

View file

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

26
tests/test_reports.py Normal file
View file

@ -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),
]

View file

@ -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,11 +98,109 @@ 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_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")
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),
]