Initial commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user