Skip to content

add: bulk merge of partners duplicated on the same VAT - #2832

Merged
dhongu merged 1 commit into
19.0from
19.0-partner-merge-module
Aug 18, 2026
Merged

add: bulk merge of partners duplicated on the same VAT#2832
dhongu merged 1 commit into
19.0from
19.0-partner-merge-module

Conversation

@dhongu

@dhongu dhongu commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Adds deltatech_partner_merge: merging partners duplicated on the same VAT number, from the
interface, in bulk.

De ce

Mecanismul standard nu e utilizabil, și nu doar la volum. Măsurat pe o copie a bazei unui client
(543.036 parteneri, 1.163.295 note contabile), cu indexurile deja create:

Operațiune Timp
Unificarea a două fișe, wizardul standard 0,81 s · 1,96 s · 13,34 s · 21,01 s · 0,97 s — medie 7,62 s
Campania întreagă (4.235 de grupuri) la ritmul ăsta 9 ore
Aceeași campanie, prin acest modul circa 5 minute

Cauza: standardul parcurge toate cele ~158 de coloane care referă res_partner pentru fiecare
pereche
. Modulul inversează buclele — o instrucțiune per coloană, pentru tot lotul.

Separat, pe aceeași bază lipseau 67 de indexuri pe coloanele FK către parteneri. Ștergerea unei
fișe: 499 ms fără ele, 27 ms cu ele (crearea tuturor a durat o secundă). Modulul semnalează
situația și oferă butonul de creare.

Cum

Reia interogările din scripts/partner_merge/*.sql, strânse în models/sql_queries.py. Singura
diferență față de scripturi e stratul de meta-comenzi psql (\gexec, \if, variabilele -v), care
devine parametri și Python. Tabelele de lucru păstrează numele, deci un lot pregătit din interfață
poate fi inspectat din psql.

Cheile străine rămân active pe tot parcursul: integritatea e garantată de PostgreSQL, nu de
corectitudinea procedurii, și nu e nevoie de superuser — deci merge și pe odoo.sh.

Patru pași: Analizează → Simulează → Aplică → Verifică, cu bară de stare care nu permite
aplicarea înainte de simulare.

Simularea rulează într-un savepoint și îl anulează, deci nu poate scrie nici dacă acest cod ar
greși. Deliberat nu pe un al doilea cursor: tabelele de lucru se creează în tranzacția curentă, iar
CREATE TABLE ține lock ACCESS EXCLUSIVE până la commit — un al doilea cursor se blochează
așteptând un commit care vine abia după ce simularea se întoarce. Am pierdut zece minute pe deadlock-ul
ăsta la prima variantă; explicația e în cod, ca să nu fie „reparat" înapoi.

Aplicarea refuză să se încheie dacă rămâne vreo referință către o fișă absorbită.

Gărzi și clasificare

Categorie Situație Tratament
A o fișă are documentele, restul sunt goale automat
B documente pe o singură fișă automat
C facturi pe mai multe fișe manual, cu contabilitatea
D sold nereconciliat pe mai multe fișe manual, după închiderea lunii

Excluse automat din lot: grupurile care conțin fișa companiei proprii, cele cu utilizatori de portal
pe mai multe fișe, și cele cu denumiri complet diferite pe același CUI.

Fișa păstrată se alege după volumul de documente — ceea ce nu e același lucru cu calitatea denumirii.
De aceea masterii cu nume care pare lipit la import sunt marcați, cu denumirile absorbite alături,
capturate înainte de merge (după, ele nu mai există).

Verificat

Pe copia de producție, din interfață:

Rezultat
Clasificarea identică cu cifrele procedurii SQL, până la ultimul grup
După simulare toate cele 30 de fișe ale lotului existau încă
După aplicare 30 absorbite, 30 de masteri păstrați
Verificare 0 abateri pe facturi, comenzi și sold; 0 fișe rămase
Denumiri degradate 3 semnalate, cu varianta corectă

Anterior, procedura SQL echivalentă a fost rulată pe staging-ul clientului: 575 de fișe în trei
loturi, zero abateri.

Note

  • depends doar pe base: sub-interogările pe account_move, sale_order, purchase_order,
    stock_picking sunt protejate cu to_regclass.
  • CREATE INDEX CONCURRENTLY nu poate rula în tranzacția Odoo, deci butonul creează indexurile
    normal (lock de secunde pe tabelele mari); formularul trimite explicit la 01_fk_indexes.sql pentru
    instanțele unde nici atât nu e acceptabil.
  • Purjarea contactelor orfane rămâne script — e o ștergere în masă care merită să fie deliberată.
  • Două grupuri de acces separate: a pregăti și a simula ≠ a aplica.

Odoo's own merge wizard walks the foreign keys group by group: for every pair it
goes through all ~158 columns that reference res_partner. Fine for a handful of
duplicates; on a client database with 6.055 duplicate groups it means over
750.000 statements and does not finish in any usable window. Deleting a single
partner has the same problem from the other side -- the database checks all 158
constraints, and any column without an index turns that into a full table scan.

This module wraps the SQL procedure from scripts/partner_merge/, which inverts the
loops: one statement per column for the whole batch. Foreign keys stay enabled
throughout, so integrity is guaranteed by PostgreSQL rather than by the procedure
being right, and no superuser is needed. Measured on a production copy of 538.000
partners: 5.350 records merged in about four and a half minutes.

The queries live in models/sql_queries.py, unchanged from the scripts except for
the psql meta-commands, which become parameters and Python. The working tables
keep their names, so a batch prepared from the UI can be inspected from psql.

Four buttons -- Analyze, Simulate, Apply, Verify -- with a status bar that does
not let you skip the simulation. The simulation runs inside a savepoint and rolls
it back, so it cannot write even if this code is wrong; deliberately NOT a second
cursor, because the working tables are created in the current transaction and a
second cursor deadlocks waiting for the lock CREATE TABLE holds until commit.
Applying refuses to finish if any reference is left pointing at an absorbed record.

Groups whose unreconciled balance is spread over several records are classified
apart -- merging those moves money between ledgers. Guarded out entirely: groups
holding the company's own partner record, groups with portal users on more than one
record, and groups whose names differ completely on the same VAT number.

The kept record is chosen by document volume, which is not the same as name quality,
so records whose surviving name looks mangled at import are flagged, with the
absorbed names captured before the merge for correction.

Depends on base only: the sub-queries touching account_move, sale_order,
purchase_order and stock_picking are guarded with to_regclass.

Verified on a production copy: the classification reproduces the scripts' figures
exactly (925/931 on A, 3.875/4.419 on B, 117/184 on C, 843/2.010 on D, 284 groups
guarded out), and after a simulation all 21 records of the test batch still exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dhongu
dhongu merged commit a5fff69 into 19.0 Aug 18, 2026
5 checks passed
@dhongu
dhongu deleted the 19.0-partner-merge-module branch August 18, 2026 11:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant