feat(v2.0.6): Fonctionnalité de duplication de lignes produits avec gratuité

Implémentation complète de la duplication de lignes produits dans les devis :

Backend (controllers/cjxdevis.php):
- Ajout endpoint duplicate_ligne_produit avec paramètre gratuite
- Recalcul automatique des ordres lors de la duplication
- Suppression du warning "Undefined array key user"
- Gestion correcte de l'ordre des lignes (fix ordre=0)

Frontend (pub/res/js/jdevis.js):
- Bouton  pour dupliquer une ligne produit
- Duplication directe en gratuité (remise 100%) sans confirmation
- Bouton 🗑️ pour supprimer les lignes à 100% de remise
- Colorisation violet clair (rgba(138, 43, 226, 0.2)) des lignes gratuites
- Limitation à 2 occurrences max par produit ( disparaît après)
- Badge (x2) dans l'onglet Sélection pour les produits dupliqués
- Déduplication de la liste de sélection (1 produit = 1 ligne)
- Première colonne sans retour à la ligne (white-space: nowrap)
- Fix: loadProduitsDevis n'existe pas → showDevisProduits
- Fix: ReferenceError remiseProduit avant initialisation

Bugs corrigés:
- Bug #1: Warning PHP "Undefined array key user" (ligne 165)
- Bug #2: Ligne dupliquée ne s'affiche pas (ordre=0)
- Bug #3: ReferenceError loadProduitsDevis non définie
- Bug #4: ReferenceError remiseProduit avant initialisation

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-01-27 13:44:48 +01:00
parent e96ad7a244
commit 639969ca1b
2 changed files with 533 additions and 186 deletions

204
controllers/cjxdevis.php Normal file → Executable file
View File

