feat(v2.0.3): Marchés hybrides et améliorations multiples

Fonctionnalités principales :

1. Marchés hybrides - Onglet Mercurial
   - Ajout onglet Mercurial avec style distinct (vert, gras, blanc)
   - Affichage des produits mercuriaux pour marchés hybrides
   - Filtrage automatique des produits "Hors Marché 999"
   - Documentation Phase 2 avec CAS 1 et CAS 2 de marchés hybrides
   - Règles métier pour validation différenciée (devis 100% mercurial vs mixte)

2. Corrections bugs
   - Fix flag chkChange sur onglet "Sélection Produits" (callback asynchrone)
   - Plus d'alerte intempestive après sauvegarde des produits

3. Outils de déploiement
   - Nouveau script deploy-file.sh pour déploiement unitaire (DEV/PROD)
   - Amélioration deploy-cleo.sh

4. Gestion multi-contacts (v2.0.3)
   - Contrôleur AJAX cjxcontacts.php
   - Script migration clients_contacts
   - Documentation complète

5. Documentation
   - Mise à jour TODO.md avec Phase 2 marchés hybrides
   - Mise à jour README.md v2.0.3
   - Ajout RULES.md
   - Ajout migration_clients_contacts.sql

6. Nettoyage
   - Suppression fichiers obsolètes (conf_new.php, conf_old.php, uof_linet_20250911.sql)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-11-05 15:40:06 +01:00
parent 443b0509df
commit a4d1c22a93
22 changed files with 11544 additions and 178178 deletions

View File

