SPB Git

spb/uqo_cours

Public

IMM1033 séance 7 : atelier quantity survey — cinq cas complets (plans, relevé, prix RSMeans 2026 T4, coûts), synthèse et exercice ; générateur + vérificateur de débordement

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Oct 28, 2026) parent b4ce24d Browse files

16 changed files +19,998 −0

modified IMM1033-20_Methodes_du_cout/02_Slides/README.md +1 −0
@@ -6,3 +6,4 @@
6 - `sources/seance02_exercices.tex` → `pdf/seance02_exercices.pdf` : **cahier d'exercices en classe de la séance 2** (deck Beamer séparé, même style, 46 p.) : 11 exercices en 5 blocs pour 60 minutes (énoncé / corrigé / discussion), distincts des exercices intégrés au deck principal et des exercices du chapitre 2 des notes. Compilation `latexmk -pdf`. Gabarit réutilisable pour les autres séances (macros `\exometa`, `\corrige`, `\discussion`). 6 - `sources/seance02_exercices.tex` → `pdf/seance02_exercices.pdf` : **cahier d'exercices en classe de la séance 2** (deck Beamer séparé, même style, 46 p.) : 11 exercices en 5 blocs pour 60 minutes (énoncé / corrigé / discussion), distincts des exercices intégrés au deck principal et des exercices du chapitre 2 des notes. Compilation `latexmk -pdf`. Gabarit réutilisable pour les autres séances (macros `\exometa`, `\corrige`, `\discussion`).
7 7
8 Les diapositives de présentation des TP (mandat, objectifs, livrables, aperçu du Dashboard) sont intégrées aux séances d'annonce; les anciens ateliers y sont conservés comme **exemples guidés** travaillés en classe. 8 Les diapositives de présentation des TP (mandat, objectifs, livrables, aperçu du Dashboard) sont intégrées aux séances d'annonce; les anciens ateliers y sont conservés comme **exemples guidés** travaillés en classe.
9+- **Séance 7 — atelier « quantity survey » (2026-10-28)** : le deck `seance07_couts_directs` inclut `sources/parties/seance07_qs_cas.tex`, **généré** par `sources/gen_qs_cas.py` à partir des cas d'étude du module Quantity Survey d'UQO Éval (`sources/images/qs-cas/casN/data.json` = révision figée, mesures, fiche ; plans PNG exportés ; `localisation.json` = mêmes lignes RSMeans dans 18 villes). Cinq cas complets (garage Trois-Rivières, bungalow Gatineau, cottage + garage Montréal, triplex Québec, chalet sur pieux Rimouski) : fiche, plans par niveau, relevé des quantités avec formules, prix unitaires RSMeans 2026 T4, divisions, majorations, lecture ; synthèse des huit cas et exercice (duplex Laval). 141 diapositives / 153 pages. Ne pas éditer la partie à la main : régénérer (`python3 gen_qs_cas.py`), compiler, puis vérifier `python3 check_overflow.py pdf/seance07_couts_directs.pdf` (0 débordement attendu). Compilation : `latexmk -pdf -bibtex- seance07_couts_directs.tex` avec le `.bbl` fourni (le biber universel de TeX Live 2026 échoue : `~/bin/biber-arm64`).
modified IMM1033-20_Methodes_du_cout/02_Slides/pdf/seance07_couts_directs.pdf +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/check_overflow.py +20 −0
@@ -0,0 +1,20 @@
1+#!/usr/bin/env python3
2+# Vérifie qu'aucune diapo d'un PDF Beamer ne déborde sur le pied de page : pour chaque page, le bas des blocs de texte et des dessins
3+# (hors pied de page) doit rester au-dessus de la ligne du pied. Usage : python3 check_overflow.py deck.pdf [page_debut]
4+import sys, fitz
5+pdf = sys.argv[1]; start = int(sys.argv[2]) if len(sys.argv) > 2 else 1
6+d = fitz.open(pdf)
7+bad = []
8+for i in range(start - 1, len(d)):
9+ p = d[i]; H = p.rect.height
10+ first = p.get_text().strip().split("\n")[0] if p.get_text().strip() else ""
11+ if first.isdigit() and len(first) <= 2: continue # page de section (gros numéro)
12+ foot = [b for b in p.get_text("blocks") if b[1] > H * 0.9 and ("Boucher" in b[4] or "/" in b[4])]
13+ footer_y = min(b[1] for b in foot) if foot else H * 0.955
14+ limit = footer_y - 1.5
15+ over = [b for b in p.get_text("blocks") if b[3] > limit + 2 and b[1] < footer_y - 4 and b not in foot and b[4].strip()]
16+ if over:
17+ bad.append((i + 1, round(max(b[3] for b in over) - footer_y, 1), p.get_text().split("\n")[0][:70]))
18+for b in bad: print(f"page {b[0]:3d} +{b[1]:5.1f} pt {b[2]}")
19+print(f"{len(bad)} page(s) en débordement sur {len(d) - start + 1}")
20+sys.exit(1 if bad else 0)
added IMM1033-20_Methodes_du_cout/02_Slides/sources/gen_qs_cas.py +374 −0
@@ -0,0 +1,374 @@
1+#!/usr/bin/env python3
2+# IMM1033 — séance 7 : génère parties/seance07_qs_cas.tex (atelier « quantity survey » : cas complets plans → quantités → coûts RSMeans)
3+# à partir des exports UQO Éval (images/qs-cas/casN/data.json + plans PNG, localisation.json). Toutes les valeurs viennent des révisions
4+# figées de la plateforme (RSMeans 2026 T4) — rien n'est inventé. Budget « zéro débordement » : vérifier avec check_overflow.py.
5+import json, os, re
6+
7+ROOT = os.path.dirname(os.path.abspath(__file__))
8+IMG = os.path.join(ROOT, "images", "qs-cas")
9+OUT = os.path.join(ROOT, "parties", "seance07_qs_cas.tex")
10+CASES = [7, 1, 2, 3, 6] # ordre pédagogique : du plus simple au plus particulier
11+ALL = [1, 2, 3, 4, 5, 6, 7, 8]
12+DIV = {"01": "Exigences générales", "03": "Béton", "04": "Maçonnerie", "05": "Métaux", "06": "Bois et composites", "07": "Enveloppe et toiture", "08": "Ouvertures", "09": "Finitions", "11": "Cuisine", "12": "Ameublement", "22": "Plomberie", "23": "CVCA", "26": "Électricité", "31": "Terrassement", "32": "Aménagements extérieurs", "33": "Services publics"}
13+UNIT = {"m": "m", "m2": "m\\textsuperscript{2}", "m3": "m\\textsuperscript{3}", "ft": "pi", "ft2": "pi\\textsuperscript{2}", "ft3": "pi\\textsuperscript{3}", "yd3": "vg\\textsuperscript{3}", "unit": "unité", "lump": "forfait", "h": "h", "in": "po"}
14+SPEC_FR = {"poured_concrete": "béton coulé", "concrete_block": "blocs de béton", "slab_on_grade": "dalle sur sol", "piers": "pieux", "icf": "coffrage isolant", "unfinished": "non fini", "finished": "fini", "none": "aucun", "crawl": "vide sanitaire", "asphalt_shingle": "bardeaux d'asphalte", "membrane": "membrane", "metal": "tôle", "gable": "deux versants", "hip": "quatre versants", "flat": "plat", "pvc": "PVC", "wood": "bois", "hybrid": "hybride", "aluminum": "aluminium", "electric_baseboard": "plinthes électriques", "heat_pump": "thermopompe centrale", "furnace_electric": "fournaise électrique", "furnace_gas": "fournaise au gaz", "standard": "standard", "economy": "économique", "superior": "supérieure", "prestige": "prestige", "vinyl": "vinyle", "brick": "brique", "fiber_cement": "fibrociment", "stone": "pierre", "steel": "acier", "hardwood": "bois franc", "ceramic": "céramique", "vinyl_plank": "vinyle", "laminate": "stratifié", "engineered": "ingénierie", "carpet": "tapis", "detached": "détaché", "attached": "attenant", "integrated": "intégré", "asphalt": "asphalte", "gravel": "gravier", "pavers": "pavés"}
15+
16+def tex(s):
17+ if s is None: return ""
18+ s = str(s)
19+ rep = [("\\", "\\textbackslash{}"), ("&", "\\&"), ("%", "\\%"), ("$", "\\$"), ("#", "\\#"), ("_", "\\_"), ("~", "\\textasciitilde{}"), ("^", "\\^{}"), ("²", "\\textsuperscript{2}"), ("³", "\\textsuperscript{3}"), ("½", "1/2"), ("¼", "1/4"), ("¾", "3/4"), ("⅝", "5/8"), ("⅜", "3/8"), ("×", "$\\times$"), ("—", "---"), ("–", "--"), ("·", "$\\cdot$"), ("≈", "$\\approx$"), ("≥", "$\\geq$"), ("≤", "$\\leq$"), ("°", "$^{\\circ}$"), ("→", "$\\rightarrow$"), ("\u00a0", "~"), ("″", "''"), ("“", "``"), ("”", "''"), ("’", "'"), ("Σ", "$\\Sigma$"), ("÷", "$\\div$"), ("−", "--"), ("ƒ", "f")]
20+ for a, b in rep: s = s.replace(a, b)
21+ return s.replace('"', "''")
22+
23+def num(v, d=0):
24+ if v is None: return "---"
25+ return f"{v:,.{d}f}".replace(",", "~").replace(".", ",")
26+def money(v, d=0): return num(v, d) + "~\\$"
27+def unit(u): return UNIT.get(u, tex(u))
28+def fr(key): return SPEC_FR.get(key, tex(key))
29+def mix(d):
30+ if not d: return "---"
31+ return ", ".join(f"{fr(k)} {round(v*100)}\\,\\%" for k, v in sorted(d.items(), key=lambda kv: -kv[1]) if v > 0)
32+def short_name(p): return re.sub(r" \(RSMeans.*\)$", "", p["name"])
33+def cut(s, n): return s if len(s) <= n else s[:n-1].rstrip() + "…"
34+
35+def load(n):
36+ d = json.load(open(os.path.join(IMG, f"cas{n}", "data.json")))
37+ d["n"] = n; d["dir"] = f"images/qs-cas/cas{n}"
38+ r = d["version"]["result"]; lines = [l for l in r["lines"] if not l["excluded"]]
39+ by_meas = {l["measurementId"]: l for l in lines if l.get("measurementId")}
40+ meas = []
41+ for m in d["measurements"]:
42+ if not m["name"].startswith("QS · "): continue
43+ note = (m.get("inputs") or {}).get("note", "") or ""
44+ formula, inputs = note, ""
45+ if " — " in note:
46+ parts = note.split(" — "); formula = parts[0]; inputs = parts[1] if len(parts) > 1 and "=" in parts[1] else ""
47+ l = by_meas.get(m["id"]); wbs = l["wbs"] if l else "00"
48+ meas.append({"name": m["name"][5:], "value": m["value"], "unit": m["unit"], "formula": formula.replace(" (metre-qs-1.0)", "").strip(), "inputs": inputs.replace(" (metre-qs-1.0)", "").strip(), "wbs": wbs})
49+ d["qs"] = meas; d["lines"] = lines
50+ emp = None
51+ for m in meas:
52+ mm = re.search(r"empreinte_m2=([\d.]+)", m["inputs"])
53+ if mm: emp = float(mm.group(1)); break
54+ heated = next((m["value"] for m in meas if m["name"].startswith("Filage brut")), None)
55+ if heated is None: heated = next((m["value"] for m in meas if m["name"].startswith("Gypse") and "plafond" in m["name"]), None)
56+ d["footprint"] = emp; d["heated"] = heated
57+ return d
58+
59+def kpi(color, value, label): return f"\\qskpi[{color}]{{{value}}}{{{label}}}"
60+
61+def table(rows, cols, align, size="\\scriptsize", stretch="0.95"):
62+ out = [f"\\begin{{center}}{size}\\renewcommand{{\\arraystretch}}{{{stretch}}}%", f"\\begin{{tabular}}{{@{{}}{align}@{{}}}}", "\\toprule", " & ".join(f"\\textbf{{{c}}}" for c in cols) + " \\\\", "\\midrule"]
63+ out += [" & ".join(r) + " \\\\" for r in rows]
64+ out += ["\\bottomrule", "\\end{tabular}\\end{center}"]
65+ return "\n".join(out)
66+
67+def frame_fiche(d):
68+ p = d["project"]; s = p["spec"]; t = d["version"]["result"]["totals"]; short = short_name(p)
69+ levels = ", ".join(x["level"] or x["label"] for x in d["pages"]); units = s.get("units", 1)
70+ bs = fr(s.get("basement")) + (f" ({round(s.get('basementFinishedPct', 0)*100)}\\,\\% fini)" if s.get("basement") == "finished" else "")
71+ out = [f"\\begin{{frame}}{{{tex(short)} --- fiche du cas}}", "\\begin{center}"]
72+ out.append(kpi("uqoBleu", num(d["footprint"]) + "~m\\textsuperscript{2}", "empreinte") + kpi("uqoTeal", num(d["heated"]) + "~m\\textsuperscript{2}", "aire chauffée") + kpi("uqoOr", f"{len(d['pages'])} / {units}", "niveaux / logements") + kpi("uqoNavy", money(t["total"]), "total hors taxes"))
73+ out.append("\\end{center}\\vspace{0.5mm}\\footnotesize")
74+ out.append("\\begin{columns}[T]\\begin{column}{0.48\\textwidth}\\uqotitle{Le plan donne}\\begin{itemize}\\setlength{\\itemsep}{1pt}")
75+ out.append(f" \\item Niveaux : {tex(levels)} (50~px/m, calibration exacte)")
76+ out.append(f" \\item {sum(len(x['rooms']) for x in d['pages'])} pièces nommées, {sum(x['windows'] for x in d['pages'])} fenêtres, {sum(x['doors'] for x in d['pages'])} portes")
77+ out.append(f" \\item Ville RSMeans : \\textbf{{{tex(p['municipality'])}}} (MO syndiquée)")
78+ out.append(f" \\item Secteur : {tex(cut(p['address'], 40))}")
79+ out.append("\\end{itemize}\\end{column}\\begin{column}{0.5\\textwidth}\\uqotitle{La fiche technique fixe}\\begin{itemize}\\setlength{\\itemsep}{1pt}")
80+ out.append(f" \\item Fondation {fr(s.get('foundation'))}, mur {num(s.get('foundationWallHeightM'), 2)}~m ; sous-sol {bs}")
81+ out.append(f" \\item Revêtements : {mix(s.get('siding'))} ; toit {fr(s.get('roofType'))} {s.get('roofPitch')}/12")
82+ out.append(f" \\item Fenêtres {fr(s.get('windows'))} ; chauffage {fr(s.get('heating'))}" + (" + clim." if s.get("hasAirConditioning") else "") + (" + VRC" if s.get("hasAirExchanger") else ""))
83+ out.append(f" \\item Cuisine et bains {fr(s.get('kitchenQuality'))} ; planchers : {mix(s.get('flooring'))}")
84+ out.append(f" \\item Garage : {fr(s.get('garage'))}" + (f" ; terrasse {num(s.get('deckM2'))}~m\\textsuperscript{{2}}" if s.get("deckM2") else "") + (f" ; entrée {fr(s.get('driveway'))}" if s.get("drivewayM2") else ""))
85+ out.append("\\end{itemize}\\end{column}\\end{columns}\\end{frame}\n")
86+ return "\n".join(out)
87+
88+def frame_plans(d):
89+ short = short_name(d["project"]); out = []
90+ for x in d["pages"]:
91+ lvl = x["level"] or x["label"]; rooms = x["rooms"]
92+ out.append(f"\\begin{{frame}}{{{tex(short)} --- plan : {tex(lvl)}}}")
93+ out.append("\\begin{columns}[T]\\begin{column}{0.6\\textwidth}\\begin{center}")
94+ out.append(f"\\includegraphics[height=0.68\\textheight,width=\\textwidth,keepaspectratio]{{{d['dir']}/{x['png']}}}")
95+ out.append("\\end{center}\\end{column}\\begin{column}{0.38\\textwidth}\\footnotesize\\uqotitle{Lecture du plan}\\begin{itemize}\\setlength{\\itemsep}{1pt}")
96+ out.append(f" \\item {x['pxPerMeter']}~px/m, {x['widthPx']}$\\times${x['heightPx']}~px ; incertitude de calibration : 0")
97+ out.append(f" \\item {len(rooms)} pièce(s), {x['windows']} fenêtre(s), {x['doors']} porte(s)")
98+ if rooms:
99+ out.append(" \\item " + cut("; ".join(tex(r["name"]) for r in rooms), 110))
100+ out.append(" \\item Murs ext. 0,30~m, cloisons 0,12~m, ouvertures : objets mesurables (longueur, aires brute et nette, comptage)")
101+ out.append("\\end{itemize}\\end{column}\\end{columns}\\end{frame}\n")
102+ return "\n".join(out)
103+
104+def frames_releve(d):
105+ short = short_name(d["project"]); out = []
106+ groups = [("site, fondations, structure", lambda w: w in ("31", "33", "03", "05", "06")), ("enveloppe, toiture, ouvertures", lambda w: w in ("07", "08")), ("finitions, mécanique, extérieur", lambda w: w in ("09", "11", "12", "22", "23", "26", "32"))]
107+ for title, pred in groups:
108+ rows = [m for m in d["qs"] if pred(m["wbs"])]
109+ if not rows: continue
110+ nch = -(-len(rows) // 11); size = -(-len(rows) // nch); chunks = [rows[i:i + size] for i in range(0, len(rows), size)]
111+ for ci, ch in enumerate(chunks):
112+ suffix = f" ({ci+1}/{len(chunks)})" if len(chunks) > 1 else ""
113+ out.append(f"\\begin{{frame}}{{{tex(short)} --- relevé : {tex(title)}{suffix}}}")
114+ trows = [[m["wbs"], cut(tex(m["name"]), 46), num(m["value"], 2 if m["value"] < 100 else 0), unit(m["unit"]), cut(tex(m["formula"].replace("Σ ", "somme des ")), 58)] for m in ch]
115+ out.append(table(trows, ["Div.", "Mesure du registre (QS)", "Quantité", "Unité", "Formule (plan ou fiche)"], "l l r l l", "\\scriptsize", "0.95"))
116+ if ci == len(chunks) - 1:
117+ out.append("{\\tiny\\textcolor{uqoGris}{Chaque mesure garde sa formule, ses entrées et ses pages sources ; « fiche » = fiche technique, sinon objets du plan.}}")
118+ out.append("\\end{frame}\n")
119+ return "\n".join(out)
120+
121+def frame_prix(d):
122+ short = short_name(d["project"])
123+ lines = sorted([l for l in d["lines"] if l["basis"] == "rsmeans"], key=lambda l: -l["located"])[:11]
124+ rows = []
125+ for l in lines:
126+ m = re.match(r"^(.*?) — (.*?) — RSMeans (\d{12})$", l["description"])
127+ fam, ln = (m.group(1), m.group(3)) if m else (l["description"], "")
128+ rows.append([cut(tex(fam), 28), f"\\texttt{{{ln}}}", num(l["quantity"], 1 if l["quantity"] < 100 else 0), unit(l["unit"]), money(l["unitMaterial"], 2), money(l["unitLabour"], 2), money(l["unitEquipment"], 2) if l["unitEquipment"] else "---", money(l["located"])])
129+ out = [f"\\begin{{frame}}{{{tex(short)} --- prix unitaires RSMeans (11 lignes les plus lourdes)}}"]
130+ out.append(table(rows, ["Famille", "Ligne RSMeans", "Qté", "Unité", "Matériau", "M.-d'\\oe uvre", "Équip.", "Montant"], "l l r l r r r r", "\\scriptsize", "0.95"))
131+ loc = d["version"]["result"]["lines"][0]["provenance"]["source"]
132+ out.append(f"{{\\tiny\\textcolor{{uqoGris}}{{Coûts unitaires bare (matériau + MO + équipement, sans O\\&P), {tex(loc)} ; quantités converties par le moteur (m\\textsuperscript{{2}} $\\rightarrow$ pi\\textsuperscript{{2}}, m\\textsuperscript{{3}} $\\rightarrow$ vg\\textsuperscript{{3}}).}}}}\\end{{frame}}\n")
133+ return "\n".join(out)
134+
135+def frame_divisions(d):
136+ short = short_name(d["project"]); r = d["version"]["result"]
137+ by = sorted(r["byWbs"], key=lambda w: w["wbs"])
138+ rows = [[f"{w['wbs']} --- {DIV.get(w['wbs'], '')}", money(w["located"]), f"{num(w['sharePct'],1)}\\,\\%"] for w in by]
139+ rows.append(["\\textbf{Total direct}", f"\\textbf{{{money(r['totals']['located'])}}}", "\\textbf{100\\,\\%}"])
140+ top = sorted(by, key=lambda w: -w["located"])[:3]
141+ mat, lab, eq = r["totals"]["material"], r["totals"]["labour"], r["totals"]["equipment"]; tot = mat + lab + eq
142+ out = [f"\\begin{{frame}}{{{tex(short)} --- coûts directs par division}}", "\\begin{columns}[T]\\begin{column}{0.55\\textwidth}"]
143+ out.append(table(rows, ["Division MasterFormat", "Direct", "Part"], "l r r", "\\scriptsize", "0.9"))
144+ out.append("\\end{column}\\begin{column}{0.43\\textwidth}\\footnotesize")
145+ out.append("\\begin{notebox}[Les trois divisions lourdes]" + " ; ".join(f"\\textbf{{{w['wbs']}}} {DIV.get(w['wbs'],'')} ({num(w['sharePct'],1)}\\,\\%)" for w in top) + ".\\end{notebox}")
146+ out.append(f"\\begin{{exbox}}[Nature des coûts]Matériaux {num(100*mat/tot,0)}\\,\\%, main-d'\\oe uvre {num(100*lab/tot,0)}\\,\\%, équipement {num(100*eq/tot,0)}\\,\\% du direct.\\end{{exbox}}")
147+ out.append("\\end{column}\\end{columns}\\end{frame}\n")
148+ return "\n".join(out)
149+
150+def frame_total(d):
151+ short = short_name(d["project"]); r = d["version"]["result"]; t = r["totals"]; pa = r["params"]
152+ rows = [["Direct (quantités $\\times$ coûts unitaires bare)", money(t["direct"])], ["Ajustement régional du module (RSMeans déjà localisé)", money(t["locationAdjustment"])], ["= Direct localisé", f"\\textbf{{{money(t['located'])}}}"], [f"Conditions générales {pa['generalConditionsPct']}\\,\\% du direct", money(t["generalConditions"])], [f"Frais généraux {pa['overheadPct']}\\,\\% du cumul", money(t["overhead"])], [f"Profit {pa['profitPct']}\\,\\% du cumul", money(t["profit"])], [f"Contingence {pa['contingencyPct']}\\,\\% du direct", money(t["contingency"])], ["\\textbf{Total hors taxes (coût à neuf 2026 T4)}", f"\\textbf{{{money(t['total'])}}}"]]
153+ heated = d["heated"] or 1
154+ out = [f"\\begin{{frame}}{{{tex(short)} --- du coût direct au coût à neuf}}", "\\begin{columns}[T]\\begin{column}{0.58\\textwidth}"]
155+ out.append(table(rows, ["Étape (ordre d'application)", "Montant"], "l r", "\\footnotesize", "1.0"))
156+ out.append("\\end{column}\\begin{column}{0.4\\textwidth}\\footnotesize\\begin{center}")
157+ out.append(kpi("uqoBleu", money(t["total"] / heated), "par m\\textsuperscript{2} chauffé") + kpi("uqoTeal", money(t["total"] / heated / 10.7639), "par pi\\textsuperscript{2} chauffé"))
158+ out.append("\\end{center}")
159+ out.append(f"\\begin{{importbox}}[Multiplicateur]Direct $\\rightarrow$ total : $\\times$~{num(t['total']/t['direct'],3)}. Le Total O\\&P de RSMeans n'est pas additionné (il contient déjà l'O\\&P de l'installateur).\\end{{importbox}}")
160+ out.append("\\end{column}\\end{columns}\\end{frame}\n")
161+ return "\n".join(out)
162+
163+LECTURE = {
164+ 7: ("Le garage : le cas d'école", ["Sans sous-sol, la fondation se réduit à des semelles peu profondes, une dalle isolée et des murs courts --- mais excavation, remblai et drain restent", "Porte de garage, ossature et toiture pèsent plus que tout le reste : sur un petit bâtiment, les \\textbf{éléments unitaires} dominent", "Le \\$/m\\textsuperscript{2} est élevé pour un bâtiment « simple » : branchements et entrée électrique ne se diluent pas"], "Pourquoi le \\$/m\\textsuperscript{2} d'un garage dépasse-t-il celui d'un bungalow ? Que se passe-t-il si l'on double la surface ?"),
165+ 1: ("Le bungalow : la référence du cours", ["Fondations (03) et enveloppe (07) sont les postes lourds : sous-sol complet obligatoire au Québec (profondeur de gel)", "Les ouvertures se comptent sur le plan ; les finitions suivent les \\textbf{usages des pièces} (céramique dans les pièces d'eau, bois franc ailleurs)", "Gatineau est une ville extraite en entier : chaque prix est celui de RSMeans pour C57, sans facteur intermédiaire"], "Quelles lignes changeraient si le sous-sol était fini ? (Cas 8 : même bungalow, sous-sol fini, à Saguenay.)"),
166+ 2: ("Le cottage avec garage : trois niveaux de plan", ["Murs extérieurs et toiture ne doublent pas avec la surface : l'étage partage fondation et toit --- d'où un \\$/m\\textsuperscript{2} inférieur au bungalow", "Brique (60\\,\\%) et sous-sol fini déplacent le poids vers l'enveloppe et les finitions", "Le garage attenant ajoute dalle, porte et murs, mais partage un mur avec la maison"], "Comparez le \\$/m\\textsuperscript{2} chauffé du cottage à celui du bungalow : lequel est le plus économique à construire, et pourquoi ?"),
167+ 3: ("Le triplex : trois logements superposés", ["Trois cuisines, trois salles de bain : les \\textbf{postes par logement} (11, 22, 26) grimpent", "La structure est mutualisée (un toit, une fondation) : le coût par logement reste inférieur à trois bungalows", "Québec (C12) : prix extraits en entier ; à comparer avec Montréal (Cas 2) et Laval (Cas 4)"], "Calculez le coût à neuf \\textbf{par logement} et comparez-le au Cas 1 : que mutualise le plex ?"),
168+ 6: ("Le chalet sur pieux : la fiche change la méthode", ["Sans sous-sol ni dalle, « fondations » devient pieux + vide sanitaire : le relevé le sait grâce à la \\textbf{fiche technique}", "Rimouski (C79) : prix RSMeans extraits (livres commercial et résidentiel), facteurs de section ailleurs", "Plinthes électriques et une seule salle de bain : mécanique légère, enveloppe dominante"], "Quelles lignes du relevé disparaissent par rapport au bungalow, et lesquelles apparaissent ?"),
169+}
170+
171+def frame_lecture(d):
172+ short = short_name(d["project"]); title, pts, q = LECTURE[d["n"]]
173+ out = [f"\\begin{{frame}}{{{tex(short)} --- lecture de l'évaluateur}}", f"\\uqotitle{{{title}}}", "\\begin{itemize}"] + [f" \\item {x}" for x in pts]
174+ out.append(f"\\end{{itemize}}\\vspace{{1mm}}\\begin{{quizbox}}[Question à la classe]{q}\\end{{quizbox}}\\end{{frame}}\n")
175+ return "\n".join(out)
176+
177+def intro(loc):
178+ out = []
179+ out.append("""\\begin{frame}{Pourquoi des cas complets?}
180+\\begin{columns}[T]\\begin{column}{0.54\\textwidth}\\footnotesize
181+\\uqotitle{Jusqu'ici :} des coûts par composante et des \\$/pi\\textsuperscript{2} de guides.\\\\[1mm]
182+\\uqotitle{Maintenant :} la \\textbf{méthode du relevé des quantités} (\\emph{quantity survey}), de bout en bout, sur de vrais plans :
183+\\begin{enumerate}\\setlength{\\itemsep}{0pt}
184+ \\item lire un plan d'architecte calibré ;
185+ \\item fixer la fiche technique ;
186+ \\item relever \\textbf{toutes} les quantités, avec leurs formules ;
187+ \\item attacher un coût unitaire RSMeans localisé ;
188+ \\item appliquer les majorations dans l'ordre ;
189+ \\item figer une révision et la lire en évaluateur.
190+\\end{enumerate}
191+\\end{column}\\begin{column}{0.44\\textwidth}\\footnotesize
192+\\begin{importbox}[Cinq cas, cinq villes]Garage (Trois-Rivières), bungalow (Gatineau), cottage + garage (Montréal), triplex (Québec), chalet sur pieux (Rimouski) ; trois autres (Laval, Sherbrooke, Saguenay) dans UQO Éval.\\end{importbox}
193+\\begin{notebox}[Source des chiffres]Révisions figées du module \\textbf{Quantity Survey} d'UQO Éval, prix \\textbf{RSMeans 2026 T4} localisés par ville.\\end{notebox}
194+\\end{column}\\end{columns}
195+\\end{frame}
196+""")
197+ out.append("""\\begin{frame}{La méthode en six étapes}
198+\\begin{center}\\footnotesize
199+\\begin{tikzpicture}[node distance=0.2cm]
200+\\node[uqostep, text width=11.5cm] (s1) {\\textbf{1. Plan} --- murs, cloisons, pièces et usages, portes, fenêtres, empreinte, lot, à l'échelle};
201+\\node[uqostep, text width=11.5cm, below=of s1] (s2) {\\textbf{2. Fiche technique} --- fondation, sous-sol, isolation, revêtements, toiture, chauffage, qualité};
202+\\node[uqostep, text width=11.5cm, below=of s2] (s3) {\\textbf{3. Relevé} --- 40 à 75 mesures, chacune avec sa formule et ses entrées (périmètre, aires, hauteurs, comptages)};
203+\\node[uqostep, text width=11.5cm, below=of s3] (s4) {\\textbf{4. Prix unitaires} --- une ou plusieurs lignes RSMeans par famille, coûts \\emph{bare} localisés à la ville};
204+\\node[uqostep, text width=11.5cm, below=of s4] (s5) {\\textbf{5. Majorations} --- conditions générales, frais généraux, profit, contingence (actualisation, taxes)};
205+\\node[uqostep, text width=11.5cm, below=of s5, fill=uqoBleu, text=uqoBlanc] (s6) {\\textbf{6. Révision figée} --- instantané immuable (lignes, prix, versions, SHA-256) $\\rightarrow$ rapport};
206+\\foreach \\a/\\b in {s1/s2,s2/s3,s3/s4,s4/s5,s5/s6} \\draw[uqoarrow] (\\a) -- (\\b);
207+\\end{tikzpicture}
208+\\end{center}
209+\\end{frame}
210+""")
211+ out.append("""\\begin{frame}{Étape 1 --- lire le plan : ce qui devient une quantité}
212+\\begin{columns}[T]\\begin{column}{0.46\\textwidth}
213+\\begin{center}\\includegraphics[width=\\textwidth]{images/qs-cas/cas1/Rez_de_chausse_e.png}\\end{center}
214+\\end{column}\\begin{column}{0.52\\textwidth}\\footnotesize
215+\\begin{itemize}\\setlength{\\itemsep}{1pt}
216+ \\item \\textbf{Murs extérieurs} (0,30~m) : longueur, aire brute, aire nette (ouvertures déduites une seule fois)
217+ \\item \\textbf{Cloisons} (0,12~m) : longueur $\\times$ hauteur $\\rightarrow$ gypse des deux faces
218+ \\item \\textbf{Pièces} nommées avec usage : aire et périmètre $\\rightarrow$ planchers, plinthes, plafonds
219+ \\item \\textbf{Ouvertures} : type (porte, fenêtre, patio, garage), largeur, hauteur, mur porteur
220+ \\item \\textbf{Empreinte} : excavation, semelles, dalle, toiture ; \\textbf{niveaux} calibrés un à un
221+ \\item Plans dessinés : échelle exacte (50~px/m) ; plans importés : calibration sur deux points, incertitude notée au registre
222+\\end{itemize}
223+\\end{column}\\end{columns}
224+\\end{frame}
225+""")
226+ out.append("""\\begin{frame}{Étape 2 --- la fiche technique : ce que le plan ne dit pas}
227+\\begin{columns}[T]\\begin{column}{0.5\\textwidth}\\footnotesize
228+\\uqotitle{Structure et enveloppe}
229+\\begin{itemize}\\setlength{\\itemsep}{1pt}
230+ \\item Fondation : béton coulé, blocs, dalle sur sol, pieux, coffrage isolant
231+ \\item Sous-sol : aucun, vide sanitaire, non fini, fini (\\% fini) ; hauteur des murs
232+ \\item Revêtements extérieurs en parts (vinyle 70\\,\\% / brique 30\\,\\%)
233+ \\item Toiture : type, géométrie, pente (facteur $\\sqrt{1+(p/12)^2}$)
234+ \\item Fenêtres : PVC, bois, hybride, aluminium
235+\\end{itemize}
236+\\end{column}\\begin{column}{0.48\\textwidth}\\footnotesize
237+\\uqotitle{Mécanique et finitions}
238+\\begin{itemize}\\setlength{\\itemsep}{1pt}
239+ \\item Chauffage : plinthes, thermopompe, fournaise électrique ou gaz ; climatisation ; VRC
240+ \\item Qualité : économique, standard, supérieure, prestige (cuisine et bains)
241+ \\item Planchers en parts (bois franc, céramique, vinyle, stratifié, tapis)
242+ \\item Extérieur : garage, terrasse, entrée, clôture, aménagement
243+\\end{itemize}
244+\\begin{importbox}[Règle]Chaque choix sélectionne des \\textbf{familles} du relevé, donc des lignes RSMeans --- une fiche incomplète donne un relevé incomplet, jamais un relevé inventé.\\end{importbox}
245+\\end{column}\\end{columns}
246+\\end{frame}
247+""")
248+ rows = [["Excavation", "(empreinte + 0,9~m $\\times$ périmètre) $\\times$ profondeur", "m\\textsuperscript{3}"], ["Semelles filantes", "périmètre (+ ½ périmètre du garage)", "m"], ["Murs de fondation", "périmètre $\\times$ hauteur de mur", "m\\textsuperscript{2}"], ["Murs extérieurs 2$\\times$6", "$\\Sigma$ murs extérieurs bruts + pignons", "m\\textsuperscript{2}"], ["Cloisons 2$\\times$4", "$\\Sigma$ cloisons (longueur $\\times$ hauteur)", "m\\textsuperscript{2}"], ["Fermes + support de toit", "(empreinte du dernier niveau + garage) $\\times$ pente $\\times$ 1,10", "m\\textsuperscript{2}"], ["Revêtement extérieur", "murs extérieurs nets $\\times$ part du revêtement", "m\\textsuperscript{2}"], ["Fenêtres, portes", "comptées sur les plans (type, mur extérieur ou non)", "unité"], ["Gypse murs", "face intérieure des murs ext. + 2 faces des cloisons", "m\\textsuperscript{2}"], ["Planchers finis", "aire des pièces par usage $\\times$ part du revêtement", "m\\textsuperscript{2}"], ["Plinthes électriques", "aire chauffée $\\div$ 14~m\\textsuperscript{2}", "unité"]]
249+ out.append("\\begin{frame}{Étape 3 --- le relevé : des formules explicites, jamais un forfait caché}")
250+ out.append(table(rows, ["Famille", "Formule", "Unité"], "l l l", "\\scriptsize", "0.95"))
251+ out.append("{\\tiny\\textcolor{uqoGris}{Extrait des $\\approx$ 55 familles du relevé automatique d'UQO Éval ; chaque mesure est inscrite au registre avec sa formule, ses entrées et ses pages sources, et peut être vérifiée ou corrigée.}}\n\\end{frame}\n")
252+ out.append("""\\begin{frame}{Étape 4 --- les prix unitaires : RSMeans 2026 T4 localisé}
253+\\begin{columns}[T]\\begin{column}{0.5\\textwidth}\\footnotesize
254+\\uqotitle{La base}
255+\\begin{itemize}\\setlength{\\itemsep}{1pt}
256+ \\item 79~044 lignes de coûts unitaires MasterFormat 2018 (Gordian) ; 4 livres : commercial (syndiqué / non syndiqué), rénovation, résidentiel, entretien
257+ \\item 18 localisations : moyenne nationale + \\textbf{17 villes du Québec}
258+ \\item Par ligne : unité, équipe, production, heures de MO, coûts \\textbf{bare} (matériau, MO, équipement) et Total O\\&P
259+\\end{itemize}
260+\\end{column}\\begin{column}{0.48\\textwidth}\\footnotesize
261+\\uqotitle{Les règles d'usage}
262+\\begin{itemize}\\setlength{\\itemsep}{1pt}
263+ \\item On attache le coût \\textbf{bare} ; les majorations du bordereau s'ajoutent ensuite
264+ \\item Unités converties : S.F. $\\rightarrow$ pi\\textsuperscript{2}, C.Y. $\\rightarrow$ vg\\textsuperscript{3}, Sq. = 100~pi\\textsuperscript{2}, M.S.F. = 1~000~pi\\textsuperscript{2}
265+ \\item Une famille = une ou plusieurs lignes (salle de bain = toilette + lavabo + robinet + baignoire + robinetterie)
266+ \\item Classe d'évidence « barème publié » ; licence Gordian (usage pédagogique, pas de redistribution)
267+\\end{itemize}
268+\\end{column}\\end{columns}
269+\\end{frame}
270+""")
271+ keep = ["NA", "MONTREAL", "QUEBEC", "GATINEAU", "TROIS-RIVIERES", "RIMOUSKI", "CHICOUTIMI"]
272+ names = {"NA": "Nat.", "MONTREAL": "Montréal", "QUEBEC": "Québec", "GATINEAU": "Gatineau", "TROIS-RIVIERES": "T.-Riv.", "RIMOUSKI": "Rimouski", "CHICOUTIMI": "Chicoutimi"}
273+ rows = []
274+ for c in loc["cmp"]:
275+ pr = {x["loc"]: x["p"] for x in c["prices"]}
276+ rows.append([cut(tex(c["desc"]), 42), tex(c["unit"])] + [money(pr[k]["total"], 2) if pr.get(k) and pr[k]["total"] is not None else "---" for k in keep])
277+ out.append("\\begin{frame}{Localisation --- la même ligne RSMeans dans sept villes (coût bare, MO syndiquée)}")
278+ out.append(table(rows, ["Ligne", "Unité"] + [names[k] for k in keep], "l l r r r r r r r", "\\scriptsize", "0.95"))
279+ out.append("\\begin{notebox}[Ce qu'on observe]\\footnotesize Les matériaux varient peu (fenêtre, toilette) ; la \\textbf{main-d'\\oe uvre} fait la différence (ossature, gypse). RSMeans localise par \\textbf{section MasterFormat} : les facteurs de section reproduisent les prix extraits à l'arrondi près (p90 $<$ 0,1\\,\\%) ; un facteur par division se trompe de 20\\,\\% une fois sur dix.\\end{notebox}\n\\end{frame}\n")
280+ out.append("""\\begin{frame}{Étape 5 --- les majorations, dans l'ordre}
281+\\begin{columns}[T]\\begin{column}{0.52\\textwidth}\\footnotesize
282+\\begin{center}
283+\\begin{tikzpicture}[node distance=0.2cm]
284+\\node[uqostep, text width=6.6cm] (a) {Direct = $\\Sigma$ quantité $\\times$ coût unitaire bare};
285+\\node[uqostep, text width=6.6cm, below=of a] (b) {+ Conditions générales (\\% du direct)};
286+\\node[uqostep, text width=6.6cm, below=of b] (c) {+ Frais généraux et profit (\\% du cumul)};
287+\\node[uqostep, text width=6.6cm, below=of c] (d) {+ Contingence (\\% du direct)};
288+\\node[uqostep, text width=6.6cm, below=of d] (e) {$\\times$ Actualisation --- + TPS / TVQ non composées};
289+\\node[uqostep, text width=6.6cm, below=of e, fill=uqoBleu, text=uqoBlanc] (f) {= Coût à neuf};
290+\\foreach \\x/\\y in {a/b,b/c,c/d,d/e,e/f} \\draw[uqoarrow] (\\x) -- (\\y);
291+\\end{tikzpicture}
292+\\end{center}
293+\\end{column}\\begin{column}{0.46\\textwidth}\\footnotesize
294+\\begin{importbox}[Deux pièges]\\textbf{Double majoration} : le Total O\\&P de RSMeans contient déjà l'O\\&P de l'installateur --- on part du bare. \\textbf{Double localisation} : un prix RSMeans « Gatineau » ne reçoit pas en plus le facteur régional du module.\\end{importbox}
295+\\begin{exbox}[Paramètres des cas]CG 8\\,\\%, FG 7\\,\\%, profit 8\\,\\%, contingence 5\\,\\% ; prix 2026 T4 sans actualisation, hors taxes ; multiplicateur direct $\\rightarrow$ total $\\approx$ 1,30.\\end{exbox}
296+\\end{column}\\end{columns}
297+\\end{frame}
298+""")
299+ out.append("""\\begin{frame}{Étape 6 --- dans UQO Éval : du gabarit au rapport}
300+\\begin{columns}[T]\\begin{column}{0.5\\textwidth}
301+\\begin{center}\\begin{uqoshot}[\\textwidth]{images/qs-cas/ui/q3-cas1-estimation.png}{www.uqo-eval.app/quantity-survey}\\end{uqoshot}\\end{center}
302+\\end{column}\\begin{column}{0.48\\textwidth}\\footnotesize
303+\\begin{enumerate}\\setlength{\\itemsep}{1pt}
304+ \\item \\textbf{Nouveau plan} : galerie de 38 gabarits (bungalows, cottages, plex, chalets, garages), plan vierge, carte satellite ou PDF
305+ \\item Onglet \\textbf{Fiche} : fondation, revêtements, chauffage, qualité
306+ \\item Onglet \\textbf{Estimation} : « Générer le relevé des quantités » avec \\textbf{prix RSMeans localisés}
307+ \\item Chaque ligne : quantité liée au registre, ligne RSMeans, coût unitaire, provenance
308+ \\item \\textbf{Figer une révision} : PDF FR/EN, XLSX, CSV, comparaison de révisions
309+ \\item Les huit cas sont verrouillés dans « Gabarits et cas d'étude » : dupliquez-les pour les modifier
310+\\end{enumerate}
311+\\end{column}\\end{columns}
312+\\end{frame}
313+""")
314+ return "\n".join(out)
315+
316+def synthese(all_ds):
317+ out = []; rows = []
318+ for d in all_ds:
319+ p = d["project"]; t = d["version"]["result"]["totals"]; short = short_name(p).replace("Cas ", ""); heated = d["heated"] or 0; u = p["spec"].get("units", 1)
320+ rows.append([cut(tex(short), 44), num(d["footprint"]), num(heated), str(u), money(t["total"]), money(t["total"] / heated) if heated else "---", money(t["total"] / heated / 10.7639) if heated else "---", money(t["total"] / u)])
321+ out.append("\\begin{frame}{Synthèse --- les huit cas côte à côte}")
322+ out.append(table(rows, ["Cas", "Empreinte", "Aire ch.", "Log.", "Total HT", "\\$/m\\textsuperscript{2}", "\\$/pi\\textsuperscript{2}", "\\$/logement"], "l r r r r r r r", "\\scriptsize", "0.95"))
323+ out.append("{\\tiny\\textcolor{uqoGris}{Aires en m\\textsuperscript{2} ; aire chauffée = pièces habitables + sous-sol fini ; totaux hors taxes avec majorations (CG 8\\,\\%, FG 7\\,\\%, profit 8\\,\\%, contingence 5\\,\\%) ; RSMeans 2026 T4 localisé par ville.}}\n\\end{frame}\n")
324+ out.append("""\\begin{frame}{Synthèse --- ce que les cas enseignent}
325+\\begin{columns}[T]\\begin{column}{0.5\\textwidth}\\footnotesize
326+\\begin{exbox}[Effet d'échelle]Le \\$/m\\textsuperscript{2} \\textbf{baisse} du garage au cottage puis au plex : fondation et toiture sont partagées par plus de surface ; les coûts fixes (branchements, entrée électrique, chauffe-eau) se diluent.\\end{exbox}
327+\\begin{exbox}[Effet de la fiche]Sous-sol fini, brique, thermopompe, qualité supérieure : chaque choix ajoute des \\emph{familles} de lignes --- comparez les Cas 1 et 8 (même bungalow, sous-sol fini, autre ville).\\end{exbox}
328+\\end{column}\\begin{column}{0.48\\textwidth}\\footnotesize
329+\\begin{exbox}[Effet de la ville]Entre villes du Québec, l'écart vient surtout de la main-d'\\oe uvre ; il reste modeste comparé à l'effet de la qualité des finitions.\\end{exbox}
330+\\begin{importbox}[Un bordereau sérieux montre]Pour chaque ligne : la quantité \\emph{et} sa formule, la source du prix \\emph{et} sa date, les majorations \\emph{et} leur ordre --- exactement ce qu'on attend au TP2.\\end{importbox}
331+\\end{column}\\end{columns}
332+\\end{frame}
333+""")
334+ d4 = next(x for x in all_ds if x["n"] == 4); s = d4["project"]["spec"]; t = d4["version"]["result"]["totals"]
335+ head = f"\\begin{{frame}}{{À vous --- Cas 4 : duplex à Laval}}\\footnotesize\n\\begin{{quizbox}}[Mandat]Dans UQO Éval, dupliquez le \\textbf{{Cas 4 --- Duplex, Laval}} ({num(d4['footprint'])}~m\\textsuperscript{{2}} d'empreinte, {len(d4['pages'])} niveaux, 2 logements, {fr(s.get('foundation'))}, revêtement {mix(s.get('siding'))}, chauffage {fr(s.get('heating'))}) et, \\textbf{{sans regarder la révision figée}} :\\end{{quizbox}}\n"
336+ body = """\\begin{enumerate}\\setlength{\\itemsep}{1pt}
337+ \\item relevez à la main : périmètre, empreinte, murs extérieurs bruts, nombre de fenêtres, aire des pièces d'eau ; comparez aux mesures « QS » du registre (écart $<$ 2\\,\\%) ;
338+ \\item régénérez le relevé avec le \\textbf{catalogue UQO Éval} plutôt que RSMeans : expliquez les trois plus gros écarts ;
339+ \\item changez la ville RSMeans pour Montréal : de combien bouge le total, et par quelles divisions ?
340+\\end{enumerate}
341+"""
342+ tail = f"\\begin{{notebox}}[Réponse attendue]Total RSMeans Laval figé : \\textbf{{{money(t['total'])}}} hors taxes ({money(t['total']/d4['heated'])}/m\\textsuperscript{{2}} chauffé). Retrouvez les quantités, pas le total au dollar près.\\end{{notebox}}\n\\end{{frame}}\n"
343+ out.append(head + body + tail)
344+ out.append("""\\begin{frame}{Résumé de l'atelier}
345+\\begin{enumerate}\\setlength{\\itemsep}{2pt}
346+ \\item Un relevé des quantités part d'un \\textbf{plan calibré} et d'une \\textbf{fiche technique} ; chaque quantité a une formule et des entrées traçables
347+ \\item Les prix unitaires viennent d'un \\textbf{barème publié, daté et localisé} (RSMeans 2026 T4 par ville) : coûts bare, puis majorations dans l'ordre
348+ \\item Divisions lourdes d'une résidence : fondations, enveloppe/toiture, finitions ; les plex déplacent le poids vers cuisine, plomberie, électricité
349+ \\item Le \\$/m\\textsuperscript{2} dépend de l'échelle, de la fiche et de la ville --- dans cet ordre d'importance
350+ \\item Une \\textbf{révision figée} rend l'estimation reproductible et discutable ligne par ligne
351+\\end{enumerate}
352+\\vspace{1mm}
353+\\begin{exbox}[Pour le TP2]Le bordereau du quadruplex suit la même logique : quantités formulées, prix sourcés, indirects documentés (séance 8), profit sur la bonne base.\\end{exbox}
354+\\end{frame}
355+""")
356+ return "\n".join(out)
357+
358+def main():
359+ loc = json.load(open(os.path.join(IMG, "localisation.json")))
360+ all_ds = [load(n) for n in ALL]; ds = {d["n"]: d for d in all_ds}
361+ parts = ["% Généré par gen_qs_cas.py — ne pas éditer à la main (données : images/qs-cas/*/data.json, révisions figées UQO Éval / RSMeans 2026 T4)",
362+ "\\providecommand{\\qskpi}[3][uqoBleu]{\\begin{tikzpicture}[baseline=(b.base)]\\node[fill=#1!8, draw=#1!40, rounded corners=2pt, inner xsep=4pt, inner ysep=2.5pt, align=center, text width=26mm] (b) {{\\textcolor{#1}{\\normalsize\\textbf{#2}}}\\\\[-1pt]{\\tiny\\textcolor{uqoGris}{#3}}};\\end{tikzpicture}}",
363+ "\\section{Atelier --- Quantity survey : cinq cas complets (plans, quantités, coûts)}", intro(loc)]
364+ for n in CASES:
365+ d = ds[n]; short = short_name(d["project"])
366+ parts.append(f"% ---------------- {short}")
367+ parts.append(f"\\begin{{frame}}[plain]\\begin{{center}}\\vspace{{1.2cm}}{{\\Huge\\textcolor{{uqoBleu}}{{\\textbf{{{tex(short)}}}}}}}\\\\[4mm]{{\\large\\textcolor{{uqoGris}}{{{tex(d['project']['municipality'])} $\\cdot$ RSMeans 2026 T4 $\\cdot$ {len(d['qs'])} mesures $\\cdot$ {len(d['lines'])} lignes chiffrées}}}}\\end{{center}}\\end{{frame}}\n")
368+ parts += [frame_fiche(d), frame_plans(d), frames_releve(d), frame_prix(d), frame_divisions(d), frame_total(d), frame_lecture(d)]
369+ parts.append(synthese(all_ds))
370+ os.makedirs(os.path.dirname(OUT), exist_ok=True)
371+ txt = "\n".join(parts); open(OUT, "w").write(txt)
372+ print("frames :", txt.count("\\begin{frame}"), "→", OUT)
373+
374+if __name__ == "__main__": main()
added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas1/Rez_de_chausse_e.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas1/Sous_sol.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas1/data.json +6288 −0
Large diff (6,289 lines) is not loaded by default to keep the page fast.Load diff
added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas2/E_tage.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas2/Rez_de_chausse_e.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas2/Sous_sol.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas2/data.json +7162 −0
Large diff (7,163 lines) is not loaded by default to keep the page fast.Load diff
added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas3/2e_e_tage.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas3/E_tage.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas3/Rez_de_chausse_e.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas3/Sous_sol.png +0 −0

Binary file not shown.

added IMM1033-20_Methodes_du_cout/02_Slides/sources/images/qs-cas/cas3/data.json +6153 −0
Large diff (6,153 lines) is not loaded by default to keep the page fast.Load diff

Diff truncated — file too large.

Menu

Theme