@@ -158,6 +158,131 @@ switch ($Route->_action) {
}
break;
case "duplicate_ligne_produit":
$data = json_decode(file_get_contents("php://input"));
if (isset($data->rowid_ligne)) {
$rowidLigneSafe = intval(nettoie_input($data->rowid_ligne));
$sql = 'SELECT * FROM devis_produits WHERE rowid = ' . $rowidLigneSafe . ';';
$ligne = getinfos($sql, 'gen');
if (count($ligne) > 0) {
$l = $ligne[0];
$fk_devis = $l['fk_devis'];
// Récupérer toutes les lignes du devis pour recalculer les ordres
$sql = 'SELECT rowid, ordre FROM devis_produits WHERE fk_devis = ' . $fk_devis . ' ORDER BY rowid;';
$lignes_devis = getinfos($sql, 'gen');
// Recalculer tous les ordres si nécessaire (au cas où il y a des doublons à 0)
$ordre_actuel = 0;
$position_source = -1;
foreach ($lignes_devis as $idx => $ligne_devis) {
if ($ligne_devis['rowid'] == $rowidLigneSafe) {
$position_source = $ordre_actuel;
}
if ($ligne_devis['ordre'] != $ordre_actuel) {
$sql = 'UPDATE devis_produits SET ordre = ' . $ordre_actuel . ' WHERE rowid = ' . $ligne_devis['rowid'] . ';';
qSQL($sql, 'gen');
}
$ordre_actuel++;
}
// Le nouvel ordre sera juste après la ligne source
$nouvel_ordre = $position_source + 1;
// Décaler toutes les lignes après la position d'insertion
$sql = 'UPDATE devis_produits SET ordre = ordre + 1 WHERE fk_devis = ' . $fk_devis . ' AND ordre >= ' . $nouvel_ordre . ';';
qSQL($sql, 'gen');
$gratuite = false;
if (isset($data->gratuite) && $data->gratuite === true) {
$gratuite = true;
}
$sql = 'INSERT INTO devis_produits SET ';
$sql .= 'fk_devis = ' . $l['fk_devis'] . ', ';
$sql .= 'fk_produit = ' . $l['fk_produit'] . ', ';
$sql .= 'ordre = ' . $nouvel_ordre . ', ';
$sql .= 'code = "' . $l['code'] . '", ';
$sql .= 'libelle = "' . $l['libelle'] . '", ';
$sql .= 'qte = ' . $l['qte'] . ', ';
if ($gratuite) {
$sql .= 'prix_vente = 0, ';
$sql .= 'pu_vente_remise = 0, ';
$sql .= 'totalht = 0, ';
$sql .= 'marge = ' . (-floatval($l['prix_achat_net']) * intval($l['qte'])) . ', ';
$sql .= 'remise = 100, ';
} else {
$sql .= 'totalht = ' . $l['totalht'] . ', ';
$sql .= 'remise = ' . $l['remise'] . ', ';
$sql .= 'marge = ' . $l['marge'] . ', ';
$sql .= 'prix_vente = ' . $l['prix_vente'] . ', ';
$sql .= 'pu_vente_remise = ' . $l['pu_vente_remise'] . ', ';
}
$sql .= 'prix_achat_net = ' . $l['prix_achat_net'] . ', ';
$sql .= 'prc_discount_1 = ' . $l['prc_discount_1'] . ', ';
$sql .= 'quantite_1 = ' . $l['quantite_1'] . ', ';
$sql .= 'prc_discount_2 = ' . $l['prc_discount_2'] . ', ';
$sql .= 'quantite_2 = ' . $l['quantite_2'] . ', ';
$sql .= 'prc_discount_3 = ' . $l['prc_discount_3'] . ', ';
$sql .= 'quantite_3 = ' . $l['quantite_3'] . ', ';
$sql .= 'prc_discount_4 = ' . $l['prc_discount_4'] . ', ';
$sql .= 'quantite_4 = ' . $l['quantite_4'] . ', ';
$sql .= 'prc_discount_5 = ' . $l['prc_discount_5'] . ', ';
$sql .= 'quantite_5 = ' . $l['quantite_5'] . ', ';
$sql .= 'prc_discount_6 = ' . $l['prc_discount_6'] . ', ';
$sql .= 'quantite_6 = ' . $l['quantite_6'] . ', ';
$sql .= 'chk_variante = ' . $l['chk_variante'] . ', ';
$sql .= 'chk_prix_net = ' . $l['chk_prix_net'] . ', ';
$sql .= 'commentaire = "' . $l['commentaire'] . '";';
eLog($sql);
qSQL($sql, 'gen');
$sql = 'SELECT dp.*, pf.marge_rr, pf.marge_dv 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 .= 'WHERE dp.fk_devis = ' . $fk_devis . ' ORDER BY dp.ordre;';
echo getinfos($sql, 'gen', 'json');
} else {
echo json_encode(['success' => false, 'msg' => 'Ligne non trouvée']);
}
} else {
echo json_encode(['success' => false, 'msg' => 'rowid_ligne manquant']);
}
break;
case "delete_ligne_produit":
$data = json_decode(file_get_contents("php://input"));
if (isset($data->rowid_ligne)) {
$rowidLigneSafe = intval(nettoie_input($data->rowid_ligne));
$sql = 'SELECT fk_devis FROM devis_produits WHERE rowid = ' . $rowidLigneSafe . ';';
$ligne = getinfos($sql, 'gen');
if (count($ligne) > 0) {
$fk_devis = $ligne[0]['fk_devis'];
$sql = 'DELETE FROM devis_produits WHERE rowid = ' . $rowidLigneSafe . ';';
eLog($sql);
qSQL($sql, 'gen');
$sql = 'SELECT dp.*, pf.marge_rr, pf.marge_dv 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 .= 'WHERE dp.fk_devis = ' . $fk_devis . ' ORDER BY dp.ordre;';
echo getinfos($sql, 'gen', 'json');
} else {
echo json_encode(['success' => false, 'msg' => 'Ligne non trouvée']);
}
} else {
echo json_encode(['success' => false, 'msg' => 'rowid_ligne manquant']);
}
break;
case "load_clients_devis":
//! On récupère les infos des clients de son secteur ou de toute la France, suivant le devis chk_clients_secteur, utilisé aussi pour l'autocomplete de la recherche de clients
$data = json_decode(file_get_contents("php://input"));
@@ -843,53 +968,54 @@ switch ($Route->_action) {
$idDevis = nettoie_input($data->inpIdDevis);
eLog("save_devis final : idDevis = " . $idDevis);
// TODO: enregistrer le prix d'achat et de vente de chaque produit au moment du devis
$sql = 'SELECT rowid FROM devis_produits WHERE fk_devis = ' . $idDevis . ';';
$lignesProduits = getinfos($sql, 'gen');
//! loop sur les datas commençant par inpQte_
foreach ($data as $key => $value) {
if (substr($key, 0, 7) == "inpQte_") {
//! on a une ligne de produit
$idProd = substr($key, 7);
$qte = nettoie_input($value);
$qte = "" ? 0 : $qte;
$rem = nettoie_input($data->{"inpRemise_" . $idProd});
$rem = "" ? 0 : $rem;
if (count($lignesProduits) > 0) {
foreach ($lignesProduits as $ligne) {
$rowidLigne = $ligne['rowid'];
$ht = nettoie_input($data->{"inpHT_" . $idProd});
$ht = "" ? 0 : $ht;
// si $ht contient un espace (délimiteur millier), on le supprime
$ht = str_replace(" ", "", $ht);
if (isset($data->{"inpQte_" . $rowidLigne})) {
$qte = nettoie_input($data->{"inpQte_" . $rowidLigne});
$qte = $qte == "" ? 0 : $qte;
$rem = nettoie_input($data->{"inpRemise_" . $rowidLigne});
$rem = $rem == "" ? 0 : $rem;
$mg = nettoie_input($data->{"inpMG_" . $idProd});
$mg = "" ? 0 : $mg;
$ht = nettoie_input($data->{"inpHT_" . $rowidLigne});
$ht = $ht == "" ? 0 : $ht;
$ht = str_replace(" ", "", $ht);
$ha = nettoie_input($data->{"achat_" . $idProd});
$ha = "" ? 0 : $ha;
$ha = str_replace(" ", "", $ha);
$mg = nettoie_input($data->{"inpMG_" . $rowidLigne});
$mg = $mg == "" ? 0 : $mg;
$pv = nettoie_input($data->{"vente_" . $idProd});
$pv = "" ? 0 : $pv;
$pv = str_replace(" ", "", $pv);
$ha = nettoie_input($data->{"achat_" . $rowidLigne});
$ha = $ha == "" ? 0 : $ha;
$ha = str_replace(" ", "", $ha);
$pu = nettoie_input($data->{"inpPUVenteRem_" . $idProd});
$pu = "" ? 0 : $pu;
$pu = str_replace(" ", "", $pu);
$pv = nettoie_input($data->{"vente_" . $rowidLigne});
$pv = $pv == "" ? 0 : $pv;
$pv = str_replace(" ", "", $pv);
$varOpt = 0;
if (isset($data->{"chkVariante_" . $idProd})) {
$varOpt = 1;
$pu = nettoie_input($data->{"inpPUVenteRem_" . $rowidLigne});
$pu = $pu == "" ? 0 : $pu;
$pu = str_replace(" ", "", $pu);
$varOpt = 0;
if (isset($data->{"chkVariante_" . $rowidLigne})) {
$varOpt = 1;
}
$comment = nettoie_input($data->{"inpCom_" . $rowidLigne});
$ordre = nettoie_input($data->{"inpOrdre_" . $rowidLigne});
if ($ordre == "") {
$ordre = 0;
}
$sql = 'UPDATE devis_produits SET qte=' . $qte . ', remise=' . $rem . ', totalht=' . $ht . ', marge=' . $mg . ', prix_achat_net=' . $ha . ', ';
$sql .= 'prix_vente=' . $pv . ', pu_vente_remise=' . $pu . ', chk_variante=' . $varOpt . ', commentaire="' . $comment . '", ordre=' . $ordre . ' ';
$sql .= 'WHERE rowid = ' . $rowidLigne . ';';
eLog($sql);
qSQL($sql, "gen");
}
$comment = nettoie_input($data->{"inpCom_" . $idProd});
$ordre = nettoie_input($data->{"inpOrdre_" . $idProd});
if ($ordre == "") {
$ordre = 0;
}
$sql = 'UPDATE devis_produits SET qte=' . $qte . ', remise=' . $rem . ', totalht=' . $ht . ', marge=' . $mg . ', prix_achat_net=' . $ha . ', ';
$sql .= 'prix_vente=' . $pv . ', pu_vente_remise=' . $pu . ', chk_variante=' . $varOpt . ', commentaire="' . $comment . '", ordre=' . $ordre . ' ';
$sql .= 'WHERE fk_devis=' . $idDevis . ' AND fk_produit=' . $idProd . ';';
eLog($sql);
qSQL($sql, "gen");
}
}