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'); // 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 = ''; /* * 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) { // Delete old lines $oldLines = new ImportLine($db); $oldLines->deleteAllByImport($oldImport->id); // Delete old files $old_dir = $conf->importzugferd->dir_output.'/imports/'.$oldImport->id; if (is_dir($old_dir)) { dol_delete_dir_recursive($old_dir); } // Delete old import record $oldImport->delete($user); } } // 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); } rename($destfile, $final_dir.'/'.$filename); // 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); } // 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(); // Prices $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 basis quantity differs 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 to per-unit if basis quantity differs if (!empty($lineObj->copper_surcharge_basis_qty) && $lineObj->copper_surcharge_basis_qty > 1) { $copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->copper_surcharge_basis_qty; } elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { $copperSurchargeForPrice = $lineObj->copper_surcharge / $lineObj->basis_quantity; } } // Selling price with markup: (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; // 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) { // 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(); // Kupferzuschlag (metal surcharge) - händlerspezifisch // Priorität: 1. Datanorm, 2. ZUGFeRD Import Line if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) { $supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge; } elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) { // Kupferzuschlag aus ZUGFeRD-Rechnung $supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge; } // Preiseinheit - händlerspezifisch // Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit; } elseif (!empty($lineObj->basis_quantity) && $lineObj->basis_quantity > 1) { // Preiseinheit aus ZUGFeRD-Rechnung (z.B. 100 für "pro 100 Meter") $supplierPriceExtrafields['options_preiseinheit'] = $lineObj->basis_quantity; } // Warengruppe aus Datanorm if (!empty($datanorm->product_group)) { $supplierPriceExtrafields['options_warengruppe'] = $datanorm->product_group; } // Add supplier price entry with EAN and extrafields $res = $prodfourn->update_buyprice( 1, // Quantity $purchasePrice, // Price $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 (kupferzuschlag, preiseinheit) ); // 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); if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) { // Insert extrafields record $kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL'; $preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1; $warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL'; $sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES ("; $sqlInsertExtra .= (int)$priceId.", "; $sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", "; $sqlInsertExtra .= $preiseinheit.", "; $sqlInsertExtra .= $warengruppe.")"; $db->query($sqlInsertExtra); } } // 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); $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(); // Kupferzuschlag - Priorität: 1. Datanorm, 2. ZUGFeRD if (!empty($datanorm->metal_surcharge) && $datanorm->metal_surcharge > 0) { $supplierPriceExtrafields['options_kupferzuschlag'] = $datanorm->metal_surcharge; } elseif (!empty($lineObj->copper_surcharge) && $lineObj->copper_surcharge > 0) { $supplierPriceExtrafields['options_kupferzuschlag'] = $lineObj->copper_surcharge; } // Preiseinheit - Priorität: 1. Datanorm, 2. ZUGFeRD basis_quantity if (!empty($datanorm->price_unit) && $datanorm->price_unit > 1) { $supplierPriceExtrafields['options_preiseinheit'] = $datanorm->price_unit; } 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; } $prodfourn->update_buyprice( 1, // Quantity $purchasePrice, // Price $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); if ($resCheckExtra && $db->num_rows($resCheckExtra) == 0) { // Insert extrafields record $kupferzuschlag = !empty($supplierPriceExtrafields['options_kupferzuschlag']) ? (float)$supplierPriceExtrafields['options_kupferzuschlag'] : 'NULL'; $preiseinheit = !empty($supplierPriceExtrafields['options_preiseinheit']) ? (int)$supplierPriceExtrafields['options_preiseinheit'] : 1; $warengruppe = !empty($supplierPriceExtrafields['options_warengruppe']) ? "'".$db->escape($supplierPriceExtrafields['options_warengruppe'])."'" : 'NULL'; $sqlInsertExtra = "INSERT INTO ".MAIN_DB_PREFIX."product_fournisseur_price_extrafields"; $sqlInsertExtra .= " (fk_object, kupferzuschlag, preiseinheit, warengruppe) VALUES ("; $sqlInsertExtra .= (int)$priceId.", "; $sqlInsertExtra .= ($kupferzuschlag === 'NULL' ? "NULL" : $kupferzuschlag).", "; $sqlInsertExtra .= $preiseinheit.", "; $sqlInsertExtra .= $warengruppe.")"; $db->query($sqlInsertExtra); } } // 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); // 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); } } $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 $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 $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'; } } // 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' => $purchasePrice, 'selling_price' => $sellingPrice, 'copper_surcharge' => $copperSurchargeForPrice, 'existing_product_id' => $existingProductId, 'action' => $productAction, 'new_ref' => 'NEW-'.$supplierPrefix.'-'.$datanorm->article_number ); } } } $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.')'; // 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 $source_pdf = $conf->importzugferd->dir_output.'/imports/'.$import->id.'/'.$import->pdf_filename; if (file_exists($source_pdf)) { $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); } copy($source_pdf, $dest_dir.'/'.$import->pdf_filename); } // 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) { $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 '
'; print ''; print ''; print '
'; print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'.$langs->trans('UploadZugferdInvoice').'
'.$langs->trans('File').' (PDF)'; print ''; print '
'.$langs->trans('ForceReimport').''; print ' '; print ''.$langs->trans('ForceReimportHelp').''; print '
'; print '
'; print '
'; print ''; print '
'; print '
'; // Show pending imports print '
'; print '
'; print ''; print ''; print ''; print ''; $sql = "SELECT i.rowid, i.ref, i.invoice_number, i.seller_name, i.total_ttc, i.status, i.date_creation"; $sql .= " FROM ".MAIN_DB_PREFIX."importzugferd_import as i"; $sql .= " WHERE i.entity = ".$conf->entity; $sql .= " AND i.status IN (".ZugferdImport::STATUS_IMPORTED.", ".ZugferdImport::STATUS_PENDING.")"; $sql .= " ORDER BY i.date_creation DESC LIMIT 10"; $resql = $db->query($sql); if ($resql) { $num = $db->num_rows($resql); if ($num > 0) { print ''; print ''; print ''; print ''; print ''; print ''; print ''; while ($obj = $db->fetch_object($resql)) { print ''; print ''; print ''; print ''; print ''; print ''; print ''; } } else { print ''; } } print '
'.$langs->trans('PendingImports').'
'.$langs->trans('Ref').''.$langs->trans('InvoiceNumber').''.$langs->trans('Supplier').''.$langs->trans('TotalTTC').''.$langs->trans('Status').'
'.$obj->ref.''.$obj->invoice_number.''.$obj->seller_name.''.price($obj->total_ttc).''; $tmpimport = new ZugferdImport($db); print $tmpimport->LibStatut($obj->status, 1); print '
'.$langs->trans('NoPendingImports').'
'; print '
'; print '
'; print '
'; 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 '
'; // Status banner if ($import->status == ZugferdImport::STATUS_PENDING) { print '
'; print ''; print $langs->trans('ManualInterventionRequired'); if ($missingProducts > 0) { print ' - '.$missingProducts.' '.$langs->trans('ProductsNotAssigned'); } if ($import->fk_soc <= 0) { print ' - '.$langs->trans('SupplierNotAssigned'); } print '

