Version 3.2: Cross-Katalog-Suche und Multi-Lieferanten-Preise

- Cross-Katalog-Suche: Artikel werden via EAN/Hersteller-Art.Nr. in allen Katalogen gefunden
- Multi-Lieferanten-Anzeige mit Preisvergleich (Prozent guenstiger/teurer)
- Fehlende Lieferantenpreise werden bei zugeordneten Produkten angeboten
- Fix: Kluxen-Datanorm Preise im A-Record (Cent -> Euro Umrechnung)
- Neue Uebersetzungen fuer Lieferanten-Alternativen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Eduard Wisch 2026-02-11 17:39:02 +01:00
parent 9e8165f1ab
commit 63dd72f5be
7 changed files with 610 additions and 26 deletions

View file

@ -1,5 +1,21 @@
# CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org) # CHANGELOG MODULE IMPORTZUGFERD FOR [DOLIBARR ERP CRM](https://www.dolibarr.org)
## 3.2
### Neue Funktionen
- Cross-Katalog-Suche: Artikel werden ueber EAN/Hersteller-Artikelnummer in allen Lieferanten-Katalogen gefunden
- Multi-Lieferanten-Anzeige: Bei Produktzuordnung werden alle verfuegbaren Lieferanten mit Preisen angezeigt
- Fehlende Lieferantenpreise: Bei zugeordneten Produkten werden fehlende EK-Preise anderer Lieferanten angeboten
- Preisvergleich mit Prozentangabe (guenstiger/teurer) fuer Lieferanten-Alternativen
### Bugfixes
- Datanorm Import: Kluxen-Format (Preise im A-Record in Cent) wird jetzt korrekt verarbeitet
- Datanorm Import: Preise aus A-Record werden von Cent in Euro umgerechnet (geteilt durch 100)
### Hinweise
- Kluxen-Katalog enthaelt nur Listenpreise (UVP), keine Netto-Einkaufspreise
- Cross-Katalog-Suche erfordert aktivierte Einstellung "In allen Lieferanten-Katalogen suchen"
## 2.1 ## 2.1
### Bugfixes ### Bugfixes

View file

