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 */ // 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; // 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; } // Selling price with markup $sellingPrice = $purchasePrice * (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 } // Add supplier price entry with EAN $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(), // Extra fields 0, // Charges array $supplierEan, // Barcode/EAN in supplier price $supplierEanType // Barcode type (EAN13) ); // 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); } // 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; } // 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(); $sellingPrice = $purchasePrice * (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; } $prodfourn->update_buyprice( 1, $purchasePrice, $user, 'HT', $supplier, 0, $datanorm->article_number, $lineObj->tax_percent ?: 19, 0, 0, 0, 0, 0, 0, array(), 0, $supplierEan, $supplierEanType ); // 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); } // 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 { // 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.')'; $invoice->cond_reglement_id = 1; $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 '
'; } /* * 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: '.$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);
$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 ' | ';
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 ' '.$line->ean.''; } print ' '; } else { print ''.$langs->trans('NoProductMatch').''; } print ' | ';
print '';
if ($hasProduct) {
// Remove assignment button
print '';
print '';
print '';
} 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 ' '.$langs->trans('CreateProduct'); 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 ''; print ' '; print dol_trunc($datanormArticle['short_text1'], 40); print ' - '.price($datanormArticle['price']); 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 '||||||