Files
CSG-Picker-Shipper-Helper/app/label_history.py
T
2026-07-06 00:24:30 -05:00

140 lines
4.9 KiB
Python

"""Local label history store.
Since the Stamps SERA API has no list-labels endpoint, we persist every
created label to appdata/label_history.json at print time. The history
window reads from here rather than polling the API.
The void/refund and SCAN form actions DO hit the API, using the label_id
stored here.
"""
from __future__ import annotations
import json
import logging
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
log = logging.getLogger(__name__)
@dataclass
class LabelEntry:
label_id: str
tracking_number: str
date_printed: str # ISO 8601
recipient: str # name / first line of address block
service: str # display name, e.g. "Ground Advantage"
package_type: str
amount: float | None
notes: str # invoice numbers / free-text notes
address_block: str
voided: bool = False
shipment_status: str = ""
date_delivered: str | None = None
carrier: str = "" # "usps" | "fedex"
@property
def date_printed_display(self) -> str:
try:
return datetime.fromisoformat(self.date_printed).strftime("%m/%d/%Y")
except Exception:
return self.date_printed
@property
def date_delivered_display(self) -> str:
if not self.date_delivered:
return ""
try:
return datetime.fromisoformat(self.date_delivered).strftime("%m/%d/%Y")
except Exception:
return self.date_delivered
class LabelHistory:
"""Load / save / query the local label history JSON file."""
def __init__(self) -> None:
from .paths import appdata_dir
self._path: Path = appdata_dir() / "label_history.json"
self._entries: list[LabelEntry] = self._load()
# ── Persistence ────────────────────────────────────────────────────────
def _load(self) -> list[LabelEntry]:
try:
if self._path.exists():
raw = json.loads(self._path.read_text(encoding="utf-8"))
return [LabelEntry(**r) for r in raw]
except Exception as exc:
log.warning("Could not load label history: %s", exc)
return []
def _save(self) -> None:
try:
data = [asdict(e) for e in self._entries]
self._path.write_text(
json.dumps(data, indent=2, default=str),
encoding="utf-8",
)
except Exception as exc:
log.warning("Could not save label history: %s", exc)
# ── Mutation ───────────────────────────────────────────────────────────
def add(self, entry: LabelEntry) -> None:
self._entries.insert(0, entry)
self._save()
def mark_voided(self, label_id: str) -> None:
for e in self._entries:
if e.label_id == label_id:
e.voided = True
e.shipment_status = "Voided"
self._save()
def update_status(self, label_id: str, status: str,
date_delivered: str | None = None) -> None:
for e in self._entries:
if e.label_id == label_id:
e.shipment_status = status
if date_delivered:
e.date_delivered = date_delivered
self._save()
# ── Query ──────────────────────────────────────────────────────────────
def all(self) -> list[LabelEntry]:
return list(self._entries)
def filter(
self,
search: str = "",
status: str = "All",
days: int | None = None,
carrier: str = "",
) -> list[LabelEntry]:
results = self._entries
if carrier:
results = [e for e in results if e.carrier == carrier]
if days is not None:
from datetime import timedelta, timezone
cutoff = (datetime.now(timezone.utc) - timedelta(days=days)).isoformat()
results = [e for e in results if e.date_printed >= cutoff]
if status != "All":
if status == "Voided":
results = [e for e in results if e.voided]
else:
results = [e for e in results
if not e.voided and e.shipment_status == status]
if search:
q = search.lower()
results = [
e for e in results
if q in e.recipient.lower()
or q in e.notes.lower()
or q in e.tracking_number.lower()
]
return results