@ -510,44 +510,56 @@ class Datanorm extends CommonObject
global $conf; global $conf;
$results = array(); $results = array();
$foundEan = '';
$foundManufacturerRef = '';
$foundIds = array(); // Track found IDs to avoid duplicates
// First try exact match with specified supplier // First try exact match with specified supplier
if ($fk_soc > 0) { if ($fk_soc > 0) {
$result = $this->fetchByArticleNumber($fk_soc, $article_number); $result = $this->fetchByArticleNumber($fk_soc, $article_number);
if ($result > 0) { if ($result > 0) {
$results[] = $this->toArray(); $results[] = $this->toArray();
$foundIds[$this->id] = true;
// Store EAN and manufacturer_ref for cross-catalog search
$foundEan = $this->ean;
$foundManufacturerRef = $this->manufacturer_ref;
// If not searching all catalogs, return immediately
if (!$searchAll) {
return $results; return $results;
} }
} }
}
// Search partial match // If searchAll is enabled and we found article with EAN/manufacturer_ref,
// search other catalogs using these identifiers (cross-catalog search)
if ($searchAll && $fk_soc > 0 && (!empty($foundEan) || !empty($foundManufacturerRef))) {
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,"; $sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,"; $sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
$sql .= " price, price_unit, discount_group, product_group, matchcode"; $sql .= " price, price_unit, discount_group, product_group, matchcode";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element; $sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'"; $sql .= " WHERE (";
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'";
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')"; $conditions = array();
if (!empty($foundEan)) {
$conditions[] = "ean = '" . $this->db->escape($foundEan) . "'";
}
if (!empty($foundManufacturerRef)) {
$conditions[] = "manufacturer_ref = '" . $this->db->escape($foundManufacturerRef) . "'";
}
$sql .= implode(' OR ', $conditions) . ")";
$sql .= " AND active = 1"; $sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity; $sql .= " AND entity = " . (int) $conf->entity;
$sql .= " AND fk_soc != " . (int) $fk_soc; // Exclude already found supplier
if ($fk_soc > 0 && !$searchAll) { $sql .= " ORDER BY price ASC"; // Show cheapest alternatives first
$sql .= " AND fk_soc = " . (int) $fk_soc; $sql .= " LIMIT " . (int) ($limit - count($results));
}
// ORDER BY clause
if ($fk_soc > 0 && $searchAll) {
// Order by matching supplier first
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, article_number";
} else {
$sql .= " ORDER BY article_number";
}
$sql .= " LIMIT " . (int) $limit;
$resql = $this->db->query($sql); $resql = $this->db->query($sql);
if ($resql) { if ($resql) {
while ($obj = $this->db->fetch_object($resql)) { while ($obj = $this->db->fetch_object($resql)) {
if (!isset($foundIds[$obj->rowid])) {
$results[] = array( $results[] = array(
'id' => $obj->rowid, 'id' => $obj->rowid,
'fk_soc' => $obj->fk_soc, 'fk_soc' => $obj->fk_soc,
@ -564,6 +576,65 @@ class Datanorm extends CommonObject
'product_group' => $obj->product_group, 'product_group' => $obj->product_group,
'matchcode' => $obj->matchcode, 'matchcode' => $obj->matchcode,
); );
$foundIds[$obj->rowid] = true;
}
}
$this->db->free($resql);
}
// If we found results via cross-catalog search, return them
if (!empty($results)) {
return $results;
}
}
// Fallback: Search by partial match on article_number, ean, or manufacturer_ref
$sql = "SELECT rowid, fk_soc, article_number, short_text1, short_text2,";
$sql .= " ean, manufacturer_ref, manufacturer_name, unit_code,";
$sql .= " price, price_unit, discount_group, product_group, matchcode";
$sql .= " FROM " . MAIN_DB_PREFIX . $this->table_element;
$sql .= " WHERE (article_number LIKE '" . $this->db->escape($article_number) . "%'";
$sql .= " OR ean = '" . $this->db->escape($article_number) . "'";
$sql .= " OR manufacturer_ref LIKE '" . $this->db->escape($article_number) . "%')";
$sql .= " AND active = 1";
$sql .= " AND entity = " . (int) $conf->entity;
if ($fk_soc > 0 && !$searchAll) {
$sql .= " AND fk_soc = " . (int) $fk_soc;
}
// ORDER BY clause
if ($fk_soc > 0 && $searchAll) {
// Order by matching supplier first, then by price
$sql .= " ORDER BY CASE WHEN fk_soc = " . (int) $fk_soc . " THEN 0 ELSE 1 END, price ASC";
} else {
$sql .= " ORDER BY article_number";
}
$sql .= " LIMIT " . (int) $limit;
$resql = $this->db->query($sql);
if ($resql) {
while ($obj = $this->db->fetch_object($resql)) {
if (!isset($foundIds[$obj->rowid])) {
$results[] = array(
'id' => $obj->rowid,
'fk_soc' => $obj->fk_soc,
'article_number' => $obj->article_number,
'short_text1' => $obj->short_text1,
'short_text2' => $obj->short_text2,
'ean' => $obj->ean,
'manufacturer_ref' => $obj->manufacturer_ref,
'manufacturer_name' => $obj->manufacturer_name,
'unit_code' => $obj->unit_code,
'price' => $obj->price,
'price_unit' => $obj->price_unit,
'discount_group' => $obj->discount_group,
'product_group' => $obj->product_group,
'matchcode' => $obj->matchcode,
);
$foundIds[$obj->rowid] = true;
}
} }
$this->db->free($resql); $this->db->free($resql);
} }

View file

