| 4 |
4 |
# pdfgen.py : génération PDF à l'identité visuelle officielle Lou-Ka |
| 5 |
5 |
# - fiche_pdf(uid) : fiche de propriété complète — tout ce que montre la |
| 6 |
6 |
# fiche web : photos, prix + analyse Lou-Ka (juste valeur, histogramme), |
| 7 |
|
−# chips, description structurée, inclusions, registre des loyers, |
|
7 |
+# chips, description structurée, inclusions, coût réel mensuel, |
|
8 |
+# historique Lou-Ka (+ vies antérieures), passeport de l'immeuble, |
|
9 |
+# gestionnaire (fiche Google + avis synchronisés), registre des loyers, |
| 8 |
10 |
# historique TAL, inondation, qualité de l'air, coût d'électricité, |
| 9 |
|
−# commerces/transport, essence, KA Scores, quartier, à proximité, QR |
|
11 |
+# commerces/transport, essence, KA Scores, score hiver, quartier, |
|
12 |
+# à proximité, QR |
| 10 |
13 |
# - rapport_pdf() : rapport global du marché (totaux, régions, histogramme |
| 11 |
14 |
# des loyers, types, villes, gestionnaires) |
| 12 |
15 |
# Palette identique au site : blanc #ffffff, marine #0b1330, orange #ff6a00, |
| 241 |
244 |
return d |
| 242 |
245 |
|
| 243 |
246 |
|
|
247 |
+def _fmt_ts(ts, jour: bool = False) -> str: |
|
248 |
+ """Timestamp unix → « 6 août 2026 » (jour=True) ou « août 2026 ».""" |
|
249 |
+ if not ts: |
|
250 |
+ return "—" |
|
251 |
+ t = time.localtime(ts) |
|
252 |
+ mois = f"{_MOIS[t.tm_mon - 1]} {t.tm_year}" |
|
253 |
+ return f"{t.tm_mday} {mois}" if jour else mois |
|
254 |
+ |
|
255 |
+ |
| 244 |
256 |
_VERDICTS = { |
| 245 |
257 |
"sous": ("Sous le marché", GREEN_DEEP, LIME_SOFT, GREEN), |
| 246 |
258 |
"marche": ("Dans le marché", INK2, SURFACE, INK), |
| 320 |
332 |
com = _safe(com_mod.nearby, lat, lng) if geo else None |
| 321 |
333 |
hyd = _safe(hydro_mod.estimate, adresse=l.get("address") or "", |
| 322 |
334 |
uid=uid, lat=lat, lng=lng, solve=False) |
|
335 |
+ # dossier d'intelligence locative (mêmes modules que les blocs du site) |
|
336 |
+ from . import building as bld_mod |
|
337 |
+ from . import coutreel as cr_mod |
|
338 |
+ from . import history as hist_mod |
|
339 |
+ from . import hiver as hiv_mod |
|
340 |
+ from . import managers as mgr_mod |
|
341 |
+ from . import recycled as rec_mod |
|
342 |
+ cout = _safe(cr_mod.compute, uid) |
|
343 |
+ histo = _safe(hist_mod.timeline, uid) |
|
344 |
+ recyc = _safe(rec_mod.lookup, uid) |
|
345 |
+ imm = (_safe(bld_mod.fiche, l["building_key"]) |
|
346 |
+ if l.get("building_key") else None) |
|
347 |
+ hiv = _safe(hiv_mod.score, lat, lng) if geo else None |
|
348 |
+ gest = _safe(mgr_mod.fiche, l["source"]) |
| 323 |
349 |
|
| 324 |
350 |
buf = io.BytesIO() |
| 325 |
351 |
c = rl_canvas.Canvas(buf, pagesize=letter) |
| 568 |
594 |
c.drawString(M, y - 2, f"Historique de prix de l'annonce : {chn}") |
| 569 |
595 |
y -= 16 |
| 570 |
596 |
|
|
597 |
+ # --- Coût réel mensuel (loyer + frais, chaque poste étiqueté honnêtement) |
|
598 |
+ if cout and cout.get("loyer") is not None: |
|
599 |
+ lignes_cr = cout.get("lignes") or [] |
|
600 |
+ y = place(y, 58 + len(lignes_cr) * 12 + 46) |
|
601 |
+ y = s.titre_section(y, "Coût réel mensuel") |
|
602 |
+ stat_cr = {"included": ("INCLUS", GREEN), "observed": ("OBSERVÉ", INK), |
|
603 |
+ "estimated": ("ESTIMÉ", LIME), "unknown": ("INCONNU", INK3)} |
|
604 |
+ for li in lignes_cr: |
|
605 |
+ lab, col = stat_cr.get(li.get("statut"), stat_cr["unknown"]) |
|
606 |
+ c.setFont("Helvetica", 8.5) |
|
607 |
+ c.setFillColor(INK2) |
|
608 |
+ c.drawString(M, y, (li.get("poste") or "")[:46]) |
|
609 |
+ c.setFont("Courier-Bold", 6.5) |
|
610 |
+ c.setFillColor(col) |
|
611 |
+ c.drawString(M + 230, y, lab) |
|
612 |
+ m_ = li.get("montant") |
|
613 |
+ if m_ is not None and m_ > 0: |
|
614 |
+ mtx = _fmt_money(m_) |
|
615 |
+ elif m_ == 0 and li.get("statut") == "included": |
|
616 |
+ mtx = f"0{NBSP}$" |
|
617 |
+ else: |
|
618 |
+ mtx = "—" |
|
619 |
+ c.setFont("Helvetica-Bold", 8.5) |
|
620 |
+ c.setFillColor(INK) |
|
621 |
+ c.drawRightString(PAGE_W - M, y, mtx) |
|
622 |
+ y -= 12 |
|
623 |
+ if cout.get("total_estime") is not None: |
|
624 |
+ c.setStrokeColor(INK) |
|
625 |
+ c.setLineWidth(1.2) |
|
626 |
+ c.line(M, y + 8, PAGE_W - M, y + 8) |
|
627 |
+ c.setFont("Helvetica-Bold", 10) |
|
628 |
+ c.setFillColor(INK) |
|
629 |
+ c.drawString(M, y - 4, "Total estimé") |
|
630 |
+ tot = f"≈{NBSP}{_fmt_money(cout['total_estime'])} / mois" |
|
631 |
+ if cout.get("annuel_estime") is not None: |
|
632 |
+ tot += f" · ≈{NBSP}{_fmt_money(cout['annuel_estime'])} / an" |
|
633 |
+ c.drawRightString(PAGE_W - M, y - 4, tot) |
|
634 |
+ y -= 18 |
|
635 |
+ if cout.get("postes_inconnus"): |
|
636 |
+ y = s.paragraphe(M, y - 2, "Postes non chiffrables avec les données " |
|
637 |
+ "publiées : " |
|
638 |
+ + ", ".join(cout["postes_inconnus"]).lower() |
|
639 |
+ + " — le total réel peut être plus élevé.", |
|
640 |
+ PAGE_W - 2 * M, taille=7.5, |
|
641 |
+ couleur=HexColor("#8a5a12"), interligne=9.5, |
|
642 |
+ max_lignes=3) |
|
643 |
+ pi2_d = cout.get("pi2") |
|
644 |
+ if pi2_d and pi2_d.get("valeur") is not None: |
|
645 |
+ v = f"{pi2_d['valeur']:.2f}".replace(".", ",") |
|
646 |
+ suite = f"{v}{NBSP}$/pi²" |
|
647 |
+ if pi2_d.get("percentile_secteur") is not None: |
|
648 |
+ suite += (f" — moins cher que {100 - pi2_d['percentile_secteur']}" |
|
649 |
+ f"{NBSP}% des {pi2_d.get('n_secteur')} comparables du " |
|
650 |
+ "secteur (~2 km)") |
|
651 |
+ elif pi2_d.get("percentile_ville") is not None: |
|
652 |
+ suite += (f" — moins cher que {100 - pi2_d['percentile_ville']}" |
|
653 |
+ f"{NBSP}% des {pi2_d.get('n_ville')} comparables " |
|
654 |
+ f"({pi2_d.get('portee_ville', '')})") |
|
655 |
+ c.setFont("Helvetica", 8) |
|
656 |
+ c.setFillColor(INK2) |
|
657 |
+ c.drawString(M, y - 2, suite[:120]) |
|
658 |
+ y -= 12 |
|
659 |
+ y -= 8 |
|
660 |
+ |
|
661 |
+ # --- Historique Lou-Ka (observations réelles des synchronisations) |
|
662 |
+ if histo: |
|
663 |
+ tl = (histo.get("timeline") or [])[:8] |
|
664 |
+ matches = ((recyc or {}).get("matches") or [])[:2] |
|
665 |
+ y = place(y, 46 + len(tl) * 11 + len(matches) * 22 + 18) |
|
666 |
+ y = s.titre_section(y, "Historique Lou-Ka") |
|
667 |
+ entete_h = (f"Suivie depuis {_fmt_ts(histo.get('premiere_observation'), jour=True)}" |
|
668 |
+ f" · {histo.get('jours_en_ligne', 0)} jour" |
|
669 |
+ f"{'s' if (histo.get('jours_en_ligne') or 0) > 1 else ''} en ligne" |
|
670 |
+ f" · {histo.get('modifications', 0)} modification" |
|
671 |
+ f"{'s' if (histo.get('modifications') or 0) > 1 else ''} observée" |
|
672 |
+ f"{'s' if (histo.get('modifications') or 0) > 1 else ''}") |
|
673 |
+ pi_, pa_ = histo.get("prix_initial"), histo.get("prix_actuel") |
|
674 |
+ if pi_ is not None and pa_ is not None and pi_ != pa_: |
|
675 |
+ var_ = histo.get("variation") |
|
676 |
+ entete_h += f" · {_fmt_money(pi_)} → {_fmt_money(pa_)}" |
|
677 |
+ if var_ is not None: |
|
678 |
+ entete_h += (f" ({'+' if var_ > 0 else '−'}" |
|
679 |
+ f"{abs(round(var_ * 100))}{NBSP}%)") |
|
680 |
+ c.setFont("Helvetica-Bold", 8.5) |
|
681 |
+ c.setFillColor(INK) |
|
682 |
+ c.drawString(M, y - 2, entete_h[:120]) |
|
683 |
+ y -= 16 |
|
684 |
+ ev_labels = {"description": "Description modifiée", |
|
685 |
+ "superficie": "Superficie modifiée", |
|
686 |
+ "dispo": "Disponibilité modifiée", |
|
687 |
+ "inclusions": "Inclusions modifiées", |
|
688 |
+ "photos": "Photos modifiées", |
|
689 |
+ "disparition": "Annonce retirée", |
|
690 |
+ "reapparition": "Annonce republiée"} |
|
691 |
+ if tl: |
|
692 |
+ for it in tl: |
|
693 |
+ c.setFont("Courier-Bold", 7) |
|
694 |
+ c.setFillColor(INK3) |
|
695 |
+ c.drawString(M, y, _fmt_ts(it.get("ts"), jour=True).upper()) |
|
696 |
+ c.setFont("Helvetica", 8) |
|
697 |
+ c.setFillColor(INK2) |
|
698 |
+ if it.get("type") == "prix": |
|
699 |
+ if it.get("prix_avant") is not None: |
|
700 |
+ sens = ("baissé" if it["prix_avant"] > (it.get("prix") or 0) |
|
701 |
+ else "monté") |
|
702 |
+ txt_ev = (f"Prix {sens} de {_fmt_money(it['prix_avant'])} " |
|
703 |
+ f"à {_fmt_money(it.get('prix'))}") |
|
704 |
+ else: |
|
705 |
+ txt_ev = f"Premier prix observé : {_fmt_money(it.get('prix'))}" |
|
706 |
+ else: |
|
707 |
+ txt_ev = ev_labels.get(it.get("type"), it.get("type") or "") |
|
708 |
+ c.drawString(M + 78, y, txt_ev[:95]) |
|
709 |
+ y -= 11 |
|
710 |
+ else: |
|
711 |
+ c.setFont("Helvetica", 8) |
|
712 |
+ c.setFillColor(INK2) |
|
713 |
+ c.drawString(M, y, "Aucune modification observée depuis la première " |
|
714 |
+ "synchronisation de cette annonce.") |
|
715 |
+ y -= 11 |
|
716 |
+ for m_ in matches: |
|
717 |
+ suite = f"Vie antérieure probable ({m_.get('confiance')}{NBSP}% de confiance)" |
|
718 |
+ if m_.get("prix") is not None: |
|
719 |
+ suite += f" — affiché {_fmt_money(m_['prix'])}" |
|
720 |
+ if m_.get("derniere_observation"): |
|
721 |
+ suite += f" jusqu'en {_fmt_ts(m_['derniere_observation'])}" |
|
722 |
+ c.setFont("Helvetica-Bold", 8) |
|
723 |
+ c.setFillColor(HexColor("#8a5a12")) |
|
724 |
+ c.drawString(M, y - 2, suite[:115]) |
|
725 |
+ y -= 10 |
|
726 |
+ c.setFont("Helvetica", 7) |
|
727 |
+ c.setFillColor(INK3) |
|
728 |
+ c.drawString(M, y - 2, ("signaux : " + " · ".join(m_.get("signaux") or []))[:125]) |
|
729 |
+ y -= 12 |
|
730 |
+ y -= 8 |
|
731 |
+ |
|
732 |
+ # --- Passeport de l'immeuble (ce que Lou-Ka observe du bâtiment) |
|
733 |
+ if imm and (imm.get("annonces_total") or 0) >= 2: |
|
734 |
+ y = place(y, 128) |
|
735 |
+ y = s.titre_section(y, "Passeport de l'immeuble") |
|
736 |
+ tw = (PAGE_W - 2 * M - 30) / 4 |
|
737 |
+ rot = imm.get("rotation") or {} |
|
738 |
+ pres = imm.get("pression_loyers") |
|
739 |
+ tuiles_im = [ |
|
740 |
+ (str(imm["annonces_total"]), |
|
741 |
+ f"annonces obs. ({imm.get('annonces_actives', 0)} actives)"), |
|
742 |
+ (f"≥{NBSP}{imm.get('unites_estimees', 0)}", "unités estimées"), |
|
743 |
+ (_fmt_money(imm.get("loyer_median")), "loyer médian (actives)"), |
|
744 |
+ (f"{imm['pi2_median']:.2f}".replace(".", ",") + f"{NBSP}$" |
|
745 |
+ if imm.get("pi2_median") is not None else "—", "médiane $/pi²"), |
|
746 |
+ ] |
|
747 |
+ for i, (val, lab) in enumerate(tuiles_im): |
|
748 |
+ s.tuile(M + i * (tw + 10), y - 44, tw, 38, val, lab) |
|
749 |
+ y -= 58 |
|
750 |
+ par_cc = imm.get("loyer_median_par_cc") or {} |
|
751 |
+ if par_cc: |
|
752 |
+ parts = [f"{cc}{NBSP}ch. {_fmt_money(v)}" |
|
753 |
+ for cc, v in sorted(par_cc.items())] |
|
754 |
+ c.setFont("Helvetica", 8) |
|
755 |
+ c.setFillColor(INK2) |
|
756 |
+ c.drawString(M, y, ("Loyer médian par nombre de chambres : " |
|
757 |
+ + " · ".join(parts))[:118]) |
|
758 |
+ y -= 13 |
|
759 |
+ c.setFont("Helvetica-Bold", 8) |
|
760 |
+ c.setFillColor(INK) |
|
761 |
+ c.drawString(M, y, "Rotation des logements :") |
|
762 |
+ c.setFont("Helvetica", 8) |
|
763 |
+ c.setFillColor(INK2) |
|
764 |
+ if rot.get("statut") == "calculated": |
|
765 |
+ c.drawString(M + 118, y, |
|
766 |
+ (f"{rot.get('classe', '')} — {rot.get('annonces_12m')} " |
|
767 |
+ f"annonces sur 12 mois pour ≥{NBSP}" |
|
768 |
+ f"{rot.get('unites_estimees')} unités")[:100]) |
|
769 |
+ else: |
|
770 |
+ c.drawString(M + 118, y, "données insuffisantes") |
|
771 |
+ y -= 12 |
|
772 |
+ c.setFont("Helvetica-Bold", 8) |
|
773 |
+ c.setFillColor(INK) |
|
774 |
+ c.drawString(M, y, "Pression sur les loyers :") |
|
775 |
+ c.setFont("Helvetica", 8) |
|
776 |
+ c.setFillColor(INK2) |
|
777 |
+ if pres: |
|
778 |
+ v12 = pres.get("variation_12m") or 0 |
|
779 |
+ c.drawString(M + 118, y, |
|
780 |
+ (f"{'+' if v12 > 0 else '−'}{abs(round(v12 * 100))}" |
|
781 |
+ f"{NBSP}% sur 12 mois — médiane des prix d'entrée " |
|
782 |
+ f"{_fmt_money(pres.get('mediane_12_24m'))} → " |
|
783 |
+ f"{_fmt_money(pres.get('mediane_12m'))} " |
|
784 |
+ f"({pres.get('n_12_24m')} vs {pres.get('n_12m')} annonces)")[:110]) |
|
785 |
+ else: |
|
786 |
+ c.drawString(M + 118, y, "échantillon insuffisant") |
|
787 |
+ y -= 12 |
|
788 |
+ c.setFont("Helvetica", 6.5) |
|
789 |
+ c.setFillColor(INK3) |
|
790 |
+ c.drawString(M, y - 2, (f"Immeuble suivi par Lou-Ka depuis " |
|
791 |
+ f"{_fmt_ts(imm.get('premiere_observation'))} — " |
|
792 |
+ "indicateurs calculés uniquement sur les annonces " |
|
793 |
+ "observées (pas un recensement du bâtiment).")[:135]) |
|
794 |
+ y -= 16 |
|
795 |
+ |
|
796 |
+ # --- Qui gère ce logement? (fiche Google associée + avis synchronisés) |
|
797 |
+ g_ = (gest or {}).get("google_maps") |
|
798 |
+ if gest and g_ and g_.get("statut") != "non_associe": |
|
799 |
+ avis_g = gest.get("avis") or {} |
|
800 |
+ dist_g = avis_g.get("distribution") or {} |
|
801 |
+ tot_g = sum(dist_g.values()) if dist_g else 0 |
|
802 |
+ y = place(y, 108 + (5 * 11 if tot_g else 0)) |
|
803 |
+ y = s.titre_section(y, "Qui gère ce logement?") |
|
804 |
+ c.setFont("Helvetica-Bold", 10.5) |
|
805 |
+ c.setFillColor(INK) |
|
806 |
+ c.drawString(M, y - 2, (gest.get("nom") or "")[:60]) |
|
807 |
+ n_act = gest.get("annonces_actives") or 0 |
|
808 |
+ c.setFont("Helvetica", 8) |
|
809 |
+ c.setFillColor(INK2) |
|
810 |
+ c.drawString(M + c.stringWidth((gest.get("nom") or "")[:60], |
|
811 |
+ "Helvetica-Bold", 10.5) + 10, y - 2, |
|
812 |
+ f"{n_act} annonce{'s' if n_act > 1 else ''} active" |
|
813 |
+ f"{'s' if n_act > 1 else ''} sur Lou-Ka") |
|
814 |
+ y -= 16 |
|
815 |
+ if g_.get("note") is not None: |
|
816 |
+ note_t = f"{g_['note']:.1f}".replace(".", ",") |
|
817 |
+ s.pilule(M, y - 10, f"Note Google {note_t} / 5", LIME, INK) |
|
818 |
+ c.setFont("Helvetica", 8) |
|
819 |
+ c.setFillColor(INK2) |
|
820 |
+ c.drawString(M + 110, y - 5, |
|
821 |
+ (f"{g_.get('nombre_avis', 0)}{NBSP}avis Google — fiche " |
|
822 |
+ f"« {g_.get('nom', '')} »")[:90]) |
|
823 |
+ y -= 22 |
|
824 |
+ if tot_g: |
|
825 |
+ bw_g = 150.0 |
|
826 |
+ for n_ in (5, 4, 3, 2, 1): |
|
827 |
+ cnt = dist_g.get(str(n_), 0) |
|
828 |
+ c.setFont("Courier-Bold", 7) |
|
829 |
+ c.setFillColor(INK2) |
|
830 |
+ c.drawString(M, y, f"{n_}*") |
|
831 |
+ c.setFillColor(SURFACE) |
|
832 |
+ c.setStrokeColor(INK) |
|
833 |
+ c.setLineWidth(0.8) |
|
834 |
+ c.rect(M + 22, y - 1, bw_g, 7, stroke=1, fill=1) |
|
835 |
+ if cnt: |
|
836 |
+ c.setFillColor(GREEN if n_ >= 4 else (LIME if n_ <= 2 else INK3)) |
|
837 |
+ c.rect(M + 22, y - 1, bw_g * cnt / tot_g, 7, stroke=0, fill=1) |
|
838 |
+ c.setFont("Courier-Bold", 7) |
|
839 |
+ c.setFillColor(INK) |
|
840 |
+ c.drawString(M + 22 + bw_g + 6, y, str(cnt)) |
|
841 |
+ y -= 11 |
|
842 |
+ y -= 2 |
|
843 |
+ stats_g = [] |
|
844 |
+ if avis_g.get("moyenne_12m") is not None: |
|
845 |
+ stats_g.append(f"moyenne des 12 derniers mois : " |
|
846 |
+ f"{avis_g['moyenne_12m']:.1f}".replace(".", ",") |
|
847 |
+ + f" ({avis_g.get('n_12m')} avis)") |
|
848 |
+ if avis_g.get("tendance"): |
|
849 |
+ stats_g.append(avis_g["tendance"]) |
|
850 |
+ if avis_g.get("plaintes_frequentes"): |
|
851 |
+ stats_g.append("plaintes récurrentes : " |
|
852 |
+ + ", ".join(avis_g["plaintes_frequentes"][:4])) |
|
853 |
+ if stats_g: |
|
854 |
+ y = s.paragraphe(M, y, " · ".join(stats_g), PAGE_W - 2 * M, |
|
855 |
+ taille=8, couleur=INK2, interligne=10, |
|
856 |
+ max_lignes=2) |
|
857 |
+ y -= 2 |
|
858 |
+ conf_g = round((g_.get("confiance_association") or 0) * 100) |
|
859 |
+ y = s.paragraphe(M, y - 2, f"Fiche Google Maps associée automatiquement " |
|
860 |
+ f"(confiance {conf_g}{NBSP}%). Statistiques sur " |
|
861 |
+ f"les {avis_g.get('n', 0)} avis synchronisés dans " |
|
862 |
+ "la base Lou-Ka — échantillon des plus récents, " |
|
863 |
+ "pas une lecture humaine.", |
|
864 |
+ PAGE_W - 2 * M, taille=6.5, couleur=INK3, |
|
865 |
+ interligne=8.5, max_lignes=3) |
|
866 |
+ y -= 6 |
|
867 |
+ |
| 571 |
868 |
# --- Registre des loyers (déclarations citoyennes à proximité) |
| 572 |
869 |
if rdl_d and rdl_d.get("n"): |
| 573 |
870 |
comp = rdl_d.get("items") or [] |
| 815 |
1112 |
f"{lab} · {sub}" if sub else lab) |
| 816 |
1113 |
y -= 62 |
| 817 |
1114 |
|
|
1115 |
+ # --- Vie quotidienne en hiver (quotidien à pied, même à −20 °C) |
|
1116 |
+ if hiv: |
|
1117 |
+ det_h = hiv.get("detail") or [] |
|
1118 |
+ y = place(y, 52 + len(det_h) * 11 + 14) |
|
1119 |
+ y = s.titre_section(y, "Vie quotidienne en hiver") |
|
1120 |
+ c.setFont("Helvetica-Bold", 17) |
|
1121 |
+ c.setFillColor(INK) |
|
1122 |
+ sc_t = str(hiv.get("score", "—")) |
|
1123 |
+ c.drawString(M, y - 8, sc_t) |
|
1124 |
+ s.pilule(M + c.stringWidth(sc_t, "Helvetica-Bold", 17) + 10, y - 12, |
|
1125 |
+ (hiv.get("classe") or "").upper()) |
|
1126 |
+ c.setFont("Helvetica", 8) |
|
1127 |
+ c.setFillColor(INK3) |
|
1128 |
+ c.drawString(M + c.stringWidth(sc_t, "Helvetica-Bold", 17) + 105, y - 7, |
|
1129 |
+ "quotidien à pied, même à −20 °C") |
|
1130 |
+ y -= 26 |
|
1131 |
+ for cr_ in det_h: |
|
1132 |
+ c.setFont("Helvetica-Bold", 8) |
|
1133 |
+ c.setFillColor(INK) |
|
1134 |
+ c.drawString(M, y, (cr_.get("critere") or "")[:24]) |
|
1135 |
+ c.setFont("Helvetica", 8) |
|
1136 |
+ c.setFillColor(INK2) |
|
1137 |
+ if cr_.get("distance_m") is not None: |
|
1138 |
+ val_h = f"≈{NBSP}{cr_.get('minutes')}{NBSP}min à pied" |
|
1139 |
+ if cr_.get("nom"): |
|
1140 |
+ val_h += f" ({cr_['nom']})" |
|
1141 |
+ else: |
|
1142 |
+ val_h = cr_.get("note") or "—" |
|
1143 |
+ c.drawString(M + 90, y, val_h[:80]) |
|
1144 |
+ c.setFont("Courier-Bold", 7.5) |
|
1145 |
+ c.setFillColor(INK) |
|
1146 |
+ c.drawRightString(PAGE_W - M, y, str(cr_.get("score", "—"))) |
|
1147 |
+ y -= 11 |
|
1148 |
+ c.setFont("Helvetica", 6.5) |
|
1149 |
+ c.setFillColor(INK3) |
|
1150 |
+ c.drawString(M, y - 2, (hiv.get("methode") or "")[:135]) |
|
1151 |
+ y -= 16 |
|
1152 |
+ |
| 818 |
1153 |
# --- Le quartier |
| 819 |
1154 |
if quartier: |
| 820 |
1155 |
y = place(y, 270) |
| 821 |
1156 |
|