Online edition + manage delivery + send emails.

This commit is contained in:
Alexis M 2019-09-30 00:31:19 +02:00
parent ff79712fb5
commit f96ca0a192
16 changed files with 398 additions and 23 deletions

View file

@ -341,7 +341,114 @@ async def import_products(request, response, id):
response.redirect = f"/livraison/{delivery.id}"
@app.route("/livraison/{id}/exporter/produits", methods=["GET"])
@app.route("/livraison/{delivery_id}/producteurices")
async def list_producers(request, response, delivery_id):
delivery = Delivery.load(delivery_id)
response.html("list_products.html", {
'list_only': True,
'edit_mode': True,
'delivery': delivery,
'referent': request.query.get('referent', None),
})
@app.route("/livraison/{delivery_id}/{producer_id}/éditer", methods=["GET", "POST"])
async def edit_producer(request, response, delivery_id, producer_id):
delivery = Delivery.load(delivery_id)
producer = delivery.producers.get(producer_id)
if request.method == 'POST':
form = request.form
producer.referent = form.get('referent')
producer.tel_referent = form.get('tel_referent')
producer.description = form.get('description')
producer.contact = form.get('contact')
delivery.producers[producer_id] = producer
delivery.persist()
response.html("edit_producer.html", {
'delivery': delivery,
'producer': producer,
'products': delivery.get_products_by(producer.id)
})
@app.route("/livraison/{delivery_id}/{producer_id}/{product_ref}/éditer", methods=["GET", "POST"])
async def edit_product(request, response, delivery_id, producer_id, product_ref):
delivery = Delivery.load(delivery_id)
product = delivery.get_product(product_ref)
if request.method == 'POST':
form = request.form
product.name = form.get('name')
product.price = form.float('price')
product.unit = form.get('unit')
product.description = form.get('description')
product.url = form.get('url')
if form.get('packing'):
product.packing = form.int('packing')
if 'rupture' in form:
product.rupture = form.get('rupture')
else:
product.rupture = None
delivery.persist()
response.message('Le produit à bien été édité')
response.redirect = f'/livraison/{delivery_id}/producteurice/{producer_id}/éditer'
response.html("edit_product.html", {
'delivery': delivery,
'product': product
})
@app.route("/livraison/{delivery_id}/{producer_id}/ajouter-produit", methods=["GET", "POST"])
async def create_product(request, response, delivery_id, producer_id):
delivery = Delivery.load(delivery_id)
product = Product(name="", ref="", price=0)
if request.method == 'POST':
product.producer = producer_id
form = request.form
product.update_from_form(form)
product.ref = slugify(f"{producer_id}-{product.name}")
delivery.products.append(product)
delivery.persist()
response.message('Le produit à bien été créé')
response.redirect = f'/livraison/{delivery_id}/producteurice/{producer_id}/éditer'
response.html("edit_product.html", {
'delivery': delivery,
'producer_id': producer_id,
'product': product,
})
@app.route("/livraison/{id}/gérer", methods=['GET'])
async def manage_delivery(request, response, id):
delivery = Delivery.load(id)
response.html("manage_delivery.html",{
'delivery': delivery
})
@app.route("/livraison/{id}/envoi-email-referentes", methods=['GET', 'POST'])
async def send_referent_emails(request, response, id):
delivery = Delivery.load(id)
date = delivery.to_date.strftime("%Y-%m-%d")
if request.method == 'POST':
email_body = request.form.get('email_body')
email_subject = request.form.get('email_subject')
for referent in delivery.get_referents():
producers = delivery.get_producers_for_referent(referent)
summary = reports.summary(delivery, producers)
emails.send(referent, email_subject, email_body, copy=delivery.contact, attachments=[
(f"{config.SITE_NAME}-{date}-{referent}.xlsx", summary)
])
response.message("Le mail à bien été envoyé")
response.redirect = f"/livraison/{id}/gérer"
response.html("prepare_referent_email.html", {
'delivery': delivery
})
@app.route("/livraison/{id}/exporter", methods=["GET"])
async def export_products(request, response, id):
delivery = Delivery.load(id)
response.xlsx(reports.products(delivery))
@ -570,7 +677,10 @@ async def delivery_balance(request, response, id):
for producer in delivery.producers.values():
group = groups.get_user_group(producer.referent)
group_id = group.id if group else producer.referent
if hasattr(group, 'id'):
group_id = group.id
else:
group_id = group
amount = delivery.total_for_producer(producer.id)
if amount:
balance.append((group_id, amount))