@ -435,13 +435,23 @@ class DatanormParser
$firstField = trim($parts[0] ?? ''); $firstField = trim($parts[0] ?? '');
if ($firstField === 'A' && isset($parts[1]) && strlen(trim($parts[1])) <= 2) { if ($firstField === 'A' && isset($parts[1]) && strlen(trim($parts[1])) <= 2) {
// Sonepar format with action code (N=New, L=Delete, A=Update) // Sonepar/Kluxen format with action code (N=New, L=Delete, A=Update)
// A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;LangTextKey // A;N;ArtNr;TextKz;Kurztext1;Kurztext2;PreisKz;PE;ME;Preis;RabGrp;WG;LangTextKey
// PE is at index 7 and is a CODE (0=1, 1=10, 2=100, 3=1000) // PE is at index 7 and is a CODE (0=1, 1=10, 2=100, 3=1000)
// Preis is at index 9 - in CENTS (e.g., 27800 = 278,00 €)
$actionCode = strtoupper(trim($parts[1] ?? 'N')); $actionCode = strtoupper(trim($parts[1] ?? 'N'));
$peCode = (int)trim($parts[7] ?? '0'); $peCode = (int)trim($parts[7] ?? '0');
$priceUnit = self::convertPriceUnitCode($peCode); $priceUnit = self::convertPriceUnitCode($peCode);
// Price from A-record (for formats without separate DATPREIS file like Kluxen)
// Price is in cents, convert to euros
$priceRaw = trim($parts[9] ?? '0');
$price = 0.0;
if (!empty($priceRaw) && is_numeric($priceRaw)) {
// Price is in cents (integer without decimal), convert to euros
$price = (float)$priceRaw / 100;
}
$article = array( $article = array(
'article_number' => trim($parts[2] ?? ''), 'article_number' => trim($parts[2] ?? ''),
'action_code' => $actionCode, // N=New, A=Update, L=Delete 'action_code' => $actionCode, // N=New, A=Update, L=Delete
@ -458,7 +468,7 @@ class DatanormParser
'manufacturer_name' => '', 'manufacturer_name' => '',
'ean' => '', 'ean' => '',
'long_text' => '', 'long_text' => '',
'price' => 0, 'price' => $price, // Price from A-record (in euros)
); );
} else { } else {
// Standard format: A;ArtNr;Matchcode;Kurztext1;Kurztext2;ME;PE;RabGrp;WG;... // Standard format: A;ArtNr;Matchcode;Kurztext1;Kurztext2;ME;PE;RabGrp;WG;...

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' $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' // Possible values for version are: 'development', 'experimental', 'dolibarr', 'dolibarr_deprecated', 'experimental_deprecated' or a version string like 'x.y.z'
$this->version = '3.0'; $this->version = '3.2';
// Url to the file with your last numberversion of this module // Url to the file with your last numberversion of this module
//$this->url_last_version = 'http://www.example.com/versionmodule.txt'; //$this->url_last_version = 'http://www.example.com/versionmodule.txt';

View file

@ -391,6 +391,80 @@ if ($action == 'removeproduct' && $line_id > 0) {
$import->fetch($id); $import->fetch($id);
} }
// Add missing supplier prices from other catalogs
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';
$addedCount = 0;
foreach ($addSupplierPrices as $socId => $datanormId) {
// Fetch the Datanorm article
$datanorm = new Datanorm($db);
if ($datanorm->fetch($datanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($socId);
$purchasePrice = $datanorm->price;
if ($datanorm->price_unit > 1) {
$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
);
if ($result > 0) {
// Create product mapping
$mapping = new ProductMapping($db);
$mapping->fk_soc = $socId;
$mapping->supplier_ref = $datanorm->article_number;
$mapping->fk_product = $fk_product;
$mapping->ean = $datanorm->ean;
$mapping->manufacturer_ref = $datanorm->manufacturer_ref;
$mapping->description = $datanorm->short_text1;
$mapping->create($user);
$addedCount++;
}
}
}
if ($addedCount > 0) {
setEventMessages($langs->trans('SupplierPricesAdded', $addedCount), null, 'mesgs');
}
}
$action = 'edit';
}
// Update supplier // Update supplier
if ($action == 'setsupplier' && $id > 0) { if ($action == 'setsupplier' && $id > 0) {
$import->fetch($id); $import->fetch($id);
@ -883,6 +957,70 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
if ($productExists && $existingProductId > 0) { if ($productExists && $existingProductId > 0) {
// Product exists - just assign it to the line // Product exists - just assign it to the line
$lineObj->setProduct($existingProductId, 'datanorm', $user); $lineObj->setProduct($existingProductId, 'datanorm', $user);
// Add additional supplier prices from selected alternatives (for existing products too)
$supplierPricesPost = GETPOST('supplier_prices', 'array');
if (!empty($supplierPricesPost[$lineObj->id])) {
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
// Check if supplier price already exists for this product/supplier
$sqlCheckSupplier = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlCheckSupplier .= " WHERE fk_product = ".(int)$existingProductId;
$sqlCheckSupplier .= " AND fk_soc = ".(int)$altSocId;
$resCheckSupplier = $db->query($sqlCheckSupplier);
if ($resCheckSupplier && $db->num_rows($resCheckSupplier) > 0) {
continue; // Skip if supplier price already exists
}
// Fetch the alternative Datanorm article
$altDatanorm = new Datanorm($db);
if ($altDatanorm->fetch($altDatanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altSocId);
$altPurchasePrice = $altDatanorm->price;
if ($altDatanorm->price_unit > 1) {
$altPurchasePrice = $altDatanorm->price / $altDatanorm->price_unit;
}
// Prepare extrafields
$altExtrafields = array();
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
$altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
}
if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
}
if (!empty($altDatanorm->product_group)) {
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
}
// Add supplier price
$altProdfourn = new ProductFournisseur($db);
$altProdfourn->id = $existingProductId;
$altProdfourn->update_buyprice(
1, $altPurchasePrice, $user, 'HT', $altSupplier, 0,
$altDatanorm->article_number, $lineObj->tax_percent ?: 19,
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
!empty($altDatanorm->ean) ? 2 : 0,
$altExtrafields
);
// Create product mapping
$altMapping = new ProductMapping($db);
$altMapping->fk_soc = $altSocId;
$altMapping->supplier_ref = $altDatanorm->article_number;
$altMapping->fk_product = $existingProductId;
$altMapping->ean = $altDatanorm->ean;
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
$altMapping->description = $altDatanorm->short_text1;
$altMapping->create($user);
}
}
}
$assignedCount++; $assignedCount++;
} else { } else {
// Create new product // Create new product
@ -1025,6 +1163,71 @@ if ($action == 'createallfromdatanorm' && $id > 0) {
$mapping->description = $datanorm->short_text1; $mapping->description = $datanorm->short_text1;
$mapping->create($user); $mapping->create($user);
// Add additional supplier prices from selected alternatives
$supplierPricesPost = GETPOST('supplier_prices', 'array');
if (!empty($supplierPricesPost[$lineObj->id])) {
foreach ($supplierPricesPost[$lineObj->id] as $altSocId => $altDatanormId) {
// Skip the main invoice supplier (already added above)
if ($altSocId == $import->fk_soc) {
continue;
}
// Fetch the alternative Datanorm article
$altDatanorm = new Datanorm($db);
if ($altDatanorm->fetch($altDatanormId) > 0) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altSocId);
$altPurchasePrice = $altDatanorm->price;
if ($altDatanorm->price_unit > 1) {
$altPurchasePrice = $altDatanorm->price / $altDatanorm->price_unit;
}
// Prepare extrafields for alternative supplier price
$altExtrafields = array();
if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0) {
$altExtrafields['options_kupferzuschlag'] = $altDatanorm->metal_surcharge;
}
if (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) {
$altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit;
}
if (!empty($altDatanorm->product_group)) {
$altExtrafields['options_warengruppe'] = $altDatanorm->product_group;
}
// Add supplier price for alternative supplier
$altProdfourn = new ProductFournisseur($db);
$altProdfourn->id = $newproduct->id;
$altProdfourn->update_buyprice(
1, // Quantity
$altPurchasePrice, // Price
$user,
'HT', // Price base
$altSupplier, // Alternative supplier
0, // Availability
$altDatanorm->article_number, // Supplier ref
$lineObj->tax_percent ?: 19, // VAT
0, 0, 0, 0, 0, 0, array(), '',
0, 'HT', 1, '',
trim($altDatanorm->short_text1 . ($altDatanorm->short_text2 ? ' ' . $altDatanorm->short_text2 : '')),
!empty($altDatanorm->ean) ? $altDatanorm->ean : '',
!empty($altDatanorm->ean) ? 2 : 0,
$altExtrafields
);
// Create product mapping for alternative supplier
$altMapping = new ProductMapping($db);
$altMapping->fk_soc = $altSocId;
$altMapping->supplier_ref = $altDatanorm->article_number;
$altMapping->fk_product = $newproduct->id;
$altMapping->ean = $altDatanorm->ean;
$altMapping->manufacturer_ref = $altDatanorm->manufacturer_ref;
$altMapping->description = $altDatanorm->short_text1;
$altMapping->create($user);
}
}
}
// Assign to import line // Assign to import line
$lineObj->setProduct($newproduct->id, 'datanorm', $user); $lineObj->setProduct($newproduct->id, 'datanorm', $user);
$createdCount++; $createdCount++;
@ -1091,14 +1294,15 @@ if ($action == 'previewdatanorm' && $id > 0) {
continue; continue;
} }
// Search in Datanorm database // Search in Datanorm database - get ALL supplier alternatives
$results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1); $results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 10);
if (empty($results) && !empty($lineObj->ean)) { if (empty($results) && !empty($lineObj->ean)) {
$results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1); $results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 10);
} }
if (!empty($results)) { if (!empty($results)) {
// Process the primary result (first = current supplier or cheapest)
$datanormArticle = $results[0]; $datanormArticle = $results[0];
$datanorm->fetch($datanormArticle['id']); $datanorm->fetch($datanormArticle['id']);
@ -1164,6 +1368,32 @@ if ($action == 'previewdatanorm' && $id > 0) {
} }
} }
// Build supplier alternatives array
$supplierAlternatives = array();
foreach ($results as $altResult) {
$altSupplier = new Societe($db);
$altSupplier->fetch($altResult['fk_soc']);
$altPurchasePrice = $altResult['price'];
if ($altResult['price_unit'] > 1) {
$altPurchasePrice = $altResult['price'] / $altResult['price_unit'];
}
$supplierAlternatives[] = array(
'datanorm_id' => $altResult['id'],
'fk_soc' => $altResult['fk_soc'],
'supplier_name' => $altSupplier->name,
'article_number' => $altResult['article_number'],
'short_text1' => $altResult['short_text1'],
'price' => $altResult['price'],
'price_unit' => $altResult['price_unit'],
'purchase_price' => $altPurchasePrice,
'ean' => $altResult['ean'],
'manufacturer_ref' => $altResult['manufacturer_ref'],
'is_invoice_supplier' => ($altResult['fk_soc'] == $import->fk_soc),
);
}
// Store match info for preview // Store match info for preview
$datanormPreviewMatches[] = array( $datanormPreviewMatches[] = array(
'line_id' => $lineObj->id, 'line_id' => $lineObj->id,
@ -1183,7 +1413,8 @@ if ($action == 'previewdatanorm' && $id > 0) {
'copper_surcharge' => $copperSurchargeForPrice, 'copper_surcharge' => $copperSurchargeForPrice,
'existing_product_id' => $existingProductId, 'existing_product_id' => $existingProductId,
'action' => $productAction, 'action' => $productAction,
'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number 'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number,
'supplier_alternatives' => $supplierAlternatives
); );
} }
} }
@ -1723,6 +1954,117 @@ if ($action == 'edit' && $import->id > 0) {
print '<a href="'.$_SERVER['PHP_SELF'].'?action=removeproduct&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen">'; print '<a href="'.$_SERVER['PHP_SELF'].'?action=removeproduct&line_id='.$line->id.'&id='.$import->id.'&token='.newToken().'" class="button buttongen">';
print '<i class="fas fa-times"></i>'; print '<i class="fas fa-times"></i>';
print '</a>'; print '</a>';
// Check for missing supplier prices from other catalogs
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";
$sqlExistingPrices .= " WHERE fk_product = ".(int)$line->fk_product;
$resExistingPrices = $db->query($sqlExistingPrices);
$existingSupplierIds = array();
if ($resExistingPrices) {
while ($objPrice = $db->fetch_object($resExistingPrices)) {
$existingSupplierIds[$objPrice->fk_soc] = true;
}
}
// 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;
}
}
// 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);
// 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']);
$altPurchasePrice = $catalogResult['price'];
if ($catalogResult['price_unit'] > 1) {
$altPurchasePrice = $catalogResult['price'] / $catalogResult['price_unit'];
}
$missingSuppliers[] = array(
'datanorm_id' => $catalogResult['id'],
'fk_soc' => $catalogResult['fk_soc'],
'supplier_name' => $altSupplier->name,
'article_number' => $catalogResult['article_number'],
'price' => $catalogResult['price'],
'price_unit' => $catalogResult['price_unit'],
'purchase_price' => $altPurchasePrice,
'ean' => $catalogResult['ean'],
);
}
}
// 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.'">';
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>';
}
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>';
}
}
}
} else { } else {
// Product selection form // Product selection form
print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block">'; print '<form method="POST" action="'.$_SERVER['PHP_SELF'].'" class="inline-block">';
@ -1953,6 +2295,123 @@ if ($action == 'edit' && $import->id > 0) {
} }
print '</td>'; print '</td>';
print '</tr>'; print '</tr>';
// Show supplier alternatives if available (more than 1 supplier found)
if (!empty($match['supplier_alternatives']) && count($match['supplier_alternatives']) > 1) {
print '<tr class="oddeven" style="background-color: #f9f9f9;">';
print '<td></td>'; // Empty checkbox column
print '<td colspan="7" style="padding: 10px;">';
print '<div style="background-color: #fff; border: 1px solid #ddd; border-radius: 5px; padding: 10px;">';
print '<div style="font-weight: bold; margin-bottom: 8px; color: #555;">';
print '<i class="fas fa-store-alt paddingright"></i>'.$langs->trans('SupplierAlternatives');
print ' <span class="badge" style="background-color: #5bc0de;">'.count($match['supplier_alternatives']).' '.$langs->trans('Suppliers').'</span>';
print '</div>';
print '<table class="noborder" style="width: 100%; font-size: 0.9em;">';
print '<tr style="background-color: #f5f5f5;">';
print '<th class="center" style="width: 40px;">'.$langs->trans('Select').'</th>';
print '<th>'.$langs->trans('Supplier').'</th>';
print '<th>'.$langs->trans('SupplierRef').'</th>';
print '<th>'.$langs->trans('EAN').'</th>';
print '<th>'.$langs->trans('ManufacturerRef').'</th>';
print '<th class="right">'.$langs->trans('UnitPrice').'</th>';
print '<th class="right">'.$langs->trans('Difference').'</th>';
print '</tr>';
$lowestPrice = PHP_FLOAT_MAX;
foreach ($match['supplier_alternatives'] as $alt) {
if ($alt['purchase_price'] < $lowestPrice) {
$lowestPrice = $alt['purchase_price'];
}
}
foreach ($match['supplier_alternatives'] as $altIdx => $alt) {
$isInvoiceSupplier = $alt['is_invoice_supplier'];
$isCheapest = ($alt['purchase_price'] == $lowestPrice);
$rowStyle = '';
if ($isInvoiceSupplier) {
$rowStyle = 'background-color: #d9edf7;'; // Blue for invoice supplier
} elseif ($isCheapest) {
$rowStyle = 'background-color: #dff0d8;'; // Green for cheapest
}
print '<tr style="'.$rowStyle.'">';
// Checkbox for selecting this supplier as purchase source
print '<td class="center">';
$checkboxName = 'supplier_prices['.$match['line_id'].']['.$alt['fk_soc'].']';
$checked = $isInvoiceSupplier ? ' checked' : '';
print '<input type="checkbox" name="'.$checkboxName.'" value="'.$alt['datanorm_id'].'"'.$checked.' title="'.$langs->trans('AddAsPurchasePrice').'">';
print '</td>';
// Supplier name
print '<td>';
print '<strong>'.dol_escape_htmltag($alt['supplier_name']).'</strong>';
if ($isInvoiceSupplier) {
print ' <span class="badge" style="background-color: #337ab7; font-size: 0.75em;"><i class="fas fa-file-invoice"></i></span>';
}
if ($isCheapest) {
print ' <span class="badge" style="background-color: #5cb85c; font-size: 0.75em;"><i class="fas fa-tag"></i></span>';
}
print '</td>';
// Supplier article number
print '<td><code>'.dol_escape_htmltag($alt['article_number']).'</code></td>';
// EAN
print '<td>';
if (!empty($alt['ean'])) {
print '<span class="small">'.dol_escape_htmltag($alt['ean']).'</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Manufacturer ref
print '<td>';
if (!empty($alt['manufacturer_ref'])) {
print '<span class="small">'.dol_escape_htmltag($alt['manufacturer_ref']).'</span>';
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
// Price
print '<td class="right nowraponall">';
if ($alt['price_unit'] > 1) {
print '<span class="small" style="color: #666;">'.price($alt['price']).'/'.$alt['price_unit'].'</span><br>';
}
print '<strong>'.price($alt['purchase_price']).'</strong>';
print '</td>';
// Difference from invoice supplier price
print '<td class="right nowraponall">';
if (!$isInvoiceSupplier && isset($match['purchase_price'])) {
$diff = $alt['purchase_price'] - $match['purchase_price'];
$diffPercent = ($match['purchase_price'] > 0) ? ($diff / $match['purchase_price'] * 100) : 0;
if ($diff < 0) {
print '<span style="color: #5cb85c;"><i class="fas fa-arrow-down"></i> '.price(abs($diff)).' ('.number_format(abs($diffPercent), 1).'%)</span>';
} elseif ($diff > 0) {
print '<span style="color: #d9534f;"><i class="fas fa-arrow-up"></i> +'.price($diff).' (+'.number_format($diffPercent, 1).'%)</span>';
} else {
print '<span class="opacitymedium">=</span>';
}
} else {
print '<span class="opacitymedium">-</span>';
}
print '</td>';
print '</tr>';
}
print '</table>';
print '<div class="small opacitymedium" style="margin-top: 5px;">';
print '<i class="fas fa-info-circle"></i> '.$langs->trans('SelectSuppliersForPurchasePrices');
print '</div>';
print '</div>';
print '</td>';
print '</tr>';
}
} }
print '</table>'; print '</table>';

