[FIX] addons: browse the leftovers by integer, not by the string psql gave

Deleting the leftovers failed on « the database search does not have the
ids (('4457',)) and has extra ids ((4457,)) ». psql returns text; the ids
went into browse() as strings, Odoo compared them against integers, found
nothing and refused the whole batch.

Nothing was lost: the backup runs before the deletion, and the tool said
plainly that nothing was removed. Measured after the fact — 15 files
saved, 15 attachments still in database.

--- FR ---

[FIX] addons : parcourir les restes par entier, pas par la chaîne de psql

L'effacement échouait sur « la recherche en base n'a pas les identifiants
(('4457',)) et a des identifiants supplémentaires ((4457,)) ». psql rend
du texte ; les identifiants partaient dans browse() en chaînes, Odoo les
comparait à des entiers, ne trouvait rien et refusait tout le lot.

Rien n'a été perdu : la sauvegarde précède l'effacement, et l'outil a dit
franchement que rien n'avait été retiré. Vérifié après coup — 15 fichiers
sauvegardés, 15 pièces jointes toujours en base.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-17 03:17:18 -04:00
parent 1c0635831f
commit e75ab8b128
2 changed files with 43 additions and 1 deletions

View file

@ -181,7 +181,11 @@ def delete_attachments(database, lst_row, config_path="./config.conf"):
Un DELETE en SQL laisserait les fichiers orphelins et les caches
incohérents ; unlink() fait le ménage complet, dans toutes les versions.
"""
lst_id = [row.split("|")[0] for row in lst_row]
# int(), pas la chaîne du psql : browse(['4457']) fait échouer Odoo sur
# « n'a pas les identifiants (('4457',)) et a des identifiants
# supplémentaires ((4457,)) » — il compare des chaînes à des entiers.
# Mesuré sur une vraie base, l'effacement n'a rien retiré.
lst_id = [int(row.split("|")[0]) for row in lst_row]
script = (
f"env['ir.attachment'].browse({lst_id!r}).unlink()\n"
"env.cr.commit()\n"

View file

@ -376,6 +376,44 @@ class TestTheQuestionMustBeVisible(unittest.TestCase):
self.assertIn("sys.stdout.isatty()", source)
class TestTheIdentifiersSentToOdoo(unittest.TestCase):
"""browse() veut des ENTIERS ; psql rend des chaînes.
Mesuré sur une vraie base : browse(['4457']) fait échouer Odoo sur
« la recherche en base n'a pas les identifiants (('4457',)) et a des
identifiants supplémentaires ((4457,)) ». Il compare des chaînes à des
entiers, ne retrouve rien, et refuse. L'effacement n'a rien retiré —
heureusement, la sauvegarde était déjà faite.
"""
def test_the_script_browses_integers(self):
import inspect
source = inspect.getsource(theme_leftover.delete_attachments)
self.assertIn("int(row.split", source)
def test_the_pushed_script_carries_no_quoted_id(self):
# Le rendu exact de ce qui part dans le shell : une seule apostrophe
# autour d'un identifiant et Odoo refuse tout le lot.
pushed = {}
original = theme_leftover.subprocess.run
theme_leftover.subprocess.run = (
lambda *a, **kw: pushed.update(script=kw.get("input", ""))
or type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})()
)
self.addCleanup(setattr, theme_leftover.subprocess, "run", original)
theme_leftover.delete_attachments(
"db", ["4457|/theme_x/a.scss|2021-03-04", "4768|/b.css|d"]
)
self.assertIn("browse([4457, 4768])", pushed["script"])
self.assertNotIn("'4457'", pushed["script"])
def test_a_non_numeric_row_is_not_sent_silently(self):
# Mieux vaut échouer ici que pousser un script qu'Odoo refusera.
with self.assertRaises(ValueError):
theme_leftover.delete_attachments("db", ["zz|/a|d"])
class TestTheMisleadingErrorCode(unittest.TestCase):
"""« 1 » veut dire « il reste des choses », pas « ça a raté »."""