dolibarr/htdocs/core/triggers
VIAL-GOUTEYRON Quentin 2d44fdf49c
CLOSE #29244 Feature to merge contacts (#39512)
Add the ability to merge two contacts, the same way third parties and members
can already be merged. The merged contact is absorbed by the current one, all
its satellite data is moved, then it is deleted.

Contact::mergeContact() orchestrates everything in a single transaction, using
the new CommonObject::commonReplaceContact() helper and the replaceContact()
static methods of Contact, ActionComm and User. A replaceContact hook lets the
external modules move their own data. No schema change, no migration.

Three specific traps of this table are handled:
- llx_element_contact.fk_socpeople references llx_socpeople only when
  c_type_contact.source is 'external', and llx_user when it is 'internal', so
  both queries filter on it. Without that filter the internal assignments of
  the users would silently be moved to the merged contact.
- Contact::updateRoles(), called by update(), deletes then reinserts every
  societe_contacts row of the contact from $this->roles. It is neutralised by
  setting roles to null, and update() is called before the links are moved.
- Every table having a unique index on the contact id is deduplicated before
  its update, so the update cannot violate it. $ignoreerrors is never used:
  the FIXME of commonReplaceThirdparty() documents the links it loses.

The files are moved after the commit and not before, unlike mergeCompany() and
mergeMembers(): a failure after the move leaves them with the database rolled
back but the files already gone. A name collision renames the file instead of
losing it, and a failure is reported to the user instead of being ignored.

Access guards live in the class and not in the page, so the REST API, the
scheduled jobs and the modules benefit from them too. They cover both contacts,
fetch() by rowid applying neither an entity nor a permission filter: entity,
private contact of another user, external user, sales representative perimeter,
absorbing a shared contact into a private one, and a contact linked to a user
account (moving llx_user.fk_socpeople would let the email of that user be
rewritten from the contact card, so it requires the permission to create users).

Form::select_contact() gets a one line fix on the way: it accepts a $filter
parameter, documents it, forwards it to the ajax branch, but dropped it when
building the combo, so the merge popup could not exclude the current contact.

Known limitations, out of the scope of this pull request: merging contacts of
different entities is refused, Contact::update() neither writes url, no_email,
fk_parent nor import_key while its $nosyncuser parameter is declared but never
read, and with CONTACT_USE_SEARCH_TO_SELECT set the current contact is still
offered in the popup, contact/ajax/contact.php overwriting the filter it gets.
2026-08-19 02:26:34 +02:00
..
dolibarrtriggers.class.php Debug v23 2025-12-21 14:33:25 +01:00
interface_20_all_Logevents.class.php
interface_20_modWorkflow_WorkflowManager.class.php Doc fix #37451 2026-03-21 01:27:30 +01:00
interface_50_modAgenda_ActionsAuto.class.php CLOSE #29244 Feature to merge contacts (#39512) 2026-08-19 02:26:34 +02:00
interface_50_modEventOrganization_EventOrganization.class.php clean code (#36233) 2025-11-12 15:32:29 +01:00
interface_50_modLdap_Ldapsynchro.class.php Qual: Fix Phan notices observed in #38164 (#38231) 2026-05-18 03:08:39 +02:00
interface_50_modMailmanspip_Mailmanspipsynchro.class.php clean code (#36317) 2025-11-20 01:47:46 +01:00
interface_50_modNotification_Notification.class.php clean code (#36317) 2025-11-20 01:47:46 +01:00
interface_50_modTicket_TicketEmail.class.php Qual: Fix 'SqlInjection' notices (#39225) 2026-07-20 02:52:07 +02:00
interface_80_modStripe_Stripe.class.php
interface_90_modSociete_ContactRoles.class.php
interface_95_modWebhook_WebhookTriggers.class.php Qual: Fix phan notices (#38948) 2026-06-22 18:22:20 +02:00
interface_95_modZapier_ZapierTriggers.class.php
interface_99_modBlockedlog_ActionsBlockedLog.class.php Fix travis 2026-06-20 02:28:25 +02:00
README Fix set status with objectonoff 2026-03-24 18:05:52 +01:00

Directory content
--------------------------------------------------------
This directory contains all available Dolibarr triggers.

Trigger file name syntax is:
> interface_xx_modModule_Mytrigger.php[-NORUN]

where:
> modModule is part of module descriptor file name found in core/modules
            directory or value 'all' if not enabled by a particular module.
> Mytrigger is name of the trigger. The PHP class inside the trigger file
            would be named InterfaceMytrigger.
> -NORUN is optional and disable completely a trigger file if set. To use
            a trigger with such a name, rename file to remove the -NORUN part.