spb/traite-evaluation-qc
Public
1"""Tests du chapitre 42 — fraction de copropriété (grille, réconciliation), cotisation spéciale, copropriété locative, conversion, exercices.2Tolérances : 1 $ sur les montants cités arrondis au dollar ; 1e-6 sur les identités comptables ; 0,0001 sur les proportions."""3import sys, pathlib, math4import numpy as np, pytest5sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases"))6import ch42_copro as cas7import ch07_servitudes_copro as c78from traite import comparables as cp9from traite import finance as fin101112@pytest.fixture(scope="module")13def res():14 return cas.main()151617class TestFraction:18 def test_passif_et_prix_marginal(self, res):19 assert cas.DEFICIT_IMMEUBLE == pytest.approx(150908, abs=1) # déficit du ch. 7, réutilisé tel quel20 assert cas.SUJET["passif"] == pytest.approx(21248, abs=1)21 assert res["grille"]["prix_marginal_m2"] == pytest.approx(2190, abs=0.5)22 assert cas.prix_marginal_m2() == pytest.approx(cas.ELASTICITE_S * cas.PRIX_M2_REF)2324 def test_prix_observes(self, res):25 prix = [c["prix"] for c in res["grille"]["comparables"]]26 assert prix == [373000, 326500, 364000, 373500, 330500]27 assert all(p % 500 == 0 for p in prix)2829 def test_ajustements_cites(self, res):30 aj = res["grille"]["ajustes"]31 a1, a2, a3, a4, a5 = aj32 assert a1.ajustements["marché"] == pytest.approx(3712, abs=1) and a1.ajustements["étage"] == pytest.approx(15068, abs=1)33 assert a1.ajustements["syndicat"] == pytest.approx(-845, abs=1) and a1.prix_ajuste == pytest.approx(390935, abs=1)34 assert a2.ajustements["superficie"] == pytest.approx(28470, abs=1) and a2.ajustements["orientation"] == pytest.approx(9958, abs=1)35 assert a2.prix_ajuste == pytest.approx(367010, abs=1)36 assert a3.ajustements["charges"] == pytest.approx(26536, abs=1) and a3.ajustements["syndicat"] == pytest.approx(-21248, abs=1)37 assert a3.prix_ajuste == pytest.approx(375475, abs=1)38 assert a4.ajustements["stationnement"] == 28000 and a4.ajustements["étage"] == pytest.approx(-7519, abs=1)39 assert a4.ajustements["syndicat"] == pytest.approx(-15848, abs=1) and a4.prix_ajuste == pytest.approx(382191, abs=1)40 assert a5.ajustements["syndicat"] == pytest.approx(9952, abs=1) and a5.prix_ajuste == pytest.approx(373416, abs=1)41 # identité : prix ajusté = prix + Σ ajustements (traite.comparables)42 for a in aj:43 assert a.prix_ajuste == pytest.approx(cp.prix_ajuste_additif(a.prix, a.ajustements), abs=1e-9)44 # indicateurs brut / net cités45 assert a1.brut_pct == pytest.approx(0.053, abs=0.001) and a4.brut_pct == pytest.approx(0.200, abs=0.001)46 assert a5.net_pct == pytest.approx(0.130, abs=0.001)4748 def test_signe_syndicat_et_charges(self, res):49 # même immeuble : le passif par fraction diffère seulement par la quote-part (q_j − q_s) × déficit50 a1 = res["grille"]["ajustes"][0]51 assert a1.ajustements["syndicat"] == pytest.approx(cas.KAPPA * (cas.Q_F2 - cas.Q_F4) * cas.DEFICIT_IMMEUBLE, abs=1e-6)52 # charges : (c_j − c_s) × S_s / R_c, positif si le comparable paie plus (sujet supérieur)53 a3 = res["grille"]["ajustes"][2]54 assert a3.ajustements["charges"] == pytest.approx((78.0 - cas.CHARGES_REF_M2) * 98.0 / 0.08, abs=1e-6)5556 def test_reconciliation(self, res):57 rec = res["grille"]["reconciliation"]; w = res["grille"]["poids"]58 assert sum(w) == pytest.approx(1.0, abs=1e-12) and w[0] == pytest.approx(0.419, abs=0.001)59 assert rec["valeur_ponderee"] == pytest.approx(381052, abs=1)60 assert rec["moyenne"] == pytest.approx(377805, abs=1) and rec["mediane"] == pytest.approx(375475, abs=1)61 assert rec["min"] == pytest.approx(367010, abs=1) and rec["max"] == pytest.approx(390935, abs=1)62 assert rec["etendue_pct"] == pytest.approx(0.063, abs=0.001)63 # la valeur « vraie » du modèle de génération est dans la fourchette64 assert rec["min"] < res["grille"]["valeur_modele_sujet"] < rec["max"]65 assert res["grille"]["valeur_modele_sujet"] == pytest.approx(375183, abs=1)6667 def test_sensibilite(self, res):68 s = res["sens_fraction"]; g = s["grille"]69 assert g[1.0][2] == pytest.approx(381052, abs=1)70 assert g[0.0][0] == pytest.approx(386889, abs=1) and g[1.0][4] == pytest.approx(379437, abs=1)71 for k in s["kappas"]:72 assert all(np.diff(g[k]) < 0) # la valeur baisse quand R_c monte (ajustement de charges net positif → plus petit)73 for j in range(len(s["r_charges"])):74 assert g[0.0][j] > g[0.5][j] > g[1.0][j]757677class TestCotisationSpeciale:78 def test_repartition(self, res):79 cs = res["cotisation"]80 assert cs["montant"] == pytest.approx(150908, abs=1)81 assert cs["par_fraction"].sum() == pytest.approx(cs["montant"], abs=1e-6)82 assert cs["par_fraction"][3] == pytest.approx(21248, abs=1) and cs["par_fraction"][5] == pytest.approx(24809, abs=1)83 assert cs["par_fraction"][0] == pytest.approx(14110, abs=1)84 assert cs["ratio_annees"][3] == pytest.approx(2.08, abs=0.01) and cs["ratio_annees"][0] == pytest.approx(2.26, abs=0.01)85 assert cs["charges_annuelles"][3] == pytest.approx(10218, abs=1)868788class TestLocative:89 def test_rne_et_taux(self, res):90 cl = res["locative"]91 assert cl["rbp"] == 20400 and cl["rbe"] == pytest.approx(19972, abs=1)92 assert cl["frais_copro"] == pytest.approx(10218, abs=1) and cl["depenses"] == pytest.approx(15117, abs=1)93 assert cl["noi"] == pytest.approx(4855, abs=1) and cl["noi_sans_reserve"] == pytest.approx(7937, abs=1)94 assert cl["taux_implicite"] == pytest.approx(0.0127, abs=0.0001) and cl["taux_implicite_sans_reserve"] == pytest.approx(0.0208, abs=0.0001)95 assert cl["valeur_investissement_5_25"] == pytest.approx(92472, abs=1) and cl["mrb"] == pytest.approx(18.7, abs=0.05)96 assert cl["ratio_depenses"] == pytest.approx(0.757, abs=0.001)97 # identités (traite.finance)98 assert cl["rbe"] == pytest.approx(fin.revenu_brut_effectif(20400, 0.021))99 assert cl["noi"] == pytest.approx(cl["rbe"] - cl["depenses"])100101102class TestConversion:103 def test_bloc(self, res):104 b = res["faisabilite"]["bloc"]105 assert b["rbp"] == 146700 and b["rbe"] == pytest.approx(142299, abs=1) and b["noi"] == pytest.approx(82533, abs=1)106 assert b["valeur"] == pytest.approx(1572065, abs=1) and b["valeur"] == pytest.approx(b["noi"] / 0.0525)107 assert b["mrb"] == pytest.approx(10.7, abs=0.05)108109 def test_valeurs_libres(self, res):110 cal = res["faisabilite"]["calendrier"]; v = cal["valeurs"]111 assert v["F4"] == pytest.approx(396431, abs=1) and v["F1"] == pytest.approx(276481, abs=1) and v["F6"] == pytest.approx(430539, abs=1)112 assert cal["somme_libre"] == pytest.approx(2802191, abs=1)113 # F4 libre de passif = F4 du sujet + part du déficit (cohérence des deux blocs)114 assert v["F4"] - cas.SUJET["passif"] == pytest.approx(res["grille"]["valeur_modele_sujet"], abs=1e-6)115 assert cal["n_libres_an1"] == 4 and cal["n_occupes"] == 4 and cal["V_occ"] == pytest.approx(1246418, abs=1)116117 def test_calendrier_identite(self, res):118 fb = res["faisabilite"]; cal = fb["calendrier"]; H = cas.HORIZON_CONV; rho, d = cas.ROTATION, cas.DECOTE_OCCUPE119 libres = cal["somme_libre"] - cal["V_occ"]120 attendu = libres + cal["V_occ"] * (1 - (1 - rho) ** (H - 1)) + cal["V_occ"] * (1 - rho) ** (H - 1) * (1 - d)121 assert cal["produit"].sum() == pytest.approx(attendu, abs=1e-6)122 assert cal["produit"][1] == pytest.approx(1705343, abs=1) and cal["produit"][6] == pytest.approx(526219, abs=1)123124 def test_faisabilite(self, res):125 fb = res["faisabilite"]126 assert fb["couts"] == 305000 and fb["flux"][0] == -305000127 assert fb["flux"][1] == pytest.approx(1595097, abs=1) and fb["flux"][6] == pytest.approx(495304, abs=1)128 assert fb["v_conv"] == pytest.approx(1692804, abs=1) and fb["prime"] == pytest.approx(120739, abs=1)129 assert fb["marge_sur_bloc"] == pytest.approx(0.077, abs=0.001)130 assert fb["frais_vente_total"] == pytest.approx(106825, abs=1) and fb["portage_total"] == pytest.approx(106784, abs=1)131 assert fb["somme_nette_non_actualisee"] == pytest.approx(2152027, abs=1)132 # V_conv est la VAN des flux au taux du promoteur (traite.finance.van)133 assert fb["v_conv"] == pytest.approx(fin.van(list(fb["flux"]), 0.10), abs=1e-6)134 assert res["faisabilite_occ"]["v_conv"] == pytest.approx(1884780, abs=1)135136 def test_sensibilite_conversion(self, res):137 sc = res["sens_conv"]; g = sc["grille"]138 assert g[0.20][2] == pytest.approx(1692804, abs=1)139 assert g[0.10][0] == pytest.approx(1636219, abs=1) and g[0.30][4] == pytest.approx(1871145, abs=1)140 assert sc["vente_occupee_an1"][0.20] == pytest.approx(1884780, abs=1)141 for d in sc["decotes"]:142 assert all(np.diff(g[d]) > 0) # plus la rotation est rapide, plus la conversion vaut143 for j in range(len(sc["rotations"])):144 assert g[0.10][j] > g[0.20][j] > g[0.30][j]145 assert res["seuil_prix"] == pytest.approx(0.937, abs=0.001)146 assert cas.faisabilite(r=0.08)["v_conv"] == pytest.approx(1768600, abs=1)147 assert cas.faisabilite(r=0.12)["v_conv"] == pytest.approx(1623172, abs=1)148149150class TestExercices:151 def test_ex1_charges(self):152 s = np.array([60, 75, 75, 90, 90, 110.0]); q = s / s.sum()153 assert s.sum() == 500 and q[3] == pytest.approx(0.18)154 total = 36000 + 15000 + 4000155 assert q[3] * total == pytest.approx(9900) and q[3] * total / 12 == pytest.approx(825)156 assert q[3] * 15000 == pytest.approx(2700) and q[3] * 36000 == pytest.approx(6480)157158 def test_ex2_attestation(self):159 q = 0.085; deficit = 180000 - 62000; part_speciale = q * 40000; part_deficit = q * deficit160 assert deficit == 118000 and part_speciale == pytest.approx(3400) and part_deficit == pytest.approx(10030)161 assert part_speciale + part_deficit == pytest.approx(13430)162 assert q * (19500 - 9000) == pytest.approx(892.5)163164 def test_ex3_capitalisation_charges(self):165 dC = (62 - 48) * 90166 assert dC == 1260167 assert dC / 0.08 == pytest.approx(15750) and dC / 0.06 == pytest.approx(21000) and dC / 0.12 == pytest.approx(10500)168 assert dC / (0.05 - 0.025) == pytest.approx(50400)169170 def test_ex4_grille(self):171 p_t = 342000 * 1.02 ** (5 / 12)172 assert p_t == pytest.approx(344834, abs=1)173 A = {"marché": p_t - 342000, "étage": p_t * 0.02, "orientation": p_t * (-0.03 - 0.03), "stationnement": -28000.0, "superficie": -4 * 2190.0}174 pa = cp.prix_ajuste_additif(342000, A)175 assert A["étage"] == pytest.approx(6897, abs=1) and A["orientation"] == pytest.approx(-20690, abs=1) and A["superficie"] == -8760176 assert pa == pytest.approx(294280, abs=1)177 bn = cp.ajustement_brut_net(A, 342000)178 assert bn["brut_pct"] == pytest.approx(0.196, abs=0.001)179180 def test_ex5_locative(self):181 v = 380000.0; noi_cible = 0.04 * v182 assert noi_cible == 15200183 rbe = (noi_cible + 3300 + 600 + 10218) / 0.95184 assert rbe == pytest.approx(30861, abs=1)185 rbp = rbe / (1 - 0.021)186 assert rbp == pytest.approx(31523, abs=1) and rbp / 12 == pytest.approx(2627, abs=1)187 # au loyer de 1 700 $ : RNE et taux comme dans l'exemple, sur 380 000 $188 cl = cas.copro_locative(v)189 assert cl["noi"] == pytest.approx(4855, abs=1) and cl["taux_implicite"] == pytest.approx(0.0128, abs=0.0001)190191 def test_ex6_conversion_script(self):192 # taux de capitalisation 4,75 % : la valeur en bloc monte et la prime devient négative193 v_bloc_475 = cas.valeur_bloc(taux=0.0475)["valeur"]194 assert v_bloc_475 == pytest.approx(cas.valeur_bloc()["noi"] / 0.0475) and v_bloc_475 == pytest.approx(1737546, abs=1)195 assert cas.faisabilite()["v_conv"] - v_bloc_475 == pytest.approx(-44742, abs=1)196 # rotation de 30 % et décote de 10 %197 assert cas.faisabilite(0.30, 0.10)["v_conv"] == pytest.approx(1893849, abs=1)198 # sans coûts d'individualisation (−120 000 $ à t = 0) : la VAN monte exactement de 120 000 $199 couts = dict(cas.COUTS_CONVERSION); couts.pop("Individualisation des services et mise aux normes (8 logements)")200 assert cas.faisabilite(couts=couts)["v_conv"] - cas.faisabilite()["v_conv"] == pytest.approx(120000, abs=1e-6)201202 def test_ex7_indivision(self):203 V = 1050000.0; parts = 3 * (V / 3) * (1 - 0.12)204 assert parts == pytest.approx(924000)205 fractions = 3 * 370000.0; couts = 60000.0206 assert fractions - parts - couts == pytest.approx(126000) and (fractions - parts - couts) / 3 == pytest.approx(42000)207