"""Druckansicht als eigenständige HTML-Seite. Warum serverseitig, obwohl die App schon `@media print` mitbringt: Der Ausdruck soll auch ohne geöffnete App möglich sein - aus einem Lesezeichen, über einen öffentlichen Link, oder von einem Rechner, auf dem niemand die PWA installiert hat. Die Gliederung stammt aus `app.list_view`, also derselben Quelle wie die Bildschirmansicht; ein zweiter Sortieralgorithmus würde mit der Zeit abweichen. """ from datetime import datetime from jinja2 import Environment, select_autoescape from app.config import settings from app.schemas_shopping import ListView # autoescape ist hier keine Formsache: Artikelnamen und Notizen sind # freie Nutzereingaben und landen direkt im HTML. _env = Environment(autoescape=select_autoescape(default=True, default_for_string=True)) _TEMPLATE = _env.from_string(""" {{ view.list_name }} – {{ app_name }}

{{ view.list_name }}

Stand {{ printed_at }} {%- if open_count %} · {{ open_count }} offene Position{{ "en" if open_count != 1 }}{% endif %} {%- if not include_bought %} · gekaufte Artikel ausgeblendet{% endif %}
{% if not view.markets %}

Die Liste ist leer.

{% endif %} {% for market in view.markets %}

{{ market.market_name }} {% if market.total_cents %}{{ money(market.total_cents) }}{% endif %}

{% for category in market.categories %}

{{ category.category_name }}

{% for item in category.items %} {% endfor %}
{{ item.count }}× {{ item.article_name }} {% set detail = [] %} {%- if item.pack_size %}{% set _ = detail.append(pack(item)) %}{% endif %} {%- if item.variant %}{% set _ = detail.append(item.variant) %}{% endif %} {%- if item.note %}{% set _ = detail.append(item.note) %}{% endif %} {%- if detail %}{{ detail | join(" · ") }}{% endif %} {{ money(item.total_cents) if item.total_cents else "" }}
{% endfor %}
{% endfor %}

{{ app_name }} · erzeugt {{ printed_at }}

""") def _money(cents: int | None) -> str: value = (cents or 0) / 100 return f"{value:,.2f} €".replace(",", "\u00a0").replace(".", ",") def _pack(item) -> str: """Gebindegröße lesbar machen: 250 statt 250.000, 1,5 statt 1.500. Nicht über rstrip("0"): Bei einem Wert ohne Dezimalpunkt - und den liefert Decimal("250") - würde das die letzte Null abschneiden und aus 250 g plötzlich 25 g machen. `normalize()` entfernt nur tatsächlich überflüssige Nachkommastellen. """ value = item.pack_size text = format(value.normalize(), "f") if hasattr(value, "normalize") else str(value) return f"{text.replace('.', ',')} {item.pack_unit or ''}".strip() def render_print(view: ListView, *, include_bought: bool = True) -> str: open_count = sum(market.open_count for market in view.markets) return _TEMPLATE.render( view=view, app_name=settings.app_name, printed_at=datetime.now().strftime("%d.%m.%Y, %H:%M"), open_count=open_count, include_bought=include_bought, money=_money, pack=_pack, )