Importer votre compte Rentila dans Locameublé
L'essentiel
- Rentila ne fait pas la liasse 2031 ni ses annexes 2033 : son site parle d'un bilan financier pour remplir votre déclaration (lu le 12 novembre 2026).
- L'API de Rentila est réservée aux comptes payants. L'export de cette page passe par votre onglet Rentila ouvert et prend environ 20 secondes.
- Le fichier est lu dans votre navigateur : biens, locataires, baux, loyers, dépenses, emprunts. Rien n'est envoyé tant que vous ne créez pas de compte.
- Le dépôt de garantie, que Rentila compte en revenu, va sur le bail et jamais dans le résultat. Les loyers passés marqués « Payé » sans paiement arrivent « attendus ».
- Les documents, contacts, garants, états des lieux, inventaires et baux vides ne passent pas encore.
Exportez votre compte Rentila en un fichier, voyez ce qu'il contient dans votre navigateur, puis reprenez-le dans Locameublé pour faire la liasse 2031 que Rentila ne fait pas.
Ramzi, loueur en meublé depuis 2021. Mis à jour le .
Ce que Rentila vous laisse exporter
Rentila tient bien la gestion : loyers, quittances, relances, révision. Pour l'impôt, son site parle d'un bilan financier « pour remplir votre déclaration ». Il ne produit ni la liasse 2031, ni ses annexes 2033, ni de FEC. La déclaration commandée depuis Rentila est faite par un service comptable, à qui vos transactions partent le 15 janvier.
Côté export, le bilan annuel sort en tableur XLSX, et 19 rapports s'exportent, dont les loyers encaissés (vu dans l'application le 9 novembre 2026). L'API, elle, est réservée aux comptes payants. L'export ci-dessous lit votre compte depuis votre onglet Rentila ouvert, par une liste fermée de pages, en environ 20 secondes. Il en fait un seul fichier .json.
Exporter, puis voir ce qui passera
Gratuit, sans compte. Le fichier est lu dans ce navigateur, rien n'est envoyé.
L'export se fait depuis un ordinateur. Le favori tourne dans le navigateur de l'ordinateur où vous êtes connecté à Rentila. Rouvrez cette page sur votre ordinateur.
-
Affichez la barre de favoris
Ctrl Maj B sur Windows, ⌘ Maj B sur Mac.
-
Glissez ce bouton dans la barre de favoris
Exporter de Rentila -
Ouvrez Rentila, connecté, puis cliquez sur le favori
Sur www.rentila.com. Le script lit votre compte sans rien y changer et télécharge un fichier .json sur votre ordinateur.
Le favori ne s'installe pas ? Collez le script dans la console
- Sur www.rentila.com, connecté, ouvrez la console : F12, ou ⌘ ⌥ J sur Mac.
- Chrome refuse le premier collage. Tapez
autoriser le collage(allow pastingsi Chrome est en anglais), puis Entrée. - Copiez le script ci-dessous, collez-le dans la console, puis Entrée.
void (async () => {
// Export Rentila vers Locameublé. Se lance sur www.rentila.com, connecté : rien ne part chez Locameublé, le fichier JSON se télécharge sur l'ordinateur.
// Lecture seule : chaque requête passe par lectureAutorisee(). Le seul POST génère un rapport, comme le bouton « Générer ».
const VERSION_EXPORT = 1;
const LECTURES = [
/^\/landlord\/(properties|tenants|leases|leases\/archive|contacts|payments|mydocuments|multilandlord|credits|bilan|reports|furniture|buildings|leases\/handovers)\?a=1(&[^#]*)?$/,
/^\/landlord\/(properties|tenants|contacts)\/\d+\/edit\?a=1$/,
/^\/landlord\/leases\/\d+\/edit\/\?step=1&a=1$/,
/^\/landlord\/payments\/\d+\/edit\?a=1$/,
/^\/landlord\/profile\/edit\?a=1$/,
/^\/landlord\/export\/\?biens=1&locations=1&locataires=1&finances=1&carnet=1&reservations=1&interventions=1&tasks=1&file_type=mo&archived=1?$/,
/^\/landlord\/reports\/download\/report_[\w]+\.xlsx\?format=csv$/,
/^\/landlord\/bilan\/download_csv\?year=\d{4}&MLandlordID=&BuildingID=0&taxregime=(LMNPReel)?$/,
/^\/landlord\/credits\/export\/csv\?(year=&&credit=[\w]+)?$/,
/^\/landlord\/mydocuments\/documents\.zip\?ids\[\]=\d+(&ids\[\]=\d+)*$/,
];
// Jamais d'envoi groupé de quittances : au-delà de 12, Rentila envoie un e-mail au locataire.
const INTERDIT =
/action=|send|delete|archive(?!d=|\?a=1)|\/new|handover\?|invite|regener|mdelete|refresh|download_quittances|update|insert|email/i;
/**
* @param {string} chemin
* @param {string} [methode]
*/
function lectureAutorisee(chemin, methode = "GET") {
if (methode === "POST") return chemin === "/landlord/reports/process";
if (methode !== "GET") return false;
return LECTURES.some((re) => re.test(chemin)) && !INTERDIT.test(chemin);
}
/**
* @typedef {Record<string, unknown>} Fiche
* @typedef {{ entete: Record<string, string>, colonnes: string[], lignes: (Record<string, string> | string[])[] }} Rapport
* @typedef {{ id: string, fiche: Fiche | null }} Objet
*/
async function exporterRentila() {
const retourSansTelecharger = Reflect.get(window, "__rentilaExportRetour") === true;
const boite = document.createElement("div");
boite.style.cssText =
"position:fixed;z-index:2147483647;right:16px;bottom:16px;max-width:380px;padding:14px 16px;background:#fff;color:#1a1a1a;border:1px solid #ccc;border-radius:10px;box-shadow:0 6px 24px rgba(0,0,0,.18);font:14px/1.4 system-ui,sans-serif";
/** @param {string} texte */
const dire = (texte) => {
boite.textContent = texte;
if (!boite.isConnected) document.body.appendChild(boite);
};
/** @param {string} texte */
const arreter = (texte) => {
dire(texte);
if (!retourSansTelecharger) alert(texte);
return { erreur: texte };
};
if (
!/^www\.rentila\.[a-z.]+$/.test(location.hostname) ||
!location.pathname.startsWith("/landlord")
) {
return arreter(
"Ce script se lance sur Rentila. Ouvrez https://www.rentila.com/landlord/, connectez-vous, puis relancez-le depuis cet onglet.",
);
}
class PasConnecte extends Error {}
/**
* @param {string} chemin
* @param {RequestInit} [options]
*/
const lire = async (chemin, options = {}) => {
if (!lectureAutorisee(chemin, options.method ?? "GET")) {
throw new Error("Adresse refusée par le garde-fou lecture seule : " + chemin);
}
const reponse = await fetch(chemin, { credentials: "include", ...options });
if (/\/register\//.test(reponse.url)) throw new PasConnecte();
return reponse;
};
/**
* @param {string} chemin
* @param {RequestInit} [options]
*/
const texte = async (chemin, options) => {
const t = await (await lire(chemin, options)).text();
if (t.length < 400 && /register\\?\/login/.test(t)) throw new PasConnecte();
return t;
};
/** @param {string} chemin */
const html = async (chemin) => new DOMParser().parseFromString(await texte(chemin), "text/html");
/** @param {string | null | undefined} s */
const propre = (s) => (s || "").replace(/\s+/g, " ").trim();
// Un formulaire d'édition Rentila lu champ par champ : nom vers valeur, et le libellé pour un choix.
/**
* @param {Document} doc
* @param {string} actionContient
* @returns {Fiche | null}
*/
const lireFormulaire = (doc, actionContient) => {
const form = [...doc.querySelectorAll("form")].find((f) =>
(f.getAttribute("action") || "").includes(actionContient),
);
if (!form) return null;
/** @type {Fiche} */
const champs = {};
/**
* @param {string} nom
* @param {unknown} valeur
*/
const ajouter = (nom, valeur) => {
if (!nom.endsWith("[]")) {
champs[nom] = valeur;
return;
}
const cle = nom.slice(0, -2);
const liste = champs[cle];
if (Array.isArray(liste)) liste.push(valeur);
else champs[cle] = [valeur];
};
for (const el of form.querySelectorAll("input,select,textarea")) {
if (
!(
el instanceof HTMLInputElement ||
el instanceof HTMLSelectElement ||
el instanceof HTMLTextAreaElement
)
)
continue;
const nom = el.name;
if (
!nom ||
/^files_template_|^submitButton$|^SignText|^smsverification|^verifPerson|^channel$/.test(
nom,
)
)
continue;
if (el instanceof HTMLSelectElement) {
const v = [...el.options]
.filter((o) => o.selected && o.value !== "")
.map((o) => ({ valeur: o.value, libelle: propre(o.textContent) }));
if (el.multiple || nom.endsWith("[]")) ajouter(nom, v);
else champs[nom] = v[0] || null;
} else if (el instanceof HTMLInputElement) {
if (["password", "file", "button", "submit"].includes(el.type)) continue;
if (el.type === "checkbox") {
if (!nom.endsWith("[]")) champs[nom] = el.checked;
else if (el.checked) ajouter(nom, el.value);
} else if (el.type === "radio") {
if (el.checked) champs[nom] = el.value;
} else ajouter(nom, el.value);
} else ajouter(nom, el.value);
}
return champs;
};
// Le tableau principal d'une liste Rentila : une ligne par objet, colonnes par en-tête, ids lus dans les liens.
/** @param {Document} doc */
const lireTableau = (doc) => {
const table = [...doc.querySelectorAll("table")].find(
(t) => t.querySelector("thead th") && t.querySelector("tbody tr"),
);
if (!table) return [];
// Une cellule d'en-tête sur 2 colonnes (case à cocher et Date) : le libellé va à la 2e.
const entetes = [...table.querySelectorAll("thead th")].flatMap((th) => [
...Array(Math.max(0, (th instanceof HTMLTableCellElement ? th.colSpan : 1) - 1)).fill(""),
propre(th.textContent),
]);
return [...table.querySelectorAll("tbody tr")].flatMap((tr) => {
const cellules = [...tr.children];
if (cellules.length < Math.max(2, entetes.length / 2)) return [];
/** @type {Record<string, string>} */
const colonnes = {};
cellules.forEach((td, i) => {
const cle = entetes[i] || "colonne_" + i;
if (cle !== "Actions") colonnes[cle] = propre(td.textContent);
});
const liens = [...tr.querySelectorAll("a[href]")].map((a) => a.getAttribute("href") || "");
return [
{
...colonnes,
_cellules: cellules.map((td) => propre(td.textContent)),
_liens: [...new Set(liens.filter((h) => /\d{3,}/.test(h) && !/action=/.test(h)))],
},
];
});
};
/**
* @param {string} t
* @param {RegExp} motif
*/
const idsDepuis = (t, motif) => [...new Set([...t.matchAll(motif)].map((m) => m[1]))];
// CSV Rentila : séparateur « ; », guillemets, un bloc d'en-tête (Rapport, Biens, Période) avant le tableau.
/** @param {string} t */
const lireCsv = (t) => {
/** @type {string[][]} */
const lignes = [];
/** @type {string[]} */
let ligne = [];
let champ = "";
let guillemets = false;
t = t.replace(/^\uFEFF/, "");
for (let i = 0; i < t.length; i++) {
const c = t[i];
if (guillemets) {
if (c === '"' && t[i + 1] === '"') {
champ += '"';
i++;
} else if (c === '"') guillemets = false;
else champ += c;
} else if (c === '"') guillemets = true;
else if (c === ";") {
ligne.push(champ);
champ = "";
} else if (c === "\n" || c === "\r") {
if (c === "\r" && t[i + 1] === "\n") i++;
ligne.push(champ);
lignes.push(ligne);
ligne = [];
champ = "";
} else champ += c;
}
if (champ || ligne.length) {
ligne.push(champ);
lignes.push(ligne);
}
return lignes.filter((l) => l.some((x) => x.trim() !== ""));
};
/**
* @param {string} t
* @returns {Rapport}
*/
const rapportCsv = (t) => {
const lignes = lireCsv(t);
const i = lignes.findIndex(
(l) => l.filter((x) => x.trim()).length >= 3 && !l.some((x) => /\d/.test(x)),
);
if (i < 0) return { entete: {}, colonnes: [], lignes };
const entete = Object.fromEntries(
lignes
.slice(0, i)
.filter((l) => l[0])
.map((l) => [l[0], l.slice(1).filter(Boolean).join(" ")]),
);
const colonnes = lignes[i];
const corps = lignes
.slice(i + 1)
.map((l) => Object.fromEntries(colonnes.map((c, k) => [c || "colonne_" + k, l[k] ?? ""])));
return { entete, colonnes, lignes: corps };
};
// XLSX sans bibliothèque : le répertoire central du ZIP, puis DecompressionStream.
/** @param {Uint8Array<ArrayBuffer>} octets */
const lireZip = (octets) => {
const v = new DataView(octets.buffer, octets.byteOffset, octets.byteLength);
let fin = octets.length - 22;
while (fin >= 0 && v.getUint32(fin, true) !== 0x06054b50) fin--;
if (fin < 0) throw new Error("ZIP illisible");
const nb = v.getUint16(fin + 10, true);
let p = v.getUint32(fin + 16, true);
/** @type {Record<string, () => Promise<Uint8Array<ArrayBuffer>>>} */
const fichiers = {};
for (let n = 0; n < nb; n++) {
const methode = v.getUint16(p + 10, true);
const taille = v.getUint32(p + 20, true);
const lNom = v.getUint16(p + 28, true);
const lExtra = v.getUint16(p + 30, true);
const lCom = v.getUint16(p + 32, true);
const local = v.getUint32(p + 42, true);
const nom = new TextDecoder().decode(octets.subarray(p + 46, p + 46 + lNom));
const debut = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
const brut = octets.subarray(debut, debut + taille);
fichiers[nom] = async () => {
if (methode === 0) return brut;
const flux = new Blob([brut]).stream().pipeThrough(new DecompressionStream("deflate-raw"));
return new Uint8Array(await new Response(flux).arrayBuffer());
};
p += 46 + lNom + lExtra + lCom;
}
return fichiers;
};
/** @param {Uint8Array<ArrayBuffer>} octets */
const lireXlsx = async (octets) => {
const zip = lireZip(octets);
/** @param {string} nom */
const xml = async (nom) =>
new DOMParser().parseFromString(
new TextDecoder().decode(await zip[nom]()),
"application/xml",
);
const partages = zip["xl/sharedStrings.xml"]
? [...(await xml("xl/sharedStrings.xml")).getElementsByTagName("si")].map((si) =>
[...si.getElementsByTagName("t")].map((t) => t.textContent).join(""),
)
: [];
const liens = Object.fromEntries(
[...(await xml("xl/_rels/workbook.xml.rels")).getElementsByTagName("Relationship")].map(
(r) => [r.getAttribute("Id") || "", r.getAttribute("Target") || ""],
),
);
/** @type {Record<string, string[][]>} */
const feuilles = {};
for (const f of (await xml("xl/workbook.xml")).getElementsByTagName("sheet")) {
const cible = (liens[f.getAttribute("r:id") || ""] || "").replace(/^\/?(xl\/)?/, "xl/");
const lignes = [...(await xml(cible)).getElementsByTagName("row")].map((row) => {
/** @type {string[]} */
const l = [];
for (const c of row.getElementsByTagName("c")) {
const col =
(c.getAttribute("r") || "")
.replace(/\d+/g, "")
.split("")
.reduce((a, ch) => a * 26 + ch.charCodeAt(0) - 64, 0) - 1;
const val =
c.getElementsByTagName("v")[0]?.textContent ??
[...c.getElementsByTagName("t")].map((t) => t.textContent).join("");
l[col] = c.getAttribute("t") === "s" ? partages[+val] : val;
}
return Array.from(l, (x) => x ?? "");
});
feuilles[f.getAttribute("name") || ""] = lignes;
}
return feuilles;
};
/**
* @param {string[][]} lignes
* @param {number} iEntete
*/
const enObjets = (lignes, iEntete) =>
lignes
.slice(iEntete + 1)
.filter((l) => l.some((x) => x !== ""))
.map((l) =>
Object.fromEntries(lignes[iEntete].map((c, k) => [c || "colonne_" + k, l[k] ?? ""])),
);
/** @param {Uint8Array} octets */
const enBase64 = (octets) => {
let s = "";
for (let i = 0; i < octets.length; i += 0x8000)
s += String.fromCharCode(...octets.subarray(i, i + 0x8000));
return btoa(s);
};
// Trois requêtes en parallèle, pas plus : on reste poli avec le serveur.
/**
* @template E, R
* @param {E[]} elements
* @param {(e: E, i: number) => Promise<R>} fn
* @returns {Promise<R[]>}
*/
const parLots = async (elements, fn) => {
/** @type {R[]} */
const sortie = [];
let suivant = 0;
await Promise.all(
Array.from({ length: 3 }, async () => {
while (suivant < elements.length) {
const i = suivant++;
sortie[i] = await fn(elements[i], i);
}
}),
);
return sortie;
};
/** @type {string[]} */
const avertissements = [];
/**
* @template T, D
* @param {string} nom
* @param {() => Promise<T>} fn
* @param {D} defaut
* @returns {Promise<T | D>}
*/
const etape = async (nom, fn, defaut) => {
dire("Export Rentila : " + nom + "…");
try {
return await fn();
} catch (e) {
if (e instanceof PasConnecte) throw e;
avertissements.push(nom + " : " + (e instanceof Error ? e.message : String(e)));
return defaut;
}
};
try {
const accueilTexte = await (await lire("/landlord/properties?a=1&paging=1000")).text();
if (/register\\?\/login/.test(accueilTexte.slice(0, 400))) throw new PasConnecte();
// 1. Export natif du compte (XLSX : biens, locations, locataires, finances, carnet, réservations, interventions, tâches)
/** @param {boolean} archives */
const lireExportNatif = async (archives) => {
const r = await lire(
"/landlord/export/?biens=1&locations=1&locataires=1&finances=1&carnet=1&reservations=1&interventions=1&tasks=1&file_type=mo&archived=" +
(archives ? "1" : ""),
);
const feuilles = await lireXlsx(new Uint8Array(await r.arrayBuffer()));
/** @type {Record<string, { totaux?: string[][], lignes: Record<string, string>[] }>} */
const sortie = {};
for (const [nom, lignes] of Object.entries(feuilles)) {
const finances = nom === "Finances";
const iEntete = finances
? Math.max(
0,
lignes.findIndex((l) => l[0] === "DATE"),
)
: 0;
sortie[nom] = {
totaux: finances ? lignes.slice(0, iEntete) : undefined,
lignes: enObjets(lignes, iEntete),
};
}
return sortie;
};
const exportNatif = {
actifs: await etape("export du compte", () => lireExportNatif(false), null),
archives: await etape("export des archives", () => lireExportNatif(true), null),
};
// 2. Fiches complètes, lues dans les formulaires d'édition, sans jamais les envoyer.
/**
* @param {string} liste
* @param {RegExp} motif
* @param {(id: string) => string} adresse
* @returns {Promise<Objet[]>}
*/
const fiches = (liste, motif, adresse) =>
parLots(idsDepuis(liste, motif), async (id) => ({
id,
fiche: lireFormulaire(await html(adresse(id)), "action=update"),
}));
const biens = await etape(
"biens",
() =>
fiches(
accueilTexte,
/#properties\/(\d+)\/edit/g,
(id) => `/landlord/properties/${id}/edit?a=1`,
),
[],
);
const listeLocataires = await etape(
"liste des locataires",
() => texte("/landlord/tenants?a=1&paging=1000"),
"",
);
const locataires = await etape(
"locataires",
() =>
fiches(
listeLocataires,
/#tenants\/(\d+)\/edit/g,
(id) => `/landlord/tenants/${id}/edit?a=1`,
),
[],
);
const listeBaux =
(await etape("liste des baux", () => texte("/landlord/leases?a=1&paging=1000"), "")) +
(await etape("baux archivés", () => texte("/landlord/leases/archive?a=1&paging=1000"), ""));
const baux = await etape(
"baux",
() =>
fiches(
listeBaux,
/#leases\/(\d+)\/(?:edit|view)/g,
(id) => `/landlord/leases/${id}/edit/?step=1&a=1`,
),
[],
);
const listeContacts = await etape(
"liste du carnet",
() => texte("/landlord/contacts?a=1&paging=1000"),
"",
);
const contacts = await etape(
"carnet",
() =>
fiches(
listeContacts,
/#contacts\/(\d+)\/edit/g,
(id) => `/landlord/contacts/${id}/edit?a=1`,
),
[],
);
const compte = await etape(
"compte et propriétaires",
async () => ({
profil: lireFormulaire(await html("/landlord/profile/edit?a=1"), "action=update"),
proprietaires: lireTableau(await html("/landlord/multilandlord?a=1&paging=1000")),
}),
{},
);
// 3. Finances : chaque mouvement (loyer, recette, dépense, amortissement), avec sa fiche.
const docPaiements = await etape(
"liste des finances",
() =>
html(
"/landlord/payments?a=1&FilterPeriodFrom=2000-01-01&FilterPeriodTo=2099-12-31&paging=100000",
),
null,
);
const lignes = docPaiements ? lireTableau(docPaiements) : [];
const cases = docPaiements
? [...docPaiements.querySelectorAll('input[name="ids[]"]')].filter(
(c) => c instanceof HTMLInputElement,
)
: [];
/** @returns {Fiche | null} La fiche, lue plus bas. */
const ficheALire = () => null;
// La ligne a une cellule de plus que l'en-tête : on garde les cellules brutes et on ne nomme que les sûres.
const mouvements = cases.map((c, i) => {
const cel = lignes[i]?._cellules || [];
const statut = c.closest("tr")?.querySelector('select[id^="changeStatus"]');
const choisi =
statut instanceof HTMLSelectElement ? [...statut.options].find((o) => o.selected) : null;
return {
id: c.value,
type: c.getAttribute("data-payment-type"),
montant: c.getAttribute("data-amount"),
quittanceDisponible: c.getAttribute("data-downloadable") === "1",
documents: (c.getAttribute("data-document-ids") || "").split(",").filter(Boolean),
date: cel[1] ?? null,
proprietaire: cel[2] ?? null,
bien: cel[3] ?? null,
tiers: cel[4] ?? null,
description: cel.at(-3) ?? null,
etat: choisi ? { valeur: choisi.value, libelle: propre(choisi.textContent) } : null,
solde: statut?.getAttribute("data-solde-amount") ?? null,
cellules: cel,
fiche: ficheALire(),
};
});
if (docPaiements) {
// Les échéances de prêt générées (des centaines) viennent déjà des tableaux d'amortissement : pas de fiche.
const aDetailler = mouvements.filter((m) => !m.cellules.some((x) => x.startsWith("Prêt :")));
await etape(
`fiches de ${aDetailler.length} mouvements`,
() =>
parLots(aDetailler, async (m, i) => {
if (i % 10 === 0) dire(`Export Rentila : mouvement ${i + 1} sur ${aDetailler.length}…`);
m.fiche = lireFormulaire(
await html(`/landlord/payments/${m.id}/edit?a=1`),
"action=update",
);
}),
null,
);
}
// 4. Prêts et tableaux d'amortissement.
const prets = await etape(
"prêts",
async () => {
const resume = rapportCsv(await texte("/landlord/credits/export/csv?"));
const ids = idsDepuis(await texte("/landlord/credits?a=1"), /credits\?&credit=([\w]+)/g);
const echeanciers = await parLots(ids, async (id) => ({
id,
echeances: rapportCsv(await texte(`/landlord/credits/export/csv?year=&&credit=${id}`)),
}));
return { resume, echeanciers };
},
null,
);
// 5. Rapports (CSV), sur toute la période.
/** @type {Record<string, string>} */
const RAPPORTS = {
1: "loyersEncaisses",
2: "loyersAppeles",
4: "loyersNonEncaisses",
5: "tva",
6: "depotsDeGarantie",
7: "soldesLocations",
9: "factures",
10: "depenses",
11: "occupants",
13: "valeurDesActifs",
15: "prets",
18: "contratsEtDiagnostics",
20: "etatLocatif",
21: "revisionsDeLoyer",
};
const finPeriode = new Date().getFullYear() + 20;
/** @type {Record<string, Rapport | null>} */
const rapports = {};
for (const [num, nom] of Object.entries(RAPPORTS)) {
rapports[nom] = await etape(
"rapport " + nom,
async () => {
const corps = new URLSearchParams({
FilterPeriodFrom: "01/01/2000",
FilterPeriodTo: `31/12/${finPeriode}`,
report: num,
FilterPropertyID: "",
a: "1",
});
const page = await texte("/landlord/reports/process", { method: "POST", body: corps });
const lien = page.match(/\/landlord\/reports\/download\/report_[\w]+\.xlsx\?format=csv/);
return lien ? rapportCsv(await texte(lien[0])) : null;
},
null,
);
}
// 6. Bilans annuels : vue générale et aide LMNP réel.
/** @type {Record<string, { general: string[][], lmnpReel: string[][] }>} */
const bilans = {};
await etape(
"bilans",
async () => {
const doc = await html("/landlord/bilan?a=1");
const select = doc.querySelector("select[name=year]");
const annees =
select instanceof HTMLSelectElement
? [...select.options].map((o) => o.value).filter((v) => /^\d{4}$/.test(v))
: [];
for (const an of annees) {
dire("Export Rentila : bilan " + an + "…");
bilans[an] = {
general: lireCsv(
await texte(
`/landlord/bilan/download_csv?year=${an}&MLandlordID=&BuildingID=0&taxregime=`,
),
),
lmnpReel: lireCsv(
await texte(
`/landlord/bilan/download_csv?year=${an}&MLandlordID=&BuildingID=0&taxregime=LMNPReel`,
),
),
};
}
},
null,
);
// 7. États des lieux, inventaires, immeubles : les listes.
const etatsDesLieux = await etape(
"états des lieux",
async () => lireTableau(await html("/landlord/leases/handovers?a=1&paging=1000")),
[],
);
const inventaires = await etape(
"inventaires",
async () => lireTableau(await html("/landlord/furniture?a=1&paging=1000")),
[],
);
const immeubles = await etape(
"immeubles",
async () => lireTableau(await html("/landlord/buildings?a=1&paging=1000")),
[],
);
// 8. Documents : « Mes documents » et pièces jointes des mouvements, une archive ZIP par id, car le ZIP ne nomme ses fichiers que par leur nom.
const documents = {
liste: /** @type {Record<string, unknown>[]} */ ([]),
archives: /** @type {{ ids: string[], octets: number, zipBase64: string }[]} */ ([]),
};
await etape(
"documents",
async () => {
const doc = await html("/landlord/mydocuments?a=1&paging=100000");
documents.liste = lireTableau(doc).map((l) => ({
...l,
id:
(l._liens.find((h) => /mydocuments\/download\/(\d+)/.test(h)) || "").match(
/download\/(\d+)/,
)?.[1] || null,
}));
const cases = [...doc.querySelectorAll('input[name="ids[]"]')].filter(
(c) => c instanceof HTMLInputElement,
);
const ids = [
...new Set([...cases.map((c) => c.value), ...mouvements.flatMap((m) => m.documents)]),
].filter((x) => /^\d+$/.test(x));
let total = 0;
let exclus = 0;
await parLots(ids, async (id, i) => {
if (total > 400 * 1024 * 1024) return void exclus++;
if (i % 10 === 0) dire(`Export Rentila : document ${i + 1} sur ${ids.length}…`);
const reponse = await lire("/landlord/mydocuments/documents.zip?ids[]=" + id);
const octets = new Uint8Array(await reponse.arrayBuffer());
total += octets.length;
if (total > 400 * 1024 * 1024) return void exclus++;
documents.archives.push({
ids: [id],
octets: octets.length,
zipBase64: enBase64(octets),
});
});
if (exclus)
avertissements.push(
`documents : arrêt à 400 Mo, ${exclus} documents non inclus. Téléchargez-les depuis « Mes documents ».`,
);
},
null,
);
// 9. Fichier. `comptes` permet à Locameublé de voir un fichier tronqué.
const exportedAt = new Date().toISOString();
const comptes = {
biens: biens.length,
locataires: locataires.length,
baux: baux.length,
contacts: contacts.length,
mouvements: mouvements.length,
mouvementsDetailles: mouvements.filter((m) => m.fiche).length,
prets: prets?.echeanciers.length || 0,
bilans: Object.keys(bilans).length,
documents: documents.archives.reduce((n, a) => n + a.ids.length, 0),
};
const bundle = {
source: "rentila",
version: VERSION_EXPORT,
exportedAt,
site: location.origin,
compte,
biens,
locataires,
baux,
contacts,
mouvements,
prets,
rapports,
bilans,
etatsDesLieux,
inventaires,
immeubles,
documents,
exportNatif,
avertissements,
comptes,
};
const json = JSON.stringify(bundle);
if (retourSansTelecharger) {
boite.remove();
return json;
}
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([json], { type: "application/json" }));
a.download = `rentila-export-${exportedAt.slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
a.remove();
dire(
`Export terminé : ${comptes.biens} bien(s), ${comptes.baux} bail(aux), ${comptes.locataires} locataire(s), ${comptes.mouvements} mouvement(s), ${comptes.documents} document(s). ` +
`Le fichier ${a.download} est dans vos téléchargements. Importez-le dans Locameublé.` +
(avertissements.length
? ` ${avertissements.length} point(s) à vérifier dans le fichier.`
: ""),
);
return comptes;
} catch (e) {
if (e instanceof PasConnecte) {
return arreter(
"Vous n'êtes pas connecté à Rentila. Connectez-vous sur https://www.rentila.com/landlord/ avec votre compte propriétaire, puis relancez l'export.",
);
}
return arreter(
"L'export s'est arrêté : " +
(e instanceof Error ? e.message : String(e)) +
". Rien n'a été modifié dans votre compte Rentila.",
);
}
}
return exporterRentila();
})();
Ce qui passe et ce qui reste
Vos biens, locataires et baux passent, avec les loyers et leur statut, les dépenses en écritures, les emprunts avec leurs échéances et le journal d'activité.
Deux pièges sont corrigés à l'import. Rentila enregistre le dépôt de garantie comme un revenu, sauf si vous cochez « Montant à titre indicatif ». Or un dépôt se rend au départ du locataire : l'import le met sur le bail, hors du résultat. Le guide du dépôt de garantie explique pourquoi. Et quand un bail commence dans le passé, Rentila crée l'historique des loyers à l'état « Payé ». L'import les fait arriver « attendus », sauf si un paiement est enregistré : vous pointez ce qui a vraiment été encaissé.
Trois points sont à compléter, parce que Rentila ne les a pas : la part du terrain, les composants du bien (gros œuvre, toiture, installations) et le mode des charges, au forfait ou en provisions. Il faut les deux premiers pour amortir le bien comme le prévoit l'article 39 C du code général des impôts. Le guide amortissement LMNP donne les durées par composant.
Ce qui ne passe pas encore : les documents (dont les PDF de quittances), les contacts et les garants, les états des lieux, les inventaires et les baux vides. Ils restent dans Rentila.
Les étapes, une par une
- Sur un ordinateur, glissez le bouton « Exporter de Rentila » dans votre barre de favoris.
- Ouvrez Rentila, connecté, et cliquez sur le favori. Il lit votre compte sans rien y changer et télécharge un fichier .json.
- Déposez ce fichier ici. L'aperçu montre les biens, baux, loyers par année, et ce qui reste à compléter.
- Cliquez sur « Importer dans Locameublé », créez votre compte, puis confirmez l'import.
- Complétez le terrain, les composants et les charges, puis pointez les loyers attendus.
Garder Rentila pour la gestion
Vous n'êtes pas obligé de quitter Rentila. S'il vous convient pour les loyers, les quittances et les états des lieux, gardez-le, et prenez Locameublé pour la liasse seulement : vous refaites l'export en début d'année et la liasse fiscale LMNP part des chiffres de l'année. Le comparatif des logiciels de gestion locative met les prix des deux usages côte à côte.
Cette page décrit un outil et les règles qu'il applique, sans connaître votre situation : pour un cas précis, demandez un conseil fiscal personnalisé. Les faits sur Rentila ont été lus sur son site le 12 novembre 2026.
Sources
- Rentila : tour du produit (vérifié le 12 novembre 2026)
- Rentila : tarifs (vérifié le 12 novembre 2026)
- Rentila : API et MCP (vérifié le 12 novembre 2026)
- Rentila : aide sur le dépôt de garantie (vérifié le 12 novembre 2026)
- Rentila : aide sur la création d'une location (vérifié le 12 novembre 2026)
- Rentila : aide sur la déclaration fiscale (vérifié le 12 novembre 2026)
- Article 39 C du code général des impôts, version en vigueur
Questions fréquentes
Faut-il résilier Rentila après l'import ?
Non. L'import lit votre compte sans rien y changer. Vous pouvez garder Rentila pour les loyers et les quittances, et ne prendre Locameublé que pour la liasse. Si vous résiliez, exportez d'abord vos documents depuis Rentila : ils ne passent pas encore.
Mes quittances Rentila passent-elles dans Locameublé ?
Les loyers passent, avec leur statut, mais pas les PDF de quittances, qui sont des documents. Gardez-les dans Rentila ou téléchargez-les. Pour les mois suivants, Locameublé fait les quittances, et l'outil gratuit de quittance aussi.
Le fichier part-il sur un serveur ?
Pas sur cette page. Le fichier est lu dans votre navigateur pour l'aperçu. Il n'est envoyé qu'au moment où vous lancez l'import, une fois votre compte créé.
Pourquoi ne pas passer par l'API de Rentila ?
Rentila la réserve aux comptes avec un abonnement payant (lu le 12 novembre 2026). L'export passe donc par votre session ouverte, avec les mêmes droits de lecture que vous.
En 10 minutes, ajoutez un bien et son bail, et voyez votre résultat 2026 estimé.
Déjà inscrit ? Connexion