View file

@ -1,18 +1,32 @@
import smtplib
from email.message import EmailMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email.mime.base import MIMEBase
from email import encoders
from . import config
def send(to, subject, body, html=None):
msg = EmailMessage()
msg.set_content(body)
def send(to, subject, body, html=None, copy=None, attachments=None):
msg = MIMEMultipart()
msg.attach(MIMEText(body, "plain"))
if html:
msg.attach(MIMEText(html, "html"))
msg["Subject"] = subject
msg["From"] = config.FROM_EMAIL
msg["To"] = to
msg["Bcc"] = config.FROM_EMAIL
if html:
msg.add_alternative(html, subtype="html")
msg["Bcc"] = copy if copy else config.FROM_EMAIL
for file_name, attachment in attachments:
part = MIMEBase('application','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8')
part.set_payload(attachment)
part.add_header('Content-Disposition',
'attachment',
filename=file_name)
encoders.encode_base64(part)
msg.attach(part)
if not config.SEND_EMAILS:
return print("Sending email", str(msg))
try:

View file

@ -152,7 +152,7 @@ class Groups(PersistedBase):
for group in self.groups.values():
if email in group.members:
return group
return None
return email
@classmethod
def init_fs(cls):
@ -164,7 +164,7 @@ class Producer(Base):
referent: str = ""
tel_referent: str = ""
contact: str = ""
location: str = ""
description: str = ""
@dataclass
@ -186,6 +186,20 @@ class Product(Base):
# out += f" ({self.unit})"
return out
def update_from_form(self, form):
self.name = form.get('name')
self.price = form.float('price')
self.unit = form.get('unit')
self.description = form.get('description')
self.url = form.get('url')
if form.get('packing'):
self.packing = form.int('packing')
if 'rupture' in form:
self.rupture = form.get('rupture')
else:
self.rupture = None
return self
@dataclass
class ProductOrder(Base):
@ -388,8 +402,23 @@ class Delivery(PersistedBase):
def get_products_by(self, producer):
return [p for p in self.products if p.producer == producer]
def get_product(self, ref):
products = [p for p in self.products if p.ref == ref]
if products:
return products[0]
def total_for_producer(self, producer, person=None):
producer_products = [p for p in self.products if p.producer == producer]
if person:
return self.orders.get(person).total(producer_products)
return round(sum(o.total(producer_products) for o in self.orders.values()), 2)
def get_producers_for_referent(self, referent):
return {
id: producer
for id, producer in self.producers.items()
if producer.referent == referent
}
def get_referents(self):
return [producer.referent for producer in self.producers.values()]

View file

