"""Tests du chapitre 42 — fraction de copropriété (grille, réconciliation), cotisation spéciale, copropriété locative, conversion, exercices. Tolérances : 1 $ sur les montants cités arrondis au dollar ; 1e-6 sur les identités comptables ; 0,0001 sur les proportions.""" import sys, pathlib, math import numpy as np, pytest sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "cases")) import ch42_copro as cas import ch07_servitudes_copro as c7 from traite import comparables as cp from traite import finance as fin @pytest.fixture(scope="module") def res(): return cas.main() class TestFraction: def test_passif_et_prix_marginal(self, res): assert cas.DEFICIT_IMMEUBLE == pytest.approx(150908, abs=1) # déficit du ch. 7, réutilisé tel quel assert cas.SUJET["passif"] == pytest.approx(21248, abs=1) assert res["grille"]["prix_marginal_m2"] == pytest.approx(2190, abs=0.5) assert cas.prix_marginal_m2() == pytest.approx(cas.ELASTICITE_S * cas.PRIX_M2_REF) def test_prix_observes(self, res): prix = [c["prix"] for c in res["grille"]["comparables"]] assert prix == [373000, 326500, 364000, 373500, 330500] assert all(p % 500 == 0 for p in prix) def test_ajustements_cites(self, res): aj = res["grille"]["ajustes"] a1, a2, a3, a4, a5 = aj assert a1.ajustements["marché"] == pytest.approx(3712, abs=1) and a1.ajustements["étage"] == pytest.approx(15068, abs=1) assert a1.ajustements["syndicat"] == pytest.approx(-845, abs=1) and a1.prix_ajuste == pytest.approx(390935, abs=1) assert a2.ajustements["superficie"] == pytest.approx(28470, abs=1) and a2.ajustements["orientation"] == pytest.approx(9958, abs=1) assert a2.prix_ajuste == pytest.approx(367010, abs=1) assert a3.ajustements["charges"] == pytest.approx(26536, abs=1) and a3.ajustements["syndicat"] == pytest.approx(-21248, abs=1) assert a3.prix_ajuste == pytest.approx(375475, abs=1) assert a4.ajustements["stationnement"] == 28000 and a4.ajustements["étage"] == pytest.approx(-7519, abs=1) assert a4.ajustements["syndicat"] == pytest.approx(-15848, abs=1) and a4.prix_ajuste == pytest.approx(382191, abs=1) assert a5.ajustements["syndicat"] == pytest.approx(9952, abs=1) and a5.prix_ajuste == pytest.approx(373416, abs=1) # identité : prix ajusté = prix + Σ ajustements (traite.comparables) for a in aj: assert a.prix_ajuste == pytest.approx(cp.prix_ajuste_additif(a.prix, a.ajustements), abs=1e-9) # indicateurs brut / net cités assert a1.brut_pct == pytest.approx(0.053, abs=0.001) and a4.brut_pct == pytest.approx(0.200, abs=0.001) assert a5.net_pct == pytest.approx(0.130, abs=0.001) def test_signe_syndicat_et_charges(self, res): # même immeuble : le passif par fraction diffère seulement par la quote-part (q_j − q_s) × déficit a1 = res["grille"]["ajustes"][0] assert a1.ajustements["syndicat"] == pytest.approx(cas.KAPPA * (cas.Q_F2 - cas.Q_F4) * cas.DEFICIT_IMMEUBLE, abs=1e-6) # charges : (c_j − c_s) × S_s / R_c, positif si le comparable paie plus (sujet supérieur) a3 = res["grille"]["ajustes"][2] assert a3.ajustements["charges"] == pytest.approx((78.0 - cas.CHARGES_REF_M2) * 98.0 / 0.08, abs=1e-6) def test_reconciliation(self, res): rec = res["grille"]["reconciliation"]; w = res["grille"]["poids"] assert sum(w) == pytest.approx(1.0, abs=1e-12) and w[0] == pytest.approx(0.419, abs=0.001) assert rec["valeur_ponderee"] == pytest.approx(381052, abs=1) assert rec["moyenne"] == pytest.approx(377805, abs=1) and rec["mediane"] == pytest.approx(375475, abs=1) assert rec["min"] == pytest.approx(367010, abs=1) and rec["max"] == pytest.approx(390935, abs=1) assert rec["etendue_pct"] == pytest.approx(0.063, abs=0.001) # la valeur « vraie » du modèle de génération est dans la fourchette assert rec["min"] < res["grille"]["valeur_modele_sujet"] < rec["max"] assert res["grille"]["valeur_modele_sujet"] == pytest.approx(375183, abs=1) def test_sensibilite(self, res): s = res["sens_fraction"]; g = s["grille"] assert g[1.0][2] == pytest.approx(381052, abs=1) assert g[0.0][0] == pytest.approx(386889, abs=1) and g[1.0][4] == pytest.approx(379437, abs=1) for k in s["kappas"]: assert all(np.diff(g[k]) < 0) # la valeur baisse quand R_c monte (ajustement de charges net positif → plus petit) for j in range(len(s["r_charges"])): assert g[0.0][j] > g[0.5][j] > g[1.0][j] class TestCotisationSpeciale: def test_repartition(self, res): cs = res["cotisation"] assert cs["montant"] == pytest.approx(150908, abs=1) assert cs["par_fraction"].sum() == pytest.approx(cs["montant"], abs=1e-6) assert cs["par_fraction"][3] == pytest.approx(21248, abs=1) and cs["par_fraction"][5] == pytest.approx(24809, abs=1) assert cs["par_fraction"][0] == pytest.approx(14110, abs=1) assert cs["ratio_annees"][3] == pytest.approx(2.08, abs=0.01) and cs["ratio_annees"][0] == pytest.approx(2.26, abs=0.01) assert cs["charges_annuelles"][3] == pytest.approx(10218, abs=1) class TestLocative: def test_rne_et_taux(self, res): cl = res["locative"] assert cl["rbp"] == 20400 and cl["rbe"] == pytest.approx(19972, abs=1) assert cl["frais_copro"] == pytest.approx(10218, abs=1) and cl["depenses"] == pytest.approx(15117, abs=1) assert cl["noi"] == pytest.approx(4855, abs=1) and cl["noi_sans_reserve"] == pytest.approx(7937, abs=1) assert cl["taux_implicite"] == pytest.approx(0.0127, abs=0.0001) and cl["taux_implicite_sans_reserve"] == pytest.approx(0.0208, abs=0.0001) assert cl["valeur_investissement_5_25"] == pytest.approx(92472, abs=1) and cl["mrb"] == pytest.approx(18.7, abs=0.05) assert cl["ratio_depenses"] == pytest.approx(0.757, abs=0.001) # identités (traite.finance) assert cl["rbe"] == pytest.approx(fin.revenu_brut_effectif(20400, 0.021)) assert cl["noi"] == pytest.approx(cl["rbe"] - cl["depenses"]) class TestFinancement: def test_divise_vs_indivise(self, res): fa, fi = res["fin_divise"], res["fin_indivise"] assert fa["mise_de_fonds"] == 19000 and fa["pret"] == 361000 assert fa["paiement"] == pytest.approx(1922, abs=1) and fa["cout_mensuel"] == pytest.approx(3049, abs=1) assert fa["tam"] == pytest.approx(0.0612) and fa["paiement_tam"] == pytest.approx(2336, abs=1) assert fi["mise_de_fonds"] == 76000 and fi["pret"] == 304000 and fi["paiement"] == pytest.approx(1619, abs=1) assert fi["mise_de_fonds"] - fa["mise_de_fonds"] == 57000 # convention canadienne : composition semestrielle (traite.finance.Hypotheque) h = fin.Hypotheque(361000, 0.0412, 300) assert fa["paiement"] == pytest.approx(h.paiement) and h.solde(300) == pytest.approx(0.0, abs=1e-6) # droits de mutation 2026 pour 380 000 $ (cités dans le texte) dm = 0.005 * 62900 + 0.01 * (315000 - 62900) + 0.015 * (380000 - 315000) assert dm == pytest.approx(3810.5, abs=0.01) class TestConversion: def test_bloc(self, res): b = res["faisabilite"]["bloc"] assert b["rbp"] == 146700 and b["rbe"] == pytest.approx(142299, abs=1) and b["noi"] == pytest.approx(82533, abs=1) assert b["valeur"] == pytest.approx(1572065, abs=1) and b["valeur"] == pytest.approx(b["noi"] / 0.0525) assert b["mrb"] == pytest.approx(10.7, abs=0.05) def test_valeurs_libres(self, res): cal = res["faisabilite"]["calendrier"]; v = cal["valeurs"] assert v["F4"] == pytest.approx(396431, abs=1) and v["F1"] == pytest.approx(276481, abs=1) and v["F6"] == pytest.approx(430539, abs=1) assert cal["somme_libre"] == pytest.approx(2802191, abs=1) # F4 libre de passif = F4 du sujet + part du déficit (cohérence des deux blocs) assert v["F4"] - cas.SUJET["passif"] == pytest.approx(res["grille"]["valeur_modele_sujet"], abs=1e-6) assert cal["n_libres_an1"] == 4 and cal["n_occupes"] == 4 and cal["V_occ"] == pytest.approx(1246418, abs=1) def test_calendrier_identite(self, res): fb = res["faisabilite"]; cal = fb["calendrier"]; H = cas.HORIZON_CONV; rho, d = cas.ROTATION, cas.DECOTE_OCCUPE libres = cal["somme_libre"] - cal["V_occ"] attendu = libres + cal["V_occ"] * (1 - (1 - rho) ** (H - 1)) + cal["V_occ"] * (1 - rho) ** (H - 1) * (1 - d) assert cal["produit"].sum() == pytest.approx(attendu, abs=1e-6) assert cal["produit"][1] == pytest.approx(1705343, abs=1) and cal["produit"][6] == pytest.approx(526219, abs=1) def test_faisabilite(self, res): fb = res["faisabilite"] assert fb["couts"] == 305000 and fb["flux"][0] == -305000 assert fb["flux"][1] == pytest.approx(1595097, abs=1) and fb["flux"][6] == pytest.approx(495304, abs=1) assert fb["v_conv"] == pytest.approx(1692804, abs=1) and fb["prime"] == pytest.approx(120739, abs=1) assert fb["marge_sur_bloc"] == pytest.approx(0.077, abs=0.001) assert fb["frais_vente_total"] == pytest.approx(106825, abs=1) and fb["portage_total"] == pytest.approx(106784, abs=1) assert fb["somme_nette_non_actualisee"] == pytest.approx(2152027, abs=1) # V_conv est la VAN des flux au taux du promoteur (traite.finance.van) assert fb["v_conv"] == pytest.approx(fin.van(list(fb["flux"]), 0.10), abs=1e-6) assert res["faisabilite_occ"]["v_conv"] == pytest.approx(1884780, abs=1) def test_sensibilite_conversion(self, res): sc = res["sens_conv"]; g = sc["grille"] assert g[0.20][2] == pytest.approx(1692804, abs=1) assert g[0.10][0] == pytest.approx(1636219, abs=1) and g[0.30][4] == pytest.approx(1871145, abs=1) assert sc["vente_occupee_an1"][0.20] == pytest.approx(1884780, abs=1) for d in sc["decotes"]: assert all(np.diff(g[d]) > 0) # plus la rotation est rapide, plus la conversion vaut for j in range(len(sc["rotations"])): assert g[0.10][j] > g[0.20][j] > g[0.30][j] assert res["seuil_prix"] == pytest.approx(0.937, abs=0.001) assert cas.faisabilite(r=0.08)["v_conv"] == pytest.approx(1768600, abs=1) assert cas.faisabilite(r=0.12)["v_conv"] == pytest.approx(1623172, abs=1) class TestExercices: def test_ex1_charges(self): s = np.array([60, 75, 75, 90, 90, 110.0]); q = s / s.sum() assert s.sum() == 500 and q[3] == pytest.approx(0.18) total = 36000 + 15000 + 4000 assert q[3] * total == pytest.approx(9900) and q[3] * total / 12 == pytest.approx(825) assert q[3] * 15000 == pytest.approx(2700) and q[3] * 36000 == pytest.approx(6480) def test_ex2_attestation(self): q = 0.085; deficit = 180000 - 62000; part_speciale = q * 40000; part_deficit = q * deficit assert deficit == 118000 and part_speciale == pytest.approx(3400) and part_deficit == pytest.approx(10030) assert part_speciale + part_deficit == pytest.approx(13430) assert q * (19500 - 9000) == pytest.approx(892.5) def test_ex3_capitalisation_charges(self): dC = (62 - 48) * 90 assert dC == 1260 assert dC / 0.08 == pytest.approx(15750) and dC / 0.06 == pytest.approx(21000) and dC / 0.12 == pytest.approx(10500) assert dC / (0.05 - 0.025) == pytest.approx(50400) def test_ex4_grille(self): p_t = 342000 * 1.02 ** (5 / 12) assert p_t == pytest.approx(344834, abs=1) A = {"marché": p_t - 342000, "étage": p_t * 0.02, "orientation": p_t * (-0.03 - 0.03), "stationnement": -28000.0, "superficie": -4 * 2190.0} pa = cp.prix_ajuste_additif(342000, A) assert A["étage"] == pytest.approx(6897, abs=1) and A["orientation"] == pytest.approx(-20690, abs=1) and A["superficie"] == -8760 assert pa == pytest.approx(294280, abs=1) bn = cp.ajustement_brut_net(A, 342000) assert bn["brut_pct"] == pytest.approx(0.196, abs=0.001) def test_ex5_locative(self): v = 380000.0; noi_cible = 0.04 * v assert noi_cible == 15200 rbe = (noi_cible + 3300 + 600 + 10218) / 0.95 assert rbe == pytest.approx(30861, abs=1) rbp = rbe / (1 - 0.021) assert rbp == pytest.approx(31523, abs=1) and rbp / 12 == pytest.approx(2627, abs=1) # au loyer de 1 700 $ : RNE et taux comme dans l'exemple, sur 380 000 $ cl = cas.copro_locative(v) assert cl["noi"] == pytest.approx(4855, abs=1) and cl["taux_implicite"] == pytest.approx(0.0128, abs=0.0001) def test_ex6_conversion_script(self): # taux de capitalisation 4,75 % : la valeur en bloc monte et la prime devient négative v_bloc_475 = cas.valeur_bloc(taux=0.0475)["valeur"] assert v_bloc_475 == pytest.approx(cas.valeur_bloc()["noi"] / 0.0475) and v_bloc_475 == pytest.approx(1737546, abs=1) assert cas.faisabilite()["v_conv"] - v_bloc_475 == pytest.approx(-44742, abs=1) # rotation de 30 % et décote de 10 % assert cas.faisabilite(0.30, 0.10)["v_conv"] == pytest.approx(1893849, abs=1) # sans coûts d'individualisation (−120 000 $ à t = 0) : la VAN monte exactement de 120 000 $ couts = dict(cas.COUTS_CONVERSION); couts.pop("Individualisation des services et mise aux normes (8 logements)") assert cas.faisabilite(couts=couts)["v_conv"] - cas.faisabilite()["v_conv"] == pytest.approx(120000, abs=1e-6) def test_ex7_indivision(self): V = 1050000.0; parts = 3 * (V / 3) * (1 - 0.12) assert parts == pytest.approx(924000) fractions = 3 * 370000.0; couts = 60000.0 assert fractions - parts - couts == pytest.approx(126000) and (fractions - parts - couts) / 3 == pytest.approx(42000)