koprogo_api/domain/comptabilite/regime_comptable.rs
1//! Le régime comptable applicable à une ACP.
2//!
3//! Art. 3.89 § 5, 15° — le syndic est chargé :
4//!
5//! > « de tenir les comptes de l'association des copropriétaires de manière
6//! > claire, précise et détaillée suivant le **plan comptable minimum
7//! > normalisé** à établir par le Roi. Toute copropriété **de moins de vingt
8//! > lots à l'exclusion des caves, des garages et parkings** est autorisée à
9//! > tenir une **comptabilité simplifiée** reflétant au minimum les recettes
10//! > et les dépenses, la situation de trésorerie ainsi que les mouvements des
11//! > disponibilités. »
12//!
13//! Le plan comptable minimum normalisé est celui de l'arrêté royal du
14//! 12 juillet 2012, implémenté depuis longtemps ici. Ce qui manquait, c'est la
15//! **question préalable** : cette ACP y est-elle seulement tenue ?
16//!
17//! Deux pièges dans le décompte, et ils vont dans le même sens — celui de
18//! surestimer le nombre de lots et d'imposer une comptabilité complète à une
19//! petite copropriété qui a le droit de faire plus simple :
20//!
21//! 1. **caves, garages et parkings sont exclus**. Ce n'est pas le nombre de
22//! lots de l'acte de base : un immeuble de 15 appartements avec 15 caves et
23//! 20 parkings compte 15 lots au sens de cet article, pas 50 ;
24//! 2. **« moins de vingt »** est strict. Vingt lots imposent le plan complet ;
25//! dix-neuf ne l'imposent pas.
26//!
27//! Le régime n'est jamais saisi à la main : il se **dérive** du décompte. Une
28//! copropriété qui franchit le seuil change de régime parce qu'elle a vendu un
29//! lot, pas parce qu'un syndic l'a décidé.
30//!
31//! Voir issue #746.
32
33pub use crate::domain::copropriete::decompte_des_lots::lots_comptes;
34use crate::domain::copropriete::unit::UnitType;
35
36/// Le régime comptable auquel une ACP est tenue.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum RegimeComptable {
39 /// Plan comptable minimum normalisé (AR du 12/07/2012), obligatoire dès
40 /// vingt lots.
41 Complet,
42 /// Comptabilité simplifiée : recettes, dépenses, situation de trésorerie,
43 /// mouvements des disponibilités.
44 Simplifie,
45}
46
47impl RegimeComptable {
48 /// Le seuil légal, en lots comptés au sens de l'Art. 3.89 § 5, 15°.
49 ///
50 /// Repris du décompte de `copropriete` : un lot est une notion de l'acte
51 /// de base, pas une notion comptable. Le régime **dérive** du décompte.
52 pub const SEUIL_LEGAL: usize = crate::domain::copropriete::decompte_des_lots::SEUIL_LEGAL;
53
54 pub fn est_simplifie(&self) -> bool {
55 matches!(self, Self::Simplifie)
56 }
57}
58
59impl std::fmt::Display for RegimeComptable {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 Self::Complet => write!(f, "plan comptable minimum normalisé (AR du 12/07/2012)"),
63 Self::Simplifie => write!(f, "comptabilité simplifiée (Art. 3.89 § 5, 15°)"),
64 }
65 }
66}
67
68/// Le décompte légal, réexporté depuis `copropriete`.
69///
70/// La règle ne vit pas ici : un lot est une notion de l'acte de base
71/// (Art. 3.85 § 1er), pas une notion comptable, et l'Art. 3.90 § 1er emploie
72/// exactement le même décompte pour le conseil de copropriété. Deux
73/// définitions du même comptage finiraient par diverger.
74///
75/// Réexporté pour que le lecteur du régime comptable trouve la règle sans
76/// changer de contexte.
77pub use crate::domain::copropriete::decompte_des_lots::compte_dans_le_seuil;
78
79/// Le régime auquel une ACP est tenue, dérivé de ses lots.
80pub fn regime_applicable(natures: &[UnitType]) -> RegimeComptable {
81 if lots_comptes(natures) < RegimeComptable::SEUIL_LEGAL {
82 RegimeComptable::Simplifie
83 } else {
84 RegimeComptable::Complet
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn lots(appartements: usize, caves: usize, parkings: usize, commerces: usize) -> Vec<UnitType> {
93 let mut v = vec![UnitType::Apartment; appartements];
94 v.extend(vec![UnitType::Cellar; caves]);
95 v.extend(vec![UnitType::Parking; parkings]);
96 v.extend(vec![UnitType::Commercial; commerces]);
97 v
98 }
99
100 #[test]
101 fn happy_une_petite_copropriete_peut_tenir_une_comptabilite_simplifiee() {
102 let regime = regime_applicable(&lots(12, 0, 0, 0));
103 assert_eq!(regime, RegimeComptable::Simplifie);
104 }
105
106 #[test]
107 fn happy_une_grande_copropriete_est_tenue_au_plan_normalise() {
108 let regime = regime_applicable(&lots(25, 0, 0, 0));
109 assert_eq!(regime, RegimeComptable::Complet);
110 }
111
112 /// Le piège du décompte : caves et parkings ne comptent pas.
113 ///
114 /// Cet immeuble a cinquante lots à l'acte de base et quinze au sens de
115 /// l'article. Le compter à cinquante lui imposerait une comptabilité
116 /// complète à laquelle la loi ne l'oblige pas.
117 #[test]
118 fn happy_caves_et_parkings_sortent_du_decompte() {
119 let natures = lots(15, 15, 20, 0);
120 assert_eq!(natures.len(), 50, "cinquante lots à l'acte de base");
121 assert_eq!(lots_comptes(&natures), 15, "quinze au sens de l'article");
122 assert_eq!(regime_applicable(&natures), RegimeComptable::Simplifie);
123 }
124
125 /// @edge — « moins de vingt » est strict : la borne est exclusive.
126 #[test]
127 fn edge_dix_neuf_lots_restent_simplifies() {
128 assert_eq!(
129 regime_applicable(&lots(19, 30, 40, 0)),
130 RegimeComptable::Simplifie
131 );
132 }
133
134 /// @edge — et vingt lots basculent.
135 #[test]
136 fn edge_vingt_lots_imposent_le_plan_normalise() {
137 assert_eq!(
138 regime_applicable(&lots(20, 0, 0, 0)),
139 RegimeComptable::Complet
140 );
141 }
142
143 /// Les commerces comptent : l'article n'exclut que caves, garages et
144 /// parkings.
145 #[test]
146 fn happy_les_commerces_comptent_dans_le_seuil() {
147 assert_eq!(
148 regime_applicable(&lots(18, 0, 0, 2)),
149 RegimeComptable::Complet
150 );
151 }
152
153 /// @security — un lot de nature indéterminée compte.
154 ///
155 /// Le doute profite à l'obligation la plus stricte : se tromper dans ce
156 /// sens fait tenir une comptabilité plus détaillée que nécessaire, alors
157 /// que l'erreur inverse met l'ACP en défaut.
158 #[test]
159 fn security_un_lot_de_nature_indeterminee_compte() {
160 let mut natures = lots(19, 0, 0, 0);
161 natures.push(UnitType::Other);
162 assert_eq!(regime_applicable(&natures), RegimeComptable::Complet);
163 }
164
165 /// @negative — une ACP sans lot encodé n'est tenue à rien de complet.
166 ///
167 /// Le cas se produit avant que le SuperAdmin ait fini d'encoder l'acte de
168 /// base. Il ne doit pas déclencher une obligation comptable sur du vide.
169 #[test]
170 fn negative_une_acp_sans_lot_reste_simplifiee() {
171 assert_eq!(regime_applicable(&[]), RegimeComptable::Simplifie);
172 }
173}