'; } elseif ($allComplete) { print '
'; print ''; print $langs->trans('ReadyToCreateInvoice'); print '
'; } // Invoice data print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'.$langs->trans('InvoiceData').' - '.$import->ref.'
'.$langs->trans('InvoiceNumber').''.dol_escape_htmltag($import->invoice_number).''.$langs->trans('InvoiceDate').''.dol_print_date($import->invoice_date, 'day').'
'.$langs->trans('Supplier').''.dol_escape_htmltag($import->seller_name).''.$langs->trans('VATIntra').''.dol_escape_htmltag($import->seller_vat).'
'.$langs->trans('BuyerReference').''.dol_escape_htmltag($import->buyer_reference).''.$langs->trans('TotalHT').''.price($import->total_ht).' '.$import->currency.'
'.$langs->trans('Status').''.$import->getLibStatut(1).''.$langs->trans('TotalTTC').''.price($import->total_ttc).' '.$import->currency.'
'; print '
'; // Supplier selection print '
'; print '
'; print ''; print ''; print ''; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print '
'.$langs->trans('SupplierAssignment').'
'.$langs->trans('SelectSupplier').' *'; print $form->select_company($import->fk_soc, 'supplier_id', 's.fournisseur = 1', 'SelectThirdParty', 0, 0, null, 0, 'minwidth300'); print ' '; print '
'; print '
'; print '
'; // Line items print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; // Initialize totals for summary row $totalDolibarrHT = 0; $totalZugferdHT = 0; $hasDolibarrPrices = false; $allProductsMatched = true; $matchedLinesCount = 0; $totalLinesCount = count($lines); foreach ($lines as $line) { $hasProduct = ($line->fk_product > 0); $rowStyle = $hasProduct ? 'background-color: #dff0d8;' : ''; // Green for matched products print ''; print ''; print ''; print ''; print ''; print ''; // Dolibarr price column - show supplier price and difference print ''; print ''; print ''; print ''; print ''; // Accumulate ZUGFeRD total $totalZugferdHT += $line->line_total; } // Summary row with total comparison // Only show full comparison if ALL products are matched with Dolibarr prices print ''; print ''; 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 ''; print ''; } else { // Not all products matched - show totals but no comparison print ''; print ''; print ''; } print ''; print '
'.$langs->trans('Position').''.$langs->trans('SupplierRef').''.$langs->trans('ProductDescription').''.$langs->trans('Qty').''.$langs->trans('UnitPrice').''.$langs->trans('DolibarrPrice').''.$langs->trans('TotalHT').''.$langs->trans('MatchedProduct').''.$langs->trans('Action').'
'.$line->line_id.''.dol_escape_htmltag($line->supplier_ref).''; print dol_escape_htmltag($line->product_name); if (!empty($line->ean) && !$hasProduct) { print '
EAN: '.$line->ean.''; } print '
'.price2num($line->quantity, 'MS').' '.zugferdGetUnitLabel($line->unit_code).''; 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 '
'; $lineDolibarrTotal = 0; if ($hasProduct && $import->fk_soc > 0) { require_once DOL_DOCUMENT_ROOT.'/fourn/class/fournisseur.product.class.php'; $productFourn = new ProductFournisseur($db); $result = $productFourn->find_min_price_product_fournisseur($line->fk_product, 1, $import->fk_soc); if ($result > 0 && $productFourn->fourn_price > 0) { $dolibarrPrice = $productFourn->fourn_price; $zugferdPrice = $line->unit_price; $priceDiff = $zugferdPrice - $dolibarrPrice; $priceDiffPercent = ($dolibarrPrice > 0) ? (($priceDiff / $dolibarrPrice) * 100) : 0; // Accumulate for summary $lineDolibarrTotal = $dolibarrPrice * $line->quantity; $totalDolibarrHT += $lineDolibarrTotal; $hasDolibarrPrices = true; $matchedLinesCount++; print price($dolibarrPrice); 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 '
'.price($line->line_total).''; 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 '
'.$line->ean.''; } print ' '; } else { print ''.$langs->trans('NoProductMatch').''; } print '
'; if ($hasProduct) { // Remove assignment button print ''; print ''; print ''; } else { // Product selection form print '
'; print ''; print ''; print ''; print ''; print $form->select_produits('', 'product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth150 maxwidth200', 1, '', 0); print ' '; 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 ' '.$langs->trans('CreateProduct'); print ''; // Product template print '
'; print '
'; print ''; print ''; print ''; print ''; print $form->select_produits('', 'template_product_id', '', 0, 0, -1, 2, '', 0, array(), 0, '1', 0, 'minwidth100 maxwidth150', 1, '', 0); 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 ''; print ''; print ''; print '
Rechnung:'.dol_trunc($line->product_name, 50).'
Datanorm:'.dol_trunc($datanormArticle['short_text1'], 50).''; print ' ('.price($datanormArticle['price']).')
'; print '
'; } } } print '
'.$langs->trans('Total').' '.$langs->trans('TotalHT').''; 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 '
'.price($totalZugferdHT).''; if ($isMatch) { print ' '.$langs->trans('SumValidationOk').''; } else { print ' '.$langs->trans('Difference').': '.price($totalDiff).' '.$import->currency.''; } print ''; if ($hasDolibarrPrices) { print ''.price($totalDolibarrHT).''; print '
('.$matchedLinesCount.'/'.$totalLinesCount.')'; } else { print '-'; } print '
'.price($totalZugferdHT).''; print ' '.$langs->trans('ProductsNotAssigned').''; print '
'; print '
'; // Datanorm Preview Section (shown when preview action was triggered) if (!empty($datanormPreviewMatches)) { print '
'; print '
'; print ''.$langs->trans('DatanormPreview'); print ' '.count($datanormPreviewMatches).' '.$langs->trans('Matches').''; print '
'; print '
'; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; print ''; $countCreate = 0; $countAssign = 0; foreach ($datanormPreviewMatches as $match) { $rowClass = ($match['action'] == 'assign') ? 'background-color: #d9edf7;' : 'background-color: #dff0d8;'; print ''; print ''; print ''; // Invoice product name print ''; // Datanorm product name print ''; // Invoice price (from ZUGFeRD) print ''; // Datanorm price - show original price and calculated unit price print ''; // Selling price print ''; // Action print ''; print ''; } print '
'.$langs->trans('SupplierRef').''.$langs->trans('InvoiceProductName').''.$langs->trans('DatanormProductName').''.$langs->trans('InvoicePrice').''.$langs->trans('DatanormPrice').''.$langs->trans('SellingPrice').''.$langs->trans('Action').'
'.$match['datanorm_article_number'].''; if (!empty($match['datanorm_ean'])) { print '
EAN: '.$match['datanorm_ean'].''; } print '
'; print ''.dol_trunc($match['line_product_name'], 40).''; print ''; print ''.dol_trunc($match['datanorm_short_text1'], 40).''; if (!empty($match['datanorm_short_text2'])) { print '
'.dol_trunc($match['datanorm_short_text2'], 40).''; } print '
'; print ''.price($match['line_unit_price']).''; print ''; if ($match['datanorm_price_unit'] > 1) { // Show original price and price unit print ''.price($match['datanorm_price']).'/'.$match['datanorm_price_unit'].''; print '
= '.price($match['purchase_price']).''; } else { print ''.price($match['purchase_price']).''; } if ($match['copper_surcharge'] > 0) { print '
+ '.price($match['copper_surcharge']).' Cu'; } print '
'.price($match['selling_price']).''; if ($match['action'] == 'assign') { print ' '.$langs->trans('Assign').''; $countAssign++; } else { print ' '.$langs->trans('Create').''; print '
'.$match['new_ref'].''; $countCreate++; } print '
'; // Summary and confirm button print '
'; print '
'; if ($countCreate > 0) { print ' '.$countCreate.' '.$langs->trans('ToCreate').''; } if ($countAssign > 0) { print ' '.$countAssign.' '.$langs->trans('ToAssign').''; } print '
'; print ''; print '   '; print ''; print ''.$langs->trans('Cancel'); print ''; print '
'; print '
'; print '
'; // JavaScript for select all checkbox print ''; } // Action buttons print '
'; if ($allComplete) { print ''; print ''.$langs->trans('CreateSupplierInvoice'); print ''; print '   '; } // Finish import button - shown when pending status and all products assigned if ($import->status == ZugferdImport::STATUS_PENDING && $allComplete) { print ''; print ''.$langs->trans('FinishImport'); print ''; print '   '; } // Datanorm buttons - show when products are missing and supplier is set if ($missingProducts > 0 && $import->fk_soc > 0 && empty($datanormPreviewMatches)) { // "Alle zuordnen" - creates all products from Datanorm print ''; print ''.$langs->trans('AssignAllFromDatanorm'); print ''; print '   '; // "Datanorm Vorschau" - preview what will be created print ''; print ''.$langs->trans('PreviewDatanormMatches'); print ''; print '   '; } print ''.$langs->trans('BackToList').''; // Delete button - show for pending imports or imports without linked invoice $canDelete = ($import->status == ZugferdImport::STATUS_PENDING) || ($import->status == ZugferdImport::STATUS_IMPORTED && $import->fk_facture_fourn <= 0); if ($canDelete) { print '   '; print ''; print ''.$langs->trans('Delete'); print ''; } print '
'; print '
'; // Modal CSS and HTML for raw Datanorm data print ''; // Modal for raw data print '
'; print '
'; print '×'; print '

Rohdaten:

'; print '
'; print '

Laden...

'; print '
'; print '
'; print '
'; print ''; } llxFooter(); $db->close();