fix: distingue un lot reloué d'un lot sorti de la gestion
La fiche du lot 15 annonçait « dernier loyer connu, arrêté après févr. 26 » alors qu'un locataire y est entré le 8 juillet — et la courbe juste dessous, où la barre de juillet est bien là, la contredisait. Le loyer en vigueur se lit sur les paliers, et un mois facturé au seul prorata n'en ouvre pas : celui d'avant s'arrête donc à la dernière mensualité pleine, quatre mois avant le nouveau bail. Ne rien lire après lui confond deux états que le compte rendu sépare : un lot qui n'est plus loué, et un lot entre deux locataires. La distinction tient à la frontière qui sert déjà à séparer loyer plein et prorata, la période portée par le compte rendu : - un prorata qui finit le dernier jour du mois sans partir du premier facture la fin du mois : quelqu'un est entré, le bail court encore après ; - un prorata qui part du premier sans l'atteindre facture le début : le bail s'arrête là ; - un montant négatif ne fait ni l'un ni l'autre. Un avoir porte parfois la période exacte de la ligne qu'il annule, et le lire comme une entrée inventerait une relocation là où rien n'a été loué. L'entrée l'emporte sur la sortie parce qu'elle vient après : ce lot voit son locataire partir le 10 mars et le suivant arriver le 8 juillet, et c'est le second qui décrit son état. Le montant affiché ne bouge pas — le nouveau bail n'a été facturé qu'au prorata, et en tirer un loyer mensuel l'inventerait. Seul ce qu'on en dit change : « dernier loyer plein · nouveau bail depuis juil. 26 ». Sans relocation, la sortie date enfin la fin de la location au mois où elle a lieu : « arrêté en cours de mars 26 » plutôt qu'« arrêté après févr. 26 », qui l'avançait d'un mois. Sur le parc : 31 lots loués, 1 relouté, 1 réellement arrêté. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -152,6 +152,40 @@ function formatM2(valeur) {
|
|||||||
return `${valeur.toFixed(2).replace('.', ',')} €/m²`
|
return `${valeur.toFixed(2).replace('.', ',')} €/m²`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ce que la tuile de tête a le droit d'affirmer. Un palier qui s'arrête ne
|
||||||
|
// suffit pas à dire qu'un lot est sorti de la gestion : entre deux baux, le
|
||||||
|
// compte rendu ne porte qu'un prorata, et le lot est bel et bien reloué.
|
||||||
|
const etatDuBail = computed(() => {
|
||||||
|
const vigueur = props.loyer.en_vigueur
|
||||||
|
if (!vigueur) return { libelle: 'Loyer', classe: 'text-gray-500', detail: '' }
|
||||||
|
|
||||||
|
if (vigueur.toujours_loue) {
|
||||||
|
return {
|
||||||
|
libelle: 'Loyer en vigueur',
|
||||||
|
classe: 'text-green-400',
|
||||||
|
detail: `depuis ${formatMoisAnnee(vigueur.depuis)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vigueur.reloue_depuis) {
|
||||||
|
// Le montant reste celui du bail d'avant : le nouveau n'a encore été
|
||||||
|
// facturé qu'au prorata, et en tirer un loyer mensuel l'inventerait.
|
||||||
|
return {
|
||||||
|
libelle: 'Dernier loyer plein',
|
||||||
|
classe: 'text-gray-400',
|
||||||
|
detail: `nouveau bail depuis ${formatMoisAnnee(vigueur.reloue_depuis)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
libelle: 'Dernier loyer connu',
|
||||||
|
classe: 'text-gray-400',
|
||||||
|
detail: vigueur.sortie_en
|
||||||
|
? `arrêté en cours de ${formatMoisAnnee(vigueur.sortie_en)}`
|
||||||
|
: `arrêté après ${formatMoisAnnee(vigueur.mois)}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const tuiles = computed(() => {
|
const tuiles = computed(() => {
|
||||||
const vigueur = props.loyer.en_vigueur
|
const vigueur = props.loyer.en_vigueur
|
||||||
|
|
||||||
@@ -166,14 +200,12 @@ const tuiles = computed(() => {
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
libelle: vigueur.toujours_loue ? 'Loyer en vigueur' : 'Dernier loyer connu',
|
libelle: etatDuBail.value.libelle,
|
||||||
valeur: formatMontant(vigueur.loyer),
|
valeur: formatMontant(vigueur.loyer),
|
||||||
classe: vigueur.toujours_loue ? 'text-green-400' : 'text-gray-400',
|
classe: etatDuBail.value.classe,
|
||||||
// Un lot sorti de la gestion garde un loyer affiché : sans cette mention,
|
// Un lot sorti de la gestion garde un loyer affiché : sans cette mention,
|
||||||
// il se lirait comme une recette courante.
|
// il se lirait comme une recette courante.
|
||||||
detail: vigueur.toujours_loue
|
detail: etatDuBail.value.detail
|
||||||
? `depuis ${formatMoisAnnee(vigueur.depuis)}`
|
|
||||||
: `arrêté après ${formatMoisAnnee(vigueur.mois)}`
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
libelle: 'Au m²',
|
libelle: 'Au m²',
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ from ...database.models import (
|
|||||||
)
|
)
|
||||||
from ...services.loyers import (
|
from ...services.loyers import (
|
||||||
EFFECTIF_MEDIANE_FIABLE,
|
EFFECTIF_MEDIANE_FIABLE,
|
||||||
|
MoisLoue,
|
||||||
loyer_au_m2,
|
loyer_au_m2,
|
||||||
mediane,
|
mediane,
|
||||||
mois_de,
|
mois_de,
|
||||||
@@ -246,7 +247,19 @@ class LoyerEnVigueur(BaseModel):
|
|||||||
#: Le dernier compte rendu de l'immeuble porte encore ce loyer. Faux pour
|
#: Le dernier compte rendu de l'immeuble porte encore ce loyer. Faux pour
|
||||||
#: un lot dont le bail s'est arrêté : son dernier loyer est une archive, et
|
#: un lot dont le bail s'est arrêté : son dernier loyer est une archive, et
|
||||||
#: l'afficher comme courant ferait croire à une recette qui n'existe plus.
|
#: l'afficher comme courant ferait croire à une recette qui n'existe plus.
|
||||||
|
#:
|
||||||
|
#: Faux aussi pendant une relocation, où le loyer plein d'avant n'a plus
|
||||||
|
#: cours et celui d'après n'est pas encore facturé : `reloue_depuis` sépare
|
||||||
|
#: alors ce lot d'un lot réellement sorti de la gestion.
|
||||||
toujours_loue: bool = True
|
toujours_loue: bool = True
|
||||||
|
#: Mois d'un prorata d'entrée postérieur à ce loyer : un nouveau bail a
|
||||||
|
#: commencé, et seuls les jours qu'il couvre sont facturés. Sans ce champ,
|
||||||
|
#: la fiche annoncerait un loyer « arrêté » sur un lot qui vient d'être
|
||||||
|
#: reloué.
|
||||||
|
reloue_depuis: str | None = None
|
||||||
|
#: Mois d'un prorata de sortie postérieur à ce loyer, quand aucune entrée
|
||||||
|
#: ne suit : la location s'arrête en cours de mois, pas à la fin du palier.
|
||||||
|
sortie_en: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class PointParc(BaseModel):
|
class PointParc(BaseModel):
|
||||||
@@ -609,6 +622,33 @@ def _mois_de_fin(ligne) -> str | None:
|
|||||||
return mois_de(borne) if borne is not None else None
|
return mois_de(borne) if borne is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
def _apres_le_palier(
|
||||||
|
serie: list[MoisLoue], mois_fin: str
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
"""Ce que les mois postérieurs au dernier loyer plein disent du bail.
|
||||||
|
|
||||||
|
Un palier qui s'arrête ne dit pas que le lot est vide : entre deux baux, le
|
||||||
|
compte rendu ne porte qu'un prorata, et un mois de transition n'ouvre pas de
|
||||||
|
palier. Lu sans lui, un lot reloué le 8 du mois passe pour sorti de la
|
||||||
|
gestion.
|
||||||
|
|
||||||
|
L'entrée l'emporte sur la sortie parce qu'elle vient après : un locataire
|
||||||
|
part le 10 mars, un autre entre le 8 juillet, et c'est le second qui décrit
|
||||||
|
l'état du lot. Sans entrée, la dernière sortie donne le mois où la location
|
||||||
|
s'arrête vraiment — plus tard que la fin du palier, qu'elle déborde.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
`(reloue_depuis, sortie_en)`, chacun `None` quand rien ne l'établit.
|
||||||
|
"""
|
||||||
|
suivants = [point for point in serie if point.mois > mois_fin]
|
||||||
|
entrees = [point.mois for point in suivants if point.entree]
|
||||||
|
if entrees:
|
||||||
|
return entrees[-1], None
|
||||||
|
|
||||||
|
sorties = [point.mois for point in suivants if point.sortie]
|
||||||
|
return None, sorties[-1] if sorties else None
|
||||||
|
|
||||||
|
|
||||||
def _loyer(session: Session, lot: Lot, periode: Periode) -> LotLoyer:
|
def _loyer(session: Session, lot: Lot, periode: Periode) -> LotLoyer:
|
||||||
"""Le loyer du lot mois par mois, son niveau actuel et sa place au m².
|
"""Le loyer du lot mois par mois, son niveau actuel et sa place au m².
|
||||||
|
|
||||||
@@ -682,6 +722,8 @@ def _loyer(session: Session, lot: Lot, periode: Periode) -> LotLoyer:
|
|||||||
)
|
)
|
||||||
).scalar()
|
).scalar()
|
||||||
|
|
||||||
|
reloue_depuis, sortie_en = _apres_le_palier(serie.mois, courant.mois_fin)
|
||||||
|
|
||||||
en_vigueur = LoyerEnVigueur(
|
en_vigueur = LoyerEnVigueur(
|
||||||
mois=courant.mois_fin,
|
mois=courant.mois_fin,
|
||||||
loyer=courant.loyer,
|
loyer=courant.loyer,
|
||||||
@@ -690,6 +732,8 @@ def _loyer(session: Session, lot: Lot, periode: Periode) -> LotLoyer:
|
|||||||
precedent=precedent,
|
precedent=precedent,
|
||||||
variation_pct=variation(precedent, courant.loyer),
|
variation_pct=variation(precedent, courant.loyer),
|
||||||
toujours_loue=dernier_cr is None or courant.mois_fin >= mois_de(dernier_cr),
|
toujours_loue=dernier_cr is None or courant.mois_fin >= mois_de(dernier_cr),
|
||||||
|
reloue_depuis=reloue_depuis,
|
||||||
|
sortie_en=sortie_en,
|
||||||
)
|
)
|
||||||
parc = _comparaison(session, lot, courant.mois_fin, en_vigueur.loyer_m2)
|
parc = _comparaison(session, lot, courant.mois_fin, en_vigueur.loyer_m2)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ aucun mois ne peut alors le revendiquer.
|
|||||||
|
|
||||||
Un mois sans loyer plein mais avec un prorata n'est donc pas une vacance : c'est
|
Un mois sans loyer plein mais avec un prorata n'est donc pas une vacance : c'est
|
||||||
un mois de transition, et la fiche le distingue au lieu de le confondre avec un
|
un mois de transition, et la fiche le distingue au lieu de le confondre avec un
|
||||||
trou.
|
trou. La même frontière dit dans quel sens il penche : un prorata qui court
|
||||||
|
jusqu'au dernier jour du mois ouvre un bail, un prorata qui part du premier sans
|
||||||
|
l'atteindre en ferme un. C'est ce qui sépare un lot reloué le 8 d'un lot sorti
|
||||||
|
de la gestion, là où le palier de loyer s'arrête dans les deux cas.
|
||||||
|
|
||||||
Le mètre carré vient de la fiche saisie (`lot_caracteristiques.surface`), pas
|
Le mètre carré vient de la fiche saisie (`lot_caracteristiques.surface`), pas
|
||||||
des comptes rendus qui l'ignorent. Sans surface, le ratio vaut ``None`` et non
|
des comptes rendus qui l'ignorent. Sans surface, le ratio vaut ``None`` et non
|
||||||
@@ -91,6 +94,32 @@ def mois_entiers(debut: date | None, fin: date | None) -> list[str]:
|
|||||||
return couverts
|
return couverts
|
||||||
|
|
||||||
|
|
||||||
|
def est_entree(debut: date, fin: date, loyers: float | None) -> bool:
|
||||||
|
"""Vrai si ce prorata ouvre un bail qui court encore le mois suivant.
|
||||||
|
|
||||||
|
Un prorata qui s'arrête au dernier jour du mois sans avoir commencé le
|
||||||
|
premier facture la fin du mois : quelqu'un est entré en cours de route. La
|
||||||
|
location ne s'arrête donc pas là, même si aucun loyer plein ne suit encore.
|
||||||
|
|
||||||
|
Un montant négatif est écarté : un avoir annule une facturation, il
|
||||||
|
n'ouvre pas un bail — et il porte parfois la même période qu'elle.
|
||||||
|
"""
|
||||||
|
if not loyers or loyers <= 0:
|
||||||
|
return False
|
||||||
|
return debut.day != 1 and fin.day == monthrange(fin.year, fin.month)[1]
|
||||||
|
|
||||||
|
|
||||||
|
def est_sortie(debut: date, fin: date, loyers: float | None) -> bool:
|
||||||
|
"""Vrai si ce prorata ferme un bail en cours de mois.
|
||||||
|
|
||||||
|
Miroir de `est_entree` : partir du premier jour sans atteindre le dernier,
|
||||||
|
c'est facturer le début du mois et s'arrêter.
|
||||||
|
"""
|
||||||
|
if not loyers or loyers <= 0:
|
||||||
|
return False
|
||||||
|
return debut.day == 1 and fin.day != monthrange(fin.year, fin.month)[1]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class MoisLoue:
|
class MoisLoue:
|
||||||
"""Ce qu'un mois a été facturé, charges et proratas à part."""
|
"""Ce qu'un mois a été facturé, charges et proratas à part."""
|
||||||
@@ -109,6 +138,12 @@ class MoisLoue:
|
|||||||
prorata: float | None = None
|
prorata: float | None = None
|
||||||
#: Vrai quand le loyer vient d'une ligne pluri-mensuelle répartie.
|
#: Vrai quand le loyer vient d'une ligne pluri-mensuelle répartie.
|
||||||
reparti: bool = False
|
reparti: bool = False
|
||||||
|
#: Vrai quand un prorata du mois court jusqu'à son dernier jour sans partir
|
||||||
|
#: du premier : un bail commence en cours de mois et continue après lui.
|
||||||
|
entree: bool = False
|
||||||
|
#: Vrai quand un prorata du mois part de son premier jour sans l'achever :
|
||||||
|
#: un bail s'arrête en cours de mois.
|
||||||
|
sortie: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def en_transition(self) -> bool:
|
def en_transition(self) -> bool:
|
||||||
@@ -161,6 +196,8 @@ def repartir(lignes) -> Serie:
|
|||||||
if debut is not None and fin is not None and mois_de(debut) == mois_de(fin):
|
if debut is not None and fin is not None and mois_de(debut) == mois_de(fin):
|
||||||
point = cumul.setdefault(mois_de(debut), MoisLoue(mois=mois_de(debut)))
|
point = cumul.setdefault(mois_de(debut), MoisLoue(mois=mois_de(debut)))
|
||||||
point.prorata = (point.prorata or 0.0) + (loyers or 0.0)
|
point.prorata = (point.prorata or 0.0) + (loyers or 0.0)
|
||||||
|
point.entree = point.entree or est_entree(debut, fin, loyers)
|
||||||
|
point.sortie = point.sortie or est_sortie(debut, fin, loyers)
|
||||||
else:
|
else:
|
||||||
ecartees.append(
|
ecartees.append(
|
||||||
LigneEcartee(
|
LigneEcartee(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ regrouper des lignes que le compte rendu a émises séparément.
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from plesna_gerance.database.models import (
|
from plesna_gerance.database.models import (
|
||||||
|
Document,
|
||||||
Immeuble,
|
Immeuble,
|
||||||
Locataire,
|
Locataire,
|
||||||
Lot,
|
Lot,
|
||||||
@@ -616,3 +617,151 @@ def test_la_comparaison_compte_les_lots_qu_elle_ne_peut_pas_voir(
|
|||||||
assert parc["mediane_immeuble"] is None
|
assert parc["mediane_immeuble"] is None
|
||||||
assert parc["nb_immeuble"] == 0
|
assert parc["nb_immeuble"] == 0
|
||||||
assert parc["sans_surface"] == 0
|
assert parc["sans_surface"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def bail_qui_change(db_session, sample_data):
|
||||||
|
"""Un lot dont le bail s'arrête en cours de mois, puis reprend au suivant.
|
||||||
|
|
||||||
|
C'est la chronologie du lot 15 du parc : deux mois pleins, une sortie le
|
||||||
|
10 mars, quatre mois vides, puis une entrée le 8 juillet facturée au seul
|
||||||
|
prorata. Le dernier compte rendu ne porte donc aucun loyer plein, alors que
|
||||||
|
le lot est bel et bien reloué.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compte_rendu(reference, date_cr, lignes):
|
||||||
|
return {
|
||||||
|
**sample_data,
|
||||||
|
"metadata": {
|
||||||
|
**sample_data["metadata"],
|
||||||
|
"document": {
|
||||||
|
"reference": reference,
|
||||||
|
"date": date_cr,
|
||||||
|
"type": "COMPTE RENDU DE GESTION",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"situation_locataires": [
|
||||||
|
{
|
||||||
|
"lot": {"numero": "01", "type": "Appartement"},
|
||||||
|
"locataire": {"nom": lignes["nom"]},
|
||||||
|
"lignes": [
|
||||||
|
{
|
||||||
|
"type": "loyer",
|
||||||
|
"periode": {
|
||||||
|
"debut": lignes["debut"],
|
||||||
|
"fin": lignes["fin"],
|
||||||
|
},
|
||||||
|
"loyers": lignes["loyers"],
|
||||||
|
"total": lignes["loyers"],
|
||||||
|
"regles": lignes["loyers"],
|
||||||
|
"impayes": 0.0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recapitulatif_operations": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
service = DatabaseService(db_session)
|
||||||
|
for reference, date_cr, lignes in [
|
||||||
|
(
|
||||||
|
"CR01",
|
||||||
|
"2026-01-26",
|
||||||
|
{
|
||||||
|
"nom": "SORTANT",
|
||||||
|
"debut": "2026-01-01",
|
||||||
|
"fin": "2026-01-31",
|
||||||
|
"loyers": 1447.05,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CR02",
|
||||||
|
"2026-02-26",
|
||||||
|
{
|
||||||
|
"nom": "SORTANT",
|
||||||
|
"debut": "2026-02-01",
|
||||||
|
"fin": "2026-02-28",
|
||||||
|
"loyers": 1447.05,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CR03",
|
||||||
|
"2026-03-23",
|
||||||
|
{
|
||||||
|
"nom": "SORTANT",
|
||||||
|
"debut": "2026-03-01",
|
||||||
|
"fin": "2026-03-10",
|
||||||
|
"loyers": 482.35,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"CR04",
|
||||||
|
"2026-07-28",
|
||||||
|
{
|
||||||
|
"nom": "ENTRANT",
|
||||||
|
"debut": "2026-07-08",
|
||||||
|
"fin": "2026-07-31",
|
||||||
|
"loyers": 1111.67,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]:
|
||||||
|
service.save_document(data=compte_rendu(reference, date_cr, lignes))
|
||||||
|
|
||||||
|
immeuble = db_session.query(Immeuble).filter(Immeuble.code == "IMM1").one()
|
||||||
|
lot = db_session.query(Lot).filter(Lot.immeuble_id == immeuble.id).one()
|
||||||
|
return lot
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_lot_reloue_au_prorata_n_est_pas_un_lot_arrete(api_client, bail_qui_change):
|
||||||
|
"""Le palier s'arrête en février, le lot est reloué en juillet.
|
||||||
|
|
||||||
|
Sans cette lecture, la fiche annonce « arrêté après févr. » sur un lot qui
|
||||||
|
vient de retrouver un locataire — et la courbe juste dessous, où la barre
|
||||||
|
de juillet est bien là, la contredit.
|
||||||
|
"""
|
||||||
|
vigueur = api_client.get(f"/api/lots/{bail_qui_change.id}/analyse").json()["loyer"][
|
||||||
|
"en_vigueur"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert vigueur["reloue_depuis"] == "2026-07"
|
||||||
|
assert vigueur["sortie_en"] is None
|
||||||
|
# Le montant reste celui du bail précédent : le nouveau n'a été facturé
|
||||||
|
# qu'au prorata, et en tirer un loyer mensuel l'inventerait.
|
||||||
|
assert vigueur["mois"] == "2026-02"
|
||||||
|
assert vigueur["loyer"] == 1447.05
|
||||||
|
assert vigueur["toujours_loue"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_une_sortie_en_cours_de_mois_deborde_le_dernier_palier(
|
||||||
|
api_client, db_session, bail_qui_change
|
||||||
|
):
|
||||||
|
"""Sans relocation, c'est la sortie qui date la fin de la location.
|
||||||
|
|
||||||
|
Le dernier loyer plein est celui de février, mais le lot est resté loué
|
||||||
|
jusqu'au 10 mars : dire « arrêté après févr. » avancerait la sortie d'un
|
||||||
|
mois.
|
||||||
|
"""
|
||||||
|
dernier = (
|
||||||
|
db_session.query(Document).order_by(Document.date.desc()).first()
|
||||||
|
)
|
||||||
|
db_session.delete(dernier)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
vigueur = api_client.get(f"/api/lots/{bail_qui_change.id}/analyse").json()["loyer"][
|
||||||
|
"en_vigueur"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert vigueur["sortie_en"] == "2026-03"
|
||||||
|
assert vigueur["reloue_depuis"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_loyer_qui_s_arrete_net_reste_un_loyer_arrete(api_client, donnees):
|
||||||
|
"""Aucun prorata après le dernier palier : rien ne nuance l'arrêt."""
|
||||||
|
_, lot = donnees
|
||||||
|
|
||||||
|
vigueur = api_client.get(f"/api/lots/{lot.id}/analyse").json()["loyer"][
|
||||||
|
"en_vigueur"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert vigueur["reloue_depuis"] is None
|
||||||
|
assert vigueur["sortie_en"] is None
|
||||||
|
|||||||
@@ -139,6 +139,57 @@ class TestRepartition:
|
|||||||
assert point.prorata == -1390.0
|
assert point.prorata == -1390.0
|
||||||
assert point.en_transition is False
|
assert point.en_transition is False
|
||||||
|
|
||||||
|
def test_un_prorata_qui_finit_le_mois_est_une_entree(self):
|
||||||
|
"""Lot 15 du parc : le locataire entre le 8 juillet. La location ne
|
||||||
|
s'arrête donc pas au dernier loyer plein, elle recommence."""
|
||||||
|
serie = repartir([ligne("2026-07-08", "2026-07-31", 1111.67)])
|
||||||
|
|
||||||
|
point = serie.mois[0]
|
||||||
|
assert point.entree is True
|
||||||
|
assert point.sortie is False
|
||||||
|
|
||||||
|
def test_un_prorata_qui_ouvre_le_mois_est_une_sortie(self):
|
||||||
|
"""Même lot, quatre mois plus tôt : le locataire part le 10 mars."""
|
||||||
|
serie = repartir([ligne("2026-03-01", "2026-03-10", 482.35)])
|
||||||
|
|
||||||
|
point = serie.mois[0]
|
||||||
|
assert point.sortie is True
|
||||||
|
assert point.entree is False
|
||||||
|
|
||||||
|
def test_un_mois_qui_change_de_locataire_porte_les_deux(self):
|
||||||
|
"""Sortie le 3, entrée le 14 : le mois ferme un bail et en ouvre un
|
||||||
|
autre, et c'est l'entrée qui dit que le lot reste loué."""
|
||||||
|
serie = repartir(
|
||||||
|
[
|
||||||
|
ligne("2025-03-01", "2025-03-03", 109.68),
|
||||||
|
ligne("2025-03-14", "2025-03-31", 622.2),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
point = serie.mois[0]
|
||||||
|
assert point.entree is True
|
||||||
|
assert point.sortie is True
|
||||||
|
|
||||||
|
def test_un_avoir_n_ouvre_ni_ne_ferme_de_bail(self):
|
||||||
|
"""Un avoir porte la période qu'il annule : lu comme une entrée, il
|
||||||
|
ferait croire à une relocation là où rien n'a été loué."""
|
||||||
|
serie = repartir(
|
||||||
|
[
|
||||||
|
ligne("2024-10-15", "2024-10-31", -700.0),
|
||||||
|
ligne("2024-11-18", "2024-11-18", -1390.0),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [point.entree for point in serie.mois] == [False, False]
|
||||||
|
assert [point.sortie for point in serie.mois] == [False, False]
|
||||||
|
|
||||||
|
def test_un_mois_plein_n_est_ni_entree_ni_sortie(self):
|
||||||
|
serie = repartir([ligne("2026-01-01", "2026-01-31", 1447.05)])
|
||||||
|
|
||||||
|
point = serie.mois[0]
|
||||||
|
assert point.entree is False
|
||||||
|
assert point.sortie is False
|
||||||
|
|
||||||
def test_une_regularisation_a_cheval_reste_hors_de_la_courbe(self):
|
def test_une_regularisation_a_cheval_reste_hors_de_la_courbe(self):
|
||||||
"""De mars à août sans couvrir un mois entier : l'étaler inventerait
|
"""De mars à août sans couvrir un mois entier : l'étaler inventerait
|
||||||
six demi-mois de loyer."""
|
six demi-mois de loyer."""
|
||||||
|
|||||||
Reference in New Issue
Block a user