0 && $j > 0 && isset($tmp[$i]) && isset($tmp2[$j]) && $tmp[$i] == $tmp2[$j]) { $i--; $j--; } if (!$res && $i > 0 && file_exists(substr($tmp, 0, ($i + 1))."/main.inc.php")) { $res = @include substr($tmp, 0, ($i + 1))."/main.inc.php"; } if (!$res && $i > 0 && file_exists(dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php")) { $res = @include dirname(substr($tmp, 0, ($i + 1)))."/main.inc.php"; } if (!$res && file_exists("../main.inc.php")) { $res = @include "../main.inc.php"; } if (!$res && file_exists("../../main.inc.php")) { $res = @include "../../main.inc.php"; } if (!$res && file_exists("../../../main.inc.php")) { $res = @include "../../../main.inc.php"; } if (!$res) { die("Include of main fails"); } require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php'; require_once DOL_DOCUMENT_ROOT.'/core/class/html.formfile.class.php'; require_once DOL_DOCUMENT_ROOT.'/societe/class/societe.class.php'; require_once DOL_DOCUMENT_ROOT.'/product/class/product.class.php'; require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.facture.class.php'; dol_include_once('/importzugferd/class/zugferdparser.class.php'); dol_include_once('/importzugferd/class/zugferdimport.class.php'); dol_include_once('/importzugferd/class/importline.class.php'); dol_include_once('/importzugferd/class/productmapping.class.php'); dol_include_once('/importzugferd/class/actions_importzugferd.class.php'); dol_include_once('/importzugferd/class/datanorm.class.php'); dol_include_once('/importzugferd/class/datanormparser.class.php'); dol_include_once('/importzugferd/class/importnotification.class.php'); dol_include_once('/importzugferd/lib/importzugferd.lib.php'); // Load translation files $langs->loadLangs(array("importzugferd@importzugferd", "bills", "products", "companies")); // Security check if (!$user->hasRight('importzugferd', 'import', 'write')) { accessforbidden(); } // Get parameters $action = GETPOST('action', 'aZ09'); $confirm = GETPOST('confirm', 'alpha'); $id = GETPOST('id', 'int'); // Import ID for editing existing imports $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); $formfile = new FormFile($db); $actions = new ActionsImportZugferd($db); $import = new ZugferdImport($db); $importLine = new ImportLine($db); $notification = new ImportNotification($db); $error = 0; $message = ''; /* * Helper-Funktionen (DRY) */ /** * Parse Aderanzahl und Querschnitt aus Kabelbezeichnung * Erkennt Formate wie: NYM-J 3x2,5 / NYM-J 5x1.5 / H07V-K 1x4 / J-Y(ST)Y 2x2x0,8 etc. * * @param string $text Kabelbezeichnung (z.B. "NYM-J 3x2,5 Eca Ri100") * @return array|null Array mit 'aderanzahl', 'querschnitt' oder null wenn kein Kabel */ function parseCableSpecsFromText($text) { // Spezialfall: Fernmeldekabel wie J-Y(ST)Y 2x2x0,8 (Paare x Adern pro Paar x Querschnitt) // Pattern: Zahl x Zahl x Zahl (z.B. 2x2x0,8 = 4 Adern mit 0,8mm²) if (preg_match('/(\d+)\s*[xX]\s*(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) { $paare = (int) $matches[1]; $adernProPaar = (int) $matches[2]; $querschnitt = (float) str_replace(',', '.', $matches[3]); $aderanzahl = $paare * $adernProPaar; // Plausibilitätsprüfung if ($aderanzahl >= 1 && $aderanzahl <= 200 && $querschnitt >= 0.14 && $querschnitt <= 400) { return array( 'aderanzahl' => $aderanzahl, 'querschnitt' => $querschnitt ); } } // Standard: NYM-J 3x2,5 (Adern x Querschnitt) // Pattern: Zahl x Zahl (mit Komma oder Punkt als Dezimaltrenner) if (preg_match('/(\d+)\s*[xX]\s*(\d+(?:[,\.]\d+)?)/', $text, $matches)) { $aderanzahl = (int) $matches[1]; $querschnitt = (float) str_replace(',', '.', $matches[2]); // Plausibilitätsprüfung if ($aderanzahl >= 1 && $aderanzahl <= 100 && $querschnitt >= 0.5 && $querschnitt <= 400) { return array( 'aderanzahl' => $aderanzahl, 'querschnitt' => $querschnitt ); } } return null; } /** * Berechne Kupfergehalt aus Aderanzahl und Querschnitt * Formel: Aderanzahl × Querschnitt × 8.9 (Dichte Kupfer) = kg/km * * @param int $aderanzahl Anzahl der Adern * @param float $querschnitt Querschnitt in mm² * @return float Kupfergehalt in kg/km */ function calculateKupfergehalt($aderanzahl, $querschnitt) { // Kupferdichte: 8.9 g/cm³ = 8.9 kg/dm³ // 1 mm² × 1 km = 1 mm² × 1000m = 1000 mm³ = 1 cm³ // Also: 1 mm² Querschnitt × 1 km Länge = 1000 cm³ = 1 dm³ = 8.9 kg return $aderanzahl * $querschnitt * 8.9; } /** * Hole aktuellen Kupferpreis aus Metallzuschlag-Modul * * @param DoliDB $db Datenbank * @param int $supplierId Lieferanten-ID (optional, für lieferantenspezifischen Preis) * @return float CU-Notiz in EUR/100kg oder 0 wenn nicht verfügbar */ function getCurrentCopperPrice($db, $supplierId = 0) { // Erst prüfen ob Metallzuschlag-Modul aktiv ist if (!isModEnabled('metallzuschlag')) { return 0; } // Lieferanten-spezifischer CU-Wert (aus societe_extrafields) if ($supplierId > 0) { $sql = "SELECT metallzuschlag_cu FROM ".MAIN_DB_PREFIX."societe_extrafields"; $sql .= " WHERE fk_object = ".(int)$supplierId; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); if (!empty($obj->metallzuschlag_cu) && (float)$obj->metallzuschlag_cu > 0) { return (float)$obj->metallzuschlag_cu; } } } // Fallback: Aktuellster CU-Wert aus History $sql = "SELECT value FROM ".MAIN_DB_PREFIX."metallzuschlag_history"; $sql .= " WHERE metal = 'CU' ORDER BY date_notiz DESC LIMIT 1"; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); return (float)$obj->value; } return 0; } /** * Berechne Kupferzuschlag für eine bestimmte Menge * Formel: Kupfergehalt (kg/km) × CU (EUR/100kg) / 100000 × Menge * * @param float $kupfergehalt Kupfergehalt in kg/km * @param float $cuPrice CU-Notiz in EUR/100kg * @param float $quantity Menge (z.B. 100 für 100m) * @return float Kupferzuschlag in EUR */ function calculateKupferzuschlag($kupfergehalt, $cuPrice, $quantity = 1) { if ($kupfergehalt <= 0 || $cuPrice <= 0) { return 0; } // kg/km × EUR/100kg / 100000 × m = EUR return round($kupfergehalt * $cuPrice / 100000 * $quantity, 2); } /** * Prüft ob ein Produkt ein Kabel ist (basierend auf Warengruppe oder Bezeichnung) * * @param Datanorm $datanorm Datanorm-Objekt * @return bool True wenn Kabel */ function isCableProduct($datanorm) { // Warengruppen die typisch für Kabel sind $cableGroups = array('KAB', 'KABEL', 'LEI', 'LEIT', 'LEITUNG'); if (!empty($datanorm->product_group)) { $group = strtoupper(substr($datanorm->product_group, 0, 5)); foreach ($cableGroups as $cg) { if (strpos($group, $cg) !== false) { return true; } } } // Typische Kabelbezeichnungen $cablePatterns = array( '/NYM[-\s]?[JYOA]/i', '/NYY[-\s]?[JO]/i', '/H0[357]V[-\s]?[KUR]/i', '/H0[357]RN[-\s]?F/i', '/NHXH/i', '/J[-\s]?Y\(ST\)Y/i', '/LiYCY/i', '/ÖLFLEX/i', ); $text = $datanorm->short_text1 . ' ' . $datanorm->short_text2; foreach ($cablePatterns as $pattern) { if (preg_match($pattern, $text)) { return true; } } return false; } /** * Ringgröße aus Kabel-Bezeichnung extrahieren * Erkennt Muster wie: Ri100, Ri.50, Ri 100, Ring100, Tr500, Fol.25m, "Ring 100m", "Trommel 500m" * * WICHTIG: Nur verwenden wenn price_unit = 1! * Bei price_unit > 1 ist das bereits die korrekte Preiseinheit (z.B. 100 für 100m) * * @param string $text Produktbezeichnung * @return int Ringgröße in Metern (0 wenn nicht gefunden) */ function extractCableRingSize($text) { // Muster für Ringgröße: Ri100, Ri.50, Ri 100, Ring100, Ring 50 if (preg_match('/Ri(?:ng)?[.\s]?(\d+)/i', $text, $matches)) { return (int)$matches[1]; } // Muster für "Ring 100m", "Ring 50 m" if (preg_match('/Ring\s+(\d+)\s*m/i', $text, $matches)) { return (int)$matches[1]; } // Muster für Trommel: Tr500, Tr.500, Trommel500, "Trommel 500m" if (preg_match('/Tr(?:ommel)?[.\s]?(\d+)/i', $text, $matches)) { return (int)$matches[1]; } if (preg_match('/Trommel\s+(\d+)\s*m/i', $text, $matches)) { return (int)$matches[1]; } // Muster für Folie/Rolle: Fol.25m, Fol25, Rol.50m if (preg_match('/(?:Fol|Rol)[.\s]?(\d+)/i', $text, $matches)) { return (int)$matches[1]; } return 0; } /** * Berechne Kabelpreis unter Berücksichtigung unterschiedlicher Lieferanten-Formate * * Logik: * - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m * - Sonepar: price_unit = 1 → Preis ist für kompletten Ring (Größe aus Name) * * @param Datanorm $datanorm Datanorm-Objekt * @param float $minQty Mindestbestellmenge (default 1) * @return array Array mit 'unitPrice', 'totalPrice', 'priceUnit' */ function calculateCablePricing($datanorm, $minQty = 1) { $priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1; $cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2; if ($priceUnit > 1) { // Kluxen/Witte-Format: price_unit gibt die Preiseinheit an (z.B. 100m) $unitPrice = $datanorm->price / $priceUnit; $effectivePriceUnit = $priceUnit; } else { // Sonepar-Format: price_unit = 1, aber Preis ist für kompletten Ring $ringSize = extractCableRingSize($cableText); if ($ringSize > 0) { $unitPrice = $datanorm->price / $ringSize; $effectivePriceUnit = $ringSize; } else { // Einzelstück $unitPrice = $datanorm->price; $effectivePriceUnit = 1; } } // Schutz gegen Division durch Null $effectivePriceUnit = max(1, $effectivePriceUnit); return array( 'unitPrice' => $unitPrice, 'totalPrice' => $unitPrice * $minQty, 'priceUnit' => $effectivePriceUnit ); } /** * Extrafields fuer Lieferantenpreis aus Datanorm-Daten zusammenstellen * * @param Datanorm $datanorm Datanorm-Objekt * @param ImportLine|null $lineObj Import-Zeile (optional, fuer ZUGFeRD-Daten) * @return array Extrafields-Array */ function datanormBuildSupplierPriceExtrafields($datanorm, $lineObj = null) { $extrafields = array(); // Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln mit Metallzuschlag // Der Preis ist bereits auf Mindestmenge (price_unit) bezogen if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) { $extrafields['options_produktpreis'] = $datanorm->price; } // Preiseinheit if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) { $extrafields['options_preiseinheit'] = $datanorm->price_unit; } elseif ($lineObj && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { $extrafields['options_preiseinheit'] = $lineObj->basis_quantity; } // Warengruppe if (!empty($datanorm->product_group)) { $extrafields['options_warengruppe'] = $datanorm->product_group; } return $extrafields; } /** * Lieferantenpreis aus Datanorm hinzufuegen * * @param DoliDB $db Datenbank * @param int $productId Produkt-ID * @param Datanorm $datanorm Datanorm-Objekt * @param Societe $supplier Lieferant-Objekt * @param User $user Benutzer * @param float $purchasePrice Einkaufspreis * @param float $taxPercent MwSt-Satz * @param array $extrafields Extrafields * @return int >0 bei Erfolg, <0 bei Fehler */ function datanormAddSupplierPrice($db, $productId, $datanorm, $supplier, $user, $purchasePrice, $taxPercent = 19, $extrafields = array()) { require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; $prodfourn = new ProductFournisseur($db); $prodfourn->id = $productId; $supplierEan = !empty($datanorm->ean) ? $datanorm->ean : ''; $supplierEanType = !empty($datanorm->ean) ? 2 : 0; $description = trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')); // Mindestbestellmenge und Verpackungseinheit vom bestehenden Lieferantenpreis übernehmen // (gleiches Produkt = gleiche Mengen, nur anderer Lieferant) $minQty = 1; $packaging = null; $sqlExisting = "SELECT quantity, packaging FROM " . MAIN_DB_PREFIX . "product_fournisseur_price"; $sqlExisting .= " WHERE fk_product = " . (int)$productId; $sqlExisting .= " AND quantity > 0"; $sqlExisting .= " ORDER BY rowid ASC LIMIT 1"; $resExisting = $db->query($sqlExisting); if ($resExisting && $db->num_rows($resExisting) > 0) { $objExisting = $db->fetch_object($resExisting); if ($objExisting->quantity > 0) { $minQty = $objExisting->quantity; } if (!empty($objExisting->packaging)) { $packaging = $objExisting->packaging; } } // Preis berechnen mit zentraler Funktion $pricing = calculateCablePricing($datanorm, $minQty); $totalPrice = $pricing['totalPrice']; $result = $prodfourn->update_buyprice( $minQty, $totalPrice, $user, 'HT', $supplier, 0, $datanorm->article_number, $taxPercent, 0, 0, 0, 0, 0, 0, array(), '', 0, 'HT', 1, '', $description, $supplierEan, $supplierEanType, $extrafields ); // Verpackungseinheit nachträglich setzen (nicht in update_buyprice verfügbar) if ($result > 0 && !empty($packaging)) { $sqlPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price"; $sqlPkg .= " SET packaging = " . (float)$packaging; $sqlPkg .= " WHERE rowid = " . (int)$result; $db->query($sqlPkg); } return $result; } /** * Extrafields in product_fournisseur_price_extrafields einfuegen * * @param DoliDB $db Datenbank * @param int $priceId ID des Lieferantenpreises * @param array $extrafields Extrafields-Array */ function datanormInsertPriceExtrafields($db, $priceId, $extrafields) { if (empty($priceId) || empty($extrafields)) { return; } $produktpreis = !empty($extrafields['options_produktpreis']) ? (float)$extrafields['options_produktpreis'] : 'NULL'; $preiseinheit = !empty($extrafields['options_preiseinheit']) ? (int)$extrafields['options_preiseinheit'] : 1; $warengruppe = !empty($extrafields['options_warengruppe']) ? "'".$db->escape($extrafields['options_warengruppe'])."'" : 'NULL'; // Pruefen ob bereits vorhanden $sqlCheck = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields WHERE fk_object = ".(int)$priceId; $resCheck = $db->query($sqlCheck); if ($resCheck && $db->num_rows($resCheck) > 0) { // Update statt Insert wenn bereits vorhanden $sql = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET "; $sql .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", "; $sql .= "preiseinheit = ".$preiseinheit.", "; $sql .= "warengruppe = ".$warengruppe." "; $sql .= "WHERE fk_object = ".(int)$priceId; if (!$db->query($sql)) { dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR); } return; } $sql = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sql .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES ("; $sql .= (int)$priceId.", "; $sql .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", "; $sql .= $preiseinheit.", "; $sql .= $warengruppe.")"; if (!$db->query($sql)) { dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR); } } /* * Actions */ // AJAX: Get raw Datanorm lines for debugging if ($action == 'get_raw_lines' && GETPOST('article_number', 'alphanohtml')) { header('Content-Type: application/json'); $article_number = GETPOST('article_number', 'alphanohtml'); $ajax_fk_soc = GETPOSTINT('fk_soc'); $result = array( 'datanorm_line' => '', 'datpreis_line' => '', 'article_number' => $article_number ); // Get the upload directory for this supplier $upload_dir = $conf->importzugferd->dir_output.'/datanorm/'.$ajax_fk_soc; if (is_dir($upload_dir)) { $allFiles = glob($upload_dir . '/*'); // Search in DATANORM files foreach ($allFiles as $file) { $basename = strtoupper(basename($file)); if (preg_match('/^DATANORM\.\d{3}$/', $basename)) { $handle = fopen($file, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { // A-Satz starts with A; and contains the article number if (preg_match('/^A;/', $line)) { $parts = explode(';', $line); if (isset($parts[2]) && trim($parts[2]) == $article_number) { $result['datanorm_line'] = trim($line); break; } } } fclose($handle); } if (!empty($result['datanorm_line'])) break; } } // Search in DATPREIS files foreach ($allFiles as $file) { $basename = strtoupper(basename($file)); if (preg_match('/^DATPREIS\.\d{3}$/', $basename)) { $handle = fopen($file, 'r'); if ($handle) { while (($line = fgets($handle)) !== false) { // P-Satz contains article numbers at various positions if (preg_match('/^P;/', $line) && strpos($line, $article_number) !== false) { $result['datpreis_line'] = trim($line); break; } } fclose($handle); } if (!empty($result['datpreis_line'])) break; } } $result['upload_dir'] = $upload_dir; } else { $result['error'] = 'Upload directory not found: ' . $upload_dir; } echo json_encode($result); exit; } // Upload and parse PDF - creates import record immediately if ($action == 'upload') { if (!empty($_FILES['zugferd_file']['tmp_name'])) { $upload_dir = $conf->importzugferd->dir_output.'/temp'; if (!is_dir($upload_dir)) { dol_mkdir($upload_dir); } $filename = dol_sanitizeFileName($_FILES['zugferd_file']['name']); $destfile = $upload_dir.'/'.$filename; if (move_uploaded_file($_FILES['zugferd_file']['tmp_name'], $destfile)) { $force_reimport = GETPOST('force_reimport', 'int'); // Check for duplicate $file_hash = hash_file('sha256', $destfile); $isDuplicate = $import->isDuplicate($file_hash); if ($isDuplicate && !$force_reimport) { $error++; $message = $langs->trans('ErrorDuplicateInvoice'); @unlink($destfile); } else { // If force reimport, delete the old record first if ($isDuplicate && $force_reimport) { $oldImport = new ZugferdImport($db); $oldImport->fetch(0, null, $file_hash); if ($oldImport->id > 0) { $db->begin(); // Alten Import-Datensatz komplett loeschen (Transaktion) $oldLines = new ImportLine($db); $oldLines->deleteAllByImport($oldImport->id); $old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id; if (is_dir($old_dir)) { dol_delete_dir_recursive($old_dir); } $oldImport->delete($user); $db->commit(); } } // Parse the file $parser = new ZugferdParser($db); $res = $parser->extractFromPdf($destfile); if ($res > 0) { $res = $parser->parse(); if ($res > 0) { $parsed_data = $parser->getInvoiceData(); // Create import record immediately $import->invoice_number = $parsed_data['invoice_number']; $import->invoice_date = $parsed_data['invoice_date']; $import->seller_name = $parsed_data['seller']['name']; $import->seller_vat = $parsed_data['seller']['vat_id']; $import->buyer_reference = $parsed_data['buyer']['reference'] ?: $parsed_data['buyer']['id']; $import->total_ht = $parsed_data['totals']['net']; $import->total_ttc = $parsed_data['totals']['gross']; $import->currency = $parsed_data['totals']['currency']; $import->xml_content = $parser->getXmlContent(); $import->pdf_filename = $filename; $import->file_hash = $file_hash; // Find supplier $supplier_id = $actions->findSupplier($parsed_data); $import->fk_soc = $supplier_id; // Process line items to find products $processed_lines = $actions->processLineItems($parsed_data['lines'], $supplier_id); // Check if all lines have products $all_have_products = true; $has_any_product = false; $total_lines = count($processed_lines); foreach ($processed_lines as $line) { if ($line['fk_product'] <= 0) { $all_have_products = false; } else { $has_any_product = true; } } // Set status based on product matching // STATUS_IMPORTED only if: supplier found, has lines, ALL lines have products if ($all_have_products && $supplier_id > 0 && $total_lines > 0 && $has_any_product) { $import->status = ZugferdImport::STATUS_IMPORTED; } else { $import->status = ZugferdImport::STATUS_PENDING; } $import->date_creation = dol_now(); $result = $import->create($user); if ($result > 0) { // Store line items in database foreach ($processed_lines as $line) { $importLineObj = new ImportLine($db); $importLineObj->fk_import = $import->id; $importLineObj->line_id = $line['line_id']; $importLineObj->supplier_ref = $line['supplier_ref']; $importLineObj->product_name = $line['name']; $importLineObj->description = $line['description']; $importLineObj->quantity = $line['quantity']; $importLineObj->unit_code = $line['unit_code']; $importLineObj->unit_price = $line['unit_price']; $importLineObj->unit_price_raw = $line['unit_price_raw']; $importLineObj->basis_quantity = $line['basis_quantity']; $importLineObj->basis_quantity_unit = $line['basis_quantity_unit']; $importLineObj->line_total = $line['line_total']; $importLineObj->tax_percent = $line['tax_percent']; $importLineObj->ean = $line['ean']; $importLineObj->fk_product = $line['fk_product']; $importLineObj->match_method = $line['match_method']; $importLineObj->create($user); } // Move PDF to permanent storage $final_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id; if (!is_dir($final_dir)) { dol_mkdir($final_dir); } if (!@rename($destfile, $final_dir.'/'.$filename)) { // Fallback: copy + delete (z.B. bei verschiedenen Dateisystemen) if (@copy($destfile, $final_dir.'/'.$filename)) { @unlink($destfile); } else { dol_syslog('ImportZugferd: Fehler beim Verschieben der PDF nach '.$final_dir, LOG_ERR); } } // Send notification if manual intervention required if ($import->status == ZugferdImport::STATUS_PENDING) { $storedLines = $importLine->fetchAllByImport($import->id); $notification->sendManualInterventionNotification($import, $storedLines); } // Check for price differences if ($import->status == ZugferdImport::STATUS_IMPORTED) { $storedLines = $importLine->fetchAllByImport($import->id); $notification->checkAndNotifyPriceDifferences($import, $storedLines); } // Redirect to edit page $id = $import->id; $action = 'edit'; setEventMessages($langs->trans('ImportRecordCreated'), null, 'mesgs'); } else { $error++; $message = $import->error; @unlink($destfile); // Send error notification $notification->sendErrorNotification($import, $message, $filename); } } else { $error++; $message = $parser->error; @unlink($destfile); } } else { $error++; $message = $parser->error; @unlink($destfile); } } } else { $error++; $message = $langs->trans('ErrorFileUploadFailed'); } } else { $error++; $message = $langs->trans('ErrorNoFileUploaded'); } } // Load existing import for editing if ($id > 0 && empty($action)) { $action = 'edit'; } if ($action == 'edit' && $id > 0) { $result = $import->fetch($id); if ($result <= 0) { $error++; $message = $langs->trans('ErrorRecordNotFound'); $action = ''; } } // Assign product to line if ($action == 'assignproduct' && $line_id > 0 && $product_id > 0) { $lineObj = new ImportLine($db); $result = $lineObj->fetch($line_id); if ($result > 0) { $lineObj->setProduct($product_id, $langs->trans('ManualAssignment'), $user); setEventMessages($langs->trans('ProductAssigned'), null, 'mesgs'); // Get import ID to reload $id = $lineObj->fk_import; // Check if all lines now have products $allHaveProducts = $importLine->allLinesHaveProducts($id); if ($allHaveProducts) { // Update import status $import->fetch($id); if ($import->status == ZugferdImport::STATUS_PENDING) { $import->status = ZugferdImport::STATUS_IMPORTED; $import->update($user); // Check for price differences now that all products are assigned $storedLines = $importLine->fetchAllByImport($id); $notification->checkAndNotifyPriceDifferences($import, $storedLines); } } } $action = 'edit'; $import->fetch($id); } // Remove product assignment from line if ($action == 'removeproduct' && $line_id > 0) { $lineObj = new ImportLine($db); $result = $lineObj->fetch($line_id); if ($result > 0) { $id = $lineObj->fk_import; $lineObj->setProduct(0, '', $user); setEventMessages($langs->trans('ProductRemoved'), null, 'mesgs'); // Update import status to pending $import->fetch($id); if ($import->status == ZugferdImport::STATUS_IMPORTED) { $import->status = ZugferdImport::STATUS_PENDING; $import->update($user); } } $action = 'edit'; $import->fetch($id); } // Fehlende Lieferantenpreise aus anderen Katalogen hinzufuegen if ($action == 'addmissingprices' && $id > 0) { $import->fetch($id); $selectedPrices = GETPOST('add_prices', 'array'); if (!empty($selectedPrices)) { $addedCount = 0; $errorCount = 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; } $datanorm = new Datanorm($db); if ($datanorm->fetch($datanormId) > 0) { $altSupplier = new Societe($db); $altSupplier->fetch($socId); $priceExtrafields = datanormBuildSupplierPriceExtrafields($datanorm); $result = datanormAddSupplierPrice($db, $productId, $datanorm, $altSupplier, $user, 0, 19, $priceExtrafields); if ($result > 0) { datanormInsertPriceExtrafields($db, $result, $priceExtrafields); $mapping = new ProductMapping($db); $mapping->fk_soc = $socId; $mapping->supplier_ref = $datanorm->article_number; $mapping->fk_product = $productId; $mapping->ean = $datanorm->ean; $mapping->manufacturer_ref = $datanorm->manufacturer_ref; $mapping->description = $datanorm->short_text1; $mapping->create($user); $addedCount++; } else { $errorCount++; dol_syslog('ImportZugferd addmissingprices: Fehler bei Lieferantenpreis product='.$productId.' supplier='.$socId, LOG_ERR); } } } if ($addedCount > 0) { setEventMessages($langs->trans('SupplierPricesAdded', $addedCount), null, 'mesgs'); } if ($errorCount > 0) { setEventMessages($addedCount.' hinzugefuegt, '.$errorCount.' Fehler', null, 'warnings'); } // Redirect to avoid "Form resubmit" warning on page reload $redirectUrl = $_SERVER['PHP_SELF'].'?id='.$id; header('Location: '.$redirectUrl); exit; } else { setEventMessages('Keine Preise ausgewählt', null, 'warnings'); } $action = 'edit'; } // Update supplier if ($action == 'setsupplier' && $id > 0) { $import->fetch($id); $import->fk_soc = $supplier_id; $import->update($user); setEventMessages($langs->trans('SupplierUpdated'), null, 'mesgs'); $action = 'edit'; } // Duplicate product from template if ($action == 'duplicateproduct' && $template_product_id > 0 && $line_id > 0) { $lineObj = new ImportLine($db); $result = $lineObj->fetch($line_id); if ($result > 0) { // Load template product $template = new Product($db); if ($template->fetch($template_product_id) > 0) { // Create new product as copy $newproduct = new Product($db); // Copy basic properties from template $newproduct->type = $template->type; $newproduct->status = $template->status; $newproduct->status_buy = $template->status_buy; $newproduct->status_batch = $template->status_batch; $newproduct->fk_product_type = $template->fk_product_type; $newproduct->price = $lineObj->unit_price; $newproduct->price_base_type = 'HT'; $newproduct->tva_tx = $lineObj->tax_percent ?: $template->tva_tx; $newproduct->weight = $template->weight; $newproduct->weight_units = $template->weight_units; $newproduct->fk_unit = $template->fk_unit; // Set label from ZUGFeRD $newproduct->label = $lineObj->product_name; // Generate unique ref $newproduct->ref = 'NEW-'.dol_print_date(dol_now(), '%Y%m%d%H%M%S'); // Build description with ZUGFeRD data $zugferd_info = ''; if (!empty($lineObj->supplier_ref)) { $zugferd_info .= $langs->trans('SupplierRef').': '.$lineObj->supplier_ref."\n"; } if (!empty($lineObj->unit_code)) { $zugferd_info .= $langs->trans('Unit').': '.zugferdGetUnitLabel($lineObj->unit_code)."\n"; } if (!empty($lineObj->ean)) { $zugferd_info .= 'EAN: '.$lineObj->ean."\n"; } $zugferd_info .= "---\n"; $newproduct->description = $zugferd_info . ($template->description ?: ''); // Create the product $result = $newproduct->create($user); if ($result > 0) { setEventMessages($langs->trans('ProductCreated'), null, 'mesgs'); // Redirect to product card for editing header('Location: '.DOL_URL_ROOT.'/product/card.php?id='.$result); exit; } else { setEventMessages($newproduct->error, $newproduct->errors, 'errors'); } } $id = $lineObj->fk_import; } $action = 'edit'; $import->fetch($id); } // Create product from Datanorm if ($action == 'createfromdatanorm' && $line_id > 0) { $lineObj = new ImportLine($db); $result = $lineObj->fetch($line_id); if ($result > 0) { $id = $lineObj->fk_import; $import->fetch($id); // Get Datanorm settings $markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30); $searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0); // Search in Datanorm database $datanorm = new Datanorm($db); $results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1); if (empty($results)) { // Try with EAN if available if (!empty($lineObj->ean)) { $results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1); } } if (!empty($results)) { $datanormArticle = $results[0]; $datanorm->fetch($datanormArticle['id']); // Load supplier for ref prefix $supplier = new Societe($db); $supplier->fetch($import->fk_soc); $supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3)); // Create new product $newproduct = new Product($db); $newproduct->type = 0; // Product $newproduct->status = 1; // On sale $newproduct->status_buy = 1; // On purchase // Generate reference $newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number; // Set default accounting codes from module settings $newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', ''); $newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', ''); $newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', ''); $newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', ''); $newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', ''); $newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', ''); // Label from Datanorm $newproduct->label = $datanorm->short_text1; if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) { $newproduct->label .= ' '.$datanorm->short_text2; } // Description $newproduct->description = $datanorm->getFullDescription(); // Preise und Kupferzuschlag // Datanorm liefert den reinen Materialpreis (ohne Kupferzuschlag) // WICHTIG: Bei Kabeln ist der Preis bereits für die Ringgröße (z.B. 49,20€ für 100m Ring) $materialPrice = $datanorm->price; $priceUnit = $datanorm->price_unit > 0 ? $datanorm->price_unit : 1; // Prüfen ob es ein Kabel ist $isCable = isCableProduct($datanorm); // Preiseinheit bestimmen - unterschiedliche Logik je nach Lieferant-Datenformat: // - Kluxen/Witte/eltric: price_unit > 1 (z.B. 100) → Preis ist für 100m // - Sonepar: price_unit = 1, aber Preis ist für kompletten Ring → Größe aus Name extrahieren $cableText = $datanorm->short_text1 . ' ' . $datanorm->short_text2; if ($priceUnit == 1) { // Sonepar-Format: Ringgröße aus Bezeichnung extrahieren $ringSize = extractCableRingSize($cableText); if ($ringSize > 0) { $priceUnit = $ringSize; // z.B. 100 für Ri100 } } // Bei price_unit > 1 (Kluxen/Witte) bleibt priceUnit unverändert $cableSpecs = null; $kupfergehalt = 0; $kupferzuschlag = 0; $cuPrice = 0; if ($isCable) { // Parse Aderanzahl und Querschnitt aus Bezeichnung $cableSpecs = parseCableSpecsFromText($datanorm->short_text1 . ' ' . $datanorm->short_text2); if ($cableSpecs) { // Kupfergehalt berechnen $kupfergehalt = calculateKupfergehalt($cableSpecs['aderanzahl'], $cableSpecs['querschnitt']); // Aktuellen Kupferpreis holen $cuPrice = getCurrentCopperPrice($db, $import->fk_soc); if ($cuPrice > 0 && $kupfergehalt > 0) { // Kupferzuschlag für die Preiseinheit berechnen (z.B. 100m) $kupferzuschlag = calculateKupferzuschlag($kupfergehalt, $cuPrice, $priceUnit); } } } // Einkaufspreis = Materialpreis + Kupferzuschlag (für die Preiseinheit) $totalPurchasePrice = $materialPrice + $kupferzuschlag; // Stückpreis (pro 1 Einheit, z.B. pro Meter) $purchasePricePerUnit = $totalPurchasePrice / $priceUnit; // Verkaufspreis mit Aufschlag $sellingPrice = $purchasePricePerUnit * (1 + $markup / 100); $newproduct->price = $sellingPrice; $newproduct->price_base_type = 'HT'; $newproduct->tva_tx = $lineObj->tax_percent ?: 19; // Weight if available if (!empty($datanorm->weight)) { $newproduct->weight = $datanorm->weight; $newproduct->weight_units = 0; // kg } // Let Dolibarr auto-generate barcode if configured // Setting barcode to '-1' triggers automatic generation if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) { $newproduct->barcode = '-1'; } // Create the product $result = $newproduct->create($user); if ($result > 0) { // Bei Kabeln: Produkt-Extrafields für Aderanzahl, Querschnitt und Kupfergehalt setzen if ($isCable && $cableSpecs && $kupfergehalt > 0) { $sqlProdExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_extrafields"; $sqlProdExtra .= " (fk_object, aderanzahl, querschnitt, kupfergehalt)"; $sqlProdExtra .= " VALUES (".(int)$newproduct->id.", "; $sqlProdExtra .= (int)$cableSpecs['aderanzahl'].", "; $sqlProdExtra .= (float)$cableSpecs['querschnitt'].", "; $sqlProdExtra .= (float)$kupfergehalt.")"; $sqlProdExtra .= " ON DUPLICATE KEY UPDATE"; $sqlProdExtra .= " aderanzahl = ".(int)$cableSpecs['aderanzahl'].","; $sqlProdExtra .= " querschnitt = ".(float)$cableSpecs['querschnitt'].","; $sqlProdExtra .= " kupfergehalt = ".(float)$kupfergehalt; if (!$db->query($sqlProdExtra)) { dol_syslog("ImportZugferd: Fehler beim Setzen der Kabel-Extrafields: ".$db->lasterror(), LOG_WARNING); } else { dol_syslog("ImportZugferd: Kabel-Extrafields gesetzt - Adern: ".$cableSpecs['aderanzahl'].", Querschnitt: ".$cableSpecs['querschnitt'].", Kupfergehalt: ".$kupfergehalt." kg/km", LOG_INFO); } } // Add supplier price require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; $prodfourn = new ProductFournisseur($db); $prodfourn->id = $newproduct->id; $prodfourn->fourn_ref = $datanorm->article_number; // Determine EAN for supplier price $supplierEan = ''; $supplierEanType = 0; if (!empty($datanorm->ean)) { $supplierEan = $datanorm->ean; $supplierEanType = 2; // EAN13 } elseif (!empty($lineObj->ean)) { $supplierEan = $lineObj->ean; $supplierEanType = 2; // EAN13 } // Prepare extrafields for supplier price $supplierPriceExtrafields = array(); // Produktpreis (reiner Materialpreis ohne Kupferzuschlag) - nur bei Kabeln if ($isCable && $materialPrice > 0) { $supplierPriceExtrafields['options_produktpreis'] = $materialPrice; } // Preiseinheit if ($priceUnit > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $priceUnit; } elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity; } // Warengruppe aus Datanorm if (!empty($datanorm->product_group)) { $supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group; } // Lieferantenpreis speichern (Gesamtpreis inkl. Kupferzuschlag für die Preiseinheit) $res = $prodfourn->update_buyprice( $priceUnit, // Quantity (Mindestmenge, z.B. 100 für 100m) $totalPurchasePrice, // Price (Gesamtpreis für die Mindestmenge inkl. Kupfer) $user, 'HT', // Price base $supplier, // Supplier 0, // Availability $datanorm->article_number, // Supplier ref $lineObj->tax_percent ?: 19, // VAT 0, // Charges 0, // Remise 0, // Remise percentage 0, // No price minimum 0, // Delivery delay 0, // Reputation array(), // Localtaxes array '', // Default VAT code 0, // Multicurrency price 'HT', // Multicurrency price base type 1, // Multicurrency tx '', // Multicurrency code trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm $supplierEan, // Barcode/EAN in supplier price $supplierEanType, // Barcode type (EAN13) $supplierPriceExtrafields // Extra fields ); dol_syslog("ImportZugferd: Lieferantenpreis - Material: ".$materialPrice.", Kupfer: ".$kupferzuschlag.", Gesamt: ".$totalPurchasePrice." (für ".$priceUnit." Einheiten)", LOG_INFO); // Manually ensure extrafields record exists for supplier price // (Dolibarr update_buyprice doesn't always create it properly) $sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; $sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id; $sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id; $sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1"; $resGetPrice = $db->query($sqlGetPrice); if ($resGetPrice && $db->num_rows($resGetPrice) > 0) { $objPrice = $db->fetch_object($resGetPrice); $priceId = $objPrice->rowid; // Check if extrafields record exists $sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId; $resCheckExtra = $db->query($sqlCheckExtra); // Werte für Extrafields vorbereiten $produktpreisVal = $isCable && $materialPrice > 0 ? (float)$materialPrice : 'NULL'; $kupferzuschlagVal = $kupferzuschlag > 0 ? (float)$kupferzuschlag : 'NULL'; $preiseinheitVal = $priceUnit > 1 ? (int)$priceUnit : 1; $warengruppeVal = !empty($datanorm->product_group) ? "'".$db->escape($datanorm->product_group)."'" : 'NULL'; if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) { // Insert extrafields record $sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlInsertExtra .= " (fk_object, produktpreis, kupferzuschlag, preiseinheit, warengruppe) VALUES ("; $sqlInsertExtra .= (int)$priceId.", "; $sqlInsertExtra .= ($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", "; $sqlInsertExtra .= ($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", "; $sqlInsertExtra .= $preiseinheitVal.", "; $sqlInsertExtra .= $warengruppeVal.")"; if (!$db->query($sqlInsertExtra)) { dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR); } } else { // Update extrafields record $sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET "; $sqlUpdateExtra .= "produktpreis = ".($produktpreisVal === 'NULL' ? "NULL" : $produktpreisVal).", "; $sqlUpdateExtra .= "kupferzuschlag = ".($kupferzuschlagVal === 'NULL' ? "NULL" : $kupferzuschlagVal).", "; $sqlUpdateExtra .= "preiseinheit = ".$preiseinheitVal.", "; $sqlUpdateExtra .= "warengruppe = ".$warengruppeVal." "; $sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId; if (!$db->query($sqlUpdateExtra)) { dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR); } } } // Create product mapping for future imports $mapping = new ProductMapping($db); $mapping->fk_soc = $import->fk_soc; $mapping->supplier_ref = $datanorm->article_number; $mapping->fk_product = $newproduct->id; $mapping->ean = $datanorm->ean; $mapping->manufacturer_ref = $datanorm->manufacturer_ref; $mapping->description = $datanorm->short_text1; $mapping->create($user); // Assign to import line $lineObj->setProduct($newproduct->id, 'datanorm', $user); setEventMessages($langs->trans('ProductCreatedFromDatanorm', $newproduct->ref), null, 'mesgs'); // Check if all lines now have products $allHaveProducts = $importLine->allLinesHaveProducts($id); if ($allHaveProducts) { $import->status = ZugferdImport::STATUS_IMPORTED; $import->update($user); } } else { setEventMessages($newproduct->error, $newproduct->errors, 'errors'); } } else { setEventMessages($langs->trans('DatanormArticleNotFound', $lineObj->supplier_ref), null, 'errors'); } } $action = 'edit'; $import->fetch($id); } // "Alle zuordnen" - Assign Datanorm matches to import lines if ($action == 'assignallfromdatanorm' && $id > 0) { $import->fetch($id); if ($import->fk_soc > 0) { $searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0); // Get all lines without product $lines = $importLine->fetchAllByImport($import->id); $datanorm = new Datanorm($db); $mapping = new ProductMapping($db); $assignedCount = 0; $datanormFoundCount = 0; foreach ($lines as $lineObj) { // Skip lines that already have a product if ($lineObj->fk_product > 0) { continue; } // Skip lines without supplier_ref if (empty($lineObj->supplier_ref)) { continue; } // Search in Datanorm database $results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1); if (empty($results) && !empty($lineObj->ean)) { $results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1); } if (!empty($results)) { $datanormFoundCount++; $datanormMatch = $results[0]; // Get Datanorm ID and article number (array access) $datanormId = isset($datanormMatch['id']) ? $datanormMatch['id'] : (isset($datanormMatch['rowid']) ? $datanormMatch['rowid'] : 0); $articleNumber = isset($datanormMatch['article_number']) ? $datanormMatch['article_number'] : ''; // Check if product already exists for this supplier ref $existingProductId = $mapping->findProductBySupplierRef($import->fk_soc, $articleNumber); if ($existingProductId > 0) { // Product exists - assign both product and Datanorm to the line $lineObj->fk_product = $existingProductId; $lineObj->fk_datanorm = $datanormId; $lineObj->match_method = 'datanorm_assign'; $lineObj->update($user); $assignedCount++; } else { // No product yet - save Datanorm reference for later product creation $lineObj->fk_datanorm = $datanormId; $lineObj->match_method = 'datanorm_pending'; $lineObj->update($user); } } } if ($assignedCount > 0) { setEventMessages($langs->trans('ProductsAssignedFromDatanorm', $assignedCount), null, 'mesgs'); } if ($datanormFoundCount > $assignedCount) { $pendingCount = $datanormFoundCount - $assignedCount; setEventMessages($langs->trans('DatanormMatchesFoundNotAssigned', $pendingCount), null, 'mesgs'); } if ($datanormFoundCount == 0) { setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings'); } } header('Location: '.$_SERVER['PHP_SELF'].'?action=edit&id='.$id.'&token='.newToken()); exit; } // Create ALL products from Datanorm (batch) if ($action == 'createallfromdatanorm' && $id > 0) { $import->fetch($id); if ($import->fk_soc > 0) { // Get Datanorm settings $markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30); $searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0); // Load supplier $supplier = new Societe($db); $supplier->fetch($import->fk_soc); $supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3)); // Get all lines without product $lines = $importLine->fetchAllByImport($import->id); $datanorm = new Datanorm($db); $createdCount = 0; $assignedCount = 0; $errorCount = 0; require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; foreach ($lines as $lineObj) { // Skip lines that already have a product if ($lineObj->fk_product > 0) { continue; } // Skip lines without supplier_ref if (empty($lineObj->supplier_ref)) { continue; } // Search in Datanorm database $results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 1); if (empty($results) && !empty($lineObj->ean)) { $results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 1); } if (!empty($results)) { $datanormArticle = $results[0]; $datanorm->fetch($datanormArticle['id']); $purchasePrice = $datanorm->price; if ($datanorm->price_unit > 1) { $purchasePrice = $datanorm->price / $datanorm->price_unit; } // Get copper surcharge for selling price calculation // Priority: 1. Datanorm, 2. ZUGFeRD $copperSurchargeForPrice = 0; if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) { $copperSurchargeForPrice = $datanorm->metal_surcharge; // Normalize to per-unit if price_unit > 1 if ($datanorm->price_unit > 1) { $copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit; } } elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) { $copperSurchargeForPrice = $lineObj->copper_surcharge; // Normalize based on copper_surcharge_basis_qty if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) { $copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty; } } // Check if product already exists in Dolibarr $existingProduct = new Product($db); $productExists = false; $existingProductId = 0; // 1. Check by supplier reference (ProductFournisseur) $sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf"; $sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc; $sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'"; $sqlCheck .= " AND pf.entity IN (".getEntity('product').")"; $resqlCheck = $db->query($sqlCheck); if ($resqlCheck && $db->num_rows($resqlCheck) > 0) { $objCheck = $db->fetch_object($resqlCheck); $existingProductId = $objCheck->fk_product; $productExists = true; } // 2. Check by product reference pattern if (!$productExists) { $expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number; $fetchResult = $existingProduct->fetch(0, $expectedRef); if ($fetchResult > 0) { $existingProductId = $existingProduct->id; $productExists = true; } } // 3. Check by EAN if available if (!$productExists && !empty($datanorm->ean)) { $sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product"; $sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'"; $sqlEan .= " AND entity IN (".getEntity('product').")"; $resqlEan = $db->query($sqlEan); if ($resqlEan && $db->num_rows($resqlEan) > 0) { $objEan = $db->fetch_object($resqlEan); $existingProductId = $objEan->rowid; $productExists = true; } } if ($productExists && $existingProductId > 0) { // Product exists - just assign it to the line $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); // Prepare extrafields $altExtrafields = array(); if (!empty($altDatanorm->metal_surcharge) && $altDatanorm->metal_surcharge > 0 && !empty($altDatanorm->price)) { $altExtrafields['options_produktpreis'] = $altDatanorm->price; } if (!empty($altDatanorm->product_group)) { $altExtrafields['options_warengruppe'] = $altDatanorm->product_group; } // Mindestbestellmenge und Verpackungseinheit vom bestehenden Preis übernehmen $altMinQty = 1; $altPackaging = null; $sqlAltExisting = "SELECT quantity, packaging FROM " . MAIN_DB_PREFIX . "product_fournisseur_price"; $sqlAltExisting .= " WHERE fk_product = " . (int)$existingProductId; $sqlAltExisting .= " AND quantity > 0 ORDER BY rowid ASC LIMIT 1"; $resAltExisting = $db->query($sqlAltExisting); if ($resAltExisting && $db->num_rows($resAltExisting) > 0) { $objAltExisting = $db->fetch_object($resAltExisting); if ($objAltExisting->quantity > 0) { $altMinQty = $objAltExisting->quantity; } if (!empty($objAltExisting->packaging)) { $altPackaging = $objAltExisting->packaging; } } // Preis berechnen - bei Kabeln ist Datanorm-Preis bereits Ringpreis! $altCableText = $altDatanorm->short_text1 . ' ' . $altDatanorm->short_text2; $altRingSize = extractCableRingSize($altCableText); if ($altRingSize > 0) { // Kabel: Stückpreis aus Ringpreis berechnen, dann auf minQty hochrechnen $altUnitPrice = $altDatanorm->price / $altRingSize; $altTotalPrice = $altUnitPrice * $altMinQty; $altExtrafields['options_preiseinheit'] = $altRingSize; } elseif (!empty($altDatanorm->price_unit) && $altDatanorm->price_unit > 1) { // Nicht-Kabel mit price_unit > 1 $altUnitPrice = $altDatanorm->price / $altDatanorm->price_unit; $altTotalPrice = $altUnitPrice * $altMinQty; $altExtrafields['options_preiseinheit'] = $altDatanorm->price_unit; } else { // Einzelstück $altTotalPrice = $altDatanorm->price * $altMinQty; } // Add supplier price $altProdfourn = new ProductFournisseur($db); $altProdfourn->id = $existingProductId; $altResult = $altProdfourn->update_buyprice( $altMinQty, $altTotalPrice, $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 ); // Verpackungseinheit nachträglich setzen if ($altResult > 0 && !empty($altPackaging)) { $sqlAltPkg = "UPDATE " . MAIN_DB_PREFIX . "product_fournisseur_price"; $sqlAltPkg .= " SET packaging = " . (float)$altPackaging; $sqlAltPkg .= " WHERE rowid = " . (int)$altResult; $db->query($sqlAltPkg); } // 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++; } else { // Create new product $newproduct = new Product($db); $newproduct->type = 0; $newproduct->status = 1; $newproduct->status_buy = 1; $newproduct->ref = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number; $newproduct->label = $datanorm->short_text1; if (!empty($datanorm->short_text2) && strlen($newproduct->label) < 100) { $newproduct->label .= ' '.$datanorm->short_text2; } $newproduct->description = $datanorm->getFullDescription(); // Set default accounting codes from module settings $newproduct->accountancy_code_sell = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL', ''); $newproduct->accountancy_code_sell_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_INTRA', ''); $newproduct->accountancy_code_sell_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_SELL_EXPORT', ''); $newproduct->accountancy_code_buy = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY', ''); $newproduct->accountancy_code_buy_intra = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_INTRA', ''); $newproduct->accountancy_code_buy_export = getDolGlobalString('IMPORTZUGFERD_ACCOUNTING_CODE_BUY_EXPORT', ''); // Selling price: (purchase price + copper surcharge) × (1 + markup%) $sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100); $newproduct->price = $sellingPrice; $newproduct->price_base_type = 'HT'; $newproduct->tva_tx = $lineObj->tax_percent ?: 19; if (!empty($datanorm->weight)) { $newproduct->weight = $datanorm->weight; $newproduct->weight_units = 0; } if (isModEnabled('barcode') && getDolGlobalString('BARCODE_PRODUCT_ADDON_NUM')) { $newproduct->barcode = '-1'; } $result = $newproduct->create($user); if ($result > 0) { // Add supplier price $prodfourn = new ProductFournisseur($db); $prodfourn->id = $newproduct->id; $prodfourn->fourn_ref = $datanorm->article_number; $supplierEan = ''; $supplierEanType = 0; if (!empty($datanorm->ean)) { $supplierEan = $datanorm->ean; $supplierEanType = 2; } elseif (!empty($lineObj->ean)) { $supplierEan = $lineObj->ean; $supplierEanType = 2; } // Prepare extrafields for supplier price $supplierPriceExtrafields = array(); // Produktpreis (reiner Materialpreis) - nur bei Kabeln mit Metallzuschlag if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0 && !empty($datanorm->price)) { $supplierPriceExtrafields['options_produktpreis'] = $datanorm->price; } // Preiseinheit - Priorität: 1. ZUGFeRD basis_quantity, 2. Datanorm price_unit if (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity; } elseif (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit; } // Warengruppe aus Datanorm if (!empty($datanorm->product_group)) { $supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group; } // Mindestbestellmenge und Preis mit zentraler Funktion berechnen $pricing = calculateCablePricing($datanorm, 1); $newMinQty = $pricing['priceUnit']; $newPackaging = $pricing['priceUnit']; $newTotalPrice = $datanorm->price; // Originalpreis aus Datanorm // Fallback auf ZUGFeRD basis_quantity wenn keine Ringgröße erkannt if ($newMinQty == 1 && !empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { $newMinQty = $lineObj->basis_quantity; $newPackaging = $lineObj->basis_quantity; $newTotalPrice = $purchasePrice * $newMinQty; } $newPriceResult = $prodfourn->update_buyprice( $newMinQty, // Quantity (Mindestbestellmenge) $newTotalPrice, // Price (Gesamtpreis für die Mindestmenge) $user, 'HT', // Price base $supplier, // Supplier 0, // Availability $datanorm->article_number, // Supplier ref $lineObj->tax_percent ?: 19, // VAT 0, // Charges 0, // Remise 0, // Remise percentage 0, // No price minimum 0, // Delivery delay 0, // Reputation array(), // Localtaxes array '', // Default VAT code 0, // Multicurrency price 'HT', // Multicurrency price base type 1, // Multicurrency tx '', // Multicurrency code trim($datanorm->short_text1 . ($datanorm->short_text2 ? ' ' . $datanorm->short_text2 : '')), // Description from Datanorm $supplierEan, // Barcode/EAN $supplierEanType, // Barcode type $supplierPriceExtrafields // Extra fields ); // Manually ensure extrafields record exists for supplier price // (Dolibarr update_buyprice doesn't always create it properly) $sqlGetPrice = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price"; $sqlGetPrice .= " WHERE fk_product = ".(int)$newproduct->id; $sqlGetPrice .= " AND fk_soc = ".(int)$supplier->id; $sqlGetPrice .= " ORDER BY rowid DESC LIMIT 1"; $resGetPrice = $db->query($sqlGetPrice); if ($resGetPrice && $db->num_rows($resGetPrice) > 0) { $objPrice = $db->fetch_object($resGetPrice); $priceId = $objPrice->rowid; // Check if extrafields record exists $sqlCheckExtra = "SELECT rowid FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlCheckExtra .= " WHERE fk_object = ".(int)$priceId; $resCheckExtra = $db->query($sqlCheckExtra); $produktpreis = !empty($supplierPriceExtrafields['options_produktpreis']) ? (float)$supplierPriceExtrafields['options_produktpreis'] : 'NULL'; $preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1; $warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL'; if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) { // Insert extrafields record $sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlInsertExtra .= " (fk_object, produktpreis, preiseinheit, warengruppe) VALUES ("; $sqlInsertExtra .= (int)$priceId.", "; $sqlInsertExtra .= ($produktpreis === 'NULL' ? "NULL" : $produktpreis).", "; $sqlInsertExtra .= $preiseinheit.", "; $sqlInsertExtra .= $warengruppe.")"; if (!$db->query($sqlInsertExtra)) { dol_syslog('ImportZugferd: Fehler beim Einfuegen der Extrafields: '.$db->lasterror(), LOG_ERR); } } else { // Update extrafields record $sqlUpdateExtra = "UPDATE ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields SET "; $sqlUpdateExtra .= "produktpreis = ".($produktpreis === 'NULL' ? "NULL" : $produktpreis).", "; $sqlUpdateExtra .= "preiseinheit = ".$preiseinheit.", "; $sqlUpdateExtra .= "warengruppe = ".$warengruppe." "; $sqlUpdateExtra .= "WHERE fk_object = ".(int)$priceId; if (!$db->query($sqlUpdateExtra)) { dol_syslog('ImportZugferd: Fehler beim Update der Extrafields: '.$db->lasterror(), LOG_ERR); } } } // Create product mapping $mapping = new ProductMapping($db); $mapping->fk_soc = $import->fk_soc; $mapping->supplier_ref = $datanorm->article_number; $mapping->fk_product = $newproduct->id; $mapping->ean = $datanorm->ean; $mapping->manufacturer_ref = $datanorm->manufacturer_ref; $mapping->description = $datanorm->short_text1; $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 && !empty($altDatanorm->price)) { $altExtrafields['options_produktpreis'] = $altDatanorm->price; } 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 $lineObj->setProduct($newproduct->id, 'datanorm', $user); $createdCount++; } else { $errorCount++; } } } } if ($createdCount > 0) { setEventMessages($langs->trans('DatanormBatchCreated', $createdCount), null, 'mesgs'); } if ($assignedCount > 0) { setEventMessages($langs->trans('DatanormBatchAssigned', $assignedCount), null, 'mesgs'); } if ($errorCount > 0) { setEventMessages($langs->trans('DatanormBatchErrors', $errorCount), null, 'warnings'); } if ($createdCount == 0 && $assignedCount == 0 && $errorCount == 0) { setEventMessages($langs->trans('DatanormBatchNoMatches'), null, 'warnings'); } // Check if all lines now have products $allHaveProducts = $importLine->allLinesHaveProducts($id); if ($allHaveProducts) { $import->status = ZugferdImport::STATUS_IMPORTED; $import->update($user); } // Redirect to avoid "Form resubmit" warning on page reload $redirectUrl = $_SERVER['PHP_SELF'].'?id='.$id; header('Location: '.$redirectUrl); exit; } $action = 'edit'; $import->fetch($id); } // Preview Datanorm matches (step 1 - show what will be created) $datanormPreviewMatches = array(); if ($action == 'previewdatanorm' && $id > 0) { $import->fetch($id); if ($import->fk_soc > 0) { // Get Datanorm settings $markup = getDolGlobalString('IMPORTZUGFERD_DATANORM_MARKUP', 30); $searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0); // Load supplier $supplier = new Societe($db); $supplier->fetch($import->fk_soc); $supplierPrefix = strtoupper(substr(preg_replace('/[^a-zA-Z]/', '', $supplier->name), 0, 3)); // Get all lines without product $lines = $importLine->fetchAllByImport($import->id); $datanorm = new Datanorm($db); require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; foreach ($lines as $lineObj) { // Skip lines that already have a product if ($lineObj->fk_product > 0) { continue; } // Skip lines without supplier_ref if (empty($lineObj->supplier_ref)) { continue; } // Search in Datanorm database - get ALL supplier alternatives $results = $datanorm->searchByArticleNumber($lineObj->supplier_ref, $import->fk_soc, $searchAll, 10); if (empty($results) && !empty($lineObj->ean)) { $results = $datanorm->searchByArticleNumber($lineObj->ean, $import->fk_soc, $searchAll, 10); } if (!empty($results)) { // Process the primary result (first = current supplier or cheapest) $datanormArticle = $results[0]; $datanorm->fetch($datanormArticle['id']); $purchasePrice = $datanorm->price; if ($datanorm->price_unit > 1) { $purchasePrice = $datanorm->price / $datanorm->price_unit; } // Get copper surcharge for selling price calculation $copperSurchargeForPrice = 0; if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) { $copperSurchargeForPrice = $datanorm->metal_surcharge; if ($datanorm->price_unit > 1) { $copperSurchargeForPrice = $datanorm->metal_surcharge / $datanorm->price_unit; } } elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) { $copperSurchargeForPrice = $lineObj->copper_surcharge; if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) { $copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty; } } // Calculate selling price $sellingPrice = ($purchasePrice + $copperSurchargeForPrice) * (1 + $markup / 100); // Check if product already exists in Dolibarr $existingProductId = 0; $productAction = 'create'; // 'create' or 'assign' // 1. Check by supplier reference (ProductFournisseur) $sqlCheck = "SELECT DISTINCT pf.fk_product FROM ".MAIN_DB_PREFIX."product_fournisseur_price as pf"; $sqlCheck .= " WHERE pf.fk_soc = ".(int)$import->fk_soc; $sqlCheck .= " AND pf.ref_fourn = '".$db->escape($datanorm->article_number)."'"; $sqlCheck .= " AND pf.entity IN (".getEntity('product').")"; $resqlCheck = $db->query($sqlCheck); if ($resqlCheck && $db->num_rows($resqlCheck) > 0) { $objCheck = $db->fetch_object($resqlCheck); $existingProductId = $objCheck->fk_product; $productAction = 'assign'; } // 2. Check by product reference pattern if ($existingProductId <= 0) { $expectedRef = 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number; $existingProduct = new Product($db); $fetchResult = $existingProduct->fetch(0, $expectedRef); if ($fetchResult > 0) { $existingProductId = $existingProduct->id; $productAction = 'assign'; } } // 3. Check by EAN if available if ($existingProductId <= 0 && !empty($datanorm->ean)) { $sqlEan = "SELECT rowid FROM ".MAIN_DB_PREFIX."product"; $sqlEan .= " WHERE barcode = '".$db->escape($datanorm->ean)."'"; $sqlEan .= " AND entity IN (".getEntity('product').")"; $resqlEan = $db->query($sqlEan); if ($resqlEan && $db->num_rows($resqlEan) > 0) { $objEan = $db->fetch_object($resqlEan); $existingProductId = $objEan->rowid; $productAction = 'assign'; } } // Build supplier alternatives array // Only show suppliers that don't already have a price for this product $supplierAlternatives = array(); $existingPriceSuppliers = array(); $currentDolibarrPrice = 0; // Current supplier price from Dolibarr // Load existing product's copper surcharge for price comparison $productCopperSurcharge = 0; $currentSupplierPriceId = 0; $currentSupplierMinQty = 1; if ($existingProductId > 0) { $sqlExisting = "SELECT pf.rowid, pf.fk_soc, pf.unitprice, pf.quantity FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf"; $sqlExisting .= " WHERE pf.fk_product = ".(int)$existingProductId; $resExisting = $db->query($sqlExisting); if ($resExisting) { while ($objEx = $db->fetch_object($resExisting)) { $existingPriceSuppliers[$objEx->fk_soc] = true; // Load current invoice supplier's Dolibarr price for comparison if ($objEx->fk_soc == $import->fk_soc) { $currentDolibarrPrice = (float)$objEx->unitprice; $currentSupplierPriceId = $objEx->rowid; $currentSupplierMinQty = max(1, $objEx->quantity); } } } // Load copper surcharge from supplier price extrafields (not product extrafields!) if ($currentSupplierPriceId > 0) { $sqlCopper = "SELECT kupferzuschlag FROM ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlCopper .= " WHERE fk_object = ".(int)$currentSupplierPriceId; $resCopper = $db->query($sqlCopper); if ($resCopper && $db->num_rows($resCopper) > 0) { $objCopper = $db->fetch_object($resCopper); if (!empty($objCopper->kupferzuschlag) && $objCopper->kupferzuschlag > 0) { $productCopperSurcharge = (float)$objCopper->kupferzuschlag; } } } } foreach ($results as $altResult) { // Skip if supplier already has a price for this product if ($existingProductId > 0 && isset($existingPriceSuppliers[$altResult['fk_soc']])) { continue; } $altSupplier = new Societe($db); $altSupplier->fetch($altResult['fk_soc']); // Calculate unit price from Datanorm $priceUnit = ($altResult['price_unit'] > 0) ? $altResult['price_unit'] : 1; $materialPrice = $altResult['price']; // Datanorm has NO metal surcharge - use copper surcharge from Dolibarr supplier price instead // $productCopperSurcharge is already loaded from extrafields above $metalSurcharge = $productCopperSurcharge; // Total price for price unit (e.g. for 100 pieces) $totalPriceForUnit = $materialPrice + $metalSurcharge; // Unit price (price per 1 piece/meter) $altPurchasePrice = $totalPriceForUnit / $priceUnit; $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'], 'effective_price_unit' => $priceUnit, // Actual divisor used 'purchase_price' => $altPurchasePrice, // Calculated unit price (Datanorm + Copper) 'datanorm_base_price' => $materialPrice / $priceUnit, // Material only per unit 'metal_surcharge' => $metalSurcharge / $priceUnit, // Copper surcharge per unit (from Dolibarr) 'ean' => $altResult['ean'], 'manufacturer_ref' => $altResult['manufacturer_ref'], 'is_invoice_supplier' => ($altResult['fk_soc'] == $import->fk_soc), ); } // Store match info for preview $datanormPreviewMatches[] = array( 'line_id' => $lineObj->id, 'line_supplier_ref' => $lineObj->supplier_ref, 'line_product_name' => $lineObj->product_name, 'line_quantity' => $lineObj->quantity, 'line_unit_price' => $lineObj->unit_price, 'datanorm_id' => $datanorm->id, 'datanorm_article_number' => $datanorm->article_number, 'datanorm_short_text1' => $datanorm->short_text1, 'datanorm_short_text2' => $datanorm->short_text2, 'datanorm_price' => $datanorm->price, 'datanorm_price_unit' => $datanorm->price_unit, 'datanorm_ean' => $datanorm->ean, 'purchase_price' => $currentDolibarrPrice, // Current Dolibarr unit price 'purchase_min_qty' => $currentSupplierMinQty, // Dolibarr minimum quantity 'purchase_copper_surcharge' => $productCopperSurcharge, // Dolibarr copper surcharge (for min qty) 'datanorm_purchase_price' => $purchasePrice, // New Datanorm unit price 'selling_price' => $sellingPrice, 'copper_surcharge' => $copperSurchargeForPrice, // Copper surcharge (per unit) 'existing_product_id' => $existingProductId, 'action' => $productAction, 'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number, 'supplier_alternatives' => $supplierAlternatives ); } } } $action = 'edit'; } // Create supplier invoice if ($action == 'createinvoice' && $id > 0) { $import->fetch($id); // Check prerequisites if ($import->fk_soc <= 0) { $error++; setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors'); } else { // Check all lines have products $lines = $importLine->fetchAllByImport($id); $allHaveProducts = true; foreach ($lines as $line) { if ($line->fk_product <= 0) { $allHaveProducts = false; break; } } if (!$allHaveProducts) { $error++; setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors'); } else { // Load supplier to get default values $supplier = new Societe($db); $supplier->fetch($import->fk_soc); // Create invoice $invoice = new FactureFournisseur($db); $invoice->socid = $import->fk_soc; $invoice->ref_supplier = $import->invoice_number; $invoice->date = $import->invoice_date; $invoice->note_private = $langs->trans('ImportedFromZugferd').' ('.$import->ref.')'; // Set label to most expensive item (for list display) $maxTotal = 0; $mostExpensiveLabel = ''; foreach ($lines as $line) { $lineTotal = $line->quantity * $line->unit_price; if ($lineTotal > $maxTotal) { $maxTotal = $lineTotal; $mostExpensiveLabel = $line->product_name; } } if (!empty($mostExpensiveLabel)) { // Truncate to 255 chars (database field limit) $invoice->label = dol_trunc($mostExpensiveLabel, 255); } // Use supplier default values for payment $invoice->cond_reglement_id = $supplier->cond_reglement_supplier_id ?: 1; $invoice->mode_reglement_id = $supplier->mode_reglement_supplier_id ?: 0; $invoice->fk_account = $supplier->fk_account ?: 0; $db->begin(); $result = $invoice->create($user); if ($result > 0) { // Add lines foreach ($lines as $line) { $res = $invoice->addline( $line->product_name, $line->unit_price, $line->tax_percent, 0, 0, $line->quantity, $line->fk_product, 0, '', '', 0, 0, 'HT', // price_base_type - Netto-Preise aus ZUGFeRD 0 // type (0=product) ); if ($res < 0) { $error++; setEventMessages($invoice->error, $invoice->errors, 'errors'); break; } // Update EAN on product if not set if (!empty($line->ean) && $line->fk_product > 0) { $product = new Product($db); $product->fetch($line->fk_product); if (empty($product->barcode)) { $product->barcode = $line->ean; $product->barcode_type = 2; // EAN13 $product->update($product->id, $user); } } } if (!$error) { // Invoice stays as draft - user can validate manually // Copy PDF to invoice and register in ECM $source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename; if (file_exists($source_pdf)) { // Relativer Pfad für ECM (ohne DOL_DATA_ROOT Prefix) $rel_dir = 'fournisseur/facture/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref; $dest_dir = $conf->fournisseur->facture->dir_output.'/'.get_exdir($invoice->id, 2, 0, 0, $invoice, 'invoice_supplier').$invoice->ref; if (!is_dir($dest_dir)) { dol_mkdir($dest_dir); } $dest_file = $dest_dir.'/'.$import->pdf_filename; if (@copy($source_pdf, $dest_file)) { // In ECM-Datenbank registrieren für korrekte Verknüpfung require_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php'; $ecmfile = new EcmFiles($db); $ecmfile->filepath = $rel_dir; $ecmfile->filename = $import->pdf_filename; $ecmfile->label = md5_file(dol_osencode($dest_file)); $ecmfile->fullpath_orig = $dest_file; $ecmfile->gen_or_uploaded = 'uploaded'; $ecmfile->description = 'ZUGFeRD Import - '.$import->invoice_number; $ecmfile->src_object_type = 'supplier_invoice'; $ecmfile->src_object_id = $invoice->id; $ecmfile->entity = $conf->entity; $result = $ecmfile->create($user); if ($result < 0) { dol_syslog('ImportZugferd: Fehler beim ECM-Eintrag: '.implode(',', $ecmfile->errors), LOG_ERR); } } else { dol_syslog('ImportZugferd: Fehler beim Kopieren der PDF nach '.$dest_dir, LOG_ERR); } } // Update import record $import->fk_facture_fourn = $invoice->id; $import->status = ZugferdImport::STATUS_PROCESSED; $import->date_import = dol_now(); $import->update($user); $db->commit(); setEventMessages($langs->trans('InvoiceCreatedSuccessfully'), null, 'mesgs'); // Redirect to invoice header('Location: '.DOL_URL_ROOT.'/fourn/facture/card.php?facid='.$invoice->id); exit; } else { $db->rollback(); } } else { $error++; setEventMessages($invoice->error, $invoice->errors, 'errors'); $db->rollback(); } } } $action = 'edit'; } // Finish import - check for existing invoice and update status if ($action == 'finishimport' && $id > 0) { $import->fetch($id); // Check all lines have products $lines = $importLine->fetchAllByImport($id); $allHaveProducts = true; foreach ($lines as $line) { if ($line->fk_product <= 0) { $allHaveProducts = false; break; } } if (!$allHaveProducts) { $error++; setEventMessages($langs->trans('ErrorNotAllProductsAssigned'), null, 'errors'); } elseif ($import->fk_soc <= 0) { $error++; setEventMessages($langs->trans('ErrorSupplierRequired'), null, 'errors'); } else { // Search for existing supplier invoice with this ref_supplier $sql = "SELECT rowid FROM ".MAIN_DB_PREFIX."facture_fourn"; $sql .= " WHERE fk_soc = ".((int) $import->fk_soc); $sql .= " AND ref_supplier = '".$db->escape($import->invoice_number)."'"; $sql .= " LIMIT 1"; $resql = $db->query($sql); if ($resql && $db->num_rows($resql) > 0) { $obj = $db->fetch_object($resql); // Found existing invoice - link it $import->fk_facture_fourn = $obj->rowid; $import->status = ZugferdImport::STATUS_PROCESSED; $import->date_import = dol_now(); $result = $import->update($user); if ($result > 0) { $invoiceLink = ''.$import->invoice_number.''; setEventMessages($langs->trans('ImportLinkedToExistingInvoice', $invoiceLink), null, 'mesgs'); } else { setEventMessages($import->error, null, 'errors'); } } else { // No existing invoice - mark as imported (ready for invoice creation) $import->status = ZugferdImport::STATUS_IMPORTED; $result = $import->update($user); if ($result > 0) { setEventMessages($langs->trans('ImportFinished'), null, 'mesgs'); } else { setEventMessages($import->error, null, 'errors'); } } } $action = 'edit'; } // Delete import record if ($action == 'confirm_delete' && $confirm == 'yes' && $id > 0 && $user->hasRight('importzugferd', 'import', 'delete')) { $import->fetch($id); // Delete lines first $importLine->deleteAllByImport($id); // Delete files $import_dir = $conf->importzugferd->dir_output.'/imports/'.$import->id; if (is_dir($import_dir)) { dol_delete_dir_recursive($import_dir); } // Delete import record $import->delete($user); setEventMessages($langs->trans('RecordDeleted'), null, 'mesgs'); header('Location: '.$_SERVER['PHP_SELF']); exit; } /* * View */ $title = $langs->trans('ZugferdImport'); llxHeader('', $title, '', '', 0, 0, '', '', '', 'mod-importzugferd page-import'); print load_fiche_titre($title, '', 'fa-file-import'); // Error message if ($error && !empty($message)) { setEventMessages($message, null, 'errors'); } /* * Upload form (shown when no import is being edited) */ if (empty($action) || ($action == 'upload' && $error)) { print '
'; } /* * Delete confirmation dialog */ if ($action == 'delete' && $id > 0) { $import->fetch($id); $formconfirm = $form->formconfirm( $_SERVER['PHP_SELF'].'?id='.$import->id, $langs->trans('DeleteImportRecord'), $langs->trans('ConfirmDeleteImportRecord', $import->ref), 'confirm_delete', '', 0, 1 ); print $formconfirm; $action = 'edit'; // Continue showing the edit form } /* * Edit/Review import */ if ($action == 'edit' && $import->id > 0) { // Fetch lines $lines = $importLine->fetchAllByImport($import->id); $missingProducts = $importLine->countLinesWithoutProduct($import->id); $allComplete = ($missingProducts == 0 && $import->fk_soc > 0); // Header info print '| '.$langs->trans('InvoiceData').' - '.$import->ref.' | '; print '|||
| '.$langs->trans('InvoiceNumber').' | '; print ''.dol_escape_htmltag($import->invoice_number).' | '; print ''.$langs->trans('InvoiceDate').' | '; print ''.dol_print_date($import->invoice_date, 'day').' | '; print '
| '.$langs->trans('Supplier').' | '; print ''.dol_escape_htmltag($import->seller_name).' | '; print ''.$langs->trans('VATIntra').' | '; print ''.dol_escape_htmltag($import->seller_vat).' | '; print '
| '.$langs->trans('BuyerReference').' | '; print ''.dol_escape_htmltag($import->buyer_reference).' | '; print ''.$langs->trans('TotalHT').' | '; print ''.price($import->total_ht).' '.$import->currency.' | '; print '
| '.$langs->trans('Status').' | '; print ''.$import->getLibStatut(1).' | '; print ''.$langs->trans('TotalTTC').' | '; print ''.price($import->total_ttc).' '.$import->currency.' | '; print '
| '.$langs->trans('Position').' | '; print ''.$langs->trans('SupplierRef').' | '; print ''.$langs->trans('ProductDescription').' | '; print ''.$langs->trans('Qty').' | '; print ''.$langs->trans('UnitPrice').' | '; print ''.$langs->trans('DolibarrPrice').' | '; print ''.$langs->trans('TotalHT').' | '; print ''.$langs->trans('MatchedProduct').' | '; print ''.$langs->trans('Action').' | '; print '||||
| '.$line->line_id.' | '; print ''.dol_escape_htmltag($line->supplier_ref).' | '; print '';
print dol_escape_htmltag($line->product_name);
if (!empty($line->ean) && !$hasProduct) {
print ' EAN: '.dol_escape_htmltag($line->ean).''; } print ' | ';
print ''.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).' | '; print '';
print price($line->unit_price);
if (!empty($line->basis_quantity) && $line->basis_quantity != 1) {
print ' ('.price($line->unit_price_raw).'/'.price2num($line->basis_quantity, 'MS').zugferdGetUnitLabel($line->basis_quantity_unit).')'; } print ' | ';
// Dolibarr price column - show supplier price and difference
print '';
$lineDolibarrTotal = 0;
if ($hasProduct && $import->fk_soc > 0) {
require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php';
$productFourn = new ProductFournisseur($db);
// Use line quantity to find price for matching quantity tier (e.g. 100m cables)
$searchQty = max(1, $line->quantity);
$result = $productFourn->find_min_price_product_fournisseur($line->fk_product, $searchQty, $import->fk_soc);
if ($result > 0 && $productFourn->fourn_unitprice > 0) {
// Use unit price for comparison (per-unit, not per-quantity-tier)
// Note: fourn_unitprice already INCLUDES copper surcharge - it's the total price from invoice
// The extrafield 'kupferzuschlag' is only informational (shows copper portion of price)
// The extrafield 'produktpreis' is only informational (shows material price without copper)
$dolibarrUnitPrice = $productFourn->fourn_unitprice;
$zugferdPrice = $line->unit_price;
$priceDiff = $zugferdPrice - $dolibarrUnitPrice;
$priceDiffPercent = ($dolibarrUnitPrice > 0) ? (($priceDiff / $dolibarrUnitPrice) * 100) : 0;
// Accumulate for summary
$lineDolibarrTotal = $dolibarrUnitPrice * $line->quantity;
$totalDolibarrHT += $lineDolibarrTotal;
$hasDolibarrPrices = true;
$matchedLinesCount++;
print price($dolibarrUnitPrice);
if (abs($priceDiffPercent) >= 0.01) {
$threshold = getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10);
$isSignificant = (abs($priceDiffPercent) >= $threshold);
print ' '; if ($priceDiff > 0) { // ZUGFeRD price is higher $iconColor = $isSignificant ? 'color: #d9534f;' : 'color: #f0ad4e;'; print ''; print ' +'.number_format($priceDiffPercent, 1).'%'; print ''; } else { // ZUGFeRD price is lower $iconColor = $isSignificant ? 'color: #5cb85c;' : 'color: #5bc0de;'; print ''; print ' '.number_format($priceDiffPercent, 1).'%'; print ''; } } else { print ' '; } } else { print ''.$langs->trans('NoPriceFound').''; $allProductsMatched = false; // No price found for matched product } } else { print '-'; $allProductsMatched = false; // Product not matched } print ' | ';
print ''.price($line->line_total).' | '; print '';
if ($hasProduct) {
$product = new Product($db);
$product->fetch($line->fk_product);
print $product->getNomUrl(1);
if (!empty($line->match_method)) {
print ' '.$langs->trans('MatchMethod').': '.$line->match_method.''; } if (!empty($line->ean)) { print ' '.dol_escape_htmltag($line->ean).''; } print ' '; // Alle Einkaufspreise des Produktes anzeigen $sqlPrices = "SELECT pfp.fk_soc, pfp.price, pfp.unitprice, pfp.ref_fourn, pfp.quantity, s.nom as supplier_name"; $sqlPrices .= " FROM ".MAIN_DB_PREFIX."product_fournisseur_price pfp"; $sqlPrices .= " LEFT JOIN ".MAIN_DB_PREFIX."societe s ON s.rowid = pfp.fk_soc"; $sqlPrices .= " WHERE pfp.fk_product = ".(int)$line->fk_product; $sqlPrices .= " ORDER BY pfp.unitprice ASC"; $resPrices = $db->query($sqlPrices); if ($resPrices && $db->num_rows($resPrices) > 0) { print ' ';
while ($objP = $db->fetch_object($resPrices)) {
$isInvoiceSupplier = ($objP->fk_soc == $import->fk_soc);
$style = $isInvoiceSupplier ? 'font-weight: bold;' : 'color: #666;';
print ' ';
}
} else {
print ''.$langs->trans('NoProductMatch').'';
}
print '';
print dol_escape_htmltag($objP->supplier_name);
print ': '.price($objP->unitprice).'';
if ($objP->quantity > 1) {
print ' ('.price($objP->price).'/'.(int)$objP->quantity.'Stk.)';
}
if (!empty($objP->ref_fourn)) {
print ' ('.dol_escape_htmltag($objP->ref_fourn).')';
}
if ($isInvoiceSupplier) {
print ' ';
}
print ' ';
}
print ' | ';
print '';
if ($hasProduct) {
// Remove assignment button
print '';
print '';
print '';
// Fehlende Lieferantenpreise aus anderen Katalogen sammeln (Anzeige weiter unten)
if ($import->fk_soc > 0 && getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL')) {
// Alle vorhandenen Lieferantenpreise fuer dieses Produkt laden
$sqlExistingPrices = "SELECT fk_soc, price, unitprice, quantity, barcode FROM ".MAIN_DB_PREFIX."product_fournisseur_price";
$sqlExistingPrices .= " WHERE fk_product = ".(int)$line->fk_product;
$resExistingPrices = $db->query($sqlExistingPrices);
$existingSupplierIds = array();
$currentSupplierPrice = 0;
$currentSupplierQty = 1;
$supplierEan = '';
if ($resExistingPrices) {
while ($objPrice = $db->fetch_object($resExistingPrices)) {
$existingSupplierIds[$objPrice->fk_soc] = true;
// Stueckpreis, Mindestmenge und EAN vom Rechnungslieferanten merken
if ($objPrice->fk_soc == $import->fk_soc) {
$currentSupplierPrice = $objPrice->unitprice;
$currentSupplierQty = max(1, $objPrice->quantity);
if (!empty($objPrice->barcode)) {
$supplierEan = $objPrice->barcode;
}
}
}
}
// Suche mit Lieferanten-Artikelnummer (die EAN wird intern für Cross-Catalog verwendet)
$datanormSearch = new Datanorm($db);
$allCatalogResults = array();
// Load copper surcharge from current supplier price extrafields for price comparison
$productCopperSurcharge = 0;
$sqlCopper = "SELECT pfe.kupferzuschlag FROM ".MAIN_DB_PREFIX."product_fournisseur_price pf";
$sqlCopper .= " LEFT JOIN ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields pfe ON pfe.fk_object = pf.rowid";
$sqlCopper .= " WHERE pf.fk_product = ".(int)$line->fk_product;
$sqlCopper .= " AND pf.fk_soc = ".(int)$import->fk_soc;
$resCopper = $db->query($sqlCopper);
if ($resCopper && $db->num_rows($resCopper) > 0) {
$objCopper = $db->fetch_object($resCopper);
if (!empty($objCopper->kupferzuschlag) && $objCopper->kupferzuschlag > 0) {
$productCopperSurcharge = (float)$objCopper->kupferzuschlag;
}
}
// Suche mit Artikelnummer - die Funktion nutzt dann die EAN für Cross-Catalog
if (!empty($line->supplier_ref)) {
$allCatalogResults = $datanormSearch->searchByArticleNumber($line->supplier_ref, $import->fk_soc, true, 10);
}
if (!empty($allCatalogResults)) {
$missingSuppliers = array();
foreach ($allCatalogResults as $catalogResult) {
if (!isset($existingSupplierIds[$catalogResult['fk_soc']])) {
$altSupplier = new Societe($db);
$altSupplier->fetch($catalogResult['fk_soc']);
// Calculate unit price from Datanorm (same logic as supplier_alternatives)
$priceUnit = ($catalogResult['price_unit'] > 0) ? $catalogResult['price_unit'] : 1;
$materialPrice = $catalogResult['price'];
// Datanorm has NO metal surcharge - use copper surcharge from Dolibarr supplier price
// $productCopperSurcharge is already loaded from extrafields above
$metalSurcharge = $productCopperSurcharge;
// Total price for price unit
$totalPriceForUnit = $materialPrice + $metalSurcharge;
// Unit price (price per 1 piece/meter)
$altPurchasePrice = $totalPriceForUnit / $priceUnit;
$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'],
'effective_price_unit' => $priceUnit,
'purchase_price' => $altPurchasePrice, // Calculated unit price (Datanorm + Copper)
'datanorm_base_price' => $materialPrice / $priceUnit, // Material only per unit
'metal_surcharge' => $metalSurcharge / $priceUnit, // Copper surcharge per unit (from Dolibarr)
'ean' => $catalogResult['ean'],
);
}
}
if (!empty($missingSuppliers)) {
// Inline-Anzeige der fehlenden Lieferantenpreise direkt bei der Produktzeile
$toggleId = 'missing_inline_'.$line->id;
$missingCount = count($missingSuppliers);
print ' ';
print ' '; // End inline box
// Track for global actions
$hasMissingPrices = true;
}
}
}
} else {
// Product selection form
print '';
// Create new product link
$create_url = DOL_URL_ROOT.'/product/card.php?action=create';
$create_url .= '&label='.urlencode($line->product_name);
$create_url .= '&price='.urlencode($line->unit_price);
$create_desc = '';
if (!empty($line->supplier_ref)) {
$create_desc .= $langs->trans('SupplierRef').': '.$line->supplier_ref."\n";
}
if (!empty($line->unit_code)) {
$create_desc .= $langs->trans('Unit').': '.zugferdGetUnitLabel($line->unit_code)."\n";
}
if (!empty($line->ean)) {
$create_desc .= 'EAN: '.$line->ean."\n";
}
$create_url .= '&description='.urlencode(trim($create_desc));
print '';
print '';
print $langs->trans('MissingSupplierPrices');
print ' '.$missingCount.'';
print ' ';
print ' ';
// Aufklappbarer Bereich (Standard: sichtbar/aufgeklappt)
print '';
foreach ($missingSuppliers as $missing) {
// Calculate price for minimum quantity
$priceUnit = !empty($missing['effective_price_unit']) ? $missing['effective_price_unit'] : $missing['price_unit'];
$totalPriceForUnit = $missing['price'] + (!empty($missing['metal_surcharge']) ? ($missing['metal_surcharge'] * $priceUnit) : 0);
$priceDiffHtml = '';
if ($currentSupplierPrice > 0) {
// Compare unit prices (Stückpreise)
$pDiff = $missing['purchase_price'] - $currentSupplierPrice;
$pDiffPercent = ($pDiff / $currentSupplierPrice) * 100;
if ($pDiff < 0) {
$priceDiffHtml = ' '.number_format(abs($pDiffPercent), 1).'%';
} elseif ($pDiff > 0) {
$priceDiffHtml = ' +'.number_format($pDiffPercent, 1).'%';
} else {
$priceDiffHtml = ' =';
}
}
// Wert: productId,socId,datanormId
$cbValue = $line->fk_product.','.$missing['fk_soc'].','.$missing['datanorm_id'];
print ' '; // End toggleable div
print '';
print '';
print ' ';
}
print ''; print ' '.$langs->trans('CreateProduct'); print ''; // Refresh-Button nach Produktanlage print ' '; print ''; print ''; // Product template print ' '; print ''; // Datanorm button (only if supplier is set and supplier_ref exists) if ($import->fk_soc > 0 && !empty($line->supplier_ref)) { // Check if Datanorm article exists $datanormCheck = new Datanorm($db); $searchAll = getDolGlobalString('IMPORTZUGFERD_DATANORM_SEARCH_ALL', 0); $datanormResults = $datanormCheck->searchByArticleNumber($line->supplier_ref, $import->fk_soc, $searchAll, 1); if (!empty($datanormResults)) { $datanormArticle = $datanormResults[0]; print ' '; print ''; print ''.$langs->trans('CreateFromDatanorm'); print ''; // Button to show raw Datanorm data print ' '; print ''; print ''; // Show comparison: Invoice name vs Datanorm name print ' ';
print ' ';
}
}
}
print '
| ';
print '||||
| '.$langs->trans('Total').' '.$langs->trans('TotalHT').' | '; if ($allProductsMatched && $hasDolibarrPrices) { // Full comparison possible - all products matched with prices $totalDiff = $totalZugferdHT - $totalDolibarrHT; $totalDiffPercent = ($totalDolibarrHT > 0) ? (($totalDiff / $totalDolibarrHT) * 100) : 0; // Determine colors: green if close match, red if significant difference $threshold = getDolGlobalInt('IMPORTZUGFERD_PRICE_DIFF_THRESHOLD', 10); $isMatch = (abs($totalDiffPercent) < 0.5); // Less than 0.5% difference = match $isSignificant = (abs($totalDiffPercent) >= $threshold); if ($isMatch) { $cellStyle = 'background-color: #dff0d8;'; // Green } elseif ($isSignificant) { $cellStyle = 'background-color: #f2dede;'; // Red } else { $cellStyle = 'background-color: #fcf8e3;'; // Yellow/warning } print '';
print ''.price($totalDolibarrHT).'';
if (abs($totalDiffPercent) >= 0.01) {
print ' '; if ($totalDiff > 0) { print ' +'.number_format($totalDiffPercent, 1).'%'; } elseif ($totalDiff < 0) { print ' '.number_format($totalDiffPercent, 1).'%'; } } print ' | ';
print ''.price($totalZugferdHT).' | '; print ''; if ($isMatch) { print ' '.$langs->trans('SumValidationOk').''; } else { print ' '.$langs->trans('Difference').': '.price($totalDiff).' '.$import->currency.''; } print ' | '; } else { // Not all products matched - show totals but no comparison print '';
if ($hasDolibarrPrices) {
print ''.price($totalDolibarrHT).'';
print ' ('.$matchedLinesCount.'/'.$totalLinesCount.')'; } else { print '-'; } print ' | ';
print ''.price($totalZugferdHT).' | '; print ''; print ' '.$langs->trans('ProductsNotAssigned').''; print ' | '; } print '||||||