@ -35,11 +35,12 @@ def summary_for_products(wb, title, delivery, total=None, products=None):
ws.append(["", "", "", "", "Total", total])
def summary(delivery):
def summary(delivery, producers=None):
wb = Workbook()
wb.remove(wb.active)
for producer in delivery.producers:
if not producers:
producers = delivery.producers
for producer in producers:
summary_for_products(
wb,
producer,

View file

@ -450,6 +450,11 @@ hr {
.notification.warning {
background-color: #f9b42d;
}
.notification.info {
background-color: #aed1b175;
color: #000;
}
.notification i {
font-size: 2rem;
}
@ -515,6 +520,7 @@ ul.delivery-head li {
list-style: none;
padding: 0;
margin: 0;
font-size: 0.8em;
}
ul.delivery-head li i{
float: left;

View file

@ -3,13 +3,18 @@
{% block body %}
<h3>{{ delivery.name }} {% include "includes/order_button.html" %}</h3>
{% include "includes/delivery_head.html" %}
{% if request['user'].email == delivery.contact %}
<div class="notification info"><i class="icon-lightbulb"></i> Vous êtes la personne référente de cette distribution <a class="button" href="/livraison/{{ delivery.id }}/gérer">Gérer la distribution</a></div>
{% elif request['user'].email in delivery.get_referents() %}
<div class="notification info"><i class="icon-lightbulb"></i> Vous êtes référent⋅e pour cette distribution (merci !). Voici <a class="button" href="/livraison/{{ delivery.id }}/producteurices?referent={{request['user'].email}}">un petit lien pour aller voir les produits dont vous vous occupez !</a></div>
{% endif %}
<article class="delivery">
{% if delivery.has_products %}
{% include "includes/delivery_table.html" %}
{% else %}
<p>Aucun produit n'est encore défini pour cette livraison.</p>
{% if request.user and request.user.is_staff %}
<div class="inline-text">Pour rajouter des produits, il faut d'abord <a href="/livraison/{{ delivery.id }}/exporter/produits"><i class="icon-layers"></i> télécharger le tableur avec les produits</a>, le modifier localement sur votre machine puis &nbsp;</div> <div class="inline-text">{% with unique_id="import-products" %}{% include "includes/modal_import_products.html" %}{% endwith %}.</div>
<div class="inline-text">Pour rajouter des produits, il faut d'abord <a href="/livraison/{{ delivery.id }}/exporter"><i class="icon-layers"></i> télécharger le tableur avec les produits</a>, le modifier localement sur votre machine puis &nbsp;</div> <div class="inline-text">{% with unique_id="import-products" %}{% include "includes/modal_import_products.html" %}{% endwith %}.</div>
{% endif %}
{% endif %}
</article>
@ -28,7 +33,7 @@
{% endif %}
{% if request.user and request.user.is_staff %}
<li>
<a href="/livraison/{{ delivery.id }}/exporter/produits"><i class="icon-layers"></i> Liste des produits</a>
<a href="/livraison/{{ delivery.id }}/exporter"><i class="icon-layers"></i> Liste des produits</a>
</li>
<li>
<a href="/livraison/{{ delivery.id }}/edit"><i class="icon-adjustments"></i> Modifier la livraison</a>

View file

@ -16,7 +16,7 @@
<input type="text" name="description" value="{{ delivery.description or '' }}">
</label>
<label>
<p>Référent</p>
<p>Référent⋅e</p>
<input type="email" name="contact" value="{{ delivery.contact or request.user.email }}" required>
</label>
<label>
@ -56,6 +56,9 @@
{% include "includes/modal_import_products.html" %}
{% endwith %}
</li>
<li>
<a href="/livraison/{{ delivery.id }}/producteurices"><i class="icon-pencil"></i> Gérer les produits / product⋅eur⋅rice⋅s</a>
</li>
</ul>
{% endif %}
{% endblock body %}

View file

@ -0,0 +1,69 @@
{% extends "base.html" %}
{% block body %}
{% if producer.id %}
<h1>Modifier les informations pour « {{ producer.id }} »</h1>
{% else %}
<h1>Nouve⋅eau⋅lle product⋅eur⋅rice</h1>
{% endif %}
<form method="post">
{% if producer.id %}
<input type="hidden" name="id" value="{{ producer.id }}">
{% else %}
<label>
<p>Nom du / de la product⋅eur⋅rice</p>
<input type="text" name="id" value="{{ producer.id or '' }}">
</label>
{% endif %}
<label>
<h5>Description</h5>
<input type="text" name="description" value="{{ producer.description or '' }}">
</label>
<label>
<p>Email de la personne référente</p>
<input type="email" name="referent" value="{{ producer.referent or '' }}" required>
</label>
<label>
<p>Téléphone de la personne référente</p>
<input type="tel" name="tel_referent" value="{{ producer.tel_referent or '' }}" required>
</label>
<label>
<h5>Contact du⋅la product⋅eur⋅rice</h5>
<input type="text" name="contact" value="{{ producer.contact or '' }}">
</label>
<div>
<input type="submit" name="submit" value="Valider" class="primary">
</div>
</form>
{% if products %}
<h3>Produits <a class="button" href="/livraison/{{ delivery.id}}/{{ producer.id }}/ajouter-produit">Ajouter un produit</a></h3>
<table>
<thead>
<tr>
<th>Produit</th>
<th>Prix</th>
<th>Unité</th>
<th>Description</th>
<th>Packaging</th>
<th>Rupture ?</th>
<th></th>
</tr>
</thead>
{% for product in products %}
<tr>
<td><a href="/livraison/{{ delivery.id }}/{{ producer.id }}/{{ product.ref }}/éditer">{{ product.name }}</a></td>
<td>{{ product.price }}€</td>
<td>{{ product.unit }}</td>
<td>{{ product.description }}</td>
<td>{% if product.packing %}{{ product.packing }}{% endif %}</td>
<td>{% if product.rupture %}RUPTURE !!{% endif %}</td>
<td><a href="/livraison/{{ delivery.id }}/{{ producer.id }}/{{ product.ref }}/éditer">éditer</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
<hr>
{% endblock body %}

View file

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block body %}
{% if product.ref %}
<h1>Modifier le produit « {{ product.name }} »</h1>
{% else %}
<h1>{{ producer_id }} : Nouveau produit</h1>
{% endif %}
<form method="post">
<label>
<p>Nom</p>
<input type="text" name="name" value="{{ product.name or '' }}" required>
</label>
<label>
<p>Prix (en €), pour une unité</p>
<input type="number" step="0.01" name="price" value="{{ product.price or '' }}" required>
</label>
<label>
<p>Unité de commande</p>
<input type="text" name="unit" placeholder="sachet de 200g" value="{{ product.unit or '' }}" required>
</label>
<label>
<h5>Description du produit</h5>
<input type="text" name="description" placeholder="Bière type American Pale Ale" value="{{ product.description or '' }}">
</label>
<label>
<h5>URL d'une image du produit</h5>
<input type="url" name="url" placeholder="https://example.com/image.png" value="{{ product.url or '' }}">
</label>
<label>
<h5>Conditionnement (par combien d'unités est vendu ce produit ?)</h5>
<h5><em>par exemple, entrez 6 si ce sont des cartons de 6 bouteilles</em></h5>
<input type="number" name="packing" placeholder="6" value="{{ product.packing or '' }}">
</label>
<br />
<label>
Produit actuellement en rupture ?
<input type="checkbox" name="rupture" value="RUPTURE" {% if product.rupture %}checked="true"{% endif %}>
</label>
<div>
<input type="submit" name="submit" value="Valider" class="primary">
</div>
</form>
{% if products %}
<h3>Produits</h3>
<table>
<thead>
<tr>
<th>Produit</th>
<th>Prix</th>
<th>Unité</th>
<th>Description</th>
<th>Packaging</th>
<th>Rupture ?</th>
<th></th>
</tr>
</thead>
{% for product in products %}
<tr>
<td><a href="/livraison/{{ delivery.id }}/produit/{{ product.ref }}/éditer">{{ product.name }}</a></td>
<td>{{ product.price }}€</td>
<td>{{ product.unit }}</td>
<td>{{ product.description }}</td>
<td>{% if product.packing %}{{ product.packing }}{% endif %}</td>
<td>{% if product.rupture %}RUPTURE !!{% endif %}</td>
<td><a href="/livraison/{{ delivery.id }}/produit/{{ product.ref }}/éditer">éditer</a></td>
</tr>
{% endfor %}
</table>
{% endif %}
<hr>
{% endblock body %}

View file

@ -1,6 +1,11 @@
{% for producer in delivery.producers %}
<h2>{{ producer }}</h2>
<h4>Référent⋅e : {{ delivery.producers[producer].referent }} / {{ delivery.producers[producer].tel_referent }}</h4>
{% if referent %}
{% set producers = delivery.get_producers_for_referent(referent) %}
{% else %}
{% set producers = delivery.producers %}
{% endif %}
{% for producer in producers %}
<h2>{{ producer }} {% if edit_mode %}<a class="button" href="/livraison/{{ delivery.id }}/{{ producer }}/éditer"><i class="icon-ribbon"></i>Éditer</a> <a class="button" href="/livraison/{{ delivery.id }}/{{ producer }}/ajouter-produit"><i class="icon-puzzle"></i>Ajouter un produit </a>{% endif %}</h2>
<h5>{% if delivery.producers[producer].description %}{{ delivery.producers[producer].description }}{% endif %}. Référent⋅e : {{ delivery.producers[producer].referent }} / {{ delivery.producers[producer].tel_referent }}</h5>
<table class="delivery">
<thead>
<tr>
@ -9,6 +14,8 @@
{% if delivery.has_packing %}
<th class="packing">Conditionnement</th>
{% endif %}
{% if edit_mode %}<th>Éditer</th>{% endif %}
{% if not list_only %}
<th class="amount">Total</th>
{% for orderer, order in delivery.orders.items() %}
<th class="person{% if delivery.is_passed and not order.paid %} not-paid{% endif %}">
@ -19,16 +26,18 @@
{% endif %}
</th>
{% endfor %}
{% endif %}
</tr>
</thead>
<tbody>
{% for product in delivery.get_products_by(producer) %}
<tr>
<th class="product {% if product.rupture %}rupture{% endif %}">{{ product }} {% if product.rupture %}(RUPTURE !){% endif %}
<th class="product {% if product.rupture %}rupture{% endif %}">{% if edit_mode %}<a href="/livraison/{{ delivery.id }}/{{ product.producer }}/{{ product.ref }}/éditer">{% endif %}{{ product }}{% if edit_mode %}</a>{% endif %}{% if product.rupture %} {{ product.rupture }}{% endif %}
<td>{{ product.price | round(2) }} €</td>
{% if delivery.has_packing %}
<td class="packing">{% if product.packing %}{{ product.packing }} x {% endif %} {{ product.unit }}</td>
{% endif %}
{% if not list_only %}
<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) }})
@ -38,8 +47,11 @@
{% for email, order in delivery.orders.items() %}
<td title="Commandé: {{ order[product.ref].wanted }} — Ajusté: {{ "%+d"|format(order[product.ref].adjustment) }}">{{ order[product.ref].quantity or "—" }}</td>
{% endfor %}
{% endif %}
{% if edit_mode %}<td><a href="/livraison/{{ delivery.id }}/{{ product.producer }}/{{ product.ref }}/éditer">modifier</a></td>{% endif %}
</tr>
{% endfor %}
{% if not list_only %}
<tr>
<th class="total"><i class="icon-pricetags"></i> Total</th><td></td>
{% if delivery.has_packing %}
@ -50,6 +62,7 @@
<td>{{ order.total(delivery.get_products_by(producer)) }} €</td>
{% endfor %}
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}

