diff --git a/copanier/__init__.py b/copanier/__init__.py index b39f108..eaddbaa 100644 --- a/copanier/__init__.py +++ b/copanier/__init__.py @@ -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)) diff --git a/copanier/emails.py b/copanier/emails.py index 10af47e..7c6be23 100644 --- a/copanier/emails.py +++ b/copanier/emails.py @@ -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: diff --git a/copanier/models.py b/copanier/models.py index 91ed5ef..a5fa0ed 100644 --- a/copanier/models.py +++ b/copanier/models.py @@ -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 @@ -185,6 +185,20 @@ class Product(Base): # if self.unit: # 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 @@ -387,9 +401,24 @@ 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()] \ No newline at end of file diff --git a/copanier/reports.py b/copanier/reports.py index ba79669..2efd61a 100644 --- a/copanier/reports.py +++ b/copanier/reports.py @@ -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, diff --git a/copanier/static/app.css b/copanier/static/app.css index 17fc706..8b8d024 100644 --- a/copanier/static/app.css +++ b/copanier/static/app.css @@ -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; diff --git a/copanier/templates/delivery.html b/copanier/templates/delivery.html index dcfb7cb..30f3b3b 100644 --- a/copanier/templates/delivery.html +++ b/copanier/templates/delivery.html @@ -3,13 +3,18 @@ {% block body %}

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

{% include "includes/delivery_head.html" %} +{% if request['user'].email == delivery.contact %} +
Vous êtes la personne référente de cette distribution Gérer la distribution
+{% elif request['user'].email in delivery.get_referents() %} +
Vous êtes référent⋅e pour cette distribution (merci !). Voici un petit lien pour aller voir les produits dont vous vous occupez !
+{% endif %}
{% if delivery.has_products %} {% include "includes/delivery_table.html" %} {% else %}

Aucun produit n'est encore défini pour cette livraison.

