144 lines
5.3 KiB
Python
144 lines
5.3 KiB
Python
"""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 "")
|