259 lines
10 KiB
Python
259 lines
10 KiB
Python
"""
|
|
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}\""},
|
|
)
|
|
|