SPB Git
13.7 KB · 223 lines python
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 TestFinancement:103    def test_divise_vs_indivise(self, res):104        fa, fi = res["fin_divise"], res["fin_indivise"]105        assert fa["mise_de_fonds"] == 19000 and fa["pret"] == 361000106        assert fa["paiement"] == pytest.approx(1922, abs=1) and fa["cout_mensuel"] == pytest.approx(3049, abs=1)107        assert fa["tam"] == pytest.approx(0.0612) and fa["paiement_tam"] == pytest.approx(2336, abs=1)108        assert fi["mise_de_fonds"] == 76000 and fi["pret"] == 304000 and fi["paiement"] == pytest.approx(1619, abs=1)109        assert fi["mise_de_fonds"] - fa["mise_de_fonds"] == 57000110        # convention canadienne : composition semestrielle (traite.finance.Hypotheque)111        h = fin.Hypotheque(361000, 0.0412, 300)112        assert fa["paiement"] == pytest.approx(h.paiement) and h.solde(300) == pytest.approx(0.0, abs=1e-6)113        # droits de mutation 2026 pour 380 000 $ (cités dans le texte)114        dm = 0.005 * 62900 + 0.01 * (315000 - 62900) + 0.015 * (380000 - 315000)115        assert dm == pytest.approx(3810.5, abs=0.01)116117118class TestConversion:119    def test_bloc(self, res):120        b = res["faisabilite"]["bloc"]121        assert b["rbp"] == 146700 and b["rbe"] == pytest.approx(142299, abs=1) and b["noi"] == pytest.approx(82533, abs=1)122        assert b["valeur"] == pytest.approx(1572065, abs=1) and b["valeur"] == pytest.approx(b["noi"] / 0.0525)123        assert b["mrb"] == pytest.approx(10.7, abs=0.05)124125    def test_valeurs_libres(self, res):126        cal = res["faisabilite"]["calendrier"]; v = cal["valeurs"]127        assert v["F4"] == pytest.approx(396431, abs=1) and v["F1"] == pytest.approx(276481, abs=1) and v["F6"] == pytest.approx(430539, abs=1)128        assert cal["somme_libre"] == pytest.approx(2802191, abs=1)129        # F4 libre de passif = F4 du sujet + part du déficit (cohérence des deux blocs)130        assert v["F4"] - cas.SUJET["passif"] == pytest.approx(res["grille"]["valeur_modele_sujet"], abs=1e-6)131        assert cal["n_libres_an1"] == 4 and cal["n_occupes"] == 4 and cal["V_occ"] == pytest.approx(1246418, abs=1)132133    def test_calendrier_identite(self, res):134        fb = res["faisabilite"]; cal = fb["calendrier"]; H = cas.HORIZON_CONV; rho, d = cas.ROTATION, cas.DECOTE_OCCUPE135        libres = cal["somme_libre"] - cal["V_occ"]136        attendu = libres + cal["V_occ"] * (1 - (1 - rho) ** (H - 1)) + cal["V_occ"] * (1 - rho) ** (H - 1) * (1 - d)137        assert cal["produit"].sum() == pytest.approx(attendu, abs=1e-6)138        assert cal["produit"][1] == pytest.approx(1705343, abs=1) and cal["produit"][6] == pytest.approx(526219, abs=1)139140    def test_faisabilite(self, res):141        fb = res["faisabilite"]142        assert fb["couts"] == 305000 and fb["flux"][0] == -305000143        assert fb["flux"][1] == pytest.approx(1595097, abs=1) and fb["flux"][6] == pytest.approx(495304, abs=1)144        assert fb["v_conv"] == pytest.approx(1692804, abs=1) and fb["prime"] == pytest.approx(120739, abs=1)145        assert fb["marge_sur_bloc"] == pytest.approx(0.077, abs=0.001)146        assert fb["frais_vente_total"] == pytest.approx(106825, abs=1) and fb["portage_total"] == pytest.approx(106784, abs=1)147        assert fb["somme_nette_non_actualisee"] == pytest.approx(2152027, abs=1)148        # V_conv est la VAN des flux au taux du promoteur (traite.finance.van)149        assert fb["v_conv"] == pytest.approx(fin.van(list(fb["flux"]), 0.10), abs=1e-6)150        assert res["faisabilite_occ"]["v_conv"] == pytest.approx(1884780, abs=1)151152    def test_sensibilite_conversion(self, res):153        sc = res["sens_conv"]; g = sc["grille"]154        assert g[0.20][2] == pytest.approx(1692804, abs=1)155        assert g[0.10][0] == pytest.approx(1636219, abs=1) and g[0.30][4] == pytest.approx(1871145, abs=1)156        assert sc["vente_occupee_an1"][0.20] == pytest.approx(1884780, abs=1)157        for d in sc["decotes"]:158            assert all(np.diff(g[d]) > 0)        # plus la rotation est rapide, plus la conversion vaut159        for j in range(len(sc["rotations"])):160            assert g[0.10][j] > g[0.20][j] > g[0.30][j]161        assert res["seuil_prix"] == pytest.approx(0.937, abs=0.001)162        assert cas.faisabilite(r=0.08)["v_conv"] == pytest.approx(1768600, abs=1)163        assert cas.faisabilite(r=0.12)["v_conv"] == pytest.approx(1623172, abs=1)164165166class TestExercices:167    def test_ex1_charges(self):168        s = np.array([60, 75, 75, 90, 90, 110.0]); q = s / s.sum()169        assert s.sum() == 500 and q[3] == pytest.approx(0.18)170        total = 36000 + 15000 + 4000171        assert q[3] * total == pytest.approx(9900) and q[3] * total / 12 == pytest.approx(825)172        assert q[3] * 15000 == pytest.approx(2700) and q[3] * 36000 == pytest.approx(6480)173174    def test_ex2_attestation(self):175        q = 0.085; deficit = 180000 - 62000; part_speciale = q * 40000; part_deficit = q * deficit176        assert deficit == 118000 and part_speciale == pytest.approx(3400) and part_deficit == pytest.approx(10030)177        assert part_speciale + part_deficit == pytest.approx(13430)178        assert q * (19500 - 9000) == pytest.approx(892.5)179180    def test_ex3_capitalisation_charges(self):181        dC = (62 - 48) * 90182        assert dC == 1260183        assert dC / 0.08 == pytest.approx(15750) and dC / 0.06 == pytest.approx(21000) and dC / 0.12 == pytest.approx(10500)184        assert dC / (0.05 - 0.025) == pytest.approx(50400)185186    def test_ex4_grille(self):187        p_t = 342000 * 1.02 ** (5 / 12)188        assert p_t == pytest.approx(344834, abs=1)189        A = {"marché": p_t - 342000, "étage": p_t * 0.02, "orientation": p_t * (-0.03 - 0.03), "stationnement": -28000.0, "superficie": -4 * 2190.0}190        pa = cp.prix_ajuste_additif(342000, A)191        assert A["étage"] == pytest.approx(6897, abs=1) and A["orientation"] == pytest.approx(-20690, abs=1) and A["superficie"] == -8760192        assert pa == pytest.approx(294280, abs=1)193        bn = cp.ajustement_brut_net(A, 342000)194        assert bn["brut_pct"] == pytest.approx(0.196, abs=0.001)195196    def test_ex5_locative(self):197        v = 380000.0; noi_cible = 0.04 * v198        assert noi_cible == 15200199        rbe = (noi_cible + 3300 + 600 + 10218) / 0.95200        assert rbe == pytest.approx(30861, abs=1)201        rbp = rbe / (1 - 0.021)202        assert rbp == pytest.approx(31523, abs=1) and rbp / 12 == pytest.approx(2627, abs=1)203        # au loyer de 1 700 $ : RNE et taux comme dans l'exemple, sur 380 000 $204        cl = cas.copro_locative(v)205        assert cl["noi"] == pytest.approx(4855, abs=1) and cl["taux_implicite"] == pytest.approx(0.0128, abs=0.0001)206207    def test_ex6_conversion_script(self):208        # taux de capitalisation 4,75 % : la valeur en bloc monte et la prime devient négative209        v_bloc_475 = cas.valeur_bloc(taux=0.0475)["valeur"]210        assert v_bloc_475 == pytest.approx(cas.valeur_bloc()["noi"] / 0.0475) and v_bloc_475 == pytest.approx(1737546, abs=1)211        assert cas.faisabilite()["v_conv"] - v_bloc_475 == pytest.approx(-44742, abs=1)212        # rotation de 30 % et décote de 10 %213        assert cas.faisabilite(0.30, 0.10)["v_conv"] == pytest.approx(1893849, abs=1)214        # sans coûts d'individualisation (−120 000 $ à t = 0) : la VAN monte exactement de 120 000 $215        couts = dict(cas.COUTS_CONVERSION); couts.pop("Individualisation des services et mise aux normes (8 logements)")216        assert cas.faisabilite(couts=couts)["v_conv"] - cas.faisabilite()["v_conv"] == pytest.approx(120000, abs=1e-6)217218    def test_ex7_indivision(self):219        V = 1050000.0; parts = 3 * (V / 3) * (1 - 0.12)220        assert parts == pytest.approx(924000)221        fractions = 3 * 370000.0; couts = 60000.0222        assert fractions - parts - couts == pytest.approx(126000) and (fractions - parts - couts) / 3 == pytest.approx(42000)223
Files · b89896847ac1aae8938f8bc73852056ce0ce4556
Menu

Theme