diff --git a/htdocs/categories/class/categorie.class.php b/htdocs/categories/class/categorie.class.php
index 9a449d63700..0aee9cbfc81 100644
--- a/htdocs/categories/class/categorie.class.php
+++ b/htdocs/categories/class/categorie.class.php
@@ -1947,6 +1947,23 @@ class Categorie extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables, 1);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'categorie_product'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Return the addtional SQL JOIN query for filtering a list by a category
*
diff --git a/htdocs/comm/propal/class/propal.class.php b/htdocs/comm/propal/class/propal.class.php
index 2f055f54a42..6b9b259fd52 100644
--- a/htdocs/comm/propal/class/propal.class.php
+++ b/htdocs/comm/propal/class/propal.class.php
@@ -3740,8 +3740,24 @@ class Propal extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
-}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'propaldet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+}
/**
* Class to manage commercial proposal lines
diff --git a/htdocs/commande/class/commande.class.php b/htdocs/commande/class/commande.class.php
index 9b315698527..0fe8d52cd1d 100644
--- a/htdocs/commande/class/commande.class.php
+++ b/htdocs/commande/class/commande.class.php
@@ -3999,6 +3999,23 @@ class Commande extends CommonOrder
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'commandedet',
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Is the customer order delayed?
*
diff --git a/htdocs/compta/facture/class/facture-rec.class.php b/htdocs/compta/facture/class/facture-rec.class.php
index 100334e046a..c868e1e9992 100644
--- a/htdocs/compta/facture/class/facture-rec.class.php
+++ b/htdocs/compta/facture/class/facture-rec.class.php
@@ -1725,6 +1725,23 @@ class FactureRec extends CommonInvoice
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'facturedet_rec'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Update frequency and unit
*
diff --git a/htdocs/compta/facture/class/facture.class.php b/htdocs/compta/facture/class/facture.class.php
index 649b7b0c093..ffa520b1284 100644
--- a/htdocs/compta/facture/class/facture.class.php
+++ b/htdocs/compta/facture/class/facture.class.php
@@ -4738,6 +4738,23 @@ class Facture extends CommonInvoice
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'facturedet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Is the customer invoice delayed?
*
diff --git a/htdocs/contrat/class/contrat.class.php b/htdocs/contrat/class/contrat.class.php
index fe8edd30ea4..cb97e6bc895 100644
--- a/htdocs/contrat/class/contrat.class.php
+++ b/htdocs/contrat/class/contrat.class.php
@@ -2459,6 +2459,23 @@ class Contrat extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'contratdet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Load an object from its id and create a new one in database
*
diff --git a/htdocs/core/class/commonobject.class.php b/htdocs/core/class/commonobject.class.php
index 5dd392ad065..601457aba71 100644
--- a/htdocs/core/class/commonobject.class.php
+++ b/htdocs/core/class/commonobject.class.php
@@ -7990,7 +7990,7 @@ abstract class CommonObject
/**
* Function used to replace a thirdparty id with another one.
- * This function is meant to be called from replaceThirdparty with the appropiate tables
+ * This function is meant to be called from replaceThirdparty with the appropriate tables
* Column name fk_soc MUST be used to identify thirdparties
*
* @param DoliDB $db Database handler
@@ -8007,7 +8007,36 @@ abstract class CommonObject
if (!$db->query($sql)) {
if ($ignoreerrors) {
- return true; // TODO Not enough. If there is A-B on kept thirdarty and B-C on old one, we must get A-B-C after merge. Not A-B.
+ return true; // TODO Not enough. If there is A-B on kept thirdparty and B-C on old one, we must get A-B-C after merge. Not A-B.
+ }
+ //$this->errors = $db->lasterror();
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Function used to replace a product id with another one.
+ * This function is meant to be called from replaceProduct with the appropriate tables
+ * Column name fk_product MUST be used to identify products
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id (the product to delete)
+ * @param int $dest_id New product id (the product that will received element of the other)
+ * @param string[] $tables Tables that need to be changed
+ * @param int $ignoreerrors Ignore errors. Return true even if errors. We need this when replacement can fails like for categories (categorie of old product may already exists on new one)
+ * @return bool True if success, False if error
+ */
+ public static function commonReplaceProduct(DoliDB $db, $origin_id, $dest_id, array $tables, $ignoreerrors = 0)
+ {
+ foreach ($tables as $table) {
+ $sql = 'UPDATE '.MAIN_DB_PREFIX.$table.' SET fk_product = '.((int) $dest_id).' WHERE fk_product = '.((int) $origin_id);
+
+ if (!$db->query($sql)) {
+ if ($ignoreerrors) {
+ return true; // TODO Not enough. If there is A-B on kept thirdparty and B-C on old one, we must get A-B-C after merge. Not A-B.
}
//$this->errors = $db->lasterror();
return false;
diff --git a/htdocs/delivery/class/delivery.class.php b/htdocs/delivery/class/delivery.class.php
index 6bfe3a33dd7..47300702baf 100644
--- a/htdocs/delivery/class/delivery.class.php
+++ b/htdocs/delivery/class/delivery.class.php
@@ -1082,6 +1082,23 @@ class Delivery extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'deliverydet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
}
diff --git a/htdocs/expedition/class/expedition.class.php b/htdocs/expedition/class/expedition.class.php
index 8ef7be75f52..8218e1c6402 100644
--- a/htdocs/expedition/class/expedition.class.php
+++ b/htdocs/expedition/class/expedition.class.php
@@ -2503,6 +2503,23 @@ class Expedition extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'expeditiondet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
}
diff --git a/htdocs/fichinter/class/fichinter.class.php b/htdocs/fichinter/class/fichinter.class.php
index 297e03fa379..af5801a48fb 100644
--- a/htdocs/fichinter/class/fichinter.class.php
+++ b/htdocs/fichinter/class/fichinter.class.php
@@ -1374,6 +1374,23 @@ class Fichinter extends CommonObject
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'fichinterdet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
}
/**
diff --git a/htdocs/fichinter/class/fichinterrec.class.php b/htdocs/fichinter/class/fichinterrec.class.php
index d5690265028..9557707ad84 100644
--- a/htdocs/fichinter/class/fichinterrec.class.php
+++ b/htdocs/fichinter/class/fichinterrec.class.php
@@ -40,7 +40,7 @@ class FichinterRec extends Fichinter
{
public $element = 'fichinterrec';
public $table_element = 'fichinter_rec';
- public $table_element_line = 'fichinter_rec';
+ public $table_element_line = 'fichinterdet_rec';
/**
* @var string Fieldname with ID of parent key if this field has a parent
@@ -693,6 +693,22 @@ class FichinterRec extends Fichinter
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'fichinterdet_rec'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
/**
* Update frequency and unit
diff --git a/htdocs/fourn/class/fournisseur.commande.class.php b/htdocs/fourn/class/fournisseur.commande.class.php
index 9b68ef199db..c5788197c68 100644
--- a/htdocs/fourn/class/fournisseur.commande.class.php
+++ b/htdocs/fourn/class/fournisseur.commande.class.php
@@ -3191,6 +3191,23 @@ class CommandeFournisseur extends CommonOrder
return CommonObject::commonReplaceThirdparty($db, $origin_id, $dest_id, $tables);
}
+ /**
+ * Function used to replace a product id with another one.
+ *
+ * @param DoliDB $db Database handler
+ * @param int $origin_id Old product id
+ * @param int $dest_id New product id
+ * @return bool
+ */
+ public static function replaceProduct(DoliDB $db, $origin_id, $dest_id)
+ {
+ $tables = array(
+ 'commande_fournisseurdet'
+ );
+
+ return CommonObject::commonReplaceProduct($db, $origin_id, $dest_id, $tables);
+ }
+
/**
* Is the supplier order delayed?
* We suppose a purchase ordered as late if a the purchase order has been sent and the delivery date is set and before the delay.
diff --git a/htdocs/langs/en_US/products.lang b/htdocs/langs/en_US/products.lang
index 205a28980a8..f4e196d0b7e 100644
--- a/htdocs/langs/en_US/products.lang
+++ b/htdocs/langs/en_US/products.lang
@@ -408,3 +408,7 @@ mandatoryHelper=Message to the user on the need to enter a start date and an end
DefaultBOM=Default BOM
DefaultBOMDesc=The default BOM recommended to use to manufacture this product. This field can be set only if nature of product is '%s'.
Rank=Rank
+MergeOriginProduct=Duplicate product (product you want to delete)
+MergeProducts=Merge products
+ConfirmMergeProducts=Are you sure you want to merge the chosen product with the current one? All linked objects (invoices, orders, ...) will be moved to the current product, after which the chosen product will be deleted.
+ProductsMergeSuccess=Products have been merged
diff --git a/htdocs/product/card.php b/htdocs/product/card.php
index 420203eedae..06a5a4102ff 100644
--- a/htdocs/product/card.php
+++ b/htdocs/product/card.php
@@ -226,6 +226,177 @@ if (empty($reshook)) {
}
$action = '';
}
+ // merge products
+ if ($action == 'confirm_merge' && $confirm == 'yes' && $user->rights->societe->creer) {
+ $error = 0;
+ $productOriginId = GETPOST('product_origin', 'int');
+ $productOrigin = new Product($db);
+
+ if ($productOriginId <= 0) {
+ $langs->load('errors');
+ setEventMessages($langs->trans('ErrorProductIdIsMandatory', $langs->transnoentitiesnoconv('MergeOriginProduct')), null, 'errors');
+ } else {
+ if (!$error && $productOrigin->fetch($productOriginId) < 1) {
+ setEventMessages($langs->trans('ErrorRecordNotFound'), null, 'errors');
+ $error++;
+ }
+
+ if (!$error) {
+ // TODO Move the merge function into class of object.
+ $db->begin();
+
+ // Recopy some data
+ //$object->client = $object->client | $productOrigin->client;
+ //$object->fournisseur = $object->fournisseur | $productOrigin->fournisseur;
+ $listofproperties = array(
+ 'ref',
+ 'ref_ext',
+ 'label',
+ 'description',
+ 'url',
+ 'barcode',
+ 'fk_barcode_type',
+ 'import_key',
+ 'mandatory_period',
+ 'accountancy_code_buy',
+ 'accountancy_code_buy_intra',
+ 'accountancy_code_buy_export',
+ 'accountancy_code_sell',
+ 'accountancy_code_sell_intra',
+ 'accountancy_code_sell_export'
+ );
+ foreach ($listofproperties as $property) {
+ if (empty($object->$property)) {
+ $object->$property = $productOrigin->$property;
+ }
+ }
+ // Concat some data
+ $listofproperties = array(
+ 'note_public', 'note_private'
+ );
+ foreach ($listofproperties as $property) {
+ $object->$property = dol_concatdesc($object->$property, $productOrigin->$property);
+ }
+
+ // Merge extrafields
+ if (is_array($productOrigin->array_options)) {
+ foreach ($productOrigin->array_options as $key => $val) {
+ if (empty($object->array_options[$key])) {
+ $object->array_options[$key] = $val;
+ }
+ }
+ }
+
+ // Merge categories
+ $static_cat = new Categorie($db);
+ $custcats_ori = $static_cat->containing($productOrigin->id, 'product', 'id');
+ $custcats = $static_cat->containing($object->id, 'product', 'id');
+ $custcats = array_merge($custcats, $custcats_ori);
+ $object->setCategories($custcats);
+
+ // If product has a new code that is same than origin, we clean origin code to avoid duplicate key from database unique keys.
+ if ($productOrigin->barcode == $object->barcode) {
+ dol_syslog("We clean customer and supplier code so we will be able to make the update of target");
+ $productOrigin->barcode = '';
+ //$productOrigin->update($productOrigin->id, $user, 0, 'merge');
+ }
+
+ // Update
+ $result = $object->update($object->id, $user, 0, 'merge');
+ if ($result <= 0) {
+ setEventMessages($object->error, $object->errors, 'errors');
+ $error++;
+ }
+
+ // Move links
+ if (!$error) {
+ // This list is also into the api_products.class.php
+ // TODO Mutualise the list into object product.class.php
+ $objects = array(
+ 'Categorie' => '/categories/class/categorie.class.php',
+ 'Propal' => '/comm/propal/class/propal.class.php',
+ 'Commande' => '/commande/class/commande.class.php',
+ 'Facture' => '/compta/facture/class/facture.class.php',
+ 'FactureRec' => '/compta/facture/class/facture-rec.class.php',
+ // 'Mo' => '/mrp/class/mo.class.php',
+ 'Contrat' => '/contrat/class/contrat.class.php',
+ 'Expedition' => '/expedition/class/expedition.class.php',
+ 'Fichinter' => '/fichinter/class/fichinter.class.php',
+ 'FichinterRec' => '/fichinter/class/fichinter.class.php',
+ 'CommandeFournisseur' => '/fourn/class/fournisseur.commande.class.php',
+ // 'FactureFournisseur' => '/fourn/class/fournisseur.facture.class.php',
+ // 'SupplierProposal' => '/supplier_proposal/class/supplier_proposal.class.php',
+ // 'ProductFournisseur' => '/fourn/class/fournisseur.product.class.php',
+ 'Delivery' => '/delivery/class/delivery.class.php',
+ // 'Project' => '/projet/class/project.class.php',
+ // 'Ticket' => '/ticket/class/ticket.class.php',
+ // 'ConferenceOrBoothAttendee' => '/eventorganization/class/conferenceorboothattendee.class.php'
+ );
+
+ //First, all core objects must update their tables
+ foreach ($objects as $object_name => $object_file) {
+ require_once DOL_DOCUMENT_ROOT.$object_file;
+
+ if (!$error && !$object_name::replaceProduct($db, $productOrigin->id, $object->id)) {
+ $error++;
+ setEventMessages($db->lasterror(), null, 'errors');
+ break;
+ }
+ }
+ }
+
+ // External modules should update their ones too
+ if (!$error) {
+ $reshook = $hookmanager->executeHooks(
+ 'replaceProduct',
+ array(
+ 'soc_origin' => $productOrigin->id,
+ 'soc_dest' => $object->id,
+ ),
+ $object,
+ $action
+ );
+
+ if ($reshook < 0) {
+ setEventMessages($hookmanager->error, $hookmanager->errors, 'errors');
+ $error++;
+ }
+ }
+
+
+ if (!$error) {
+ $object->context = array(
+ 'merge' => 1,
+ 'mergefromid' => $productOrigin->id,
+ );
+
+ // Call trigger
+ $result = $object->call_trigger('PRODUCT_MODIFY', $user);
+ if ($result < 0) {
+ setEventMessages($object->error, $object->errors, 'errors');
+ $error++;
+ }
+ // End call triggers
+ }
+
+ if (!$error) {
+ // We finally remove the old product
+ if ($productOrigin->delete($user) < 1) {
+ $error++;
+ }
+ }
+
+ if (!$error) {
+ setEventMessages($langs->trans('ProductsMergeSuccess'), null, 'mesgs');
+ $db->commit();
+ } else {
+ $langs->load("errors");
+ setEventMessages($langs->trans('ErrorsProductsMerge'), null, 'errors');
+ $db->rollback();
+ }
+ }
+ }
+ }
// Type
if ($action == 'setfk_product_type' && $usercancreate) {
@@ -2502,6 +2673,17 @@ if (($action == 'delete' && (empty($conf->use_javascript_ajax) || !empty($conf->
|| (!empty($conf->use_javascript_ajax) && empty($conf->dol_use_jmobile))) { // Always output when not jmobile nor js
$formconfirm = $form->formconfirm("card.php?id=".$object->id, $langs->trans("DeleteProduct"), $langs->trans("ConfirmDeleteProduct"), "confirm_delete", '', 0, "action-delete");
}
+if ($action == 'merge') {
+ $formquestion = array(
+ array(
+ 'name' => 'product_origin',
+ 'label' => $langs->trans('MergeOriginProduct'),
+ 'type' => 'other',
+ 'value' => $form->select_produits('', 'product_origin', '', 0, 0, 1, 2, '', 1, array(), 0, 1, 0, '', 0, '', null, 1),
+ )
+ );
+ $formconfirm = $form->formconfirm($_SERVER["PHP_SELF"]."?id=".$object->id, $langs->trans("MergeProducts"), $langs->trans("ConfirmMergeProducts"), "confirm_merge", $formquestion, 'no', 1, 250);
+}
// Clone confirmation
if (($action == 'clone' && (empty($conf->use_javascript_ajax) || !empty($conf->dol_use_jmobile))) // Output when action = clone if jmobile or no js
@@ -2569,6 +2751,9 @@ if ($action != 'create' && $action != 'edit') {
} else {
print ''.$langs->trans("Delete").'';
}
+ if (getDolGlobalInt('MAIN_FEATURES_LEVEL') > 1) {
+ print ''.$langs->trans('Merge').''."\n";
+ }
} else {
print ''.$langs->trans("Delete").'';
}
diff --git a/htdocs/product/class/product.class.php b/htdocs/product/class/product.class.php
index a7865474014..1018f43832a 100644
--- a/htdocs/product/class/product.class.php
+++ b/htdocs/product/class/product.class.php
@@ -491,8 +491,7 @@ class Product extends CommonObject
'import_key' =>array('type'=>'varchar(14)', 'label'=>'ImportId', 'enabled'=>1, 'visible'=>-2, 'notnull'=>-1, 'index'=>0, 'position'=>1000),
//'tosell' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
//'tobuy' =>array('type'=>'integer', 'label'=>'Status', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000, 'arrayofkeyval'=>array(0=>'Draft', 1=>'Active', -1=>'Cancel')),
- 'mandatory_period' =>array('type'=>'integer', 'label'=>'mandatory_period', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000),
-
+ 'mandatory_period' => array('type'=>'integer', 'label'=>'mandatory_period', 'enabled'=>1, 'visible'=>1, 'notnull'=>1, 'default'=>0, 'index'=>1, 'position'=>1000),
);
/**