mirror of
https://github.com/almet/copanier.git
synced 2025-04-28 19:42:37 +02:00
Merge branch 'archive' into 'master'
Archive deliveries See merge request ybon/copanier!15
This commit is contained in:
commit
04d5fca259
11 changed files with 205 additions and 65 deletions
|
@ -177,6 +177,35 @@ async def home(request, response):
|
|||
response.html("home.html", incoming=Delivery.incoming(), former=Delivery.former())
|
||||
|
||||
|
||||
@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={})
|
||||
|
|
|
@ -151,6 +151,7 @@ class Delivery(Base):
|
|||
CLOSED = 0
|
||||
OPEN = 1
|
||||
ADJUSTMENT = 2
|
||||
ARCHIVED = 3
|
||||
|
||||
producer: str
|
||||
from_date: datetime_field
|
||||
|
@ -162,11 +163,16 @@ class Delivery(Base):
|
|||
where: str = "Marché de la Briche"
|
||||
products: List[Product] = field(default_factory=list)
|
||||
orders: Dict[str, Order] = field(default_factory=dict)
|
||||
id: str = field(default_factory=lambda *a, **k: uuid.uuid4().hex)
|
||||
infos_url: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
self.id = None # Not a field because we don't want to persist it.
|
||||
super().__post_init__()
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if self.is_archived:
|
||||
return self.ARCHIVED
|
||||
if self.is_open:
|
||||
return self.OPEN
|
||||
if self.needs_adjustment:
|
||||
|
@ -197,9 +203,14 @@ class Delivery(Base):
|
|||
def needs_adjustment(self):
|
||||
return self.has_packing and any(self.product_missing(p) for p in self.products)
|
||||
|
||||
@property
|
||||
def is_archived(self):
|
||||
return self.id and self.id.startswith("archive/")
|
||||
|
||||
@classmethod
|
||||
def init_fs(cls):
|
||||
cls.get_root().mkdir(parents=True, exist_ok=True)
|
||||
cls.get_root().joinpath("archive").mkdir(exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def get_root(cls):
|
||||
|
@ -210,12 +221,21 @@ class Delivery(Base):
|
|||
path = cls.get_root() / f"{id}.yml"
|
||||
if not path.exists():
|
||||
raise DoesNotExist
|
||||
return cls(**yaml.safe_load(path.read_text()))
|
||||
data = yaml.safe_load(path.read_text())
|
||||
# Tolerate extra fields (but we'll lose them if instance is persisted)
|
||||
data = {k: v for k, v in data.items() if k in cls.__dataclass_fields__}
|
||||
delivery = cls(**data)
|
||||
delivery.id = id
|
||||
return delivery
|
||||
|
||||
@classmethod
|
||||
def all(cls):
|
||||
for path in cls.get_root().glob("*.yml"):
|
||||
yield Delivery.load(path.stem)
|
||||
def all(cls, is_archived=False):
|
||||
root = cls.get_root()
|
||||
if is_archived:
|
||||
root = root / "archive"
|
||||
for path in root.glob("*.yml"):
|
||||
id_ = str(path.relative_to(cls.get_root())).replace(".yml", "")
|
||||
yield Delivery.load(id_)
|
||||
|
||||
@classmethod
|
||||
def incoming(cls):
|
||||
|
@ -225,10 +245,32 @@ class Delivery(Base):
|
|||
def former(cls):
|
||||
return [d for d in cls.all() if not d.is_foreseen]
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
assert self.id, "Cannot operate on unsaved deliveries"
|
||||
return self.get_root() / f"{self.id}.yml"
|
||||
|
||||
def persist(self):
|
||||
with self.__lock__:
|
||||
path = self.get_root() / f"{self.id}.yml"
|
||||
path.write_text(self.dump())
|
||||
if not self.id:
|
||||
self.id = uuid.uuid4().hex
|
||||
self.path.write_text(self.dump())
|
||||
|
||||
def archive(self):
|
||||
if self.is_archived:
|
||||
raise ValueError("La livraison est déjà archivée")
|
||||
current = self.path
|
||||
self.id = f"archive/{self.id}"
|
||||
current.rename(self.path)
|
||||
|
||||
def unarchive(self):
|
||||
if not self.is_archived:
|
||||
raise ValueError(
|
||||
"Impossible de désarchiver une livraison qui n'est pas archivée"
|
||||
)
|
||||
current = self.path
|
||||
self.id = self.path.stem
|
||||
current.rename(self.path)
|
||||
|
||||
def product_wanted(self, product):
|
||||
total = 0
|
||||
|
|
|
@ -208,6 +208,13 @@ input[type=submit].primary {
|
|||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
button.primary:hover,
|
||||
a.button.primary:hover,
|
||||
input[type=submit].primary:hover {
|
||||
background-color: #fff;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
button.danger,
|
||||
a.button.danger,
|
||||
input[type=submit].danger {
|
||||
|
@ -215,6 +222,13 @@ input[type=submit].danger {
|
|||
border-color: #d9534f;
|
||||
}
|
||||
|
||||
button.danger:hover,
|
||||
a.button.danger:hover,
|
||||
input[type=submit].danger:hover {
|
||||
background-color: #d9534f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
/* Forms */
|
||||
|
||||
|
@ -445,7 +459,6 @@ hr {
|
|||
background: white;
|
||||
padding: 5px;
|
||||
z-index: 100;
|
||||
text-align: center;
|
||||
overflow-y: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
|
|
5
copanier/templates/archive.html
Normal file
5
copanier/templates/archive.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% block body %}
|
||||
<h2>Livraisons archivées</h2>
|
||||
{% include "includes/delivery_list.html" %}
|
||||
{% endblock body %}
|
|
@ -58,6 +58,7 @@
|
|||
</article>
|
||||
<hr>
|
||||
<ul class="toolbox">
|
||||
{% if not delivery.is_archived %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/bon-de-commande.xlsx"><i class="icon-ribbon"></i> Bon de commande</a>
|
||||
</li>
|
||||
|
@ -67,33 +68,40 @@
|
|||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/émargement" target="_blank"><i class="icon-document"></i> Liste d'émargement</a>
|
||||
</li>
|
||||
{% if request.user and request.user.is_staff %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/exporter/produits"><i class="icon-layers"></i> Télécharger les produits</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/edit"><i class="icon-adjustments"></i> Modifier la livraison</a>
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="import-command" %}
|
||||
{% include "includes/modal_import_command.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="add-command" %}
|
||||
{% include "includes/modal_add_command.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="copy-emails" %}
|
||||
{% include "includes/modal_copy_emails.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
{% if delivery.is_passed %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/solde"><i class="icon-wallet"></i> Gérer les soldes</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if request.user and request.user.is_staff %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/exporter/produits"><i class="icon-layers"></i> Télécharger les produits</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/edit"><i class="icon-adjustments"></i> Modifier la livraison</a>
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="import-command" %}
|
||||
{% include "includes/modal_import_command.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="add-command" %}
|
||||
{% include "includes/modal_add_command.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
<li>
|
||||
{% with unique_id="copy-emails" %}
|
||||
{% include "includes/modal_copy_emails.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
{% if delivery.is_passed %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/solde"><i class="icon-wallet"></i> Gérer les soldes</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if request.user and request.user.is_staff %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/désarchiver" class="button danger"><i class="icon-hazardous"></i> Désarchiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -45,32 +45,17 @@
|
|||
</form>
|
||||
<hr>
|
||||
{% if delivery %}
|
||||
<h3>Importer des produits</h3>
|
||||
<p>Formats pris en charge: xlsx, csv</p>
|
||||
<details>
|
||||
<summary>Détails des colonnes</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>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>
|
||||
</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">
|
||||
</form>
|
||||
<ul class="toolbox">
|
||||
{% if delivery.status == delivery.CLOSED %}
|
||||
<li>
|
||||
<a href="/livraison/{{ delivery.id }}/archiver" class="button danger"><i class="icon-hazardous"></i> Archiver</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
{% with unique_id="import-products" %}
|
||||
{% include "includes/modal_import_products.html" %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock body %}
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
{% with deliveries=former %}
|
||||
{% include "includes/delivery_list.html" %}
|
||||
{% endwith %}
|
||||
<a href="/archives">Voir les livraisons archivées</a>
|
||||
{% endblock body %}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<li><i class="icon-streetsign"></i> <strong>Lieu</strong> {{ delivery.where }}</li>
|
||||
<li><i class="icon-strategy"></i> <strong>Référent</strong> <a href="mailto:{{ delivery.contact }}">{{ delivery.contact }}</a></li>
|
||||
<li><i class="icon-clock"></i> <strong>Date de livraison</strong> <time datetime="{{ delivery.from_date }}">{{ delivery.from_date|date }} de {{ delivery.from_date|time }} à {{ delivery.to_date|time }}</time></li>
|
||||
<li><i class="icon-hourglass"></i> {% if delivery.status == delivery.OPEN %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before|date }}</time>{% elif delivery.status == delivery.ADJUSTMENT %}<strong>Ajustement en cours</strong>{% else %}<strong>Fermée</strong>{% endif %}</li>
|
||||
<li><i class="icon-hourglass"></i> {% if delivery.status == delivery.OPEN %}<strong>Date limite de commande</strong> <time datetime="{{ delivery.order_before.date() }}">{{ delivery.order_before|date }}</time>{% elif delivery.status == delivery.ADJUSTMENT %}<strong>Ajustement en cours</strong>{% 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 %}
|
||||
</ul>
|
||||
|
|
32
copanier/templates/includes/modal_import_products.html
Normal file
32
copanier/templates/includes/modal_import_products.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% extends "includes/modal.html" %}
|
||||
|
||||
{% block modal_label %}<i class="icon-upload"></i> Importer les produits{% endblock modal_label %}
|
||||
{% block modal_body %}
|
||||
<h3>Importer des produits</h3>
|
||||
<p>Formats pris en charge: xlsx, csv</p>
|
||||
<details>
|
||||
<summary>Détails des colonnes</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>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>
|
||||
</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">
|
||||
</form>
|
||||
{% endblock modal_body %}
|
|
@ -1,4 +1,4 @@
|
|||
{% if delivery.status != delivery.CLOSED %}
|
||||
{% if delivery.status == delivery.OPEN or delivery.status == delivery.ADJUSTMENT %}
|
||||
<a class="button" href="/livraison/{{ delivery.id }}/commander">
|
||||
{% if delivery.status == delivery.ADJUSTMENT %}
|
||||
Ajuster ma commande
|
||||
|
|
|
@ -20,7 +20,7 @@ def test_can_create_delivery():
|
|||
assert delivery.producer == "Andines"
|
||||
assert delivery.where == "Marché de la Briche"
|
||||
assert delivery.from_date.year == now().year
|
||||
assert delivery.id
|
||||
assert not delivery.id
|
||||
|
||||
|
||||
def test_wrong_datetime_raise_valueerror():
|
||||
|
@ -106,7 +106,12 @@ def test_order_has_adjustments():
|
|||
|
||||
|
||||
def test_can_persist_delivery(delivery):
|
||||
with pytest.raises(AssertionError):
|
||||
delivery.path
|
||||
assert not delivery.id
|
||||
delivery.persist()
|
||||
assert delivery.id
|
||||
assert delivery.path.exists()
|
||||
|
||||
|
||||
def test_can_load_delivery(delivery):
|
||||
|
@ -138,3 +143,23 @@ def test_productorder_quantity():
|
|||
assert choice.quantity == 5
|
||||
choice = ProductOrder(wanted=3, adjustment=-1)
|
||||
assert choice.quantity == 2
|
||||
|
||||
|
||||
def test_archive_delivery(delivery):
|
||||
delivery.persist()
|
||||
old_id = delivery.id
|
||||
old_path = delivery.path
|
||||
assert str(old_path).endswith(f"delivery/{delivery.id}.yml")
|
||||
assert old_path.exists()
|
||||
delivery.archive()
|
||||
assert delivery.is_archived
|
||||
assert delivery.id.startswith("archive/")
|
||||
new_path = delivery.path
|
||||
assert str(new_path).endswith(f"delivery/archive/{old_id}.yml")
|
||||
assert not old_path.exists()
|
||||
assert new_path.exists()
|
||||
delivery.unarchive()
|
||||
assert not delivery.id.startswith("archive/")
|
||||
assert old_path.exists()
|
||||
assert not new_path.exists()
|
||||
assert not delivery.is_archived
|
||||
|
|
Loading…
Reference in a new issue