"""Generate a printable PRS dope card as PDF (A4 portrait).""" from fpdf import FPDF # Column widths in mm _W = { "num": 10, "name": 28, "dist": 20, "time": 18, "pos": 26, "dope_e": 30, "dope_w": 30, "hits": 24, "notes": 0, # fills remaining width } _ROW_H = 8 _HEAD_H = 9 _DARK = (26, 26, 46) # #1a1a2e _LIGHT = (240, 244, 255) # #f0f4ff _GRID = (200, 210, 230) def _notes_w(epw: float) -> float: fixed = sum(v for k, v in _W.items() if k != "notes") return max(0, epw - fixed) def generate_dope_card(session, stages: list) -> bytes: pdf = FPDF(orientation="P", unit="mm", format="A4") pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() pdf.set_margins(10, 12, 10) epw = pdf.w - pdf.l_margin - pdf.r_margin nw = _notes_w(epw) # ── Header ────────────────────────────────────────────────────────────── pdf.set_font("Helvetica", "B", 18) pdf.set_text_color(*_DARK) pdf.cell(0, 10, "FICHE DE TIR — PRS", new_x="LMARGIN", new_y="NEXT", align="C") pdf.set_font("Helvetica", "", 9) pdf.set_text_color(80, 80, 80) parts = [session.session_date.strftime("%d/%m/%Y")] if session.location_name: parts.append(session.location_name) if session.rifle: parts.append(session.rifle.name) if session.rifle.caliber: parts.append(session.rifle.caliber) if session.ammo_brand: parts.append(session.ammo_brand) if session.ammo_weight_gr: parts.append(f"{session.ammo_weight_gr} gr") pdf.cell(0, 5, " | ".join(parts), new_x="LMARGIN", new_y="NEXT", align="C") pdf.ln(4) # ── Column headers ─────────────────────────────────────────────────────── pdf.set_fill_color(*_DARK) pdf.set_text_color(255, 255, 255) pdf.set_font("Helvetica", "B", 8) headers = [ ("N°", _W["num"]), ("Nom", _W["name"]), ("Dist.(m)", _W["dist"]), ("Temps(s)", _W["time"]), ("Position", _W["pos"]), ("Dope Élév.", _W["dope_e"]), ("Dope Dérive", _W["dope_w"]), ("Coups/Poss.", _W["hits"]), ("Notes", nw), ] for label, w in headers: pdf.cell(w, _HEAD_H, label, border=0, fill=True, align="C") pdf.ln() # ── Stage rows ─────────────────────────────────────────────────────────── pdf.set_text_color(30, 30, 30) pdf.set_font("Helvetica", "", 9) for i, st in enumerate(stages): fill = i % 2 == 0 pdf.set_fill_color(*(_LIGHT if fill else (255, 255, 255))) pdf.set_draw_color(*_GRID) hits_str = "" if st.get("hits") is not None: hits_str = str(st["hits"]) if st.get("possible"): hits_str += f"/{st['possible']}" elif st.get("possible"): hits_str = f"—/{st['possible']}" row = [ (str(st.get("num", i + 1)), _W["num"], "C"), (st.get("name") or "", _W["name"], "L"), (str(st.get("distance_m") or ""), _W["dist"], "C"), (str(st.get("time_s") or ""), _W["time"], "C"), (_pos_label(st.get("position", "")), _W["pos"], "L"), (st.get("dope_elevation") or "", _W["dope_e"], "C"), (st.get("dope_windage") or "", _W["dope_w"], "C"), (hits_str, _W["hits"], "C"), (st.get("notes") or "", nw, "L"), ] for val, w, align in row: pdf.cell(w, _ROW_H, val, border="B", fill=fill, align=align) pdf.ln() # ── Blank rows for hand-written stages ────────────────────────────────── spare = max(0, 10 - len(stages)) for i in range(min(spare, 5)): fill = (len(stages) + i) % 2 == 0 pdf.set_fill_color(*(_LIGHT if fill else (255, 255, 255))) for _, w, _ in row: # reuse last row widths pdf.cell(w, _ROW_H, "", border="B", fill=fill) pdf.ln() # ── Footer ─────────────────────────────────────────────────────────────── pdf.ln(4) pdf.set_font("Helvetica", "I", 7) pdf.set_text_color(160, 160, 160) pdf.cell(0, 5, "The Shooter's Network — fiche générée automatiquement", new_x="LMARGIN", new_y="NEXT", align="C") return bytes(pdf.output()) _POSITION_LABELS = { "prone": "Couché", "standing": "Debout", "kneeling": "Agenouillé", "sitting": "Assis", "barricade": "Barricade", "rooftop": "Toit", "unknown": "Variable", } def _pos_label(slug: str) -> str: return _POSITION_LABELS.get(slug, slug.replace("_", " ").title() if slug else "")