View file

@ -1,6 +1,6 @@
{% extends "includes/modal.html" %}
{% block modal_label %}<i class="icon-upload"></i> Importer les produits{% endblock modal_label %}
{% block modal_label %}<i class="icon-upload"></i> Importer les produits depuis un tableur{% endblock modal_label %}
{% block modal_body %}
<h3>Importer des produits</h3>
<p>Format pris en charge: xlsx</p>

View file

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block body %}
<h3>{{ delivery.name }}</h3>
{% if referent %}<p>Voici la liste des product⋅eurs⋅rices dont {{ referent }} est référent⋅e. <a class="button" href="/livraison/{{delivery.id}}/producteurices">voir tous les produits</a></p>{% endif %}
<article class="delivery">
{% include "includes/delivery_table.html" %}
</article>
{% endblock body %}

View file

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block body %}
<h1>Gérer « {{ delivery.name }} »</h1>
<h3>Avant et pendant la distribution</h3>
<a class="button" href="/livraison/{{ delivery.id }}/edit"><i class="icon-pencil"></i>&nbsp; Modifier la commande (dates, lieu, référent⋅e, etc)</a>
<a class="button" href="/livraison/{{ delivery.id }}/producteurices"><i class="icon-pencil"></i>&nbsp; Modifier les produits, les product⋅rices⋅eurs</a>
<a class="button" href="/groupes"><i class="icon-globe"></i>&nbsp; Gérer les groupes / colocs</a>
<h3>Une fois les commandes passées (après le {{ delivery.order_before|date }})</h3>
<a class="button" href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-download"></i>&nbsp; Télécharger le récap (global) des commandes</a>
<a class="button" href="/livraison/{{ delivery.id }}/envoi-email-referentes"><i class="icon-envelope"></i>&nbsp; Envoyer les infos de commande aux référent⋅e⋅s</a>
<h3>Pour préparer la distribution (le {{ delivery.from_date|date }})</h3>
<a class="button" href="/livraison/{{ delivery.id }}/émargement"><i class="icon-document"></i>&nbsp; Fiches de commandes par groupe</a>
<a class="button" href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-grid"></i>&nbsp; Télécharger le résumé général des commandes</a>
<a class="button" href="/livraison/{{ delivery.id }}/solde"><i class="icon-gears"></i>&nbsp; Faire la répartition des paiements</a>
{% endblock %}

