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

515
pub/res/js/jdevis.js Normal file → Executable file
View File

@@ -693,16 +693,29 @@ window.addEventListener('DOMContentLoaded', (event) => {
// au moins un produit trouvé pour ce devis
let nbProduits = ret.length
// on récupère le premier fk_produit, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle
const fkProduit1 = ret[0]['fk_produit']
// on récupère le premier rowid, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle
const rowidLigne1 = ret[0]['rowid']
// Compter les occurrences de chaque produit pour masquer le + si >= 2
const produitsCount = {}
const produitsUniques = {}
for (let key in ret) {
if (ret.hasOwnProperty(key)) {
// Récupération des valeurs de la ligne
let val = ret[key]
const fkProduit = ret[key]['fk_produit']
produitsCount[fkProduit] = (produitsCount[fkProduit] || 0) + 1
// Garder la première occurrence de chaque produit
if (!produitsUniques[fkProduit]) {
produitsUniques[fkProduit] = ret[key]
}
}
}
// On initialise le readonlyremise par produit pour gérer les cas de marché hybride où leurs produits sont en Prix Nets
readonlyRemiseProduit = readonlyRemise
// Remplir le tableau tblProduitsSelect avec des produits uniques
for (let fkProduit in produitsUniques) {
if (produitsUniques.hasOwnProperty(fkProduit)) {
let val = produitsUniques[fkProduit]
const count = produitsCount[fkProduit]
const badgeCount = count > 1 ? ' <span style="color: #8a2be2; font-weight: bold;">(x' + count + ')</span>' : ''
// Insertion d'une nouvelle ligne et création de ses colonnes : on prend ici le fk_produit
let newRowSelect = tblBodySelect.insertRow(-1)
@@ -723,18 +736,29 @@ window.addEventListener('DOMContentLoaded', (event) => {
'"/>'
let celCode = newRowSelect.insertCell(1)
celCode.innerHTML = val['code']
celCode.innerHTML = val['code'] + badgeCount
let celLibelle = newRowSelect.insertCell(2)
celLibelle.innerHTML = val['libelle']
}
}
for (let key in ret) {
if (ret.hasOwnProperty(key)) {
// Récupération des valeurs de la ligne
let val = ret[key]
// On initialise le readonlyremise par produit pour gérer les cas de marché hybride où leurs produits sont en Prix Nets
readonlyRemiseProduit = readonlyRemise
// Sur le tableau tblBodyPro
// Insertion d'une nouvelle ligne et création de ses colonnes : on prend ici le rowid de devis_produits
let newRowPro = tblBodyPro.insertRow(-1)
newRowPro.id = 'trPro_' + val['fk_produit']
newRowPro.id = 'trPro_' + val['rowid']
newRowPro.dataset.ordre = val['ordre']
newRowPro.dataset.rid = val['fk_produit']
newRowPro.dataset.rowidligne = val['rowid']
newRowPro.dataset.fkproduit = val['fk_produit']
newRowPro.dataset.code = val['code']
newRowPro.dataset.achat = val['prix_achat_net']
newRowPro.dataset.achatdiscount = val['prix_achat_net']
@@ -758,36 +782,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
newRowPro.addEventListener('drop', handleDrop)
let celCodePro = newRowPro.insertCell(-1)
const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red'
const svgComment =
'<svg id="commentProd_' +
val['fk_produit'] +
'" class="clickable" data-rid="' +
val['fk_produit'] +
'" data-code="' +
val['code'] +
'"><use xlink:href="pub/res/svg/icons.svg#message" style="fill: ' +
svgColor +
';"/></svg>'
let inputOrdreHidden =
'<input type="hidden" id="inpOrdre_' +
val['fk_produit'] +
'" name="inpOrdre_' +
val['fk_produit'] +
'" value="' +
val['ordre'] +
'" />'
let inputCommentHidden =
'<input type="hidden" id="inpCom_' +
val['fk_produit'] +
'" name="inpCom_' +
val['fk_produit'] +
'" value="' +
val['commentaire'] +
'" />'
celCodePro.innerHTML = val['code'] + ' ' + svgComment + inputOrdreHidden + inputCommentHidden
document.getElementById('commentProd_' + val['fk_produit']).addEventListener('click', showCommentProd)
celCodePro.style.whiteSpace = 'nowrap'
let celLibellePro = newRowPro.insertCell(1)
celLibellePro.innerHTML = val['libelle']
@@ -799,10 +794,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
let celQtePro = newRowPro.insertCell(3)
celQtePro.innerHTML =
'<input type="number" class="form-control numeric" id="inpQte_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpQte_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -811,15 +808,15 @@ window.addEventListener('DOMContentLoaded', (event) => {
'" value="' +
val['qte'] +
'" min="0" max="1000" step="1" style="width: 100px; text-align: right;"><input type="hidden" name="achat_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['prix_achat_net'] +
'"/><input type="hidden" name="vente_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['prix_vente'] +
'"/>'
document.getElementById('inpQte_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('inpQte_' + val['rowid']).addEventListener('change', calculDevis)
let celRemisePro = newRowPro.insertCell(4)
// Nouveau code 21/09
@@ -834,6 +831,76 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
// Fin du nouveau code du 21/09
// Colorisation si remise à 100% (gratuité)
if (parseFloat(remiseProduit) === 100) {
newRowPro.style.backgroundColor = 'rgba(138, 43, 226, 0.2)'
}
// Remplissage de la cellule code produit (on le fait ici car on a besoin de remiseProduit)
const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red'
const svgComment =
'<svg id="commentProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'"><use xlink:href="pub/res/svg/icons.svg#message" style="fill: ' +
svgColor +
';"/></svg>'
// N'afficher le + que si le produit apparaît moins de 2 fois dans le devis
let svgDuplicate = ''
if (produitsCount[val['fk_produit']] < 2) {
svgDuplicate =
'<svg id="duplicateProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'" title="Dupliquer cette ligne"><use xlink:href="pub/res/svg/icons.svg#add" style="fill: green;"/></svg>'
}
// N'afficher la trash que pour les produits dupliqués avec remise à 100%
let svgDelete = ''
if (parseFloat(remiseProduit) === 100) {
svgDelete =
'<svg id="deleteProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'" title="Supprimer cette ligne"><use xlink:href="pub/res/svg/icons.svg#trash" style="fill: red;"/></svg>'
}
let inputOrdreHidden =
'<input type="hidden" id="inpOrdre_' +
val['rowid'] +
'" name="inpOrdre_' +
val['rowid'] +
'" value="' +
val['ordre'] +
'" />'
let inputCommentHidden =
'<input type="hidden" id="inpCom_' +
val['rowid'] +
'" name="inpCom_' +
val['rowid'] +
'" value="' +
val['commentaire'] +
'" />'
celCodePro.innerHTML = val['code'] + ' ' + svgComment + ' ' + svgDuplicate + ' ' + svgDelete + inputOrdreHidden + inputCommentHidden
document.getElementById('commentProd_' + val['rowid']).addEventListener('click', showCommentProd)
if (produitsCount[val['fk_produit']] < 2) {
document.getElementById('duplicateProd_' + val['rowid']).addEventListener('click', duplicateLigneProduit)
}
if (parseFloat(remiseProduit) === 100) {
document.getElementById('deleteProd_' + val['rowid']).addEventListener('click', deleteLigneProduit)
}
// AJOUT DU 20/02/25 : on regarde si ce produit a un chk_prix_net et s'il est à 1 (marché hybride)
if (val['chk_prix_net']) {
console.log('on a un chk_prix_net : ' + val['chk_prix_net'])
@@ -845,10 +912,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
celRemisePro.innerHTML =
'<div class="input-group"><input type="number" class="form-control numeric" id="inpRemise_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpRemise_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -860,16 +929,16 @@ window.addEventListener('DOMContentLoaded', (event) => {
readonlyRemiseProduit +
'/><div class="input-group-addon">%</div></div>'
if (readonlyRemiseProduit == '') {
document.getElementById('inpRemise_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('inpRemise_' + val['rowid']).addEventListener('change', calculDevis)
}
// nouvelle colonne PU vente avec remise
let celPUVenteRemPro = newRowPro.insertCell(5)
celPUVenteRemPro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpPUVenteRem_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpPUVenteRem_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
formatAmount(val['totalht']) +
'" style="width: 100px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">&euro;</div></div>'
@@ -878,9 +947,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
let celHTPro = newRowPro.insertCell(6)
celHTPro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpHT_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpHT_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
formatAmount(val['totalht']) +
'" style="width: 100px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">&euro;</div></div>'
@@ -889,10 +958,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
celVariante.className = 'text-center'
celVariante.innerHTML =
'<input type="checkbox" id="chkVariante_' +
val['fk_produit'] +
val['rowid'] +
'" name="chkVariante_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -903,14 +974,14 @@ window.addEventListener('DOMContentLoaded', (event) => {
'" ' +
(val['chk_variante'] == 1 ? 'checked' : '') +
' />'
document.getElementById('chkVariante_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('chkVariante_' + val['rowid']).addEventListener('change', calculDevis)
let celMargePro = newRowPro.insertCell(8)
celMargePro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpMG_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpMG_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['marge'] +
'" style="width: 80px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">%</div></div>'
@@ -996,7 +1067,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_latitudeDV').value = seuilMargeDV
// On simule le changement de quantité sur la première ligne pour recalculer les totaux
const inpQte = document.getElementById('inpQte_' + fkProduit1)
const inpQte = document.getElementById('inpQte_' + rowidLigne1)
const event = new Event('change')
inpQte.dispatchEvent(event)
@@ -2070,8 +2141,17 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
// Fin de l'ajout du 26 juin 2024
// on récupère le premier fk_produit, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle
const fkProduit1 = data[0]['fk_produit']
// on récupère le premier rowid, pour simuler un changement sur ce produit pour recalculer les totaux en fin de boucle
const rowidLigne1 = data[0]['rowid']
// Compter les occurrences de chaque produit pour masquer le + si >= 2
const produitsCount = {}
for (let key in data) {
if (data.hasOwnProperty(key)) {
const fkProduit = data[key]['fk_produit']
produitsCount[fkProduit] = (produitsCount[fkProduit] || 0) + 1
}
}
for (let key in data) {
if (data.hasOwnProperty(key)) {
@@ -2084,27 +2164,28 @@ window.addEventListener('DOMContentLoaded', (event) => {
// on insère la ligne pour la saisie du commentaire au-dessus de la ligne du produit
let newRowCom = tblBodyPro.insertRow(-1)
newRowCom.className = 'hidden'
newRowCom.id = 'trCom_' + val['fk_produit']
newRowCom.dataset.rid = val['fk_produit']
newRowCom.id = 'trCom_' + val['rowid']
newRowCom.dataset.rowidligne = val['rowid']
let celCom = newRowCom.insertCell(0)
celCom.colSpan = 8
celCom.innerHTML =
'<div class="col-md-2"><label for="inpCom_' +
val['fk_produit'] +
val['rowid'] +
'">Commentaire ' +
val['code'] +
': </label></div><div class="col-md-9"><input type="text" class="form-control w-75" id="inpCom_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpCom_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['commentaire'] +
'" /></div>'
// Insertion d'une nouvelle ligne et création de ses colonnes
let newRowPro = tblBodyPro.insertRow(-1)
newRowPro.id = 'trPro_' + val['fk_produit']
newRowPro.dataset.rid = val['fk_produit']
newRowPro.id = 'trPro_' + val['rowid']
newRowPro.dataset.rowidligne = val['rowid']
newRowPro.dataset.fkproduit = val['fk_produit']
newRowPro.dataset.ordre = val['ordre']
newRowPro.dataset.code = val['code']
newRowPro.dataset.achat = val['prix_achat_net']
@@ -2124,27 +2205,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
newRowPro.dataset.quantite6 = val['quantite_6']
let celCodePro = newRowPro.insertCell(0)
const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red'
const svgComment =
'<svg id="commentProd_' +
val['fk_produit'] +
'" class="clickable" data-rid="' +
val['fk_produit'] +
'" data-code="' +
val['code'] +
'"><use xlink:href="pub/res/svg/icons.svg#message" style="fill: ' +
svgColor +
';"/></svg>'
let inputOrdreHidden =
'<input type="hidden" id="inpOrdre_' +
val['fk_produit'] +
'" name="inpOrdre_' +
val['fk_produit'] +
'" value="' +
val['ordre'] +
'" />'
celCodePro.innerHTML = val['code'] + ' ' + svgComment + inputOrdreHidden
document.getElementById('commentProd_' + val['fk_produit']).addEventListener('click', showCommentProd)
celCodePro.style.whiteSpace = 'nowrap'
let celLibellePro = newRowPro.insertCell(1)
celLibellePro.innerHTML = val['libelle']
@@ -2156,10 +2217,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
let celQtePro = newRowPro.insertCell(3)
celQtePro.innerHTML =
'<input type="number" class="form-control numeric" id="inpQte_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpQte_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -2168,15 +2231,15 @@ window.addEventListener('DOMContentLoaded', (event) => {
'" value="' +
val['qte'] +
'" min="0" max="1000" step="1" style="width: 100px; text-align: right;"/><input type="hidden" name="achat_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['prix_achat_net'] +
'"/><input type="hidden" name="vente_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['prix_vente'] +
'"/>'
document.getElementById('inpQte_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('inpQte_' + val['rowid']).addEventListener('change', calculDevis)
let celRemisePro = newRowPro.insertCell(4)
// Nouveau code 21/09
@@ -2191,6 +2254,68 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
// Fin du nouveau code du 21/09
// Colorisation si remise à 100% (gratuité)
if (parseFloat(remiseProduit) === 100) {
newRowPro.style.backgroundColor = 'rgba(138, 43, 226, 0.2)'
}
// Remplissage de la cellule code produit (on le fait ici car on a besoin de remiseProduit)
const svgColor = val['commentaire'] == '' ? 'lightgray' : 'red'
const svgComment =
'<svg id="commentProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'"><use xlink:href="pub/res/svg/icons.svg#message" style="fill: ' +
svgColor +
';"/></svg>'
// N'afficher le + que si le produit apparaît moins de 2 fois dans le devis
let svgDuplicate = ''
if (produitsCount[val['fk_produit']] < 2) {
svgDuplicate =
'<svg id="duplicateProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'" title="Dupliquer cette ligne"><use xlink:href="pub/res/svg/icons.svg#add" style="fill: green;"/></svg>'
}
// N'afficher la trash que pour les produits dupliqués avec remise à 100%
let svgDelete = ''
if (parseFloat(remiseProduit) === 100) {
svgDelete =
'<svg id="deleteProd_' +
val['rowid'] +
'" class="clickable" data-rowidligne="' +
val['rowid'] +
'" data-code="' +
val['code'] +
'" title="Supprimer cette ligne"><use xlink:href="pub/res/svg/icons.svg#trash" style="fill: red;"/></svg>'
}
let inputOrdreHidden =
'<input type="hidden" id="inpOrdre_' +
val['rowid'] +
'" name="inpOrdre_' +
val['rowid'] +
'" value="' +
val['ordre'] +
'" />'
celCodePro.innerHTML = val['code'] + ' ' + svgComment + ' ' + svgDuplicate + ' ' + svgDelete + inputOrdreHidden
document.getElementById('commentProd_' + val['rowid']).addEventListener('click', showCommentProd)
if (produitsCount[val['fk_produit']] < 2) {
document.getElementById('duplicateProd_' + val['rowid']).addEventListener('click', duplicateLigneProduit)
}
if (parseFloat(remiseProduit) === 100) {
document.getElementById('deleteProd_' + val['rowid']).addEventListener('click', deleteLigneProduit)
}
// 20/02/2025
if (val['chk_prix_net']) {
if (val['chk_prix_net'] == 1) {
@@ -2201,10 +2326,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
celRemisePro.innerHTML =
'<div class="input-group"><input type="number" class="form-control numeric" id="inpRemise_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpRemise_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -2216,16 +2343,16 @@ window.addEventListener('DOMContentLoaded', (event) => {
readonlyRemiseProduit +
' /><div class="input-group-addon">%</div></div>'
if (readonlyRemiseProduit == '') {
document.getElementById('inpRemise_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('inpRemise_' + val['rowid']).addEventListener('change', calculDevis)
}
// nouvelle colonne PU vente avec remise
let celPUVenteRemPro = newRowPro.insertCell(5)
celPUVenteRemPro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpPUVenteRem_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpPUVenteRem_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
formatAmount(val['totalht']) +
'" style="width: 100px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">&euro;</div></div>'
@@ -2234,9 +2361,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
let celHTPro = newRowPro.insertCell(6)
celHTPro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpHT_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpHT_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
formatAmount(val['totalht']) +
'" style="width: 100px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">&euro;</div></div>'
@@ -2245,10 +2372,12 @@ window.addEventListener('DOMContentLoaded', (event) => {
celVariante.className = 'text-center'
celVariante.innerHTML =
'<input type="checkbox" id="chkVariante_' +
val['fk_produit'] +
val['rowid'] +
'" name="chkVariante_' +
val['fk_produit'] +
'" data-rid="' +
val['rowid'] +
'" data-rowidligne="' +
val['rowid'] +
'" data-fkproduit="' +
val['fk_produit'] +
'" data-achat="' +
val['prix_achat_net'] +
@@ -2259,14 +2388,14 @@ window.addEventListener('DOMContentLoaded', (event) => {
'" ' +
(val['chk_variante'] == 1 ? 'checked' : '') +
' />'
document.getElementById('chkVariante_' + val['fk_produit']).addEventListener('change', calculDevis)
document.getElementById('chkVariante_' + val['rowid']).addEventListener('change', calculDevis)
let celMargePro = newRowPro.insertCell(8)
celMargePro.innerHTML =
'<div class="input-group"><input type="text" class="form-control numeric" id="inpMG_' +
val['fk_produit'] +
val['rowid'] +
'" name="inpMG_' +
val['fk_produit'] +
val['rowid'] +
'" value="' +
val['marge'] +
'" style="width: 80px; text-align: right;" readonly tabindex="-1" /><div class="input-group-addon">%</div></div>'
@@ -2358,23 +2487,115 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_latitudeDV').value = seuilMargeDV
// On simule le changement de quantité sur la première ligne pour recalculer les totaux
const inpQte = document.getElementById('inpQte_' + fkProduit1)
const inpQte = document.getElementById('inpQte_' + rowidLigne1)
const event = new Event('change')
inpQte.dispatchEvent(event)
}
}
let showCommentProd = function () {
console.log('click sur le SVG commentProd de la ligne ' + this.dataset.rid)
document.getElementById('inp_commentProdId').value = this.dataset.rid
console.log('click sur le SVG commentProd de la ligne ' + this.dataset.rowidligne)
document.getElementById('inp_commentProdId').value = this.dataset.rowidligne
document.getElementById('modCommentProdTitre').innerHTML = 'Commentaire sur le produit ' + this.dataset.code
const inpComment = document.getElementById('inp_commentProd')
inpComment.value = document.getElementById('inpCom_' + this.dataset.rid).value
inpComment.value = document.getElementById('inpCom_' + this.dataset.rowidligne).value
showModal(document.getElementById('modalCommentProd'))
inpComment.focus()
return false
}
let duplicateLigneProduit = function () {
const rowidLigne = this.dataset.rowidligne
const codeProduit = this.dataset.code
console.log('Duplication de la ligne ' + rowidLigne + ' - produit ' + codeProduit)
// Duplication directe en gratuité (remise 100%)
duplicateLigne(rowidLigne, true)
return false
}
function duplicateLigne(rowidLigne, gratuite) {
showLoading()
const data = {
rowid_ligne: rowidLigne,
gratuite: gratuite
}
fetch('/jxdevis/duplicate_ligne_produit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((response) => {
if (!response.ok) {
hideLoading()
showNotification('Erreur', 'La duplication de la ligne a échoué', 'error')
throw new Error('Erreur lors de la duplication')
}
return response.json()
})
.then((ret) => {
hideLoading()
const message = gratuite ? 'Ligne dupliquée en GRATUITÉ' : 'Ligne dupliquée avec succès'
showNotification('Succès', message, 'success')
showDevisProduits(ret)
chkChange = 1
})
.catch((error) => {
hideLoading()
console.error('Erreur:', error)
showNotification('Erreur', 'Une erreur est survenue lors de la duplication', 'error')
})
}
let deleteLigneProduit = function () {
const rowidLigne = this.dataset.rowidligne
const codeProduit = this.dataset.code
console.log('Suppression de la ligne ' + rowidLigne + ' - produit ' + codeProduit)
if (!confirm('Êtes-vous sûr de vouloir supprimer cette ligne ?\n\nProduit : ' + codeProduit)) {
return false
}
showLoading()
const data = {
rowid_ligne: rowidLigne
}
fetch('/jxdevis/delete_ligne_produit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((response) => {
if (!response.ok) {
hideLoading()
showNotification('Erreur', 'La suppression de la ligne a échoué', 'error')
throw new Error('Erreur lors de la suppression')
}
return response.json()
})
.then((ret) => {
hideLoading()
showNotification('Succès', 'Ligne supprimée avec succès', 'success')
showDevisProduits(ret)
chkChange = 1
})
.catch((error) => {
hideLoading()
console.error('Erreur:', error)
showNotification('Erreur', 'Une erreur est survenue lors de la suppression', 'error')
})
return false
}
function controlRemisesProduits(totalHT) {
// Contrôle des remises du marché en fonction du total HT du devis
@@ -2424,8 +2645,8 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
}
function getPrixAchatAvecDiscount(idProduit, qte) {
let trPro = document.getElementById('trPro_' + idProduit)
function getPrixAchatAvecDiscount(rowidLigne, qte) {
let trPro = document.getElementById('trPro_' + rowidLigne)
const prixAchat = parseFloat(trPro.dataset.achat)
const qtt = parseInt(qte, 10)
// console.log("==== Début de getPrixAchatAvecDiscount pour la ref " + trPro.dataset.code + " : prix achat " + prixAchat + " et qte " + qtt);
@@ -2448,8 +2669,8 @@ window.addEventListener('DOMContentLoaded', (event) => {
// on applique le prc_discount
prixAchatDiscount = prixAchat - (prixAchat * dscnt) / 100
console.log(
'=== idProduit : ' +
idProduit +
'=== rowidLigne : ' +
rowidLigne +
' (prc_discount_' +
inc +
' applique de ' +
@@ -2474,15 +2695,15 @@ window.addEventListener('DOMContentLoaded', (event) => {
let calculDevis = function () {
console.log('calculDevis...')
const idProduit = this.dataset.rid
const rowidLigne = this.dataset.rowidligne
// On récupère toutes les infos de ce produit au niveau de sa ligne trPro_XX stockées en dataset
let trPro = document.getElementById('trPro_' + idProduit)
let trPro = document.getElementById('trPro_' + rowidLigne)
const code = trPro.dataset.code
let prixAchat = trPro.dataset.achat
const prixVente = trPro.dataset.vente
// console.log("idProduit: " + idProduit + ", code : " + code + ", prixAchat: " + prixAchat + ", prixVente: " + prixVente);
// console.log("rowidLigne: " + rowidLigne + ", code : " + code + ", prixAchat: " + prixAchat + ", prixVente: " + prixVente);
let qte = 0
let remise = 0
@@ -2491,20 +2712,20 @@ window.addEventListener('DOMContentLoaded', (event) => {
if (this.name.indexOf('inpQte') > -1) {
// c'est la quantité qui a changé
qte = this.value
remise = document.getElementById('inpRemise_' + idProduit).value
variante = document.getElementById('chkVariante_' + idProduit).checked
remise = document.getElementById('inpRemise_' + rowidLigne).value
variante = document.getElementById('chkVariante_' + rowidLigne).checked
typeInput = 'qte'
} else if (this.name.indexOf('inpRemise') > -1) {
// c'est la remise qui a changé
qte = document.getElementById('inpQte_' + idProduit).value
qte = document.getElementById('inpQte_' + rowidLigne).value
remise = this.value
variante = document.getElementById('chkVariante_' + idProduit).checked
variante = document.getElementById('chkVariante_' + rowidLigne).checked
typeInput = 'remise'
chkSaisieRemise = true
} else if (this.name.indexOf('chkVariante') > -1) {
// c'est la variante qui a changé
qte = document.getElementById('inpQte_' + idProduit).value
remise = document.getElementById('inpRemise_' + idProduit).value
qte = document.getElementById('inpQte_' + rowidLigne).value
remise = document.getElementById('inpRemise_' + rowidLigne).value
variante = this.checked
typeInput = 'variante'
}
@@ -2526,7 +2747,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
totalHt = (prixVente * 1 - remiseProduit * 1) * (qte * 1)
}
let inpHT = document.getElementById('inpHT_' + idProduit)
let inpHT = document.getElementById('inpHT_' + rowidLigne)
inpHT.value = parseFloat(totalHt).toFixed(2)
// Modif du 25/04 : on calcule la marge même si c'est une variante / option
@@ -2552,8 +2773,8 @@ window.addEventListener('DOMContentLoaded', (event) => {
} else {
txMarge = 0
console.log(
'ERREUR idProduit : ' +
idProduit +
'ERREUR rowidLigne : ' +
rowidLigne +
', code : ' +
code +
' - prixAchat : ' +
@@ -2569,22 +2790,22 @@ window.addEventListener('DOMContentLoaded', (event) => {
)
}
//}
let inpMG = document.getElementById('inpMG_' + idProduit)
let inpMG = document.getElementById('inpMG_' + rowidLigne)
inpMG.value = parseFloat(txMarge).toFixed(2)
console.log('Boucle 1 : calcul Total HT sans remise')
//! on boucle sur tous les éléments dont le name commence par inpQte_ pour calculer le total HT sans remise
for (let i = 0, elInp; (elInp = document.querySelectorAll("[name ^= 'inpQte_' ]")[i]); i++) {
const idProd = elInp.dataset.rid
const ligne = document.getElementById('trPro_' + idProd)
const rowidLigneProd = elInp.dataset.rowidligne
const ligne = document.getElementById('trPro_' + rowidLigneProd)
const code = ligne.dataset.code
const vente = ligne.dataset.vente * 1
const qte = elInp.value * 1
// Mise à jour du 09/11 : on calcule le prix d'achat du produit avec éventuel discount suivant sa qté
const achat = getPrixAchatAvecDiscount(idProd, elInp.value)
const achat = getPrixAchatAvecDiscount(rowidLigneProd, elInp.value)
// Fin de la mise à jour du 09/11
const varOption = document.getElementById('chkVariante_' + idProd).checked
const varOption = document.getElementById('chkVariante_' + rowidLigneProd).checked
if (!varOption) {
// calcul avec juste la quantité et le prix de vente
const vente = elInp.dataset.vente * 1
@@ -2593,14 +2814,14 @@ window.addEventListener('DOMContentLoaded', (event) => {
// Mise à jour du 03/11/2023 : nouvelle colonne du Prix de Vente Unitaire (avec remise)
// On met à jour le PUVenteRem sur la ligne
const remProd = document.getElementById('inpRemise_' + idProd).value
const remProd = document.getElementById('inpRemise_' + rowidLigneProd).value
const remise = remProd * 1
let puVenteApresRemise = vente
if (remise > 0) {
puVenteApresRemise = vente - (vente * remise) / 100
}
document.getElementById('inpPUVenteRem_' + idProd).value = puVenteApresRemise.toFixed(2)
document.getElementById('inpPUVenteRem_' + rowidLigneProd).value = puVenteApresRemise.toFixed(2)
console.log('--- 1 Produit : ' + code + ' - PUVenteApresRemise : ' + puVenteApresRemise)
// Fin de la mise à jour du 03/11/2023
@@ -2638,7 +2859,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
)
}
//}
let inpMG = document.getElementById('inpMG_' + idProd)
let inpMG = document.getElementById('inpMG_' + rowidLigneProd)
inpMG.value = parseFloat(txMarge).toFixed(2)
}
@@ -2663,8 +2884,8 @@ window.addEventListener('DOMContentLoaded', (event) => {
//! on boucle sur tous les éléments dont le name commence par inpRemise_ pour calculer le total HT avec remise
for (let i = 0, elInp; (elInp = document.querySelectorAll("[name ^= 'inpRemise_' ]")[i]); i++) {
// calcul avec la quantité, le prix de vente et la remise
const idProd = elInp.dataset.rid
const ligne = document.getElementById('trPro_' + idProd)
const rowidLigneProd = elInp.dataset.rowidligne
const ligne = document.getElementById('trPro_' + rowidLigneProd)
const vente = ligne.dataset.vente * 1
const achat = ligne.dataset.achatdiscount * 1
const rem = elInp.value * 1
@@ -2689,9 +2910,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
// elInp.readOnly = false;
}
const varOption = document.getElementById('chkVariante_' + idProd).checked
const varOption = document.getElementById('chkVariante_' + rowidLigneProd).checked
if (!varOption) {
const inpQte = document.getElementById('inpQte_' + idProd)
const inpQte = document.getElementById('inpQte_' + rowidLigneProd)
const qte = inpQte.value
const remiseProduit = (remise * vente) / 100
@@ -2701,8 +2922,8 @@ window.addEventListener('DOMContentLoaded', (event) => {
console.log(
'--- 2 ligne code ' +
ligne.dataset.code +
' = idProd : ' +
idProd +
' = rowidLigneProd : ' +
rowidLigneProd +
', vente : ' +
vente +
', achat : ' +
@@ -3473,7 +3694,7 @@ window.addEventListener('DOMContentLoaded', (event) => {
inputs.forEach((input) => (input.disabled = false))
// Ajouter un événement click à l'élément svg
const svgElement = dropElem.querySelector('#commentProd_' + dropElem.dataset.rid)
const svgElement = dropElem.querySelector('#commentProd_' + dropElem.dataset.rowidligne)
svgElement.addEventListener('click', showCommentProd)
addDnDHandlers(dropElem)
@@ -3495,9 +3716,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
const sonCode = row.dataset.code
if (sonCode) {
row.dataset.ordre = index + 1
const fkProduit = row.dataset.rid
const rowidLigne = row.dataset.rowidligne
console.log('index : ' + index + ' code : ' + sonCode)
document.getElementById('inpOrdre_' + fkProduit).value = index
document.getElementById('inpOrdre_' + rowidLigne).value = index
}
})
showNotification(