Version 3.4: Fehlende Lieferantenpreise und Bugfixes

- EAN-basierte Suche fuer Cross-Katalog Lieferantenpreise
- Unique-Key auf Barcode entfernt (mehrere Lieferanten pro EAN)
- Variable $extrafields Namenskollision mit Dolibarr-Core behoben
- Duplikate bei gleichen Produkten auf mehreren Rechnungszeilen vermieden
- select2-Suche auf allen Zeilen (eindeutige HTML-IDs)
- Konsolidierter Bereich fuer fehlende Lieferantenpreise
- Refresh-Button und Alle/Keine auswaehlen

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-17 07:01:13 +01:00
parent 1b2357a2aa
commit 82d4e8a323
5 changed files with 172 additions and 107 deletions

View file

@ -1,5 +1,19 @@
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 3.4
### Bugfixes
- Fehlende Lieferantenpreise: EAN-basierte Suche nutzt jetzt Barcode aus Lieferantenpreis statt Artikelnummer
- Fehlende Lieferantenpreise: Unique-Key auf Barcode entfernt (mehrere Lieferanten koennen gleichen EAN haben)
- Fehlende Lieferantenpreise: Variable $extrafields Namenskollision mit Dolibarr-Core behoben
- Fehlende Lieferantenpreise: Duplikate bei gleichen Produkten auf mehreren Rechnungszeilen vermieden
- Produktauswahl: select2-Suche funktioniert jetzt auf allen Zeilen (eindeutige HTML-IDs)
### Verbesserungen
- Fehlende Lieferantenpreise in konsolidiertem Bereich unterhalb der Produkttabelle
- Refresh-Button fuer Produktlisten nach Anlage neuer Produkte
- Alle auswaehlen / Keine auswaehlen fuer fehlende Lieferantenpreise
## 3.3
### Sicherheit und Code-Qualitaet

View file

