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