diff --git a/backend/app/routers/groups.py b/backend/app/routers/groups.py
index dddf363..abca975 100644
--- a/backend/app/routers/groups.py
+++ b/backend/app/routers/groups.py
@@ -6,6 +6,7 @@ from ..deps import get_current_user, require_admin
from ..models import Barcode, Group, Product, User
from ..schemas import (
BarcodeCreate,
+ BarcodeNoteUpdate,
BarcodeOut,
GroupCreate,
GroupOut,
@@ -171,6 +172,36 @@ def add_group_barcode(
return _group_to_out(db, group)
+@router.patch("/{group_id}/barcodes/{code}", response_model=GroupOut)
+def update_group_barcode_note(
+ group_id: int,
+ code: str,
+ payload: BarcodeNoteUpdate,
+ db: Session = Depends(get_db),
+ _: User = Depends(require_admin),
+) -> GroupOut:
+ """Notiz zu einem Gruppen-Code setzen.
+
+ Betrifft ausdrücklich auch Codes, die beim Zuordnen eines Artikels
+ automatisch entstanden sind – die hatten bisher gar keine Möglichkeit,
+ eine Notiz zu bekommen.
+ """
+ group = db.get(Group, group_id)
+ if group is None:
+ raise HTTPException(status.HTTP_404_NOT_FOUND, "Gruppe nicht gefunden")
+ entry = (
+ db.query(Barcode).filter(Barcode.group_id == group_id, Barcode.code == code).first()
+ )
+ if entry is None:
+ raise HTTPException(status.HTTP_404_NOT_FOUND, "Code nicht gefunden")
+
+ note = (payload.note or "").strip()
+ entry.note = note or None
+ db.commit()
+ db.refresh(group)
+ return _group_to_out(db, group)
+
+
@router.delete("/{group_id}/barcodes/{code}", status_code=status.HTTP_204_NO_CONTENT)
def delete_group_barcode(
group_id: int,
diff --git a/backend/app/routers/transfer.py b/backend/app/routers/transfer.py
index e7de3d8..82eb80a 100644
--- a/backend/app/routers/transfer.py
+++ b/backend/app/routers/transfer.py
@@ -47,6 +47,15 @@ PACKAGE_TOKENS = {"packung", "package", "pkg", "pack"}
# --------------------------------------------------------------------------
# Export
# --------------------------------------------------------------------------
+def _zeitstempel() -> str:
+ """Ortszeit als YYYY-MM-DD_HHMM für Dateinamen.
+
+ Ohne Zeitstempel heißen mehrere Ausleitungen alle gleich und der Browser
+ haengt (1), (2) an – dann weiß niemand mehr, welche die aktuelle ist.
+ """
+ return datetime.now().strftime("%Y-%m-%d_%H%M")
+
+
def _article_unit(product: Product) -> tuple[float, str]:
"""Faktor und Bezeichnung der Artikeleinheit (Gebinde, sonst Produkteinheit)."""
unit_name, unit_factor = display_unit_info(product)
@@ -104,7 +113,9 @@ def export_stock_csv(
return Response(
content="" + buf.getvalue(),
media_type="text/csv; charset=utf-8",
- headers={"Content-Disposition": 'attachment; filename="bestand.csv"'},
+ headers={
+ "Content-Disposition": f'attachment; filename="bestand_{_zeitstempel()}.csv"'
+ },
)
@@ -119,6 +130,7 @@ def export_backup_json(
data = {
"version": 1,
"exported_at": datetime.now(timezone.utc).isoformat(),
+ "exported_at_local": datetime.now().isoformat(timespec="seconds"),
"units": [
{"name": u.name, "kind": u.kind.value, "factor": u.factor}
for u in db.query(Unit).order_by(Unit.id).all()
@@ -171,7 +183,10 @@ def export_backup_json(
return Response(
content=json.dumps(data, ensure_ascii=False, indent=2),
media_type="application/json; charset=utf-8",
- headers={"Content-Disposition": 'attachment; filename="project-good-backup.json"'},
+ headers={
+ "Content-Disposition":
+ f'attachment; filename="project-good-backup_{_zeitstempel()}.json"'
+ },
)
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 1937548..885f689 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -244,6 +244,11 @@ class BarcodeCreate(BaseModel):
note: str | None = Field(default=None, max_length=120)
+class BarcodeNoteUpdate(BaseModel):
+ """Notiz zu einem Code nachtragen oder ändern."""
+ note: str | None = Field(default=None, max_length=120)
+
+
# ---- Stock movements ----
class CheckInRequest(BaseModel):
product_id: int | None = None
diff --git a/web/src/api.js b/web/src/api.js
index 33563ad..81433d3 100644
--- a/web/src/api.js
+++ b/web/src/api.js
@@ -72,6 +72,14 @@ async function request(path, { method = "GET", body, form, formData } = {}) {
return data;
}
+/** Ortszeit als YYYY-MM-DD_HHMM fuer Dateinamen. */
+function zeitstempel() {
+ const jetzt = new Date();
+ const zwei = (n) => String(n).padStart(2, "0");
+ return `${jetzt.getFullYear()}-${zwei(jetzt.getMonth() + 1)}-${zwei(jetzt.getDate())}`
+ + `_${zwei(jetzt.getHours())}${zwei(jetzt.getMinutes())}`;
+}
+
// Datei mit Auth-Header laden und im Browser als Download anbieten.
export async function downloadFile(path, filename) {
const token = getToken();
@@ -148,12 +156,16 @@ export const api = {
updateGroup: (id, body) => request(`/groups/${id}`, { method: "PATCH", body }),
deleteGroup: (id) => request(`/groups/${id}`, { method: "DELETE" }),
addGroupBarcode: (id, body) => request(`/groups/${id}/barcodes`, { method: "POST", body }),
+ updateGroupBarcodeNote: (id, code, body) =>
+ request(`/groups/${id}/barcodes/${encodeURIComponent(code)}`, { method: "PATCH", body }),
deleteGroupBarcode: (id, code) =>
request(`/groups/${id}/barcodes/${encodeURIComponent(code)}`, { method: "DELETE" }),
// Export / Import
- exportCsv: () => downloadFile("/export/stock.csv", "bestand.csv"),
- exportJson: () => downloadFile("/export/backup.json", "project-good-backup.json"),
+ // Zeitstempel im Dateinamen, sonst heissen mehrere Ausleitungen alle gleich.
+ exportCsv: () => downloadFile("/export/stock.csv", `bestand_${zeitstempel()}.csv`),
+ exportJson: () =>
+ downloadFile("/export/backup.json", `project-good-backup_${zeitstempel()}.json`),
importStock: (file, mode = "add") => {
const fd = new FormData();
fd.append("file", file);
diff --git a/web/src/components/BarcodeList.jsx b/web/src/components/BarcodeList.jsx
index bc26b4e..72a872c 100644
--- a/web/src/components/BarcodeList.jsx
+++ b/web/src/components/BarcodeList.jsx
@@ -12,7 +12,7 @@ import Icon from "./Icon";
* onAdd({ code, note }) und onDelete(code) werden vom Aufrufer bereitgestellt.
*/
export default function BarcodeList({
- barcodes = [], productBarcodes = [], onAdd, onDelete, disabled = false, hint,
+ barcodes = [], productBarcodes = [], onAdd, onDelete, onEditNote, disabled = false, hint,
}) {
const [code, setCode] = useState("");
const [note, setNote] = useState("");
@@ -45,9 +45,20 @@ export default function BarcodeList({
))}
{barcodes.map((b) => (
-
+
{b.code}
- {b.note && {b.note}}
+ {/* Notiz auch nachtraeglich aenderbar - automatisch angelegte Codes
+ hatten sonst nie eine Gelegenheit, eine zu bekommen. */}
+ {onEditNote && !disabled ? (
+ {
+ const wert = e.target.value.trim();
+ if (wert !== (b.note || "")) onEditNote(b.code, wert);
+ }} />
+ ) : (
+ b.note && {b.note}
+ )}
{!disabled && (