changes
This commit is contained in:
258
app/api/labels.py
Normal file
258
app/api/labels.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""
|
||||
Mailing Labels & Envelopes API
|
||||
|
||||
Endpoints:
|
||||
- POST /api/labels/rolodex/labels-5160
|
||||
- POST /api/labels/files/labels-5160
|
||||
- POST /api/labels/rolodex/envelopes
|
||||
- POST /api/labels/files/envelopes
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Sequence
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.orm import Session
|
||||
import io
|
||||
import csv
|
||||
|
||||
from app.auth.security import get_current_user
|
||||
from app.database.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.rolodex import Rolodex
|
||||
from app.services.customers_search import apply_customer_filters
|
||||
from app.services.mailing import (
|
||||
Address,
|
||||
build_addresses_from_files,
|
||||
build_addresses_from_rolodex,
|
||||
build_address_from_rolodex,
|
||||
render_labels_html,
|
||||
render_envelopes_html,
|
||||
save_html_bytes,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class Labels5160Request(BaseModel):
|
||||
ids: List[str] = Field(default_factory=list, description="Rolodex IDs or File numbers depending on route")
|
||||
start_position: int = Field(default=1, ge=1, le=30, description="Starting label position on sheet (1-30)")
|
||||
include_name: bool = Field(default=True, description="Include name/company as first line")
|
||||
|
||||
|
||||
class GenerateResult(BaseModel):
|
||||
url: Optional[str] = None
|
||||
storage_path: Optional[str] = None
|
||||
mime_type: str
|
||||
size: int
|
||||
created_at: str
|
||||
|
||||
|
||||
@router.post("/rolodex/labels-5160", response_model=GenerateResult)
|
||||
async def generate_rolodex_labels(
|
||||
payload: Labels5160Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if not payload.ids:
|
||||
raise HTTPException(status_code=400, detail="No rolodex IDs provided")
|
||||
addresses = build_addresses_from_rolodex(db, payload.ids)
|
||||
if not addresses:
|
||||
raise HTTPException(status_code=404, detail="No matching rolodex entries found")
|
||||
html_bytes = render_labels_html(addresses, start_position=payload.start_position, include_name=payload.include_name)
|
||||
result = save_html_bytes(html_bytes, filename_hint=f"labels_5160_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/labels")
|
||||
return GenerateResult(**result)
|
||||
|
||||
|
||||
@router.post("/files/labels-5160", response_model=GenerateResult)
|
||||
async def generate_file_labels(
|
||||
payload: Labels5160Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if not payload.ids:
|
||||
raise HTTPException(status_code=400, detail="No file numbers provided")
|
||||
addresses = build_addresses_from_files(db, payload.ids)
|
||||
if not addresses:
|
||||
raise HTTPException(status_code=404, detail="No matching file owners found")
|
||||
html_bytes = render_labels_html(addresses, start_position=payload.start_position, include_name=payload.include_name)
|
||||
result = save_html_bytes(html_bytes, filename_hint=f"labels_5160_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/labels")
|
||||
return GenerateResult(**result)
|
||||
|
||||
|
||||
class EnvelopesRequest(BaseModel):
|
||||
ids: List[str] = Field(default_factory=list, description="Rolodex IDs or File numbers depending on route")
|
||||
include_name: bool = Field(default=True)
|
||||
return_address_lines: Optional[List[str]] = Field(default=None, description="Lines for return address (top-left)")
|
||||
|
||||
|
||||
@router.post("/rolodex/envelopes", response_model=GenerateResult)
|
||||
async def generate_rolodex_envelopes(
|
||||
payload: EnvelopesRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if not payload.ids:
|
||||
raise HTTPException(status_code=400, detail="No rolodex IDs provided")
|
||||
addresses = build_addresses_from_rolodex(db, payload.ids)
|
||||
if not addresses:
|
||||
raise HTTPException(status_code=404, detail="No matching rolodex entries found")
|
||||
html_bytes = render_envelopes_html(addresses, return_address_lines=payload.return_address_lines, include_name=payload.include_name)
|
||||
result = save_html_bytes(html_bytes, filename_hint=f"envelopes_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/envelopes")
|
||||
return GenerateResult(**result)
|
||||
|
||||
|
||||
@router.post("/files/envelopes", response_model=GenerateResult)
|
||||
async def generate_file_envelopes(
|
||||
payload: EnvelopesRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
if not payload.ids:
|
||||
raise HTTPException(status_code=400, detail="No file numbers provided")
|
||||
addresses = build_addresses_from_files(db, payload.ids)
|
||||
if not addresses:
|
||||
raise HTTPException(status_code=404, detail="No matching file owners found")
|
||||
html_bytes = render_envelopes_html(addresses, return_address_lines=payload.return_address_lines, include_name=payload.include_name)
|
||||
result = save_html_bytes(html_bytes, filename_hint=f"envelopes_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}", subdir="mailing/envelopes")
|
||||
return GenerateResult(**result)
|
||||
|
||||
|
||||
@router.get("/rolodex/labels-5160/export")
|
||||
async def export_rolodex_labels_5160(
|
||||
start_position: int = Query(1, ge=1, le=30, description="Starting label position on sheet (1-30)"),
|
||||
include_name: bool = Query(True, description="Include name/company as first line"),
|
||||
group: Optional[str] = Query(None, description="Filter by customer group (exact match)"),
|
||||
groups: Optional[List[str]] = Query(None, description="Filter by multiple groups (repeat param)"),
|
||||
name_prefix: Optional[str] = Query(None, description="Prefix search across first/last name"),
|
||||
format: str = Query("html", description="Output format: html | csv"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Generate Avery 5160 labels for Rolodex entries selected by filters and stream as HTML or CSV."""
|
||||
fmt = (format or "").strip().lower()
|
||||
if fmt not in {"html", "csv"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid format. Use 'html' or 'csv'.")
|
||||
|
||||
q = db.query(Rolodex)
|
||||
q = apply_customer_filters(
|
||||
q,
|
||||
search=None,
|
||||
group=group,
|
||||
state=None,
|
||||
groups=groups,
|
||||
states=None,
|
||||
name_prefix=name_prefix,
|
||||
)
|
||||
entries = q.all()
|
||||
if not entries:
|
||||
raise HTTPException(status_code=404, detail="No matching rolodex entries found")
|
||||
|
||||
if fmt == "html":
|
||||
addresses = [build_address_from_rolodex(r) for r in entries]
|
||||
html_bytes = render_labels_html(addresses, start_position=start_position, include_name=include_name)
|
||||
from fastapi.responses import StreamingResponse
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"labels_5160_{ts}.html"
|
||||
return StreamingResponse(
|
||||
iter([html_bytes]),
|
||||
media_type="text/html",
|
||||
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""},
|
||||
)
|
||||
else:
|
||||
# CSV of address fields
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["Name", "Address 1", "Address 2", "Address 3", "City", "State", "ZIP"])
|
||||
for r in entries:
|
||||
addr = build_address_from_rolodex(r)
|
||||
writer.writerow([
|
||||
addr.display_name,
|
||||
r.a1 or "",
|
||||
r.a2 or "",
|
||||
r.a3 or "",
|
||||
r.city or "",
|
||||
r.abrev or "",
|
||||
r.zip or "",
|
||||
])
|
||||
output.seek(0)
|
||||
from fastapi.responses import StreamingResponse
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"labels_5160_{ts}.csv"
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/rolodex/envelopes/export")
|
||||
async def export_rolodex_envelopes(
|
||||
include_name: bool = Query(True, description="Include name/company"),
|
||||
return_address_lines: Optional[List[str]] = Query(None, description="Optional return address lines"),
|
||||
group: Optional[str] = Query(None, description="Filter by customer group (exact match)"),
|
||||
groups: Optional[List[str]] = Query(None, description="Filter by multiple groups (repeat param)"),
|
||||
name_prefix: Optional[str] = Query(None, description="Prefix search across first/last name"),
|
||||
format: str = Query("html", description="Output format: html | csv"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Generate envelopes for Rolodex entries selected by filters and stream as HTML or CSV of addresses."""
|
||||
fmt = (format or "").strip().lower()
|
||||
if fmt not in {"html", "csv"}:
|
||||
raise HTTPException(status_code=400, detail="Invalid format. Use 'html' or 'csv'.")
|
||||
|
||||
q = db.query(Rolodex)
|
||||
q = apply_customer_filters(
|
||||
q,
|
||||
search=None,
|
||||
group=group,
|
||||
state=None,
|
||||
groups=groups,
|
||||
states=None,
|
||||
name_prefix=name_prefix,
|
||||
)
|
||||
entries = q.all()
|
||||
if not entries:
|
||||
raise HTTPException(status_code=404, detail="No matching rolodex entries found")
|
||||
|
||||
if fmt == "html":
|
||||
addresses = [build_address_from_rolodex(r) for r in entries]
|
||||
html_bytes = render_envelopes_html(addresses, return_address_lines=return_address_lines, include_name=include_name)
|
||||
from fastapi.responses import StreamingResponse
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"envelopes_{ts}.html"
|
||||
return StreamingResponse(
|
||||
iter([html_bytes]),
|
||||
media_type="text/html",
|
||||
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""},
|
||||
)
|
||||
else:
|
||||
# CSV of address fields
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(["Name", "Address 1", "Address 2", "Address 3", "City", "State", "ZIP"])
|
||||
for r in entries:
|
||||
addr = build_address_from_rolodex(r)
|
||||
writer.writerow([
|
||||
addr.display_name,
|
||||
r.a1 or "",
|
||||
r.a2 or "",
|
||||
r.a3 or "",
|
||||
r.city or "",
|
||||
r.abrev or "",
|
||||
r.zip or "",
|
||||
])
|
||||
output.seek(0)
|
||||
from fastapi.responses import StreamingResponse
|
||||
ts = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"envelopes_{ts}.csv"
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": f"attachment; filename=\"{filename}\""},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user