View file

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block body %}
<h1>Envoi d'un mail aux référent⋅e⋅s</h1>
<p>Le mail ci dessous (vous pouvez le modifier !) va être envoyé aux référent⋅e⋅s, avec en pièce jointe le tableau comportant les commandes pour les product⋅eurs⋅rices.</p>
<form method="post">
<input type="text" name="email_subject" style="width:800px" value="{{ config.SITE_NAME }} - Les commandes pour vos producteurs⋅rices" /><br />
<textarea name="email_body" cols="80" rows="10">
Bonjour,
Et voilà, les commandes maintenant terminées, il est maintenant temps de passer à l'action !
En pièce-jointe, les informations pour les producteurs⋅rices dont tu est référent⋅e.
Rendez-vous pour la distribution, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }} à {{ delivery.where }}.
A bientôt !
</textarea><br />
<input type="submit" name="submit" value="Envoyer le mail aux référent⋅e⋅s (avec le tableur en PJ)" class="primary">
</form>
{% endblock %}

View file

@ -9,7 +9,7 @@
<body>
<h2>{{ delivery.name }} {{ delivery.from_date.date() }} - liste d'émargement</h2>
{% for email, order in delivery.orders.items() %}
<h3>{{ email }}</h3>
<h3>{{ request.groups.groups[email].name }}</h3>
{% include "includes/order_summary.html" %}
<hr>
{% endfor %}

View file

@ -239,7 +239,7 @@ async def test_post_delivery_balance(client, delivery):
async def test_export_products(client, delivery):
delivery.persist()
resp = await client.get(f"/livraison/{delivery.id}/exporter/produits")
resp = await client.get(f"/livraison/{delivery.id}/exporter")
wb = load_workbook(filename=BytesIO(resp.body))
assert list(wb.active.values) == [
("name", "ref", "price", "unit", "description", "url", "img", "packing", "producer"),