diff --git a/copanier/__init__.py b/copanier/__init__.py index 10a092f..feee140 100644 --- a/copanier/__init__.py +++ b/copanier/__init__.py @@ -14,7 +14,7 @@ from collections import defaultdict from functools import partial from . import config, reports, session, utils, emails, loggers, imports -from .models import Delivery, Order, Person, Product, ProductOrder, Groups, Group +from .models import Delivery, Order, Person, Product, ProductOrder, Groups, Group class Response(Response): @@ -109,17 +109,14 @@ async def auth_required(request, response): if not email: response.redirect = f"/sésame?next={request.path}" return response - + groups = Groups.load() request["groups"] = groups group = groups.get_user_group(email) - user_info = {'email': email} + user_info = {"email": email} if group: - user_info.update(dict( - group_id=group.id, - group_name=group.name) - ) + user_info.update(dict(group_id=group.id, group_name=group.name)) user = Person(**user_info) request["user"] = user session.user.set(user) @@ -192,14 +189,15 @@ async def logout(request, response): @app.route("/", methods=["GET"]) async def home(request, response): - if not request['user'].group_id: + if not request["user"].group_id: response.redirect = "/groupes" return response.html( "home.html", incoming=Delivery.incoming(), former=Delivery.former(), - archives=list(Delivery.all(is_archived=True))) + archives=list(Delivery.all(is_archived=True)), + ) @app.route("/groupes", methods=["GET"]) @@ -211,9 +209,9 @@ async def handle_groups(request, response): async def join_group(request, response, id): user = session.user.get(None) group = request["groups"].add_user(user.email, id) - request['groups'].persist() - redirect = '/' if not request['user'].group_id else '/groupes' - + request["groups"].persist() + redirect = "/" if not request["user"].group_id else "/groupes" + response.message(f"Vous avez bien rejoint le groupe '{group.name}'") response.redirect = redirect @@ -224,16 +222,15 @@ async def create_group(request, response): if request.method == "POST": form = request.form members = [] - if form.get('members'): - members = [m.strip() for m in form.get('members').split(',')] - - if not request['user'].group_id and request['user'].email not in members: - members.append(request['user'].email) - + if form.get("members"): + members = [m.strip() for m in form.get("members").split(",")] + + if not request["user"].group_id and request["user"].email not in members: + members.append(request["user"].email) + group = Group.create( - id=slugify(form.get('name')), - name=form.get('name'), - members=members) + id=slugify(form.get("name")), name=form.get("name"), members=members + ) request["groups"].add_group(group) request["groups"].persist() response.message(f"Le groupe {group.name} à bien été créé") @@ -248,10 +245,10 @@ async def edit_group(request, response, id): if request.method == "POST": form = request.form members = [] - if form.get('members'): - members = [m.strip() for m in form.get('members').split(',')] + if form.get("members"): + members = [m.strip() for m in form.get("members").split(",")] group.members = members - group.name = form.get('name') + group.name = form.get("name") request["groups"].groups[id] = group request["groups"].persist() response.redirect = "/groupes" @@ -344,68 +341,77 @@ async def import_products(request, response, id): @app.route("/livraison/{delivery_id}/producteurices") async def list_producers(request, response, delivery_id): delivery = Delivery.load(delivery_id) - response.html("list_products.html", { - 'edit_mode': True, - 'delivery': delivery, - 'referent': request.query.get('referent', None), - }) - + response.html( + "list_products.html", + { + "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': + 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') + 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) - }) + 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"]) +@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': + 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') + 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") else: product.packing = None - if 'rupture' in form: - product.rupture = form.get('rupture') + if "rupture" in form: + product.rupture = form.get("rupture") else: product.rupture = None delivery.persist() - response.message('Le produit à bien été modifié') - response.redirect = f'/livraison/{delivery_id}/{producer_id}/éditer' + response.message("Le produit à bien été modifié") + response.redirect = f"/livraison/{delivery_id}/{producer_id}/éditer" - response.html("edit_product.html", { - 'delivery': delivery, - 'product': product - }) + response.html("edit_product.html", {"delivery": delivery, "product": product}) -@app.route("/livraison/{delivery_id}/{producer_id}/ajouter-produit", methods=["GET", "POST"]) + +@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': + if request.method == "POST": product.producer = producer_id form = request.form product.update_from_form(form) @@ -413,63 +419,75 @@ async def create_product(request, response, delivery_id, producer_id): delivery.products.append(product) delivery.persist() - response.message('Le produit à bien été créé') - response.redirect = f'/livraison/{delivery_id}/producteurice/{producer_id}/éditer' + 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, - }) + response.html( + "edit_product.html", + {"delivery": delivery, "producer_id": producer_id, "product": product}, + ) -@app.route("/livraison/{id}/gérer", methods=['GET']) + +@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, - 'referents': [p.referent for p in delivery.producers.values()] - }) + response.html( + "manage_delivery.html", + { + "delivery": delivery, + "referents": [p.referent for p in delivery.producers.values()], + }, + ) -@app.route("/livraison/{id}/envoi-email-referentes", methods=['GET', 'POST']) + +@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') + 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) - ]) + 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 - }) + response.html("prepare_referent_email.html", {"delivery": delivery}) -@app.route("/livraison/{id}/bon-de-commande-referent⋅e", methods=['GET']) +@app.route("/livraison/{id}/bon-de-commande-referent⋅e", methods=["GET"]) async def download_referent_summary(request, response, id): delivery = Delivery.load(id) date = delivery.to_date.strftime("%Y-%m-%d") - if not request['user'].is_referent(delivery): + if not request["user"].is_referent(delivery): return - referent = request['user'].email + referent = request["user"].email producers = delivery.get_producers_for_referent(referent) summary = reports.summary(delivery, producers) response.xlsx(summary, filename=f"{config.SITE_NAME}-{date}-{referent}.xlsx") -@app.route("/livraison/{id}/product⋅eur⋅rice/{producer}/bon-de-commande", methods=["GET"]) +@app.route( + "/livraison/{id}/product⋅eur⋅rice/{producer}/bon-de-commande", methods=["GET"] +) async def download_producer_report(request, response, id, producer): delivery = Delivery.load(id) - summary = reports.summary(delivery, [producer, ]) + summary = reports.summary(delivery, [producer]) date = delivery.to_date.strftime("%Y-%m-%d") - response.xlsx(summary, filename=f"{config.SITE_NAME}-{date}-{producer}-bon-de-commande.xlsx") - + response.xlsx( + summary, filename=f"{config.SITE_NAME}-{date}-{producer}-bon-de-commande.xlsx" + ) + @app.route("/livraison/{id}/exporter", methods=["GET"]) async def export_products(request, response, id): @@ -524,13 +542,13 @@ async def place_order(request, response, id): return if request.method == "POST": - + # When the delivery is closed, only staff can access. - if delivery.status == delivery.CLOSED and not (user and user.is_staff) : + if delivery.status == delivery.CLOSED and not (user and user.is_staff): 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: @@ -546,7 +564,7 @@ async def place_order(request, response, id): order.products[product.ref] = ProductOrder( wanted=wanted, adjustment=adjustment ) - + if not delivery.orders: delivery.orders = {} @@ -559,21 +577,31 @@ async def place_order(request, response, id): return delivery.orders[orderer.id] = order delivery.persist() - + if user and orderer.id == user.id: # Only send email if order has been placed by the user itself. # Send the emails to everyone in the group. - groups = request['groups'].groups + groups = request["groups"].groups if orderer.group_id in groups.keys(): for email in groups[orderer.group_id].members: emails.send_order( - request, env, person=Person(email=email), delivery=delivery, order=order + request, + env, + person=Person(email=email), + delivery=delivery, + order=order, ) else: emails.send_order( - request, env, person=Person(email=orderer.email), delivery=delivery, order=order - ) - response.message(f"La commande pour « {orderer.name} » a bien été prise en compte!") + request, + env, + person=Person(email=orderer.email), + delivery=delivery, + order=order, + ) + response.message( + f"La commande pour « {orderer.name} » a bien été prise en compte!" + ) response.redirect = f"/livraison/{delivery.id}" else: order = delivery.orders.get(orderer.id) or Order() @@ -630,11 +658,11 @@ async def import_multiple_commands(request, response, id): request.files.get("data").read().decode().splitlines(), delimiter=";" ) orders = defaultdict(Order) - + current_ref = None for row in reader: for label, value in row.items(): - if label == 'ref': + if label == "ref": current_ref = value else: wanted = int(value or 0) @@ -652,14 +680,20 @@ async def import_multiple_commands(request, response, id): async def xls_report(request, response, id): delivery = Delivery.load(id) date = delivery.to_date.strftime("%Y-%m-%d") - response.xlsx(reports.summary(delivery), filename=f"{config.SITE_NAME}-{date}-bon-de-commande.xlsx") + response.xlsx( + reports.summary(delivery), + filename=f"{config.SITE_NAME}-{date}-bon-de-commande.xlsx", + ) @app.route("/livraison/{id}/rapport-complet.xlsx", methods=["GET"]) async def xls_full_report(request, response, id): delivery = Delivery.load(id) date = delivery.to_date.strftime("%Y-%m-%d") - response.xlsx(reports.full(delivery), filename=f"{config.SITE_NAME}-{date}-rapport-complet.xlsx") + response.xlsx( + reports.full(delivery), + filename=f"{config.SITE_NAME}-{date}-rapport-complet.xlsx", + ) @app.route("/livraison/{id}/ajuster/{ref}", methods=["GET", "POST"]) @@ -691,23 +725,23 @@ async def adjust_product(request, response, id, ref): @staff_only async def delivery_balance(request, response, id): delivery = Delivery.load(id) - groups = request['groups'] + groups = request["groups"] delivery_url = f"/livraison/{delivery.id}" balance = [] for group_id, order in delivery.orders.items(): balance.append((group_id, order.total(delivery.products) * -1)) - + for producer in delivery.producers.values(): group = groups.get_user_group(producer.referent) - if hasattr(group, 'id'): + 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)) - + debiters, crediters = order_balance(balance) check_balance(debiters, crediters) results = reduce_balance(debiters[:], crediters[:]) @@ -717,13 +751,16 @@ async def delivery_balance(request, response, id): for debiter, amount, crediter in results: results_dict[debiter][crediter] = amount - response.html("delivery_balance.html", { - "delivery": delivery, - "debiters": debiters, - "crediters": crediters, - "results": results_dict, - "groups": groups.groups, - }) + response.html( + "delivery_balance.html", + { + "delivery": delivery, + "debiters": debiters, + "crediters": crediters, + "results": results_dict, + "groups": groups.groups, + }, + ) @app.route("/livraison/{id}/solde.xlsx", methods=["GET"]) diff --git a/copanier/emails.py b/copanier/emails.py index 4f7ce44..a4ac7ff 100644 --- a/copanier/emails.py +++ b/copanier/emails.py @@ -13,27 +13,28 @@ def send(to, subject, body, html=None, copy=None, attachments=None): if not attachments: attachments = [] - msg = MIMEMultipart('alternative') + msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = config.FROM_EMAIL msg["To"] = to 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 = MIMEBase( + "application", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8", + ) part.set_payload(attachment) - part.add_header('Content-Disposition', - 'attachment', - filename=file_name) + part.add_header("Content-Disposition", "attachment", filename=file_name) encoders.encode_base64(part) msg.attach(part) msg.attach(MIMEText(body, "plain")) if html: msg.attach(MIMEText(html, "html")) - + if not config.SEND_EMAILS: - body = body.replace('https', 'http') + body = body.replace("https", "http") return print("Sending email", str(body)) try: server = smtplib.SMTP_SSL(config.SMTP_HOST) @@ -61,5 +62,5 @@ def send_order(request, env, person, delivery, order): display_prices=True, order=order, delivery=delivery, - request=request + request=request, ) diff --git a/copanier/imports.py b/copanier/imports.py index d9f1afd..89094ab 100644 --- a/copanier/imports.py +++ b/copanier/imports.py @@ -47,9 +47,13 @@ def products_and_producers_from_xlsx(delivery, data): raise ValueError("Le fichier doit comporter deux onglets.") # First, get the products data from the first tab. products_sheet = data.get_sheet_by_name(sheet_names[0]) - delivery.products = items_from_xlsx(list(products_sheet.values), [], Product, PRODUCT_FIELDS, append_list) - + delivery.products = items_from_xlsx( + list(products_sheet.values), [], Product, PRODUCT_FIELDS, append_list + ) + # Then import producers info producers_sheet = data.get_sheet_by_name(sheet_names[1]) - delivery.producers = items_from_xlsx(list(producers_sheet.values), {}, Producer, PRODUCER_FIELDS, append_dict) - delivery.persist() \ No newline at end of file + delivery.producers = items_from_xlsx( + list(producers_sheet.values), {}, Producer, PRODUCER_FIELDS, append_dict + ) + delivery.persist() diff --git a/copanier/models.py b/copanier/models.py index 2e4e45b..24a3ed8 100644 --- a/copanier/models.py +++ b/copanier/models.py @@ -74,15 +74,14 @@ class Base: def dump(self): return yaml.dump(asdict(self), allow_unicode=True) + @dataclass class PersistedBase(Base): - @classmethod def get_root(cls): return Path(config.DATA_ROOT) / cls.__root__ - @dataclass class Person(Base): email: str @@ -94,14 +93,14 @@ class Person(Base): @property def is_staff(self): return not config.STAFF or self.email in config.STAFF - + def is_referent(self, delivery): return self.email in delivery.get_referents() or self.email == delivery.contact - + @property def id(self): return self.group_id or self.email - + @property def name(self): return self.group_name or self.email @@ -119,7 +118,7 @@ class Groups(PersistedBase): __root__ = "groups" __lock__ = threading.Lock() groups: Dict[str, Group] - + @classmethod def load(cls): path = cls.get_root() / "groups.yml" @@ -127,19 +126,19 @@ class Groups(PersistedBase): data = yaml.safe_load(path.read_text()) data = {k: v for k, v in data.items() if k in cls.__dataclass_fields__} else: - data = {'groups': {}} + data = {"groups": {}} groups = cls(**data) groups.path = path return groups - + def persist(self): with self.__lock__: self.path.write_text(self.dump()) - + def add_group(self, group): assert group.id not in self.groups, "Un groupe avec ce nom existe déjà." self.groups[group.id] = group - + def add_user(self, email, group_id): self.remove_user(email) group = self.groups[group_id] @@ -155,11 +154,12 @@ class Groups(PersistedBase): for group in self.groups.values(): if email in group.members: return group - + @classmethod def init_fs(cls): cls.get_root().mkdir(parents=True, exist_ok=True) + @dataclass class Producer(Base): id: str @@ -187,17 +187,17 @@ 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') + 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 @@ -282,7 +282,7 @@ class Delivery(PersistedBase): if self.needs_adjustment: return self.ADJUSTMENT return self.CLOSED - + @property def has_products(self): return len(self.products) > 0 @@ -302,7 +302,7 @@ class Delivery(PersistedBase): @property def is_passed(self): return not self.is_foreseen - + @property def can_generate_reports(self): return not self.is_open and not self.needs_adjustment @@ -399,7 +399,7 @@ 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: @@ -410,13 +410,13 @@ class Delivery(PersistedBase): 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() + 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 + return [producer.referent for producer in self.producers.values()] diff --git a/copanier/reports.py b/copanier/reports.py index 2efd61a..2d53318 100644 --- a/copanier/reports.py +++ b/copanier/reports.py @@ -5,6 +5,7 @@ from openpyxl.writer.excel import save_virtual_workbook from .models import Product, Producer + def summary_for_products(wb, title, delivery, total=None, products=None): if products == None: products = delivery.products @@ -12,26 +13,23 @@ def summary_for_products(wb, title, delivery, total=None, products=None): total = delivery.total ws = wb.create_sheet(title) - ws.append([ - "ref", - "produit", - "prix unitaire", - "quantité commandée", - "unité", - "total", - ]) + ws.append( + ["ref", "produit", "prix unitaire", "quantité commandée", "unité", "total"] + ) for product in products: wanted = delivery.product_wanted(product) if not wanted: continue - ws.append([ - product.ref, - str(product), - product.price, - wanted, - product.unit, - round(product.price * wanted, 2), - ]) + ws.append( + [ + product.ref, + str(product), + product.price, + wanted, + product.unit, + round(product.price * wanted, 2), + ] + ) ws.append(["", "", "", "", "Total", total]) @@ -46,9 +44,9 @@ def summary(delivery, producers=None): producer, delivery, total=delivery.total_for_producer(producer), - products=delivery.get_products_by(producer) + products=delivery.get_products_by(producer), ) - + return save_virtual_workbook(wb) @@ -69,7 +67,7 @@ def full(delivery): ws.append(row) footer = ( ["Total", "", ""] - + [round(o.total(delivery.products),2) for o in delivery.orders.values()] + + [round(o.total(delivery.products), 2) for o in delivery.orders.values()] + [round(delivery.total, 2)] ) footer.insert(1, "") @@ -92,7 +90,7 @@ def products(delivery): producer_sheet.append(producer_fields) for producer in delivery.producers.values(): producer_sheet.append([getattr(producer, field) for field in producer_fields]) - + return save_virtual_workbook(wb) @@ -105,4 +103,4 @@ def balance(delivery): ws.append( [email, order.total(delivery.products), "oui" if order.paid else "non"] ) - return save_virtual_workbook(wb) \ No newline at end of file + return save_virtual_workbook(wb)