@ -76,7 +76,7 @@ class modImportZugferd extends DolibarrModules
$this->editor_squarred_logo = ''; // Must be image filename into the module/img directory followed with @modulename. Example: 'myimage.png@importzugferd'
// Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.3';
$this->version = '3.4';
// Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -77,6 +77,13 @@ $supplier_id = GETPOST('supplier_id', 'int');
$line_id = GETPOST('line_id', 'int');
$product_id = GETPOST('product_id', 'int');
$template_product_id = GETPOST('template_product_id', 'int');
// Zeilenspezifische Produkt-IDs (wegen eindeutiger select2-IDs pro Zeile)
if (empty($product_id) && $line_id > 0) {
$product_id = GETPOST('product_id_'.$line_id, 'int');
}
if (empty($template_product_id) && $line_id > 0) {
$template_product_id = GETPOST('template_product_id_'.$line_id, 'int');
}
// Initialize objects
$form = new Form($db);
@ -499,21 +506,34 @@ if ($action == 'removeproduct' && $line_id > 0) {
$import->fetch($id);
}
// Add missing supplier prices from other catalogs
// Fehlende Lieferantenpreise aus anderen Katalogen hinzufuegen
if ($action == 'addmissingprices' && $id > 0) {
$import->fetch($id);
$fk_product = GETPOSTINT('fk_product');
$addSupplierPrices = GETPOST('add_supplier_prices', 'array');
if ($fk_product > 0 && !empty($addSupplierPrices)) {
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
require_once './class/datanorm.class.php';
require_once './class/productmapping.class.php';
$selectedPrices = GETPOST('add_prices', 'array');
if (!empty($selectedPrices)) {
$addedCount = 0;
$processedKeys = array();
foreach ($selectedPrices as $entry) {
// Duplikate ueberspringen
if (isset($processedKeys[$entry])) {
continue;
}
$processedKeys[$entry] = true;
$parts = explode(',', $entry);
if (count($parts) !== 3) {
continue;
}
$productId = (int) $parts[0];
$socId = (int) $parts[1];
$datanormId = (int) $parts[2];
if ($productId <= 0 || $socId <= 0 || $datanormId <= 0) {
continue;
}
foreach ($addSupplierPrices as $socId => $datanormId) {
// Fetch the Datanorm article
$datanorm = new Datanorm($db);
if ($datanorm->fetch($datanormId) > 0) {
$altSupplier = new Societe($db);
@ -524,38 +544,16 @@ if ($action == 'addmissingprices' && $id > 0) {
$purchasePrice = $datanorm->price / $datanorm->price_unit;
}
// Prepare extrafields
$extrafields = array();
if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) {
$extrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge;
}
if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) {
$extrafields['options_preiseinheit'] = $datanorm->price_unit;
}
if (!empty($datanorm->product_group)) {
$extrafields['options_warengruppe'] = $datanorm->product_group;
}
// Add supplier price
$prodfourn = new ProductFournisseur($db);
$prodfourn->id = $fk_product;
$result = $prodfourn->update_buyprice(
1, $purchasePrice, $user, 'HT', $altSupplier, 0,
$datanorm->article_number, 19,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')),
!empty($datanorm->ean) ? $datanorm->ean : '',
!empty($datanorm->ean) ? 2 : 0,
$extrafields
);
$priceExtrafields = datanormBuildSupplierPriceExtrafields($datanorm);
$result = datanormAddSupplierPrice($db, $productId, $datanorm, $altSupplier, $user, $purchasePrice, 19, $priceExtrafields);
if ($result > 0) {
// Create product mapping
datanormInsertPriceExtrafields($db, $result, $priceExtrafields);
$mapping = new ProductMapping($db);
$mapping->fk_soc = $socId;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $fk_product;
$mapping->fk_product = $productId;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
@ -1968,6 +1966,7 @@ if ($action == 'edit' && $import->id > 0) {
$allProductsMatched = true;
$matchedLinesCount = 0;
$totalLinesCount = count($lines);
$allMissingPrices = array(); // Fehlende Lieferantenpreise sammeln
foreach ($lines as $line) {
$hasProduct = ($line->fk_product > 0);
@ -2069,44 +2068,48 @@ if ($action == 'edit' && $import->id > 0) {
print '<i class="fas fa-times"></i>';
print '</a>';
// Check for missing supplier prices from other catalogs
// Fehlende Lieferantenpreise aus anderen Katalogen sammeln (Anzeige weiter unten)
if ($import->fk_soc > 0 && getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL')) {
// Get existing supplier prices for this product
$sqlExistingPrices = "SELECT fk_soc FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
// Alle vorhandenen Lieferantenpreise fuer dieses Produkt laden
$sqlExistingPrices = "SELECT fk_soc, price, barcode FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlExistingPrices .= " WHERE fk_product = ".(int)$line->fk_product;
$resExistingPrices = $db->query($sqlExistingPrices);
$existingSupplierIds = array();
$currentSupplierPrice = 0;
$supplierEan = '';
if ($resExistingPrices) {
while ($objPrice = $db->fetch_object($resExistingPrices)) {
$existingSupplierIds[$objPrice->fk_soc] = true;
// Preis und EAN vom Rechnungslieferanten merken
if ($objPrice->fk_soc == $import->fk_soc) {
$currentSupplierPrice = $objPrice->price;
if (!empty($objPrice->barcode)) {
$supplierEan = $objPrice->barcode;
}
}
}
}
// Get current supplier price for comparison
$currentSupplierPrice = 0;
if (isset($existingSupplierIds[$import->fk_soc])) {
$sqlCurrentPrice = "SELECT price FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlCurrentPrice .= " WHERE fk_product = ".(int)$line->fk_product;
$sqlCurrentPrice .= " AND fk_soc = ".(int)$import->fk_soc;
$sqlCurrentPrice .= " ORDER BY unitprice ASC LIMIT 1";
$resCurrentPrice = $db->query($sqlCurrentPrice);
if ($resCurrentPrice && $db->num_rows($resCurrentPrice) > 0) {
$objCurrentPrice = $db->fetch_object($resCurrentPrice);
$currentSupplierPrice = $objCurrentPrice->price;
}
}
// EAN-Quellen: 1. Lieferantenpreis-Barcode, 2. Import-Zeile EAN
$searchEan = !empty($supplierEan) ? $supplierEan : (!empty($line->ean) ? $line->ean : '');
// Search in all Datanorm catalogs for this article
$datanormSearch = new Datanorm($db);
$searchRef = !empty($line->supplier_ref) ? $line->supplier_ref : (!empty($line->ean) ? $line->ean : '');
if (!empty($searchRef)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($searchRef, $import->fk_soc, true, 10);
$allCatalogResults = array();
// Primaer: EAN-Suche in Datanorm-Katalogen (zuverlaessigste Methode)
if (!empty($searchEan)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($searchEan, $import->fk_soc, true, 10);
}
// Fallback: Lieferanten-Artikelnummer wenn EAN nichts fand
if (empty($allCatalogResults) && !empty($line->supplier_ref)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($line->supplier_ref, $import->fk_soc, true, 10);
}
if (!empty($allCatalogResults)) {
// Filter to only show suppliers without existing price
$missingSuppliers = array();
foreach ($allCatalogResults as $catalogResult) {
if (!isset($existingSupplierIds[$catalogResult['fk_soc']])) {
// Load supplier name
$altSupplier = new Societe($db);
$altSupplier->fetch($catalogResult['fk_soc']);
@ -2128,54 +2131,20 @@ if ($action == 'edit' && $import->id > 0) {
}
}
// Show missing suppliers with checkboxes
if (!empty($missingSuppliers)) {
print '<div style="margin-top: 8px; padding: 8px; background-color: #fcf8e3; border: 1px solid #faebcc; border-radius: 4px; font-size: 0.85em;">';
print '<div style="font-weight: bold; color: #8a6d3b; margin-bottom: 5px;">';
print '<i class="fas fa-plus-circle"></i> '.$langs->trans('MissingSupplierPrices');
print '</div>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" style="margin: 0;">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="addmissingprices">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print '<input type="hidden" name="fk_product" value="'.$line->fk_product.'">';
// Hinweis in der Zeile anzeigen
print ' <span class="badge badge-warning" title="'.$langs->trans('MissingSupplierPrices').'"><i class="fas fa-plus-circle"></i> '.count($missingSuppliers).'</span>';
foreach ($missingSuppliers as $missing) {
$priceDiff = 0;
$priceDiffPercent = 0;
$diffHtml = '';
if ($currentSupplierPrice > 0) {
$priceDiff = $missing['purchase_price'] - $currentSupplierPrice;
$priceDiffPercent = ($priceDiff / $currentSupplierPrice) * 100;
if ($priceDiff < 0) {
$diffHtml = '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format(abs($priceDiffPercent), 1).'%</span>';
} elseif ($priceDiff > 0) {
$diffHtml = '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($priceDiffPercent, 1).'%</span>';
} else {
$diffHtml = '<span class="opacitymedium">=</span>';
}
}
print '<div style="padding: 3px 0;">';
print '<label style="cursor: pointer;">';
print '<input type="checkbox" name="add_supplier_prices['.$missing['fk_soc'].']" value="'.$missing['datanorm_id'].'" style="margin-right: 5px;">';
print '<strong>'.dol_escape_htmltag($missing['supplier_name']).'</strong>';
print ' <code style="font-size: 0.9em;">'.dol_escape_htmltag($missing['article_number']).'</code>';
print ' @ <strong>'.price($missing['purchase_price']).'</strong>';
if (!empty($diffHtml)) {
print ' '.$diffHtml;
}
print '</label>';
print '</div>';
// Duplikate vermeiden (gleiches Produkt auf mehreren Rechnungszeilen)
if (!isset($allMissingPrices[$line->fk_product])) {
$allMissingPrices[$line->fk_product] = array(
'product_id' => $line->fk_product,
'product_ref' => $product->ref,
'product_label' => $product->label,
'current_price' => $currentSupplierPrice,
'missing' => $missingSuppliers,
);
}
print '<button type="submit" class="button buttongen small" style="margin-top: 5px;">';
print '<i class="fas fa-plus"></i> '.$langs->trans('AddSelectedPrices');
print '</button>';
print '</form>';
print '</div>';
}
}
}
@ -2186,7 +2155,7 @@ if ($action == 'edit' && $import->id > 0) {
print '<input type="hidden" name="action" value="assignproduct">';
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print $form->select_produits('', 'product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth150 maxwidth200', 1, '', 0);
print $form->select_produits('', 'product_id_'.$line->id, '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth150 maxwidth200', 1, '', 0);
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('AssignProduct').'">';
print '<i class="fas fa-link"></i>';
print '</button>';
@ -2212,6 +2181,11 @@ if ($action == 'edit' && $import->id > 0) {
print '<i class="fas fa-plus-circle"></i> '.$langs->trans('CreateProduct');
print '</a>';
// Refresh-Button nach Produktanlage
print ' <a href="'.$_SERVER['PHP_SELF'].'?id='.$import->id.'" class="button buttongen margintoponlyshort" title="'.$langs->trans('RefreshProductListHelp').'">';
print '<i class="fas fa-sync-alt"></i>';
print '</a>';
// Product template
print '<br>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block margintoponlyshort">';
@ -2219,7 +2193,7 @@ if ($action == 'edit' && $import->id > 0) {
print '<input type="hidden" name="action" value="duplicateproduct">';
print '<input type="hidden" name="line_id" value="'.$line->id.'">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
print $form->select_produits('', 'template_product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0);
print $form->select_produits('', 'template_product_id_'.$line->id, '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0);
print ' <button type="submit" class="button buttongen" title="'.$langs->trans('ProductTemplateHelp').'">';
print '<i class="fas fa-copy"></i>';
print '</button>';
@ -2324,6 +2298,77 @@ if ($action == 'edit' && $import->id > 0) {
print '</table>';
print '</div>';
// Fehlende Lieferantenpreise - konsolidierter Bereich
if (!empty($allMissingPrices)) {
print '<div style="margin-top: 15px; padding: 12px; background-color: #fcf8e3; border: 1px solid #faebcc; border-radius: 4px;">';
print '<div class="titre" style="margin-bottom: 10px; color: #8a6d3b;">';
print '<i class="fas fa-plus-circle paddingright"></i>'.$langs->trans('MissingSupplierPrices');
$totalMissing = 0;
foreach ($allMissingPrices as $mp) {
$totalMissing += count($mp['missing']);
}
print ' <span class="badge badge-warning">'.$totalMissing.'</span>';
print '</div>';
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'">';
print '<input type="hidden" name="token" value="'.newToken().'">';
print '<input type="hidden" name="action" value="addmissingprices">';
print '<input type="hidden" name="id" value="'.$import->id.'">';
foreach ($allMissingPrices as $mpData) {
print '<div style="margin-bottom: 10px; padding: 8px; background-color: #fff; border: 1px solid #eee; border-radius: 3px;">';
print '<div style="font-weight: bold; margin-bottom: 5px;">';
print '<i class="fas fa-cube paddingright" style="color: #555;"></i>';
print dol_escape_htmltag($mpData['product_ref'].' - '.$mpData['product_label']);
if ($mpData['current_price'] > 0) {
print ' <span class="opacitymedium">('.price($mpData['current_price']).')</span>';
}
print '</div>';
foreach ($mpData['missing'] as $missing) {
$priceDiffHtml = '';
if ($mpData['current_price'] > 0) {
$pDiff = $missing['purchase_price'] - $mpData['current_price'];
$pDiffPercent = ($pDiff / $mpData['current_price']) * 100;
if ($pDiff < 0) {
$priceDiffHtml = ' <span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.number_format(abs($pDiffPercent), 1).'%</span>';
} elseif ($pDiff > 0) {
$priceDiffHtml = ' <span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.number_format($pDiffPercent, 1).'%</span>';
} else {
$priceDiffHtml = ' <span class="opacitymedium">=</span>';
}
}
// Wert: productId,socId,datanormId
$cbValue = $mpData['product_id'].','.$missing['fk_soc'].','.$missing['datanorm_id'];
print '<div style="padding: 3px 0 3px 20px;">';
print '<label style="cursor: pointer; display: block;">';
print '<input type="checkbox" name="add_prices[]" value="'.dol_escape_htmltag($cbValue).'" checked class="cb-missing-price" style="margin-right: 5px;">';
print '<strong>'.dol_escape_htmltag($missing['supplier_name']).'</strong>';
print ' <code style="font-size: 0.9em;">'.dol_escape_htmltag($missing['article_number']).'</code>';
print ' @ <strong>'.price($missing['purchase_price']).'</strong>';
print $priceDiffHtml;
print '</label>';
print '</div>';
}
print '</div>';
}
// Alle auswaehlen / Keine auswaehlen + Submit
print '<div style="margin-top: 8px;">';
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', true); return false;" class="small paddingright">'.$langs->trans('SelectAll').'</a>';
print ' / ';
print '<a href="#" onclick="$(\'.cb-missing-price\').prop(\'checked\', false); return false;" class="small paddingleft">'.$langs->trans('DeselectAll').'</a>';
print ' &nbsp; ';
print '<button type="submit" class="button buttongen">';
print '<i class="fas fa-plus paddingright"></i>'.$langs->trans('AddSelectedPrices');
print '</button>';
print '</div>';
print '</form>';
print '</div>';
}
// Datanorm Preview Section (shown when preview action was triggered)
if (!empty($datanormPreviewMatches)) {
print '<div class="div-table-responsive-no-min" style="margin-top: 20px;">';

View file

@ -486,3 +486,6 @@ AddSelectedPrices = Ausgewählte hinzufügen
SupplierPricesAdded = %s Lieferantenpreise hinzugefügt
CheaperBy = %s%% günstiger
MoreExpensiveBy = %s%% teurer
RefreshProductListHelp = Produktlisten neu laden (nach Anlage neuer Produkte)
SelectAll = Alle auswählen
DeselectAll = Keine auswählen

View file

@ -417,3 +417,6 @@ AddSelectedPrices = Add Selected
SupplierPricesAdded = %s supplier prices added
CheaperBy = %s%% cheaper
MoreExpensiveBy = %s%% more expensive
RefreshProductListHelp = Refresh product lists (after creating new products)
SelectAll = Select all
DeselectAll = Deselect all