mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 19:42:37 +02:00
Allow to define ProductOrder.ajustment when product needs adjustment
This commit is contained in:
parent
165427bf8f
commit
425222a0a2
16 changed files with 219 additions and 48 deletions
|
@ -80,7 +80,7 @@ async def auth_required(request, response):
|
||||||
# Should be handler Roll side?
|
# Should be handler Roll side?
|
||||||
# In dev mode, we serve the static, but we don't have yet a way to mark static
|
# In dev mode, we serve the static, but we don't have yet a way to mark static
|
||||||
# route as unprotected.
|
# route as unprotected.
|
||||||
if request.path.startswith('/static/'):
|
if request.path.startswith("/static/"):
|
||||||
return
|
return
|
||||||
if request.route.payload and not request.route.payload.get("unprotected"):
|
if request.route.payload and not request.route.payload.get("unprotected"):
|
||||||
token = request.cookies.get("token")
|
token = request.cookies.get("token")
|
||||||
|
@ -198,7 +198,7 @@ async def import_products(request, response, id):
|
||||||
response.message(err, status="error")
|
response.message(err, status="error")
|
||||||
response.redirect = path
|
response.redirect = path
|
||||||
return
|
return
|
||||||
elif data.filename.endswith('.xlsx'):
|
elif data.filename.endswith(".xlsx"):
|
||||||
try:
|
try:
|
||||||
imports.products_from_xlsx(delivery, data)
|
imports.products_from_xlsx(delivery, data)
|
||||||
except ValueError as err:
|
except ValueError as err:
|
||||||
|
@ -258,15 +258,25 @@ async def place_order(request, response, id):
|
||||||
response.redirect = delivery_url
|
response.redirect = delivery_url
|
||||||
return
|
return
|
||||||
if request.method == "POST":
|
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
|
form = request.form
|
||||||
order = Order(paid=form.bool("paid", False))
|
order = Order(paid=form.bool("paid", False))
|
||||||
for product in delivery.products:
|
for product in delivery.products:
|
||||||
try:
|
try:
|
||||||
quantity = form.int(product.ref, 0)
|
wanted = form.int(f"wanted:{product.ref}", 0)
|
||||||
except HttpError:
|
except HttpError:
|
||||||
continue
|
continue
|
||||||
if quantity:
|
try:
|
||||||
order.products[product.ref] = ProductOrder(wanted=quantity)
|
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:
|
if not delivery.orders:
|
||||||
delivery.orders = {}
|
delivery.orders = {}
|
||||||
if not order.products:
|
if not order.products:
|
||||||
|
|
|
@ -33,7 +33,6 @@ def price_field(value):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Base:
|
class Base:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, data=None, **kwargs):
|
def create(cls, data=None, **kwargs):
|
||||||
if isinstance(data, Base):
|
if isinstance(data, Base):
|
||||||
|
@ -109,7 +108,11 @@ class Product(Base):
|
||||||
@dataclass
|
@dataclass
|
||||||
class ProductOrder(Base):
|
class ProductOrder(Base):
|
||||||
wanted: int
|
wanted: int
|
||||||
ordered: int = 0
|
adjustment: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quantity(self):
|
||||||
|
return self.wanted + self.adjustment
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -117,14 +120,15 @@ class Order(Base):
|
||||||
products: Dict[str, ProductOrder] = field(default_factory=dict)
|
products: Dict[str, ProductOrder] = field(default_factory=dict)
|
||||||
paid: bool = False
|
paid: bool = False
|
||||||
|
|
||||||
def get_quantity(self, product):
|
def __getitem__(self, ref):
|
||||||
choice = self.products.get(product.ref)
|
if isinstance(ref, Product):
|
||||||
return choice.wanted if choice else 0
|
ref = ref.ref
|
||||||
|
return self.products.get(ref, ProductOrder(wanted=0))
|
||||||
|
|
||||||
def total(self, products):
|
def total(self, products):
|
||||||
products = {p.ref: p for p in products}
|
products = {p.ref: p for p in products}
|
||||||
return round(
|
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"
|
__root__ = "delivery"
|
||||||
__lock__ = threading.Lock()
|
__lock__ = threading.Lock()
|
||||||
|
CLOSED = 0
|
||||||
|
OPEN = 1
|
||||||
|
ADJUSTMENT = 2
|
||||||
|
|
||||||
producer: str
|
producer: str
|
||||||
from_date: datetime_field
|
from_date: datetime_field
|
||||||
|
@ -145,6 +152,14 @@ class Delivery(Base):
|
||||||
orders: Dict[str, Order] = field(default_factory=dict)
|
orders: Dict[str, Order] = field(default_factory=dict)
|
||||||
id: str = field(default_factory=lambda *a, **k: uuid.uuid4().hex)
|
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
|
@property
|
||||||
def total(self):
|
def total(self):
|
||||||
return round(sum(o.total(self.products) for o in self.orders.values()), 2)
|
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):
|
def has_packing(self):
|
||||||
return any(p.packing for p in self.products)
|
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
|
@classmethod
|
||||||
def init_fs(cls):
|
def init_fs(cls):
|
||||||
cls.get_root().mkdir(parents=True, exist_ok=True)
|
cls.get_root().mkdir(parents=True, exist_ok=True)
|
||||||
|
@ -202,7 +221,7 @@ class Delivery(Base):
|
||||||
total = 0
|
total = 0
|
||||||
for order in self.orders.values():
|
for order in self.orders.values():
|
||||||
if product.ref in order.products:
|
if product.ref in order.products:
|
||||||
total += order.products[product.ref].wanted
|
total += order.products[product.ref].quantity
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def product_missing(self, product):
|
def product_missing(self, product):
|
||||||
|
@ -211,3 +230,6 @@ class Delivery(Base):
|
||||||
wanted = self.product_wanted(product)
|
wanted = self.product_wanted(product)
|
||||||
orphan = wanted % product.packing
|
orphan = wanted % product.packing
|
||||||
return product.packing - orphan if orphan else 0
|
return product.packing - orphan if orphan else 0
|
||||||
|
|
||||||
|
def has_order(self, person):
|
||||||
|
return person.email in self.orders
|
||||||
|
|
|
@ -10,7 +10,15 @@ def summary(delivery):
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
ws = wb.active
|
ws = wb.active
|
||||||
ws.title = f"{delivery.producer} {delivery.from_date.date()}"
|
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:
|
for product in delivery.products:
|
||||||
wanted = delivery.product_wanted(product)
|
wanted = delivery.product_wanted(product)
|
||||||
ws.append(
|
ws.append(
|
||||||
|
@ -19,10 +27,11 @@ def summary(delivery):
|
||||||
product.label,
|
product.label,
|
||||||
product.price,
|
product.price,
|
||||||
wanted,
|
wanted,
|
||||||
|
product.unit,
|
||||||
round(product.price * wanted, 2),
|
round(product.price * wanted, 2),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
ws.append(["", "", "", "Total", delivery.total])
|
ws.append(["", "", "", "", "Total", delivery.total])
|
||||||
return save_virtual_workbook(wb)
|
return save_virtual_workbook(wb)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,12 +45,14 @@ def full(delivery):
|
||||||
row = [product.ref, product.label, product.price]
|
row = [product.ref, product.label, product.price]
|
||||||
for order in delivery.orders.values():
|
for order in delivery.orders.values():
|
||||||
wanted = order.products.get(product.ref)
|
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))
|
row.append(delivery.product_wanted(product))
|
||||||
ws.append(row)
|
ws.append(row)
|
||||||
footer = ["Total", "", ""] + [
|
footer = (
|
||||||
o.total(delivery.products) for o in delivery.orders.values()
|
["Total", "", ""]
|
||||||
] + [delivery.total]
|
+ [o.total(delivery.products) for o in delivery.orders.values()]
|
||||||
|
+ [delivery.total]
|
||||||
|
)
|
||||||
ws.append(footer)
|
ws.append(footer)
|
||||||
return save_virtual_workbook(wb)
|
return save_virtual_workbook(wb)
|
||||||
|
|
||||||
|
|
|
@ -236,7 +236,6 @@ textarea {
|
||||||
line-height: 1rem;
|
line-height: 1rem;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border: .05rem solid #bbc;
|
border: .05rem solid #bbc;
|
||||||
border-radius: .1rem;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,6 +275,9 @@ input:focus,
|
||||||
select:focus {
|
select:focus {
|
||||||
box-shadow: 0 0 .1rem var(--primary-color);
|
box-shadow: 0 0 .1rem var(--primary-color);
|
||||||
}
|
}
|
||||||
|
input[readonly] {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
@ -355,6 +357,7 @@ td.with-input {
|
||||||
}
|
}
|
||||||
td.with-input input {
|
td.with-input input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
article.delivery {
|
article.delivery {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block body %}
|
{% 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" %}
|
{% include "includes/delivery_head.html" %}
|
||||||
<article class="delivery">
|
<article class="delivery">
|
||||||
<table class="delivery">
|
<table class="delivery">
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
<th class="product">Produit</th>
|
<th class="product">Produit</th>
|
||||||
<th class="price">Prix</th>
|
<th class="price">Prix</th>
|
||||||
{% if delivery.has_packing %}
|
{% if delivery.has_packing %}
|
||||||
<th class="packing">Lot</th>
|
<th class="packing">Conditionnement</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th class="amount">Total</th>
|
<th class="amount">Total</th>
|
||||||
{% for email, order in delivery.orders.items() %}
|
{% for email, order in delivery.orders.items() %}
|
||||||
|
@ -32,13 +32,9 @@
|
||||||
{% if delivery.has_packing %}
|
{% if delivery.has_packing %}
|
||||||
<td class="packing">{{ product.packing or '—'}}</td>
|
<td class="packing">{{ product.packing or '—'}}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<th{% if delivery.product_missing(product) %} class="missing" title="Les commandes individuelles ne correspondent pas aux lots"{% endif %}>{{ delivery.product_wanted(product) }}{% if delivery.product_missing(product) %} ({{ delivery.product_missing(product) }}){% endif %}</th>
|
<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 delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }}){% endif %}</th>
|
||||||
{% for email, order in delivery.orders.items() %}
|
{% for email, order in delivery.orders.items() %}
|
||||||
{% if product.ref in order.products %}
|
<td>{{ order[product.ref].quantity or "—" }}</td>
|
||||||
<td>{{ order.products[product.ref].wanted }}</td>
|
|
||||||
{% else %}
|
|
||||||
<td>—</td>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -57,7 +53,7 @@
|
||||||
<hr>
|
<hr>
|
||||||
<ul class="toolbox">
|
<ul class="toolbox">
|
||||||
<li>
|
<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>
|
||||||
<li>
|
<li>
|
||||||
<a href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-grid"></i> Rapport complet</a>
|
<a href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-grid"></i> Rapport complet</a>
|
||||||
|
|
|
@ -8,27 +8,27 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label>
|
<label>
|
||||||
<h5>Producteur</h5>
|
<p>Producteur</p>
|
||||||
<input type="text" name="producer" value="{{ delivery.producer or '' }}" required>
|
<input type="text" name="producer" value="{{ delivery.producer or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Description des produits</h5>
|
<p>Description des produits</p>
|
||||||
<input type="text" name="description" value="{{ delivery.description or '' }}" required>
|
<input type="text" name="description" value="{{ delivery.description or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Lieu</h5>
|
<p>Lieu</p>
|
||||||
<input type="text" name="where" value="{{ delivery.where or '' }}" required>
|
<input type="text" name="where" value="{{ delivery.where or '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<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>
|
<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>
|
||||||
<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>
|
<input type="date" name="order_before" value="{{ delivery.order_before.date() if delivery.order_before else '' }}" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
<h5>Instructions particulières</h5>
|
<p>Instructions particulières</p>
|
||||||
<input type="text" name="instructions" value="{{ delivery.instructions or '' }}">
|
<input type="text" name="instructions" value="{{ delivery.instructions or '' }}">
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -5,8 +5,8 @@ Voici le résumé de votre commande «{{ delivery.producer }}»
|
||||||
Produit | Prix unitaire | Quantité
|
Produit | Prix unitaire | Quantité
|
||||||
|
|
||||||
{% for product in delivery.products %}
|
{% for product in delivery.products %}
|
||||||
{% if order.get_quantity(product) %}
|
{% if order[product].quantity %}
|
||||||
{{ product.name }} | {{ product.price }} € | {{ order.get_quantity(product) }}
|
{{ product.name }} | {{ product.price }} € | {{ order[product].quantity }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
<li><i class="icon-basket"></i> <strong>Produits</strong> {{ delivery.description }}</li>
|
<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-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-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 %}
|
{% if delivery.instructions %}<li><i class="icon-lightbulb"></i> <strong>À savoir</strong> {{ delivery.instructions }}</li>{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<ul class="delivery">
|
<ul class="delivery">
|
||||||
{% for delivery in deliveries %}
|
{% for delivery in deliveries %}
|
||||||
<li>
|
<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" %}
|
{% include "includes/delivery_head.html" %}
|
||||||
</li>
|
</li>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
9
copanier/templates/includes/order_button.html
Normal file
9
copanier/templates/includes/order_button.html
Normal 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 %}
|
|
@ -1,10 +1,10 @@
|
||||||
<table class="order">
|
<table class="order">
|
||||||
<tr><th class="product">Produit</th><th class="price">Prix unitaire</th><th class="amount">Quantité</th></tr>
|
<tr><th class="product">Produit</th><th class="price">Prix unitaire</th><th class="amount">Quantité</th></tr>
|
||||||
{% for product in delivery.products %}
|
{% for product in delivery.products %}
|
||||||
{% if order.get_quantity(product) %}
|
{% if order[product].quantity %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="product" style="text-align: left;">{{ product.label }}</th>
|
<th class="product" style="text-align: left;">{{ product.label }}</th>
|
||||||
<td>{{ product.price }} €</td><td>{{ order.get_quantity(product) }}</td>
|
<td>{{ product.price }} €</td><td>{{ order[product].quantity }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -6,7 +6,14 @@
|
||||||
{% include "includes/delivery_head.html" %}
|
{% include "includes/delivery_head.html" %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<table class="order">
|
<table class="order">
|
||||||
<tr><th class="product">Produit</th><th class="price">Prix</th><th class="packing">Lot</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 %}
|
{% for product in delivery.products %}
|
||||||
<tr>
|
<tr>
|
||||||
<th class="product">{{ product.label }}
|
<th class="product">{{ product.label }}
|
||||||
|
@ -17,8 +24,14 @@
|
||||||
{% endif %}</p>
|
{% endif %}</p>
|
||||||
</th>
|
</th>
|
||||||
<td>{{ product.price }} €</td>
|
<td>{{ product.price }} €</td>
|
||||||
<td{% if delivery.product_missing(product) %} class="missing" title="Les commandes individuelles ne correspondent pas aux lots"{% endif %}>{{ product.packing or "—" }}{% if delivery.product_missing(product) %} (manque {{ delivery.product_missing(product) }}){% endif %}</td>
|
{% if delivery.has_packing %}
|
||||||
<td class="with-input"><input type="number" name="{{ product.ref }}" value="{{ order.get_quantity(product) }}"></td></tr>
|
<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 %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
<p>Total: {{ order.total(delivery.products) if order else 0 }} €</p>
|
<p>Total: {{ order.total(delivery.products) if order else 0 }} €</p>
|
||||||
|
|
14
setup.cfg
14
setup.cfg
|
@ -7,13 +7,25 @@ packages = find:
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
install_requires =
|
install_requires =
|
||||||
Jinja2==2.10
|
Jinja2==2.10
|
||||||
minicli==0.4.4
|
|
||||||
openpyxl==2.6.1
|
openpyxl==2.6.1
|
||||||
PyJWT==1.7.1
|
PyJWT==1.7.1
|
||||||
PyYAML==5.1
|
PyYAML==5.1
|
||||||
roll==0.10.1
|
roll==0.10.1
|
||||||
ujson==1.35
|
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]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
|
|
|
@ -36,6 +36,13 @@ def test_delivery_is_open_when_order_before_is_in_the_future(delivery):
|
||||||
assert delivery.is_open
|
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():
|
def test_can_create_product():
|
||||||
product = Product(name="Lait 1.5L", ref="123", price=1.5)
|
product = Product(name="Lait 1.5L", ref="123", price=1.5)
|
||||||
assert product.ref == "123"
|
assert product.ref == "123"
|
||||||
|
@ -103,3 +110,13 @@ def test_person_is_staff_if_no_staff_in_config(monkeypatch):
|
||||||
monkeypatch.setattr(config, 'STAFF', [])
|
monkeypatch.setattr(config, 'STAFF', [])
|
||||||
person = Person(email="foo@bar.fr")
|
person = Person(email="foo@bar.fr")
|
||||||
assert person.is_staff
|
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
26
tests/test_reports.py
Normal 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),
|
||||||
|
]
|
|
@ -1,9 +1,11 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from openpyxl import load_workbook
|
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
|
pytestmark = pytest.mark.asyncio
|
||||||
|
|
||||||
|
@ -52,7 +54,7 @@ async def test_create_delivery(client):
|
||||||
|
|
||||||
async def test_place_order_with_session(client, delivery):
|
async def test_place_order_with_session(client, delivery):
|
||||||
delivery.persist()
|
delivery.persist()
|
||||||
body = {"123": "3"}
|
body = {"wanted:123": "3"}
|
||||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
||||||
assert resp.status == 302
|
assert resp.status == 302
|
||||||
delivery = Delivery.load(id=delivery.id)
|
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):
|
async def test_place_order_with_empty_string(client, delivery):
|
||||||
delivery.persist()
|
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)
|
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
||||||
assert resp.status == 302
|
assert resp.status == 302
|
||||||
delivery = Delivery.load(id=delivery.id)
|
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):
|
async def test_change_paid_status_when_placing_order(client, delivery):
|
||||||
delivery.persist()
|
delivery.persist()
|
||||||
body = {"123": "3", "paid": 1}
|
body = {"wanted:123": "3", "paid": 1}
|
||||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
||||||
assert resp.status == 302
|
assert resp.status == 302
|
||||||
delivery = Delivery.load(id=delivery.id)
|
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
|
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):
|
async def test_export_products(client, delivery):
|
||||||
delivery.persist()
|
delivery.persist()
|
||||||
resp = await client.get(f"/livraison/{delivery.id}/exporter/produits")
|
resp = await client.get(f"/livraison/{delivery.id}/exporter/produits")
|
||||||
|
|
Loading…
Reference in a new issue