#!/usr/bin/env python3 # © 2021-2026 TechnoLibre (http://www.technolibre.ca) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) """Rendre visibles les documents DMS qu'une migration a rendus muets. Le symptôme : après le palier 13, les documents DMS « ont disparu ». Ils n'ont pas disparu. Mesuré sur une base migrée : 69 fichiers, 16 dossiers, 23 Mo de `content_binary`, présents à l'identique de la 12 à la 18. Rien n'a été perdu. Ce qui a changé, c'est le MODÈLE DE SÉCURITÉ. En 12, MuK DMS ne filtrait que sur la société : ['|', ('company','=',False), ('company','child_of',[...])] OCA DMS, à partir de la 13, ajoute des règles GLOBALES sur une permission calculée : [('permission_read', '=', user.id)] `_search_permission_read` n'accorde l'accès que par deux portes : 1. Une `dms.access.group` dont l'utilisateur est membre, reliée au dossier — la migration n'en a créé AUCUNE, parce que MuK n'en avait aucune à convertir. 2. L'héritage depuis l'enregistrement lié, qui exige `storage.save_type = 'attachment'` — or les stockages migrés sont en `database`. Les deux portes fermées, la règle globale filtre TOUT. Pas un dossier, pas un fichier, pour personne — l'administrateur compris, car `admin` n'est pas le super-utilisateur au sens d'Odoo. Ce que fait la réparation ------------------------- Une seule `dms.access.group`, adossée au groupe Odoo `dms.group_dms_user` plutôt qu'à une liste d'utilisateurs — ainsi l'appartenance continue de suivre le groupe, comme avant la migration —, reliée aux dossiers RACINES. Les enfants héritent : ils portent tous `inherit_group_ids = true`. Ce qu'elle ne fait pas : inventer une granularité que MuK n'avait pas. L'ancien modèle ne connaissait pas d'ACL par dossier ; en fabriquer une ici serait décider à la place de l'utilisateur, sur des données qu'il n'a jamais saisies. Codes de sortie : 0 rien à faire, 1 il y a à faire (ou c'est fait), 2 échec. """ import argparse import os import sys sys.path.append( os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", "..")) ) from script.odoo.migration import database_cleanup # noqa: E402 try: from script.todo.todo_i18n import t except Exception: # pragma: no cover - repli si i18n indisponible def t(key: str) -> str: return key # Les sentinelles de `run_shell`, pas les nôtres : il les impose, et # elles ne servent qu'à isoler le rapport des journaux d'Odoo. En # inventer une seconde paire aurait créé un contrat parallèle à tenir. DEBUT = database_cleanup.START FIN = database_cleanup.END NOM_GROUPE = "Migration MuK → DMS" # Le script poussé dans `odoo-bin shell`. Il compte AVANT et APRÈS avec un # vrai utilisateur : c'est la seule preuve qui vaille — la règle globale # ne s'applique pas au super-utilisateur, donc un compte fait en sudo # dirait « tout va bien » alors que personne ne voit rien. SCRIPT = """ import json DRY = {dry} NOM = {nom!r} rapport = {{"dry_run": DRY}} try: # Sans DMS, il n'y a rien à réparer et ce n'est pas une erreur : cet # outil tourne à chaque migration vers la 13, et la plupart des bases # n'ont jamais eu de DMS. Lever ici les ferait toutes échouer. if "dms.file" not in env: rapport["absent"] = True print({debut!r}) print(json.dumps(rapport)) print({fin!r}) raise SystemExit(0) Dossier = env["dms.directory"].sudo() Fichier = env["dms.file"].sudo() Groupe = env["dms.access.group"].sudo() rapport["files"] = Fichier.search_count([]) rapport["directories"] = Dossier.search_count([]) rapport["access_groups_before"] = Groupe.search_count([]) # Un vrai utilisateur, pas le super-utilisateur : la règle globale ne # s'applique qu'à lui, et c'est justement ce qu'on mesure. membres = env.ref("dms.group_dms_user").users.filtered( lambda u: u.id != 1 and u.active ) temoin = membres[:1] rapport["witness"] = temoin.login if temoin else None def visible(): if not temoin: return None return {{ "directories": Dossier.with_user(temoin).search_count([]), "files": Fichier.with_user(temoin).search_count([]), }} rapport["before"] = visible() racines = Dossier.search([("is_root_directory", "=", True)]) rapport["roots"] = racines.mapped("name") deja = Groupe.search([("name", "=", NOM)]) rapport["already_repaired"] = bool(deja) if not DRY and not deja and racines: Groupe.create({{ "name": NOM, "perm_create": True, "perm_write": True, "perm_unlink": True, "group_ids": [(6, 0, [env.ref("dms.group_dms_user").id])], "directory_ids": [(6, 0, racines.ids)], }}) env.cr.commit() env.registry.clear_cache() rapport["created"] = True rapport["after"] = visible() rapport["access_groups_after"] = Groupe.search_count([]) except Exception as exc: rapport["error"] = "%s: %s" % (type(exc).__name__, exc) print({debut!r}) print(json.dumps(rapport)) print({fin!r}) """ def build_script(dry_run): return SCRIPT.format( dry="True" if dry_run else "False", nom=NOM_GROUPE, debut=DEBUT, fin=FIN, ) def render(rapport, dry_run): if rapport.get("absent"): return [f"ℹ️ {t('No DMS in this database, nothing to repair.')}"] lignes = [ f"📁 {t('DMS documents in the database')} :" f" {rapport.get('files', '?')} {t('file(s)')}," f" {rapport.get('directories', '?')} {t('folder(s)')}" ] avant = rapport.get("before") temoin = rapport.get("witness") if avant is None: lignes.append(f" ⚠ {t('No DMS user to test visibility with.')}") else: lignes.append( f" {t('Visible to')} {temoin} :" f" {avant['directories']} {t('folder(s)')}," f" {avant['files']} {t('file(s)')}" ) lignes.append( f" {t('DMS access groups:')} {rapport.get('access_groups_before', '?')}" ) if rapport.get("already_repaired"): lignes.append(f" ℹ️ {t('Already repaired: the group exists.')}") return lignes if dry_run: lignes.append( f" → {t('Would create one access group over')}" f" {len(rapport.get('roots') or [])} {t('root folder(s)')} :" f" {', '.join(rapport.get('roots') or [])}" ) lignes.append(f" {t('Nothing written. Re-run with --apply.')}") return lignes apres = rapport.get("after") if apres: lignes.append( f" ✅ {t('Now visible to')} {temoin} :" f" {apres['directories']} {t('folder(s)')}," f" {apres['files']} {t('file(s)')}" ) return lignes def main(argv=None): parser = argparse.ArgumentParser( description=( "Restore visibility of DMS documents after a MuK DMS to OCA DMS" " migration. Reports without writing unless --apply is given." ) ) parser.add_argument("-d", "--database", required=True) parser.add_argument("-c", "--config", default="config.conf") parser.add_argument( "--apply", action="store_true", help="actually create the access group (default: report only)", ) config = parser.parse_args(argv) # Le même garde-fou que partout ailleurs : un Odoo d'une autre version # ÉCRIT dans la base avant d'échouer. souci = database_cleanup.require_matching_version(config.database) if souci: print(f"❌ {souci}") return 2 try: rapport = database_cleanup.run_shell( config.database, config.config, build_script(not config.apply), echo=lambda texte: print(f"⧖ {texte}", flush=True), ) except RuntimeError as exc: print(f"❌ {exc}") return 2 if rapport.get("error"): print(f"❌ {rapport['error']}") return 2 print("\n".join(render(rapport, not config.apply))) if rapport.get("absent"): return 0 avant = rapport.get("before") or {} if config.apply: # Une réparation RÉUSSIE doit rendre 0. Rendre « il y a des # trouvailles » après avoir tout remis en place ferait échouer # n'importe quelle chaîne make qui appelle l'outil. apres = rapport.get("after") or {} if rapport.get("already_repaired"): return 0 return 0 if apres.get("files") else 2 return 1 if not avant.get("files") else 0 if __name__ == "__main__": sys.exit(main())