View file

@ -472,3 +472,17 @@ BoxNewProductsToReview = Neue Produkte prüfen
NewProductsToReview = Neue Produkte prüfen NewProductsToReview = Neue Produkte prüfen
NoNewProductsToReview = Keine neuen Produkte zur Überprüfung NoNewProductsToReview = Keine neuen Produkte zur Überprüfung
ShowAll = Alle anzeigen ShowAll = Alle anzeigen
#
# Multi-Supplier Alternatives
#
SupplierAlternatives = Lieferanten-Alternativen
Suppliers = Lieferanten
AddAsPurchasePrice = Als Einkaufspreis hinzufügen
SelectSuppliersForPurchasePrices = Wählen Sie die Lieferanten aus, bei denen ein Einkaufspreis hinterlegt werden soll
ManufacturerRef = Hersteller-Art.Nr.
MissingSupplierPrices = Fehlende Lieferantenpreise
AddSelectedPrices = Ausgewählte hinzufügen
SupplierPricesAdded = %s Lieferantenpreise hinzugefügt
CheaperBy = %s%% günstiger
MoreExpensiveBy = %s%% teurer

View file

@ -403,3 +403,17 @@ BoxNewProductsToReview = New Products to Review
NewProductsToReview = New Products to Review NewProductsToReview = New Products to Review
NoNewProductsToReview = No new products to review NoNewProductsToReview = No new products to review
ShowAll = Show all ShowAll = Show all
#
# Multi-Supplier Alternatives
#
SupplierAlternatives = Supplier Alternatives
Suppliers = Suppliers
AddAsPurchasePrice = Add as Purchase Price
SelectSuppliersForPurchasePrices = Select suppliers where a purchase price should be stored
ManufacturerRef = Manufacturer Ref
MissingSupplierPrices = Missing Supplier Prices
AddSelectedPrices = Add Selected
SupplierPricesAdded = %s supplier prices added
CheaperBy = %s%% cheaper
MoreExpensiveBy = %s%% more expensive