@@ -21,11 +21,8 @@
OR c.contact_fonction LIKE :search
ORDER BY c.libelle';
$stmt = $db->prepare($sql);
$searchParam = '%' . $search . '%';
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
$stmt->execute();
$aModel["clients"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
$aModel["clients"] = $db->fetchAll($sql, [':search' => $searchParam]);
} catch (Exception $e) {
error_log("Erreur recherche clients : " . $e->getMessage());
$aModel["clients"] = [];

View File

@@ -17,9 +17,7 @@ switch ($fkRole) {
try {
$db = Database::getInstance();
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
$stmt = $db->prepare($sql);
$stmt->execute([':fkParent' => $fkUser]);
$aRR = $stmt->fetchAll(PDO::FETCH_ASSOC);
$aRR = $db->fetchAll($sql, [':fkParent' => $fkUser]);
$rrIds = array_column($aRR, 'rowid');
if (!empty($rrIds)) {

View File

@@ -57,163 +57,172 @@ switch ($Route->_action) {
case "export_sap_devis":
$cid = nettoie_input($Route->_param1);
$cidSafe = intval($cid);
$sql = 'SELECT d.* FROM devis d WHERE d.rowid=' . $cidSafe . ';';
eLog("Export Excel SAP Devis : " . $sql);
$dev = getinfos($sql, "gen");
$devis = $dev[0];
$fileName = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".xls";
try {
$db = Database::getInstance();
$sql = 'SELECT d.* FROM devis d WHERE d.rowid = :devis_id';
$devis = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
// On affiche les données client
$fields = array("Code", "Etablissement", "Adresse 1", "Adresse 2", "Adresse 3", "Code Postal", "Ville");
$excelData = implode("\t", array_values($fields)) . "\n";
if (!$devis) {
throw new Exception("Devis non trouvé");
}
$fkClientSafe = intval($devis["fk_client"]);
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
$cli = getinfos($sql, "gen");
if (count($cli) == 0) {
// c'est un nouveau client, on affiche les données client enregistrées dans le devis
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$cli = getinfos($sql, "gen");
$client = $cli[0];
eLog("Export Excel SAP Devis : " . $cidSafe);
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
$fileName = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".xls";
// une ligne vierge de séparation
$excelData .= "\n";
// On affiche les données client
$fields = array("Code", "Etablissement", "Adresse 1", "Adresse 2", "Adresse 3", "Code Postal", "Ville");
$excelData = implode("\t", array_values($fields)) . "\n";
// Les données du contact à prendre aussi dans le devis
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$cont = getinfos($sql, "gen");
$contact = $cont[0];
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
$fkClientSafe = intval($devis["fk_client"]);
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
if ($fkClientSafe == 0) {
// Nouveau client : données depuis le devis
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid = :devis_id';
$client = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
} else {
$client = $cli[0];
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
$excelData .= "\n";
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
// Contact depuis le devis
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid = :devis_id';
$contact = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
// une ligne vierge de séparation
$excelData .= "\n";
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
// Les données du contact
$sql = 'SELECT c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
$cont = getinfos($sql, "gen");
$contact = $cont[0];
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
} else {
// Client existant : données depuis la table clients
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid = :client_id';
$client = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
}
// une ligne vierge de séparation
$excelData .= "\n";
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
$excelData .= "\n";
// On affiche les données devis
$sql = 'SELECT d.rowid, d.num_opportunite, IF(d.date_demande IS NULL OR d.date_demande="0000-00-00", "", DATE_FORMAT(d.date_demande, "%d/%m/%Y")) AS datedem, ';
$sql .= 'IF(d.date_remise IS NULL OR d.date_remise="0000-00-00", "", DATE_FORMAT(d.date_remise, "%d/%m/%Y")) AS daterem, m.libelle AS lib_marche, m.numero AS num_marche, m.nom AS nom_marche, ';
$sql .= 'IF(d.chk_devis_photos=1, "Oui", "Non") AS photos, d.commentaire, IF(d.chk_speciaux=1, "Oui", "Non") AS speciaux ';
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid=' . $cidSafe . ';';
$dev = getinfos($sql, "gen");
$devis = $dev[0];
$chkSpeciaux = $devis["speciaux"];
$fields = array("Devis", "Opportunite", "Date Demande", "Date remise client", "Marche", "Num Marche", "Nom Marche", "Avec photos", "Commentaire RR", "Speciaux");
$excelData .= implode("\t", array_values($fields)) . "\n";
// Contact lié au devis via devis.fk_contact et clients_contacts
$fkContactSafe = intval($devis["fk_contact"]);
if ($fkContactSafe > 0) {
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.rowid = :contact_id AND cc.active = 1';
$contact = $db->fetchOne($sql, [':contact_id' => $fkContactSafe]);
} else {
// Fallback : contact principal du client
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.fk_client = :client_id AND cc.chk_principal = 1 AND cc.active = 1 LIMIT 1';
$contact = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
}
array_walk($devis, 'filterData');
$excelData .= implode("\t", array_values($devis)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n";
// on affiche les totaux du devis
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$dev = getinfos($sql, "gen");
$totaux = $dev[0];
$fields = array("Total HT", "Total HT Remise", "Marge Totale");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($totaux, 'filterData');
$excelData .= implode("\t", array_values($totaux)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n";
// on affiche les produits
$sql = 'SELECT p.code, p.libelle, IF(p.prix_vente=0, "", FORMAT(p.prix_vente, 2, "fr_FR")) AS vente, ';
$sql .= 'IF(dp.qte=0, "", dp.qte) AS qute, IF(dp.remise=0, "", FORMAT(dp.remise, 2, "fr_FR")) as remise, IF(dp.pu_vente_remise=0, "", FORMAT(dp.pu_vente_remise, 2, "fr_FR")) as puventeremise, ';
$sql .= 'IF(dp.totalht=0, "", FORMAT(dp.totalht, 2, "fr_FR")) AS totht, IF(dp.marge=0, "", FORMAT(dp.marge, 2, "fr_FR")) AS marge, dp.commentaire ';
$sql .= 'FROM devis_produits dp ';
$sql .= 'LEFT JOIN produits p ON dp.fk_produit=p.rowid ';
$sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe ';
$sql .= 'LEFT JOIN x_familles xf ON pf.fk_famille=xf.rowid ';
$sql .= 'WHERE dp.fk_devis=' . $cidSafe . ' ORDER BY dp.ordre, xf.ordre, p.libelle;';
$data = getinfos($sql, "gen");
$fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire");
$excelData .= implode("\t", array_values($fields)) . "\n";
foreach ($data as $row) {
array_walk($row, 'filterData');
$excelData .= implode("\t", $row) . "\n";
}
if ($chkSpeciaux == "Oui") {
// une ligne vierge de séparation
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "PRODUITS SPECIAUX" . "\n";
$excelData .= "----" . "\n";
$sql = 'SELECT IF(ds.chk_livr_multi=1, "Oui", "Non") AS livr_multi, ds.nb_livr, DATE_FORMAT(ds.date_livr_1, "%d/%m/%Y") AS datelivr ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
$spec = getinfos($sql, "gen");
$speciaux = $spec[0];
$fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n";
$excelData .= "\n";
$fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description");
$excelData .= implode("\t", array_values($fields)) . "\n";
for ($i = 1; $i <= 5; $i++) {
$sql = 'SELECT ds.fk_produit_' . $i . ', ds.code_produit_' . $i . ', ds.lib_produit_' . $i . ', ds.qte_' . $i . ', IF(ds.surcout_' . $i . '=0, "", FORMAT(ds.surcout_' . $i . ', 2, "fr_FR")), IF(ds.chk_echantillon_' . $i . '=1, "Oui", "Non") AS echantillon, ';
$sql .= 'DATE_FORMAT(ds.date_echantillon_' . $i . ', "%d/%m/%Y") AS date_ech, ds.lib_concurrent_' . $i . ', ds.description_' . $i . ' ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
eLog($sql, "sql");
$spec = getinfos($sql, "gen");
$speciaux = $spec[0];
if ($speciaux["fk_produit_" . $i] > 0) {
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n";
if ($contact) {
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
} else {
// Aucun contact trouvé
$excelData .= "Contact Nom\tPrenom\tFonction\tFixe\tMobile\tEmail\n";
$excelData .= "\t\t\t\t\t\n";
}
}
}
// une ligne vierge de séparation
$excelData .= "\n";
// une ligne vierge de séparation
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "FIN DU DEVIS" . "\n";
$excelData .= "----" . "\n";
$excelData .= "\n";
header('Content-Type: application/vnd.ms-excel; charset=utf-16le');
header("Content-type: application/x-msexcel; charset=utf-16le");
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
echo $excelData;
// On affiche les données devis
$sql = 'SELECT d.rowid, d.num_opportunite, IF(d.date_demande IS NULL OR d.date_demande="0000-00-00", "", DATE_FORMAT(d.date_demande, "%d/%m/%Y")) AS datedem, ';
$sql .= 'IF(d.date_remise IS NULL OR d.date_remise="0000-00-00", "", DATE_FORMAT(d.date_remise, "%d/%m/%Y")) AS daterem, m.libelle AS lib_marche, m.numero AS num_marche, m.nom AS nom_marche, ';
$sql .= 'IF(d.chk_devis_photos=1, "Oui", "Non") AS photos, d.commentaire, IF(d.chk_speciaux=1, "Oui", "Non") AS speciaux ';
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid = :devis_id';
$devisData = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$chkSpeciaux = $devisData["speciaux"];
$fields = array("Devis", "Opportunite", "Date Demande", "Date remise client", "Marche", "Num Marche", "Nom Marche", "Avec photos", "Commentaire RR", "Speciaux");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($devisData, 'filterData');
$excelData .= implode("\t", array_values($devisData)) . "\n";
$excelData .= "\n";
// on affiche les totaux du devis
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid = :devis_id';
$totaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$fields = array("Total HT", "Total HT Remise", "Marge Totale");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($totaux, 'filterData');
$excelData .= implode("\t", array_values($totaux)) . "\n";
$excelData .= "\n";
// on affiche les produits
$sql = 'SELECT p.code, p.libelle, IF(p.prix_vente=0, "", FORMAT(p.prix_vente, 2, "fr_FR")) AS vente, ';
$sql .= 'IF(dp.qte=0, "", dp.qte) AS qute, IF(dp.remise=0, "", FORMAT(dp.remise, 2, "fr_FR")) as remise, IF(dp.pu_vente_remise=0, "", FORMAT(dp.pu_vente_remise, 2, "fr_FR")) as puventeremise, ';
$sql .= 'IF(dp.totalht=0, "", FORMAT(dp.totalht, 2, "fr_FR")) AS totht, IF(dp.marge=0, "", FORMAT(dp.marge, 2, "fr_FR")) AS marge, dp.commentaire ';
$sql .= 'FROM devis_produits dp ';
$sql .= 'LEFT JOIN produits p ON dp.fk_produit=p.rowid ';
$sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe ';
$sql .= 'LEFT JOIN x_familles xf ON pf.fk_famille=xf.rowid ';
$sql .= 'WHERE dp.fk_devis = :devis_id ORDER BY dp.ordre, xf.ordre, p.libelle';
$data = $db->fetchAll($sql, [':devis_id' => $cidSafe]);
$fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire");
$excelData .= implode("\t", array_values($fields)) . "\n";
foreach ($data as $row) {
array_walk($row, 'filterData');
$excelData .= implode("\t", $row) . "\n";
}
if ($chkSpeciaux == "Oui") {
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "PRODUITS SPECIAUX" . "\n";
$excelData .= "----" . "\n";
$sql = 'SELECT IF(ds.chk_livr_multi=1, "Oui", "Non") AS livr_multi, ds.nb_livr, DATE_FORMAT(ds.date_livr_1, "%d/%m/%Y") AS datelivr ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
$speciaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n";
$excelData .= "\n";
$fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description");
$excelData .= implode("\t", array_values($fields)) . "\n";
for ($i = 1; $i <= 5; $i++) {
$sql = 'SELECT ds.fk_produit_' . $i . ', ds.code_produit_' . $i . ', ds.lib_produit_' . $i . ', ds.qte_' . $i . ', IF(ds.surcout_' . $i . '=0, "", FORMAT(ds.surcout_' . $i . ', 2, "fr_FR")), IF(ds.chk_echantillon_' . $i . '=1, "Oui", "Non") AS echantillon, ';
$sql .= 'DATE_FORMAT(ds.date_echantillon_' . $i . ', "%d/%m/%Y") AS date_ech, ds.lib_concurrent_' . $i . ', ds.description_' . $i . ' ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
$produitSpecial = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
if ($produitSpecial && $produitSpecial["fk_produit_" . $i] > 0) {
array_walk($produitSpecial, 'filterData');
$excelData .= implode("\t", array_values($produitSpecial)) . "\n";
}
}
}
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "FIN DU DEVIS" . "\n";
$excelData .= "----" . "\n";
$excelData .= "\n";
header('Content-Type: application/vnd.ms-excel; charset=utf-16le');
header("Content-type: application/x-msexcel; charset=utf-16le");
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
echo $excelData;
} catch (Exception $e) {
error_log("Erreur export Excel : " . $e->getMessage());
http_response_code(500);
echo "Erreur lors de l'export du devis";
}
exit();
}