{% if request.user and request.user.is_staff %} -
Pour rajouter des produits, il faut d'abord télécharger le tableur avec les produits, le modifier localement sur votre machine puis  
{% with unique_id="import-products" %}{% include "includes/modal_import_products.html" %}{% endwith %}.
+
Pour rajouter des produits, il faut d'abord télécharger le tableur avec les produits, le modifier localement sur votre machine puis  
{% with unique_id="import-products" %}{% include "includes/modal_import_products.html" %}{% endwith %}.
{% endif %} {% endif %}
@@ -28,7 +33,7 @@ {% endif %} {% if request.user and request.user.is_staff %}
  • - Liste des produits + Liste des produits
  • Modifier la livraison diff --git a/copanier/templates/edit_delivery.html b/copanier/templates/edit_delivery.html index 48edc8d..a1478f8 100644 --- a/copanier/templates/edit_delivery.html +++ b/copanier/templates/edit_delivery.html @@ -16,7 +16,7 @@
  • +
  • + Gérer les produits / product⋅eur⋅rice⋅s +
  • {% endif %} {% endblock body %} diff --git a/copanier/templates/edit_producer.html b/copanier/templates/edit_producer.html new file mode 100644 index 0000000..40ec4e5 --- /dev/null +++ b/copanier/templates/edit_producer.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} + +{% block body %} +{% if producer.id %} +

    Modifier les informations pour « {{ producer.id }} »

    +{% else %} +

    Nouve⋅eau⋅lle product⋅eur⋅rice

    +{% endif %} +
    + {% if producer.id %} + + {% else %} + + {% endif %} + + + + +
    + +
    +
    + +{% if products %} +

    Produits Ajouter un produit

    + + + + + + + + + + + + + +{% for product in products %} + + + + + + + + + +{% endfor %} +
    ProduitPrixUnitéDescriptionPackagingRupture ?
    {{ product.name }}{{ product.price }}€{{ product.unit }}{{ product.description }}{% if product.packing %}{{ product.packing }}{% endif %}{% if product.rupture %}RUPTURE !!{% endif %}éditer
    +{% endif %} + +
    +{% endblock body %} diff --git a/copanier/templates/edit_product.html b/copanier/templates/edit_product.html new file mode 100644 index 0000000..963b640 --- /dev/null +++ b/copanier/templates/edit_product.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} + +{% block body %} +{% if product.ref %} +

    Modifier le produit « {{ product.name }} »

    +{% else %} +

    {{ producer_id }} : Nouveau produit

    +{% endif %} +
    + + + + + + +
    + +
    + +
    +
    + +{% if products %} +

    Produits

    + + + + + + + + + + + + + +{% for product in products %} + + + + + + + + + +{% endfor %} +
    ProduitPrixUnitéDescriptionPackagingRupture ?
    {{ product.name }}{{ product.price }}€{{ product.unit }}{{ product.description }}{% if product.packing %}{{ product.packing }}{% endif %}{% if product.rupture %}RUPTURE !!{% endif %}éditer
    +{% endif %} + +
    +{% endblock body %} diff --git a/copanier/templates/includes/delivery_table.html b/copanier/templates/includes/delivery_table.html index b951e87..7449361 100644 --- a/copanier/templates/includes/delivery_table.html +++ b/copanier/templates/includes/delivery_table.html @@ -1,6 +1,11 @@ -{% for producer in delivery.producers %} -

    {{ producer }}

    -

    Référent⋅e : {{ delivery.producers[producer].referent }} / {{ delivery.producers[producer].tel_referent }}

    +{% if referent %} +{% set producers = delivery.get_producers_for_referent(referent) %} +{% else %} +{% set producers = delivery.producers %} +{% endif %} +{% for producer in producers %} +

    {{ producer }} {% if edit_mode %}Éditer Ajouter un produit {% endif %}

    +
    {% if delivery.producers[producer].description %}{{ delivery.producers[producer].description }}{% endif %}. Référent⋅e : {{ delivery.producers[producer].referent }} / {{ delivery.producers[producer].tel_referent }}
    @@ -9,6 +14,8 @@ {% if delivery.has_packing %} {% endif %} + {% if edit_mode %}{% endif %} + {% if not list_only %} {% for orderer, order in delivery.orders.items() %} {% endfor %} + {% endif %} {% for product in delivery.get_products_by(producer) %} - {% if delivery.has_packing %} {% endif %} + {% if not list_only %} {{ 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() %} {% endfor %} + {% endif %} + {% if edit_mode %}{% endif %} {% endfor %} + {% if not list_only %} {% if delivery.has_packing %} @@ -50,6 +62,7 @@ {% endfor %} + {% endif %}
    ConditionnementÉditerTotal @@ -19,16 +26,18 @@ {% endif %}
    {{ product }} {% if product.rupture %}(RUPTURE !){% endif %} + {% if edit_mode %}{% endif %}{{ product }}{% if edit_mode %}{% endif %}{% if product.rupture %} {{ product.rupture }}{% endif %} {{ product.price | round(2) }} €{% if product.packing %}{{ product.packing }} x {% endif %} {{ product.unit }}{{ order[product.ref].quantity or "—" }}modifier
    Total{{ order.total(delivery.get_products_by(producer)) }} €
    {% endfor %} \ No newline at end of file diff --git a/copanier/templates/includes/modal_import_products.html b/copanier/templates/includes/modal_import_products.html index 4a8e7f5..70e24cb 100644 --- a/copanier/templates/includes/modal_import_products.html +++ b/copanier/templates/includes/modal_import_products.html @@ -1,6 +1,6 @@ {% extends "includes/modal.html" %} -{% block modal_label %} Importer les produits{% endblock modal_label %} +{% block modal_label %} Importer les produits depuis un tableur{% endblock modal_label %} {% block modal_body %}

    Importer des produits

    Format pris en charge: xlsx

    diff --git a/copanier/templates/list_products.html b/copanier/templates/list_products.html new file mode 100644 index 0000000..a3014b8 --- /dev/null +++ b/copanier/templates/list_products.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block body %} +

    {{ delivery.name }}

    +{% if referent %}

    Voici la liste des product⋅eurs⋅rices dont {{ referent }} est référent⋅e. voir tous les produits

    {% endif %} +
    +{% include "includes/delivery_table.html" %} +
    +{% endblock body %} diff --git a/copanier/templates/manage_delivery.html b/copanier/templates/manage_delivery.html new file mode 100644 index 0000000..2cb2451 --- /dev/null +++ b/copanier/templates/manage_delivery.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} + +{% block body %} +

    Gérer « {{ delivery.name }} »

    + +

    Avant et pendant la distribution

    +  Modifier la commande (dates, lieu, référent⋅e, etc) +  Modifier les produits, les product⋅rices⋅eurs +  Gérer les groupes / colocs + +

    Une fois les commandes passées (après le {{ delivery.order_before|date }})

    +  Télécharger le récap (global) des commandes +  Envoyer les infos de commande aux référent⋅e⋅s + +

    Pour préparer la distribution (le {{ delivery.from_date|date }})

    +  Fiches de commandes par groupe +  Télécharger le résumé général des commandes +  Faire la répartition des paiements + +{% endblock %} \ No newline at end of file diff --git a/copanier/templates/prepare_referent_email.html b/copanier/templates/prepare_referent_email.html new file mode 100644 index 0000000..c81c635 --- /dev/null +++ b/copanier/templates/prepare_referent_email.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block body %} +

    Envoi d'un mail aux référent⋅e⋅s

    +

    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.

    + +
    +
    +
    + +
    +{% endblock %} \ No newline at end of file diff --git a/copanier/templates/signing_sheet.html b/copanier/templates/signing_sheet.html index 9ef43f9..8280f71 100644 --- a/copanier/templates/signing_sheet.html +++ b/copanier/templates/signing_sheet.html @@ -9,7 +9,7 @@

    {{ delivery.name }} {{ delivery.from_date.date() }} - liste d'émargement

    {% for email, order in delivery.orders.items() %} -

    {{ email }}

    +

    {{ request.groups.groups[email].name }}

    {% include "includes/order_summary.html" %}
    {% endfor %} diff --git a/tests/test_views.py b/tests/test_views.py index ff56793..e460334 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -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"),