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 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 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