mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 19:42:37 +02:00
Reorganize the files,
I've split everything in different concepts (groups, login, products and delivery) so that it's easier to find what we're looking for (the __init__.py file was really too big).
This commit is contained in:
parent
11f6d7054b
commit
b451a82c7b
57 changed files with 1336 additions and 1338 deletions
32
TODO
32
TODO
|
@ -1,21 +1,25 @@
|
|||
Repenser l'interface pour la rendre plus simple
|
||||
Refaire une interface plus simple (moins de boutons partout, qu'on s'y retrouve plus)
|
||||
Uniformiser le nom des choses. Livraison → Distribution
|
||||
Éditer directement depuis la vue « distribution »
|
||||
x Ajouter un menu pour revenir à l'élément parent facilement
|
||||
x Repenser l'interface pour la rendre plus simple
|
||||
x Refaire une interface plus simple (moins de boutons partout, qu'on s'y retrouve plus)
|
||||
x Uniformiser le nom des choses. Livraison → Distribution
|
||||
x Ajouter un numéro de téléphone de la personne référente de la commande
|
||||
x Il semble que les prix trop précis ne sont pas acceptés
|
||||
x Utiliser une version lowercase des emails
|
||||
x Permettre l'ajout de produits
|
||||
x Ajouter un moyen d'ajouter un⋅e producteurice
|
||||
x Ajouter un moyen de changer le nom de la personne référente pour un producteur / productrice
|
||||
x Faciliter la duplication de distribution
|
||||
x Si un produit est en rupture de stock, alors il n'est pas compté dans les totaux
|
||||
x Permettre la supression des produits
|
||||
|
||||
Permettre la suppression de producteurs
|
||||
|
||||
Ajouter un numéro de téléphone de la personne référente de la commande
|
||||
Permettre la supression des produits
|
||||
Gérer les frais de livraison
|
||||
Bug par rapport aux ruptures
|
||||
|
||||
Rendre plus visible l'action de modifier une commande
|
||||
|
||||
Ajouter la trame (agenda d’une distribution) dans la boite à outil du coordinateur
|
||||
Explorer la possibilité de faire des ajustements automatiques
|
||||
Il semble que les prix trop précis ne sont pas acceptés
|
||||
utiliser une version lowercase des emails
|
||||
|
||||
Permettre l'ajout de produits
|
||||
Ajouter un moyen d'ajouter un⋅e producteurice
|
||||
Ajouter un moyen de changer le nom de la personne référente pour un producteur / productrice
|
||||
Explorer la possibilité de faire des ajustements automatiques
|
||||
Ajouter une info « prix mis à jour » pour les référent⋅e⋅s
|
||||
Faciliter la duplication de distribution
|
||||
Ajouter une note explicative pour la répartition des chèques
|
|
@ -1,828 +1,9 @@
|
|||
import csv
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from functools import partial
|
||||
|
||||
import ujson as json
|
||||
import minicli
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from roll import Roll as BaseRoll, Response as RollResponse, HttpError
|
||||
from roll.extensions import traceback, simple_server, static
|
||||
from slugify import slugify
|
||||
from roll.extensions import simple_server, static
|
||||
|
||||
from debts.solver import order_balance, check_balance, reduce_balance
|
||||
from weasyprint import HTML
|
||||
|
||||
from . import config, reports, session, utils, emails, loggers, imports
|
||||
from .models import Delivery, Order, Person, Product, ProductOrder, Groups, Group
|
||||
|
||||
|
||||
class Response(RollResponse):
|
||||
def render_template(self, template_name, *args, **kwargs):
|
||||
context = app.context()
|
||||
context.update(kwargs)
|
||||
context["request"] = self.request
|
||||
context["config"] = config
|
||||
context["request"] = self.request
|
||||
if self.request.cookies.get("message"):
|
||||
context["message"] = json.loads(self.request.cookies["message"])
|
||||
self.cookies.set("message", "")
|
||||
return env.get_template(template_name).render(*args, **context)
|
||||
|
||||
def html(self, template_name, *args, **kwargs):
|
||||
self.headers["Content-Type"] = "text/html; charset=utf-8"
|
||||
self.body = self.render_template(template_name, *args, **kwargs)
|
||||
|
||||
def render_pdf(self, template_name, *args, **kwargs):
|
||||
html = self.render_template(template_name, *args, **kwargs)
|
||||
|
||||
static_folder = Path(__file__).parent / "static"
|
||||
stylesheets = [
|
||||
static_folder / "app.css",
|
||||
static_folder / "icomoon.css",
|
||||
static_folder / "page.css",
|
||||
]
|
||||
if "css" in kwargs:
|
||||
stylesheets.append(static_folder / kwargs["css"])
|
||||
|
||||
return HTML(string=html).write_pdf(stylesheets=stylesheets)
|
||||
|
||||
def pdf(self, template_name, *args, **kwargs):
|
||||
self.body = self.render_pdf(template_name, *args, **kwargs)
|
||||
mimetype = "application/pdf"
|
||||
filename = kwargs.get("filename", "file.pdf")
|
||||
self.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
self.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||
|
||||
def xlsx(self, body, filename=f"{config.SITE_NAME}.xlsx"):
|
||||
self.body = body
|
||||
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
self.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
self.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||
|
||||
def redirect(self, location):
|
||||
self.status = 302
|
||||
self.headers["Location"] = location
|
||||
|
||||
redirect = property(None, redirect)
|
||||
|
||||
def message(self, text, status="success"):
|
||||
self.cookies.set("message", json.dumps((text, status)))
|
||||
|
||||
|
||||
class Roll(BaseRoll):
|
||||
Response = Response
|
||||
|
||||
_context_func = []
|
||||
|
||||
def context(self):
|
||||
context = {}
|
||||
for func in self._context_func:
|
||||
context.update(func())
|
||||
return context
|
||||
|
||||
def register_context(self, func):
|
||||
self._context_func.append(func)
|
||||
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader("copanier", "templates"),
|
||||
autoescape=select_autoescape(["copanier"]),
|
||||
)
|
||||
|
||||
|
||||
def date_filter(value):
|
||||
return value.strftime(r"%A %d %B")
|
||||
|
||||
|
||||
def time_filter(value):
|
||||
return value.strftime(r"%H:%M")
|
||||
|
||||
|
||||
env.filters["date"] = date_filter
|
||||
env.filters["time"] = time_filter
|
||||
|
||||
app = Roll()
|
||||
traceback(app)
|
||||
|
||||
|
||||
def staff_only(view):
|
||||
async def decorator(request, response, *args, **kwargs):
|
||||
user = session.user.get(None)
|
||||
if not user or not user.is_staff:
|
||||
response.message("Désolé, c'est réservé au staff par ici", "warning")
|
||||
response.redirect = request.headers.get("REFERRER", "/")
|
||||
return
|
||||
return await view(request, response, *args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def auth_required(request, response):
|
||||
# Should be handler Roll side?
|
||||
# In dev mode, we serve the static, but we don't have yet a way to mark static
|
||||
# route as unprotected.
|
||||
if request.path.startswith("/static/"):
|
||||
return
|
||||
if request.route.payload and not request.route.payload.get("unprotected"):
|
||||
token = request.cookies.get("token")
|
||||
email = None
|
||||
if token:
|
||||
decoded = utils.read_token(token)
|
||||
email = decoded.get("sub")
|
||||
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}
|
||||
if group:
|
||||
user_info.update(dict(group_id=group.id, group_name=group.name))
|
||||
user = Person(**user_info)
|
||||
request["user"] = user
|
||||
session.user.set(user)
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def attach_request(request, response):
|
||||
response.request = request
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def log_request(request, response):
|
||||
if request.method == "POST":
|
||||
message = {
|
||||
"date": utils.utcnow().isoformat(),
|
||||
"data": request.form,
|
||||
"user": request.get("user"),
|
||||
}
|
||||
loggers.request_logger.info(
|
||||
json.dumps(message, sort_keys=True, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
@app.listen("startup")
|
||||
async def on_startup():
|
||||
configure()
|
||||
Delivery.init_fs()
|
||||
Groups.init_fs()
|
||||
|
||||
|
||||
@app.route("/sésame", methods=["GET"], unprotected=True)
|
||||
async def sesame(request, response):
|
||||
response.html("sesame.html")
|
||||
|
||||
|
||||
@app.route("/sésame", methods=["POST"], unprotected=True)
|
||||
async def send_sesame(request, response):
|
||||
email = request.form.get("email")
|
||||
token = utils.create_token(email)
|
||||
try:
|
||||
emails.send_from_template(
|
||||
env,
|
||||
"access_granted",
|
||||
email,
|
||||
f"Sésame {config.SITE_NAME}",
|
||||
hostname=request.host,
|
||||
token=token.decode(),
|
||||
)
|
||||
except RuntimeError:
|
||||
response.message("Oops, impossible d'envoyer le courriel…", status="error")
|
||||
else:
|
||||
response.message(f"Un sésame vous a été envoyé à l'adresse '{email}'")
|
||||
response.redirect = "/"
|
||||
|
||||
|
||||
@app.route("/sésame/{token}", methods=["GET"], unprotected=True)
|
||||
async def set_sesame(request, response, token):
|
||||
decoded = utils.read_token(token)
|
||||
if not decoded:
|
||||
response.message("Sésame invalide :(", status="error")
|
||||
else:
|
||||
response.message("Yay! Le sésame a fonctionné. Bienvenue à bord! :)")
|
||||
response.cookies.set(
|
||||
name="token", value=token, httponly=True, max_age=60 * 60 * 24 * 7
|
||||
)
|
||||
response.redirect = "/"
|
||||
|
||||
|
||||
@app.route("/déconnexion", methods=["GET"])
|
||||
async def logout(request, response):
|
||||
response.cookies.set(name="token", value="", httponly=True)
|
||||
response.redirect = "/"
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
async def home(request, response):
|
||||
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)),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/groupes", methods=["GET"])
|
||||
async def handle_groups(request, response):
|
||||
response.html("groups.html", {"groups": request["groups"]})
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/rejoindre", method=["GET"])
|
||||
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"
|
||||
|
||||
response.message(f"Vous avez bien rejoint le groupe '{group.name}'")
|
||||
response.redirect = redirect
|
||||
|
||||
|
||||
@app.route("/groupes/créer", methods=["GET", "POST"])
|
||||
async def create_group(request, response):
|
||||
group = None
|
||||
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)
|
||||
|
||||
group = Group.create(
|
||||
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éé")
|
||||
response.redirect = "/"
|
||||
response.html("edit_group.html", group=group)
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/éditer", methods=["GET", "POST"])
|
||||
async def edit_group(request, response, id):
|
||||
assert id in request["groups"].groups, "Impossible de trouver le groupe"
|
||||
group = request["groups"].groups[id]
|
||||
if request.method == "POST":
|
||||
form = request.form
|
||||
members = []
|
||||
if form.get("members"):
|
||||
members = [m.strip() for m in form.get("members").split(",")]
|
||||
group.members = members
|
||||
group.name = form.get("name")
|
||||
request["groups"].groups[id] = group
|
||||
request["groups"].persist()
|
||||
response.redirect = "/groupes"
|
||||
response.html("edit_group.html", group=group)
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/supprimer", methods=["GET"])
|
||||
async def delete_group(request, response, id):
|
||||
assert id in request["groups"].groups, "Impossible de trouver le groupe"
|
||||
deleted = request["groups"].groups.pop(id)
|
||||
request["groups"].persist()
|
||||
response.message(f"Le groupe {deleted.name} à bien été supprimé")
|
||||
response.redirect = "/groupes"
|
||||
|
||||
|
||||
@app.route("/archives", methods=["GET"])
|
||||
async def view_archives(request, response):
|
||||
response.html("archive.html", {"deliveries": Delivery.all(is_archived=True)})
|
||||
|
||||
|
||||
@app.route("/livraison/archive/{id}", methods=["GET"])
|
||||
async def view_archive(request, response, id):
|
||||
delivery = Delivery.load(f"archive/{id}")
|
||||
response.html("delivery.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/archiver", methods=["GET"])
|
||||
@staff_only
|
||||
async def archive_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
delivery.archive()
|
||||
response.message("La livraison a été archivée")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison/archive/{id}/désarchiver", methods=["GET"])
|
||||
@staff_only
|
||||
async def unarchive_delivery(request, response, id):
|
||||
delivery = Delivery.load(f"archive/{id}")
|
||||
delivery.unarchive()
|
||||
response.message("La livraison a été désarchivée")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison", methods=["GET"])
|
||||
async def new_delivery(request, response):
|
||||
response.html("edit_delivery.html", delivery={})
|
||||
|
||||
|
||||
@app.route("/livraison", methods=["POST"])
|
||||
@staff_only
|
||||
async def create_delivery(request, response):
|
||||
form = request.form
|
||||
data = {}
|
||||
data["from_date"] = f"{form.get('date')} {form.get('from_time')}"
|
||||
data["to_date"] = f"{form.get('date')} {form.get('to_time')}"
|
||||
for name in Delivery.__dataclass_fields__.keys():
|
||||
if name in form:
|
||||
data[name] = form.get(name)
|
||||
delivery = Delivery(**data)
|
||||
delivery.persist()
|
||||
response.message("La livraison a bien été créée!")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/importer/produits", methods=["POST"])
|
||||
@staff_only
|
||||
async def import_products(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
delivery.products = []
|
||||
data = request.files.get("data")
|
||||
error_path = f"/livraison/{delivery.id}/edit"
|
||||
|
||||
if data.filename.endswith(".xlsx"):
|
||||
try:
|
||||
imports.products_and_producers_from_xlsx(delivery, data)
|
||||
except ValueError as err:
|
||||
message = f"Impossible d'importer le fichier. {err.args[0]}"
|
||||
response.message(message, status="error")
|
||||
response.redirect = error_path
|
||||
return
|
||||
else:
|
||||
response.message("Format de fichier inconnu", status="error")
|
||||
response.redirect = error_path
|
||||
return
|
||||
response.message("Les produits et producteur⋅ice⋅s ont bien été mis à jour!")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/producteurices")
|
||||
@app.route("/livraison/{id}/producteurices.pdf")
|
||||
async def list_producers(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
template_name = "list_products.html"
|
||||
template_params = {
|
||||
"edit_mode": True,
|
||||
"list_only": True,
|
||||
"delivery": delivery,
|
||||
"referent": request.query.get("referent", None),
|
||||
}
|
||||
|
||||
if request.url.endswith(b".pdf"):
|
||||
template_params["edit_mode"] = False
|
||||
response.pdf(
|
||||
template_name,
|
||||
template_params,
|
||||
filename=utils.prefix("producteurices.pdf", delivery),
|
||||
)
|
||||
else:
|
||||
response.html(template_name, template_params)
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/{producer}/bon-de-commande.pdf", methods=["GET"])
|
||||
async def pdf_for_producer(request, response, id, producer):
|
||||
delivery = Delivery.load(id)
|
||||
date = delivery.to_date.strftime("%Y-%m-%d")
|
||||
response.pdf(
|
||||
"list_products.html",
|
||||
{"list_only": True, "delivery": delivery, "producers": [producer]},
|
||||
filename=utils.prefix(f"bon-de-commande-{producer}.pdf", delivery),
|
||||
)
|
||||
|
||||
|
||||
@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.referent_tel = form.get("referent_tel")
|
||||
producer.referent_name = form.get("referent_name")
|
||||
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")
|
||||
else:
|
||||
product.packing = None
|
||||
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.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 delivery_toolbox(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html(
|
||||
"delivery_toolbox.html",
|
||||
{
|
||||
"delivery": delivery,
|
||||
"referents": [p.referent for p in delivery.producers.values()],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/envoi-email-referentes", methods=["GET", "POST"])
|
||||
async def send_referent_emails(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
if request.method == "POST":
|
||||
email_body = request.form.get("email_body")
|
||||
email_subject = request.form.get("email_subject")
|
||||
sent_mails = 0
|
||||
for referent in delivery.get_referents():
|
||||
producers = delivery.get_producers_for_referent(referent)
|
||||
attachments = []
|
||||
for producer in producers:
|
||||
if delivery.producers[producer].has_active_products(delivery):
|
||||
pdf_file = response.render_pdf(
|
||||
"list_products.html",
|
||||
{
|
||||
"list_only": True,
|
||||
"delivery": delivery,
|
||||
"producers": [producer],
|
||||
},
|
||||
)
|
||||
|
||||
attachments.append(
|
||||
(
|
||||
utils.prefix(f"{producer}.pdf", delivery),
|
||||
pdf_file,
|
||||
"application/pdf",
|
||||
)
|
||||
)
|
||||
|
||||
if attachments:
|
||||
sent_mails = sent_mails + 1
|
||||
emails.send(
|
||||
referent,
|
||||
email_subject,
|
||||
email_body,
|
||||
copy=delivery.contact,
|
||||
attachments=attachments,
|
||||
)
|
||||
response.message(f"Un mail à été envoyé aux {sent_mails} référent⋅e⋅s")
|
||||
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))
|
||||
|
||||
|
||||
@app.route("/livraison/archive/{id}/exporter/produits", methods=["GET"])
|
||||
async def export_archived_products(request, response, id):
|
||||
delivery = Delivery.load(f"archive/{id}")
|
||||
response.xlsx(reports.products(delivery))
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/edit", methods=["GET"])
|
||||
@staff_only
|
||||
async def edit_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html("edit_delivery.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/edit", methods=["POST"])
|
||||
@staff_only
|
||||
async def post_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
form = request.form
|
||||
delivery.from_date = f"{form.get('date')} {form.get('from_time')}"
|
||||
delivery.to_date = f"{form.get('date')} {form.get('to_time')}"
|
||||
for name in Delivery.__dataclass_fields__.keys():
|
||||
if name in form:
|
||||
setattr(delivery, name, form.get(name))
|
||||
delivery.persist()
|
||||
response.message("La livraison a bien été mise à jour!")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison/{id}", methods=["GET"])
|
||||
async def view_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html("delivery.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/commander", methods=["POST", "GET"])
|
||||
async def place_order(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
# email = request.query.get("email", None)
|
||||
user = session.user.get(None)
|
||||
orderer = request.query.get("orderer", None)
|
||||
if orderer:
|
||||
orderer = Person(email=orderer, group_id=orderer)
|
||||
|
||||
delivery_url = f"/livraison/{delivery.id}"
|
||||
if not orderer and user:
|
||||
orderer = user
|
||||
|
||||
if not orderer:
|
||||
response.message("Impossible de comprendre pour qui passer commande…", "error")
|
||||
response.redirect = delivery_url
|
||||
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):
|
||||
response.message("La livraison est fermée", "error")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
|
||||
form = request.form
|
||||
order = Order(phone_number=form.get('phone_number', ''))
|
||||
for product in delivery.products:
|
||||
try:
|
||||
wanted = form.int(f"wanted:{product.ref}", 0)
|
||||
except HttpError:
|
||||
continue
|
||||
try:
|
||||
adjustment = form.int(f"adjustment:{product.ref}", 0)
|
||||
except HttpError:
|
||||
adjustment = 0
|
||||
if wanted or adjustment:
|
||||
order.products[product.ref] = ProductOrder(
|
||||
wanted=wanted, adjustment=adjustment
|
||||
)
|
||||
|
||||
if not delivery.orders:
|
||||
delivery.orders = {}
|
||||
|
||||
if not order.products:
|
||||
if orderer.id in delivery.orders:
|
||||
del delivery.orders[orderer.id]
|
||||
delivery.persist()
|
||||
response.message("La commande est vide.", status="warning")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
delivery.orders[orderer.id] = order
|
||||
delivery.persist()
|
||||
|
||||
if user and orderer.id == user.id:
|
||||
# Send the emails to everyone in the group.
|
||||
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,
|
||||
)
|
||||
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!"
|
||||
)
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
else:
|
||||
order = delivery.orders.get(orderer.id) or Order()
|
||||
force_adjustment = "adjust" in request.query and user and user.is_staff
|
||||
response.html(
|
||||
"place_order.html",
|
||||
delivery=delivery,
|
||||
person=orderer,
|
||||
order=order,
|
||||
force_adjustment=force_adjustment,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/émargement", methods=["GET"])
|
||||
async def signing_sheet(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.pdf(
|
||||
"signing_sheet.html",
|
||||
{"delivery": delivery},
|
||||
css="signing-sheet.css",
|
||||
filename=utils.prefix("commandes-par-groupe.pdf", delivery),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/importer/commande", methods=["POST"])
|
||||
@staff_only
|
||||
async def import_commande(request, response, id):
|
||||
email = request.form.get("email")
|
||||
order = Order()
|
||||
reader = csv.DictReader(
|
||||
request.files.get("data").read().decode().splitlines(), delimiter=";"
|
||||
)
|
||||
for row in reader:
|
||||
wanted = int(row["wanted"] or 0)
|
||||
if wanted:
|
||||
order.products[row["ref"]] = ProductOrder(wanted=wanted)
|
||||
delivery = Delivery.load(id)
|
||||
delivery.orders[email] = order
|
||||
delivery.persist()
|
||||
response.message(f"Yallah! La commande de {email} a bien été importée !")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/importer/commandes", methods=["POST"])
|
||||
@staff_only
|
||||
async def import_multiple_commands(request, response, id):
|
||||
reader = csv.DictReader(
|
||||
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":
|
||||
current_ref = value
|
||||
else:
|
||||
wanted = int(value or 0)
|
||||
if wanted:
|
||||
orders[label].products[current_ref] = ProductOrder(wanted=wanted)
|
||||
delivery = Delivery.load(id)
|
||||
for email, order in orders.items():
|
||||
delivery.orders[email] = order
|
||||
delivery.persist()
|
||||
response.message(f"Yes ! Les commandes ont bien été importées !")
|
||||
response.redirect = f"/livraison/{delivery.id}"
|
||||
|
||||
|
||||
@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",
|
||||
)
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/ajuster/{ref}", methods=["GET", "POST"])
|
||||
@staff_only
|
||||
async def adjust_product(request, response, id, ref):
|
||||
delivery = Delivery.load(id)
|
||||
delivery_url = f"/livraison/{delivery.id}"
|
||||
product = None
|
||||
for product in delivery.products:
|
||||
if product.ref == ref:
|
||||
break
|
||||
else:
|
||||
response.message(f"Référence inconnue: {ref}")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
if request.method == "POST":
|
||||
form = request.form
|
||||
for email, order in delivery.orders.items():
|
||||
choice = order[product]
|
||||
choice.adjustment = form.int(email, 0)
|
||||
order[product] = choice
|
||||
delivery.persist()
|
||||
response.message(f"Le produit «{product.ref}» a bien été ajusté!")
|
||||
response.redirect = delivery_url
|
||||
else:
|
||||
response.html("adjust_product.html", {"delivery": delivery, "product": product})
|
||||
|
||||
|
||||
@app.route("/livraison/{id}/solde", methods=["GET"])
|
||||
@app.route("/livraison/{id}/solde.pdf", methods=["GET"])
|
||||
@staff_only
|
||||
async def delivery_balance(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
groups = request["groups"]
|
||||
|
||||
balance = []
|
||||
for group_id, order in delivery.orders.items():
|
||||
balance.append((group_id, order.total(delivery.products) * -1))
|
||||
|
||||
producer_groups = {}
|
||||
|
||||
for producer in delivery.producers.values():
|
||||
group = groups.get_user_group(producer.referent)
|
||||
# When a group contains multiple producer contacts,
|
||||
# the first one is elected to receive the money,
|
||||
# and all the other ones are separated in the table.
|
||||
group_id = None
|
||||
if hasattr(group, "id"):
|
||||
if (
|
||||
group.id not in producer_groups
|
||||
or producer_groups[group.id] == producer.referent_name
|
||||
):
|
||||
producer_groups[group.id] = producer.referent_name
|
||||
group_id = group.id
|
||||
if not group_id:
|
||||
group_id = producer.referent_name
|
||||
|
||||
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[:])
|
||||
|
||||
results_dict = defaultdict(partial(defaultdict, float))
|
||||
|
||||
for debiter, amount, crediter in results:
|
||||
results_dict[debiter][crediter] = amount
|
||||
|
||||
template_name = "delivery_balance.html"
|
||||
template_args = {
|
||||
"delivery": delivery,
|
||||
"debiters": debiters,
|
||||
"crediters": crediters,
|
||||
"results": results_dict,
|
||||
"debiters_groups": groups.groups,
|
||||
"crediters_groups": producer_groups,
|
||||
}
|
||||
|
||||
if request.url.endswith(b".pdf"):
|
||||
date = delivery.to_date.strftime("%Y-%m-%d")
|
||||
response.pdf(
|
||||
template_name,
|
||||
template_args,
|
||||
filename=utils.prefix("répartition-des-chèques.pdf", delivery),
|
||||
)
|
||||
else:
|
||||
response.html(template_name, template_args)
|
||||
|
||||
|
||||
def configure():
|
||||
config.init()
|
||||
from .models import Product, Person, Order, Delivery
|
||||
from .views.core import app
|
||||
|
||||
|
||||
@minicli.cli()
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import csv
|
||||
import functools
|
||||
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from openpyxl import load_workbook, Workbook
|
||||
|
@ -30,7 +27,7 @@ def items_from_xlsx(data, items, model_class, required_fields, append_method):
|
|||
raw = {k: v for k, v in dict(zip(headers, row)).items() if v}
|
||||
try:
|
||||
append_method(items, model_class(**raw))
|
||||
except TypeError as e:
|
||||
except TypeError:
|
||||
raise ValueError(f"Erreur durant l'importation de {raw['ref']}")
|
||||
return items
|
||||
|
||||
|
|
|
@ -163,6 +163,7 @@ class Groups(PersistedBase):
|
|||
@dataclass
|
||||
class Producer(Base):
|
||||
id: str
|
||||
name: str
|
||||
referent: str = ""
|
||||
referent_tel: str = ""
|
||||
referent_name: str = ""
|
||||
|
@ -182,8 +183,6 @@ class Product(Base):
|
|||
last_update: datetime_field = datetime.now()
|
||||
unit: str = ""
|
||||
description: str = ""
|
||||
url: str = ""
|
||||
img: str = ""
|
||||
packing: int = None
|
||||
producer: str = ""
|
||||
rupture: str = None
|
||||
|
@ -199,7 +198,6 @@ class Product(Base):
|
|||
self.price = form.float("price")
|
||||
self.unit = form.get("unit")
|
||||
self.description = form.get("description")
|
||||
self.url = form.get("url")
|
||||
self.last_update = datetime.now()
|
||||
if form.get("packing"):
|
||||
self.packing = form.int("packing")
|
||||
|
@ -241,13 +239,12 @@ class Order(Base):
|
|||
def total(self, products):
|
||||
def _get_price(ref):
|
||||
product = products.get(ref)
|
||||
return product.price if product else 0
|
||||
return product.price if product and not product.rupture else 0
|
||||
|
||||
products = {p.ref: p for p in products}
|
||||
return round(
|
||||
sum(p.quantity * _get_price(ref) for ref, p in self.products.items()), 2
|
||||
)
|
||||
return round(total, 2)
|
||||
|
||||
@property
|
||||
def has_adjustments(self):
|
||||
|
@ -270,13 +267,11 @@ class Delivery(PersistedBase):
|
|||
to_date: datetime_field
|
||||
order_before: datetime_field
|
||||
contact: str
|
||||
description: str = ""
|
||||
instructions: str = ""
|
||||
where: str = "Marché de la Briche"
|
||||
products: List[Product] = field(default_factory=list)
|
||||
producers: Dict[str, Producer] = field(default_factory=dict)
|
||||
orders: Dict[str, Order] = field(default_factory=dict)
|
||||
infos_url: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
self.id = None # Not a field because we don't want to persist it.
|
||||
|
@ -379,7 +374,7 @@ class Delivery(PersistedBase):
|
|||
|
||||
def archive(self):
|
||||
if self.is_archived:
|
||||
raise ValueError("La livraison est déjà archivée")
|
||||
raise ValueError("La distribution est déjà archivée")
|
||||
current = self.path
|
||||
self.id = f"archive/{self.id}"
|
||||
current.rename(self.path)
|
||||
|
@ -387,7 +382,7 @@ class Delivery(PersistedBase):
|
|||
def unarchive(self):
|
||||
if not self.is_archived:
|
||||
raise ValueError(
|
||||
"Impossible de désarchiver une livraison qui n'est pas archivée"
|
||||
"Impossible de désarchiver une distribution qui n'est pas archivée"
|
||||
)
|
||||
current = self.path
|
||||
self.id = self.path.stem
|
||||
|
@ -418,6 +413,12 @@ class Delivery(PersistedBase):
|
|||
if products:
|
||||
return products[0]
|
||||
|
||||
def delete_product(self, ref):
|
||||
product = self.get_product(ref)
|
||||
if product:
|
||||
self.products.remove(product)
|
||||
return product
|
||||
|
||||
def total_for_producer(self, producer, person=None):
|
||||
producer_products = [p for p in self.products if p.producer == producer]
|
||||
if person:
|
||||
|
|
|
@ -671,3 +671,13 @@ ul.actions > li {
|
|||
display: inline;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#groups ul, #groups li{
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.toplink {
|
||||
padding-left: 0.5em;
|
||||
padding-top: 0.5em;
|
||||
padding-bottom: -0.5em;
|
||||
}
|
1
copanier/static/flash.min.css
vendored
Normal file
1
copanier/static/flash.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
.flash-container{position:fixed;top:75px;right:15px;z-index:1000;max-width:25%}.flash-container .flash-message{position:relative;opacity:0;min-height:28px;-webkit-transform:translateX(-20px);-ms-transform:translateX(-20px);transform:translateX(-20px);-webkit-transition:all .5s;-o-transition:all .5s;transition:all .5s;background-color:#fff;color:#2c3433;-webkit-border-radius:0;border-radius:0;-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;margin-bottom:10px;padding:5px 35px 5px 20px;-webkit-box-shadow:2px 2px 33px 8px rgba(0,0,0,.1);box-shadow:2px 2px 33px 8px rgba(0,0,0,.1);line-height:1.4;cursor:pointer}.flash-container .flash-message .flash-progress{position:absolute;right:0;top:auto;bottom:0;left:0;width:0;height:3px;opacity:1;background-color:rgba(0,0,0,.15);-webkit-transition:opacity .1s;-o-transition:opacity .1s;transition:opacity .1s}.flash-container .flash-message .flash-progress.is-hidden{opacity:0}.flash-container .flash-message .flash-progress.flash-progress-top{top:0;bottom:auto}.flash-container .flash-message:before{position:absolute;content:"";width:7px;height:100%;top:0;bottom:0;left:-7px;background-color:transparent;-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px}.flash-container .flash-message:after{position:absolute;content:"";font-family:fontAwesome;top:5px;right:8px;text-align:center;vertical-align:middle;color:#9e9e9e}.flash-container .flash-message.is-visible{opacity:1;-webkit-transform:translateX(0);-ms-transform:translateX(0);transform:translateX(0)}.flash-container .flash-message.flash-success .flash-progress{background-color:rgba(76,175,80,.15)}.flash-container .flash-message.flash-success:before{background-color:#4caf50}.flash-container .flash-message.flash-success:after{color:rgba(76,175,80,.5);content:"\F058"}.flash-container .flash-message.flash-warning .flash-progress{background-color:rgba(255,133,27,.15)}.flash-container .flash-message.flash-warning:before{background-color:#ff851b}.flash-container .flash-message.flash-warning:after{color:rgba(255,133,27,.5);content:"\F071"}.flash-container .flash-message.flash-danger .flash-progress,.flash-container .flash-message.flash-error .flash-progress{background-color:rgba(255,65,54,.15)}.flash-container .flash-message.flash-danger:before,.flash-container .flash-message.flash-error:before{background-color:#ff4136}.flash-container .flash-message.flash-danger:after,.flash-container .flash-message.flash-error:after{color:rgba(255,65,54,.5);content:"\F06A"}.flash-container .flash-message.flash-info .flash-progress{background-color:rgba(0,116,217,.15)}.flash-container .flash-message.flash-info:before{background-color:#0074d9}.flash-container .flash-message.flash-info:after{color:rgba(0,116,217,.5);content:"\F05A"}.flash-container .flash-message.flash-bug .flash-progress{background-color:rgba(138,43,226,.15)}.flash-container .flash-message.flash-bug:before{background-color:#8a2be2}.flash-container .flash-message.flash-bug:after{color:rgba(138,43,226,.5);content:"\F188"}.flash-container .flash-message.flash-disabled .flash-progress{background-color:hsla(0,0%,67%,.15)}.flash-container .flash-message.flash-disabled:before{background-color:#aaa}.flash-container .flash-message.flash-disabled:after{color:hsla(0,0%,67%,.5);content:"\F05E"}.flash-container .flash-message.flash-default{padding-right:20px}@media (max-width:1280px){.flash-container{max-width:33.334%}}@media (max-width:768px){.flash-container{max-width:50%}}@media (max-width:480px){.flash-container{right:10px;left:10px;max-width:100%}}.flash-container .flash-message.dark-theme{background-color:#2c3433;color:#fff}.flash-container .flash-message.dark-theme .flash-progress{background-color:hsla(0,0%,100%,.5)}
|
1
copanier/static/js/flash.min.js
vendored
Normal file
1
copanier/static/js/flash.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -9,7 +9,7 @@
|
|||
}
|
||||
|
||||
@media print {
|
||||
nav, .notification, .toolbox, .edit, .hide-on-print, .button{
|
||||
nav, .notification, .toolbox, .edit, .hide-on-print, .header, #menu, .toplink, .button{
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -250,3 +250,9 @@ Hides the menu at `48em`, but modify this based on your app's needs.
|
|||
.pure-menu-item a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#sitename {
|
||||
color: #ccc;
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
}
|
|
@ -9,6 +9,7 @@
|
|||
<link rel="stylesheet" type="text/css" href="/static/icomoon.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/purecss.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/side-menu.css">
|
||||
<link rel="stylesheet" type="text/css" href="/static/flash.min.css">
|
||||
|
||||
{% block head %}
|
||||
{% endblock head %}
|
||||
|
@ -20,62 +21,35 @@
|
|||
<a href="#menu" id="menuLink" class="menu-link">
|
||||
<span></span>
|
||||
</a>
|
||||
|
||||
{% if request.user %}
|
||||
<div id="menu">
|
||||
<div class="pure-menu">
|
||||
|
||||
<h1>{{ config.SITE_NAME }}</h1>
|
||||
<h1 id="sitename">{{ config.SITE_NAME }}</h1>
|
||||
<ul class="pure-menu-list">
|
||||
{% if delivery %}
|
||||
<li class="pure-menu-heading">
|
||||
Cette distrib
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/commander">Commander</a>
|
||||
<a href="/"><i class="icon-happy"></i> Voir les distrib</a>
|
||||
</li>
|
||||
{% if request.user and request.user.is_staff %}
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/émargement">Fiches de commandes par coloc</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/producteurices.pdf">Fiches commandes product⋅eurs⋅ices</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/solde">Paiements</a>
|
||||
</li>
|
||||
{% if request.user.is_staff %}
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/gérer">Boîte à outils</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison/{{ delivery.id }}/edit">Modifier les infos</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li class="pure-menu-heading">
|
||||
Actions
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="/"><i class="icon-happy"></i> Voir les distributions</a>
|
||||
</li>
|
||||
{% if request.user.is_staff %}
|
||||
<li class="pure-menu-item">
|
||||
<a href="/livraison"><i class="icon-hotairballoon"></i> Nouvelle distribution</a>
|
||||
<a href="/distribution"><i class="icon-hotairballoon"></i> Nouvelle distribution</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/groupes"><i class="icon-globe"></i> Gérer les groupes</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a href="#" class="pure-menu-link"><i class="icon-lock"></i> Se déconnecter</a>
|
||||
<a href="/déconnexion" class="pure-menu-link"><i class="icon-lock"></i> Se déconnecter</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
{% if message %}
|
||||
<section class="notification {{ message[1] }}"><i class="icon-megaphone"></i> {{ message[0] }}</section>
|
||||
{% else %}
|
||||
<h1 id="sitename">{{ config.SITE_NAME }}</h1>
|
||||
{% endif %}
|
||||
<div class="toplink">
|
||||
{% block toplink %}{% endblock %}
|
||||
</div>
|
||||
<div id="main">
|
||||
<div class="content">
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
|
@ -84,13 +58,15 @@
|
|||
</div>
|
||||
|
||||
|
||||
|
||||
<header>
|
||||
<section class="menu">
|
||||
|
||||
</section>
|
||||
</header>
|
||||
<main>
|
||||
</main>
|
||||
<script src="/static/js/flash.min.js"></script>
|
||||
{% if message %}
|
||||
<script>
|
||||
new window.FlashMessage("{{ message[0] }}", "{{ message[1] }}", {
|
||||
progress: true,
|
||||
interactive: true,
|
||||
timeout: 8000,
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
|
||||
<div class="header">
|
||||
<h1>{{ delivery.name }} ({% if delivery.status == delivery.OPEN %}Commandes jusqu'au {{ delivery.order_before|date|capitalize }}{% elif delivery.status == delivery.ADJUSTMENT %}Ajustements en cours{% elif delivery.status == delivery.CLOSED %}Fermée{% else %}Archivée{% endif %})
|
||||
</h1>
|
||||
<h4>
|
||||
Distribution <i class="icon-clock"></i> {{ delivery.from_date|date|capitalize }}, {{ delivery.from_date|time }} - {{ delivery.to_date|time }}, à <i class="icon-streetsign"></i> {{ delivery.where }}.
|
||||
</h4>
|
||||
<ul class="actions">
|
||||
{% include "includes/order_button.html" %}
|
||||
{% if request['user'].email == delivery.contact and delivery.status > delivery.EMPTY %}
|
||||
<li><a class="button" href="/livraison/{{ delivery.id }}/gérer">Voir la boîte à outils</a></li>
|
||||
{% elif request['user'].email in delivery.get_referents() %}
|
||||
<li><a class="button" href="/livraison/{{ delivery.id }}/producteurices?referent={{request['user'].email}}">Gérer les produits dont vous vous occupez</a></li>
|
||||
{% endif %}
|
||||
{% if not delivery.is_archived and delivery.status == delivery.CLOSED %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/producteurices.pdf"><i class="icon-ribbon"></i> Bon de commande</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-grid"></i> Rapport complet</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/émargement" target="_blank"><i class="icon-document"></i> Résumé de commandes</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user and request.user.is_staff %}
|
||||
{% if delivery.is_archived %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/désarchiver" class="button danger"><i class="icon-hazardous"></i> Désarchiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/exporter"><i class="icon-layers"></i> Télécharger les infos produits</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/edit"><i class="icon-adjustments"></i> Modifier la livraison</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/solde"><i class="icon-wallet"></i> Répartition des paiements</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="progressbar">
|
||||
<li {% if delivery.status == delivery.EMPTY %}class="active"{% endif %}>Mise à jour des prix</li>
|
||||
<li {% if delivery.status == delivery.OPEN %}class="active"{% endif %}>Ouverture des commandes</li>
|
||||
<li {% if delivery.status == delivery.ADJUSTMENT %}class="active"{% endif %}>Ajustements</li>
|
||||
<li {% if delivery.can_generate_reports %}class="active"{% endif %}>Achats par les référent⋅e⋅s</li>
|
||||
<li>Distribution</li>
|
||||
</ul>
|
||||
<article class="delivery">
|
||||
{% if delivery.has_products %}
|
||||
{% include "includes/delivery_table.html" %}
|
||||
{% else %}
|
||||
<div class="placeholder">
|
||||
<h2>Aucun produit n'est encore défini pour cette livraison.</h2>
|
||||
{% if request.user and request.user.is_staff %}
|
||||
<div class="inline-text">
|
||||
Pour rajouter des produits, deux options:
|
||||
<ol>
|
||||
<li><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 <div class="inline-text">{% with unique_id="import-products" %}{% include "includes/modal_import_products.html" %}{% endwith %}</div></li>
|
||||
<li>Ou bien ajouter les product⋅eurs⋅rices un⋅e par un⋅e dans l'interface</li>
|
||||
</ol>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<hr>
|
||||
<ul class="toolbox">
|
||||
|
||||
</ul>
|
||||
{% endblock body %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<h2>Livraisons archivées</h2>
|
||||
<h2>Distributions archivées</h2>
|
||||
{% include "includes/delivery_list.html" %}
|
||||
{% endblock body %}
|
|
@ -1,5 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}<h1><i class="icon-wallet"></i> Répartition des paiements <a id="download" class="button" href="/livraison/{{ delivery.id }}/solde.pdf">Télécharger</a></h1>{% endblock %}
|
||||
{% block title %}<h1><i class="icon-wallet"></i> Répartition des paiements <a id="download" class="button" href="/distribution/{{ delivery.id }}/solde.pdf">Télécharger</a></h1>{% endblock %}
|
||||
{% block body %}
|
||||
<p class="info">{{ delivery.name }} du {{ delivery.to_date | date }}.</p>
|
||||
<p class="info">Les personnes indiquées avec un <code>*</code> à côté de leur nom sont celles qui ont payé cette commande pour leur groupe.</p>
|
22
copanier/templates/delivery/copy.html
Normal file
22
copanier/templates/delivery/copy.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
<h1>Copier les produits d'une autre distribution</h1>
|
||||
</div>
|
||||
|
||||
<form method="post">
|
||||
<label>
|
||||
<p>Distribution à copier</p>
|
||||
<select name="to_copy">
|
||||
{% for delivery in deliveries %}
|
||||
<option value="{{ delivery.id }}">{{ delivery.name }} / {{ delivery.to_date | date }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<div>
|
||||
<input type="submit" name="submit" value="Valider" class="primary">
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
{% endblock body %}
|
|
@ -1,20 +1,31 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block toplink %}{% if delivery.id %}<a href="/distribution/{{ delivery.id }}">↶ Retourner à la distribution</a>{% endif %}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
{% if delivery.id %}
|
||||
<h1>Modifier la livraison</h1>
|
||||
{% else %}
|
||||
<h1>Nouvelle livraison</h1>
|
||||
<h1>Modifier la distribution</h1>
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
{% if delivery.status == delivery.CLOSED %}
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link danger" href="/distribution/{{ delivery.id }}/archiver"><i class="icon-layers"></i> Archiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id }}/produits"><i class="icon-pencil"></i> Gérer les produits / product⋅eur⋅rice⋅s</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1>Nouvelle distribution</h1>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<form method="post">
|
||||
<label>
|
||||
<p>Nom de la livraison</p>
|
||||
<p>Nom de la distribution</p>
|
||||
<input type="text" name="name" value="{{ delivery.name or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<h5>Description des produits</h5>
|
||||
<input type="text" name="description" value="{{ delivery.description or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<p>Référent⋅e</p>
|
||||
<input type="email" name="contact" value="{{ delivery.contact or request.user.email }}" required>
|
||||
|
@ -24,7 +35,7 @@
|
|||
<input type="text" name="where" value="{{ delivery.where or '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
<p>Date de livraison</p>
|
||||
<p>Date de distribution</p>
|
||||
<input type="date" name="date" value="{{ delivery.from_date.date() if delivery.from_date else '' }}" required> de <input type="time" name="from_time" value="{{ delivery.from_date.time() if delivery.from_date else '' }}" required> à <input type="time" name="to_time" value="{{ delivery.to_date.time() if delivery.to_date else '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
|
@ -35,30 +46,9 @@
|
|||
<p>Instructions particulières</p>
|
||||
<input type="text" name="instructions" value="{{ delivery.instructions or '' }}">
|
||||
</label>
|
||||
<label>
|
||||
<p>URL externe d'information</p>
|
||||
<input type="url" name="infos_url" value="{{ delivery.infos_url or '' }}">
|
||||
</label>
|
||||
<div>
|
||||
<input type="submit" name="submit" value="Valider" class="primary">
|
||||
</div>
|
||||
</form>
|
||||
<hr>
|
||||
{% if delivery %}
|
||||
<ul class="toolbox">
|
||||
{% if delivery.status == delivery.CLOSED %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/archiver" class="button danger"><i class="icon-layers"></i> Archiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{% with unique_id="import-products" %}
|
||||
{% 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 %}
|
33
copanier/templates/delivery/list.html
Normal file
33
copanier/templates/delivery/list.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
|
||||
<div class="header">
|
||||
<h1>Distributions</h1>
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution"><i class="icon-hotairballoon"></i> Créer une nouvelle distribution</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/archives"><i class="icon-telescope"></i> Voir les distributions archivées</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% with deliveries=incoming %}
|
||||
{% include "includes/delivery_list.html" %}
|
||||
{% endwith %}
|
||||
{% if former %}
|
||||
<div class="header">
|
||||
<h1>Distributions passées</h1>
|
||||
</div>
|
||||
{% with deliveries=former %}
|
||||
{% include "includes/delivery_small_list.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if archives %}
|
||||
<a href="/archives">Voir les distributions archivées</a>
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
|
@ -1,10 +1,10 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
{% set group_name = request.groups.groups[person.name].name %}
|
||||
{% set group_name = request.groups.groups.get(person.name, person).name %}
|
||||
<article class="order">
|
||||
<h3>
|
||||
<a href="/livraison/{{ delivery.id }}">{{ delivery.name }}</a>
|
||||
<a href="/distribution/{{ delivery.id }}">{{ delivery.name }}</a>
|
||||
— Commande pour {{ group_name }}
|
||||
{% if order.phone_number %}
|
||||
(<a href="tel:{{ order.phone_number }}">{{ order.phone_number }}</a>)
|
||||
|
@ -13,8 +13,8 @@
|
|||
{% include "includes/delivery_head.html" %}
|
||||
|
||||
<form method="post">
|
||||
{% for producer in delivery.producers %}
|
||||
<h2>{{ producer }}</h2>
|
||||
{% for producer in delivery.producers.values() %}
|
||||
<h2>{{ producer.name }}</h2>
|
||||
<table class="order">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -30,7 +30,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for product in delivery.get_products_by(producer) %}
|
||||
{% for product in delivery.get_products_by(producer.id) %}
|
||||
<tr>
|
||||
<th class="product {% if product.rupture %}rupture{% endif %}">{{ product }} {% if product.rupture %}(RUPTURE !){% endif %}
|
||||
{% if product.description or product.img %}
|
||||
|
@ -54,13 +54,12 @@
|
|||
{% endfor %}
|
||||
<p>On y est presque ! Est-ce que tu peux entrer un numéro de téléphone au cas où on ait besoin de vous joindre ?</p>
|
||||
<input type="text" name="phone_number" value="{{ order.phone_number }}" placeholder="06 12 34 56 78">
|
||||
<p>Total: {{ order.total(delivery.products) if order else 0 }} €</p>
|
||||
<input type="hidden" name="email" value="{{ person.email }}">
|
||||
{% if delivery.status != delivery.CLOSED or request.user.is_staff %}
|
||||
<input type="submit" value="Enregistrer la commande" class="primary">
|
||||
{% endif %}
|
||||
{% if request.user.is_staff and delivery.status == delivery.CLOSED %}
|
||||
<a class="button danger" href="/livraison/{{ delivery.id }}/commander?email={{ person.email }}&adjust">Ajuster</a>
|
||||
<a class="button danger" href="/distribution/{{ delivery.id }}/commander?email={{ person.email }}&adjust">Ajuster</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</article>
|
|
@ -1,8 +1,9 @@
|
|||
{% extends "base.html" %}
|
||||
{% block toplink %}<a href="/distribution/{{ delivery.id }}/gérer">↶ Retourner à la boîte à outils</a>{% endblock %}
|
||||
|
||||
{% 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>
|
||||
<p>Le mail ci dessous (modifiable) va être envoyé aux référent⋅e⋅s, avec en pièce jointe les commandes pour les product⋅eurs⋅rices de chaque référent⋅e.</p>
|
||||
|
||||
<form method="post">
|
||||
<input type="text" name="email_subject" style="width:800px" value="{{ config.SITE_NAME }} - Les commandes pour vos producteurs⋅rices" /><br />
|
||||
|
@ -12,7 +13,7 @@ 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.
|
||||
|
||||
Tu peux aussi retrouver le doc à cette URL : https://{{ request.host }}/livraison/{{ delivery.id }}/producteurices
|
||||
Tu peux aussi retrouver le doc à cette URL : https://{{ request.host }}/distribution/{{ delivery.id }}/produits
|
||||
|
||||
Rendez-vous pour la distribution, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }} à {{ delivery.where }}.
|
||||
|
68
copanier/templates/delivery/show.html
Normal file
68
copanier/templates/delivery/show.html
Normal file
|
@ -0,0 +1,68 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
|
||||
<div class="header">
|
||||
<h1>{{ delivery.name }}</h1>
|
||||
<h4>{% if delivery.products %}{% if delivery.status == delivery.OPEN %}Commandes jusqu'au {{ delivery.order_before|date|capitalize }}{% elif delivery.status == delivery.ADJUSTMENT %}Ajustements en cours{% elif delivery.status == delivery.CLOSED %}Fermée{% else %}Archivée{% endif %}{% endif %}.
|
||||
{% include "includes/order_button.html" %}
|
||||
</h4>
|
||||
<h4>
|
||||
Distribution <i class="icon-clock"></i> {{ delivery.from_date|date|capitalize }}, {{ delivery.from_date|time }} - {{ delivery.to_date|time }}, à <i class="icon-streetsign"></i> {{ delivery.where }}.
|
||||
</h4>
|
||||
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item pure-menu-has-children pure-menu-allow-hover">
|
||||
<a href="#" class="pure-menu-link"><i class="icon-printer"></i> Imprimer…</a>
|
||||
<ul class="pure-menu-children">
|
||||
<li class="pure-menu-item"><a href="/distribution/{{ delivery.id }}/produits.pdf" class="pure-menu-link"><i class="icon-ribbon"></i> Commande producteurs⋅rices</a></li>
|
||||
<li class="pure-menu-item"><a href="/distribution/{{ delivery.id }}/émargement" class="pure-menu-link"><i class="icon-grid"></i> Résumé de commande par groupe</a></li>
|
||||
<li class="pure-menu-item"><a href="/distribution/{{ delivery.id }}/solde" class="pure-menu-link"><i class="icon-wallet"></i> Répartition des paiements</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% if request['user'].email == delivery.contact and delivery.status > delivery.EMPTY %}
|
||||
<li class="pure-menu-item"><a class="pure-menu-link" href="/distribution/{{ delivery.id }}/gérer"><i class="icon-tools"></i> Boîte à outils</a></li> {% endif %}
|
||||
{% if request.user and request.user.is_staff %}
|
||||
{% if delivery.is_archived %}
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id }}/désarchiver"><i class="icon-hazardous"></i> Désarchiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id }}/edit"><i class="icon-adjustments"></i> Modifier la distrib</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id }}/produits"><i class="icon-pricetags"></i> Produits</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<article class="delivery">
|
||||
{% if delivery.has_products %}
|
||||
{% include "includes/delivery_table.html" %}
|
||||
{% else %}
|
||||
<div class="placeholder">
|
||||
<h2>😔 Pour le moment, cette distribution est bien vide…</h2>
|
||||
{% if request.user and request.user.is_staff %}
|
||||
Occupons-nous donc de ça ! Deux options :
|
||||
<ol>
|
||||
<li><a href="/producteurices/créer/{{ delivery.id }}">Ajouter les product⋅eurs⋅rices</a> à la main ;</li>
|
||||
<li>Ou bien <a href="/distribution/{{ delivery.id }}/copier">copier les produits d'une autre distribution</a>.</li>
|
||||
</ol>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</article>
|
||||
<hr>
|
||||
<ul class="toolbox">
|
||||
|
||||
</ul>
|
||||
{% endblock body %}
|
29
copanier/templates/delivery/toolbox.html
Normal file
29
copanier/templates/delivery/toolbox.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block toplink %}<a href="/distribution/{{ delivery.id }}">↶ Retourner à la distribution</a>{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Gérer « {{ delivery.name }} »</h1>
|
||||
|
||||
{% set display_counts = True %}
|
||||
{% include "includes/delivery_head.html" %}
|
||||
|
||||
<h3>Emails des référent⋅e⋅s</h3>
|
||||
<p>Au cas où, quoi.</p>
|
||||
<textarea class="list-emails" rows=5>{{ referents | join(', ') }}</textarea>
|
||||
|
||||
<h3>Avant et pendant la distribution</h3>
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/edit"><i class="icon-pencil"></i> Modifier la commande (dates, lieu, référent⋅e, etc)</a>
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/products"><i class="icon-pencil"></i> Modifier les produits, les product⋅rices⋅eurs</a>
|
||||
<a class="button" href="/groupes"><i class="icon-globe"></i> 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="/distribution/{{ delivery.id }}/products.pdf"><i class="icon-download"></i> Télécharger les bons de distribution</a>
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-download"></i> Télécharger le tableau des commandes</a>
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/envoi-email-referentes"><i class="icon-envelope"></i> 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="/distribution/{{ delivery.id }}/émargement"><i class="icon-document"></i> Fiches de commandes par groupe</a>
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/solde"><i class="icon-gears"></i> Faire la répartition des paiements</a>
|
||||
|
||||
{% endblock %}
|
|
@ -1,27 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Gérer « {{ delivery.name }} »</h1>
|
||||
|
||||
{% set display_counts = True %}
|
||||
{% include "includes/delivery_head.html" %}
|
||||
|
||||
<h3>Emails des référent⋅e⋅s</h3>
|
||||
<p>Au cas où, quoi.</p>
|
||||
<textarea class="list-emails" rows=5>{{ referents | join(', ') }}</textarea>
|
||||
|
||||
<h3>Avant et pendant la distribution</h3>
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/edit"><i class="icon-pencil"></i> Modifier la commande (dates, lieu, référent⋅e, etc)</a>
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/producteurices"><i class="icon-pencil"></i> Modifier les produits, les product⋅rices⋅eurs</a>
|
||||
<a class="button" href="/groupes"><i class="icon-globe"></i> 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 }}/producteurices.pdf"><i class="icon-download"></i> Télécharger les bons de livraison</a>
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/rapport-complet.xlsx"><i class="icon-download"></i> Télécharger le tableau des commandes</a>
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/envoi-email-referentes"><i class="icon-envelope"></i> 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> Fiches de commandes par groupe</a>
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/solde"><i class="icon-gears"></i> Faire la répartition des paiements</a>
|
||||
|
||||
{% endblock %}
|
|
@ -1,64 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div id="layout">
|
||||
<a href="#menu" id="menuLink" class="menu-link">
|
||||
<span></span>
|
||||
</a>
|
||||
|
||||
<div id="menu">
|
||||
<div class="pure-menu">
|
||||
<a class="pure-menu-heading" href="#">Company</a>
|
||||
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Home</a></li>
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link">About</a></li>
|
||||
|
||||
<li class="pure-menu-item menu-item-divided pure-menu-selected">
|
||||
<a href="#" class="pure-menu-link">Services</a>
|
||||
</li>
|
||||
|
||||
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Contact</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<div class="header">
|
||||
<h1>Page Title</h1>
|
||||
<h2>A subtitle for your page goes here</h2>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<h2 class="content-subhead">How to use this layout</h2>
|
||||
<p>
|
||||
To use this layout, you can just copy paste the HTML, along with the CSS in <a href="/css/layouts/side-menu.css" alt="Side Menu CSS">side-menu.css</a>, and the JavaScript in <a href="/js/ui.js">ui.js</a>. The JS file uses vanilla JavaScript to simply toggle an <code>active</code> class that makes the menu responsive.
|
||||
</p>
|
||||
|
||||
<h2 class="content-subhead">Now Let's Speak Some Latin</h2>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-4">
|
||||
<img class="pure-img-responsive" src="http://farm3.staticflickr.com/2875/9069037713_1752f5daeb.jpg" alt="Peyto Lake">
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<img class="pure-img-responsive" src="http://farm3.staticflickr.com/2813/9069585985_80da8db54f.jpg" alt="Train">
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<img class="pure-img-responsive" src="http://farm6.staticflickr.com/5456/9121446012_c1640e42d0.jpg" alt="T-Shirt Store">
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<img class="pure-img-responsive" src="http://farm8.staticflickr.com/7357/9086701425_fda3024927.jpg" alt="Mountain">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="content-subhead">Try Resizing your Browser</h2>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
|
@ -1,9 +1,9 @@
|
|||
<p>Bonjour,</p>
|
||||
<p>Voici le résumé de ta commande «{{ delivery.name }}»</p>
|
||||
{% include "includes/order_summary.html" %}
|
||||
<p>Livraison: {{ delivery.where }}, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</p>
|
||||
<p>Distribution: {{ delivery.where }}, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</p>
|
||||
{% if delivery.is_open %}
|
||||
<p>Tu peux la modifier (jusqu'au {{ delivery.order_before|date }}) <a href="https://{{ request.host }}/livraison/{{ delivery.id }}/commander">en cliquant ici</a>.</p>
|
||||
<p>Tu peux la modifier (jusqu'au {{ delivery.order_before|date }}) <a href="https://{{ request.host }}/distribution/{{ delivery.id }}/commander">en cliquant ici</a>.</p>
|
||||
{% endif %}
|
||||
<p>Bonne journée!</p>
|
||||
<p>{{ config.EMAIL_SIGNATURE }}</p>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
Salut salut,
|
||||
|
||||
Voici le résumé de votre commande «{{ delivery.name }}» pour « {{ request['user']['group_name'] }} ».
|
||||
Voici le résumé de la commande « {{ delivery.name }} » pour « {{ request['user']['group_name'] }} ».
|
||||
|
||||
Produit | Prix unitaire | Quantité
|
||||
|
||||
|
@ -9,12 +9,12 @@ Produit | Prix unitaire | Quantité
|
|||
|
||||
Total: {{ order.total(delivery.products) if order else 0 }} €
|
||||
|
||||
Livraison: {{ delivery.where }}, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}
|
||||
Distribution: {{ delivery.where }}, le {{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}
|
||||
|
||||
{% if delivery.is_open %}
|
||||
Tu peux la modifier (jusqu'au {{ delivery.order_before|date }}) en cliquant ici:
|
||||
|
||||
https://{{ request.host }}/livraison/{{ delivery.id }}/commander
|
||||
https://{{ request.host }}/distribution/{{ delivery.id }}/commander
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
<h1>Groupes</h1>
|
||||
<ul class="actions">
|
||||
<li><a class="button" href="/groupes/créer"><i class="icon-globe"></i> Créer un nouveau groupe</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% if not request['user'].group_id %}
|
||||
<p>Bienvenue ! Avant de pouvoir commander, peux-tu nous indiquer de quelle coloc / famille fais-tu partie ? </p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% for group in groups.groups.values() %}
|
||||
<h3>{{ group.name }} {% if group.id != request['user'].group_id %}<a class="button" href="/groupes/{{ group.id}}/rejoindre">rejoindre</a>{% endif %} <a class="button" href="/groupes/{{ group.id }}/éditer">éditer</a></h3>
|
||||
<ul>
|
||||
{% for member in group.members %}
|
||||
<li>{{ member }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
47
copanier/templates/groups/list.html
Normal file
47
copanier/templates/groups/list.html
Normal file
|
@ -0,0 +1,47 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
<h1>Groupes</h1>
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/groupes/créer"><i class="icon-globe"></i> Créer un nouveau groupe</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not request['user'].group_id %}
|
||||
<p>Bienvenue ! Avant de pouvoir commander, peux-tu nous indiquer de quelle coloc / famille fais-tu partie ? </p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<table id="groups" class="pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Groupe</th>
|
||||
<th>Membres</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for group in groups.groups.values() %}
|
||||
<tr>
|
||||
<th>{{ group.name }}</th>
|
||||
<td>
|
||||
<ul>
|
||||
{% for member in group.members %}
|
||||
<li>{{ member }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>{% if group.id != request['user'].group_id %}<a class="pure-button" href="/groupes/{{ group.id}}/rejoindre">rejoindre</a>{% endif %} <a class="pure-button" href="/groupes/{{ group.id }}/éditer">éditer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Tu ne fais partie d'aucun des groupes listés ici ? Tu peux <a class="button" href="/groupes/créer"><i class="icon-globe"></i> en créer un nouveau</a></p>
|
||||
|
||||
{% endblock %}
|
|
@ -1,30 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
<h1>Distributions</h1>
|
||||
<ul class="actions">
|
||||
<li>
|
||||
<a class="button" href="/livraison"><i class="icon-hotairballoon"></i> Créer une nouvelle distribution</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" href="/livraison"><i class="icon-telescope"></i> Voir les distribution passées</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% with deliveries=incoming %}
|
||||
{% include "includes/delivery_list.html" %}
|
||||
{% endwith %}
|
||||
{% if former %}
|
||||
<div class="header">
|
||||
<h1>Distributions passées</h1>
|
||||
</div>
|
||||
{% with deliveries=former %}
|
||||
{% include "includes/delivery_small_list.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if archives %}
|
||||
<a href="/archives">Voir les livraisons archivées</a>
|
||||
<hr>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
|
@ -5,7 +5,7 @@
|
|||
{% else %}
|
||||
<li class="hide-on-print"><i class="icon-strategy"></i> <strong>Référent⋅e</strong> <a href="mailto:{{ delivery.contact }}">{{ delivery.contact }}</a></li>
|
||||
{% endif %}
|
||||
<li><i class="icon-clock"></i> <strong>Date de livraison</strong> <time datetime="{{ delivery.from_date }}">{{ delivery.from_date|date|capitalize }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</time></li>
|
||||
<li><i class="icon-clock"></i> <strong>Date de distribution</strong> <time datetime="{{ delivery.from_date }}">{{ delivery.from_date|date|capitalize }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</time></li>
|
||||
<li class="hide-on-print"><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|capitalize }}</time>{% elif delivery.status == delivery.ADJUSTMENT %}<strong>Ajustement en cours</strong>{% elif delivery.status == delivery.CLOSED %}<strong>Fermée</strong>{% else %}<strong>Archivée</strong>{% endif %}</li>
|
||||
{% if delivery.instructions %}<li><i class="icon-lightbulb"></i> <strong>À savoir</strong> {{ delivery.instructions }}</li>{% endif %}
|
||||
{% if delivery.infos_url %}<li><i class="icon-global"></i><strong>Plus d'infos</strong> <a href="{{ delivery.infos_url }}" title="{{ delivery.infos_url }}">{{ delivery.infos_url|truncate(20)}}</a></li>{% endif %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<ul class="delivery">
|
||||
{% for delivery in deliveries %}
|
||||
<li>
|
||||
<h3><a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.name }}</a> {% include "includes/order_button.html" %} <a class="button" href="/livraison/{{ delivery.id }}">Voir les commandes</a></h3>
|
||||
<h3><a href="/distribution/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.name }}</a> {% include "includes/order_button.html" %} <a class="button" href="/distribution/{{ delivery.id }}">Voir les commandes</a></h3>
|
||||
{% include "includes/delivery_head.html" %}
|
||||
</li>
|
||||
<hr>
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
<ul class="delivery">
|
||||
{% for delivery in deliveries %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.name }}</a> du {{ delivery.from_date|date|capitalize }}
|
||||
<a href="/distribution/{{ delivery.id }}"><i class="icon-hotairballoon"></i> {{ delivery.name }}</a> du {{ delivery.from_date|date|capitalize }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>Aucune livraison.</p>
|
||||
<p>Aucune distribution.</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -5,19 +5,18 @@
|
|||
{% endif %}
|
||||
{% for producer in producers %}
|
||||
{% set producer_obj = delivery.producers[producer] %}
|
||||
<h2>{{ producer }}
|
||||
<h2>{{ producer_obj.name }}
|
||||
{% if edit_mode or request.user.is_staff or producer_obj.referent == request.user.email %}
|
||||
<span class="edit">
|
||||
<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>
|
||||
{% if delivery.can_generate_reports %}
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/{{ producer }}/bon-de-commande.pdf"><i class="icon-grid"></i> Télécharger le bon de commande</a>
|
||||
{% endif %}
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/{{ producer_obj.id }}/éditer"><i class="icon-ribbon"></i> Gérer ce⋅tte producteur⋅rice</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h5>{% if producer_obj.description %}{{ producer_obj.description }}{% endif %}. Référent⋅e : <a href="mailto:{{ producer_obj.referent }}">{{ producer_obj.referent_name }}</a> / {{ producer_obj.referent_tel }}</h5>
|
||||
<table class="delivery">
|
||||
{% if not delivery.get_products_by(producer) %}
|
||||
😔 Ce⋅tte producteur⋅rice n'a pas encore de produits. Voulez vous <a href="/distribution/{{ delivery.id }}/{{ producer }}/ajouter-produit">en rajouter un ?</a>
|
||||
{% else %}
|
||||
<table class="delivery pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="product">Produit</th>
|
||||
|
@ -25,14 +24,13 @@
|
|||
{% if delivery.has_packing %}
|
||||
<th class="packing">Conditionnement</th>
|
||||
{% endif %}
|
||||
{% if edit_mode %}<th>Éditer</th>{% endif %}
|
||||
<th class="amount">Total</th>
|
||||
{% if not list_only %}
|
||||
{% for orderer, order in delivery.orders.items() %}
|
||||
{% set orderer_name = request.groups.groups[orderer].name %}
|
||||
<th class="person">
|
||||
{% if request.user and (request.user.is_staff or request.user.is_referent(delivery)) %}
|
||||
<a href="/livraison/{{ delivery.id }}/commander?orderer={{ orderer }}" title="{{ orderer }}">{{ orderer_name }}</a>
|
||||
<a href="/distribution/{{ delivery.id }}/commander?orderer={{ orderer }}" title="{{ orderer }}">{{ orderer_name }}</a>
|
||||
{% else %}
|
||||
<span title="{{ orderer }}">{{ orderer_name }}</span>
|
||||
{% endif %}
|
||||
|
@ -44,16 +42,15 @@
|
|||
<tbody>
|
||||
{% for product in delivery.get_products_by(producer) %}
|
||||
<tr>
|
||||
<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 %}
|
||||
<th class="product {% if product.rupture %}rupture{% endif %}">{% if edit_mode %}<a href="/distribution/{{ delivery.id }}/{{ product.producer }}/produit/{{ product.ref }}/éditer">{% endif %}{% if edit_mode %}<i class="icon-pencil"></i> {% 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 edit_mode %}<td><a href="/livraison/{{ delivery.id }}/{{ product.producer }}/{{ product.ref }}/éditer">modifier</a></td>{% endif %}
|
||||
<th{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} class="missing" title="Les commandes individuelles ne correspondent pas aux conditionnements"{% endif %}>
|
||||
{{ delivery.product_wanted(product) }}
|
||||
{% if delivery.status == delivery.ADJUSTMENT and delivery.product_missing(product) %} (−{{ delivery.product_missing(product) }})
|
||||
{% if request.user.is_staff %}<a href="/livraison/{{ delivery.id }}/ajuster/{{ product.ref }}" class="button" title="ajuster le produit">ajuster</a>{% endif %}
|
||||
{% if request.user.is_staff %}<a href="/distribution/{{ delivery.id }}/ajuster/{{ product.ref }}" class="button" title="ajuster le produit">ajuster</a>{% endif %}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% if not list_only %}
|
||||
|
@ -80,4 +77,6 @@
|
|||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<br />
|
||||
{% endfor %}
|
|
@ -1,8 +0,0 @@
|
|||
<label for="modal-{{ unique_id }}" class="toggle-label">{% block modal_label %}{% endblock %}</label>
|
||||
<input type="checkbox" id="modal-{{ unique_id }}" class="toggle">
|
||||
<label for="modal-{{ unique_id }}" class="toggle-background"></label>
|
||||
<div class="toggle-container">
|
||||
{% block modal_body %}
|
||||
{% endblock modal_body %}
|
||||
<label for="modal-{{ unique_id }}" class="toggle-label">Fermer</label>
|
||||
</div>
|
|
@ -1,10 +0,0 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% block modal_label %}<i class="icon-hazardous"></i> Ajouter une commande{% endblock modal_label %}
|
||||
{% block modal_body %}
|
||||
<form action="/livraison/{{ delivery.id }}/commander">
|
||||
<h4>Ajouter une commande pour quelqu'un d'autre</h4>
|
||||
<input type="text" name="orderer" placeholder="Identifiant de groupe" required>
|
||||
<input type="submit" value="Commander">
|
||||
</form>
|
||||
{% endblock modal_body %}
|
|
@ -1,8 +0,0 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% block modal_label %}<i class="icon-clipboard"></i> Copier les emails{% endblock modal_label %}
|
||||
{% block modal_body %}
|
||||
<div>
|
||||
<textarea>{{ ",".join(delivery.orders.keys()) }}</textarea>
|
||||
</div>
|
||||
{% endblock modal_body %}
|
|
@ -1,19 +0,0 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% block modal_label %}<i class="icon-paperclip"></i> Importer une commande{% endblock modal_label %}
|
||||
{% block modal_body %}
|
||||
<h4>Importer une commande</h4>
|
||||
<p>Colonnes: ref*, wanted*</p>
|
||||
<form action="/livraison/{{ delivery.id }}/importer/commande" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="data">
|
||||
<input type="email" name="email" placeholder="email">
|
||||
<input type="submit" name="Importer">
|
||||
</form>
|
||||
|
||||
<h4>Importer plusieurs commandes</h4>
|
||||
<p>Colonnes: ref*, toto@domain.tld, etc.</p>
|
||||
<form action="/livraison/{{ delivery.id }}/importer/commandes" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="data">
|
||||
<input type="submit" name="Importer">
|
||||
</form>
|
||||
{% endblock modal_body %}
|
|
@ -1,44 +0,0 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% 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>
|
||||
<details>
|
||||
<p>Le tableur doit contenir deux onglets, le premier avec les produits, le second avec les producteurs.</p>
|
||||
<summary>Détails des colonnes produits</summary>
|
||||
<dl>
|
||||
<dt class="mandatory">ref</dt>
|
||||
<dd>Référence unique du produit (permet de mettre à jour les produits en cours de commande ou d'importer des commandes individuelles).</dd>
|
||||
<dt class="mandatory">name</dt>
|
||||
<dd>Nom du produit: mettre juste assez d'info pour distinguer les produits les uns des autres.</dd>
|
||||
<dt class="mandatory">price</dt>
|
||||
<dd>Prix d'une unité, en euros.</dd>
|
||||
<dt>unit</dt>
|
||||
<dd>Conditionnement d'une unité: 1kg, 33cl…</dd>
|
||||
<dt>producer</dt>
|
||||
<dd>Le nom du producteur. Utilisé pour pouvoir grouper les produits par producteur.</dd>
|
||||
<dt>description</dt>
|
||||
<dd>Plus de détails sur le produit.</dd>
|
||||
<dt>packing</dt>
|
||||
<dd>Contionnement final pour grouper les commandes, le cas échéant, en nombre d'unités.</dd>
|
||||
<dt>url</dt>
|
||||
<dd>Une URL éventuelle pointant sur une page présentant le produit.</dd>
|
||||
<dt>img</dt>
|
||||
<dd>Une URL éventuelle pointant sur une image du produit (attention, utiliser seulement des liens https).</dd>
|
||||
</dl>
|
||||
<summary>Détails des colonnes producteur</summary>
|
||||
<dl>
|
||||
<dt class="mandatory">id</dt>
|
||||
<dd>Nom du producteur (doit être le même que le premier onglet).</dd>
|
||||
<dt>referent</dt>
|
||||
<dd>Le nom du ou de la référent⋅e</dd>
|
||||
<dt>contact</dt>
|
||||
<dd>Un moyen de contacter le⋅a product⋅eur⋅rice</dd>
|
||||
</dl>
|
||||
</details>
|
||||
<form action="/livraison/{{ delivery.id }}/importer/produits" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="data" required>
|
||||
<input type="submit" value="Mettre à jour les produits et product⋅eur⋅rice⋅s">
|
||||
</form>
|
||||
{% endblock modal_body %}
|
|
@ -1,9 +0,0 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% block modal_label %}(Détails){% endblock modal_label %}
|
||||
{% block modal_body %}
|
||||
<p>{{ product.description }}</p>
|
||||
<p>{% if product.img %}
|
||||
<img src="{{ product.img }}">
|
||||
{% endif %}</p>
|
||||
{% endblock modal_body %}
|
|
@ -1,5 +1,5 @@
|
|||
{% if (delivery.status == delivery.OPEN or delivery.status == delivery.ADJUSTMENT) and delivery.has_products %}
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/commander">
|
||||
<a class="button" href="/distribution/{{ delivery.id }}/commander">
|
||||
{% if delivery.status == delivery.ADJUSTMENT %}
|
||||
Ajuster ma commande
|
||||
{% elif delivery.status == delivery.OPEN %}
|
||||
|
|
7
copanier/templates/includes/toplinks.html
Normal file
7
copanier/templates/includes/toplinks.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% if link == "deliveries" %}
|
||||
↶ Retourner à la liste des distributions
|
||||
{% elif link == "products" %}
|
||||
↶ Retourner à la liste des produits
|
||||
{% elif link == "producer" %}
|
||||
↶ Retourner à la fiche product⋅eur⋅rice
|
||||
{% endif %}
|
|
@ -1,10 +0,0 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<p class="hide-on-print">
|
||||
<a href="/livraison/{{ delivery.id }}/producteurices.pdf{% if referent %}?referent={{ referent }}{% endif %}">Télécharger la liste des commandes en PDF</a>
|
||||
{% if referent %}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>{% endif %}
|
||||
</p>
|
||||
<article class="delivery">
|
||||
{% include "includes/delivery_table.html" %}
|
||||
</article>
|
||||
{% endblock body %}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
{% block body %}
|
||||
<article>
|
||||
<h3><a href="/livraison/{{ delivery.id }}">{{ delivery.producer }}</a> — Ajuster le produit «{{ product.name }}»</h3>
|
||||
<h3><a href="/distribution/{{ delivery.id }}">{{ delivery.producer }}</a> — Ajuster le produit «{{ product.name }}»</h3>
|
||||
<p><strong>Conditionnement</strong> {{ product.packing }} x {{ product.unit }}</p>
|
||||
<p><strong>Total commandé</strong> {{ delivery.product_wanted(product) }}</p>
|
||||
<p><strong>Manquant</strong> {{ delivery.product_missing(product) }}</p>
|
67
copanier/templates/products/delete.html
Normal file
67
copanier/templates/products/delete.html
Normal file
|
@ -0,0 +1,67 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Supprimer le produit « {{ product.name }} »</h1>
|
||||
<form method="post">
|
||||
<label>
|
||||
<p>Êtes vous sur⋅e de vouloir supprimer ce produit ?</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.001" 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>Conditionnement</h5>
|
||||
<h5><strong>A indiquer uniquement si ces produits ne peuvent pas être achetés à l'unité</strong>. Par exemple, si il est uniquement possible d'acheter des cartons de 12 bouteilles, alors indiquez « 12 » ici. Dans le doute, laissez vide.</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="/distribution/{{ 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="/distribution/{{ delivery.id }}/produit/{{ product.ref }}/éditer">éditer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
{% endblock body %}
|
|
@ -1,10 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block toplink %}<a href="/distribution/{{ delivery.id }}/{{ producer.id }}/éditer">↶ Retourner aux produits de {{ producer.name }}</a>{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if product.ref %}
|
||||
<h1>Modifier le produit « {{ product.name }} »</h1>
|
||||
{% else %}
|
||||
<h1>{{ producer_id }} : Nouveau produit</h1>
|
||||
<h1>{{ producer.name }} : Nouveau produit</h1>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<label>
|
||||
|
@ -13,7 +15,7 @@
|
|||
</label>
|
||||
<label>
|
||||
<p>Prix (en €), pour une unité</p>
|
||||
<input type="number" step="0.01" name="price" value="{{ product.price or '' }}" required>
|
||||
<input type="number" step="0.001" name="price" value="{{ product.price or '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
<p>Unité de commande</p>
|
||||
|
@ -24,12 +26,8 @@
|
|||
<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>
|
||||
<h5>Conditionnement</h5>
|
||||
<h5><strong>A indiquer uniquement si ces produits ne peuvent pas être achetés à l'unité</strong>. Par exemple, si il est uniquement possible d'acheter des cartons de 12 bouteilles, alors indiquez « 12 » ici. Dans le doute, laissez vide.</h5>
|
||||
<input type="number" name="packing" placeholder="6" value="{{ product.packing or '' }}">
|
||||
</label>
|
||||
<br />
|
||||
|
@ -59,13 +57,13 @@
|
|||
</thead>
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td><a href="/livraison/{{ delivery.id }}/produit/{{ product.ref }}/éditer">{{ product.name }}</a></td>
|
||||
<td><a href="/distribution/{{ 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>
|
||||
<td><a href="/distribution/{{ delivery.id }}/produit/{{ product.ref }}/éditer">éditer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
|
@ -1,18 +1,28 @@
|
|||
{% extends "base.html" %}
|
||||
{% block toplink %}<a href="/distribution/{{ delivery.id }}/produits">↶ Retourner aux produits</a>{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% if producer.id %}
|
||||
<h1>Modifier les informations pour « {{ producer.id }} »</h1>
|
||||
<div class="header">
|
||||
{% if producer and producer.id %}
|
||||
<h1>Modifier les informations pour « {{ producer.name }} »</h1>
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id}}/{{ producer.id }}/ajouter-produit">Ajouter un produit</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1>Nouve⋅eau⋅lle product⋅eur⋅rice</h1>
|
||||
<h1>Ajouter un⋅e producteur⋅rice</h1>
|
||||
{% endif %}
|
||||
</div>
|
||||
<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 '' }}">
|
||||
<p>Nom</p>
|
||||
<input type="text" name="name" value="{{ producer.name or '' }}">
|
||||
</label>
|
||||
{% endif %}
|
||||
<label>
|
||||
|
@ -32,7 +42,7 @@
|
|||
<input type="tel" name="referent_tel" value="{{ producer.referent_tel or '' }}" required>
|
||||
</label>
|
||||
<label>
|
||||
<h5>Contact du⋅la product⋅eur⋅rice</h5>
|
||||
<h5>Contact product⋅eur⋅rice</h5>
|
||||
<input type="text" name="contact" value="{{ producer.contact or '' }}">
|
||||
</label>
|
||||
<div>
|
||||
|
@ -41,9 +51,9 @@
|
|||
</form>
|
||||
|
||||
{% if products %}
|
||||
<h3>Produits <a class="button" href="/livraison/{{ delivery.id}}/{{ producer.id }}/ajouter-produit">Ajouter un produit</a></h3>
|
||||
|
||||
<table>
|
||||
<h3>Produits <a class="button" href="/distribution/{{ delivery.id}}/{{ producer.id }}/ajouter-produit">Ajouter un produit</a></h3>
|
||||
<p>Vous pouvez éditer les produits en cliquant sur leur nom.</p>
|
||||
<table class="pure-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Produit</th>
|
||||
|
@ -57,13 +67,13 @@
|
|||
</thead>
|
||||
{% for product in products %}
|
||||
<tr>
|
||||
<td><a href="/livraison/{{ delivery.id }}/{{ producer.id }}/{{ product.ref }}/éditer">{{ product.name }}</a></td>
|
||||
<td><a href="/distribution/{{ delivery.id }}/{{ producer.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 }}/{{ producer.id }}/{{ product.ref }}/éditer">éditer</a></td>
|
||||
<td><a href="/distribution/{{ delivery.id }}/{{ producer.id }}/{{ product.ref }}/supprimer">supprimer</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
27
copanier/templates/products/list.html
Normal file
27
copanier/templates/products/list.html
Normal file
|
@ -0,0 +1,27 @@
|
|||
{% extends "base.html" %}
|
||||
{% block toplink %}<a href="/distribution/{{ delivery.id }}">↶ Retourner à la distribution</a>{% endblock %}
|
||||
{% block body %}
|
||||
<div class="header">
|
||||
<h1>Gérer les produits</h1>
|
||||
<div class="pure-menu pure-menu-horizontal">
|
||||
<ul class="pure-menu-list">
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/producteurices/créer/{{ delivery.id }}"><i class="icon-heart"></i> Ajouter un⋅e producteur⋅rice</a>
|
||||
</li>
|
||||
<li class="pure-menu-item">
|
||||
<a class="pure-menu-link" href="/distribution/{{ delivery.id }}/copier"><i class="icon-hotairballoon"></i> Réutiliser les produits d'une autre distrib</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hide-on-print">
|
||||
{% if producers %}
|
||||
<a href="/distribution/{{ delivery.id }}/producteurices.pdf{% if referent %}?referent={{ referent }}{% endif %}">Télécharger la liste des commandes en PDF</a>
|
||||
{% endif %}
|
||||
{% if referent %}Voici la liste des product⋅eurs⋅rices dont {{ referent }} est référent⋅e. <a class="button" href="/distribution/{{delivery.id}}/producteurices">voir tous les produits</a>{% endif %}
|
||||
</p>
|
||||
|
||||
<article class="delivery">
|
||||
{% include "includes/delivery_table.html" %}
|
||||
</article>
|
||||
{% endblock body %}
|
|
@ -27,3 +27,11 @@ def read_token(token):
|
|||
def prefix(string, delivery):
|
||||
date = delivery.to_date.strftime("%Y-%m-%d")
|
||||
return f"{config.SITE_NAME}-{date}-{string}"
|
||||
|
||||
|
||||
def date_filter(value):
|
||||
return value.strftime(r"%A %d %B")
|
||||
|
||||
|
||||
def time_filter(value):
|
||||
return value.strftime(r"%H:%M")
|
||||
|
|
1
copanier/views/__init__.py
Normal file
1
copanier/views/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from . import delivery, products, groups, login # noqa : import to scan the routes.
|
130
copanier/views/core.py
Normal file
130
copanier/views/core.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import ujson as json
|
||||
|
||||
from pathlib import Path
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
from roll.extensions import traceback
|
||||
from roll import Roll as BaseRoll, Response as RollResponse
|
||||
|
||||
from weasyprint import HTML
|
||||
|
||||
from . import session
|
||||
from .. import config, utils, loggers
|
||||
|
||||
|
||||
class Response(RollResponse):
|
||||
def render_template(self, template_name, *args, **kwargs):
|
||||
context = app.context()
|
||||
context.update(kwargs)
|
||||
context["request"] = self.request
|
||||
context["config"] = config
|
||||
context["request"] = self.request
|
||||
if self.request.cookies.get("message"):
|
||||
context["message"] = json.loads(self.request.cookies["message"])
|
||||
self.cookies.set("message", "")
|
||||
return env.get_template(template_name).render(*args, **context)
|
||||
|
||||
def html(self, template_name, *args, **kwargs):
|
||||
self.headers["Content-Type"] = "text/html; charset=utf-8"
|
||||
self.body = self.render_template(template_name, *args, **kwargs)
|
||||
|
||||
def render_pdf(self, template_name, *args, **kwargs):
|
||||
html = self.render_template(template_name, *args, **kwargs)
|
||||
|
||||
static_folder = Path(__file__).parent.parent / "static"
|
||||
stylesheets = [
|
||||
static_folder / "app.css",
|
||||
static_folder / "icomoon.css",
|
||||
static_folder / "page.css",
|
||||
]
|
||||
if "css" in kwargs:
|
||||
stylesheets.append(static_folder / kwargs["css"])
|
||||
|
||||
return HTML(string=html).write_pdf(stylesheets=stylesheets)
|
||||
|
||||
def pdf(self, template_name, *args, **kwargs):
|
||||
self.body = self.render_pdf(template_name, *args, **kwargs)
|
||||
mimetype = "application/pdf"
|
||||
filename = kwargs.get("filename", "file.pdf")
|
||||
self.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
self.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||
|
||||
def xlsx(self, body, filename=f"{config.SITE_NAME}.xlsx"):
|
||||
self.body = body
|
||||
mimetype = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
self.headers["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||
self.headers["Content-Type"] = f"{mimetype}; charset=utf-8"
|
||||
|
||||
def redirect(self, location):
|
||||
self.status = 302
|
||||
self.headers["Location"] = location
|
||||
|
||||
redirect = property(None, redirect)
|
||||
|
||||
def message(self, text, status="success"):
|
||||
self.cookies.set("message", json.dumps((text, status)))
|
||||
|
||||
|
||||
class Roll(BaseRoll):
|
||||
Response = Response
|
||||
|
||||
_context_func = []
|
||||
|
||||
def context(self):
|
||||
context = {}
|
||||
for func in self._context_func:
|
||||
context.update(func())
|
||||
return context
|
||||
|
||||
def register_context(self, func):
|
||||
self._context_func.append(func)
|
||||
|
||||
|
||||
def staff_only(view):
|
||||
async def decorator(request, response, *args, **kwargs):
|
||||
user = session.user.get(None)
|
||||
if not user or not user.is_staff:
|
||||
response.message("Désolé, c'est réservé au staff par ici", "warning")
|
||||
response.redirect = request.headers.get("REFERRER", "/")
|
||||
return
|
||||
return await view(request, response, *args, **kwargs)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def configure():
|
||||
config.init()
|
||||
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader("copanier", "templates"),
|
||||
autoescape=select_autoescape(["copanier"]),
|
||||
)
|
||||
|
||||
env.filters["date"] = utils.date_filter
|
||||
env.filters["time"] = utils.time_filter
|
||||
|
||||
app = Roll()
|
||||
traceback(app)
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def attach_request(request, response):
|
||||
response.request = request
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def log_request(request, response):
|
||||
if request.method == "POST":
|
||||
message = {
|
||||
"date": utils.utcnow().isoformat(),
|
||||
"data": request.form,
|
||||
"user": request.get("user"),
|
||||
}
|
||||
loggers.request_logger.info(
|
||||
json.dumps(message, sort_keys=True, ensure_ascii=False)
|
||||
)
|
||||
|
||||
|
||||
@app.listen("startup")
|
||||
async def on_startup():
|
||||
configure()
|
379
copanier/views/delivery.py
Normal file
379
copanier/views/delivery.py
Normal file
|
@ -0,0 +1,379 @@
|
|||
from collections import defaultdict
|
||||
from functools import partial
|
||||
from roll import HttpError
|
||||
|
||||
from debts.solver import order_balance, check_balance, reduce_balance
|
||||
|
||||
from .core import app, staff_only, session, env
|
||||
from ..models import Delivery, Person, Order, ProductOrder
|
||||
from .. import utils, reports, emails, config
|
||||
|
||||
|
||||
@app.listen("startup")
|
||||
async def on_startup():
|
||||
Delivery.init_fs()
|
||||
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
async def home(request, response):
|
||||
if not request["user"].group_id:
|
||||
response.redirect = "/groupes"
|
||||
return
|
||||
response.html(
|
||||
"delivery/list.html",
|
||||
incoming=Delivery.incoming(),
|
||||
former=Delivery.former(),
|
||||
archives=list(Delivery.all(is_archived=True)),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/archives", methods=["GET"])
|
||||
async def view_archives(request, response):
|
||||
response.html(
|
||||
"delivery/archives.html", {"deliveries": Delivery.all(is_archived=True)}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/archive/{id}", methods=["GET"])
|
||||
async def view_archive(request, response, id):
|
||||
delivery = Delivery.load(f"archive/{id}")
|
||||
response.html("delivery/show.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/archiver", methods=["GET"])
|
||||
@staff_only
|
||||
async def archive_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
delivery.archive()
|
||||
response.message("La distribution a été archivée")
|
||||
response.redirect = f"/distribution/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/distribution/archive/{id}/désarchiver", methods=["GET"])
|
||||
@staff_only
|
||||
async def unarchive_delivery(request, response, id):
|
||||
delivery = Delivery.load(f"archive/{id}")
|
||||
delivery.unarchive()
|
||||
response.message("La distribution a été désarchivée")
|
||||
response.redirect = f"/distribution/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/distribution", methods=["GET"])
|
||||
async def new_delivery(request, response):
|
||||
response.html("delivery/edit.html", delivery={})
|
||||
|
||||
|
||||
@app.route("/distribution", methods=["POST"])
|
||||
@staff_only
|
||||
async def create_delivery(request, response):
|
||||
form = request.form
|
||||
data = {}
|
||||
data["from_date"] = f"{form.get('date')} {form.get('from_time')}"
|
||||
data["to_date"] = f"{form.get('date')} {form.get('to_time')}"
|
||||
for name in Delivery.__dataclass_fields__.keys():
|
||||
if name in form:
|
||||
data[name] = form.get(name)
|
||||
delivery = Delivery(**data)
|
||||
delivery.persist()
|
||||
response.message("La distribution a bien été créée!")
|
||||
response.redirect = f"/distribution/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/{producer}/bon-de-commande.pdf", methods=["GET"])
|
||||
async def pdf_for_producer(request, response, id, producer):
|
||||
delivery = Delivery.load(id)
|
||||
response.pdf(
|
||||
"product_list.html",
|
||||
{"list_only": True, "delivery": delivery, "producers": [producer]},
|
||||
filename=utils.prefix(f"bon-de-commande-{producer}.pdf", delivery),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/gérer", methods=["GET"])
|
||||
async def delivery_toolbox(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html(
|
||||
"delivery/toolbox.html",
|
||||
{
|
||||
"delivery": delivery,
|
||||
"referents": [p.referent for p in delivery.producers.values()],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/envoi-email-referentes", methods=["GET", "POST"])
|
||||
async def send_referent_emails(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
if request.method == "POST":
|
||||
email_body = request.form.get("email_body")
|
||||
email_subject = request.form.get("email_subject")
|
||||
sent_mails = 0
|
||||
for referent in delivery.get_referents():
|
||||
producers = delivery.get_producers_for_referent(referent)
|
||||
attachments = []
|
||||
for producer in producers:
|
||||
if delivery.producers[producer].has_active_products(delivery):
|
||||
pdf_file = response.render_pdf(
|
||||
"list_products.html",
|
||||
{
|
||||
"list_only": True,
|
||||
"delivery": delivery,
|
||||
"producers": [producer],
|
||||
},
|
||||
)
|
||||
|
||||
attachments.append(
|
||||
(
|
||||
utils.prefix(f"{producer}.pdf", delivery),
|
||||
pdf_file,
|
||||
"application/pdf",
|
||||
)
|
||||
)
|
||||
|
||||
if attachments:
|
||||
sent_mails = sent_mails + 1
|
||||
emails.send(
|
||||
referent,
|
||||
email_subject,
|
||||
email_body,
|
||||
copy=delivery.contact,
|
||||
attachments=attachments,
|
||||
)
|
||||
response.message(f"Un mail à été envoyé aux {sent_mails} référent⋅e⋅s")
|
||||
response.redirect = f"/distribution/{id}/gérer"
|
||||
|
||||
response.html("delivery/referent_email.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/exporter", methods=["GET"])
|
||||
async def export_products(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.xlsx(reports.products(delivery))
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/edit", methods=["GET"])
|
||||
@staff_only
|
||||
async def edit_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html("delivery/edit.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/edit", methods=["POST"])
|
||||
@staff_only
|
||||
async def post_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
form = request.form
|
||||
delivery.from_date = f"{form.get('date')} {form.get('from_time')}"
|
||||
delivery.to_date = f"{form.get('date')} {form.get('to_time')}"
|
||||
for name in Delivery.__dataclass_fields__.keys():
|
||||
if name in form:
|
||||
setattr(delivery, name, form.get(name))
|
||||
delivery.persist()
|
||||
response.message("La distribution a bien été mise à jour!")
|
||||
response.redirect = f"/distribution/{delivery.id}"
|
||||
|
||||
|
||||
@app.route("/distribution/{id}", methods=["GET"])
|
||||
async def view_delivery(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.html("delivery/show.html", {"delivery": delivery})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/commander", methods=["POST", "GET"])
|
||||
async def place_order(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
# email = request.query.get("email", None)
|
||||
user = session.user.get(None)
|
||||
orderer = request.query.get("orderer", None)
|
||||
if orderer:
|
||||
orderer = Person(email=orderer, group_id=orderer)
|
||||
|
||||
delivery_url = f"/distribution/{delivery.id}"
|
||||
if not orderer and user:
|
||||
orderer = user
|
||||
|
||||
if not orderer:
|
||||
response.message("Impossible de comprendre pour qui passer commande…", "error")
|
||||
response.redirect = delivery_url
|
||||
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):
|
||||
response.message("La distribution est fermée", "error")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
|
||||
form = request.form
|
||||
order = Order(phone_number=form.get("phone_number", ""))
|
||||
for product in delivery.products:
|
||||
try:
|
||||
wanted = form.int(f"wanted:{product.ref}", 0)
|
||||
except HttpError:
|
||||
continue
|
||||
try:
|
||||
adjustment = form.int(f"adjustment:{product.ref}", 0)
|
||||
except HttpError:
|
||||
adjustment = 0
|
||||
if wanted or adjustment:
|
||||
order.products[product.ref] = ProductOrder(
|
||||
wanted=wanted, adjustment=adjustment
|
||||
)
|
||||
|
||||
if not delivery.orders:
|
||||
delivery.orders = {}
|
||||
|
||||
if not order.products:
|
||||
if orderer.id in delivery.orders:
|
||||
del delivery.orders[orderer.id]
|
||||
delivery.persist()
|
||||
response.message("La commande est vide.", status="warning")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
delivery.orders[orderer.id] = order
|
||||
delivery.persist()
|
||||
|
||||
if user and orderer.id == user.id:
|
||||
# Send the emails to everyone in the group.
|
||||
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,
|
||||
)
|
||||
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, "
|
||||
"on a envoyé un récap par email 😘"
|
||||
)
|
||||
response.redirect = f"/distribution/{delivery.id}"
|
||||
else:
|
||||
order = delivery.orders.get(orderer.id) or Order()
|
||||
force_adjustment = "adjust" in request.query and user and user.is_staff
|
||||
response.html(
|
||||
"delivery/place_order.html",
|
||||
delivery=delivery,
|
||||
person=orderer,
|
||||
order=order,
|
||||
force_adjustment=force_adjustment,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/émargement", methods=["GET"])
|
||||
async def signing_sheet(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
response.pdf(
|
||||
"delivery/signing_sheet.html",
|
||||
{"delivery": delivery},
|
||||
css="signing-sheet.css",
|
||||
filename=utils.prefix("commandes-par-groupe.pdf", delivery),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{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",
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/ajuster/{ref}", methods=["GET", "POST"])
|
||||
@staff_only
|
||||
async def adjust_product(request, response, id, ref):
|
||||
delivery = Delivery.load(id)
|
||||
delivery_url = f"/distribution/{delivery.id}"
|
||||
product = None
|
||||
for product in delivery.products:
|
||||
if product.ref == ref:
|
||||
break
|
||||
else:
|
||||
response.message(f"Référence inconnue: {ref}")
|
||||
response.redirect = delivery_url
|
||||
return
|
||||
if request.method == "POST":
|
||||
form = request.form
|
||||
for email, order in delivery.orders.items():
|
||||
choice = order[product]
|
||||
choice.adjustment = form.int(email, 0)
|
||||
order[product] = choice
|
||||
delivery.persist()
|
||||
response.message(f"Le produit «{product.ref}» a bien été ajusté!")
|
||||
response.redirect = delivery_url
|
||||
else:
|
||||
response.html("adjust_product.html", {"delivery": delivery, "product": product})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/solde", methods=["GET"])
|
||||
@app.route("/distribution/{id}/solde.pdf", methods=["GET"])
|
||||
@staff_only
|
||||
async def delivery_balance(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
groups = request["groups"]
|
||||
|
||||
balance = []
|
||||
for group_id, order in delivery.orders.items():
|
||||
balance.append((group_id, order.total(delivery.products) * -1))
|
||||
|
||||
producer_groups = {}
|
||||
|
||||
for producer in delivery.producers.values():
|
||||
group = groups.get_user_group(producer.referent)
|
||||
# When a group contains multiple producer contacts,
|
||||
# the first one is elected to receive the money,
|
||||
# and all the other ones are separated in the table.
|
||||
group_id = None
|
||||
if hasattr(group, "id"):
|
||||
if (
|
||||
group.id not in producer_groups
|
||||
or producer_groups[group.id] == producer.referent_name
|
||||
):
|
||||
producer_groups[group.id] = producer.referent_name
|
||||
group_id = group.id
|
||||
if not group_id:
|
||||
group_id = producer.referent_name
|
||||
|
||||
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[:])
|
||||
|
||||
results_dict = defaultdict(partial(defaultdict, float))
|
||||
|
||||
for debiter, amount, crediter in results:
|
||||
results_dict[debiter][crediter] = amount
|
||||
|
||||
template_name = "delivery/balance.html"
|
||||
template_args = {
|
||||
"delivery": delivery,
|
||||
"debiters": debiters,
|
||||
"crediters": crediters,
|
||||
"results": results_dict,
|
||||
"debiters_groups": groups.groups,
|
||||
"crediters_groups": producer_groups,
|
||||
}
|
||||
|
||||
if request.url.endswith(b".pdf"):
|
||||
response.pdf(
|
||||
template_name,
|
||||
template_args,
|
||||
filename=utils.prefix("répartition-des-chèques.pdf", delivery),
|
||||
)
|
||||
else:
|
||||
response.html(template_name, template_args)
|
72
copanier/views/groups.py
Normal file
72
copanier/views/groups.py
Normal file
|
@ -0,0 +1,72 @@
|
|||
from slugify import slugify
|
||||
from ..models import Groups, Group
|
||||
from .core import app, session
|
||||
|
||||
|
||||
@app.listen("startup")
|
||||
async def on_startup():
|
||||
Groups.init_fs()
|
||||
|
||||
|
||||
@app.route("/groupes", methods=["GET"])
|
||||
async def handle_groups(request, response):
|
||||
response.html("groups/list.html", {"groups": request["groups"]})
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/rejoindre", method=["GET"])
|
||||
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"
|
||||
|
||||
response.message(f"Vous avez bien rejoint le groupe « {group.name} »")
|
||||
response.redirect = redirect
|
||||
|
||||
|
||||
@app.route("/groupes/créer", methods=["GET", "POST"])
|
||||
async def create_group(request, response):
|
||||
group = None
|
||||
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)
|
||||
|
||||
group = Group.create(
|
||||
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éé")
|
||||
response.redirect = "/"
|
||||
response.html("groups/edit.html", group=group)
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/éditer", methods=["GET", "POST"])
|
||||
async def edit_group(request, response, id):
|
||||
assert id in request["groups"].groups, "Impossible de trouver le groupe"
|
||||
group = request["groups"].groups[id]
|
||||
if request.method == "POST":
|
||||
form = request.form
|
||||
members = []
|
||||
if form.get("members"):
|
||||
members = [m.strip() for m in form.get("members").split(",")]
|
||||
group.members = members
|
||||
group.name = form.get("name")
|
||||
request["groups"].groups[id] = group
|
||||
request["groups"].persist()
|
||||
response.redirect = "/groupes"
|
||||
response.html("groups/edit.html", group=group)
|
||||
|
||||
|
||||
@app.route("/groupes/{id}/supprimer", methods=["GET"])
|
||||
async def delete_group(request, response, id):
|
||||
assert id in request["groups"].groups, "Impossible de trouver le groupe"
|
||||
deleted = request["groups"].groups.pop(id)
|
||||
request["groups"].persist()
|
||||
response.message(f"Le groupe {deleted.name} à bien été supprimé")
|
||||
response.redirect = "/groupes"
|
77
copanier/views/login.py
Normal file
77
copanier/views/login.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from .core import app, session, env
|
||||
|
||||
from ..models import Groups, Person
|
||||
from .. import utils, emails, config
|
||||
|
||||
|
||||
@app.listen("request")
|
||||
async def auth_required(request, response):
|
||||
# Should be handled Roll side?
|
||||
# In dev mode, we serve the static, but we don't have yet a way to mark static
|
||||
# route as unprotected.
|
||||
if request.path.startswith("/static/"):
|
||||
return
|
||||
if request.route.payload and not request.route.payload.get("unprotected"):
|
||||
token = request.cookies.get("token")
|
||||
email = None
|
||||
if token:
|
||||
decoded = utils.read_token(token)
|
||||
email = decoded.get("sub")
|
||||
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}
|
||||
if group:
|
||||
user_info.update(dict(group_id=group.id, group_name=group.name))
|
||||
user = Person(**user_info)
|
||||
request["user"] = user
|
||||
session.user.set(user)
|
||||
|
||||
|
||||
@app.route("/sésame", methods=["GET"], unprotected=True)
|
||||
async def sesame(request, response):
|
||||
response.html("sesame.html")
|
||||
|
||||
|
||||
@app.route("/sésame", methods=["POST"], unprotected=True)
|
||||
async def send_sesame(request, response):
|
||||
email = request.form.get("email").lower()
|
||||
token = utils.create_token(email)
|
||||
try:
|
||||
emails.send_from_template(
|
||||
env,
|
||||
"access_granted",
|
||||
email,
|
||||
f"Sésame {config.SITE_NAME}",
|
||||
hostname=request.host,
|
||||
token=token.decode(),
|
||||
)
|
||||
except RuntimeError:
|
||||
response.message("Oops, impossible d'envoyer le courriel…", status="error")
|
||||
else:
|
||||
response.message(f"Un sésame vous a été envoyé à l'adresse '{email}'")
|
||||
response.redirect = "/"
|
||||
|
||||
|
||||
@app.route("/sésame/{token}", methods=["GET"], unprotected=True)
|
||||
async def set_sesame(request, response, token):
|
||||
decoded = utils.read_token(token)
|
||||
if not decoded:
|
||||
response.message("Sésame invalide :(", status="error")
|
||||
else:
|
||||
response.message("Yay! Le sésame a fonctionné. Bienvenue à bord! :)")
|
||||
response.cookies.set(
|
||||
name="token", value=token, httponly=True, max_age=60 * 60 * 24 * 7
|
||||
)
|
||||
response.redirect = "/"
|
||||
|
||||
|
||||
@app.route("/déconnexion", methods=["GET"])
|
||||
async def logout(request, response):
|
||||
response.cookies.set(name="token", value="", httponly=True)
|
||||
response.redirect = "/"
|
186
copanier/views/products.py
Normal file
186
copanier/views/products.py
Normal file
|
@ -0,0 +1,186 @@
|
|||
from slugify import slugify
|
||||
from .core import app
|
||||
from ..models import Delivery, Product, Producer
|
||||
from .. import utils
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/produits")
|
||||
@app.route("/distribution/{id}/produits.pdf")
|
||||
async def list_products(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
template_name = "products/list.html"
|
||||
template_params = {
|
||||
"edit_mode": True,
|
||||
"list_only": True,
|
||||
"delivery": delivery,
|
||||
"referent": request.query.get("referent", None),
|
||||
}
|
||||
|
||||
if request.url.endswith(b".pdf"):
|
||||
template_params["edit_mode"] = False
|
||||
response.pdf(
|
||||
template_name,
|
||||
template_params,
|
||||
filename=utils.prefix("producteurices.pdf", delivery),
|
||||
)
|
||||
else:
|
||||
response.html(template_name, template_params)
|
||||
|
||||
|
||||
@app.route("/distribution/{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.referent_tel = form.get("referent_tel")
|
||||
producer.referent_name = form.get("referent_name")
|
||||
producer.description = form.get("description")
|
||||
producer.contact = form.get("contact")
|
||||
delivery.producers[producer_id] = producer
|
||||
delivery.persist()
|
||||
|
||||
response.html(
|
||||
"products/edit_producer.html",
|
||||
{
|
||||
"delivery": delivery,
|
||||
"producer": producer,
|
||||
"products": delivery.get_products_by(producer.id),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{delivery_id}/{producer_id}/frais", methods=["GET", "POST"])
|
||||
async def handle_shipping_fees(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.referent_tel = form.get("referent_tel")
|
||||
producer.referent_name = form.get("referent_name")
|
||||
producer.description = form.get("description")
|
||||
producer.contact = form.get("contact")
|
||||
delivery.producers[producer_id] = producer
|
||||
delivery.persist()
|
||||
|
||||
response.html(
|
||||
"products/shipping_fees.html", {"delivery": delivery, "producer": producer}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/producteurices/créer/{delivery_id}", methods=["GET", "POST"])
|
||||
async def create_producer(request, response, delivery_id):
|
||||
delivery = Delivery.load(delivery_id)
|
||||
producer = None
|
||||
if request.method == "POST":
|
||||
form = request.form
|
||||
name = form.get("name")
|
||||
producer_id = slugify(name)
|
||||
|
||||
producer = Producer(name=name, id=producer_id)
|
||||
producer.referent = form.get("referent")
|
||||
producer.referent_tel = form.get("referent_tel")
|
||||
producer.referent_name = form.get("referent_name")
|
||||
producer.description = form.get("description")
|
||||
producer.contact = form.get("contact")
|
||||
|
||||
delivery.producers[producer_id] = producer
|
||||
delivery.persist()
|
||||
response.redirect = f"/distribution/{delivery.id}/produits"
|
||||
|
||||
response.html(
|
||||
"products/edit_producer.html",
|
||||
{"delivery": delivery, "producer": producer or None},
|
||||
)
|
||||
|
||||
|
||||
@app.route(
|
||||
"/distribution/{delivery_id}/{producer_id}/produit/{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)
|
||||
producer = delivery.producers.get(producer_id)
|
||||
|
||||
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", None)
|
||||
if form.get("packing"):
|
||||
product.packing = form.int("packing")
|
||||
else:
|
||||
product.packing = None
|
||||
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"/distribution/{delivery_id}/{producer_id}/éditer"
|
||||
return
|
||||
|
||||
response.html(
|
||||
"products/edit.html",
|
||||
{"delivery": delivery, "product": product, "producer": producer},
|
||||
)
|
||||
|
||||
|
||||
@app.route(
|
||||
"/distribution/{delivery_id}/{producer_id}/{product_ref}/supprimer", methods=["GET"]
|
||||
)
|
||||
async def delete_product(request, response, delivery_id, producer_id, product_ref):
|
||||
delivery = Delivery.load(delivery_id)
|
||||
product = delivery.delete_product(product_ref)
|
||||
delivery.persist()
|
||||
response.message(f"Le produit « { product.name } » à bien été supprimé.")
|
||||
response.redirect = f"/distribution/{delivery_id}/{producer_id}/éditer"
|
||||
|
||||
|
||||
@app.route(
|
||||
"/distribution/{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)
|
||||
producer = delivery.producers.get(producer_id)
|
||||
|
||||
if request.method == "POST":
|
||||
product.producer = producer_id
|
||||
form = request.form
|
||||
product.update_from_form(form)
|
||||
product.ref = slugify(f"{producer_id}-{product.name}-{product.unit}")
|
||||
|
||||
delivery.products.append(product)
|
||||
delivery.persist()
|
||||
response.message("Le produit à bien été créé")
|
||||
response.redirect = (
|
||||
f"/distribution/{delivery_id}/producteurice/{producer_id}/éditer"
|
||||
)
|
||||
return
|
||||
|
||||
response.html(
|
||||
"products/edit.html",
|
||||
{"delivery": delivery, "producer": producer, "product": product},
|
||||
)
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/copier", methods=["GET"])
|
||||
async def copy_products(request, response, id):
|
||||
deliveries = Delivery.all()
|
||||
response.html("delivery/copy.html", {"deliveries": deliveries})
|
||||
|
||||
|
||||
@app.route("/distribution/{id}/copier", methods=["POST"])
|
||||
async def copy_products(request, response, id):
|
||||
delivery = Delivery.load(id)
|
||||
to_copy = delivery.load(request.form.get("to_copy"))
|
||||
delivery.producers = to_copy.producers
|
||||
delivery.products = to_copy.products
|
||||
delivery.persist()
|
||||
response.redirect = f"/distribution/{id}/producteurices"
|
|
@ -7,7 +7,7 @@ Vous êtes la personne référente pour une distribution, et vous vous demandez
|
|||
### Création d'une nouvelle distribution
|
||||
|
||||
La première chose à faire, c'est de créer une nouvelle distribution.
|
||||
Dans l'interface, en bas à gauche il y a un bouton « Nouvelle livraison ».
|
||||
Dans l'interface, en bas à gauche il y a un bouton « Nouvelle distribution ».
|
||||
|
||||
Il vous sera demandé quelques informations, puis vous pouvez valider.
|
||||
|
||||
|
@ -16,15 +16,15 @@ Il vous sera demandé quelques informations, puis vous pouvez valider.
|
|||
Par défaut, une nouvelle distribution est vide. Il faut donc ajouter les produits.
|
||||
Le plus simple à cette étape est de le faire avec un tableur.
|
||||
|
||||
Soit on télécharge un tableur vide et on le remplit, soit on récupère les infos d'une autre livraison.
|
||||
Soit on télécharge un tableur vide et on le remplit, soit on récupère les infos d'une autre distribution.
|
||||
|
||||
### Récupérer les infos d'une autre livraison
|
||||
### Récupérer les infos d'une autre distribution
|
||||
|
||||
Il faut aller dans la livraison en question, puis, en bas, faire « Télécharger les infos produits »
|
||||
Il faut aller dans la distribution en question, puis, en bas, faire « Télécharger les infos produits »
|
||||
|
||||
### Importer des produits
|
||||
|
||||
Une fois la nouvelle livraison créée, il est possible d'importer les produits. Quand on clique dessus, il y a un bouton qui permet de le faire, on selectionne le tableur avec les infos produits et on l'importe.
|
||||
Une fois la nouvelle distribution créée, il est possible d'importer les produits. Quand on clique dessus, il y a un bouton qui permet de le faire, on selectionne le tableur avec les infos produits et on l'importe.
|
||||
|
||||
## Mise à jour des prix
|
||||
|
||||
|
@ -43,7 +43,7 @@ Avant et pendant la distrib :
|
|||
Gérer les groupes / colocs
|
||||
|
||||
Une fois les commandes passées (après le dimanche 26 janvier)
|
||||
Télécharger les bons de livraison
|
||||
Télécharger les bons de distribution
|
||||
Télécharger le tableau des commandes
|
||||
Envoyer les infos de commande aux référent⋅e⋅s
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ async def test_create_delivery(client):
|
|||
"order_before": "2019-02-12",
|
||||
"contact": "lucky@you.me",
|
||||
}
|
||||
resp = await client.post("/livraison", body=body)
|
||||
resp = await client.post("/distribution", body=body)
|
||||
assert resp.status == 302
|
||||
assert len(list(Delivery.all())) == 1
|
||||
delivery = list(Delivery.all())[0]
|
||||
|
@ -56,7 +56,7 @@ async def test_create_delivery(client):
|
|||
async def test_place_order_with_session(client, delivery):
|
||||
delivery.persist()
|
||||
body = {"wanted:123": "3"}
|
||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
||||
resp = await client.post(f"/distribution/{delivery.id}/commander", body=body)
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert delivery.orders["foo@bar.org"]
|
||||
|
@ -65,7 +65,7 @@ async def test_place_order_with_session(client, delivery):
|
|||
|
||||
async def test_place_empty_order(client, delivery):
|
||||
delivery.persist()
|
||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body={})
|
||||
resp = await client.post(f"/distribution/{delivery.id}/commander", body={})
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert not delivery.orders
|
||||
|
@ -74,7 +74,7 @@ async def test_place_empty_order(client, delivery):
|
|||
async def test_place_empty_order_should_delete_previous(client, delivery):
|
||||
delivery.orders["foo@bar.org"] = Order(products={"123": ProductOrder(wanted=1)})
|
||||
delivery.persist()
|
||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body={})
|
||||
resp = await client.post(f"/distribution/{delivery.id}/commander", body={})
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(delivery.id)
|
||||
assert not delivery.orders
|
||||
|
@ -83,7 +83,7 @@ async def test_place_empty_order_should_delete_previous(client, delivery):
|
|||
async def test_place_order_with_empty_string(client, delivery):
|
||||
delivery.persist()
|
||||
body = {"wanted:123": ""} # User deleted the field value.
|
||||
resp = await client.post(f"/livraison/{delivery.id}/commander", body=body)
|
||||
resp = await client.post(f"/distribution/{delivery.id}/commander", body=body)
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert not delivery.orders
|
||||
|
@ -94,7 +94,7 @@ async def test_get_place_order_with_closed_delivery(client, delivery, monkeypatc
|
|||
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")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="wanted:123"]').attr("readonly")
|
||||
assert not doc('[name="adjustment:123"]')
|
||||
|
@ -102,7 +102,7 @@ async def test_get_place_order_with_closed_delivery(client, delivery, monkeypatc
|
|||
|
||||
|
||||
async def test_get_place_order_with_adjustment_status(client, delivery):
|
||||
resp = await client.get(f"/livraison/{delivery.id}/commander")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander")
|
||||
doc = pq(resp.body)
|
||||
assert not doc('[name="wanted:123"]').attr("readonly")
|
||||
assert not doc('[name="adjustment:123"]')
|
||||
|
@ -115,7 +115,7 @@ async def test_get_place_order_with_adjustment_status(client, delivery):
|
|||
)
|
||||
delivery.persist()
|
||||
assert delivery.status == delivery.ADJUSTMENT
|
||||
resp = await client.get(f"/livraison/{delivery.id}/commander")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="wanted:123"]').attr("readonly")
|
||||
assert doc('[name="adjustment:123"]')
|
||||
|
@ -138,7 +138,7 @@ async def test_get_place_order_with_closed_delivery_but_adjustments(client, deli
|
|||
)
|
||||
delivery.persist()
|
||||
assert delivery.status == delivery.CLOSED
|
||||
resp = await client.get(f"/livraison/{delivery.id}/commander")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="wanted:123"]').attr("readonly")
|
||||
assert doc('[name="adjustment:123"]')
|
||||
|
@ -149,11 +149,11 @@ async def test_get_place_order_with_closed_delivery_but_force(client, delivery):
|
|||
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")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="wanted:123"]').attr("readonly") is not None
|
||||
assert not doc('[name="adjustment:123"]')
|
||||
resp = await client.get(f"/livraison/{delivery.id}/commander?adjust")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/commander?adjust")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="wanted:123"]').attr("readonly") is not None
|
||||
assert doc('[name="adjustment:123"]')
|
||||
|
@ -164,7 +164,7 @@ async def test_cannot_place_order_on_closed_delivery(client, delivery, monkeypat
|
|||
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)
|
||||
resp = await client.post(f"/distribution/{delivery.id}/commander", body=body)
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert not delivery.orders
|
||||
|
@ -178,7 +178,7 @@ async def test_get_adjust_product(client, delivery):
|
|||
)
|
||||
delivery.persist()
|
||||
assert delivery.status == delivery.ADJUSTMENT
|
||||
resp = await client.get(f"/livraison/{delivery.id}/ajuster/123")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/ajuster/123")
|
||||
doc = pq(resp.body)
|
||||
assert doc('[name="foo@bar.org"]')
|
||||
assert doc('[name="foo@bar.org"]').attr("value") == "1"
|
||||
|
@ -191,7 +191,7 @@ async def test_post_adjust_product(client, delivery):
|
|||
delivery.persist()
|
||||
assert delivery.status == delivery.ADJUSTMENT
|
||||
body = {"foo@bar.org": "1"}
|
||||
resp = await client.post(f"/livraison/{delivery.id}/ajuster/123", body=body)
|
||||
resp = await client.post(f"/distribution/{delivery.id}/ajuster/123", body=body)
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert delivery.orders["foo@bar.org"].products["123"].wanted == 2
|
||||
|
@ -204,10 +204,10 @@ async def test_only_staff_can_adjust_product(client, delivery, monkeypatch):
|
|||
delivery.orders["foo@bar.org"] = Order(products={"123": ProductOrder(wanted=2)})
|
||||
delivery.persist()
|
||||
monkeypatch.setattr("copanier.config.STAFF", ["someone@else.org"])
|
||||
resp = await client.get(f"/livraison/{delivery.id}/ajuster/123")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/ajuster/123")
|
||||
assert resp.status == 302
|
||||
body = {"foo@bar.org": "1"}
|
||||
resp = await client.post(f"/livraison/{delivery.id}/ajuster/123", body=body)
|
||||
resp = await client.post(f"/distribution/{delivery.id}/ajuster/123", body=body)
|
||||
assert resp.status == 302
|
||||
delivery = Delivery.load(id=delivery.id)
|
||||
assert delivery.orders["foo@bar.org"].products["123"].wanted == 2
|
||||
|
@ -216,7 +216,7 @@ async def test_only_staff_can_adjust_product(client, delivery, monkeypatch):
|
|||
|
||||
async def test_export_products(client, delivery):
|
||||
delivery.persist()
|
||||
resp = await client.get(f"/livraison/{delivery.id}/exporter")
|
||||
resp = await client.get(f"/distribution/{delivery.id}/exporter")
|
||||
wb = load_workbook(filename=BytesIO(resp.body))
|
||||
assert list(wb.active.values) == [
|
||||
("name", "ref", "price", "unit", "description", "url", "img", "packing", "producer"),
|
||||
|
|
Loading…
Reference in a new issue