Backend: Beleg-Analyse um Kaufdatum + Shop; Analyse-Endpoint
- guess_acquired_on (Kaufdatum) und guess_shop (bekannter Shop erkannt -> id; sonst Kandidatenname zum Anlegen) ergaenzt. - Upload liefert zusaetzlich suggested_acquired_on / shop_id / shop_name. - Neuer POST /items/analyze-document: analysiert einen Beleg OHNE zu speichern (zum Vorbefuellen beim Anlegen). Gemeinsamer Helfer _doc_suggestions. - 6 neue Tests; Suite 150 gruen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,7 @@ from ..models import (
|
||||
User,
|
||||
)
|
||||
from ..schemas import (
|
||||
DocumentSuggestions,
|
||||
ItemCreate,
|
||||
ItemDocumentUploadOut,
|
||||
ItemOut,
|
||||
@@ -33,8 +34,10 @@ from ..schemas import (
|
||||
from ..services.items import generate_uid, item_to_out
|
||||
from ..services.warranty import (
|
||||
extract_pdf_text,
|
||||
guess_acquired_on,
|
||||
guess_price_candidates,
|
||||
guess_price_cents,
|
||||
guess_shop,
|
||||
guess_warranty_until,
|
||||
)
|
||||
|
||||
@@ -208,20 +211,8 @@ def _safe_filename(name: str | None) -> str:
|
||||
return cleaned[:255] or "beleg"
|
||||
|
||||
|
||||
@router.post(
|
||||
"/items/{item_id}/documents",
|
||||
response_model=ItemDocumentUploadOut,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def upload_item_document(
|
||||
item_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> ItemDocumentUploadOut:
|
||||
"""Beleg (PDF oder Bild) hochladen. Bei PDF wird ein Garantieende vorgeschlagen."""
|
||||
item = _item_or_404(db, item_id)
|
||||
data = await file.read()
|
||||
def _read_document(file: UploadFile, data: bytes) -> str:
|
||||
"""Validiert Größe/Typ eines hochgeladenen Belegs und gibt den Content-Type."""
|
||||
if not data:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "Leere Datei")
|
||||
if len(data) > DOC_MAX_BYTES:
|
||||
@@ -234,6 +225,43 @@ async def upload_item_document(
|
||||
raise HTTPException(
|
||||
status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, "Nur PDF oder Bild erlaubt."
|
||||
)
|
||||
return content_type
|
||||
|
||||
|
||||
def _doc_suggestions(
|
||||
db: Session, data: bytes, content_type: str, item_acquired_on=None
|
||||
) -> DocumentSuggestions:
|
||||
"""Garantie, Preis, Kaufdatum und Shop aus einem PDF schätzen (Bilder: leer)."""
|
||||
if content_type != "application/pdf":
|
||||
return DocumentSuggestions()
|
||||
text = extract_pdf_text(data)
|
||||
shop_id, shop_name = guess_shop(text, [(s.id, s.name) for s in db.query(Shop).all()])
|
||||
return DocumentSuggestions(
|
||||
suggested_warranty_until=guess_warranty_until(text, acquired_on=item_acquired_on),
|
||||
suggested_price_cents=guess_price_cents(text),
|
||||
suggested_price_candidates=guess_price_candidates(text),
|
||||
suggested_acquired_on=guess_acquired_on(text),
|
||||
suggested_shop_id=shop_id,
|
||||
suggested_shop_name=shop_name,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/items/{item_id}/documents",
|
||||
response_model=ItemDocumentUploadOut,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
async def upload_item_document(
|
||||
item_id: int,
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> ItemDocumentUploadOut:
|
||||
"""Beleg (PDF oder Bild) hochladen. Bei PDF werden Garantie, Preis, Kaufdatum
|
||||
und Shop vorgeschlagen."""
|
||||
item = _item_or_404(db, item_id)
|
||||
data = await file.read()
|
||||
content_type = _read_document(file, data)
|
||||
|
||||
doc = ItemDocument(
|
||||
item_id=item.id,
|
||||
@@ -245,26 +273,28 @@ async def upload_item_document(
|
||||
db.commit()
|
||||
db.refresh(doc)
|
||||
|
||||
# Garantieende und Preis nur aus PDFs schätzen (Bilder haben keine Textebene).
|
||||
warranty = price = None
|
||||
candidates: list[int] = []
|
||||
if content_type == "application/pdf":
|
||||
text = extract_pdf_text(data)
|
||||
warranty = guess_warranty_until(text, acquired_on=item.acquired_on)
|
||||
price = guess_price_cents(text)
|
||||
candidates = guess_price_candidates(text)
|
||||
|
||||
vorschlag = _doc_suggestions(db, data, content_type, item.acquired_on)
|
||||
return ItemDocumentUploadOut(
|
||||
id=doc.id,
|
||||
filename=doc.filename,
|
||||
content_type=doc.content_type,
|
||||
uploaded_at=doc.uploaded_at,
|
||||
suggested_warranty_until=warranty,
|
||||
suggested_price_cents=price,
|
||||
suggested_price_candidates=candidates,
|
||||
**vorschlag.model_dump(),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/items/analyze-document", response_model=DocumentSuggestions)
|
||||
async def analyze_item_document(
|
||||
file: UploadFile = File(...),
|
||||
db: Session = Depends(get_db),
|
||||
_: User = Depends(require_admin),
|
||||
) -> DocumentSuggestions:
|
||||
"""Beleg nur analysieren (nichts speichern) – zum Vorbefüllen beim Anlegen."""
|
||||
data = await file.read()
|
||||
content_type = _read_document(file, data)
|
||||
return _doc_suggestions(db, data, content_type)
|
||||
|
||||
|
||||
@router.get("/items/{item_id}/documents/{doc_id}")
|
||||
def get_item_document(
|
||||
item_id: int,
|
||||
|
||||
Reference in New Issue
Block a user